Skip to content

Instantly share code, notes, and snippets.

@kennwhite
Last active November 12, 2021 08:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kennwhite/983217f927bc8422639dd2c1a37d3be9 to your computer and use it in GitHub Desktop.
Save kennwhite/983217f927bc8422639dd2c1a37d3be9 to your computer and use it in GitHub Desktop.
Implementation of AEAD-AES-256-CBC-HMAC-SHA-512 using spec test vectors
/*
Demonstration of AEAD_AES_256_CBC_HMAC_SHA_512, an authenticated encryption with associated
data (AEAD) scheme using a composition of AES in the CBC mode of operation with an
HMAC-SHA512 message authentication code, an Encrypt-Then-MAC construction.
From IETF draft spec by McGrew, Foley, & Paterson:
https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-2.7
Disclaimer:
This is strictly a proof of concept personal project and has not had any proper cryptography review.
Do not use for anything even vaguely important.
v. 0.1
https://play.golang.org/p/dHRe3yxnrgp
@todo verify slice logic tests on common edge cases, e.g., empty plaintext or associated data
@todo robust debug logging (output for decryption error oracles is intentionally spartan)
*/
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/sha512"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"log"
)
const (
K_LEN = 64 // size in bytes of composite input key K: 2.7, pg 10
T_LEN = 32 // size in bytes of MAC tag T: 2.8, pg 10
AL_LEN = 8 // size in bytes of AL, a uint64 expressing the number of bits in Associated Data A: #4, pg 6
IV_LEN = 16 // size in bytes of IV (b bits/blocksize of AES is 128): 5.4, pg 20
DEBUG = false // toggles (dangerously) verbose stdout logging, (true | false)
FAIL = "decryption failed" // special symbol per spec: 2.2, pg 7)
)
// Test vectors from: https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5.4
type TestCase struct {
inputKey []byte // K
hmacKey []byte // MAC_KEY
encryptKey []byte // ENC_KEY
plaintextMsg []byte // P
iv []byte // IV
assocData []byte // A
padStr []byte // PS
assocDataLen []byte // AL
ciphertextMsg []byte // Q
ciphertextWithIV []byte // S
authTag []byte // T
aeadCiphertext []byte // C
}
func main() {
fmt.Printf("Begin authenticated encryption...\n\n")
var testVals *TestCase = loadTestVals()
aeadPayload := aeadEncryptor(
testVals.inputKey,
testVals.iv,
testVals.plaintextMsg,
testVals.assocData,
testVals.assocDataLen)
fmt.Printf("Encryption complete: \n")
fmt.Printf("AEAD output payload (A || IV || enc(P||PS) || AL || T) \n%s \n", hex.Dump(aeadPayload))
fmt.Printf("Begin authenticating & decrypting AEAD payload...\n\n")
assocData, plaintextMsg, err := aeadDecryptor(testVals.inputKey, aeadPayload)
// Per spec, decryption output shoud ONLY be either authenticated plaintext or a FAIL indication. 2.2, pg7
if err != nil {
fmt.Println(FAIL)
} else {
fmt.Printf("Authenticated Associated Data: \n%s\n", hex.Dump(assocData))
fmt.Printf("Authenticated plaintext msg: \n%s\n", hex.Dump(plaintextMsg))
}
}
/*
Perform decryption operations according to method described in spec: 2.2, pg 7
Returns: Associated Data, unpadded plaintext message, error
*/
func aeadDecryptor(inputKey []byte, payload []byte) ([]byte, []byte, error) {
// Step 1: Generate secondary keys from input key K
if len(inputKey) != K_LEN {
if DEBUG {
log.Printf("Decryption fail: Invalid size for input key K. Should be %d bytes", K_LEN)
}
return []byte{}, []byte{}, errors.New(FAIL)
}
hmacKey, encryptKey := genKeys(inputKey)
// Step 2: Strip final T_LEN octets which contain the MAC auth tag T
// Sanity check:
// Min payload should be >= size of MAC tag T + AD bit counter AL + empty AD + IV + encrypted empty padded P
if len(payload) < (T_LEN + AL_LEN + IV_LEN + aes.BlockSize) {
if DEBUG {
log.Printf("Decryption fail: Corrupt payload smaller than expected minimum")
}
return []byte{}, []byte{}, errors.New(FAIL)
}
// Parse trailing MAC auth tag T
macExpected := payload[(len(payload) - T_LEN):]
// Remove it from the payload
payload = payload[:(len(payload) - T_LEN)]
// Step 3: Check integrity and authenticity of AD & padded ciphertext, fail fast
mac := hmac.New(sha512.New, hmacKey)
mac.Write(payload)
macComputed := mac.Sum(nil)
macComputed = macComputed[:T_LEN] // Truncate to HMAC-SHA-512 to 256 bits, T_LEN bytes: 2.8, pg 10
// Constant time MAC comparison to minimize timing-related side channel leaks
// Strive to fail fast to avoid error oracles
if !hmac.Equal(macComputed, macExpected) {
if DEBUG {
log.Printf("Authentication fail: Computed AEAD MAC != supplied payload MAC")
}
return []byte{}, []byte{}, errors.New(FAIL)
}
if DEBUG {
fmt.Printf("Split MAC & ENC keys: \n%s\n%s\n", hex.Dump(hmacKey), hex.Dump(encryptKey))
fmt.Printf("MAC input (AEAD payload): \n%s \n", hex.Dump(payload))
fmt.Printf("Expected MAC tag: \n%s \n", hex.Dump(macExpected))
fmt.Printf("Computed MAC tag: \n%s \n", hex.Dump(macComputed))
fmt.Printf("Payload successfully authenticated... \n\n")
}
// Parse value of current trailing segment containing data structure AL expressing size of Associated Data in bits
assocDataLenInBits := payload[(len(payload) - AL_LEN):]
assocDataLenInBytes := binary.BigEndian.Uint64(assocDataLenInBits) / 8
// Strip off trailing AL structure from payload
payload = payload[:(len(payload) - AL_LEN)]
// Parse Associated Data
assocData := payload[:assocDataLenInBytes]
payload = payload[assocDataLenInBytes:]
// Step 4: Parse IV, decrypt remaining (authenticated) payload of ciphertext of plaintext msg + padding string
iv := payload[:aes.BlockSize]
ciphertext := payload[aes.BlockSize:]
if DEBUG {
fmt.Printf("IV: \n%s \n", hex.Dump(iv))
fmt.Printf("Ciphertext: \n%s \n", hex.Dump(ciphertext))
}
block, err := aes.NewCipher(encryptKey)
if err != nil {
// Decryption failure, return empty slices to indicate FAIL response
return []byte{}, []byte{}, errors.New(FAIL)
}
plaintextMsg := make([]byte, len(ciphertext))
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(plaintextMsg, ciphertext)
// Step 5: Strip padding string. The final padding octet indicates the size of the full pad: #5 pg 7
padSize := int(plaintextMsg[len(plaintextMsg)-1])
plaintextMsg = plaintextMsg[:(len(plaintextMsg) - padSize)]
// Step 6: Return authenticated associated data & plaintext with no errors
return assocData, plaintextMsg, nil
}
/*
Perform authenticated encryption operations according to method described in spec: 2.1, pg 5
Returns: AEAD payload byte slice in the following format
(A || IV || enc(P||PS) || AL || T)
Associated Data || IV || encrypted ( plaintext || padding string ) || length of Associated Data || MAC tag
*/
func aeadEncryptor(inputKey []byte, iv []byte, plaintextMsg []byte, assocData []byte, assocDataLen []byte) []byte {
hmacKey, encryptKey := genKeys(inputKey)
msgPadded := pkcs5Pad(plaintextMsg)
if len(msgPadded)%aes.BlockSize != 0 {
panic("Padded plaintext msg is not a multiple of the block size")
}
block, err := aes.NewCipher(encryptKey)
if err != nil {
panic(err)
}
// The IV needs to be unique, but not secure. Therefore it's common to
// include it at the beginning of the ciphertext.
ivLen := aes.BlockSize
ciphertext := make([]byte, ivLen+len(msgPadded))
copy(ciphertext[:ivLen], iv)
mode := cipher.NewCBCEncrypter(block, iv)
// If len(dst) < len(src), CryptBlocks should panic per https://pkg.go.dev/crypto/cipher#BlockMode
mode.CryptBlocks(ciphertext[ivLen:], msgPadded)
if len(ciphertext)%aes.BlockSize != 0 {
panic("Ciphertext is not a multiple of the block size")
}
// Concatenate (A || S || AL)
macInput := append(assocData, append(ciphertext, assocDataLen...)...)
// Calculate MAC auth tag T: 2.7, pg 10
mac := hmac.New(sha512.New, hmacKey)
mac.Write(macInput)
authTag := mac.Sum(nil)
authTag = authTag[:T_LEN] // Truncate to HMAC-SHA-512 to 256 bits, T_LEN bytes: 2.8, pg 10
// Add MAC to the end before returning payload
aeadPayload := append(macInput, authTag...)
fmt.Printf("Split MAC & ENC keys: \n%s\n%s\n", hex.Dump(hmacKey), hex.Dump(encryptKey))
fmt.Printf("Associated Data (A): \n%s\n", hex.Dump(assocData))
fmt.Printf("Associated Data length (AL): \n%s\n", hex.Dump(assocDataLen))
fmt.Printf("IV:\n%s\n", hex.Dump(iv))
fmt.Printf("MAC tag T:\n%s\n", hex.Dump(authTag))
fmt.Printf("Plaintext msg P with Padding String PS ( P || PS): \n%s\n", hex.Dump(msgPadded))
return aeadPayload
}
/*
PKCS#5/7 padding is not currently supported in Go's standard library
See: https://groups.google.com/g/golang-dev/c/q6wSqGyJo2s/m/cOy-g8pUBwAJ
But note: https://github.com/search?l=Go&o=desc&q=%22func+pkcs5Pad%22&s=indexed&type=Code
And also: https://github.com/search?l=Go&o=desc&q=%22func+pkcs7Pad%22&s=indexed&type=Code
@todo: File PR to attempt to revive this feature request for Go std lib
Source: https://github.com/elastic/cloud-on-k8s/blob/master/pkg/controller/common/license/crypto.go#L58
*/
func pkcs5Pad(data []byte) []byte {
padLen := aes.BlockSize - len(data)%aes.BlockSize
padding := bytes.Repeat([]byte{byte(padLen)}, padLen)
return append(data, padding...)
}
func genKeys(inputKey []byte) ([]byte, []byte) {
if len(inputKey) != 64 {
panic("Composite key must be 512 bits.")
}
hmacKey := inputKey[:32]
encryptKey := inputKey[32:]
if len(encryptKey) != aes.BlockSize*2 {
panic("AEAD_AES_256_CBC_HMAC_SHA_512 encryption requires a 256 bit key")
}
return hmacKey, encryptKey
}
/*
All definitions & values here are from AEAD_AES_256_CBC_HMAC_SHA512 (pg 20) test vectors in:
https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5.4
*/
func loadTestVals() *TestCase {
// K - 2.1, pg 5
inputKey, _ := hex.DecodeString(
"000102030405060708090a0b0c0d0e0f" +
"101112131415161718191a1b1c1d1e1f" +
"202122232425262728292a2b2c2d2e2f" +
"303132333435363738393a3b3c3d3e3f")
// MAC_KEY - 2.1, pg 5
hmacKey, _ := hex.DecodeString(
"000102030405060708090a0b0c0d0e0f" +
"101112131415161718191a1b1c1d1e1f")
// ENC_KEY - 2.1, pg 5
encryptKey, _ := hex.DecodeString(
"202122232425262728292a2b2c2d2e2f" +
"303132333435363738393a3b3c3d3e3f")
// P - 2.1, pg 5
plaintextMsg, _ := hex.DecodeString(
"41206369706865722073797374656d20" +
"6d757374206e6f742062652072657175" +
"6972656420746f206265207365637265" +
"742c20616e64206974206d7573742062" +
"652061626c6520746f2066616c6c2069" +
"6e746f207468652068616e6473206f66" +
"2074686520656e656d7920776974686f" +
"757420696e636f6e76656e69656e6365")
// IV - Appendix A, pg 28
iv, _ := hex.DecodeString(
"1af38c2dc2b96ffdd86694092341bc04")
// A - 2.1, pg 5
assocData, _ := hex.DecodeString(
"546865207365636f6e64207072696e63" +
"69706c65206f66204175677573746520" +
"4b6572636b686f666673")
// PS - #2, pg 6
padStr, _ := hex.DecodeString(
"10101010101010101010101010101010")
// AL - #4, pg 6
assocDataLen, _ := hex.DecodeString(
"0000000000000150")
// Q - Appendix A, pg 28
ciphertextMsg, _ := hex.DecodeString(
"4affaaadb78c31c5da4b1b590d10ffbd" +
"3dd8d5d302423526912da037ecbcc7bd" +
"822c301dd67c373bccb584ad3e9279c2" +
"e6d12a1374b77f077553df829410446b" +
"36ebd97066296ae6427ea75c2e0846a1" +
"1a09ccf5370dc80bfecbad28c73f09b3" +
"a3b75e662a2594410ae496b2e2e6609e" +
"31e6e02cc837f053d21f37ff4f51950b" +
"be2638d09dd7a4930930806d0703b1f6")
// S - Appendix A, pg 21
ciphertextWithIV, _ := hex.DecodeString(
"1af38c2dc2b96ffdd86694092341bc04" +
"4affaaadb78c31c5da4b1b590d10ffbd" +
"3dd8d5d302423526912da037ecbcc7bd" +
"822c301dd67c373bccb584ad3e9279c2" +
"e6d12a1374b77f077553df829410446b" +
"36ebd97066296ae6427ea75c2e0846a1" +
"1a09ccf5370dc80bfecbad28c73f09b3" +
"a3b75e662a2594410ae496b2e2e6609e" +
"31e6e02cc837f053d21f37ff4f51950b" +
"be2638d09dd7a4930930806d0703b1f6")
// T - #5, pg 6
authTag, _ := hex.DecodeString(
"4dd3b4c088a7f45c216839645b2012bf" +
"2e6269a8c56a816dbc1b267761955bc5")
// C - #6, pg 6
aeadCiphertext, _ := hex.DecodeString(
"1af38c2dc2b96ffdd86694092341bc04" +
"4affaaadb78c31c5da4b1b590d10ffbd" +
"3dd8d5d302423526912da037ecbcc7bd" +
"822c301dd67c373bccb584ad3e9279c2" +
"e6d12a1374b77f077553df829410446b" +
"36ebd97066296ae6427ea75c2e0846a1" +
"1a09ccf5370dc80bfecbad28c73f09b3" +
"a3b75e662a2594410ae496b2e2e6609e" +
"31e6e02cc837f053d21f37ff4f51950b" +
"be2638d09dd7a4930930806d0703b1f6" +
"4dd3b4c088a7f45c216839645b2012bf" +
"2e6269a8c56a816dbc1b267761955bc5")
return &TestCase{
inputKey: inputKey,
hmacKey: hmacKey,
encryptKey: encryptKey,
plaintextMsg: plaintextMsg,
iv: iv,
assocData: assocData,
padStr: padStr,
assocDataLen: assocDataLen,
ciphertextMsg: ciphertextMsg,
ciphertextWithIV: ciphertextWithIV,
authTag: authTag,
aeadCiphertext: aeadCiphertext}
}
$ go run main.go
Begin authenticated encryption...
Split MAC & ENC keys:
00000000 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
00000010 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f |................|
00000000 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f | !"#$%&'()*+,-./|
00000010 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f |0123456789:;<=>?|
Associated Data (A):
00000000 54 68 65 20 73 65 63 6f 6e 64 20 70 72 69 6e 63 |The second princ|
00000010 69 70 6c 65 20 6f 66 20 41 75 67 75 73 74 65 20 |iple of Auguste |
00000020 4b 65 72 63 6b 68 6f 66 66 73 |Kerckhoffs|
Associated Data length (AL):
00000000 00 00 00 00 00 00 01 50 |.......P|
IV:
00000000 1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04 |...-..o..f..#A..|
MAC tag T:
00000000 4d d3 b4 c0 88 a7 f4 5c 21 68 39 64 5b 20 12 bf |M......\!h9d[ ..|
00000010 2e 62 69 a8 c5 6a 81 6d bc 1b 26 77 61 95 5b c5 |.bi..j.m..&wa.[.|
Plaintext msg P with Padding String PS ( P || PS):
00000000 41 20 63 69 70 68 65 72 20 73 79 73 74 65 6d 20 |A cipher system |
00000010 6d 75 73 74 20 6e 6f 74 20 62 65 20 72 65 71 75 |must not be requ|
00000020 69 72 65 64 20 74 6f 20 62 65 20 73 65 63 72 65 |ired to be secre|
00000030 74 2c 20 61 6e 64 20 69 74 20 6d 75 73 74 20 62 |t, and it must b|
00000040 65 20 61 62 6c 65 20 74 6f 20 66 61 6c 6c 20 69 |e able to fall i|
00000050 6e 74 6f 20 74 68 65 20 68 61 6e 64 73 20 6f 66 |nto the hands of|
00000060 20 74 68 65 20 65 6e 65 6d 79 20 77 69 74 68 6f | the enemy witho|
00000070 75 74 20 69 6e 63 6f 6e 76 65 6e 69 65 6e 63 65 |ut inconvenience|
00000080 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 |................|
Encryption complete:
AEAD output payload (A || IV || enc(P||PS) || AL || T)
00000000 54 68 65 20 73 65 63 6f 6e 64 20 70 72 69 6e 63 |The second princ|
00000010 69 70 6c 65 20 6f 66 20 41 75 67 75 73 74 65 20 |iple of Auguste |
00000020 4b 65 72 63 6b 68 6f 66 66 73 1a f3 8c 2d c2 b9 |Kerckhoffs...-..|
00000030 6f fd d8 66 94 09 23 41 bc 04 4a ff aa ad b7 8c |o..f..#A..J.....|
00000040 31 c5 da 4b 1b 59 0d 10 ff bd 3d d8 d5 d3 02 42 |1..K.Y....=....B|
00000050 35 26 91 2d a0 37 ec bc c7 bd 82 2c 30 1d d6 7c |5&.-.7.....,0..||
00000060 37 3b cc b5 84 ad 3e 92 79 c2 e6 d1 2a 13 74 b7 |7;....>.y...*.t.|
00000070 7f 07 75 53 df 82 94 10 44 6b 36 eb d9 70 66 29 |..uS....Dk6..pf)|
00000080 6a e6 42 7e a7 5c 2e 08 46 a1 1a 09 cc f5 37 0d |j.B~.\..F.....7.|
00000090 c8 0b fe cb ad 28 c7 3f 09 b3 a3 b7 5e 66 2a 25 |.....(.?....^f*%|
000000a0 94 41 0a e4 96 b2 e2 e6 60 9e 31 e6 e0 2c c8 37 |.A......`.1..,.7|
000000b0 f0 53 d2 1f 37 ff 4f 51 95 0b be 26 38 d0 9d d7 |.S..7.OQ...&8...|
000000c0 a4 93 09 30 80 6d 07 03 b1 f6 00 00 00 00 00 00 |...0.m..........|
000000d0 01 50 4d d3 b4 c0 88 a7 f4 5c 21 68 39 64 5b 20 |.PM......\!h9d[ |
000000e0 12 bf 2e 62 69 a8 c5 6a 81 6d bc 1b 26 77 61 95 |...bi..j.m..&wa.|
000000f0 5b c5 |[.|
Begin authenticating & decrypting AEAD payload...
Authenticated Associated Data:
00000000 54 68 65 20 73 65 63 6f 6e 64 20 70 72 69 6e 63 |The second princ|
00000010 69 70 6c 65 20 6f 66 20 41 75 67 75 73 74 65 20 |iple of Auguste |
00000020 4b 65 72 63 6b 68 6f 66 66 73 |Kerckhoffs|
Authenticated plaintext msg:
00000000 41 20 63 69 70 68 65 72 20 73 79 73 74 65 6d 20 |A cipher system |
00000010 6d 75 73 74 20 6e 6f 74 20 62 65 20 72 65 71 75 |must not be requ|
00000020 69 72 65 64 20 74 6f 20 62 65 20 73 65 63 72 65 |ired to be secre|
00000030 74 2c 20 61 6e 64 20 69 74 20 6d 75 73 74 20 62 |t, and it must b|
00000040 65 20 61 62 6c 65 20 74 6f 20 66 61 6c 6c 20 69 |e able to fall i|
00000050 6e 74 6f 20 74 68 65 20 68 61 6e 64 73 20 6f 66 |nto the hands of|
00000060 20 74 68 65 20 65 6e 65 6d 79 20 77 69 74 68 6f | the enemy witho|
00000070 75 74 20 69 6e 63 6f 6e 76 65 6e 69 65 6e 63 65 |ut inconvenience|
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment