From e18b6d1c9b5ff6e93e729d84826cd5c00ec997b1 Mon Sep 17 00:00:00 2001 From: Fernando Lobato Date: Fri, 13 Dec 2024 09:42:10 -0800 Subject: [PATCH] Improve the performance of AES-CTR-HMAC by avoiding unnecessary allocations and copies. PiperOrigin-RevId: 705911944 Change-Id: I3a6a2deb8ae12f89454e95bbb9526f10fdce74da --- aead/aesctrhmac/aead.go | 74 ++++++--- aead/aesctrhmac/key_manager.go | 36 +++-- aead/subtle/aes_ctr.go | 74 +-------- internal/aead/aesctr.go | 108 +++++++++++++ internal/aead/aesctr_test.go | 269 +++++++++++++++++++++++++++++++++ internal/mac/hmac/hmac.go | 104 +++++++++++++ internal/mac/hmac/hmac_test.go | 208 +++++++++++++++++++++++++ mac/subtle/hmac.go | 64 ++------ mac/subtle/hmac_test.go | 14 -- 9 files changed, 783 insertions(+), 168 deletions(-) create mode 100644 internal/aead/aesctr.go create mode 100644 internal/aead/aesctr_test.go create mode 100644 internal/mac/hmac/hmac.go create mode 100644 internal/mac/hmac/hmac_test.go diff --git a/aead/aesctrhmac/aead.go b/aead/aesctrhmac/aead.go index 26fd666..3b4b2de 100644 --- a/aead/aesctrhmac/aead.go +++ b/aead/aesctrhmac/aead.go @@ -16,18 +16,22 @@ package aesctrhmac import ( "bytes" + "encoding/binary" + "errors" "fmt" - "slices" - "github.com/tink-crypto/tink-go/v2/aead/subtle" "github.com/tink-crypto/tink-go/v2/insecuresecretdataaccess" + "github.com/tink-crypto/tink-go/v2/internal/aead" + "github.com/tink-crypto/tink-go/v2/internal/mac/hmac" "github.com/tink-crypto/tink-go/v2/key" - subtlemac "github.com/tink-crypto/tink-go/v2/mac/subtle" "github.com/tink-crypto/tink-go/v2/tink" ) type fullAEAD struct { - aead *subtle.EncryptThenAuthenticate + aesCTR *aead.AESCTR + hmac *hmac.HMAC + ivSize int + tagSize int prefix []byte variant Variant } @@ -35,43 +39,75 @@ type fullAEAD struct { var _ tink.AEAD = (*fullAEAD)(nil) func newAEAD(key *Key) (tink.AEAD, error) { - aesCTR, err := subtle.NewAESCTR(key.AESKeyBytes().Data(insecuresecretdataaccess.Token{}), key.parameters.IVSizeInBytes()) + tagSize := key.parameters.TagSizeInBytes() + ivSize := key.parameters.IVSizeInBytes() + aesCTR, err := aead.NewAESCTR(key.AESKeyBytes().Data(insecuresecretdataaccess.Token{}), ivSize) if err != nil { return nil, err } - hmac, err := subtlemac.NewHMAC(key.parameters.HashType().String(), key.HMACKeyBytes().Data(insecuresecretdataaccess.Token{}), uint32(key.parameters.TagSizeInBytes())) - if err != nil { - return nil, err - } - eta, err := subtle.NewEncryptThenAuthenticate(aesCTR, hmac, key.parameters.TagSizeInBytes()) + hmac, err := hmac.New(key.parameters.HashType().String(), key.HMACKeyBytes().Data(insecuresecretdataaccess.Token{}), uint32(tagSize)) if err != nil { return nil, err } return &fullAEAD{ - aead: eta, + aesCTR: aesCTR, + hmac: hmac, + ivSize: ivSize, + tagSize: tagSize, prefix: key.OutputPrefix(), variant: key.parameters.Variant(), }, nil } +func aadSizeInBits(associatedData []byte) []byte { + n := uint64(len(associatedData)) * 8 + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, n) + return buf +} + +// Encrypt encrypts plaintext with associatedData. +// +// The plaintext is encrypted with an AES-CTR, then HMAC is computed over +// (associatedData || ciphertext || n) where n is associatedData's length +// in bits represented as a 64-bit big endian unsigned integer. The final +// ciphertext format is (IND-CPA ciphertext || mac). func (a *fullAEAD) Encrypt(plaintext, associatedData []byte) ([]byte, error) { - ciphertext, err := a.aead.Encrypt(plaintext, associatedData) + ctSize := len(a.prefix) + a.ivSize + len(plaintext) + ciphertext := make([]byte, ctSize, ctSize+a.tagSize) + if n := copy(ciphertext, a.prefix); n != len(a.prefix) { + return nil, fmt.Errorf("aesctrhmac: failed to copy prefix") + } + ctNoPrefix, err := a.aesCTR.Encrypt(ciphertext[len(a.prefix):], plaintext) + if err != nil { + return nil, err + } + tag, err := a.hmac.ComputeMAC(associatedData, ctNoPrefix, aadSizeInBits(associatedData)) if err != nil { return nil, err } - return slices.Concat(a.prefix, ciphertext), nil + if len(tag) != a.tagSize { + return nil, errors.New("aesctrhmac: invalid tag size") + } + return append(ciphertext, tag...), nil } +// Decrypt decrypts ciphertext with associatedData. func (a *fullAEAD) Decrypt(ciphertext, associatedData []byte) ([]byte, error) { - if len(ciphertext) < len(a.prefix) { - return nil, fmt.Errorf("ciphertext with size %d is too short", len(ciphertext)) + prefixSize := len(a.prefix) + if len(ciphertext) < prefixSize+a.ivSize+a.tagSize { + return nil, fmt.Errorf("aesctrhmac: ciphertext with size %d is too short", len(ciphertext)) } - prefix := ciphertext[:len(a.prefix)] - ciphertextNoPrefix := ciphertext[len(a.prefix):] + prefix := ciphertext[:prefixSize] if !bytes.Equal(prefix, a.prefix) { - return nil, fmt.Errorf("ciphertext prefix does not match: got %x, want %x", prefix, a.prefix) + return nil, fmt.Errorf("aesctrhmac: ciphertext prefix does not match: got %x, want %x", prefix, a.prefix) + } + payload := ciphertext[prefixSize : len(ciphertext)-a.tagSize] + tag := ciphertext[len(ciphertext)-a.tagSize:] + if err := a.hmac.VerifyMAC(tag, associatedData, payload, aadSizeInBits(associatedData)); err != nil { + return nil, fmt.Errorf("aesctrhmac: %v", err) } - return a.aead.Decrypt(ciphertextNoPrefix, associatedData) + return a.aesCTR.Decrypt(nil, payload) } // primitiveConstructor creates a [tink.AEAD] from a [key.Key]. diff --git a/aead/aesctrhmac/key_manager.go b/aead/aesctrhmac/key_manager.go index 69be2eb..03bf422 100644 --- a/aead/aesctrhmac/key_manager.go +++ b/aead/aesctrhmac/key_manager.go @@ -20,8 +20,9 @@ import ( "google.golang.org/protobuf/proto" "github.com/tink-crypto/tink-go/v2/aead/subtle" + "github.com/tink-crypto/tink-go/v2/insecuresecretdataaccess" "github.com/tink-crypto/tink-go/v2/keyset" - subtleMac "github.com/tink-crypto/tink-go/v2/mac/subtle" + "github.com/tink-crypto/tink-go/v2/secretdata" "github.com/tink-crypto/tink-go/v2/subtle/random" ctrpb "github.com/tink-crypto/tink-go/v2/proto/aes_ctr_go_proto" aeadpb "github.com/tink-crypto/tink-go/v2/proto/aes_ctr_hmac_aead_go_proto" @@ -44,28 +45,35 @@ type keyManager struct{} // Primitive creates a [subtle.NewEncryptThenAuthenticate] primitive for the given // serialized [aeadpb.AesCtrHmacAeadKey]. func (km *keyManager) Primitive(serializedKey []byte) (any, error) { - key := new(aeadpb.AesCtrHmacAeadKey) - if err := proto.Unmarshal(serializedKey, key); err != nil { + protoKey := new(aeadpb.AesCtrHmacAeadKey) + if err := proto.Unmarshal(serializedKey, protoKey); err != nil { return nil, fmt.Errorf("aes_ctr_hmac_aead_key_manager: invalid key") } - if err := km.validateKey(key); err != nil { + if err := km.validateKey(protoKey); err != nil { return nil, err } - - ctr, err := subtle.NewAESCTR(key.GetAesCtrKey().GetKeyValue(), int(key.GetAesCtrKey().GetParams().GetIvSize())) + params, err := NewParameters(ParametersOpts{ + AESKeySizeInBytes: int(len(protoKey.GetAesCtrKey().GetKeyValue())), + HMACKeySizeInBytes: int(len(protoKey.GetHmacKey().GetKeyValue())), + IVSizeInBytes: int(protoKey.GetAesCtrKey().GetParams().GetIvSize()), + TagSizeInBytes: int(protoKey.GetHmacKey().GetParams().GetTagSize()), + HashType: HashType(protoKey.GetHmacKey().GetParams().GetHash()), + Variant: VariantNoPrefix, + }) if err != nil { - return nil, fmt.Errorf("aes_ctr_hmac_aead_key_manager: cannot create new primitive: %v", err) + return nil, fmt.Errorf("aes_ctr_hmac_aead_key_manager: %s", err) } - - hmacKey := key.GetHmacKey() - hmac, err := subtleMac.NewHMAC(hmacKey.GetParams().GetHash().String(), hmacKey.GetKeyValue(), hmacKey.GetParams().GetTagSize()) + key, err := NewKey(KeyOpts{ + AESKeyBytes: secretdata.NewBytesFromData(protoKey.GetAesCtrKey().GetKeyValue(), insecuresecretdataaccess.Token{}), + HMACKeyBytes: secretdata.NewBytesFromData(protoKey.GetHmacKey().GetKeyValue(), insecuresecretdataaccess.Token{}), + Parameters: params, + }) if err != nil { - return nil, fmt.Errorf("aes_ctr_hmac_aead_key_manager: cannot create mac primitive, error: %v", err) + return nil, fmt.Errorf("aes_ctr_hmac_aead_key_manager: %s", err) } - - aead, err := subtle.NewEncryptThenAuthenticate(ctr, hmac, int(hmacKey.GetParams().GetTagSize())) + aead, err := newAEAD(key) if err != nil { - return nil, fmt.Errorf("aes_ctr_hmac_aead_key_manager: cannot create encrypt then authenticate primitive, error: %v", err) + return nil, fmt.Errorf("aes_ctr_hmac_aead_key_manager: %s", err) } return aead, nil } diff --git a/aead/subtle/aes_ctr.go b/aead/subtle/aes_ctr.go index 330073a..e0d2b2e 100644 --- a/aead/subtle/aes_ctr.go +++ b/aead/subtle/aes_ctr.go @@ -15,12 +15,9 @@ package subtle import ( - "crypto/aes" - "crypto/cipher" - "fmt" + "github.com/tink-crypto/tink-go/v2/internal/aead" // Placeholder for internal crypto/cipher allowlist, please ignore. - "github.com/tink-crypto/tink-go/v2/subtle/random" ) const ( @@ -30,7 +27,7 @@ const ( // AESCTR is an implementation of AEAD interface. type AESCTR struct { - key []byte + ctr *aead.AESCTR IVSize int } @@ -39,76 +36,21 @@ type AESCTR struct { // AES-128 or AES-256. // ivSize specifies the size of the IV in bytes. func NewAESCTR(key []byte, ivSize int) (*AESCTR, error) { - keySize := uint32(len(key)) - if err := ValidateAESKeySize(keySize); err != nil { - return nil, fmt.Errorf("aes_ctr: %s", err) - } - if ivSize < AESCTRMinIVSize || ivSize > aes.BlockSize { - return nil, fmt.Errorf("aes_ctr: invalid IV size: %d", ivSize) + ctr, err := aead.NewAESCTR(key, ivSize) + if err != nil { + return nil, err } - return &AESCTR{key: key, IVSize: ivSize}, nil + return &AESCTR{ctr: ctr, IVSize: ivSize}, nil } // Encrypt encrypts plaintext using AES in CTR mode. // The resulting ciphertext consists of two parts: // (1) the IV used for encryption and (2) the actual ciphertext. func (a *AESCTR) Encrypt(plaintext []byte) ([]byte, error) { - if len(plaintext) > maxInt-a.IVSize { - return nil, fmt.Errorf("aes_ctr: plaintext too long") - } - iv := a.newIV() - stream, err := newCipher(a.key, iv) - if err != nil { - return nil, err - } - - ciphertext := make([]byte, a.IVSize+len(plaintext)) - if n := copy(ciphertext, iv); n != a.IVSize { - return nil, fmt.Errorf("aes_ctr: failed to copy IV (copied %d/%d bytes)", n, a.IVSize) - } - - stream.XORKeyStream(ciphertext[a.IVSize:], plaintext) - return ciphertext, nil + return a.ctr.Encrypt(nil, plaintext) } // Decrypt decrypts ciphertext. func (a *AESCTR) Decrypt(ciphertext []byte) ([]byte, error) { - if len(ciphertext) < a.IVSize { - return nil, fmt.Errorf("aes_ctr: ciphertext too short") - } - - iv := ciphertext[:a.IVSize] - stream, err := newCipher(a.key, iv) - if err != nil { - return nil, err - } - - plaintext := make([]byte, len(ciphertext)-a.IVSize) - stream.XORKeyStream(plaintext, ciphertext[a.IVSize:]) - return plaintext, nil -} - -// newIV creates a new IV for encryption. -func (a *AESCTR) newIV() []byte { - return random.GetRandomBytes(uint32(a.IVSize)) -} - -// newCipher creates a new AES-CTR cipher using the given key, IV and the crypto library. -func newCipher(key, iv []byte) (cipher.Stream, error) { - block, err := aes.NewCipher(key) - if err != nil { - return nil, fmt.Errorf("aes_ctr: failed to create block cipher, error: %v", err) - } - - // If the IV is less than BlockSize bytes we need to pad it with zeros - // otherwise NewCTR will panic. - if len(iv) < aes.BlockSize { - paddedIV := make([]byte, aes.BlockSize) - if n := copy(paddedIV, iv); n != len(iv) { - return nil, fmt.Errorf("aes_ctr: failed to pad IV") - } - return cipher.NewCTR(block, paddedIV), nil - } - - return cipher.NewCTR(block, iv), nil + return a.ctr.Decrypt(nil, ciphertext) } diff --git a/internal/aead/aesctr.go b/internal/aead/aesctr.go new file mode 100644 index 0000000..ffcbd86 --- /dev/null +++ b/internal/aead/aesctr.go @@ -0,0 +1,108 @@ +// Copyright 2024 Google LLC +// +// 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 aead + +import ( + "crypto/aes" + "crypto/cipher" + "fmt" + + "github.com/tink-crypto/tink-go/v2/subtle/random" +) + +const ( + aesCTRMinIVSize = 12 +) + +// AESCTR is an implementation of IndCpa Interface. +type AESCTR struct { + key []byte + ivSize int +} + +// NewAESCTR returns an instance of [AESCTR] unauthenticated encryption. +func NewAESCTR(key []byte, ivSize int) (*AESCTR, error) { + keySize := uint32(len(key)) + if err := ValidateAESKeySize(keySize); err != nil { + return nil, fmt.Errorf("aes_ctr: %s", err) + } + if ivSize < aesCTRMinIVSize || ivSize > aes.BlockSize { + return nil, fmt.Errorf("aes_ctr: invalid IV size: %d", ivSize) + } + return &AESCTR{key: key, ivSize: ivSize}, nil +} + +// Encrypt encrypts plaintext using AES in CTR mode. +func (a *AESCTR) Encrypt(dst, plaintext []byte) ([]byte, error) { + if len(plaintext) > maxInt-a.ivSize { + return nil, fmt.Errorf("aes_ctr: plaintext too long") + } + ctSize := len(plaintext) + a.ivSize + if len(dst) == 0 { + dst = make([]byte, ctSize) + } + if len(dst) < ctSize { + return nil, fmt.Errorf("aes_ctr: destination buffer too small (%d vs %d)", len(dst), ctSize) + } + + iv := random.GetRandomBytes(uint32(a.ivSize)) + stream, err := newCipher(a.key, iv) + if err != nil { + return nil, err + } + if n := copy(dst, iv); n != a.ivSize { + return nil, fmt.Errorf("aes_ctr: failed to copy IV (copied %d/%d bytes)", n, a.ivSize) + } + stream.XORKeyStream(dst[a.ivSize:], plaintext) + return dst, nil +} + +// Decrypt decrypts ciphertext in the format (prefix || iv || ciphertext). +func (a *AESCTR) Decrypt(dst, ciphertext []byte) ([]byte, error) { + if len(ciphertext) < a.ivSize { + return nil, fmt.Errorf("aes_ctr: ciphertext too short") + } + ptSize := len(ciphertext) - a.ivSize + if len(dst) == 0 { + dst = make([]byte, ptSize) + } + if len(dst) < ptSize { + return nil, fmt.Errorf("aes_ctr: destination buffer too small (%d vs %d)", len(dst), ptSize) + } + stream, err := newCipher(a.key, ciphertext[:a.ivSize]) + if err != nil { + return nil, err + } + stream.XORKeyStream(dst, ciphertext[a.ivSize:]) + return dst, nil +} + +func newCipher(key, iv []byte) (cipher.Stream, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("aes_ctr: failed to create block cipher, error: %v", err) + } + + // If the IV is less than BlockSize bytes we need to pad it with zeros + // otherwise NewCTR will panic. + if len(iv) < aes.BlockSize { + paddedIV := make([]byte, aes.BlockSize) + if n := copy(paddedIV, iv); n != len(iv) { + return nil, fmt.Errorf("aes_ctr: failed to pad IV") + } + iv = paddedIV + } + return cipher.NewCTR(block, iv), nil +} diff --git a/internal/aead/aesctr_test.go b/internal/aead/aesctr_test.go new file mode 100644 index 0000000..956b03d --- /dev/null +++ b/internal/aead/aesctr_test.go @@ -0,0 +1,269 @@ +// Copyright 2024 Google LLC +// +// 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 aead_test + +import ( + "bytes" + "crypto/aes" + "encoding/hex" + "strings" + "testing" + + "github.com/tink-crypto/tink-go/v2/aead/subtle" + "github.com/tink-crypto/tink-go/v2/internal/aead" + "github.com/tink-crypto/tink-go/v2/subtle/random" +) + +func TestNewAESCTR(t *testing.T) { + key := make([]byte, 64) + + // Test various key sizes with a fixed IV size. + for i := 0; i < 64; i++ { + k := key[:i] + _, err := aead.NewAESCTR(k, subtle.AESCTRMinIVSize) + switch len(k) { + case 16: + fallthrough + case 32: + // Valid key sizes. + if err != nil { + t.Errorf("want: valid cipher (key size=%d), got: error %v", len(k), err) + } + default: + // Invalid key sizes. + if !strings.Contains(err.Error(), "aes_ctr: invalid AES key size; want 16 or 32") { + t.Errorf("wrong error message; want a string starting with \"aes_ctr: invalid AES key size; want 16 or 32\", got %v", err) + } + } + } + + // Test different IV sizes with a fixed key. + for i := 0; i < 64; i++ { + k := key[:16] + _, err := aead.NewAESCTR(k, i) + if i >= subtle.AESCTRMinIVSize && i <= aes.BlockSize { + if err != nil { + t.Errorf("want: valid cipher (IV size=%d), got: error %v", i, err) + } + continue + } + if !strings.Contains(err.Error(), "aes_ctr: invalid IV size:") { + t.Errorf("want: error invalid IV size, got: %v", err) + } + } +} + +func TestNistTestVector(t *testing.T) { + // NIST SP 800-38A pp 55 + key, err := hex.DecodeString("2b7e151628aed2a6abf7158809cf4f3c") + if err != nil { + t.Fatalf("failed to hex decode key, error: %v", err) + } + + // NIST IV + iv := "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff" + // NIST ciphertext blocks + c := "874d6191b620e3261bef6864990db6ce" + + "9806f66b7970fdff8617187bb9fffdff" + + "5ae4df3edbd5d35e5b4f09020db03eab" + + "1e031dda2fbe03d1792170a0f3009cee" + ciphertext, err := hex.DecodeString(iv + c) + if err != nil { + t.Fatalf("failed to hex decode ciphertext, error: %v", err) + } + + // NIST plaintext blocks + p := "6bc1bee22e409f96e93d7e117393172a" + + "ae2d8a571e03ac9c9eb76fac45af8e51" + + "30c81c46a35ce411e5fbc1191a0a52ef" + + "f69f2445df4f9b17ad2b417be66c3710" + message, err := hex.DecodeString(p) + if err != nil { + t.Fatalf("failed to hex decode message, error: %v", err) + } + + stream, err := aead.NewAESCTR(key, len(iv)/2) + if err != nil { + t.Fatalf("failed to create AESCTR instance, error: %v", err) + } + + plaintext, err := stream.Decrypt(nil, ciphertext) + if err != nil { + t.Errorf("failed to decrypt ciphertext, error: %v", err) + } + + if !bytes.Equal(plaintext, message) { + t.Errorf("plaintext doesn't match message") + } +} + +func TestMultipleEncrypt(t *testing.T) { + key := random.GetRandomBytes(16) + + stream, err := aead.NewAESCTR(key, subtle.AESCTRMinIVSize) + if err != nil { + t.Fatalf("failed to create AESCTR instance, error: %v", err) + } + + plaintext := []byte("Some data to encrypt.") + ct1, err := stream.Encrypt(nil, plaintext) + if err != nil { + t.Errorf("encryption failed, error: %v", err) + } + ct2, err := stream.Encrypt(nil, plaintext) + if err != nil { + t.Errorf("encryption failed, error: %v", err) + } + if bytes.Equal(ct1, ct2) { + t.Error("the two ciphertexts cannot be equal") + } + // Encrypt 100 times and verify that the result is 100 different ciphertexts. + ciphertexts := map[string]bool{} + for i := 0; i < 100; i++ { + c, err := stream.Encrypt(nil, plaintext) + if err != nil { + t.Errorf("encryption failed for iteration %d, error: %v", i, err) + } + ciphertexts[string(c)] = true + } + + if len(ciphertexts) != 100 { + t.Errorf("got: %d ciphertexts, want: 100 ciphertexts", len(ciphertexts)) + } +} + +func TestEncryptDecrypt(t *testing.T) { + key, err := hex.DecodeString("000102030405060708090a0b0c0d0e0f") + if err != nil { + t.Fatal("failed to hex decode key") + } + + stream, err := aead.NewAESCTR(key, subtle.AESCTRMinIVSize) + if err != nil { + t.Fatalf("failed to get AESCTR instance, error: %v", err) + } + + message := []byte("Some data to encrypt.") + ciphertext, err := stream.Encrypt(nil, message) + if err != nil { + t.Errorf("encryption failed, error: %v", err) + } + + if len(ciphertext) != len(message)+subtle.AESCTRMinIVSize { + t.Errorf("ciphertext incorrect size, got: %d, want: %d", len(ciphertext), len(message)+subtle.AESCTRMinIVSize) + } + + plaintext, err := stream.Decrypt(nil, ciphertext) + if err != nil { + t.Errorf("decryption failed, error: %v", err) + } + + if !bytes.Equal(message, plaintext) { + t.Errorf("decryption result mismatch, got: %v, want: %v", plaintext, message) + } +} + +func TestEncryptRandomMessage(t *testing.T) { + key := random.GetRandomBytes(16) + + stream, err := aead.NewAESCTR(key, subtle.AESCTRMinIVSize) + if err != nil { + t.Errorf("failed to instantiate AESCTR, error: %v", err) + } + + for i := 0; i < 256; i++ { + message := random.GetRandomBytes(uint32(i)) + ciphertext, err := stream.Encrypt(nil, message) + if err != nil { + t.Errorf("encryption failed at iteration %d, error: %v", i, err) + } + if len(ciphertext) != len(message)+subtle.AESCTRMinIVSize { + t.Errorf("invalid ciphertext length for i = %d", i) + } + + plaintext, err := stream.Decrypt(nil, ciphertext) + if err != nil { + t.Errorf("decryption failed at iteration %d, error: %v", i, err) + } + + if !bytes.Equal(plaintext, message) { + t.Errorf("plaintext doesn't match message, i = %d", i) + } + } +} + +func TestEncryptRandomKeyAndMessage(t *testing.T) { + for i := 0; i < 256; i++ { + key := random.GetRandomBytes(16) + + stream, err := aead.NewAESCTR(key, subtle.AESCTRMinIVSize) + if err != nil { + t.Errorf("failed to instantiate AESCTR, error: %v", err) + } + + message := random.GetRandomBytes(uint32(i)) + ciphertext, err := stream.Encrypt(nil, message) + if err != nil { + t.Errorf("encryption failed at iteration %d, error: %v", i, err) + } + if len(ciphertext) != len(message)+subtle.AESCTRMinIVSize { + t.Errorf("invalid ciphertext length for i = %d", i) + } + + plaintext, err := stream.Decrypt(nil, ciphertext) + if err != nil { + t.Errorf("decryption failed at iteration %d, error: %v", i, err) + } + + if !bytes.Equal(plaintext, message) { + t.Errorf("plaintext doesn't match message, i = %d", i) + } + } +} + +func TestEncryptDecryptWithDestinationBuffer(t *testing.T) { + aesCTR, err := aead.NewAESCTR(random.GetRandomBytes(16), subtle.AESCTRMinIVSize) + if err != nil { + t.Fatalf("NewAESCTR failed, error: %v, want nil", err) + } + pt := random.GetRandomBytes(16) + ct := make([]byte, len(pt)+subtle.AESCTRMinIVSize) + if _, err = aesCTR.Encrypt(ct, pt); err != nil { + t.Fatalf("Encrypt failed, error: %v, want nil", err) + } + rpt := make([]byte, len(pt)) + if _, err := aesCTR.Decrypt(rpt, ct); err != nil { + t.Fatalf("Decrypt failed, error: %v, want nil", err) + } + if !bytes.Equal(pt, rpt) { + t.Errorf("recovered plaintext doesn't match original plaintext, got: %v, want: %v", hex.EncodeToString(pt), hex.EncodeToString(rpt)) + } +} + +func TestEncryptDecryptWithInvalidDestinationBufferFails(t *testing.T) { + aesCTR, err := aead.NewAESCTR(random.GetRandomBytes(16), subtle.AESCTRMinIVSize) + if err != nil { + t.Fatalf("NewAESCTR failed, error: %v, want nil", err) + } + pt := random.GetRandomBytes(16) + ct := make([]byte, len(pt)+subtle.AESCTRMinIVSize) + if _, err = aesCTR.Encrypt(ct[len(ct)-1:], pt); err == nil { + t.Fatal("Encrypt() succeeded, want error", err) + } + rpt := make([]byte, len(pt)-1) + if _, err := aesCTR.Decrypt(rpt, ct); err == nil { + t.Fatal("Decrypt() succeeded, want error", err) + } +} diff --git a/internal/mac/hmac/hmac.go b/internal/mac/hmac/hmac.go new file mode 100644 index 0000000..ba1f107 --- /dev/null +++ b/internal/mac/hmac/hmac.go @@ -0,0 +1,104 @@ +// Copyright 2024 Google LLC +// +// 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 hmac implements the hmac algorithm. +package hmac + +import ( + "crypto/hmac" + "errors" + "fmt" + "hash" + + "github.com/tink-crypto/tink-go/v2/subtle" +) + +const ( + // Minimum key size in bytes. + minKeySizeInBytes = uint32(16) + + // Minimum tag size in bytes. This provides minimum 80-bit security strength. + minTagSizeInBytes = uint32(10) +) + +// HMAC implements the MAC interface. +type HMAC struct { + HashFunc func() hash.Hash + key []byte + tagSize uint32 +} + +// ValidateHMACParams validates parameters of HMAC constructor. +func ValidateHMACParams(hash string, keySize uint32, tagSize uint32) error { + // validate tag size + digestSize, err := subtle.GetHashDigestSize(hash) + if err != nil { + return err + } + if tagSize > digestSize { + return fmt.Errorf("tag size too big") + } + if tagSize < minTagSizeInBytes { + return fmt.Errorf("tag size too small") + } + // validate key size + if keySize < minKeySizeInBytes { + return fmt.Errorf("key too short") + } + return nil +} + +// New returns a new HMAC instance. +func New(hashAlg string, key []byte, tagSize uint32) (*HMAC, error) { + if err := ValidateHMACParams(hashAlg, uint32(len(key)), tagSize); err != nil { + return nil, err + } + hashFunc := subtle.GetHashFunc(hashAlg) + if hashFunc == nil { + return nil, fmt.Errorf("hmac: invalid hash algorithm") + } + return &HMAC{ + HashFunc: hashFunc, + key: key, + tagSize: tagSize, + }, nil +} + +// ComputeMAC computes message authentication code (MAC) for the given data. +func (h *HMAC) ComputeMAC(data ...[]byte) ([]byte, error) { + if h.HashFunc == nil { + return nil, fmt.Errorf("hmac: invalid hash algorithm") + } + mac := hmac.New(h.HashFunc, h.key) + for _, d := range data { + if _, err := mac.Write(d); err != nil { + return nil, fmt.Errorf("hmac: failed to write data: %v", err) + } + } + tag := mac.Sum(nil) + return tag[:h.tagSize], nil +} + +// VerifyMAC verifies whether the given MAC is a correct message authentication +// code (MAC) the given data. +func (h *HMAC) VerifyMAC(mac []byte, data ...[]byte) error { + expectedMAC, err := h.ComputeMAC(data...) + if err != nil { + return err + } + if hmac.Equal(expectedMAC, mac) { + return nil + } + return errors.New("HMAC: invalid MAC") +} diff --git a/internal/mac/hmac/hmac_test.go b/internal/mac/hmac/hmac_test.go new file mode 100644 index 0000000..b8b7150 --- /dev/null +++ b/internal/mac/hmac/hmac_test.go @@ -0,0 +1,208 @@ +// Copyright 2024 Google LLC +// +// 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 hmac_test + +import ( + "encoding/hex" + "strings" + "testing" + + "github.com/tink-crypto/tink-go/v2/internal/mac/hmac" + "github.com/tink-crypto/tink-go/v2/subtle/random" +) + +var key, _ = hex.DecodeString("000102030405060708090a0b0c0d0e0f") +var data = []byte("Hello") +var hmacTests = []struct { + desc string + hashAlg string + tagSize uint32 + key []byte + data []byte + expectedMac string +}{ + { + desc: "with SHA256 and 32 byte tag", + hashAlg: "SHA256", + tagSize: 32, + data: data, + key: key, + expectedMac: "e0ff02553d9a619661026c7aa1ddf59b7b44eac06a9908ff9e19961d481935d4", + }, + { + desc: "with SHA512 and 64 byte tag", + hashAlg: "SHA512", + tagSize: 64, + data: data, + key: key, + expectedMac: "481e10d823ba64c15b94537a3de3f253c16642451ac45124dd4dde120bf1e5c15" + + "e55487d55ba72b43039f235226e7954cd5854b30abc4b5b53171a4177047c9b", + }, + // empty data + { + desc: "empty data", + hashAlg: "SHA256", + tagSize: 32, + data: []byte{}, + key: key, + expectedMac: "07eff8b326b7798c9ccfcbdbe579489ac785a7995a04618b1a2813c26744777d", + }, +} + +func TestHMACBasic(t *testing.T) { + for _, test := range hmacTests { + t.Run(test.desc, func(t *testing.T) { + cipher, err := hmac.New(test.hashAlg, test.key, test.tagSize) + if err != nil { + t.Fatalf("hmac.New() err = %q, want nil", err) + } + mac, err := cipher.ComputeMAC(test.data) + if err != nil { + t.Fatalf("cipher.ComputeMAC() err = %q, want nil", err) + } + if hex.EncodeToString(mac) != test.expectedMac { + t.Errorf("hex.EncodeToString(mac) = %q, want %q", + hex.EncodeToString(mac), test.expectedMac) + } + if err := cipher.VerifyMAC(mac, test.data); err != nil { + t.Errorf("cipher.VerifyMAC() err = %q, want nil", err) + } + }) + } +} + +func TestNewHMACWithInvalidInput(t *testing.T) { + // invalid hash algorithm + _, err := hmac.New("MD5", random.GetRandomBytes(16), 32) + if err == nil || !strings.Contains(err.Error(), "invalid hash algorithm") { + t.Errorf("expect an error when hash algorithm is invalid") + } + // key too short + _, err = hmac.New("SHA256", random.GetRandomBytes(1), 32) + if err == nil || !strings.Contains(err.Error(), "key too short") { + t.Errorf("expect an error when key is too short") + } + // tag too short + _, err = hmac.New("SHA256", random.GetRandomBytes(16), 9) + if err == nil || !strings.Contains(err.Error(), "tag size too small") { + t.Errorf("expect an error when tag size is too small") + } + // tag too big + _, err = hmac.New("SHA1", random.GetRandomBytes(16), 21) + if err == nil || !strings.Contains(err.Error(), "tag size too big") { + t.Errorf("expect an error when tag size is too big") + } + _, err = hmac.New("SHA256", random.GetRandomBytes(16), 33) + if err == nil || !strings.Contains(err.Error(), "tag size too big") { + t.Errorf("expect an error when tag size is too big") + } + _, err = hmac.New("SHA512", random.GetRandomBytes(16), 65) + if err == nil || !strings.Contains(err.Error(), "tag size too big") { + t.Errorf("expect an error when tag size is too big") + } +} + +func TestHMACWithNilHashFunc(t *testing.T) { + cipher, err := hmac.New("SHA256", random.GetRandomBytes(32), 32) + if err != nil { + t.Fatalf("hmac.New() err = %v", err) + } + + // Modify exported field. + cipher.HashFunc = nil + + if _, err := cipher.ComputeMAC([]byte{}); err == nil { + t.Errorf("cipher.ComputerMAC() err = nil, want not nil") + } +} + +func TestHMAComputeVerifyWithNilInput(t *testing.T) { + cipher, err := hmac.New("SHA256", random.GetRandomBytes(16), 32) + if err != nil { + t.Errorf("unexpected error when creating new HMAC") + } + tag, err := cipher.ComputeMAC(nil) + if err != nil { + t.Errorf("cipher.ComputeMAC(nil) failed: %v", err) + } + if err := cipher.VerifyMAC(tag, nil); err != nil { + t.Errorf("cipher.VerifyMAC(tag, nil) failed: %v", err) + } +} + +func TestVerifyMACWithInvalidInput(t *testing.T) { + cipher, err := hmac.New("SHA256", random.GetRandomBytes(16), 32) + if err != nil { + t.Errorf("unexpected error when creating new HMAC") + } + if err := cipher.VerifyMAC(nil, []byte{1}); err == nil { + t.Errorf("expect an error when mac is nil") + } + if err := cipher.VerifyMAC([]byte{1}, nil); err == nil { + t.Errorf("expect an error when data is nil") + } + if err := cipher.VerifyMAC(nil, nil); err == nil { + t.Errorf("cipher.VerifyMAC(nil, nil) succeeded unexpectedly") + } +} + +func TestHMACModification(t *testing.T) { + for _, test := range hmacTests { + t.Run(test.desc, func(t *testing.T) { + cipher, err := hmac.New(test.hashAlg, test.key, test.tagSize) + if err != nil { + t.Fatalf("hmac.New() err = %q, want nil", err) + } + mac, err := cipher.ComputeMAC(test.data) + if err != nil { + t.Fatalf("cipher.ComputeMAC() err = %q, want nil", err) + } + for i := 0; i < len(mac); i++ { + tmp := mac[i] + for j := 0; j < 8; j++ { + mac[i] ^= 1 << uint8(j) + err := cipher.VerifyMAC(mac, test.data) + if err == nil { + t.Errorf("cipher.VerifyMAC() of valid mac modified at position (%d, %d) is nil, want error", i, j) + } + mac[i] = tmp + } + } + }) + } +} + +func TestHMACTruncation(t *testing.T) { + for i, test := range hmacTests { + t.Run(test.desc, func(t *testing.T) { + cipher, err := hmac.New(test.hashAlg, test.key, test.tagSize) + if err != nil { + t.Fatalf("hmac.New() err = %q, want nil", err) + } + mac, err := cipher.ComputeMAC(test.data) + if err != nil { + t.Fatalf("cipher.ComputeMAC() err = %q, want nil", err) + } + for truncatedLen := 1; i < len(mac); i++ { + truncatedMAC := mac[:truncatedLen] + err := cipher.VerifyMAC(truncatedMAC, test.data) + if err == nil { + t.Errorf("cipher.VerifyMAC() of a valid mac truncated to %d bytes is nil, want error", + truncatedLen) + } + } + }) + } +} diff --git a/mac/subtle/hmac.go b/mac/subtle/hmac.go index daf8e39..32ee938 100644 --- a/mac/subtle/hmac.go +++ b/mac/subtle/hmac.go @@ -16,20 +16,10 @@ package subtle import ( - "crypto/hmac" "errors" - "fmt" "hash" - "github.com/tink-crypto/tink-go/v2/subtle" -) - -const ( - // Minimum key size in bytes. - minKeySizeInBytes = uint32(16) - - // Minimum tag size in bytes. This provides minimum 80-bit security strength. - minTagSizeInBytes = uint32(10) + "github.com/tink-crypto/tink-go/v2/internal/mac/hmac" ) var errHMACInvalidInput = errors.New("HMAC: invalid input") @@ -37,67 +27,31 @@ var errHMACInvalidInput = errors.New("HMAC: invalid input") // HMAC implementation of interface tink.MAC type HMAC struct { HashFunc func() hash.Hash - key []byte TagSize uint32 + hmac *hmac.HMAC } // NewHMAC creates a new instance of HMAC with the specified key and tag size. func NewHMAC(hashAlg string, key []byte, tagSize uint32) (*HMAC, error) { - keySize := uint32(len(key)) - if err := ValidateHMACParams(hashAlg, keySize, tagSize); err != nil { - return nil, fmt.Errorf("hmac: %s", err) - } - hashFunc := subtle.GetHashFunc(hashAlg) - if hashFunc == nil { - return nil, fmt.Errorf("hmac: invalid hash algorithm") + h, err := hmac.New(hashAlg, key, tagSize) + if err != nil { + return nil, err } - return &HMAC{ - HashFunc: hashFunc, - key: key, - TagSize: tagSize, - }, nil + return &HMAC{hmac: h, TagSize: tagSize, HashFunc: h.HashFunc}, nil } // ValidateHMACParams validates parameters of HMAC constructor. func ValidateHMACParams(hash string, keySize uint32, tagSize uint32) error { - // validate tag size - digestSize, err := subtle.GetHashDigestSize(hash) - if err != nil { - return err - } - if tagSize > digestSize { - return fmt.Errorf("tag size too big") - } - if tagSize < minTagSizeInBytes { - return fmt.Errorf("tag size too small") - } - // validate key size - if keySize < minKeySizeInBytes { - return fmt.Errorf("key too short") - } - return nil + return hmac.ValidateHMACParams(hash, keySize, tagSize) } // ComputeMAC computes message authentication code (MAC) for the given data. func (h *HMAC) ComputeMAC(data []byte) ([]byte, error) { - if h.HashFunc == nil { - return nil, fmt.Errorf("hmac: invalid hash algorithm") - } - mac := hmac.New(h.HashFunc, h.key) - mac.Write(data) - tag := mac.Sum(nil) - return tag[:h.TagSize], nil + return h.hmac.ComputeMAC(data) } // VerifyMAC verifies whether the given MAC is a correct message authentication // code (MAC) the given data. func (h *HMAC) VerifyMAC(mac []byte, data []byte) error { - expectedMAC, err := h.ComputeMAC(data) - if err != nil { - return err - } - if hmac.Equal(expectedMAC, mac) { - return nil - } - return errors.New("HMAC: invalid MAC") + return h.hmac.VerifyMAC(mac, data) } diff --git a/mac/subtle/hmac_test.go b/mac/subtle/hmac_test.go index 1c94807..1d9aca2 100644 --- a/mac/subtle/hmac_test.go +++ b/mac/subtle/hmac_test.go @@ -114,20 +114,6 @@ func TestNewHMACWithInvalidInput(t *testing.T) { } } -func TestHMACWithNilHashFunc(t *testing.T) { - cipher, err := subtle.NewHMAC("SHA256", random.GetRandomBytes(32), 32) - if err != nil { - t.Fatalf("subtle.NewHMAC() err = %v", err) - } - - // Modify exported field. - cipher.HashFunc = nil - - if _, err := cipher.ComputeMAC([]byte{}); err == nil { - t.Errorf("cipher.ComputerMAC() err = nil, want not nil") - } -} - func TestHMAComputeVerifyWithNilInput(t *testing.T) { cipher, err := subtle.NewHMAC("SHA256", random.GetRandomBytes(16), 32) if err != nil {