199 lines
4.9 KiB
Go
199 lines
4.9 KiB
Go
package memberlist
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"fmt"
|
|
"io"
|
|
)
|
|
|
|
/*
|
|
|
|
Encrypted messages are prefixed with an encryptionVersion byte
|
|
that is used for us to be able to properly encode/decode. We
|
|
currently support the following versions:
|
|
|
|
0 - AES-GCM 128, using PKCS7 padding
|
|
1 - AES-GCM 128, no padding. Padding not needed, caused bloat.
|
|
|
|
*/
|
|
type encryptionVersion uint8
|
|
|
|
const (
|
|
minEncryptionVersion encryptionVersion = 0
|
|
maxEncryptionVersion encryptionVersion = 1
|
|
)
|
|
|
|
const (
|
|
versionSize = 1
|
|
nonceSize = 12
|
|
tagSize = 16
|
|
maxPadOverhead = 16
|
|
blockSize = aes.BlockSize
|
|
)
|
|
|
|
// pkcs7encode is used to pad a byte buffer to a specific block size using
|
|
// the PKCS7 algorithm. "Ignores" some bytes to compensate for IV
|
|
func pkcs7encode(buf *bytes.Buffer, ignore, blockSize int) {
|
|
n := buf.Len() - ignore
|
|
more := blockSize - (n % blockSize)
|
|
for i := 0; i < more; i++ {
|
|
buf.WriteByte(byte(more))
|
|
}
|
|
}
|
|
|
|
// pkcs7decode is used to decode a buffer that has been padded
|
|
func pkcs7decode(buf []byte, blockSize int) []byte {
|
|
if len(buf) == 0 {
|
|
panic("Cannot decode a PKCS7 buffer of zero length")
|
|
}
|
|
n := len(buf)
|
|
last := buf[n-1]
|
|
n -= int(last)
|
|
return buf[:n]
|
|
}
|
|
|
|
// encryptOverhead returns the maximum possible overhead of encryption by version
|
|
func encryptOverhead(vsn encryptionVersion) int {
|
|
switch vsn {
|
|
case 0:
|
|
return 45 // Version: 1, IV: 12, Padding: 16, Tag: 16
|
|
case 1:
|
|
return 29 // Version: 1, IV: 12, Tag: 16
|
|
default:
|
|
panic("unsupported version")
|
|
}
|
|
}
|
|
|
|
// encryptedLength is used to compute the buffer size needed
|
|
// for a message of given length
|
|
func encryptedLength(vsn encryptionVersion, inp int) int {
|
|
// If we are on version 1, there is no padding
|
|
if vsn >= 1 {
|
|
return versionSize + nonceSize + inp + tagSize
|
|
}
|
|
|
|
// Determine the padding size
|
|
padding := blockSize - (inp % blockSize)
|
|
|
|
// Sum the extra parts to get total size
|
|
return versionSize + nonceSize + inp + padding + tagSize
|
|
}
|
|
|
|
// encryptPayload is used to encrypt a message with a given key.
|
|
// We make use of AES-128 in GCM mode. New byte buffer is the version,
|
|
// nonce, ciphertext and tag
|
|
func encryptPayload(vsn encryptionVersion, key []byte, msg []byte, data []byte, dst *bytes.Buffer) error {
|
|
// Get the AES block cipher
|
|
aesBlock, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get the GCM cipher mode
|
|
gcm, err := cipher.NewGCM(aesBlock)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Grow the buffer to make room for everything
|
|
offset := dst.Len()
|
|
dst.Grow(encryptedLength(vsn, len(msg)))
|
|
|
|
// Write the encryption version
|
|
dst.WriteByte(byte(vsn))
|
|
|
|
// Add a random nonce
|
|
io.CopyN(dst, rand.Reader, nonceSize)
|
|
afterNonce := dst.Len()
|
|
|
|
// Ensure we are correctly padded (only version 0)
|
|
if vsn == 0 {
|
|
io.Copy(dst, bytes.NewReader(msg))
|
|
pkcs7encode(dst, offset+versionSize+nonceSize, aes.BlockSize)
|
|
}
|
|
|
|
// Encrypt message using GCM
|
|
slice := dst.Bytes()[offset:]
|
|
nonce := slice[versionSize : versionSize+nonceSize]
|
|
|
|
// Message source depends on the encryption version.
|
|
// Version 0 uses padding, version 1 does not
|
|
var src []byte
|
|
if vsn == 0 {
|
|
src = slice[versionSize+nonceSize:]
|
|
} else {
|
|
src = msg
|
|
}
|
|
out := gcm.Seal(nil, nonce, src, data)
|
|
|
|
// Truncate the plaintext, and write the cipher text
|
|
dst.Truncate(afterNonce)
|
|
dst.Write(out)
|
|
return nil
|
|
}
|
|
|
|
// decryptMessage performs the actual decryption of ciphertext. This is in its
|
|
// own function to allow it to be called on all keys easily.
|
|
func decryptMessage(key, msg []byte, data []byte) ([]byte, error) {
|
|
// Get the AES block cipher
|
|
aesBlock, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get the GCM cipher mode
|
|
gcm, err := cipher.NewGCM(aesBlock)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Decrypt the message
|
|
nonce := msg[versionSize : versionSize+nonceSize]
|
|
ciphertext := msg[versionSize+nonceSize:]
|
|
plain, err := gcm.Open(nil, nonce, ciphertext, data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Success!
|
|
return plain, nil
|
|
}
|
|
|
|
// decryptPayload is used to decrypt a message with a given key,
|
|
// and verify it's contents. Any padding will be removed, and a
|
|
// slice to the plaintext is returned. Decryption is done IN PLACE!
|
|
func decryptPayload(keys [][]byte, msg []byte, data []byte) ([]byte, error) {
|
|
// Ensure we have at least one byte
|
|
if len(msg) == 0 {
|
|
return nil, fmt.Errorf("Cannot decrypt empty payload")
|
|
}
|
|
|
|
// Verify the version
|
|
vsn := encryptionVersion(msg[0])
|
|
if vsn > maxEncryptionVersion {
|
|
return nil, fmt.Errorf("Unsupported encryption version %d", msg[0])
|
|
}
|
|
|
|
// Ensure the length is sane
|
|
if len(msg) < encryptedLength(vsn, 0) {
|
|
return nil, fmt.Errorf("Payload is too small to decrypt: %d", len(msg))
|
|
}
|
|
|
|
for _, key := range keys {
|
|
plain, err := decryptMessage(key, msg, data)
|
|
if err == nil {
|
|
// Remove the PKCS7 padding for vsn 0
|
|
if vsn == 0 {
|
|
return pkcs7decode(plain, aes.BlockSize), nil
|
|
} else {
|
|
return plain, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("No installed keys could decrypt the message")
|
|
}
|