Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 227 additions & 0 deletions crypto/aescbcaead/aescbcaead.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/*
Copyright 2022 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package aescbcaead

// This implements an AEAD with AES-CBC and HMAC-SHA256
// Specs:
// - https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05
// - https://www.rfc-editor.org/rfc/rfc7518#section-5.2
//
// The code is inspired by https://github.com/codahale/etm
// Copyright (c) 2014 Coda Hale
// License: MIT https://github.com/codahale/etm/blob/master/LICENSE

import (
"crypto"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"encoding/binary"
"errors"
"fmt"
"hash"

"github.com/dapr/kit/crypto/padding"
)

// NewAESCBC128SHA256 returns an AEAD_AES_128_CBC_HMAC_SHA_256 instance given a
// 32-byte key or an error if the key is the wrong size.
// AEAD_AES_128_CBC_HMAC_SHA_256 combines AES-128 in CBC mode with
// HMAC-SHA-256-128.
func NewAESCBC128SHA256(key []byte) (cipher.AEAD, error) {
return NewAESCBCAEAD(aesCBCAEADParams{
macAlg: crypto.SHA256.New,
encKeySize: 16,
macKeySize: 16,
tagSize: 16,
key: key,
})
}

// NewAESCBC192SHA384 returns an AEAD_AES_192_CBC_HMAC_SHA_384 instance given a
// 48-byte key or an error if the key is the wrong size.
// AEAD_AES_192_CBC_HMAC_SHA_384 combines AES-192 in CBC mode with
// HMAC-SHA-384-192.
func NewAESCBC192SHA384(key []byte) (cipher.AEAD, error) {
return NewAESCBCAEAD(aesCBCAEADParams{
macAlg: crypto.SHA384.New,
encKeySize: 24,
macKeySize: 24,
tagSize: 24,
key: key,
})
}

// NewAESCBC256SHA384 returns an AEAD_AES_256_CBC_HMAC_SHA_384 instance given a
// 56-byte key or an error if the key is the wrong size.
// AEAD_AES_256_CBC_HMAC_SHA_384 combines AES-256 in CBC mode with
// HMAC-SHA-384-192.
func NewAESCBC256SHA384(key []byte) (cipher.AEAD, error) {
return NewAESCBCAEAD(aesCBCAEADParams{
macAlg: crypto.SHA384.New,
encKeySize: 32,
macKeySize: 24,
tagSize: 24,
key: key,
})
}

// NewAESCBC256SHA512 returns an AEAD_AES_256_CBC_HMAC_SHA_512 instance given a
// 64-byte key or an error if the key is the wrong size.
// AEAD_AES_256_CBC_HMAC_SHA_512 combines AES-256 in CBC mode with
// HMAC-SHA-512-256.
func NewAESCBC256SHA512(key []byte) (cipher.AEAD, error) {
return NewAESCBCAEAD(aesCBCAEADParams{
macAlg: crypto.SHA512.New,
encKeySize: 32,
macKeySize: 32,
tagSize: 32,
key: key,
})
}

type aesCBCAEADParams struct {
encKeySize, macKeySize, tagSize int

key []byte
macAlg func() hash.Hash
}

// NewAESCBCAEAD creates a new AEAD cipher based on AES-CBC with HMAC-SHA.
func NewAESCBCAEAD(p aesCBCAEADParams) (cipher.AEAD, error) {
l := p.encKeySize + p.macKeySize
if len(p.key) != l {
return nil, fmt.Errorf("key must be %d bytes long", l)
}
macKey := p.key[0:p.macKeySize]
encKey := p.key[len(p.key)-p.encKeySize:]
return &aesCBCAEAD{
aesCBCAEADParams: p,
encKey: encKey,
macKey: macKey,
}, nil
}

type aesCBCAEAD struct {
aesCBCAEADParams
encKey, macKey []byte
}

func (aead *aesCBCAEAD) Overhead() int {
return aead.tagSize
}

func (aead *aesCBCAEAD) NonceSize() int {
return aes.BlockSize
}

func (aead *aesCBCAEAD) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
// In this method, we panic in case of errors because the aead.Seal() interface doesn't allow returning errors
// However, errors in this method should only happen due to development-time mistakes, so we should never have to panic at runtime.
if len(nonce) != aes.BlockSize {
panic("invalid nonce")
}

// Create the cipher
block, err := aes.NewCipher(aead.encKey)
if err != nil {
panic(err)
}

// Pad the plaintext with PKCS#7 per specs
plaintext, err = padding.PadPKCS7(plaintext, aes.BlockSize)
if err != nil {
panic(err)
}

// Allocate a byte slice large enough to contain the ciphertext and the tag
size := len(plaintext) + aead.tagSize
dstLen := len(dst)
if cap(dst) >= (dstLen + size) {
dst = dst[:dstLen+size]
} else {
d := make([]byte, dstLen+size)
copy(d, dst)
dst = d
}
out := dst[dstLen:]

// Encrypt the message
cipher.NewCBCEncrypter(block, nonce).
CryptBlocks(out[:len(out)-aead.tagSize], plaintext)

// Compute the authentication tag and append it at the end
tag := aead.hmacTag(hmac.New(aead.macAlg, aead.macKey), additionalData, nonce, out[:len(out)-aead.tagSize], aead.tagSize)
copy(out[len(out)-aead.tagSize:], tag)

return dst
}

func (aead *aesCBCAEAD) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
if len(ciphertext) < aead.tagSize {
return nil, errors.New("invalid ciphertext size")
}

// Remove the tag from the end of the ciphertext
ciphertextTag := ciphertext[len(ciphertext)-aead.tagSize:]
ciphertext = ciphertext[:len(ciphertext)-aead.tagSize]

// First, check the authentication tag matches
expectTag := aead.hmacTag(hmac.New(aead.macAlg, aead.macKey), additionalData, nonce, ciphertext, aead.tagSize)
if !hmac.Equal(ciphertextTag, expectTag) {
return nil, errors.New("message authentication failed")
}

// Ensure the destination slice has enough capacity
size := len(ciphertext)
dstLen := len(dst)
if cap(dst) >= (dstLen + size) {
dst = dst[:dstLen+size]
} else {
d := make([]byte, dstLen+size)
copy(d, dst)
dst = d
}
out := dst[dstLen:]

// Decrypt the ciphertext
block, err := aes.NewCipher(aead.encKey)
if err != nil {
// Should never happen
return nil, err
}
cipher.NewCBCDecrypter(block, nonce).
CryptBlocks(out, ciphertext)

// Remove PKCS#7 padding
out, err = padding.UnpadPKCS7(out, aes.BlockSize)
if err != nil {
return nil, err
}
dst = dst[:dstLen+len(out)]
return dst, nil
}

// Computes the HMAC tag as per specs.
func (aead aesCBCAEAD) hmacTag(h hash.Hash, additionalData, nonce, ciphertext []byte, l int) []byte {
al := make([]byte, 8)
binary.BigEndian.PutUint64(al, uint64(len(additionalData)<<3)) // In bits

h.Write(additionalData)
h.Write(nonce)
h.Write(ciphertext)
h.Write(al)

return h.Sum(nil)[:l]
}
170 changes: 170 additions & 0 deletions crypto/aescbcaead/aescbcaead_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
Copyright 2022 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package aescbcaead

import (
"encoding/hex"
"testing"

"github.com/stretchr/testify/require"
)

func TestNewAESCBCAEAD(t *testing.T) {
t.Run("test cases from RFC", func(t *testing.T) {
// These test cases come from https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5
t.Run("AEAD_AES_128_CBC_HMAC_SHA256", func(t *testing.T) {
key, _ := hex.DecodeString("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
plaintext, _ := hex.DecodeString("41206369706865722073797374656d206d757374206e6f7420626520726571756972656420746f206265207365637265742c20616e64206974206d7573742062652061626c6520746f2066616c6c20696e746f207468652068616e6473206f662074686520656e656d7920776974686f757420696e636f6e76656e69656e6365")
nonce, _ := hex.DecodeString("1af38c2dc2b96ffdd86694092341bc04")
aad, _ := hex.DecodeString("546865207365636f6e64207072696e6369706c65206f662041756775737465204b6572636b686f666673")
ciphertext, _ := hex.DecodeString("c80edfa32ddf39d5ef00c0b468834279a2e46a1b8049f792f76bfe54b903a9c9a94ac9b47ad2655c5f10f9aef71427e2fc6f9b3f399a221489f16362c703233609d45ac69864e3321cf82935ac4096c86e133314c54019e8ca7980dfa4b9cf1b384c486f3a54c51078158ee5d79de59fbd34d848b3d69550a67646344427ade54b8851ffb598f7f80074b9473c82e2db652c3fa36b0a7c5b3219fab3a30bc1c4")

aead, err := NewAESCBC128SHA256(key)
require.NoError(t, err)
require.Equal(t, len(nonce), aead.NonceSize())
require.Equal(t, 16, aead.Overhead())

gotCiphertext := aead.Seal(nil, nonce, plaintext, aad)
require.Equal(t, ciphertext, gotCiphertext)

gotPlaintext, err := aead.Open(nil, nonce, gotCiphertext, aad)
require.NoError(t, err)
require.Equal(t, plaintext, gotPlaintext)
})

t.Run("AEAD_AES_192_CBC_HMAC_SHA384", func(t *testing.T) {
key, _ := hex.DecodeString("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f")
plaintext, _ := hex.DecodeString("41206369706865722073797374656d206d757374206e6f7420626520726571756972656420746f206265207365637265742c20616e64206974206d7573742062652061626c6520746f2066616c6c20696e746f207468652068616e6473206f662074686520656e656d7920776974686f757420696e636f6e76656e69656e6365")
nonce, _ := hex.DecodeString("1af38c2dc2b96ffdd86694092341bc04")
aad, _ := hex.DecodeString("546865207365636f6e64207072696e6369706c65206f662041756775737465204b6572636b686f666673")
ciphertext, _ := hex.DecodeString("ea65da6b59e61edb419be62d19712ae5d303eeb50052d0dfd6697f77224c8edb000d279bdc14c1072654bd30944230c657bed4ca0c9f4a8466f22b226d1746214bf8cfc2400add9f5126e479663fc90b3bed787a2f0ffcbf3904be2a641d5c2105bfe591bae23b1d7449e532eef60a9ac8bb6c6b01d35d49787bcd57ef484927f280adc91ac0c4e79c7b11efc60054e38490ac0e58949bfe51875d733f93ac2075168039ccc733d7")

aead, err := NewAESCBC192SHA384(key)
require.NoError(t, err)
require.Equal(t, len(nonce), aead.NonceSize())
require.Equal(t, 24, aead.Overhead())

gotCiphertext := aead.Seal(nil, nonce, plaintext, aad)
require.Equal(t, ciphertext, gotCiphertext)

gotPlaintext, err := aead.Open(nil, nonce, gotCiphertext, aad)
require.NoError(t, err)
require.Equal(t, plaintext, gotPlaintext)
})

t.Run("AEAD_AES_256_CBC_HMAC_SHA384", func(t *testing.T) {
key, _ := hex.DecodeString("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637")
plaintext, _ := hex.DecodeString("41206369706865722073797374656d206d757374206e6f7420626520726571756972656420746f206265207365637265742c20616e64206974206d7573742062652061626c6520746f2066616c6c20696e746f207468652068616e6473206f662074686520656e656d7920776974686f757420696e636f6e76656e69656e6365")
nonce, _ := hex.DecodeString("1af38c2dc2b96ffdd86694092341bc04")
aad, _ := hex.DecodeString("546865207365636f6e64207072696e6369706c65206f662041756775737465204b6572636b686f666673")
ciphertext, _ := hex.DecodeString("893129b0f4ee9eb18d75eda6f2aaa9f3607c98c4ba0444d34162170d8961884e58f27d4a35a5e3e3234aa99404f327f5c2d78e986e5749858b88bcddc2ba05218f195112d6ad48fa3b1e89aa7f20d596682f10b3648d3bb0c983c3185f59e36d28f647c1c13988de8ea0d821198c150977e28ca768080bc78c35faed69d8c0b7d9f506232198a489a1a6ae03a319fb30dd131d05ab3467dd056f8e882bad70637f1e9a541d9c23e7")

aead, err := NewAESCBC256SHA384(key)
require.NoError(t, err)
require.Equal(t, len(nonce), aead.NonceSize())
require.Equal(t, 24, aead.Overhead())

gotCiphertext := aead.Seal(nil, nonce, plaintext, aad)
require.Equal(t, ciphertext, gotCiphertext)

gotPlaintext, err := aead.Open(nil, nonce, gotCiphertext, aad)
require.NoError(t, err)
require.Equal(t, plaintext, gotPlaintext)
})

t.Run("AEAD_AES_256_CBC_HMAC_SHA512", func(t *testing.T) {
key, _ := hex.DecodeString("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f")
plaintext, _ := hex.DecodeString("41206369706865722073797374656d206d757374206e6f7420626520726571756972656420746f206265207365637265742c20616e64206974206d7573742062652061626c6520746f2066616c6c20696e746f207468652068616e6473206f662074686520656e656d7920776974686f757420696e636f6e76656e69656e6365")
nonce, _ := hex.DecodeString("1af38c2dc2b96ffdd86694092341bc04")
aad, _ := hex.DecodeString("546865207365636f6e64207072696e6369706c65206f662041756775737465204b6572636b686f666673")
ciphertext, _ := hex.DecodeString("4affaaadb78c31c5da4b1b590d10ffbd3dd8d5d302423526912da037ecbcc7bd822c301dd67c373bccb584ad3e9279c2e6d12a1374b77f077553df829410446b36ebd97066296ae6427ea75c2e0846a11a09ccf5370dc80bfecbad28c73f09b3a3b75e662a2594410ae496b2e2e6609e31e6e02cc837f053d21f37ff4f51950bbe2638d09dd7a4930930806d0703b1f64dd3b4c088a7f45c216839645b2012bf2e6269a8c56a816dbc1b267761955bc5")

aead, err := NewAESCBC256SHA512(key)
require.NoError(t, err)
require.Equal(t, len(nonce), aead.NonceSize())
require.Equal(t, 32, aead.Overhead())

gotCiphertext := aead.Seal(nil, nonce, plaintext, aad)
require.Equal(t, ciphertext, gotCiphertext)

gotPlaintext, err := aead.Open(nil, nonce, gotCiphertext, aad)
require.NoError(t, err)
require.Equal(t, plaintext, gotPlaintext)
})
})

t.Run("dst buffer", func(t *testing.T) {
key, _ := hex.DecodeString("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
plaintext, _ := hex.DecodeString("41206369706865722073797374656d206d757374206e6f7420626520726571756972656420746f206265207365637265742c20616e64206974206d7573742062652061626c6520746f2066616c6c20696e746f207468652068616e6473206f662074686520656e656d7920776974686f757420696e636f6e76656e69656e6365")
nonce, _ := hex.DecodeString("1af38c2dc2b96ffdd86694092341bc04")
aad, _ := hex.DecodeString("546865207365636f6e64207072696e6369706c65206f662041756775737465204b6572636b686f666673")
ciphertext, _ := hex.DecodeString("c80edfa32ddf39d5ef00c0b468834279a2e46a1b8049f792f76bfe54b903a9c9a94ac9b47ad2655c5f10f9aef71427e2fc6f9b3f399a221489f16362c703233609d45ac69864e3321cf82935ac4096c86e133314c54019e8ca7980dfa4b9cf1b384c486f3a54c51078158ee5d79de59fbd34d848b3d69550a67646344427ade54b8851ffb598f7f80074b9473c82e2db652c3fa36b0a7c5b3219fab3a30bc1c4")

aead, err := NewAESCBC128SHA256(key)
require.NoError(t, err)

t.Run("encrypt with empty dst buffer", func(t *testing.T) {
dst := []byte{}
gotCiphertext := aead.Seal(dst, nonce, plaintext, aad)
require.Equal(t, ciphertext, gotCiphertext)
})

t.Run("encrypt with dst buffer with capacity", func(t *testing.T) {
dst := make([]byte, 0, 1024)
gotCiphertext := aead.Seal(dst, nonce, plaintext, aad)
require.Equal(t, ciphertext, gotCiphertext)
})

t.Run("encrypt with non-empty dst buffer", func(t *testing.T) {
dst := []byte{0x01, 0x02, 0x03, 0x04}
gotCiphertext := aead.Seal(dst, nonce, plaintext, aad)
require.Equal(t, append([]byte{0x01, 0x02, 0x03, 0x04}, ciphertext...), gotCiphertext)
})

t.Run("encrypt with non-empty dst buffer with capacity", func(t *testing.T) {
dst := make([]byte, 4, 1024)
copy(dst, []byte{0x01, 0x02, 0x03, 0x04})
gotCiphertext := aead.Seal(dst, nonce, plaintext, aad)
require.Equal(t, append([]byte{0x01, 0x02, 0x03, 0x04}, ciphertext...), gotCiphertext)
})

t.Run("decrypt with empty dst buffer", func(t *testing.T) {
dst := []byte{}
gotPlaintext, err := aead.Open(dst, nonce, ciphertext, aad)
require.NoError(t, err)
require.Equal(t, plaintext, gotPlaintext)
})

t.Run("decrypt with dst buffer with capacity", func(t *testing.T) {
dst := make([]byte, 0, 1024)
gotPlaintext, err := aead.Open(dst, nonce, ciphertext, aad)
require.NoError(t, err)
require.Equal(t, plaintext, gotPlaintext)
})

t.Run("decrypt with non-empty dst buffer", func(t *testing.T) {
dst := []byte{0x01, 0x02, 0x03, 0x04}
gotPlaintext, err := aead.Open(dst, nonce, ciphertext, aad)
require.NoError(t, err)
require.Equal(t, append([]byte{0x01, 0x02, 0x03, 0x04}, plaintext...), gotPlaintext)
})

t.Run("decrypt with non-empty dst buffer with capacity", func(t *testing.T) {
dst := []byte{0x01, 0x02, 0x03, 0x04}
gotPlaintext, err := aead.Open(dst, nonce, ciphertext, aad)
require.NoError(t, err)
require.Equal(t, append([]byte{0x01, 0x02, 0x03, 0x04}, plaintext...), gotPlaintext)
})
})
}
Loading