diff --git a/internal/api/api.go b/internal/api/api.go index 83bcdabf..018f66aa 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -35,6 +35,7 @@ const ( PathKeyGenerate = "/v1/key/generate/" PathKeyEncrypt = "/v1/key/encrypt/" PathKeyDecrypt = "/v1/key/decrypt/" + PathKeyHMAC = "/v1/key/hmac/" PathPolicyDescribe = "/v1/policy/describe/" PathPolicyRead = "/v1/policy/read/" diff --git a/internal/api/request.go b/internal/api/request.go index 5086080d..a149fe69 100644 --- a/internal/api/request.go +++ b/internal/api/request.go @@ -26,3 +26,8 @@ type DecryptKeyRequest struct { Ciphertext []byte `json:"ciphertext"` Context []byte `json:"context"` // optional } + +// HMACRequest is the request sent by clients when calling the HMAC API. +type HMACRequest struct { + Message []byte `json:"message"` +} diff --git a/internal/api/response.go b/internal/api/response.go index 57d4cd3d..61983440 100644 --- a/internal/api/response.go +++ b/internal/api/response.go @@ -71,6 +71,11 @@ type DecryptKeyResponse struct { Plaintext []byte `json:"plaintext"` } +// HMACResponse is the response sent to clients by the HMAC API. +type HMACResponse struct { + Sum []byte `json:"hmac"` +} + // ReadPolicyResponse is the response sent to clients by the ReadPolicy API. type ReadPolicyResponse struct { Name string `json:"name"` diff --git a/internal/key/ciphertext.go b/internal/crypto/ciphertext.go similarity index 67% rename from internal/key/ciphertext.go rename to internal/crypto/ciphertext.go index 0b1a39d5..dd937d44 100644 --- a/internal/key/ciphertext.go +++ b/internal/crypto/ciphertext.go @@ -1,43 +1,46 @@ -// Copyright 2022 - MinIO, Inc. All rights reserved. -// Use of this source code is governed by the AGPLv3 -// license that can be found in the LICENSE file. - -package key +package crypto import ( "encoding/json" + "slices" "github.com/minio/kes-go" "github.com/tinylib/msgp/msgp" ) -// decodeCiphertext parses the given bytes as -// ciphertext. If it fails to unmarshal the -// given bytes, decodeCiphertext returns -// ErrDecrypt. -func decodeCiphertext(bytes []byte) (ciphertext, error) { - if len(bytes) == 0 { - return ciphertext{}, kes.ErrDecrypt +// parseCiphertext parses and converts a ciphertext into +// the format expected by a SecretKey. +// +// Previous implementations of a SecretKey produced a structured +// ciphertext. parseCiphertext converts all previously generated +// formats into the one that SecretKey.Decrypt expects. +func parseCiphertext(b []byte) []byte { + if len(b) == 0 { + return b } var c ciphertext - switch bytes[0] { + switch b[0] { case 0x95: // msgp first byte - if err := c.UnmarshalBinary(bytes); err != nil { - return ciphertext{}, kes.ErrDecrypt + if err := c.UnmarshalBinary(b); err != nil { + return b } + + b = b[:0] + b = append(b, c.Bytes...) + b = append(b, c.IV...) + b = append(b, c.Nonce...) case 0x7b: // JSON first byte - if err := c.UnmarshalJSON(bytes); err != nil { - return ciphertext{}, kes.ErrDecrypt - } - default: - if err := c.UnmarshalBinary(bytes); err != nil { - if err = c.UnmarshalJSON(bytes); err != nil { - return ciphertext{}, kes.ErrDecrypt - } + if err := c.UnmarshalJSON(b); err != nil { + return b } + + b = b[:0] + b = append(b, c.Bytes...) + b = append(b, c.IV...) + b = append(b, c.Nonce...) } - return c, nil + return b } // ciphertext is a structure that contains the encrypted @@ -51,22 +54,6 @@ type ciphertext struct { Bytes []byte } -// MarshalBinary returns the ciphertext's binary representation. -func (c *ciphertext) MarshalBinary() ([]byte, error) { - // We encode a ciphertext simply as message-pack - // flat array. - const Items = 5 - - var b []byte - b = msgp.AppendArrayHeader(b, Items) - b = msgp.AppendString(b, c.Algorithm.String()) - b = msgp.AppendString(b, c.ID) - b = msgp.AppendBytes(b, c.IV) - b = msgp.AppendBytes(b, c.Nonce) - b = msgp.AppendBytes(b, c.Bytes) - return b, nil -} - // UnmarshalBinary parses b as binary-encoded ciphertext. func (c *ciphertext) UnmarshalBinary(b []byte) error { const ( @@ -117,7 +104,7 @@ func (c *ciphertext) UnmarshalBinary(b []byte) error { c.ID = id c.IV = iv[:] c.Nonce = nonce[:] - c.Bytes = clone(bytes...) + c.Bytes = slices.Clone(bytes) return nil } diff --git a/internal/crypto/key.go b/internal/crypto/key.go new file mode 100644 index 00000000..fc1f11de --- /dev/null +++ b/internal/crypto/key.go @@ -0,0 +1,495 @@ +// Copyright 2024 - MinIO, Inc. All rights reserved. +// Use of this source code is governed by the AGPLv3 +// license that can be found in the LICENSE file. + +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "slices" + "strconv" + "time" + + "github.com/minio/kes-go" + "github.com/minio/kes/internal/fips" + pb "github.com/minio/kes/internal/protobuf" + "golang.org/x/crypto/chacha20" + "golang.org/x/crypto/chacha20poly1305" +) + +// SecretKeySize is the size of a secret key in bytes. +const SecretKeySize = 32 + +// SecretKeyType defines the type of a secret key. Secret keys with +// different types are not compatible since they may differ in the +// encryption algorithm, key length, cipher mode, etc. +type SecretKeyType uint + +// Supported secret key types. +const ( + // AES256 represents the AES-256-GCM secret key type. + AES256 SecretKeyType = iota + 1 + + // ChaCha20 represents the ChaCha20-Poly1305 secret key type. + ChaCha20 +) + +// ParseSecretKeyType parse s as SecretKeyType string representation +// and returns an error if s is not a valid representation. +func ParseSecretKeyType(s string) (SecretKeyType, error) { + switch s { + case "AES256", "AES256-GCM_SHA256": + return AES256, nil + case "ChaCha20", "XCHACHA20-POLY1305": + return ChaCha20, nil + default: + return 0, fmt.Errorf("crypto: secret key type '%s' is not supported", s) + } +} + +// String returns the string representation of the SecretKeyType. +func (s SecretKeyType) String() string { + switch s { + case AES256: + return "AES256" + case ChaCha20: + return "ChaCha20" + default: + return "!INVALID:" + strconv.Itoa(int(s)) + } +} + +// Supported cryptographic hash functions. +const ( + // SHA256 represents the SHA-256 hash function. + SHA256 Hash = iota + 1 +) + +// Hash identifies a cryptographic hash function. +type Hash uint + +// String returns the string representation of the hash function. +func (h Hash) String() string { + switch h { + case SHA256: + return "SHA256" + default: + return "!INVALID:" + strconv.Itoa(int(h)) + } +} + +// EncodeKeyVersion base64-encoded binary representation of a key. +// +// It encodes the key's binary data as base64 since some KMS keystore +// implementations do not accept or handle binary data properly. +func EncodeKeyVersion(key KeyVersion) ([]byte, error) { + proto, err := pb.Marshal(&key) + if err != nil { + return nil, err + } + + b := make([]byte, base64.StdEncoding.EncodedLen(len(proto))) + base64.StdEncoding.Encode(b, proto) + return b, nil +} + +// ParseKeyVersion parses b as ParseKeyVersion. +func ParseKeyVersion(b []byte) (KeyVersion, error) { + if json.Valid(b) { + type JSON struct { + Bytes []byte `json:"bytes"` + Type string `json:"algorithm"` + CreatedAt time.Time `json:"created_at"` + CreatedBy kes.Identity `json:"created_by"` + } + + var value JSON + if err := json.Unmarshal(b, &value); err != nil { + return KeyVersion{}, err + } + + var cipher SecretKeyType + if value.Type == "" { + cipher = AES256 + } else { + var err error + if cipher, err = ParseSecretKeyType(value.Type); err != nil { + return KeyVersion{}, err + } + } + key, err := NewSecretKey(cipher, value.Bytes) + if err != nil { + return KeyVersion{}, err + } + + return KeyVersion{ + Key: key, + CreatedAt: value.CreatedAt, + CreatedBy: value.CreatedBy, + }, nil + } + + raw, err := base64.StdEncoding.DecodeString(string(b)) + if err != nil { + return KeyVersion{}, err + } + + var key KeyVersion + if err := pb.Unmarshal(raw, &key); err != nil { + return KeyVersion{}, err + } + return key, nil +} + +// KeyVersion represents a version of a secret key. +type KeyVersion struct { + Key SecretKey // The secret key + HMACKey HMACKey // The HMAC key + CreatedAt time.Time // The creation timestamp of the key version + CreatedBy kes.Identity // The identity of the entity that created the key version +} + +// HasHMACKey reports whether the KeyVersion has an HMAC key. +// +// Keys created in the past did not generate a HMAC key. +func (s *KeyVersion) HasHMACKey() bool { + return s.HMACKey.initialized +} + +// MarshalPB converts the KeyVersion into its protobuf representation. +func (s *KeyVersion) MarshalPB(v *pb.KeyVersion) error { + v.Key, v.HMACKey = &pb.SecretKey{}, &pb.HMACKey{} + if err := s.Key.MarshalPB(v.Key); err != nil { + return err + } + if err := s.HMACKey.MarshalPB(v.HMACKey); err != nil { + return err + } + + v.CreatedAt = pb.Time(s.CreatedAt) + v.CreatedBy = s.CreatedBy.String() + return nil +} + +// UnmarshalPB initializes the KeyVersion from its protobuf representation. +func (s *KeyVersion) UnmarshalPB(v *pb.KeyVersion) error { + var ( + key SecretKey + hmacKey HMACKey + ) + if err := key.UnmarshalPB(v.Key); err != nil { + return err + } + if err := hmacKey.UnmarshalPB(v.HMACKey); err != nil { + return err + } + + s.Key = key + s.HMACKey = hmacKey + s.CreatedAt = v.CreatedAt.AsTime() + s.CreatedBy = kes.Identity(v.CreatedBy) + return nil +} + +// NewSecretKey creates a new SecretKey with the specified cipher and key. +// +// The key must be SecretKeySize bytes long. +func NewSecretKey(cipher SecretKeyType, key []byte) (SecretKey, error) { + if n := len(key); n != SecretKeySize { + return SecretKey{}, fmt.Errorf("crypto: invalid key length '%d' for '%s'", n, cipher) + } + + return SecretKey{ + cipher: cipher, + key: [SecretKeySize]byte(key), + initialized: true, + }, nil +} + +// GenerateSecretKey generates a new random SecretKey with the specified +// cipher. +// +// If random is nil the standard library crypto/rand.Reader is used. +func GenerateSecretKey(cipher SecretKeyType, random io.Reader) (SecretKey, error) { + if random == nil { + random = rand.Reader + } + + var bytes [SecretKeySize]byte + if _, err := io.ReadFull(random, bytes[:]); err != nil { + return SecretKey{}, err + } + return NewSecretKey(cipher, bytes[:]) +} + +// SecretKey represents a secret key used for encryption and decryption. +type SecretKey struct { + cipher SecretKeyType + key [SecretKeySize]byte + + initialized bool +} + +const randSize = 28 + +// Type returns the SecretKey's type. +func (s SecretKey) Type() SecretKeyType { return s.cipher } + +// Overhead returns the size difference between a plaintext +// and its ciphertext. +func (s SecretKey) Overhead() int { return randSize + 16 } + +// Encrypt encrypts and authenticates the plaintext and +// authenticates the associatedData. +// +// The same associatedData must be provided when decrypting. +func (s SecretKey) Encrypt(plaintext, associatedData []byte) ([]byte, error) { + if !s.initialized { + panic("crypto: usage of empty or uninitialized secret key") + } + if fips.Enabled { + if s.cipher != AES256 { + return nil, errors.New("crypto: cipher not available in FIPS mode") + } + } + + var random [randSize]byte + if _, err := rand.Read(random[:]); err != nil { + return nil, err + } + iv, nonce := random[:16], random[16:] + + var aead cipher.AEAD + switch s.cipher { + case AES256: + prf := hmac.New(sha256.New, s.key[:]) + prf.Write(iv) + key := prf.Sum(make([]byte, 0, prf.Size())) + + block, err := aes.NewCipher(key[:]) + if err != nil { + return nil, err + } + aead, err = cipher.NewGCM(block) + if err != nil { + return nil, err + } + case ChaCha20: + key, err := chacha20.HChaCha20(s.key[:], iv) + if err != nil { + return nil, err + } + c, err := chacha20poly1305.New(key[:]) + if err != nil { + return nil, err + } + aead = c + default: + panic("crypto: unknown secret key cipher '" + strconv.Itoa(int(s.cipher)) + "'") + } + + ciphertext := extend(plaintext, s.Overhead()) + ciphertext = aead.Seal(ciphertext[:0], nonce, plaintext, associatedData) + ciphertext = append(ciphertext, random[:]...) + return ciphertext, nil +} + +// Decrypt decrypts and authenticates the ciphertext and +// authenticates the associatedData. +// +// The same associatedData used during encryption must be +// provided. +func (s SecretKey) Decrypt(ciphertext, associatedData []byte) ([]byte, error) { + if !s.initialized { + panic("crypto: usage of empty or uninitialized secret key detected") + } + if fips.Enabled { + if s.cipher != AES256 { + return nil, errors.New("crypto: cipher not available in FIPS mode") + } + } + ciphertext = parseCiphertext(ciphertext) // handle previous ciphertext formats + + if len(ciphertext) <= randSize { + return nil, kes.ErrDecrypt + } + ciphertext, random := ciphertext[:len(ciphertext)-randSize], ciphertext[len(ciphertext)-randSize:] + iv, nonce := random[:16], random[16:] + + var aead cipher.AEAD + switch s.cipher { + case AES256: + prf := hmac.New(sha256.New, s.key[:]) + prf.Write(iv) + key := prf.Sum(make([]byte, 0, prf.Size())) + + block, err := aes.NewCipher(key[:]) + if err != nil { + return nil, err + } + aead, err = cipher.NewGCM(block) + if err != nil { + return nil, err + } + case ChaCha20: + key, err := chacha20.HChaCha20(s.key[:], iv) + if err != nil { + return nil, err + } + c, err := chacha20poly1305.New(key[:]) + if err != nil { + return nil, err + } + aead = c + default: + panic("crypto: unknown secret key type '" + strconv.Itoa(int(s.cipher)) + "'") + } + + plaintext, err := aead.Open(ciphertext[:0], nonce[:], ciphertext, associatedData) + if err != nil { + return nil, kes.ErrDecrypt + } + return plaintext, nil +} + +// MarshalPB converts the SecretKey into its protobuf representation. +func (s *SecretKey) MarshalPB(v *pb.SecretKey) error { + if !s.initialized { + return errors.New("crypto: secret key is not initialized") + } + if s.cipher != AES256 && s.cipher != ChaCha20 { + return errors.New("crypto: invalid secret key type '" + strconv.Itoa(int(s.cipher)) + "'") + } + + v.Key = slices.Clone(s.key[:]) + v.Type = uint32(s.cipher) + return nil +} + +// UnmarshalPB initializes the SecretKey from its protobuf representation. +func (s *SecretKey) UnmarshalPB(v *pb.SecretKey) error { + if n := len(v.Key); n != SecretKeySize { + return errors.New("crypto: invalid secret key length '" + strconv.Itoa(n) + "'") + } + if t := SecretKeyType(v.Type); t != AES256 && t != ChaCha20 { + return errors.New("crypto: invalid secret key type '" + strconv.Itoa(int(s.cipher)) + "'") + } + + s.key = [SecretKeySize]byte(v.Key) + s.cipher = SecretKeyType(v.Type) + s.initialized = true + return nil +} + +// NewHMACKey creates a new HMACKey with the specified hash function and key. +// +// The key must be 32 bytes long. +func NewHMACKey(hash Hash, key []byte) (HMACKey, error) { + if n := len(key); n != 32 { + return HMACKey{}, fmt.Errorf("crypto: invalid key length '%d' for '%s'", n, hash) + } + + return HMACKey{ + hash: hash, + key: [32]byte(key), + initialized: true, + }, nil +} + +// GenerateHMACKey generates a new random HMACKey with the specified hash function. +// +// If random is nil the standard library crypto/rand.Reader is used. +func GenerateHMACKey(hash Hash, random io.Reader) (HMACKey, error) { + if random == nil { + random = rand.Reader + } + + var bytes [32]byte + if _, err := io.ReadFull(random, bytes[:]); err != nil { + return HMACKey{}, err + } + return NewHMACKey(hash, bytes[:]) +} + +// HMACKey represents a secret key used for computing HMAC checksums. +type HMACKey struct { + key [32]byte + hash Hash + + initialized bool +} + +// Type returns the HMACKey's hash function. +func (k HMACKey) Type() Hash { return k.hash } + +// Sum computes and returns the HMAC checksum of msg. +func (k *HMACKey) Sum(msg []byte) []byte { + if !k.initialized { + panic("crypto: usage of empty or uninitialized HMAC key detected") + } + + switch k.hash { + case SHA256: + mac := hmac.New(sha256.New, k.key[:]) + mac.Write(msg) + return mac.Sum(make([]byte, 0, mac.Size())) + default: + panic("crypto: unknown HMAC key hash '" + strconv.Itoa(int(k.hash)) + "'") + } +} + +// Equal reports whether mac1 and mac2 are equal without +// leaking any timing information. +func (k *HMACKey) Equal(mac1, mac2 []byte) bool { + return subtle.ConstantTimeCompare(mac1, mac2) == 1 +} + +// MarshalPB converts the HMACKey into its protobuf representation. +func (k *HMACKey) MarshalPB(v *pb.HMACKey) error { + if !k.initialized { + return errors.New("crypto: HMAC key is not initialized") + } + if k.hash != SHA256 { + return errors.New("crypto: invalid HMAC key hash '" + strconv.Itoa(int(k.hash)) + "'") + } + + v.Key = slices.Clone(k.key[:]) + v.Hash = uint32(k.hash) + return nil +} + +// UnmarshalPB initializes the HMACKey from its protobuf representation. +func (k *HMACKey) UnmarshalPB(v *pb.HMACKey) error { + if n := len(v.Key); n != 32 { + return errors.New("crypto: invalid HMAC key length '" + strconv.Itoa(n) + "'") + } + if Hash(v.Hash) != SHA256 { + return errors.New("crypto: invalid HMAC key hash '" + strconv.Itoa(int(k.hash)) + "'") + } + + k.key = [32]byte(v.Key) + k.hash = Hash(v.Hash) + k.initialized = true + return nil +} + +func extend(b []byte, n int) []byte { + total := len(b) + n + if cap(b) >= total { + return b[:total] + } + + c := make([]byte, total) + copy(c, b) + return c +} diff --git a/internal/crypto/key_test.go b/internal/crypto/key_test.go new file mode 100644 index 00000000..63c82d60 --- /dev/null +++ b/internal/crypto/key_test.go @@ -0,0 +1,346 @@ +// Copyright 2024 - MinIO, Inc. All rights reserved. +// Use of this source code is governed by the AGPLv3 +// license that can be found in the LICENSE file. + +package crypto + +import ( + "encoding/base64" + "testing" + "time" +) + +func TestEncodeKeyVersion(t *testing.T) { + t.Parallel() + + for i, test := range encodeSecretKeyVersionTests { + b, err := EncodeKeyVersion(test.Key) + if err != nil && !test.ShouldFail { + t.Fatalf("Test %d: failed to encode key: %v", i, err) + } + if err == nil && test.ShouldFail { + t.Fatalf("Test %d: encoded invalid key successfully", i) + } + if test.ShouldFail { + continue + } + + key, err := ParseKeyVersion(b) + if err != nil { + t.Fatalf("Test %d: failed to decode encoded key: %v", i, err) + } + if key != test.Key { + t.Fatalf("Test %d: got '%+v' - want '%+v'", i, key, test.Key) + } + } +} + +func TestSecretKeyEncrypt(t *testing.T) { + t.Parallel() + + for i, test := range secretKeyEncryptTests { + plaintext := mustDecodeB64(test.Plaintext) + associatedData := mustDecodeB64(test.AssociatedData) + + ciphertext, err := test.Key.Encrypt(plaintext, associatedData) + if err != nil { + t.Fatalf("Test %d: failed to encrypt plaintext: %v", i, err) + } + + p, err := test.Key.Decrypt(ciphertext, associatedData) + if err != nil { + t.Fatalf("Test %d: failed to decrypt ciphertext: %v", i, err) + } + if p := base64.StdEncoding.EncodeToString(p); p != test.Plaintext { + t.Fatalf("Test %d: got '%s' - want '%s'", i, p, test.Plaintext) + } + } +} + +func TestSecretKeyDecrypt(t *testing.T) { + t.Parallel() + + for i, test := range secretKeyDecryptTests { + plaintext, err := test.Key.Decrypt([]byte(test.Ciphertext), mustDecodeB64(test.AssociatedData)) + if err != nil && !test.ShouldFail { + t.Fatalf("Test %d: failed to decrypt ciphertext: %v", i, err) + } + if err == nil && test.ShouldFail { + t.Fatalf("Test %d: decrypted invalid ciphertext successfully", i) + } + if test.ShouldFail { + continue + } + if p := base64.StdEncoding.EncodeToString(plaintext); p != test.Plaintext { + t.Fatalf("Test %d: got %s - want %s", i, p, test.Plaintext) + } + } +} + +func TestParseKeyVersion(t *testing.T) { + for i, test := range parseKeyVersionTests { + key, err := ParseKeyVersion([]byte(test.Raw)) + if err != nil && !test.ShouldFail { + t.Fatalf("Test %d: failed to parse key: %v", i, err) + } + if err == nil && test.ShouldFail { + t.Fatalf("Test %d: parsing should have failed but succeeded", i) + } + if test.ShouldFail { + continue + } + + if key.Key != test.Key.Key { + t.Fatalf("Test %d: got '%+v' - want '%+v'", i, key.Key, test.Key.Key) + } + if key.HMACKey != test.Key.HMACKey { + t.Fatalf("Test %d: got '%+v' - want '%+v'", i, key.HMACKey, test.Key.HMACKey) + } + if key.CreatedAt != test.Key.CreatedAt { + t.Fatalf("Test %d: got %v - want %v", i, key.CreatedAt, test.Key.CreatedAt) + } + if key.CreatedBy != test.Key.CreatedBy { + t.Fatalf("Test %d: got %v - want %v", i, key.CreatedBy, test.Key.CreatedBy) + } + } +} + +var encodeSecretKeyVersionTests = []struct { + Key KeyVersion + ShouldFail bool +}{ + { // 0 + Key: KeyVersion{ + Key: mustSecretKey(AES256, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), + HMACKey: mustHMACKey(SHA256, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), + CreatedAt: mustTime("2024-01-12T11:39:20.886816+01:00"), + CreatedBy: "3ecfcdf38fcbe141ae26a1030f81e96b753365a46760ae6b578698a97c59fd22", + }, + }, + { // 1 + Key: KeyVersion{ + Key: mustSecretKey(AES256, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), + CreatedAt: mustTime("2024-01-12T11:39:20.886816+01:00"), + CreatedBy: "3ecfcdf38fcbe141ae26a1030f81e96b753365a46760ae6b578698a97c59fd22", + }, + ShouldFail: true, + }, + { // 2 + Key: KeyVersion{ + HMACKey: mustHMACKey(SHA256, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), + CreatedAt: mustTime("2024-01-12T11:39:20.886816+01:00"), + CreatedBy: "3ecfcdf38fcbe141ae26a1030f81e96b753365a46760ae6b578698a97c59fd22", + }, + ShouldFail: true, + }, +} + +var secretKeyEncryptTests = []struct { + Key SecretKey + Plaintext string + AssociatedData string +}{ + { // 0 + Key: mustSecretKey(AES256, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), + Plaintext: "AAAAAAAAAAAAAAAAAAAAAA==", + }, + { // 1 + Key: mustSecretKey(AES256, "dDHbTWgo+Yh3u804SYB5OyVMy6RiLeJYBQth1f6KlEU="), + Plaintext: "AAAAAAAAAAAAAAAAAAAAAA==", + }, + { // 2 + Key: mustSecretKey(AES256, "dDHbTWgo+Yh3u804SYB5OyVMy6RiLeJYBQth1f6KlEU="), + Plaintext: "CLcJoykFCWZDkEIiUq9bJRqwCwW9ZDvdgu8EMA==", + AssociatedData: "AAAAAAAAAAAAAAAAAAAAAA==", + }, + { // 3 + Key: mustSecretKey(ChaCha20, "dDHbTWgo+Yh3u804SYB5OyVMy6RiLeJYBQth1f6KlEU="), + Plaintext: "CLcJoykFCWZDkEIiUq9bJRqwCwW9ZDvdgu8EMA==", + AssociatedData: "AAAAAAAAAAAAAAAAAAAAAA==", + }, +} + +var secretKeyDecryptTests = []struct { + Key SecretKey + Plaintext string + Ciphertext string + AssociatedData string + ShouldFail bool +}{ + { // 0 + Key: mustSecretKey(AES256, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), + Plaintext: "AAAAAAAAAAAAAAAAAAAAAA==", + Ciphertext: `{"aead":"AES-256-GCM-HMAC-SHA-256","iv":"xLxIN3tSCkg2xMafuvwUwg==","nonce":"gu0mGwUkwcvMEoi5","bytes":"WVgRjeIJm3w50C/l+y7y2i6mbNg5NCAqN1zvOYWZKmc="}`, // JSON + }, + { // 1 + Key: mustSecretKey(ChaCha20, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), + Plaintext: "AAAAAAAAAAAAAAAAAAAAAA==", + Ciphertext: `{"aead":"ChaCha20Poly1305","iv":"s3fSZ6vk5m+DfQA8yZWeUg==","nonce":"8/kHMnCMs3h9NZ2a","bytes":"cw22HjLq/4cx8507SW4hhSrYbDiMuRao4b5+GE+XfbE="}`, // JSON + }, + { // 2 + Key: mustSecretKey(ChaCha20, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), + Plaintext: "AAAAAAAAAAAAAAAAAAAAAA==", + Ciphertext: `{"aead":"ChaCha20Poly1305","id":"66687aadf862bd776c8fc18b8e9f8e20","iv":"EC0eZp7Pqt+LnkOae5xaAg==","nonce":"X1ejXKmH/ugFZPkk","bytes":"wIGBTDs6aOvsqJfekZ0PYRT/OHyFX2TXqeNwl1SLXOI="}`, // JSON + }, + { // 3 + Key: mustSecretKey(AES256, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), + Plaintext: "AAAAAAAAAAAAAAAAAAAAAA==", + Ciphertext: string(mustDecodeB64("lbFBRVMyNTYtR0NNX1NIQTI1NtkgNjY2ODdhYWRmODYyYmQ3NzZjOGZjMThiOGU5ZjhlMjDEEExv7LAd4oz0SaHZrX5LBufEDEKME1ow1CDfUFrqv8QgJuy7Sw+jVqz99TK1HV851LT3K4mwwDv46TB2ngWkAJQ=")), // MSGP + }, + { // 4 + Key: mustSecretKey(ChaCha20, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), + Plaintext: "AAAAAAAAAAAAAAAAAAAAAA==", + Ciphertext: string(mustDecodeB64("lbJYQ0hBQ0hBMjAtUE9MWTEzMDXZIDY2Njg3YWFkZjg2MmJkNzc2YzhmYzE4YjhlOWY4ZTIwxBBAr+aptD4x2+qfOhiErbnkxAxYs8RmNC1JJXD1hiHEIJ2KqM0jjkME7ndx8nyVseesN83Np0rM5ejVUun+fNFu")), // MSGP + }, + { // 5 + Key: mustSecretKey(AES256, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), + Plaintext: "AAAAAAAAAAAAAAAAAAAAAA==", + Ciphertext: string(mustDecodeB64("zwdDgHeRlIFRnJ7+DIhs/ka7GFK2CFSfDELqg1VCyTQzZ58o7MAdupMLk3ZjlMo2ZUDwldL2o41nAWDc")), + }, + { // 6 + Key: mustSecretKey(AES256, "dDHbTWgo+Yh3u804SYB5OyVMy6RiLeJYBQth1f6KlEU="), + Plaintext: "CLcJoykFCWZDkEIiUq9bJRqwCwW9ZDvdgu8EMA==", + AssociatedData: "AAAAAAAAAAAAAAAAAAAAAA==", + Ciphertext: string(mustDecodeB64("s2gmfQHeGdlyL8x1yEWSACUV3GrSoz3t160hugMgzKWgyqesXDVUJ5Dw5Mt076rR1PNiU9X4YjLH14D8a81t2r2xsz4gVZac")), + }, + { // 7 + Key: mustSecretKey(ChaCha20, "dDHbTWgo+Yh3u804SYB5OyVMy6RiLeJYBQth1f6KlEU="), + Plaintext: "CLcJoykFCWZDkEIiUq9bJRqwCwW9ZDvdgu8EMA==", + AssociatedData: "AAAAAAAAAAAAAAAAAAAAAA==", + Ciphertext: string(mustDecodeB64("gO4woRIswAJdjUjW7z2ApsSQxJlwB24yjLrmH4eI7sB0uh5nfEJfk9ybTPFft5FRFaZCVBmzhx7OJs9n0WWtxH3sySIxecIK")), + }, + + { // 8 + Key: mustSecretKey(AES256, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), + Ciphertext: `{"aead":"AES-256-GCM","iv":"xLxIN3tSCkg2xMafuvwUwg==","nonce":"gu0mGwUkwcvMEoi5","bytes":"WVgRjeIJm3w50C/l+y7y2i6mbNg5NCAqN1zvOYWZKmc="}`, + ShouldFail: true, // Invalid algorithm + }, + { // 9 + Key: mustSecretKey(AES256, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), + Ciphertext: `{"aead":"AES-256-GCM-HMAC-SHA-256","iv":"EjOY4JKqjIrPmQ5z1KSR8zlhggY=","nonce":"gu0mGwUkwcvMEoi5","bytes":"WVgRjeIJm3w50C/l+y7y2i6mbNg5NCAqN1zvOYWZKmc="}`, + ShouldFail: true, // invalid IV length + }, + { // 10 + Key: mustSecretKey(ChaCha20, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), + Ciphertext: `{"aead":"ChaCha20Poly1305","iv":"s3fSZ6vk5m+DfQA8yZWeUg==","nonce":"SXAbms731/c=","bytes":"cw22HjLq/4cx8507SW4hhSrYbDiMuRao4b5+GE+XfbE="}`, + ShouldFail: true, // invalid nonce length + }, + { // 11 + Key: mustSecretKey(AES256, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), + Ciphertext: `{"aead":"AES-256-GCM-HMAC-SHA-256","iv":"xLxIN3tSCkg2xMafuvwUwg==","nonce":"efY+4kYF9n8=","bytes":"WVgRjeIJm3w50C/l+y7y2i6mbNg5NCAqN1zvOYWZKmc="}`, + ShouldFail: true, // invalid nonce length + }, + { // 12 + Key: mustSecretKey(AES256, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), + Ciphertext: `{"aead":"AES-256-GCM-HMAC-SHA-256","iv":"xLxIN3tSCkg2xMafuvwUwg==","nonce":"gu0mGwUkwcvMEoi5","bytes":"QTza1g5oX3f9cGJMbY1xJwWPj1F7R2VnNl6XpFKYQy0="}`, + ShouldFail: true, // ciphertext not authentic + }, + { // 13 + Key: mustSecretKey(ChaCha20, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), + Ciphertext: `{"aead":"ChaCha20Poly1305","iv":"s3fSZ6vk5m+DfQA8yZWeUg==","nonce":"8/kHMnCMs3h9NZ2a","bytes":"TTi8pkO+Jh1JWAHvPxZeUk/iVoBPUCE4ZSVGBy3fW2s="}`, + ShouldFail: true, // ciphertext not authentic + }, + { // 14 + Key: mustSecretKey(AES256, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), + Ciphertext: `{"aead":"AES-256-GCM-HMAC-SHA-256" "iv":"xLxIN3tSCkg2xMafuvwUwg==","nonce":"gu0mGwUkwcvMEoi5","bytes":"WVgRjeIJm3w50C/l+y7y2i6mbNg5NCAqN1zvOYWZKmc="}`, + ShouldFail: true, // invalid JSON + }, +} + +var parseKeyVersionTests = []struct { + Raw string + Key KeyVersion + ShouldFail bool +}{ + { + Raw: `{"bytes":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="}`, + Key: KeyVersion{ + Key: mustSecretKey(AES256, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), + }, + }, + { + Raw: `{"bytes":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE="}`, + Key: KeyVersion{ + Key: mustSecretKey(AES256, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE="), + }, + }, + { + Raw: `{"bytes":"J8qmOyEV2ce2yoAC+5t0Y7CSP/hTMppL7XHpAnyc+0E="}`, + Key: KeyVersion{ + Key: mustSecretKey(AES256, "J8qmOyEV2ce2yoAC+5t0Y7CSP/hTMppL7XHpAnyc+0E="), + }, + }, + { + Raw: `{"bytes":"J8qmOyEV2ce2yoAC+5t0Y7CSP/hTMppL7XHpAnyc+0E=","algorithm":"AES256-GCM_SHA256","created_at":"2009-11-10T23:00:00Z","created_by":"40235905b7b83e0537a002db523cd019d6709b899adc249c957860cd00fa9f78"}`, + Key: KeyVersion{ + Key: mustSecretKey(AES256, "J8qmOyEV2ce2yoAC+5t0Y7CSP/hTMppL7XHpAnyc+0E="), + CreatedAt: mustTime("2009-11-10T23:00:00Z"), + CreatedBy: "40235905b7b83e0537a002db523cd019d6709b899adc249c957860cd00fa9f78", + }, + }, + { + Raw: `{"bytes":"9ew6BCae3+13sniOUwttEJ62amg98YXc0OW0WBhNiCY=","algorithm":"XCHACHA20-POLY1305","created_at":"2009-11-10T23:00:00Z","created_by":"189d9de5331e3ee8abe9e4bd40d474ad621d79ccf83a711f6ac68050eb15a52a"}`, + Key: KeyVersion{ + Key: mustSecretKey(ChaCha20, "9ew6BCae3+13sniOUwttEJ62amg98YXc0OW0WBhNiCY="), + CreatedAt: mustTime("2009-11-10T23:00:00Z"), + CreatedBy: "189d9de5331e3ee8abe9e4bd40d474ad621d79ccf83a711f6ac68050eb15a52a", + }, + }, + { + Raw: "CiQKIMiHG7XrN94FuwblJJor4f7f6rbqAl7DwsLaiIoyz0D2EAESJAognLXl4yqa4cjZTcSbsU6sNnN8kP9ARWkXwa20YQZa9HgQARoMCNithK0GEID67qYDIkAzZWNmY2RmMzhmY2JlMTQxYWUyNmExMDMwZjgxZTk2Yjc1MzM2NWE0Njc2MGFlNmI1Nzg2OThhOTdjNTlmZDIy", + Key: KeyVersion{ + Key: mustSecretKey(AES256, "yIcbtes33gW7BuUkmivh/t/qtuoCXsPCwtqIijLPQPY="), + HMACKey: mustHMACKey(SHA256, "nLXl4yqa4cjZTcSbsU6sNnN8kP9ARWkXwa20YQZa9Hg="), + CreatedAt: mustTime("2024-01-12T11:39:20.886816+01:00"), + CreatedBy: "3ecfcdf38fcbe141ae26a1030f81e96b753365a46760ae6b578698a97c59fd22", + }, + }, + {Raw: `"bytes":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="}`, ShouldFail: true}, // Missing: { + {Raw: `{bytes":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="}`, ShouldFail: true}, // Missing first: " + {Raw: `{"bytes""AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="}`, ShouldFail: true}, // Missing: : + {Raw: `"bytes":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="`, ShouldFail: true}, // Missing final } +} + +func mustSecretKey(cipher SecretKeyType, base64Key string) SecretKey { + key, err := base64.StdEncoding.DecodeString(base64Key) + if err != nil { + panic(err) + } + sk, err := NewSecretKey(cipher, key) + if err != nil { + panic(err) + } + return sk +} + +func mustHMACKey(hash Hash, base64Key string) HMACKey { + key, err := base64.StdEncoding.DecodeString(base64Key) + if err != nil { + panic(err) + } + sk, err := NewHMACKey(hash, key) + if err != nil { + panic(err) + } + return sk +} + +func mustDecodeB64(s string) []byte { + b, err := base64.StdEncoding.DecodeString(s) + if err != nil { + panic(err) + } + return b +} + +func mustTime(s string) time.Time { + t, err := time.Parse(time.RFC3339, s) + if err != nil { + t, err = time.Parse(time.RFC3339Nano, s) + if err != nil { + panic(err) + } + } + t = t.UTC() + return t +} diff --git a/internal/key/key.go b/internal/key/key.go deleted file mode 100644 index 351dab3f..00000000 --- a/internal/key/key.go +++ /dev/null @@ -1,309 +0,0 @@ -// Copyright 2022 - MinIO, Inc. All rights reserved. -// Use of this source code is governed by the AGPLv3 -// license that can be found in the LICENSE file. - -package key - -import ( - "bytes" - "crypto/aes" - "crypto/cipher" - "crypto/hmac" - "crypto/rand" - "crypto/sha256" - "crypto/subtle" - "encoding" - "encoding/gob" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "time" - - "github.com/minio/kes-go" - "github.com/minio/kes/internal/fips" - "golang.org/x/crypto/chacha20" - "golang.org/x/crypto/chacha20poly1305" -) - -const ( - // MaxSize is the maximum byte size of an encoded key. - MaxSize = 1 << 20 - - // Size is the byte size of a cryptographic key. - Size = 256 / 8 -) - -// Parse parses b as encoded Key. -func Parse(b []byte) (Key, error) { - var key Key - if err := key.UnmarshalText(b); err != nil { - return Key{}, err - } - return key, nil -} - -// Len returns the length of keys for the given Algorithm in bytes. -func Len(a kes.KeyAlgorithm) int { - switch a { - case kes.AES256: - return 256 / 8 - case kes.ChaCha20: - return 256 / 8 - default: - fmt.Println(int(a)) - return -1 - } -} - -// New returns an new Key for the given cryptographic algorithm. -// The key len must match algorithm's key size. The returned key -// is owned to the specified identity. -func New(algorithm kes.KeyAlgorithm, key []byte, owner kes.Identity) (Key, error) { - if len(key) != Len(algorithm) { - return Key{}, errors.New("key: invalid key size") - } - return Key{ - bytes: clone(key...), - algorithm: algorithm, - createdAt: time.Now().UTC(), - createdBy: owner, - }, nil -} - -// Random generates a new random Key for the cryptographic algorithm. -// The returned key is owned to the specified identity. -func Random(algorithm kes.KeyAlgorithm, owner kes.Identity) (Key, error) { - key, err := randomBytes(Len(algorithm)) - if err != nil { - return Key{}, err - } - return New(algorithm, key, owner) -} - -func randomBytes(length int) ([]byte, error) { - b := make([]byte, length) - if _, err := rand.Read(b); err != nil { - return nil, err - } - return b, nil -} - -// Key is a symmetric cryptographic key. -type Key struct { - bytes []byte - - algorithm kes.KeyAlgorithm - createdAt time.Time - createdBy kes.Identity -} - -var ( - _ encoding.TextMarshaler = Key{} - _ encoding.BinaryMarshaler = Key{} - _ encoding.TextUnmarshaler = (*Key)(nil) - _ encoding.BinaryUnmarshaler = (*Key)(nil) -) - -// Algorithm returns the cryptographic algorithm for which the -// key can be used. -func (k *Key) Algorithm() kes.KeyAlgorithm { return k.algorithm } - -// CreatedAt returns the point in time when the key has -// been created. -func (k *Key) CreatedAt() time.Time { return k.createdAt } - -// CreatedBy returns the identity that created the key. -func (k *Key) CreatedBy() kes.Identity { return k.createdBy } - -// ID returns the k's key ID. -func (k *Key) ID() string { - const Size = 128 / 8 - h := sha256.Sum256(k.bytes) - return hex.EncodeToString(h[:Size]) -} - -// Clone returns a deep copy of the key. -func (k *Key) Clone() Key { - return Key{ - bytes: clone(k.bytes...), - algorithm: k.Algorithm(), - createdAt: k.CreatedAt(), - createdBy: k.CreatedBy(), - } -} - -// Equal returns true if and only if both keys -// are identical. -func (k *Key) Equal(other Key) bool { - if k.Algorithm() != other.Algorithm() { - return false - } - return subtle.ConstantTimeCompare(k.bytes, other.bytes) == 1 -} - -// MarshalText returns the key's text representation. -func (k Key) MarshalText() ([]byte, error) { - type JSON struct { - Version version `json:"version"` - Bytes []byte `json:"bytes"` - Algorithm kes.KeyAlgorithm `json:"algorithm,omitempty"` - CreatedAt time.Time `json:"created_at,omitempty"` - CreatedBy kes.Identity `json:"created_by,omitempty"` - } - return json.Marshal(JSON{ - Version: v1, - Bytes: k.bytes, - Algorithm: k.Algorithm(), - CreatedAt: k.CreatedAt(), - CreatedBy: k.CreatedBy(), - }) -} - -// UnmarshalText parses and decodes text as encoded key. -func (k *Key) UnmarshalText(text []byte) error { - type JSON struct { - Version version `json:"version"` - Bytes []byte `json:"bytes"` - Algorithm kes.KeyAlgorithm `json:"algorithm"` - CreatedAt time.Time `json:"created_at"` - CreatedBy kes.Identity `json:"created_by"` - } - var value JSON - if err := json.Unmarshal(text, &value); err != nil { - return err - } - k.bytes = value.Bytes - k.algorithm = value.Algorithm - k.createdAt = value.CreatedAt - k.createdBy = value.CreatedBy - return nil -} - -// MarshalBinary returns the Key's binary representation. -func (k Key) MarshalBinary() ([]byte, error) { - type GOB struct { - Version version - Bytes []byte - Algorithm kes.KeyAlgorithm - CreatedAt time.Time - CreatedBy kes.Identity - } - - var buffer bytes.Buffer - err := gob.NewEncoder(&buffer).Encode(GOB{ - Version: v1, - Bytes: k.bytes, - Algorithm: k.Algorithm(), - CreatedAt: k.CreatedAt(), - CreatedBy: k.CreatedBy(), - }) - return buffer.Bytes(), err -} - -// UnmarshalBinary unmarshals the Key's binary representation. -func (k *Key) UnmarshalBinary(b []byte) error { - type GOB struct { - Version version - Bytes []byte - Algorithm kes.KeyAlgorithm - CreatedAt time.Time - CreatedBy kes.Identity - } - - var value GOB - if err := gob.NewDecoder(bytes.NewReader(b)).Decode(&value); err != nil { - return err - } - k.bytes = value.Bytes - k.algorithm = value.Algorithm - k.createdAt = value.CreatedAt - k.createdBy = value.CreatedBy - return nil -} - -// Wrap encrypts the given plaintext and binds -// the associatedData to the returned ciphertext. -// -// To unwrap the ciphertext the same associatedData -// has to be provided again. -func (k *Key) Wrap(plaintext, associatedData []byte) ([]byte, error) { - iv, err := randomBytes(16) - if err != nil { - return nil, err - } - - algorithm := k.Algorithm() - cipher, err := newAEAD(algorithm, k.bytes, iv) - if err != nil { - return nil, err - } - - nonce, err := randomBytes(cipher.NonceSize()) - if err != nil { - return nil, err - } - ciphertext := ciphertext{ - Algorithm: algorithm, - ID: k.ID(), - IV: iv, - Nonce: nonce, - Bytes: cipher.Seal(nil, nonce, plaintext, associatedData), - } - return ciphertext.MarshalBinary() -} - -// Unwrap decrypts the ciphertext and returns the -// resulting plaintext. -// -// It verifies that the associatedData matches the -// value used when the ciphertext has been generated. -func (k *Key) Unwrap(ciphertext, associatedData []byte) ([]byte, error) { - text, err := decodeCiphertext(ciphertext) - if err != nil { - return nil, kes.ErrDecrypt - } - - cipher, err := newAEAD(text.Algorithm, k.bytes, text.IV) - if err != nil { - return nil, kes.ErrDecrypt - } - plaintext, err := cipher.Open(nil, text.Nonce, text.Bytes, associatedData) - if err != nil { - return nil, kes.ErrDecrypt - } - return plaintext, nil -} - -// newAEAD returns a new AEAD cipher that implements the given -// algorithm and is initialized with the given key and iv. -func newAEAD(algorithm kes.KeyAlgorithm, Key, IV []byte) (cipher.AEAD, error) { - switch algorithm { - case kes.AES256: - mac := hmac.New(sha256.New, Key) - mac.Write(IV) - sealingKey := mac.Sum(nil) - - block, err := aes.NewCipher(sealingKey) - if err != nil { - return nil, err - } - return cipher.NewGCM(block) - case kes.ChaCha20: - if fips.Enabled { - return nil, kes.ErrDecrypt - } - sealingKey, err := chacha20.HChaCha20(Key, IV) - if err != nil { - return nil, err - } - return chacha20poly1305.New(sealingKey) - default: - return nil, kes.ErrDecrypt - } -} - -func clone(b ...byte) []byte { - c := make([]byte, 0, len(b)) - return append(c, b...) -} diff --git a/internal/key/key_test.go b/internal/key/key_test.go deleted file mode 100644 index 23f55f95..00000000 --- a/internal/key/key_test.go +++ /dev/null @@ -1,248 +0,0 @@ -// Copyright 2022 - MinIO, Inc. All rights reserved. -// Use of this source code is governed by the AGPLv3 -// license that can be found in the LICENSE file. - -package key - -import ( - "bytes" - "encoding/base64" - "encoding/hex" - "testing" - "time" - - "github.com/minio/kes-go" -) - -var parseTests = []struct { - Raw string - - Bytes []byte - Algorithm kes.KeyAlgorithm - CreatedAt time.Time - CreatedBy kes.Identity - - ShouldFail bool -}{ - {Raw: `{"bytes":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="}`, Bytes: make([]byte, 32)}, - {Raw: `{"bytes":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE="}`, Bytes: append(make([]byte, 31), 1)}, - {Raw: `{"bytes":"J8qmOyEV2ce2yoAC+5t0Y7CSP/hTMppL7XHpAnyc+0E="}`, Bytes: mustDecodeHex("27caa63b2115d9c7b6ca8002fb9b7463b0923ff853329a4bed71e9027c9cfb41")}, - - { - Raw: `{"bytes":"J8qmOyEV2ce2yoAC+5t0Y7CSP/hTMppL7XHpAnyc+0E=","algorithm":"AES256-GCM_SHA256","created_at":"2009-11-10T23:00:00Z","created_by":"40235905b7b83e0537a002db523cd019d6709b899adc249c957860cd00fa9f78"}`, - Bytes: mustDecodeHex("27caa63b2115d9c7b6ca8002fb9b7463b0923ff853329a4bed71e9027c9cfb41"), - Algorithm: kes.AES256, - CreatedAt: mustDecodeTime("2009-11-10T23:00:00Z"), - CreatedBy: "40235905b7b83e0537a002db523cd019d6709b899adc249c957860cd00fa9f78", - }, - { - Raw: `{"bytes":"9ew6BCae3+13sniOUwttEJ62amg98YXc0OW0WBhNiCY=","algorithm":"XCHACHA20-POLY1305","created_at":"2009-11-10T23:00:00Z","created_by":"189d9de5331e3ee8abe9e4bd40d474ad621d79ccf83a711f6ac68050eb15a52a"}`, - Bytes: mustDecodeHex("f5ec3a04269edfed77b2788e530b6d109eb66a683df185dcd0e5b458184d8826"), - Algorithm: kes.ChaCha20, - CreatedAt: mustDecodeTime("2009-11-10T23:00:00Z"), - CreatedBy: "189d9de5331e3ee8abe9e4bd40d474ad621d79ccf83a711f6ac68050eb15a52a", - }, - - {Raw: `"bytes":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="}`, ShouldFail: true}, // Missing: { - {Raw: `{bytes":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="}`, ShouldFail: true}, // Missing first: " - {Raw: `{"bytes""AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="}`, ShouldFail: true}, // Missing: : - {Raw: `"bytes":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="`, ShouldFail: true}, // Missing final } -} - -func TestParse(t *testing.T) { - for i, test := range parseTests { - key, err := Parse([]byte(test.Raw)) - if err != nil && !test.ShouldFail { - t.Fatalf("Test %d: Failed to parse key: %v", i, err) - } - if err == nil && test.ShouldFail { - t.Fatalf("Test %d: Parsing should have failed but succeeded", i) - } - if err == nil { - if !bytes.Equal(key.bytes, test.Bytes) { - t.Fatalf("Test %d: got %x - want %x", i, key.bytes, test.Bytes) - } - if key.Algorithm() != test.Algorithm { - t.Fatalf("Test %d: algorithm mismatch: got %v - want %v", i, key.Algorithm(), test.Algorithm) - } - if key.CreatedAt() != test.CreatedAt { - t.Fatalf("Test %d: created at mismatch: got %v - want %v", i, key.CreatedAt(), test.CreatedAt) - } - if key.CreatedBy() != test.CreatedBy { - t.Fatalf("Test %d: created by mismatch: got %v - want %v", i, key.CreatedBy(), test.CreatedBy) - } - } - } -} - -var keyWrapTests = []struct { - KeyLen int - AssociatedData []byte -}{ - {KeyLen: 0, AssociatedData: nil}, // 0 - {KeyLen: 1, AssociatedData: nil}, // 1 - {KeyLen: 16, AssociatedData: make([]byte, 0)}, // 2 - {KeyLen: 32, AssociatedData: mustDecodeHex("ff")}, // 3 - {KeyLen: 128, AssociatedData: make([]byte, 1024)}, // 4 - {KeyLen: 1024, AssociatedData: mustDecodeHex("a2e31cb681f3")}, // 5 - {KeyLen: 63, AssociatedData: mustDecodeHex("cb653b4c5426e0d41f5ae673ffa0f659")}, // 6 -} - -func TestKeyWrap(t *testing.T) { - algorithms := []kes.KeyAlgorithm{kes.AES256, kes.ChaCha20} - for _, a := range algorithms { - key, err := Random(a, "") - if err != nil { - t.Fatalf("Failed to create key: %v", err) - } - for i, test := range keyWrapTests { - data := make([]byte, test.KeyLen) - ciphertext, err := key.Wrap(data, test.AssociatedData) - if err != nil { - t.Logf("Test %d: Algorithm: %v , Secret: %x\n", i, key.Algorithm(), key.bytes) - t.Fatalf("Test %d: Failed to wrap data: %v", i, err) - } - plaintext, err := key.Unwrap(ciphertext, test.AssociatedData) - if err != nil { - t.Logf("Test %d: Algorithm: %v , Secret: %x\n", i, key.Algorithm(), key.bytes) - t.Fatalf("Test %d: Failed to unwrap data: %v", i, err) - } - if !bytes.Equal(data, plaintext) { - t.Logf("Test %d: Secret: %x\n", i, key.bytes) - t.Fatalf("Test %d: Original plaintext does not match unwrapped plaintext", i) - } - } - } -} - -var keyUnwrapTests = []struct { - Algorithm kes.KeyAlgorithm - Ciphertext string - AssociatedData []byte - - ShouldFail bool - Err error -}{ - { // 0 - Algorithm: kes.AES256, - Ciphertext: `{"aead":"AES-256-GCM-HMAC-SHA-256","iv":"xLxIN3tSCkg2xMafuvwUwg==","nonce":"gu0mGwUkwcvMEoi5","bytes":"WVgRjeIJm3w50C/l+y7y2i6mbNg5NCAqN1zvOYWZKmc="}`, - AssociatedData: nil, - }, - { // 1 - Algorithm: kes.ChaCha20, - Ciphertext: `{"aead":"ChaCha20Poly1305","iv":"s3fSZ6vk5m+DfQA8yZWeUg==","nonce":"8/kHMnCMs3h9NZ2a","bytes":"cw22HjLq/4cx8507SW4hhSrYbDiMuRao4b5+GE+XfbE="}`, - AssociatedData: nil, - }, - { // 2 - Algorithm: kes.ChaCha20, - Ciphertext: `{"aead":"ChaCha20Poly1305","id":"66687aadf862bd776c8fc18b8e9f8e20","iv":"EC0eZp7Pqt+LnkOae5xaAg==","nonce":"X1ejXKmH/ugFZPkk","bytes":"wIGBTDs6aOvsqJfekZ0PYRT/OHyFX2TXqeNwl1SLXOI="}`, - AssociatedData: nil, - }, - { // 3 - Algorithm: kes.AES256, - Ciphertext: string(mustDecodeB64("lbFBRVMyNTYtR0NNX1NIQTI1NtkgNjY2ODdhYWRmODYyYmQ3NzZjOGZjMThiOGU5ZjhlMjDEEExv7LAd4oz0SaHZrX5LBufEDEKME1ow1CDfUFrqv8QgJuy7Sw+jVqz99TK1HV851LT3K4mwwDv46TB2ngWkAJQ=")), - AssociatedData: nil, - }, - { // 4 - Algorithm: kes.ChaCha20, - Ciphertext: string(mustDecodeB64("lbJYQ0hBQ0hBMjAtUE9MWTEzMDXZIDY2Njg3YWFkZjg2MmJkNzc2YzhmYzE4YjhlOWY4ZTIwxBBAr+aptD4x2+qfOhiErbnkxAxYs8RmNC1JJXD1hiHEIJ2KqM0jjkME7ndx8nyVseesN83Np0rM5ejVUun+fNFu")), - AssociatedData: nil, - }, - - { // 5 - Algorithm: kes.AES256, - Ciphertext: `{"aead":"AES-256-GCM","iv":"xLxIN3tSCkg2xMafuvwUwg==","nonce":"gu0mGwUkwcvMEoi5","bytes":"WVgRjeIJm3w50C/l+y7y2i6mbNg5NCAqN1zvOYWZKmc="}`, - AssociatedData: nil, - ShouldFail: true, // Invalid algorithm - Err: kes.ErrDecrypt, - }, - { // 6 - Algorithm: kes.AES256, - Ciphertext: `{"aead":"AES-256-GCM-HMAC-SHA-256","iv":"EjOY4JKqjIrPmQ5z1KSR8zlhggY=","nonce":"gu0mGwUkwcvMEoi5","bytes":"WVgRjeIJm3w50C/l+y7y2i6mbNg5NCAqN1zvOYWZKmc="}`, - AssociatedData: nil, - ShouldFail: true, // invalid IV length - Err: kes.ErrDecrypt, - }, - { // 7 - Algorithm: kes.ChaCha20, - Ciphertext: `{"aead":"ChaCha20Poly1305","iv":"s3fSZ6vk5m+DfQA8yZWeUg==","nonce":"SXAbms731/c=","bytes":"cw22HjLq/4cx8507SW4hhSrYbDiMuRao4b5+GE+XfbE="}`, - AssociatedData: nil, - ShouldFail: true, // invalid nonce length - Err: kes.ErrDecrypt, - }, - { // 8 - Algorithm: kes.AES256, - Ciphertext: `{"aead":"AES-256-GCM-HMAC-SHA-256","iv":"xLxIN3tSCkg2xMafuvwUwg==","nonce":"efY+4kYF9n8=","bytes":"WVgRjeIJm3w50C/l+y7y2i6mbNg5NCAqN1zvOYWZKmc="}`, - AssociatedData: nil, - ShouldFail: true, // invalid nonce length - Err: kes.ErrDecrypt, - }, - { // 9 - Algorithm: kes.AES256, - Ciphertext: `{"aead":"AES-256-GCM-HMAC-SHA-256","iv":"xLxIN3tSCkg2xMafuvwUwg==","nonce":"gu0mGwUkwcvMEoi5","bytes":"QTza1g5oX3f9cGJMbY1xJwWPj1F7R2VnNl6XpFKYQy0="}`, - AssociatedData: nil, - ShouldFail: true, // ciphertext not authentic - Err: kes.ErrDecrypt, - }, - { // 10 - Algorithm: kes.ChaCha20, - Ciphertext: `{"aead":"ChaCha20Poly1305","iv":"s3fSZ6vk5m+DfQA8yZWeUg==","nonce":"8/kHMnCMs3h9NZ2a","bytes":"TTi8pkO+Jh1JWAHvPxZeUk/iVoBPUCE4ZSVGBy3fW2s="}`, - AssociatedData: nil, - ShouldFail: true, // ciphertext not authentic - Err: kes.ErrDecrypt, - }, - { // 11 - Algorithm: kes.AES256, - Ciphertext: `{"aead":"AES-256-GCM-HMAC-SHA-256" "iv":"xLxIN3tSCkg2xMafuvwUwg==","nonce":"gu0mGwUkwcvMEoi5","bytes":"WVgRjeIJm3w50C/l+y7y2i6mbNg5NCAqN1zvOYWZKmc="}`, - AssociatedData: nil, - ShouldFail: true, // invalid JSON - Err: kes.ErrDecrypt, - }, -} - -func TestKeyUnwrap(t *testing.T) { - Plaintext := make([]byte, 16) - for i, test := range keyUnwrapTests { - key, err := New(test.Algorithm, make([]byte, Len(test.Algorithm)), "") - if err != nil { - t.Fatalf("Test %d: Failed to create key: %v", i, err) - } - plaintext, err := key.Unwrap([]byte(test.Ciphertext), test.AssociatedData) - if err != nil && !test.ShouldFail { - t.Fatalf("Test %d: Failed to unwrap ciphertext: %v", i, err) - } - if err == nil && test.ShouldFail { - t.Fatalf("Test %d: Expected to fail but succeeded", i) - } - if test.ShouldFail && err != test.Err { - t.Fatalf("Test %d: Invalid error response: got %v - want %v", i, err, test.Err) - } - if !test.ShouldFail && !bytes.Equal(plaintext, Plaintext) { - t.Fatalf("Test %d: Plaintext mismatch: got %x - want %x", i, plaintext, Plaintext) - } - } -} - -func mustDecodeTime(s string) time.Time { - t, err := time.Parse(time.RFC3339, s) - if err != nil { - panic(err) - } - return t -} - -func mustDecodeHex(s string) []byte { - b, err := hex.DecodeString(s) - if err != nil { - panic(err) - } - return b -} - -func mustDecodeB64(s string) []byte { - b, err := base64.StdEncoding.DecodeString(s) - if err != nil { - panic(err) - } - return b -} diff --git a/internal/key/version.go b/internal/key/version.go deleted file mode 100644 index c3b3b2cd..00000000 --- a/internal/key/version.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2022 - MinIO, Inc. All rights reserved. -// Use of this source code is governed by the AGPLv3 -// license that can be found in the LICENSE file. - -package key - -import ( - "errors" - "strconv" -) - -const ( - v1 version = 1 -) - -// version is an enum representing the format version -// of text/binary encoded keys. -type version uint - -// String returns the version's string representation. -func (v version) String() string { - switch v { - case v1: - return "v1" - default: - return "invalid version '" + strconv.Itoa(int(v)) + "'" - } -} - -// String returns the version's text representation. -// In contrast to String, it returns an error for invalid -// versions. -func (v version) MarshalText() ([]byte, error) { - switch v { - case v1: - return []byte("v1"), nil - default: - return nil, errors.New("key: invalid version '" + strconv.Itoa(int(v)) + "'") - } -} - -// UnmarshalText parses text as version text representation. -func (v *version) UnmarshalText(text []byte) error { - switch s := string(text); s { - case "v1": - *v = v1 - return nil - default: - return errors.New("key: invalid version '" + s + "'") - } -} diff --git a/internal/keystore/azure/client.go b/internal/keystore/azure/client.go index de445ef9..444099bf 100644 --- a/internal/keystore/azure/client.go +++ b/internal/keystore/azure/client.go @@ -18,7 +18,6 @@ import ( "aead.dev/mem" "github.com/Azure/go-autorest/autorest" xhttp "github.com/minio/kes/internal/http" - "github.com/minio/kes/internal/key" ) // status represents a KeyVault operation results. @@ -147,8 +146,8 @@ func (c *client) GetSecret(ctx context.Context, name, version string) (string, s } limit := mem.Size(resp.ContentLength) - if limit < 0 || limit > key.MaxSize { - limit = key.MaxSize + if limit < 0 || limit > mem.MB { + limit = mem.MB } var response Response if err = json.NewDecoder(mem.LimitReader(resp.Body, limit)).Decode(&response); err != nil { diff --git a/internal/keystore/fortanix/keystore.go b/internal/keystore/fortanix/keystore.go index e41e4640..d76ea8eb 100644 --- a/internal/keystore/fortanix/keystore.go +++ b/internal/keystore/fortanix/keystore.go @@ -26,7 +26,6 @@ import ( "github.com/minio/kes" kesdk "github.com/minio/kes-go" xhttp "github.com/minio/kes/internal/http" - "github.com/minio/kes/internal/key" "github.com/minio/kes/internal/keystore" ) @@ -312,7 +311,7 @@ func (s *Store) Delete(ctx context.Context, name string) error { KeyID string `json:"kid"` } var response Response - if err := json.NewDecoder(mem.LimitReader(resp.Body, key.MaxSize)).Decode(&response); err != nil { + if err := json.NewDecoder(mem.LimitReader(resp.Body, mem.MB)).Decode(&response); err != nil { return fmt.Errorf("fortanix: failed to delete '%s': failed to parse key metadata: %v", name, err) } @@ -387,7 +386,7 @@ func (s *Store) Get(ctx context.Context, name string) ([]byte, error) { Enabled bool `json:"enabled"` } var response Response - if err := json.NewDecoder(mem.LimitReader(resp.Body, key.MaxSize)).Decode(&response); err != nil { + if err := json.NewDecoder(mem.LimitReader(resp.Body, mem.MB)).Decode(&response); err != nil { return nil, fmt.Errorf("fortanix: failed to fetch '%s': failed to parse server response %v", name, err) } if !response.Enabled { @@ -440,7 +439,7 @@ func (s *Store) List(ctx context.Context, prefix string, n int) ([]string, strin Name string `json:"name"` } var keys []Response - if err := json.NewDecoder(mem.LimitReader(resp.Body, 10*key.MaxSize)).Decode(&keys); err != nil { + if err := json.NewDecoder(mem.LimitReader(resp.Body, 10*mem.MB)).Decode(&keys); err != nil { return nil, "", fmt.Errorf("fortanix: failed to list keys: failed to parse server response: %v", err) } if len(keys) == 0 { diff --git a/internal/protobuf/crypto.pb.go b/internal/protobuf/crypto.pb.go new file mode 100644 index 00000000..67370dc3 --- /dev/null +++ b/internal/protobuf/crypto.pb.go @@ -0,0 +1,335 @@ +// Copyright 2024 - MinIO, Inc. All rights reserved. +// Use of this source code is governed by the AGPLv3 +// license that can be found in the LICENSE file. + +// Generate the Go protobuf code by running the protobuf compiler +// from the repository root: +// +// $ protoc -I=./internal/protobuf --go_out=. ./internal/protobuf/*.proto + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc v4.25.1 +// source: crypto.proto + +package protobuf + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SecretKey struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key []byte `protobuf:"bytes,1,opt,name=Key,json=key,proto3" json:"Key,omitempty"` + Type uint32 `protobuf:"varint,2,opt,name=Type,json=type,proto3" json:"Type,omitempty"` +} + +func (x *SecretKey) Reset() { + *x = SecretKey{} + if protoimpl.UnsafeEnabled { + mi := &file_crypto_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SecretKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SecretKey) ProtoMessage() {} + +func (x *SecretKey) ProtoReflect() protoreflect.Message { + mi := &file_crypto_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SecretKey.ProtoReflect.Descriptor instead. +func (*SecretKey) Descriptor() ([]byte, []int) { + return file_crypto_proto_rawDescGZIP(), []int{0} +} + +func (x *SecretKey) GetKey() []byte { + if x != nil { + return x.Key + } + return nil +} + +func (x *SecretKey) GetType() uint32 { + if x != nil { + return x.Type + } + return 0 +} + +type HMACKey struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key []byte `protobuf:"bytes,1,opt,name=Key,json=key,proto3" json:"Key,omitempty"` + Hash uint32 `protobuf:"varint,2,opt,name=Hash,json=hash,proto3" json:"Hash,omitempty"` +} + +func (x *HMACKey) Reset() { + *x = HMACKey{} + if protoimpl.UnsafeEnabled { + mi := &file_crypto_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HMACKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HMACKey) ProtoMessage() {} + +func (x *HMACKey) ProtoReflect() protoreflect.Message { + mi := &file_crypto_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HMACKey.ProtoReflect.Descriptor instead. +func (*HMACKey) Descriptor() ([]byte, []int) { + return file_crypto_proto_rawDescGZIP(), []int{1} +} + +func (x *HMACKey) GetKey() []byte { + if x != nil { + return x.Key + } + return nil +} + +func (x *HMACKey) GetHash() uint32 { + if x != nil { + return x.Hash + } + return 0 +} + +type KeyVersion struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key *SecretKey `protobuf:"bytes,1,opt,name=Key,json=key,proto3" json:"Key,omitempty"` + HMACKey *HMACKey `protobuf:"bytes,2,opt,name=HMACKey,json=hmac_key,proto3" json:"HMACKey,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=CreatedAt,json=created_at,proto3" json:"CreatedAt,omitempty"` + CreatedBy string `protobuf:"bytes,4,opt,name=CreatedBy,json=created_by,proto3" json:"CreatedBy,omitempty"` +} + +func (x *KeyVersion) Reset() { + *x = KeyVersion{} + if protoimpl.UnsafeEnabled { + mi := &file_crypto_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *KeyVersion) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KeyVersion) ProtoMessage() {} + +func (x *KeyVersion) ProtoReflect() protoreflect.Message { + mi := &file_crypto_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use KeyVersion.ProtoReflect.Descriptor instead. +func (*KeyVersion) Descriptor() ([]byte, []int) { + return file_crypto_proto_rawDescGZIP(), []int{2} +} + +func (x *KeyVersion) GetKey() *SecretKey { + if x != nil { + return x.Key + } + return nil +} + +func (x *KeyVersion) GetHMACKey() *HMACKey { + if x != nil { + return x.HMACKey + } + return nil +} + +func (x *KeyVersion) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *KeyVersion) GetCreatedBy() string { + if x != nil { + return x.CreatedBy + } + return "" +} + +var File_crypto_proto protoreflect.FileDescriptor + +var file_crypto_proto_rawDesc = []byte{ + 0x0a, 0x0c, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0b, + 0x6d, 0x69, 0x6e, 0x69, 0x6f, 0x68, 0x71, 0x2e, 0x6b, 0x6d, 0x73, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x31, 0x0a, 0x09, + 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x4b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x54, + 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, + 0x2f, 0x0a, 0x07, 0x48, 0x4d, 0x41, 0x43, 0x4b, 0x65, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x4b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, + 0x48, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, + 0x22, 0xc1, 0x01, 0x0a, 0x0a, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, + 0x28, 0x0a, 0x03, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, + 0x69, 0x6e, 0x69, 0x6f, 0x68, 0x71, 0x2e, 0x6b, 0x6d, 0x73, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2f, 0x0a, 0x07, 0x48, 0x4d, 0x41, + 0x43, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x69, 0x6e, + 0x69, 0x6f, 0x68, 0x71, 0x2e, 0x6b, 0x6d, 0x73, 0x2e, 0x48, 0x4d, 0x41, 0x43, 0x4b, 0x65, 0x79, + 0x52, 0x08, 0x68, 0x6d, 0x61, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x12, 0x39, 0x0a, 0x09, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x64, 0x5f, 0x61, 0x74, 0x12, 0x1d, 0x0a, 0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, + 0x42, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x5f, 0x62, 0x79, 0x42, 0x13, 0x5a, 0x11, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_crypto_proto_rawDescOnce sync.Once + file_crypto_proto_rawDescData = file_crypto_proto_rawDesc +) + +func file_crypto_proto_rawDescGZIP() []byte { + file_crypto_proto_rawDescOnce.Do(func() { + file_crypto_proto_rawDescData = protoimpl.X.CompressGZIP(file_crypto_proto_rawDescData) + }) + return file_crypto_proto_rawDescData +} + +var file_crypto_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_crypto_proto_goTypes = []interface{}{ + (*SecretKey)(nil), // 0: miniohq.kms.SecretKey + (*HMACKey)(nil), // 1: miniohq.kms.HMACKey + (*KeyVersion)(nil), // 2: miniohq.kms.KeyVersion + (*timestamppb.Timestamp)(nil), // 3: google.protobuf.Timestamp +} +var file_crypto_proto_depIdxs = []int32{ + 0, // 0: miniohq.kms.KeyVersion.Key:type_name -> miniohq.kms.SecretKey + 1, // 1: miniohq.kms.KeyVersion.HMACKey:type_name -> miniohq.kms.HMACKey + 3, // 2: miniohq.kms.KeyVersion.CreatedAt:type_name -> google.protobuf.Timestamp + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_crypto_proto_init() } +func file_crypto_proto_init() { + if File_crypto_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_crypto_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SecretKey); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_crypto_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HMACKey); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_crypto_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*KeyVersion); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_crypto_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_crypto_proto_goTypes, + DependencyIndexes: file_crypto_proto_depIdxs, + MessageInfos: file_crypto_proto_msgTypes, + }.Build() + File_crypto_proto = out.File + file_crypto_proto_rawDesc = nil + file_crypto_proto_goTypes = nil + file_crypto_proto_depIdxs = nil +} diff --git a/internal/protobuf/crypto.proto b/internal/protobuf/crypto.proto new file mode 100644 index 00000000..d49cfdc6 --- /dev/null +++ b/internal/protobuf/crypto.proto @@ -0,0 +1,33 @@ +// Copyright 2024 - MinIO, Inc. All rights reserved. +// Use of this source code is governed by the AGPLv3 +// license that can be found in the LICENSE file. + +// Generate the Go protobuf code by running the protobuf compiler +// from the repository root: +// +// $ protoc -I=./internal/protobuf --go_out=. ./internal/protobuf/*.proto + +syntax = "proto3"; + +package miniohq.kms; + +import "google/protobuf/timestamp.proto"; + +option go_package = "internal/protobuf"; + +message SecretKey { + bytes Key = 1 [ json_name = "key" ]; + uint32 Type = 2 [ json_name = "type" ]; +} + +message HMACKey { + bytes Key = 1 [ json_name = "key" ]; + uint32 Hash = 2 [ json_name = "hash" ]; +} + +message KeyVersion { + SecretKey Key = 1 [ json_name = "key" ]; + HMACKey HMACKey = 2 [ json_name = "hmac_key" ]; + google.protobuf.Timestamp CreatedAt = 3 [ json_name = "created_at" ]; + string CreatedBy = 4 [ json_name = "created_by" ]; +} diff --git a/internal/protobuf/proto.go b/internal/protobuf/proto.go new file mode 100644 index 00000000..dbbb6611 --- /dev/null +++ b/internal/protobuf/proto.go @@ -0,0 +1,85 @@ +// Copyright 2024 - MinIO, Inc. All rights reserved. +// Use of this source code is governed by the AGPLv3 +// license that can be found in the LICENSE file. + +package protobuf + +import ( + "time" + + "google.golang.org/protobuf/proto" + pbt "google.golang.org/protobuf/types/known/timestamppb" +) + +// Marshaler is an interface implemented by types that +// know how to marshal themselves into their protobuf +// representation T. +type Marshaler[T proto.Message] interface { + MarshalPB(T) error +} + +// Unmarshaler is an interface implemented by types that +// know how to unmarshal themselves from their protobuf +// representation T. +type Unmarshaler[T proto.Message] interface { + UnmarshalPB(T) error +} + +// Marshal returns v's protobuf binary data by first converting +// v into its protobuf representation type M and then marshaling +// M into the protobuf wire format. +func Marshal[M any, P Pointer[M], T Marshaler[P]](v T) ([]byte, error) { + var m M + if err := v.MarshalPB(&m); err != nil { + return nil, err + } + + var p P = &m + return proto.Marshal(p) +} + +// Unmarshal unmarshales v from b by first decoding b into v's +// protobuf representation M before converting M to v. It returns +// an error if b is not a valid protobuf representation of v. +func Unmarshal[M any, P Pointer[M], T Unmarshaler[P]](b []byte, v T) error { + var m M + var p P = &m + if err := proto.Unmarshal(b, p); err != nil { + return err + } + return v.UnmarshalPB(p) +} + +// Time returns a new protobuf timestamp from the given t. +func Time(t time.Time) *pbt.Timestamp { return pbt.New(t) } + +// Pointer is a type constraint used to express that some +// type P is a pointer of some other type T such that: +// +// var t T +// var p P = &t +// +// This proposition is useful when unmarshaling data into types +// without additional dynamic dispatch or heap allocations. +// +// A generic function that wants to use the default value of +// some type T but also wants to call pointer receiver methods +// on instances of T has to have two type parameters: +// +// func foo[T any, P pointer[T]]() { +// var t T +// var p P = &t +// } +// +// This functionality cannot be achieved with a single type +// parameter because: +// +// func foo[T proto.Message]() { +// var t T // compiles but t is nil if T is a pointer type +// var t2 T = *new(T) // compiles but t2 is nil if T is a pointer type +// var t3 = T{} // compiler error - e.g. T may be a pointer type +// } +type Pointer[M any] interface { + proto.Message + *M // Anything implementing Pointer must also be a pointer type of M +} diff --git a/keystore.go b/keystore.go index e2542ded..8e279b3f 100644 --- a/keystore.go +++ b/keystore.go @@ -6,6 +6,7 @@ package kes import ( "context" + "encoding/base64" "errors" "io" "slices" @@ -15,8 +16,9 @@ import ( "github.com/minio/kes-go" "github.com/minio/kes/internal/cache" - "github.com/minio/kes/internal/key" + "github.com/minio/kes/internal/crypto" "github.com/minio/kes/internal/keystore" + pb "github.com/minio/kes/internal/protobuf" ) // A KeyStore stores key-value pairs. It provides durable storage for a @@ -236,7 +238,7 @@ type keyCache struct { // A cache entry with a recently used flag. type cacheEntry struct { - Key key.Key + Key crypto.KeyVersion Used atomic.Bool } @@ -253,11 +255,12 @@ func (c *keyCache) Status(ctx context.Context) (KeyStoreState, error) { // Create creates a new key with the given name if and only if // no such entry exists. Otherwise, kes.ErrKeyExists is returned. -func (c *keyCache) Create(ctx context.Context, name string, key key.Key) error { - b, err := key.MarshalText() +func (c *keyCache) Create(ctx context.Context, name string, key crypto.KeyVersion) error { + b, err := pb.Marshal(&key) if err != nil { return err } + b = []byte(base64.StdEncoding.EncodeToString(b)) if err = c.store.Create(ctx, name, b); err != nil { if errors.Is(err, kes.ErrKeyExists) { @@ -289,7 +292,7 @@ func (c *keyCache) Delete(ctx context.Context, name string) error { // Get tries to make as few calls to the underlying key store. Multiple // concurrent Get calls for the same key, that is not in the cache, are // serialized. -func (c *keyCache) Get(ctx context.Context, name string) (key.Key, error) { +func (c *keyCache) Get(ctx context.Context, name string) (crypto.KeyVersion, error) { if entry, ok := c.cache.Get(name); ok { entry.Used.Store(true) return entry.Key, nil @@ -312,14 +315,14 @@ func (c *keyCache) Get(ctx context.Context, name string) (key.Key, error) { b, err := c.store.Get(ctx, name) if err != nil { if errors.Is(err, kes.ErrKeyNotFound) { - return key.Key{}, kes.ErrKeyNotFound + return crypto.KeyVersion{}, kes.ErrKeyNotFound } - return key.Key{}, err + return crypto.KeyVersion{}, err } - k, err := key.Parse(b) + k, err := crypto.ParseKeyVersion(b) if err != nil { - return key.Key{}, err + return crypto.KeyVersion{}, err } entry := &cacheEntry{ @@ -327,7 +330,7 @@ func (c *keyCache) Get(ctx context.Context, name string) (key.Key, error) { } entry.Used.Store(true) c.cache.Set(name, entry) - return k, nil + return entry.Key, nil } // List returns the first n key names, that start with the given prefix, diff --git a/server.go b/server.go index a0e9975d..c68ea4d4 100644 --- a/server.go +++ b/server.go @@ -25,10 +25,10 @@ import ( "github.com/minio/kes-go" "github.com/minio/kes/internal/api" "github.com/minio/kes/internal/cpu" + "github.com/minio/kes/internal/crypto" "github.com/minio/kes/internal/fips" "github.com/minio/kes/internal/headers" "github.com/minio/kes/internal/https" - "github.com/minio/kes/internal/key" "github.com/minio/kes/internal/keystore" "github.com/minio/kes/internal/metric" "github.com/minio/kes/internal/sys" @@ -552,21 +552,32 @@ func (s *Server) createKey(resp *api.Response, req *api.Request) { return } - var algorithm kes.KeyAlgorithm + var cipher crypto.SecretKeyType if fips.Enabled || cpu.HasAESGCM() { - algorithm = kes.AES256 + cipher = kes.AES256 } else { - algorithm = kes.ChaCha20 + cipher = kes.ChaCha20 } - key, err := key.Random(algorithm, req.Identity) + key, err := crypto.GenerateSecretKey(cipher, rand.Reader) + if err != nil { + s.state.Load().Log.ErrorContext(req.Context(), err.Error(), "req", req) + resp.Fail(http.StatusInternalServerError, "failed to generate encryption key") + return + } + hmac, err := crypto.GenerateHMACKey(crypto.SHA256, rand.Reader) if err != nil { s.state.Load().Log.ErrorContext(req.Context(), err.Error(), "req", req) resp.Fail(http.StatusInternalServerError, "failed to generate encryption key") return } - if err = s.state.Load().Keys.Create(req.Context(), req.Resource, key); err != nil { + if err = s.state.Load().Keys.Create(req.Context(), req.Resource, crypto.KeyVersion{ + Key: key, + HMACKey: hmac, + CreatedAt: time.Now().UTC(), + CreatedBy: req.Identity, + }); err != nil { if err, ok := api.IsError(err); ok { resp.Failr(err) return @@ -604,33 +615,44 @@ func (s *Server) importKey(resp *api.Response, req *api.Request) { return } - var algorithm kes.KeyAlgorithm + var cipher crypto.SecretKeyType switch imp.Cipher { case "AES256", "AES256-GCM_SHA256": - algorithm = kes.AES256 + cipher = crypto.AES256 case "ChaCha20", "XCHACHA20-POLY1305": if fips.Enabled { resp.Failf(http.StatusNotAcceptable, "algorithm '%s' not supported by FIPS 140-2", imp.Cipher) return } - algorithm = kes.ChaCha20 + cipher = crypto.ChaCha20 default: resp.Failf(http.StatusNotAcceptable, "algorithm '%s' is not supported", imp.Cipher) return } - if len(imp.Bytes) != key.Len(algorithm) { + if len(imp.Bytes) != crypto.SecretKeySize { resp.Failf(http.StatusNotAcceptable, "invalid key size for '%s'", imp.Cipher) return } - key, err := key.New(algorithm, imp.Bytes, req.Identity) + key, err := crypto.NewSecretKey(cipher, imp.Bytes) if err != nil { s.state.Load().Log.ErrorContext(req.Context(), err.Error(), "req", req) resp.Fail(http.StatusInternalServerError, "failed to create key") return } - if err = s.state.Load().Keys.Create(req.Context(), req.Resource, key); err != nil { + hmac, err := crypto.GenerateHMACKey(crypto.SHA256, rand.Reader) + if err != nil { + s.state.Load().Log.ErrorContext(req.Context(), err.Error(), "req", req) + resp.Fail(http.StatusInternalServerError, "failed to create key") + return + } + if err = s.state.Load().Keys.Create(req.Context(), req.Resource, crypto.KeyVersion{ + Key: key, + HMACKey: hmac, + CreatedAt: time.Now().UTC(), + CreatedBy: req.Identity, + }); err != nil { if err, ok := api.IsError(err); ok { resp.Failr(err) return @@ -670,9 +692,9 @@ func (s *Server) describeKey(resp *api.Response, req *api.Request) { api.ReplyWith(resp, http.StatusOK, api.DescribeKeyResponse{ Name: req.Resource, - Algorithm: key.Algorithm().String(), - CreatedAt: key.CreatedAt(), - CreatedBy: key.CreatedBy().String(), + Algorithm: key.Key.Type().String(), + CreatedAt: key.CreatedAt, + CreatedBy: key.CreatedBy.String(), }) } @@ -759,7 +781,7 @@ func (s *Server) encryptKey(resp *api.Response, req *api.Request) { resp.Fail(http.StatusBadGateway, "failed to read key") return } - ciphertext, err := key.Wrap(enc.Plaintext, enc.Context) + ciphertext, err := key.Key.Encrypt(enc.Plaintext, enc.Context) if err != nil { s.state.Load().Log.ErrorContext(req.Context(), err.Error(), "req", req) resp.Fail(http.StatusInternalServerError, "failed to encrypt plaintext") @@ -809,7 +831,7 @@ func (s *Server) generateKey(resp *api.Response, req *api.Request) { resp.Fail(http.StatusInternalServerError, "failed to generate encryption key") return } - ciphertext, err := key.Wrap(dataKey, gen.Context) + ciphertext, err := key.Key.Encrypt(dataKey, gen.Context) if err != nil { s.state.Load().Log.ErrorContext(req.Context(), err.Error(), "req", req) resp.Fail(http.StatusInternalServerError, "failed to generate encryption key") @@ -851,7 +873,7 @@ func (s *Server) decryptKey(resp *api.Response, req *api.Request) { resp.Fail(http.StatusBadGateway, "failed to read key") return } - plaintext, err := key.Unwrap(enc.Ciphertext, enc.Context) + plaintext, err := key.Key.Decrypt(enc.Ciphertext, enc.Context) if err != nil { if err, ok := api.IsError(err); ok { resp.Failr(err) @@ -868,6 +890,45 @@ func (s *Server) decryptKey(resp *api.Response, req *api.Request) { }) } +func (s *Server) hmacKey(resp *api.Response, req *api.Request) { + if !validName(req.Resource) { + resp.Failf(http.StatusBadRequest, "key name '%s' is empty, too long or contains invalid characters", req.Resource) + return + } + + var body api.HMACRequest + if err := api.ReadBody(req, &body); err != nil { + if err, ok := api.IsError(err); ok { + resp.Failr(err) + return + } + + s.state.Load().Log.ErrorContext(req.Context(), err.Error(), "req", req) + resp.Fail(http.StatusBadRequest, "invalid request body") + return + } + + key, err := s.state.Load().Keys.Get(req.Context(), req.Resource) + if err != nil { + if err, ok := api.IsError(err); ok { + resp.Failr(err) + return + } + + s.state.Load().Log.ErrorContext(req.Context(), err.Error(), "req", req) + resp.Fail(http.StatusBadGateway, "failed to read key") + return + } + if !key.HasHMACKey() { + resp.Fail(http.StatusConflict, "key does not support HMAC") + return + } + + api.ReplyWith(resp, http.StatusOK, api.DecryptKeyResponse{ + Plaintext: key.HMACKey.Sum(body.Message), + }) +} + func (s *Server) describePolicy(resp *api.Response, req *api.Request) { if !validName(req.Resource) { resp.Failf(http.StatusBadRequest, "policy name '%s' is empty, too long or contains invalid characters", req.Resource) diff --git a/state.go b/state.go index 5b5b43ab..cb64fd9b 100644 --- a/state.go +++ b/state.go @@ -147,6 +147,14 @@ func initRoutes(s *Server, routeConfig map[string]RouteConfig) (*http.ServeMux, Auth: (*verifyIdentity)(&s.state), Handler: api.HandlerFunc(s.decryptKey), }, + api.PathKeyHMAC: { + Method: http.MethodPut, + Path: api.PathKeyHMAC, + MaxBody: 1 * mem.MB, + Timeout: 15 * time.Second, + Auth: (*verifyIdentity)(&s.state), + Handler: api.HandlerFunc(s.hmacKey), + }, api.PathPolicyDescribe: { Method: http.MethodGet,