239 lines
6.3 KiB
Go
239 lines
6.3 KiB
Go
package aescbc
|
|
|
|
import (
|
|
"crypto/cipher"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"crypto/sha512"
|
|
"crypto/subtle"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"hash"
|
|
)
|
|
|
|
const (
|
|
NonceSize = 16
|
|
)
|
|
|
|
func pad(buf []byte, n int) []byte {
|
|
rem := n - len(buf)%n
|
|
if rem == 0 {
|
|
return buf
|
|
}
|
|
|
|
newbuf := make([]byte, len(buf)+rem)
|
|
copy(newbuf, buf)
|
|
|
|
for i := len(buf); i < len(newbuf); i++ {
|
|
newbuf[i] = byte(rem)
|
|
}
|
|
return newbuf
|
|
}
|
|
|
|
// ref. https://github.com/golang/go/blob/c3db64c0f45e8f2d75c5b59401e0fc925701b6f4/src/crypto/tls/conn.go#L279-L324
|
|
//
|
|
// extractPadding returns, in constant time, the length of the padding to remove
|
|
// from the end of payload. It also returns a byte which is equal to 255 if the
|
|
// padding was valid and 0 otherwise. See RFC 2246, Section 6.2.3.2.
|
|
func extractPadding(payload []byte) (toRemove int, good byte) {
|
|
if len(payload) < 1 {
|
|
return 0, 0
|
|
}
|
|
|
|
paddingLen := payload[len(payload)-1]
|
|
t := uint(len(payload)) - uint(paddingLen)
|
|
// if len(payload) > paddingLen then the MSB of t is zero
|
|
good = byte(int32(^t) >> 31)
|
|
|
|
// The maximum possible padding length plus the actual length field
|
|
toCheck := 256
|
|
// The length of the padded data is public, so we can use an if here
|
|
if toCheck > len(payload) {
|
|
toCheck = len(payload)
|
|
}
|
|
|
|
for i := 1; i <= toCheck; i++ {
|
|
t := uint(paddingLen) - uint(i)
|
|
// if i <= paddingLen then the MSB of t is zero
|
|
mask := byte(int32(^t) >> 31)
|
|
b := payload[len(payload)-i]
|
|
good &^= mask&paddingLen ^ mask&b
|
|
}
|
|
|
|
// We AND together the bits of good and replicate the result across
|
|
// all the bits.
|
|
good &= good << 4
|
|
good &= good << 2
|
|
good &= good << 1
|
|
good = uint8(int8(good) >> 7)
|
|
|
|
// Zero the padding length on error. This ensures any unchecked bytes
|
|
// are included in the MAC. Otherwise, an attacker that could
|
|
// distinguish MAC failures from padding failures could mount an attack
|
|
// similar to POODLE in SSL 3.0: given a good ciphertext that uses a
|
|
// full block's worth of padding, replace the final block with another
|
|
// block. If the MAC check passed but the padding check failed, the
|
|
// last byte of that block decrypted to the block size.
|
|
//
|
|
// See also macAndPaddingGood logic below.
|
|
paddingLen &= good
|
|
|
|
toRemove = int(paddingLen)
|
|
return
|
|
}
|
|
|
|
type Hmac struct {
|
|
blockCipher cipher.Block
|
|
hash func() hash.Hash
|
|
keysize int
|
|
tagsize int
|
|
integrityKey []byte
|
|
}
|
|
|
|
type BlockCipherFunc func([]byte) (cipher.Block, error)
|
|
|
|
func New(key []byte, f BlockCipherFunc) (hmac *Hmac, err error) {
|
|
keysize := len(key) / 2
|
|
ikey := key[:keysize]
|
|
ekey := key[keysize:]
|
|
|
|
bc, ciphererr := f(ekey)
|
|
if ciphererr != nil {
|
|
err = fmt.Errorf(`failed to execute block cipher function: %w`, ciphererr)
|
|
return
|
|
}
|
|
|
|
var hfunc func() hash.Hash
|
|
switch keysize {
|
|
case 16:
|
|
hfunc = sha256.New
|
|
case 24:
|
|
hfunc = sha512.New384
|
|
case 32:
|
|
hfunc = sha512.New
|
|
default:
|
|
return nil, fmt.Errorf("unsupported key size %d", keysize)
|
|
}
|
|
|
|
return &Hmac{
|
|
blockCipher: bc,
|
|
hash: hfunc,
|
|
integrityKey: ikey,
|
|
keysize: keysize,
|
|
tagsize: keysize, // NonceSize,
|
|
// While investigating GH #207, I stumbled upon another problem where
|
|
// the computed tags don't match on decrypt. After poking through the
|
|
// code using a bunch of debug statements, I've finally found out that
|
|
// tagsize = keysize makes the whole thing work.
|
|
}, nil
|
|
}
|
|
|
|
// NonceSize fulfills the crypto.AEAD interface
|
|
func (c Hmac) NonceSize() int {
|
|
return NonceSize
|
|
}
|
|
|
|
// Overhead fulfills the crypto.AEAD interface
|
|
func (c Hmac) Overhead() int {
|
|
return c.blockCipher.BlockSize() + c.tagsize
|
|
}
|
|
|
|
func (c Hmac) ComputeAuthTag(aad, nonce, ciphertext []byte) ([]byte, error) {
|
|
var buf [8]byte
|
|
binary.BigEndian.PutUint64(buf[:], uint64(len(aad)*8))
|
|
|
|
h := hmac.New(c.hash, c.integrityKey)
|
|
|
|
// compute the tag
|
|
// no need to check errors because Write never returns an error: https://pkg.go.dev/hash#Hash
|
|
//
|
|
// > Write (via the embedded io.Writer interface) adds more data to the running hash.
|
|
// > It never returns an error.
|
|
h.Write(aad)
|
|
h.Write(nonce)
|
|
h.Write(ciphertext)
|
|
h.Write(buf[:])
|
|
s := h.Sum(nil)
|
|
return s[:c.tagsize], nil
|
|
}
|
|
|
|
func ensureSize(dst []byte, n int) []byte {
|
|
// if the dst buffer has enough length just copy the relevant parts to it.
|
|
// Otherwise create a new slice that's big enough, and operate on that
|
|
// Note: I think go-jose has a bug in that it checks for cap(), but not len().
|
|
ret := dst
|
|
if diff := n - len(dst); diff > 0 {
|
|
// dst is not big enough
|
|
ret = make([]byte, n)
|
|
copy(ret, dst)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// Seal fulfills the crypto.AEAD interface
|
|
func (c Hmac) Seal(dst, nonce, plaintext, data []byte) []byte {
|
|
ctlen := len(plaintext)
|
|
ciphertext := make([]byte, ctlen+c.Overhead())[:ctlen]
|
|
copy(ciphertext, plaintext)
|
|
ciphertext = pad(ciphertext, c.blockCipher.BlockSize())
|
|
|
|
cbc := cipher.NewCBCEncrypter(c.blockCipher, nonce)
|
|
cbc.CryptBlocks(ciphertext, ciphertext)
|
|
|
|
authtag, err := c.ComputeAuthTag(data, nonce, ciphertext)
|
|
if err != nil {
|
|
// Hmac implements cipher.AEAD interface. Seal can't return error.
|
|
// But currently it never reach here because of Hmac.ComputeAuthTag doesn't return error.
|
|
panic(fmt.Errorf("failed to seal on hmac: %v", err))
|
|
}
|
|
|
|
retlen := len(dst) + len(ciphertext) + len(authtag)
|
|
|
|
ret := ensureSize(dst, retlen)
|
|
out := ret[len(dst):]
|
|
n := copy(out, ciphertext)
|
|
copy(out[n:], authtag)
|
|
|
|
return ret
|
|
}
|
|
|
|
// Open fulfills the crypto.AEAD interface
|
|
func (c Hmac) Open(dst, nonce, ciphertext, data []byte) ([]byte, error) {
|
|
if len(ciphertext) < c.keysize {
|
|
return nil, fmt.Errorf(`invalid ciphertext (too short)`)
|
|
}
|
|
|
|
tagOffset := len(ciphertext) - c.tagsize
|
|
if tagOffset%c.blockCipher.BlockSize() != 0 {
|
|
return nil, fmt.Errorf(
|
|
"invalid ciphertext (invalid length: %d %% %d != 0)",
|
|
tagOffset,
|
|
c.blockCipher.BlockSize(),
|
|
)
|
|
}
|
|
tag := ciphertext[tagOffset:]
|
|
ciphertext = ciphertext[:tagOffset]
|
|
|
|
expectedTag, err := c.ComputeAuthTag(data, nonce, ciphertext[:tagOffset])
|
|
if err != nil {
|
|
return nil, fmt.Errorf(`failed to compute auth tag: %w`, err)
|
|
}
|
|
|
|
cbc := cipher.NewCBCDecrypter(c.blockCipher, nonce)
|
|
buf := make([]byte, tagOffset)
|
|
cbc.CryptBlocks(buf, ciphertext)
|
|
|
|
toRemove, good := extractPadding(buf)
|
|
cmp := subtle.ConstantTimeCompare(expectedTag, tag) & int(good)
|
|
if cmp != 1 {
|
|
return nil, errors.New(`invalid ciphertext`)
|
|
}
|
|
|
|
plaintext := buf[:len(buf)-toRemove]
|
|
ret := ensureSize(dst, len(plaintext))
|
|
out := ret[len(dst):]
|
|
copy(out, plaintext)
|
|
return ret, nil
|
|
}
|