Skip to content

Commit

Permalink
implement AES GCM mode
Browse files Browse the repository at this point in the history
  • Loading branch information
qmuntal committed Mar 18, 2022
1 parent cc8dbe8 commit 938ad52
Show file tree
Hide file tree
Showing 4 changed files with 328 additions and 11 deletions.
174 changes: 170 additions & 4 deletions cng/aes.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func (c *aesCipher) Encrypt(dst, src []byte) {
panic("crypto/aes: output not full block")
}
var ret uint32
err := bcrypt.Encrypt(c.kh, &src[0], uint32(len(src)), 0, nil, 0, &dst[0], uint32(len(dst)), &ret, 0)
err := bcrypt.Encrypt(c.kh, &src[0], uint32(len(src)), nil, nil, 0, &dst[0], uint32(len(dst)), &ret, 0)
if err != nil {
panic(err)
}
Expand All @@ -127,7 +127,7 @@ func (c *aesCipher) Decrypt(dst, src []byte) {
}

var ret uint32
err := bcrypt.Decrypt(c.kh, &src[0], uint32(len(src)), 0, nil, 0, &dst[0], uint32(len(dst)), &ret, 0)
err := bcrypt.Decrypt(c.kh, &src[0], uint32(len(src)), nil, nil, 0, &dst[0], uint32(len(dst)), &ret, 0)
if err != nil {
panic(err)
}
Expand All @@ -145,6 +145,28 @@ func (c *aesCipher) NewCBCDecrypter(iv []byte) cipher.BlockMode {
return newCBC(false, c.key, iv)
}

type noGCM struct {
cipher.Block
}

func (c *aesCipher) NewGCM(nonceSize, tagSize int) (cipher.AEAD, error) {
if nonceSize != gcmStandardNonceSize && tagSize != gcmTagSize {
return nil, errors.New("crypto/aes: GCM tag and nonce sizes can't be non-standard at the same time")
}
// Fall back to standard library for GCM with non-standard nonce or tag size.
if nonceSize != gcmStandardNonceSize {
return cipher.NewGCMWithNonceSize(&noGCM{c}, nonceSize)
}
if tagSize != gcmTagSize {
return cipher.NewGCMWithTagSize(&noGCM{c}, tagSize)
}
return newGCM(c.key, false)
}

func (c *aesCipher) NewGCMTLS() (cipher.AEAD, error) {
return newGCM(c.key, true)
}

type aesCBC struct {
kh bcrypt.KEY_HANDLE
iv [aesBlockSize]byte
Expand Down Expand Up @@ -188,9 +210,9 @@ func (x *aesCBC) CryptBlocks(dst, src []byte) {
var ret uint32
var err error
if x.encrypt {
err = bcrypt.Encrypt(x.kh, &src[0], uint32(len(src)), 0, &x.iv[0], uint32(len(x.iv)), &dst[0], uint32(len(dst)), &ret, 0)
err = bcrypt.Encrypt(x.kh, &src[0], uint32(len(src)), nil, &x.iv[0], uint32(len(x.iv)), &dst[0], uint32(len(dst)), &ret, 0)
} else {
err = bcrypt.Decrypt(x.kh, &src[0], uint32(len(src)), 0, &x.iv[0], uint32(len(x.iv)), &dst[0], uint32(len(dst)), &ret, 0)
err = bcrypt.Decrypt(x.kh, &src[0], uint32(len(src)), nil, &x.iv[0], uint32(len(x.iv)), &dst[0], uint32(len(dst)), &ret, 0)
}
if err != nil {
panic(err)
Expand All @@ -207,3 +229,147 @@ func (x *aesCBC) SetIV(iv []byte) {
}
copy(x.iv[:], iv)
}

const (
gcmTagSize = 16
gcmStandardNonceSize = 12
gcmTlsAddSize = 13
gcmTlsFixedNonceSize = 4
)

type aesGCM struct {
kh bcrypt.KEY_HANDLE
tls bool
minNextNonce uint64
}

func (g *aesGCM) finalize() {
bcrypt.DestroyKey(g.kh)
}

func newGCM(key []byte, tls bool) (*aesGCM, error) {
h, err := loadAes(bcrypt.AES_ALGORITHM, bcrypt.CHAIN_MODE_GCM)
if err != nil {
return nil, err
}
g := &aesGCM{tls: tls}
err = bcrypt.GenerateSymmetricKey(h.h, &g.kh, nil, 0, &key[0], uint32(len(key)), 0)
if err != nil {
return nil, err
}
runtime.SetFinalizer(g, (*aesGCM).finalize)
return g, nil
}

func (g *aesGCM) NonceSize() int {
return gcmStandardNonceSize
}

func (g *aesGCM) Overhead() int {
return gcmTagSize
}

func (g *aesGCM) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
if len(nonce) != gcmStandardNonceSize {
panic("cipher: incorrect nonce length given to GCM")
}
if uint64(len(plaintext)) > ((1<<32)-2)*aesBlockSize || len(plaintext)+gcmTagSize < len(plaintext) {
panic("cipher: message too large for GCM")
}
if len(dst)+len(plaintext)+gcmTagSize < len(dst) {
panic("cipher: message too large for buffer")
}
if g.tls {
if len(additionalData) != gcmTlsAddSize {
panic("cipher: incorrect additional data length given to GCM TLS")
}
// BoringCrypto enforces strictly monotonically increasing explicit nonces
// and to fail after 2^64 - 1 keys as per FIPS 140-2 IG A.5,
// but BCrypt does not perform this check, so it is implemented here.
const maxUint64 = 1<<64 - 1
counter := bigUint64(nonce[gcmTlsFixedNonceSize:])
if counter == maxUint64 {
panic("cipher: nonce counter must be less than 2^64 - 1")
}
if counter < g.minNextNonce {
panic("cipher: nonce counter must be strictly monotonically increasing")
}
defer func() {
g.minNextNonce = counter + 1
}()
}
// Make room in dst to append plaintext+overhead.
ret, out := sliceForAppend(dst, len(plaintext)+gcmTagSize)

// Check delayed until now to make sure len(dst) is accurate.
if subtle.InexactOverlap(out, plaintext) {
panic("cipher: invalid buffer overlap")
}

info := bcrypt.NewAUTHENTICATED_CIPHER_MODE_INFO(nonce, additionalData, out[len(out)-gcmTagSize:])
var encSize uint32
err := bcrypt.Encrypt(g.kh, &plaintext[0], uint32(len(plaintext)), info, nil, 0, &out[0], uint32(len(out)), &encSize, 0)
if err != nil {
panic(err)
}
if int(encSize) != len(plaintext) {
panic("crypto/aes: plaintext not fully encrypted")
}
runtime.KeepAlive(g)
return ret
}

var errOpen = errors.New("cipher: message authentication failed")

func (g *aesGCM) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
if len(nonce) != gcmStandardNonceSize {
panic("cipher: incorrect nonce length given to GCM")
}
if len(ciphertext) < gcmTagSize {
return nil, errOpen
}
if uint64(len(ciphertext)) > ((1<<32)-2)*aesBlockSize+gcmTagSize {
return nil, errOpen
}

tag := ciphertext[len(ciphertext)-gcmTagSize:]
ciphertext = ciphertext[:len(ciphertext)-gcmTagSize]

// Make room in dst to append ciphertext without tag.
ret, out := sliceForAppend(dst, len(ciphertext))

// Check delayed until now to make sure len(dst) is accurate.
if subtle.InexactOverlap(out, ciphertext) {
panic("cipher: invalid buffer overlap")
}

info := bcrypt.NewAUTHENTICATED_CIPHER_MODE_INFO(nonce, additionalData, tag)
var decSize uint32
err := bcrypt.Decrypt(g.kh, &ciphertext[0], uint32(len(ciphertext)), info, nil, 0, &out[0], uint32(len(out)), &decSize, 0)
if err != nil || int(decSize) != len(ciphertext) {
for i := range out {
out[i] = 0
}
return nil, errOpen
}
runtime.KeepAlive(g)
return ret, nil
}

// sliceForAppend is a mirror of crypto/cipher.sliceForAppend.
func sliceForAppend(in []byte, n int) (head, tail []byte) {
if total := len(in) + n; cap(in) >= total {
head = in[:total]
} else {
head = make([]byte, total)
copy(head, in)
}
tail = head[len(in):]
return
}

func bigUint64(b []byte) uint64 {
_ = b[7] // bounds check hint to compiler; see go.dev/issue/14808
return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 |
uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56
}
152 changes: 151 additions & 1 deletion cng/aes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,157 @@ import (
"testing"
)

var key = []byte("D249BF6DEC97B1EBD69BC4D6B3A3C49D")

func TestNewGCMNonce(t *testing.T) {
ci, err := NewAESCipher(key)
if err != nil {
t.Fatal(err)
}
c := ci.(*aesCipher)
_, err = c.NewGCM(gcmStandardNonceSize-1, gcmTagSize-1)
if err == nil {
t.Error("expected error for non-standard tag and nonce size at the same time, got none")
}
_, err = c.NewGCM(gcmStandardNonceSize-1, gcmTagSize)
if err != nil {
t.Errorf("expected no error for non-standard nonce size with standard tag size, got: %#v", err)
}
_, err = c.NewGCM(gcmStandardNonceSize, gcmTagSize-1)
if err != nil {
t.Errorf("expected no error for standard tag size, got: %#v", err)
}
_, err = c.NewGCM(gcmStandardNonceSize, gcmTagSize)
if err != nil {
t.Errorf("expected no error for standard tag / nonce size, got: %#v", err)
}
}

func TestSealAndOpen(t *testing.T) {
ci, err := NewAESCipher(key)
if err != nil {
t.Fatal(err)
}
c := ci.(*aesCipher)
gcm, err := c.NewGCM(gcmStandardNonceSize, gcmTagSize)
if err != nil {
t.Fatal(err)
}
nonce := []byte{0x91, 0xc7, 0xa7, 0x54, 0x52, 0xef, 0x10, 0xdb, 0x91, 0xa8, 0x6c, 0xf9}
plainText := []byte{0x01, 0x02, 0x03}
additionalData := []byte{0x05, 0x05, 0x07}
sealed := gcm.Seal(nil, nonce, plainText, additionalData)
decrypted, err := gcm.Open(nil, nonce, sealed, additionalData)
if err != nil {
t.Error(err)
}
if !bytes.Equal(decrypted, plainText) {
t.Errorf("unexpected decrypted result\ngot: %#v\nexp: %#v", decrypted, plainText)
}
}

func TestSealAndOpenTLS(t *testing.T) {
ci, err := NewAESCipher(key)
if err != nil {
t.Fatal(err)
}
c := ci.(*aesCipher)
gcm, err := c.NewGCMTLS()
if err != nil {
t.Fatal(err)
}
nonce := [12]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
nonce1 := [12]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
nonce9 := [12]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9}
nonce10 := [12]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10}
nonceMax := [12]byte{0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255}
plainText := []byte{0x01, 0x02, 0x03}
additionalData := make([]byte, 13)
additionalData[11] = byte(len(plainText) >> 8)
additionalData[12] = byte(len(plainText))
sealed := gcm.Seal(nil, nonce[:], plainText, additionalData)
assertPanic(t, func() {
gcm.Seal(nil, nonce[:], plainText, additionalData)
})
sealed1 := gcm.Seal(nil, nonce1[:], plainText, additionalData)
gcm.Seal(nil, nonce10[:], plainText, additionalData)
assertPanic(t, func() {
gcm.Seal(nil, nonce9[:], plainText, additionalData)
})
assertPanic(t, func() {
gcm.Seal(nil, nonceMax[:], plainText, additionalData)
})
if bytes.Equal(sealed, sealed1) {
t.Errorf("different nonces should produce different outputs\ngot: %#v\nexp: %#v", sealed, sealed1)
}
decrypted, err := gcm.Open(nil, nonce[:], sealed, additionalData)
if err != nil {
t.Error(err)
}
decrypted1, err := gcm.Open(nil, nonce1[:], sealed1, additionalData)
if err != nil {
t.Error(err)
}
if !bytes.Equal(decrypted, plainText) {
t.Errorf("unexpected decrypted result\ngot: %#v\nexp: %#v", decrypted, plainText)
}
if !bytes.Equal(decrypted, decrypted1) {
t.Errorf("unexpected decrypted result\ngot: %#v\nexp: %#v", decrypted, decrypted1)
}
}

func TestSealAndOpenAuthenticationError(t *testing.T) {
ci, err := NewAESCipher(key)
if err != nil {
t.Fatal(err)
}
c := ci.(*aesCipher)
gcm, err := c.NewGCM(gcmStandardNonceSize, gcmTagSize)
if err != nil {
t.Fatal(err)
}
nonce := []byte{0x91, 0xc7, 0xa7, 0x54, 0x52, 0xef, 0x10, 0xdb, 0x91, 0xa8, 0x6c, 0xf9}
plainText := []byte{0x01, 0x02, 0x03}
additionalData := []byte{0x05, 0x05, 0x07}
sealed := gcm.Seal(nil, nonce, plainText, additionalData)
_, err = gcm.Open(nil, nonce, sealed, nil)
if err != errOpen {
t.Errorf("expected authentication error, got: %#v", err)
}
}

func assertPanic(t *testing.T, f func()) {
t.Helper()
defer func() {
if r := recover(); r == nil {
t.Errorf("The code did not panic")
}
}()
f()
}

func TestSealPanic(t *testing.T) {
ci, err := NewAESCipher(key)
if err != nil {
t.Fatal(err)
}
c := ci.(*aesCipher)
gcm, err := c.NewGCM(gcmStandardNonceSize, gcmTagSize)
if err != nil {
t.Fatal(err)
}
assertPanic(t, func() {
gcm.Seal(nil, make([]byte, gcmStandardNonceSize-1), []byte{0x01, 0x02, 0x03}, nil)
})
assertPanic(t, func() {
// maxInt is implemented as math.MaxInt, but this constant
// is only available since go1.17.
// TODO: use math.MaxInt once go1.16 is no longer supported.
maxInt := int((^uint(0)) >> 1)
gcm.Seal(nil, make([]byte, gcmStandardNonceSize), make([]byte, maxInt), nil)
})
}

func TestAESInvalidKeySize(t *testing.T) {
_, err := NewAESCipher([]byte{1})
if err == nil {
Expand All @@ -20,7 +171,6 @@ func TestAESInvalidKeySize(t *testing.T) {
}

func TestEncryptAndDecrypt(t *testing.T) {
key := []byte("D249BF6DEC97B1EBD69BC4D6B3A3C49D")
ci, err := NewAESCipher(key)
if err != nil {
t.Fatal(err)
Expand Down
5 changes: 3 additions & 2 deletions internal/bcrypt/bcrypt_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const (
CHAINING_MODE = "ChainingMode"
CHAIN_MODE_ECB = "ChainingModeECB"
CHAIN_MODE_CBC = "ChainingModeCBC"
CHAIN_MODE_GCM = "ChainingModeGCM"
KEY_LENGTHS = "KeyLengths"
)

Expand Down Expand Up @@ -108,5 +109,5 @@ func NewAUTHENTICATED_CIPHER_MODE_INFO(nonce, additionalData, tag []byte) *AUTHE

//sys GenerateSymmetricKey(hAlgorithm ALG_HANDLE, phKey *KEY_HANDLE, pbKeyObject *byte, cbKeyObject uint32, pbSecret *byte, cbSecret uint32, dwFlags uint32) (s error) = bcrypt.BCryptGenerateSymmetricKey
//sys DestroyKey(hKey KEY_HANDLE) (s error) = bcrypt.BCryptDestroyKey
//sys Encrypt(hKey KEY_HANDLE, pbInput *byte, cbInput uint32, pPaddingInfo uintptr, pbIV *byte, cbIV uint32, pbOutput *byte, cbOutput uint32, pcbResult *uint32, dwFlags uint32) (s error) = bcrypt.BCryptEncrypt
//sys Decrypt(hKey KEY_HANDLE, pbInput *byte, cbInput uint32, pPaddingInfo uintptr, pbIV *byte, cbIV uint32, pbOutput *byte, cbOutput uint32, pcbResult *uint32, dwFlags uint32) (s error) = bcrypt.BCryptDecrypt
//sys Encrypt(hKey KEY_HANDLE, pbInput *byte, cbInput uint32, pPaddingInfo *AUTHENTICATED_CIPHER_MODE_INFO, pbIV *byte, cbIV uint32, pbOutput *byte, cbOutput uint32, pcbResult *uint32, dwFlags uint32) (s error) = bcrypt.BCryptEncrypt
//sys Decrypt(hKey KEY_HANDLE, pbInput *byte, cbInput uint32, pPaddingInfo *AUTHENTICATED_CIPHER_MODE_INFO, pbIV *byte, cbIV uint32, pbOutput *byte, cbOutput uint32, pcbResult *uint32, dwFlags uint32) (s error) = bcrypt.BCryptDecrypt
Loading

0 comments on commit 938ad52

Please sign in to comment.