diff --git a/README.md b/README.md index c569758..c823a0e 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,15 @@ This repo provides a list of utility modules for common crypto operations. - encrypt & decrypt methods, the output ciphertext is prefixed with the random nonce. - verify the constructed cipher against the check value +### AES-CBC + +- factory methods to construct an AES-CBC cipher from the raw key bytes or hex text +- encrypt & decrypt methods for block-aligned or ISO/IEC 9797-1 method 2 padded input +- generate a random 16-byte initialisation vector +- verify the constructed cipher against the check value + +AES-CBC does not authenticate ciphertext. Callers must authenticate the initialisation vector and ciphertext separately. + ### DES - factory methods to construct an DES or 3DES cipher from the raw key bytes or hex text diff --git a/VERSION.txt b/VERSION.txt index f88cf52..cd99d38 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -1.13.0 \ No newline at end of file +1.14.0 \ No newline at end of file diff --git a/aes/aes_cbc_cipher.go b/aes/aes_cbc_cipher.go new file mode 100644 index 0000000..f9d34c4 --- /dev/null +++ b/aes/aes_cbc_cipher.go @@ -0,0 +1,113 @@ +/* + 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 aes + +import ( + "crypto/cipher" + "errors" + "fmt" +) + +// CBCCipher encrypts and decrypts data with AES-CBC. +// +// CBC does not authenticate data. Callers must authenticate the IV and +// ciphertext separately and must not expose padding errors to untrusted callers. +// The caller supplies the IV; CBCCipher does not include it in the ciphertext. +type CBCCipher struct { + block cipher.Block + KeyBytes []byte +} + +// Encrypt encrypts non-empty, block-aligned plainBytes with a 16-byte IV. +// It does not add padding or modify its inputs. +func (c *CBCCipher) Encrypt(plainBytes []byte, iv []byte) ([]byte, error) { + blockSize := c.block.BlockSize() + if err := validateIV(iv, blockSize); err != nil { + return nil, err + } + if len(plainBytes) == 0 { + return nil, errors.New("input is empty") + } + if len(plainBytes)%blockSize != 0 { + return nil, fmt.Errorf("input length %d is not a multiplier of block size %d", len(plainBytes), blockSize) + } + + cipherBytes := make([]byte, len(plainBytes)) + cipher.NewCBCEncrypter(c.block, cloneBytes(iv)).CryptBlocks(cipherBytes, plainBytes) + return cipherBytes, nil +} + +// Decrypt decrypts non-empty, block-aligned cipherBytes with a 16-byte IV. +// It does not remove padding or modify its inputs. +func (c *CBCCipher) Decrypt(cipherBytes []byte, iv []byte) ([]byte, error) { + blockSize := c.block.BlockSize() + if err := validateIV(iv, blockSize); err != nil { + return nil, err + } + if len(cipherBytes) == 0 { + return nil, errors.New("input is empty") + } + if len(cipherBytes)%blockSize != 0 { + return nil, fmt.Errorf("input length %d is not a multiplier of block size %d", len(cipherBytes), blockSize) + } + + plainBytes := make([]byte, len(cipherBytes)) + cipher.NewCBCDecrypter(c.block, cloneBytes(iv)).CryptBlocks(plainBytes, cipherBytes) + return plainBytes, nil +} + +// EncryptISO9797M2Padded pads plainBytes with ISO/IEC 9797-1 method 2, then +// encrypts it with a 16-byte IV. It accepts empty input and does not modify its +// inputs. +func (c *CBCCipher) EncryptISO9797M2Padded(plainBytes []byte, iv []byte) ([]byte, error) { + blockSize := c.block.BlockSize() + if err := validateIV(iv, blockSize); err != nil { + return nil, err + } + + return c.Encrypt(padISO9797M2(plainBytes, blockSize), iv) +} + +// DecryptISO9797M2Padded decrypts cipherBytes with a 16-byte IV, then removes +// ISO/IEC 9797-1 method 2 padding. It does not modify its inputs. +func (c *CBCCipher) DecryptISO9797M2Padded(cipherBytes []byte, iv []byte) ([]byte, error) { + paddedBytes, err := c.Decrypt(cipherBytes, iv) + if err != nil { + return nil, err + } + + return unpadISO9797M2(paddedBytes, c.block.BlockSize()) +} + +// CheckValue returns the AES key check value. +func (c *CBCCipher) CheckValue() string { + return deriveCheckValue(c.KeyBytes) +} + +// VerifyCheckValue reports whether checkValue matches the key, ignoring case. +func (c *CBCCipher) VerifyCheckValue(checkValue string) bool { + return verifyCheckValue(c.KeyBytes, checkValue) +} + +func validateIV(iv []byte, blockSize int) error { + if len(iv) != blockSize { + return fmt.Errorf("IV must be %d bytes but got %d", blockSize, len(iv)) + } + return nil +} + +func cloneBytes(src []byte) []byte { + dst := make([]byte, len(src)) + copy(dst, src) + return dst +} diff --git a/aes/aes_cbc_cipher_test.go b/aes/aes_cbc_cipher_test.go new file mode 100644 index 0000000..b212bcd --- /dev/null +++ b/aes/aes_cbc_cipher_test.go @@ -0,0 +1,490 @@ +/* + 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 aes + +import ( + "bytes" + "encoding/hex" + "testing" +) + +// NIST SP 800-38A appendix F.2 uses the same plaintext and IV for each key size. +const ( + nistIV = "000102030405060708090a0b0c0d0e0f" + nistPlaintext = "6bc1bee22e409f96e93d7e117393172a" + + "ae2d8a571e03ac9c9eb76fac45af8e51" + + "30c81c46a35ce411e5fbc1191a0a52ef" + + "f69f2445df4f9b17ad2b417be66c3710" +) + +var nistCBCVectors = []struct { + name string + key string + ciphertext string +}{ + { + // F.2.1 / F.2.2 CBC-AES128 + name: "AES-128", + key: "2b7e151628aed2a6abf7158809cf4f3c", + ciphertext: "7649abac8119b246cee98e9b12e9197d" + + "5086cb9b507219ee95db113a917678b2" + + "73bed6b8e3c1743b7116e69e22229516" + + "3ff1caa1681fac09120eca307586e1a7", + }, + { + // F.2.3 / F.2.4 CBC-AES192 + name: "AES-192", + key: "8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b", + ciphertext: "4f021db243bc633d7178183a9fa071e8" + + "b4d9ada9ad7dedf4e5e738763f69145a" + + "571b242012fb7ae07fa9baac3df102e0" + + "08b0e27988598881d920a9e64f5615cd", + }, + { + // F.2.5 / F.2.6 CBC-AES256 + name: "AES-256", + key: "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4", + ciphertext: "f58c4c04d6e5f1ba779eabfb5f7bfbd6" + + "9cfc4e967edb808d679f777bc6702c7d" + + "39f23369a9d9bacfa530e26304231461" + + "b2eb05e2c39be9fcda6c19078c6a9d1b", + }, +} + +func TestAESCBCCipher_EncryptMatchesNISTVectors(t *testing.T) { + iv, _ := hex.DecodeString(nistIV) + plainBytes, _ := hex.DecodeString(nistPlaintext) + + for _, vector := range nistCBCVectors { + t.Run(vector.name, func(t *testing.T) { + cipher, err := CreateCBCFromKeyString(vector.key) + if err != nil { + t.Fatalf("Did not expect an error but got %q", err) + } + + cipherBytes, err := cipher.Encrypt(plainBytes, iv) + if err != nil { + t.Fatalf("Did not expect an encryption error but got %q", err) + } + + if hex.EncodeToString(cipherBytes) != vector.ciphertext { + t.Errorf("Expected %s but get %s", vector.ciphertext, hex.EncodeToString(cipherBytes)) + } + }) + } +} + +func TestAESCBCCipher_DecryptMatchesNISTVectors(t *testing.T) { + iv, _ := hex.DecodeString(nistIV) + + for _, vector := range nistCBCVectors { + t.Run(vector.name, func(t *testing.T) { + cipher, err := CreateCBCFromKeyString(vector.key) + if err != nil { + t.Fatalf("Did not expect an error but got %q", err) + } + cipherBytes, _ := hex.DecodeString(vector.ciphertext) + + plainBytes, err := cipher.Decrypt(cipherBytes, iv) + if err != nil { + t.Fatalf("Did not expect a decryption error but got %q", err) + } + + if hex.EncodeToString(plainBytes) != nistPlaintext { + t.Errorf("Expected %s but get %s", nistPlaintext, hex.EncodeToString(plainBytes)) + } + }) + } +} + +func TestAESCBCCipher_EncryptRejectsInvalidInputLength(t *testing.T) { + cipher, _ := CreateCBCFromKeyString(nistCBCVectors[0].key) + iv, _ := hex.DecodeString(nistIV) + + testCases := []struct { + name string + length int + }{ + {"empty", 0}, + {"one byte", 1}, + {"one byte short of a block", 15}, + {"one byte over a block", 17}, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + if _, err := cipher.Encrypt(make([]byte, testCase.length), iv); err == nil { + t.Error("Expected an error but got none") + } + if _, err := cipher.Decrypt(make([]byte, testCase.length), iv); err == nil { + t.Error("Expected an error but got none") + } + }) + } +} + +func TestAESCBCCipher_RejectsInvalidIVLength(t *testing.T) { + plainBytes, _ := hex.DecodeString(nistPlaintext) + + // An AES IV is always 16 bytes, regardless of key size. + for _, vector := range nistCBCVectors { + cipher, _ := CreateCBCFromKeyString(vector.key) + + for _, ivLength := range []int{0, 8, 15, 17, 24, 32} { + iv := make([]byte, ivLength) + + if _, err := cipher.Encrypt(plainBytes, iv); err == nil { + t.Errorf("%s: expected Encrypt to reject a %d byte IV", vector.name, ivLength) + } + if _, err := cipher.Decrypt(plainBytes, iv); err == nil { + t.Errorf("%s: expected Decrypt to reject a %d byte IV", vector.name, ivLength) + } + if _, err := cipher.EncryptISO9797M2Padded(plainBytes, iv); err == nil { + t.Errorf("%s: expected EncryptISO9797M2Padded to reject a %d byte IV", vector.name, ivLength) + } + if _, err := cipher.DecryptISO9797M2Padded(plainBytes, iv); err == nil { + t.Errorf("%s: expected DecryptISO9797M2Padded to reject a %d byte IV", vector.name, ivLength) + } + } + + if _, err := cipher.Encrypt(plainBytes, nil); err == nil { + t.Errorf("%s: expected Encrypt to reject a nil IV", vector.name) + } + } +} + +func TestAESCBCCipher_EncryptAndDecryptISO9797M2PaddedRoundTrip(t *testing.T) { + iv, _ := hex.DecodeString(nistIV) + + for _, vector := range nistCBCVectors { + t.Run(vector.name, func(t *testing.T) { + cipher, _ := CreateCBCFromKeyString(vector.key) + + // Cover both sides of two block boundaries. + for length := 0; length <= 33; length++ { + plainBytes := make([]byte, length) + for i := range plainBytes { + plainBytes[i] = byte(i) + } + + cipherBytes, err := cipher.EncryptISO9797M2Padded(plainBytes, iv) + if err != nil { + t.Errorf("Did not expect an encryption error for length %d but got %q", length, err) + continue + } + if len(cipherBytes)%16 != 0 || len(cipherBytes) <= length { + t.Errorf("Expected padded ciphertext longer than %d and block aligned but get %d", length, len(cipherBytes)) + } + + decryptedBytes, err := cipher.DecryptISO9797M2Padded(cipherBytes, iv) + if err != nil { + t.Errorf("Did not expect a decryption error for length %d but got %q", length, err) + continue + } + + if !bytes.Equal(plainBytes, decryptedBytes) { + t.Errorf("Expected %s but get %s", hex.EncodeToString(plainBytes), hex.EncodeToString(decryptedBytes)) + } + } + }) + } +} + +// A trailing padding block must not change the preceding NIST ciphertext blocks. +func TestAESCBCCipher_EncryptISO9797M2PaddedMatchesNISTVectors(t *testing.T) { + iv, _ := hex.DecodeString(nistIV) + plainBytes, _ := hex.DecodeString(nistPlaintext) + + for _, vector := range nistCBCVectors { + t.Run(vector.name, func(t *testing.T) { + cipher, _ := CreateCBCFromKeyString(vector.key) + expectedBytes, _ := hex.DecodeString(vector.ciphertext) + + cipherBytes, err := cipher.EncryptISO9797M2Padded(plainBytes, iv) + if err != nil { + t.Fatalf("Did not expect an encryption error but got %q", err) + } + + if len(cipherBytes) != len(expectedBytes)+16 { + t.Fatalf("Expected %d bytes of ciphertext but got %d", len(expectedBytes)+16, len(cipherBytes)) + } + if !bytes.Equal(cipherBytes[:len(expectedBytes)], expectedBytes) { + t.Errorf("Expected leading blocks %s but get %s", hex.EncodeToString(expectedBytes), hex.EncodeToString(cipherBytes[:len(expectedBytes)])) + } + }) + } +} + +func TestAESCBCCipher_DecryptISO9797M2PaddedRejectsMalformedPadding(t *testing.T) { + cipher, _ := CreateCBCFromKeyString(nistCBCVectors[0].key) + iv, _ := hex.DecodeString(nistIV) + + // Encrypt malformed padded plaintext to exercise the public decrypt path. + testCases := []struct { + name string + block string + }{ + { + name: "final byte is neither the marker nor zero filler", + block: "000102030405060708090a0b0c0d0e01", + }, + { + name: "zero filler runs back to a non marker byte", + block: "000102030405060708090a0b0c0d0e00", + }, + { + name: "block is all zeroes so the marker is missing", + block: "00000000000000000000000000000000", + }, + { + name: "PKCS#7 padding is not accepted in place of the marker", + block: "000102030405060708090a0b0c0d0202", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + unpaddableBlock, _ := hex.DecodeString(testCase.block) + + cipherBytes, err := cipher.Encrypt(unpaddableBlock, iv) + if err != nil { + t.Fatalf("Did not expect an encryption error but got %q", err) + } + + if _, err := cipher.DecryptISO9797M2Padded(cipherBytes, iv); err == nil { + t.Error("Expected DecryptISO9797M2Padded to reject malformed padding but got no error") + } + + // Raw decryption does not validate padding. + plainBytes, err := cipher.Decrypt(cipherBytes, iv) + if err != nil { + t.Errorf("Did not expect a raw decryption error but got %q", err) + } + if !bytes.Equal(plainBytes, unpaddableBlock) { + t.Errorf("Expected %s but get %s", hex.EncodeToString(unpaddableBlock), hex.EncodeToString(plainBytes)) + } + }) + } +} + +// CBC decryption cannot detect ciphertext changes. +func TestAESCBCCipher_DecryptDoesNotDetectModifiedCiphertext(t *testing.T) { + cipher, _ := CreateCBCFromKeyString(nistCBCVectors[0].key) + iv, _ := hex.DecodeString(nistIV) + plainBytes, _ := hex.DecodeString(nistPlaintext) + + cipherBytes, err := cipher.Encrypt(plainBytes, iv) + if err != nil { + t.Fatalf("Did not expect an encryption error but got %q", err) + } + + modifiedCipherBytes := cloneBytes(cipherBytes) + modifiedCipherBytes[0] ^= 0x01 + + decryptedBytes, err := cipher.Decrypt(modifiedCipherBytes, iv) + if err != nil { + t.Errorf("Expected a modified ciphertext to still decrypt without error but got %q", err) + } + if bytes.Equal(decryptedBytes, plainBytes) { + t.Error("Expected the recovered plaintext to differ from the original") + } +} + +// Changing the IV changes the first plaintext block without invalidating padding. +func TestAESCBCCipher_DecryptISO9797M2PaddedDoesNotDetectModifiedIV(t *testing.T) { + cipher, _ := CreateCBCFromKeyString(nistCBCVectors[0].key) + iv, _ := hex.DecodeString(nistIV) + plainBytes, _ := hex.DecodeString("000102030405060708090a0b0c0d0e0f") + + // Padding occupies a separate block. + cipherBytes, err := cipher.EncryptISO9797M2Padded(plainBytes, iv) + if err != nil { + t.Fatalf("Did not expect an encryption error but got %q", err) + } + + modifiedIV := cloneBytes(iv) + modifiedIV[0] ^= 0xff + + decryptedBytes, err := cipher.DecryptISO9797M2Padded(cipherBytes, modifiedIV) + if err != nil { + t.Fatalf("Expected a modified IV to still decrypt without error but got %q", err) + } + + expectedBytes := cloneBytes(plainBytes) + expectedBytes[0] ^= 0xff + if !bytes.Equal(decryptedBytes, expectedBytes) { + t.Errorf("Expected the attacker chosen plaintext %s but get %s", hex.EncodeToString(expectedBytes), hex.EncodeToString(decryptedBytes)) + } +} + +func TestAESCBCCipher_DoesNotModifyInputs(t *testing.T) { + cipher, _ := CreateCBCFromKeyString(nistCBCVectors[0].key) + iv, _ := hex.DecodeString(nistIV) + plainBytes, _ := hex.DecodeString(nistPlaintext) + cipherBytes, _ := hex.DecodeString(nistCBCVectors[0].ciphertext) + paddedCipherBytes, _ := cipher.EncryptISO9797M2Padded(plainBytes, iv) + + operations := []struct { + name string + input []byte + run func(input []byte, iv []byte) ([]byte, error) + }{ + {"Encrypt", plainBytes, cipher.Encrypt}, + {"Decrypt", cipherBytes, cipher.Decrypt}, + {"EncryptISO9797M2Padded", plainBytes, cipher.EncryptISO9797M2Padded}, + {"DecryptISO9797M2Padded", paddedCipherBytes, cipher.DecryptISO9797M2Padded}, + } + + for _, operation := range operations { + t.Run(operation.name, func(t *testing.T) { + input := cloneBytes(operation.input) + operationIV := cloneBytes(iv) + + if _, err := operation.run(input, operationIV); err != nil { + t.Fatalf("Did not expect an error but got %q", err) + } + + if !bytes.Equal(input, operation.input) { + t.Errorf("Expected input to be left as %s but get %s", hex.EncodeToString(operation.input), hex.EncodeToString(input)) + } + if !bytes.Equal(operationIV, iv) { + t.Errorf("Expected IV to be left as %s but get %s", hex.EncodeToString(iv), hex.EncodeToString(operationIV)) + } + }) + } +} + +func TestAESCBCCipher_DifferentIVsProduceDifferentCiphertext(t *testing.T) { + cipher, _ := CreateCBCFromKeyString(nistCBCVectors[0].key) + plainBytes, _ := hex.DecodeString(nistPlaintext) + + firstIV, err := GenerateCBCIV() + if err != nil { + t.Fatalf("Did not expect an error but got %q", err) + } + secondIV, err := GenerateCBCIV() + if err != nil { + t.Fatalf("Did not expect an error but got %q", err) + } + + firstCipherBytes, _ := cipher.Encrypt(plainBytes, firstIV) + secondCipherBytes, _ := cipher.Encrypt(plainBytes, secondIV) + + if bytes.Equal(firstCipherBytes, secondCipherBytes) { + t.Error("Expected the same plaintext under different IVs to produce different ciphertext") + } + + for _, testCase := range []struct { + cipherBytes []byte + iv []byte + }{{firstCipherBytes, firstIV}, {secondCipherBytes, secondIV}} { + decryptedBytes, err := cipher.Decrypt(testCase.cipherBytes, testCase.iv) + if err != nil { + t.Errorf("Did not expect a decryption error but got %q", err) + continue + } + if !bytes.Equal(decryptedBytes, plainBytes) { + t.Errorf("Expected %s but get %s", nistPlaintext, hex.EncodeToString(decryptedBytes)) + } + } +} + +func TestGenerateCBCIV(t *testing.T) { + seen := make(map[string]bool) + + for i := 0; i < 32; i++ { + iv, err := GenerateCBCIV() + if err != nil { + t.Fatalf("Did not expect an error but got %q", err) + } + if len(iv) != 16 { + t.Errorf("Expected a 16 byte IV but get %d", len(iv)) + } + + encodedIV := hex.EncodeToString(iv) + if seen[encodedIV] { + t.Errorf("Expected every generated IV to be unique but %s repeated", encodedIV) + } + seen[encodedIV] = true + } +} + +func TestCreateCBCFromKeyBytes(t *testing.T) { + for _, keyLength := range []int{16, 24, 32} { + keyBytes := make([]byte, keyLength) + + cipher, err := CreateCBCFromKeyBytes(keyBytes) + if err != nil { + t.Errorf("Did not expect an error for a %d byte key but got %q", keyLength, err) + continue + } + if !bytes.Equal(cipher.KeyBytes, keyBytes) { + t.Errorf("Expected key %s but get %s", hex.EncodeToString(keyBytes), hex.EncodeToString(cipher.KeyBytes)) + } + } + + for _, keyLength := range []int{0, 8, 15, 17, 23, 25, 31, 33, 64} { + if _, err := CreateCBCFromKeyBytes(make([]byte, keyLength)); err == nil { + t.Errorf("Expected a %d byte key to be rejected", keyLength) + } + } +} + +func TestCreateCBCFromKeyString(t *testing.T) { + cipher, err := CreateCBCFromKeyString(nistCBCVectors[0].key) + if err != nil { + t.Fatalf("Did not expect an error but got %q", err) + } + if hex.EncodeToString(cipher.KeyBytes) != nistCBCVectors[0].key { + t.Errorf("Expected key %s but get %s", nistCBCVectors[0].key, hex.EncodeToString(cipher.KeyBytes)) + } + + if _, err := CreateCBCFromKeyString("not hex"); err == nil { + t.Error("Expected a non hex key to be rejected") + } + if _, err := CreateCBCFromKeyString("2b7e151628aed2a6abf7158809cf4f"); err == nil { + t.Error("Expected a hex key of the wrong length to be rejected") + } +} + +// A KCV depends on the key, not the AES mode. +func TestAESCBCCheckValueMatchesGCMCipher(t *testing.T) { + keyBytes, _ := hex.DecodeString("2b7e151628aed2a6abf7158809cf4f3c") + + cbcCipher, _ := CreateCBCFromKeyBytes(keyBytes) + gcmCipher, _ := CreateFromKeyBytes(keyBytes) + + if cbcCipher.CheckValue() != "7df76b" { + t.Errorf("Expected check value 7df76b but got %s", cbcCipher.CheckValue()) + } + if cbcCipher.CheckValue() != gcmCipher.CheckValue() { + t.Errorf("Expected check value %s but got %s", gcmCipher.CheckValue(), cbcCipher.CheckValue()) + } +} + +func TestAESCBCVerifyCheckValue(t *testing.T) { + cipher, _ := CreateCBCFromKeyString("2b7e151628aed2a6abf7158809cf4f3c") + + if !cipher.VerifyCheckValue("7df76b") { + t.Error("Expected check value to verify successfully") + } + if !cipher.VerifyCheckValue("7DF76B") { + t.Error("Expected check value verification to be case insensitive") + } + if cipher.VerifyCheckValue("abcdef") { + t.Error("Expected wrong check value to fail verification") + } + if cipher.VerifyCheckValue("") { + t.Error("Expected empty check value to fail verification") + } +} diff --git a/aes/aes_cbc_factory.go b/aes/aes_cbc_factory.go new file mode 100644 index 0000000..1f5fb44 --- /dev/null +++ b/aes/aes_cbc_factory.go @@ -0,0 +1,53 @@ +/* + 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 aes + +import ( + "crypto/aes" + "encoding/hex" + "errors" + + "github.com/hashicorp/go-uuid" +) + +// CreateCBCFromKeyBytes creates an AES-CBC cipher from a 16, 24, or 32-byte key. +func CreateCBCFromKeyBytes(keyBytes []byte) (CBCCipher, error) { + if len(keyBytes) != 16 && len(keyBytes) != 24 && len(keyBytes) != 32 { + return CBCCipher{}, errors.New("AES key must be 16, 24, or 32 bytes") + } + + keyBlock, err := aes.NewCipher(keyBytes) + if err != nil { + return CBCCipher{}, errors.New("invalid AES keyBlock") + } + + return CBCCipher{keyBlock, keyBytes}, nil +} + +// CreateCBCFromKeyString creates an AES-CBC cipher from a hex-encoded key. +func CreateCBCFromKeyString(key string) (CBCCipher, error) { + keyBytes, err := hex.DecodeString(key) + if err != nil { + return CBCCipher{}, errors.New("AES key is not in correct hex format") + } + return CreateCBCFromKeyBytes(keyBytes) +} + +// GenerateCBCIV returns a random 16-byte IV. Generate a new IV for each message. +func GenerateCBCIV() ([]byte, error) { + iv, err := uuid.GenerateRandomBytes(aes.BlockSize) + if err != nil { + return nil, errors.New("fail to generate IV") + } + return iv, nil +} diff --git a/aes/aes_cipher.go b/aes/aes_cipher.go index 89e7f13..270d5d9 100644 --- a/aes/aes_cipher.go +++ b/aes/aes_cipher.go @@ -10,7 +10,7 @@ limitations under the License. */ -// Package aes provides wrapper methods on top of the AES GCM cipher for our own usage +// Package aes provides AES-GCM and AES-CBC helpers. package aes import ( @@ -68,7 +68,17 @@ func (cipher *Cipher) Decrypt(cipherBytes []byte, nonce []byte) ([]byte, error) } func (c *Cipher) CheckValue() string { - block, err := aes.NewCipher(c.KeyBytes) + return deriveCheckValue(c.KeyBytes) +} + +func (c *Cipher) VerifyCheckValue(checkValue string) bool { + return verifyCheckValue(c.KeyBytes, checkValue) +} + +// deriveCheckValue encrypts a zero block with AES-ECB and returns the first +// three bytes as hex. It returns an empty string for an invalid key. +func deriveCheckValue(keyBytes []byte) string { + block, err := aes.NewCipher(keyBytes) if err != nil { return "" } @@ -77,12 +87,12 @@ func (c *Cipher) CheckValue() string { return hex.EncodeToString(cipherBytes[:checkValueBytes]) } -func (c *Cipher) VerifyCheckValue(checkValue string) bool { +func verifyCheckValue(keyBytes []byte, checkValue string) bool { if checkValue == "" { return false } - derivedCheckValue := c.CheckValue() + derivedCheckValue := deriveCheckValue(keyBytes) if derivedCheckValue == "" { return false } diff --git a/aes/iso9797m2.go b/aes/iso9797m2.go new file mode 100644 index 0000000..8b87f9b --- /dev/null +++ b/aes/iso9797m2.go @@ -0,0 +1,57 @@ +/* + 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 aes + +import "fmt" + +const iso9797M2Marker = 0x80 + +// padISO9797M2 appends 0x80 and zeroes to the next block boundary. It always +// adds padding and returns a new slice. +func padISO9797M2(plainBytes []byte, blockSize int) []byte { + padLen := blockSize - len(plainBytes)%blockSize + + padded := make([]byte, 0, len(plainBytes)+padLen) + padded = append(padded, plainBytes...) + padded = append(padded, iso9797M2Marker) + padded = append(padded, make([]byte, padLen-1)...) + return padded +} + +// unpadISO9797M2 removes ISO/IEC 9797-1 method 2 padding. It rejects malformed +// padding and returns a new slice. +func unpadISO9797M2(paddedBytes []byte, blockSize int) ([]byte, error) { + if len(paddedBytes) == 0 { + return nil, fmt.Errorf("padded input is empty") + } + if len(paddedBytes)%blockSize != 0 { + return nil, fmt.Errorf("padded input length %d is not a multiplier of block size %d", len(paddedBytes), blockSize) + } + + markerIndex := len(paddedBytes) - 1 + for markerIndex >= 0 && paddedBytes[markerIndex] == 0x00 { + markerIndex-- + } + if markerIndex < 0 || paddedBytes[markerIndex] != iso9797M2Marker { + return nil, fmt.Errorf("invalid padding") + } + + // Valid padding starts within the final block. + if len(paddedBytes)-markerIndex > blockSize { + return nil, fmt.Errorf("invalid padding") + } + + plainBytes := make([]byte, markerIndex) + copy(plainBytes, paddedBytes) + return plainBytes, nil +} diff --git a/aes/iso9797m2_test.go b/aes/iso9797m2_test.go new file mode 100644 index 0000000..2b6a0fc --- /dev/null +++ b/aes/iso9797m2_test.go @@ -0,0 +1,302 @@ +/* + 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 aes + +import ( + "bytes" + "encoding/hex" + "testing" +) + +const testBlockSize = 16 + +func TestISO9797M2Pad(t *testing.T) { + testCases := []struct { + name string + input string + expected string + }{ + { + name: "empty input gains a whole block", + input: "", + expected: "80000000000000000000000000000000", + }, + { + name: "one byte short of a block gains only the marker", + input: "000102030405060708090a0b0c0d0e", + expected: "000102030405060708090a0b0c0d0e80", + }, + { + name: "block aligned input gains a whole extra block", + input: "000102030405060708090a0b0c0d0e0f", + expected: "000102030405060708090a0b0c0d0e0f" + "80000000000000000000000000000000", + }, + { + name: "single byte gains marker and fourteen zeroes", + input: "ff", + expected: "ff800000000000000000000000000000", + }, + { + name: "input ending in the marker byte is still unambiguous", + input: "0080", + expected: "00808000000000000000000000000000", + }, + { + name: "input ending in zeroes is still unambiguous", + input: "ff0000", + expected: "ff000080000000000000000000000000", + }, + { + name: "input spanning more than one block", + input: "000102030405060708090a0b0c0d0e0f" + "1011", + expected: "000102030405060708090a0b0c0d0e0f" + "10118000000000000000000000000000", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + inputBytes := mustDecodeHex(t, testCase.input) + expectedBytes := mustDecodeHex(t, testCase.expected) + + padded := padISO9797M2(inputBytes, testBlockSize) + + if !bytes.Equal(padded, expectedBytes) { + t.Fatalf("padded = %x, want %x", padded, expectedBytes) + } + if len(padded)%testBlockSize != 0 { + t.Fatalf("padded length %d is not a multiple of %d", len(padded), testBlockSize) + } + if len(padded) <= len(inputBytes) { + t.Fatalf("padding must always add at least one byte, input %d padded %d", len(inputBytes), len(padded)) + } + }) + } +} + +func TestISO9797M2PadAlwaysAddsBetweenOneAndBlockSizeBytes(t *testing.T) { + for inputLen := 0; inputLen <= 3*testBlockSize; inputLen++ { + inputBytes := make([]byte, inputLen) + padded := padISO9797M2(inputBytes, testBlockSize) + + padLen := len(padded) - inputLen + if padLen < 1 || padLen > testBlockSize { + t.Fatalf("input length %d: padding of %d bytes is out of range 1..%d", inputLen, padLen, testBlockSize) + } + if padded[inputLen] != iso9797M2Marker { + t.Fatalf("input length %d: marker byte = %#x, want %#x", inputLen, padded[inputLen], iso9797M2Marker) + } + for i, padByte := range padded[inputLen+1:] { + if padByte != 0x00 { + t.Fatalf("input length %d: filler byte %d = %#x, want 0x00", inputLen, i, padByte) + } + } + } +} + +func TestISO9797M2PadDoesNotModifyInput(t *testing.T) { + inputBytes := mustDecodeHex(t, "000102030405060708090a0b0c0d0e0f1011") + original := append([]byte(nil), inputBytes...) + + padISO9797M2(inputBytes, testBlockSize) + + if !bytes.Equal(inputBytes, original) { + t.Fatalf("input was modified: got %x, want %x", inputBytes, original) + } +} + +func TestISO9797M2Unpad(t *testing.T) { + testCases := []struct { + name string + input string + expected string + }{ + { + name: "whole block of padding yields empty output", + input: "80000000000000000000000000000000", + expected: "", + }, + { + name: "marker only", + input: "000102030405060708090a0b0c0d0e80", + expected: "000102030405060708090a0b0c0d0e", + }, + { + name: "extra block of padding after block aligned message", + input: "000102030405060708090a0b0c0d0e0f" + "80000000000000000000000000000000", + expected: "000102030405060708090a0b0c0d0e0f", + }, + { + name: "message ending in the marker byte", + input: "00808000000000000000000000000000", + expected: "0080", + }, + { + name: "message ending in zeroes", + input: "ff000080000000000000000000000000", + expected: "ff0000", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + inputBytes := mustDecodeHex(t, testCase.input) + expectedBytes := mustDecodeHex(t, testCase.expected) + + unpadded, err := unpadISO9797M2(inputBytes, testBlockSize) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !bytes.Equal(unpadded, expectedBytes) { + t.Fatalf("unpadded = %x, want %x", unpadded, expectedBytes) + } + }) + } +} + +func TestISO9797M2UnpadRejectsMalformedPadding(t *testing.T) { + testCases := []struct { + name string + input string + }{ + { + name: "empty input", + input: "", + }, + { + name: "input not a multiple of the block size", + input: "000102030405060708090a0b0c0d0e", + }, + { + name: "no marker at all", + input: "000102030405060708090a0b0c0d0e0f", + }, + { + name: "trailing byte is neither marker nor zero", + input: "000102030405060708090a0b0c0d0e01", + }, + { + name: "all zeroes, so the marker is missing", + input: "00000000000000000000000000000000", + }, + { + name: "final block is all zeroes with the marker too far back", + input: "000102030405060708090a0b0c0d0e80" + "00000000000000000000000000000000", + }, + { + name: "PKCS#7 padding is rejected rather than silently accepted", + input: "000102030405060708090a0b0c0d" + "0202", + }, + { + name: "PKCS#7 whole block of padding is rejected", + input: "10101010101010101010101010101010", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + inputBytes := mustDecodeHex(t, testCase.input) + + unpadded, err := unpadISO9797M2(inputBytes, testBlockSize) + if err == nil { + t.Fatalf("expected an error but got output %x", unpadded) + } + if unpadded != nil { + t.Fatalf("expected nil output alongside the error but got %x", unpadded) + } + }) + } +} + +func TestISO9797M2UnpadDoesNotModifyInput(t *testing.T) { + inputBytes := mustDecodeHex(t, "ff000080000000000000000000000000") + original := append([]byte(nil), inputBytes...) + + if _, err := unpadISO9797M2(inputBytes, testBlockSize); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !bytes.Equal(inputBytes, original) { + t.Fatalf("input was modified: got %x, want %x", inputBytes, original) + } +} + +func TestISO9797M2UnpadReturnsCopyOfInput(t *testing.T) { + inputBytes := mustDecodeHex(t, "ff112280000000000000000000000000") + + unpadded, err := unpadISO9797M2(inputBytes, testBlockSize) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + unpadded[0] = 0x00 + if inputBytes[0] != 0xff { + t.Fatal("mutating the returned slice also mutated the input") + } +} + +func TestISO9797M2RoundTrip(t *testing.T) { + for inputLen := 0; inputLen <= 3*testBlockSize; inputLen++ { + inputBytes := make([]byte, inputLen) + for i := range inputBytes { + inputBytes[i] = byte(i) + } + + padded := padISO9797M2(inputBytes, testBlockSize) + unpadded, err := unpadISO9797M2(padded, testBlockSize) + if err != nil { + t.Fatalf("input length %d: unexpected error: %v", inputLen, err) + } + if !bytes.Equal(unpadded, inputBytes) { + t.Fatalf("input length %d: round trip gave %x, want %x", inputLen, unpadded, inputBytes) + } + } +} + +func TestISO9797M2RoundTripWithTrailingMarkerAndZeroBytes(t *testing.T) { + // Padding remains unambiguous when plaintext ends in 0x80 or 0x00. + testCases := []string{ + "80", + "00", + "8000", + "0080", + "800000000000000000000000000000", + "00000000000000000000000000000080", + "ff" + "00000000000000000000000000000000", + "ff" + "80000000000000000000000000000000", + } + + for _, testCase := range testCases { + t.Run(testCase, func(t *testing.T) { + inputBytes := mustDecodeHex(t, testCase) + + padded := padISO9797M2(inputBytes, testBlockSize) + unpadded, err := unpadISO9797M2(padded, testBlockSize) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !bytes.Equal(unpadded, inputBytes) { + t.Fatalf("round trip gave %x, want %x", unpadded, inputBytes) + } + }) + } +} + +func mustDecodeHex(t *testing.T, s string) []byte { + t.Helper() + + decoded, err := hex.DecodeString(s) + if err != nil { + t.Fatalf("could not decode hex %q: %v", s, err) + } + return decoded +}