diff --git a/aead/aesgcm/BUILD.bazel b/aead/aesgcm/BUILD.bazel new file mode 100644 index 0000000..ffce5e1 --- /dev/null +++ b/aead/aesgcm/BUILD.bazel @@ -0,0 +1,30 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "aesgcm", + srcs = ["aesgcm.go"], + importpath = "github.com/tink-crypto/tink-go/v2/aead/aesgcm", + visibility = ["//visibility:public"], + deps = [ + "//internal/outputprefix", + "//key", + "//secretdata", + ], +) + +alias( + name = "go_default_library", + actual = ":aesgcm", + visibility = ["//visibility:public"], +) + +go_test( + name = "aesgcm_test", + srcs = ["aesgcm_test.go"], + deps = [ + ":aesgcm", + "//core/cryptofmt", + "//insecuresecretdataaccess", + "//secretdata", + ], +) diff --git a/aead/aesgcm/aesgcm.go b/aead/aesgcm/aesgcm.go new file mode 100644 index 0000000..d53ffff --- /dev/null +++ b/aead/aesgcm/aesgcm.go @@ -0,0 +1,195 @@ +// 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 aesgcm implements AES-GCM parameters and key. +package aesgcm + +import ( + "bytes" + "fmt" + + "github.com/tink-crypto/tink-go/v2/internal/outputprefix" + "github.com/tink-crypto/tink-go/v2/key" + "github.com/tink-crypto/tink-go/v2/secretdata" +) + +// Variant is the prefix variant of AES-GCM keys. +// +// It describes how the prefix of the ciphertext is constructed. For AEAD there +// are three options: +// +// * TINK: prepends '0x01' to the ciphertext. +// * CRUNCHY: prepends '0x00' to the ciphertext. +// * NO_PREFIX: adds no prefix to the ciphertext. +type Variant int + +const ( + // VariantUnknown is the default and invalid value of Variant. + VariantUnknown Variant = iota + // VariantTink prefixes '0x01' to the ciphertext. + VariantTink + // VariantCrunchy prefixes '0x00' to the ciphertext. + VariantCrunchy + // VariantNoPrefix adds no prefix to the ciphertext. + VariantNoPrefix +) + +func (variant Variant) String() string { + switch variant { + case VariantTink: + return "TINK" + case VariantCrunchy: + return "CRUNCHY" + case VariantNoPrefix: + return "NO_PREFIX" + default: + return "UNKNOWN" + } +} + +// calculateOutputPrefix calculates the output prefix from keyID. +func calculateOutputPrefix(variant Variant, keyID uint32) ([]byte, error) { + switch variant { + case VariantTink: + return outputprefix.Tink(keyID), nil + case VariantCrunchy: + return outputprefix.Legacy(keyID), nil + case VariantNoPrefix: + return nil, nil + default: + return nil, fmt.Errorf("invalid output prefix variant: %v", variant) + } +} + +// Parameters specifies an AES-GCM key. +type Parameters struct { + keySizeInBytes int + ivSizeInBytes int + tagSizeInBytes int + variant Variant +} + +var _ key.Parameters = (*Parameters)(nil) + +// KeySizeInBytes returns the size of the key in bytes. +func (p *Parameters) KeySizeInBytes() int { return p.keySizeInBytes } + +// IVSizeInBytes returns the size of the IV in bytes. +func (p *Parameters) IVSizeInBytes() int { return p.ivSizeInBytes } + +// TagSizeInBytes returns the size of the tag in bytes. +func (p *Parameters) TagSizeInBytes() int { return p.tagSizeInBytes } + +// Variant returns the variant of the key. +func (p *Parameters) Variant() Variant { return p.variant } + +// ParametersOpts specifies options for creating AES-GCM parameters. +type ParametersOpts struct { + KeySizeInBytes int + IVSizeInBytes int + TagSizeInBytes int + Variant Variant +} + +// NewParameters creates a new AES-GCM Parameters object. +func NewParameters(opts ParametersOpts) (*Parameters, error) { + if opts.KeySizeInBytes != 16 && opts.KeySizeInBytes != 24 && opts.KeySizeInBytes != 32 { + return nil, fmt.Errorf("aesgcm.Parameters: unsupported key size; want 16, 24, or 32, got: %v", opts.KeySizeInBytes) + } + if opts.IVSizeInBytes <= 0 { + return nil, fmt.Errorf("aesgcm.Parameters: unsupported IV size; want > 0, got: %v", opts.IVSizeInBytes) + } + if opts.TagSizeInBytes < 12 || opts.TagSizeInBytes > 16 { + return nil, fmt.Errorf("aesgcm.Parameters: unsupported tag size; want >= 12 and <= 16, got: %v", opts.TagSizeInBytes) + } + if opts.Variant == VariantUnknown { + return nil, fmt.Errorf("aesgcm.Parameters: unsupported variant: %v", opts.Variant) + } + return &Parameters{ + keySizeInBytes: opts.KeySizeInBytes, + ivSizeInBytes: opts.IVSizeInBytes, + tagSizeInBytes: opts.TagSizeInBytes, + variant: opts.Variant, + }, nil +} + +// HasIDRequirement returns whether the key has an ID requirement. +func (p *Parameters) HasIDRequirement() bool { return p.variant != VariantNoPrefix } + +// Equals returns whether this Parameters object is equal to other. +func (p *Parameters) Equals(other key.Parameters) bool { + actualParams, ok := other.(*Parameters) + return ok && p.HasIDRequirement() == actualParams.HasIDRequirement() && + p.keySizeInBytes == actualParams.keySizeInBytes && + p.ivSizeInBytes == actualParams.ivSizeInBytes && + p.tagSizeInBytes == actualParams.tagSizeInBytes && + p.variant == actualParams.variant +} + +// Key represents an AES-GCM key. +type Key struct { + keyBytes secretdata.Bytes + id uint32 + outputPrefix []byte + parameters *Parameters +} + +var _ key.Key = (*Key)(nil) + +// NewKey creates a new AES-GCM key with key, keyID and parameters. +func NewKey(keyBytes secretdata.Bytes, keyID uint32, parameters *Parameters) (*Key, error) { + if parameters == nil { + return nil, fmt.Errorf("aesgcm.NewKey: parameters is nil") + } + if keyBytes.Len() != int(parameters.KeySizeInBytes()) { + return nil, fmt.Errorf("aesgcm.NewKey: key.Len() = %v, want %v", keyBytes.Len(), parameters.KeySizeInBytes()) + } + outputPrefix, err := calculateOutputPrefix(parameters.Variant(), keyID) + if err != nil { + return nil, fmt.Errorf("aesgcm.NewKey: %v", err) + } + return &Key{ + keyBytes: keyBytes, + id: keyID, + outputPrefix: outputPrefix, + parameters: parameters, + }, nil +} + +// KeyBytes returns the key material. +// +// This function provides access to partial key material. See +// https://developers.google.com/tink/design/access_control#access_of_parts_of_a_key +// for more information. +func (k *Key) KeyBytes() secretdata.Bytes { return k.keyBytes } + +// Parameters returns the parameters of this key. +func (k *Key) Parameters() key.Parameters { return k.parameters } + +// IDRequirement returns whether the key ID and whether it is required +// +// If not required, the returned key ID is not usable. +func (k *Key) IDRequirement() (uint32, bool) { return k.id, k.Parameters().HasIDRequirement() } + +// OutputPrefix returns the output prefix. +func (k *Key) OutputPrefix() []byte { return bytes.Clone(k.outputPrefix) } + +// Equals returns whether this key object is equal to other. +func (k *Key) Equals(other key.Key) bool { + that, ok := other.(*Key) + return ok && k.Parameters().Equals(that.Parameters()) && + k.id == that.id && + k.keyBytes.Equals(&that.keyBytes) && + bytes.Equal(k.outputPrefix, that.outputPrefix) +} diff --git a/aead/aesgcm/aesgcm_test.go b/aead/aesgcm/aesgcm_test.go new file mode 100644 index 0000000..809522a --- /dev/null +++ b/aead/aesgcm/aesgcm_test.go @@ -0,0 +1,475 @@ +// 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 aesgcm_test + +import ( + "bytes" + "testing" + + "github.com/tink-crypto/tink-go/v2/aead/aesgcm" + "github.com/tink-crypto/tink-go/v2/core/cryptofmt" + "github.com/tink-crypto/tink-go/v2/insecuresecretdataaccess" + "github.com/tink-crypto/tink-go/v2/secretdata" +) + +var ( + key128Bits = []byte{ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + } + key256Bits = []byte{ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + } +) + +func TestNewParametersInvalidKeySize(t *testing.T) { + for _, keySize := range []int{1, 15, 17, 31, 33} { + opts := aesgcm.ParametersOpts{ + KeySizeInBytes: keySize, + IVSizeInBytes: 12, + TagSizeInBytes: 16, + Variant: aesgcm.VariantTink, + } + if _, err := aesgcm.NewParameters(opts); err == nil { + t.Errorf("aesgcm.NewParameters(%v) err = nil, want error", opts) + } + } +} + +func TestNewParametersInvalidIVSize(t *testing.T) { + for _, ivSize := range []int{-1, 0} { + opts := aesgcm.ParametersOpts{ + KeySizeInBytes: 16, + IVSizeInBytes: ivSize, + TagSizeInBytes: 16, + Variant: aesgcm.VariantTink, + } + if _, err := aesgcm.NewParameters(opts); err == nil { + t.Errorf("aesgcm.NewParameters(%v) err = nil, want error", opts) + } + } +} + +func TestNewParametersInvalidTagSize(t *testing.T) { + for _, tagSize := range []int{1, 11, 17} { + opts := aesgcm.ParametersOpts{ + KeySizeInBytes: 16, + IVSizeInBytes: 12, + TagSizeInBytes: tagSize, + Variant: aesgcm.VariantTink, + } + if _, err := aesgcm.NewParameters(opts); err == nil { + t.Errorf("aesgcm.NewParameters(%v) err = nil, want error", opts) + } + } +} + +func TestNewParametersInvalidVariant(t *testing.T) { + opts := aesgcm.ParametersOpts{ + KeySizeInBytes: 16, + IVSizeInBytes: 12, + TagSizeInBytes: 16, + Variant: aesgcm.VariantUnknown, + } + if _, err := aesgcm.NewParameters(opts); err == nil { + t.Errorf("aesgcm.NewParameters(%v) err = nil, want error", opts) + } +} + +func TestOutputPrefix(t *testing.T) { + for _, test := range []struct { + name string + variant aesgcm.Variant + id uint32 + want []byte + }{ + { + name: "Tink", + variant: aesgcm.VariantTink, + id: uint32(0x01020304), + want: []byte{cryptofmt.TinkStartByte, 0x01, 0x02, 0x03, 0x04}, + }, + { + name: "Crunchy", + variant: aesgcm.VariantCrunchy, + id: uint32(0x01020304), + want: []byte{cryptofmt.LegacyStartByte, 0x01, 0x02, 0x03, 0x04}, + }, + { + name: "No prefix", + variant: aesgcm.VariantNoPrefix, + id: uint32(0x01020304), + want: nil, + }, + } { + t.Run(test.name, func(t *testing.T) { + opts := aesgcm.ParametersOpts{ + KeySizeInBytes: 32, + IVSizeInBytes: 12, + TagSizeInBytes: 16, + Variant: test.variant, + } + params, err := aesgcm.NewParameters(opts) + if err != nil { + t.Fatalf("aesgcm.NewParameters(%v) err = %v, want nil", opts, err) + } + keyBytes, err := secretdata.NewBytesFromRand(32) + if err != nil { + t.Fatalf("secretdata.NewBytes(32fNewBytesFromRand() err = %v, want nil", err) + } + key, err := aesgcm.NewKey(*keyBytes, test.id, params) + if err != nil { + t.Fatalf("aesgcm.NewKey(keyBytes, %v, %v) err = %v, want nil", test.id, params, err) + } + if got := key.OutputPrefix(); !bytes.Equal(got, test.want) { + t.Errorf("params.OutputPrefix() = %v, want %v", got, test.want) + } + }) + } +} + +func TestNewParametersWorks(t *testing.T) { + for _, test := range []struct { + name string + keySize int + variant aesgcm.Variant + }{ + { + name: "128-bit key with Tink prefix", + keySize: 16, + variant: aesgcm.VariantTink, + }, + { + name: "128-bit key with Crunchy prefix", + keySize: 16, + variant: aesgcm.VariantCrunchy, + }, + { + name: "128-bit key with NoPrefix prefix", + keySize: 16, + variant: aesgcm.VariantNoPrefix, + }, + { + name: "256-bit key with Tink prefix", + keySize: 32, + variant: aesgcm.VariantTink, + }, + { + name: "256-bit key with Crunchy prefix", + keySize: 32, + variant: aesgcm.VariantCrunchy, + }, + { + name: "256-bit key with NoPrefix prefix", + keySize: 32, + variant: aesgcm.VariantNoPrefix, + }, + } { + t.Run(test.name, func(t *testing.T) { + opts := aesgcm.ParametersOpts{ + KeySizeInBytes: test.keySize, + IVSizeInBytes: 12, + TagSizeInBytes: 16, + Variant: test.variant, + } + params, err := aesgcm.NewParameters(opts) + if err != nil { + t.Fatalf("aesgcm.NewParameters(%v) err = %v, want nil", opts, err) + } + if params.HasIDRequirement() != (test.variant != aesgcm.VariantNoPrefix) { + t.Errorf("params.HasIDRequirement() = %v, want %v", params.HasIDRequirement(), (test.variant != aesgcm.VariantNoPrefix)) + } + if params.KeySizeInBytes() != test.keySize { + t.Errorf("params.KeySizeInBytes()() = %v, want %v", params.KeySizeInBytes(), test.keySize) + } + if params.TagSizeInBytes() != 16 { + t.Errorf("params.TagSizeInBytes() = %v, want 16", params.TagSizeInBytes()) + } + if params.IVSizeInBytes() != 12 { + t.Errorf("params.IVSizeInBytes() = %v, want 12", params.IVSizeInBytes()) + } + if params.Variant() != test.variant { + t.Errorf("params.Variant() = %v, want %v", params.Variant(), test.variant) + } + otherParams, err := aesgcm.NewParameters(opts) + if err != nil { + t.Fatalf("aesgcm.NewParameters(%v) err = %v, want nil", opts, err) + } + if !params.Equals(otherParams) { + t.Errorf("params.Equals(otherParams) = %v, want true", params.Equals(otherParams)) + } + }) + } +} + +func TestParametersEqualsFalseIfDifferent(t *testing.T) { + for _, test := range []struct { + name string + key1Size int + key1Variant aesgcm.Variant + key2Size int + key2Variant aesgcm.Variant + }{ + { + name: "different key size", + key1Size: 16, + key1Variant: aesgcm.VariantTink, + key2Size: 32, + key2Variant: aesgcm.VariantTink, + }, + { + name: "different prefix variant", + key1Size: 16, + key1Variant: aesgcm.VariantCrunchy, + key2Size: 16, + key2Variant: aesgcm.VariantTink, + }, + } { + t.Run(test.name, func(t *testing.T) { + opts1 := aesgcm.ParametersOpts{ + KeySizeInBytes: test.key1Size, + IVSizeInBytes: 12, + TagSizeInBytes: 16, + Variant: test.key1Variant, + } + params1, err := aesgcm.NewParameters(opts1) + if err != nil { + t.Fatalf("aesgcm.NewParameters(%v) err = %v, want nil", opts1, err) + } + opts2 := aesgcm.ParametersOpts{ + KeySizeInBytes: test.key2Size, + IVSizeInBytes: 12, + TagSizeInBytes: 16, + Variant: test.key2Variant, + } + params2, err := aesgcm.NewParameters(opts2) + if err != nil { + t.Errorf("aesgcm.NewParameters(%v) err = %v, want nil", opts2, err) + } + if params1.Equals(params2) { + t.Errorf("params.Equals(params2) = %v, want false", params1.Equals(params2)) + } + }) + } +} + +type TestKey struct { + name string + keySize int + id uint32 + key []byte + variant aesgcm.Variant +} + +func TestNewKeyWorks(t *testing.T) { + for _, test := range []TestKey{ + { + name: "128-bit key with Tink prefix", + keySize: 16, + id: 0x01, + key: key128Bits, + variant: aesgcm.VariantTink, + }, + { + name: "128-bit key with Crunchy prefix", + keySize: 16, + id: 0x01, + key: key128Bits, + variant: aesgcm.VariantCrunchy, + }, + { + name: "128-bit key with NoPrefix prefix", + keySize: 16, + id: 0x01, + key: key128Bits, + variant: aesgcm.VariantNoPrefix, + }, + { + name: "256-bit key with Tink prefix", + keySize: 32, + id: 0x01, + key: key256Bits, + variant: aesgcm.VariantTink, + }, + { + name: "256-bit key with Crunchy prefix", + keySize: 32, + id: 0x01, + key: key256Bits, + variant: aesgcm.VariantCrunchy, + }, + { + name: "256-bit key with NoPrefix prefix", + keySize: 32, + id: 0x01, + key: key256Bits, + variant: aesgcm.VariantNoPrefix, + }, + } { + t.Run(test.name, func(t *testing.T) { + opts := aesgcm.ParametersOpts{ + KeySizeInBytes: test.keySize, + IVSizeInBytes: 12, + TagSizeInBytes: 16, + Variant: test.variant, + } + params, err := aesgcm.NewParameters(opts) + if err != nil { + t.Fatalf("aesgcm.NewParameters(%v) err = %v, want nil", opts, err) + } + keyBytes := secretdata.NewBytesFromData(test.key, insecuresecretdataaccess.Token{}) + + // Create two keys with the same parameters and key bytes. + key1, err := aesgcm.NewKey(*keyBytes, test.id, params) + if err != nil { + t.Fatalf("aesgcm.NewKey(keyBytes, %v, %v) err = %v, want nil", test.id, params, err) + } + if !key1.Parameters().Equals(params) { + t.Errorf("key1.Parameters() = %v, want %v", key1.Parameters(), params) + } + key1Bytes := key1.KeyBytes() + if !keyBytes.Equals(&key1Bytes) { + t.Errorf("keyBytes.Equals(key1Bytes) = false, want true") + } + id, required := key1.IDRequirement() + if required != (test.variant != aesgcm.VariantNoPrefix) { + t.Errorf("key1.ID() = %v, want %v", required, (test.variant == aesgcm.VariantNoPrefix)) + } + if id != test.id { + t.Errorf("id = %v, want %v", id, test.id) + } + key2, err := aesgcm.NewKey(*keyBytes, test.id, params) + if err != nil { + t.Fatalf("aesgcm.NewKey(keyBytes, %v, %v) err = %v, want nil", test.id, params, err) + } + // Test Equals. + if !key1.Equals(key2) { + t.Errorf("key1.Equals(key2) = %v, want true", key1.Equals(key2)) + } + }) + } +} + +func TestKeyEqualsReturnsFalseIfDifferent(t *testing.T) { + for _, test := range []struct { + name string + first TestKey + second TestKey + }{ + { + name: "different key size", + first: TestKey{ + keySize: 16, + variant: aesgcm.VariantTink, + key: key128Bits, + id: 0x01, + }, + second: TestKey{ + keySize: 32, + variant: aesgcm.VariantTink, + key: key256Bits, + id: 0x01, + }, + }, + { + name: "different prefix variant", + first: TestKey{ + keySize: 16, + variant: aesgcm.VariantTink, + key: key128Bits, + id: 0x01, + }, + second: TestKey{ + keySize: 16, + variant: aesgcm.VariantCrunchy, + key: key128Bits, + id: 0x01, + }, + }, + { + name: "different key IDs", + first: TestKey{ + keySize: 16, + variant: aesgcm.VariantTink, + key: key128Bits, + id: 0x01, + }, + second: TestKey{ + keySize: 16, + variant: aesgcm.VariantTink, + key: key128Bits, + id: 0x02, + }, + }, + { + name: "different key bytes", + first: TestKey{ + keySize: 16, + variant: aesgcm.VariantCrunchy, + key: key128Bits, + id: 0x01, + }, + second: TestKey{ + keySize: 16, + variant: aesgcm.VariantCrunchy, + key: []byte{ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x09, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + }, + id: 0x01, + }, + }, + } { + t.Run(test.name, func(t *testing.T) { + firstOpts := aesgcm.ParametersOpts{ + KeySizeInBytes: test.first.keySize, + IVSizeInBytes: 12, + TagSizeInBytes: 16, + Variant: test.first.variant, + } + firstParams, err := aesgcm.NewParameters(firstOpts) + if err != nil { + t.Fatalf("aesgcm.NewParameters(%v) err = %v, want nil", firstOpts, err) + } + firstKeyBytes := secretdata.NewBytesFromData(test.first.key, insecuresecretdataaccess.Token{}) + firstKey, err := aesgcm.NewKey(*firstKeyBytes, test.first.id, firstParams) + if err != nil { + t.Fatalf("aesgcm.NewKey(firstKeyBytes, %v, %v) err = %v, want nil", test.first.id, firstParams, err) + } + + secondOpts := aesgcm.ParametersOpts{ + KeySizeInBytes: test.second.keySize, + IVSizeInBytes: 12, + TagSizeInBytes: 16, + Variant: test.second.variant, + } + secondParams, err := aesgcm.NewParameters(secondOpts) + if err != nil { + t.Fatalf("aesgcm.NewParameters(%v) err = %v, want nil", secondOpts, err) + } + secondKeyBytes := secretdata.NewBytesFromData(test.second.key, insecuresecretdataaccess.Token{}) + secondKey, err := aesgcm.NewKey(*secondKeyBytes, test.second.id, secondParams) + if err != nil { + t.Fatalf("aesgcm.NewKey(secondKeyBytes, %v, %v) err = %v, want nil", test.second.id, secondParams, err) + } + if firstKey.Equals(secondKey) { + t.Errorf("firstKey.Equals(secondKey) = true, want false") + } + }) + } +}