219 lines
5.7 KiB
Go
219 lines
5.7 KiB
Go
package aescbc
|
|
|
|
import (
|
|
"crypto/cipher"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"crypto/sha512"
|
|
"crypto/subtle"
|
|
"encoding/binary"
|
|
"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
|
|
}
|
|
|
|
func unpad(buf []byte, n int) ([]byte, error) {
|
|
lbuf := len(buf)
|
|
rem := lbuf % n
|
|
|
|
// First, `buf` must be a multiple of `n`
|
|
if rem != 0 {
|
|
return nil, fmt.Errorf("input buffer must be multiple of block size %d", n)
|
|
}
|
|
|
|
// Find the last byte, which is the encoded padding
|
|
// i.e. 0x1 == 1 byte worth of padding
|
|
last := buf[lbuf-1]
|
|
|
|
// This is the number of padding bytes that we expect
|
|
expected := int(last)
|
|
|
|
if expected == 0 || /* we _have_ to have padding here. therefore, 0x0 is not an option */
|
|
expected > n || /* we also must make sure that we don't go over the block size (n) */
|
|
expected > lbuf /* finally, it can't be more than the buffer itself. unlikely, but could happen */ {
|
|
return nil, fmt.Errorf(`invalid padding byte at the end of buffer`)
|
|
}
|
|
|
|
// start i = 1 because we have already established that expected == int(last) where
|
|
// last = buf[lbuf-1].
|
|
//
|
|
// we also don't check against lbuf-i in range, because we have established expected <= lbuf
|
|
for i := 1; i < expected; i++ {
|
|
if buf[lbuf-i] != last {
|
|
return nil, fmt.Errorf(`invalid padding`)
|
|
}
|
|
}
|
|
|
|
return buf[:lbuf-expected], nil
|
|
}
|
|
|
|
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) {
|
|
buf := make([]byte, len(aad)+len(nonce)+len(ciphertext)+8)
|
|
n := 0
|
|
n += copy(buf, aad)
|
|
n += copy(buf[n:], nonce)
|
|
n += copy(buf[n:], ciphertext)
|
|
binary.BigEndian.PutUint64(buf[n:], uint64(len(aad)*8))
|
|
|
|
h := hmac.New(c.hash, c.integrityKey)
|
|
if _, err := h.Write(buf); err != nil {
|
|
return nil, fmt.Errorf(`failed to write ComputeAuthTag using Hmac: %w`, err)
|
|
}
|
|
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)
|
|
}
|
|
|
|
if subtle.ConstantTimeCompare(expectedTag, tag) != 1 {
|
|
return nil, fmt.Errorf(`invalid ciphertext (tag mismatch)`)
|
|
}
|
|
|
|
cbc := cipher.NewCBCDecrypter(c.blockCipher, nonce)
|
|
buf := make([]byte, tagOffset)
|
|
cbc.CryptBlocks(buf, ciphertext)
|
|
|
|
plaintext, err := unpad(buf, c.blockCipher.BlockSize())
|
|
if err != nil {
|
|
return nil, fmt.Errorf(`failed to generate plaintext from decrypted blocks: %w`, err)
|
|
}
|
|
ret := ensureSize(dst, len(plaintext))
|
|
out := ret[len(dst):]
|
|
copy(out, plaintext)
|
|
return ret, nil
|
|
}
|