From 2ea56a92fa31b460e4c192b63f043acdb41c43ac Mon Sep 17 00:00:00 2001 From: Andreas Auernhammer Date: Fri, 12 Jan 2024 17:30:21 +0100 Subject: [PATCH] add HMAC API and use KMS secret key crypto This commit does two closely related things: - It replaces the existing AES/ChaCha20 secret key implementation with a newer one based on the MinIO KMS implementation. - Adds an HMAC API such that clients can compute a HMAC checksum over a message. The new secret key implementation is located in the `internal/crypto` package. It is fully backwards compatible with the previous implementation that resided in `internal/key` and is removed by this PR. In particular, all ciphertexts produced with existing keys can be decrypted with the added implementation. The new implementation cleans up some design issues in the previous AES-256 and ChaCha20 ciphertext generation: - Now, ciphertexts have no structure or format. They only consist of the AEAD ciphertext (encrypted plaintext + auth. tag) and the 224 bit (28 byte) nonce. - Forward and backward compatibility information is no longer embedded into the ciphertext but instead into the key. For example, if we ever want to update the AES scheme, e.g. from AES-GCM or AES-SIV, (which we never did btw) we just create a new secret key type. This has also the side effect of ciphertexts getting significantly smaller (half the size). Since each MinIO object embeds at least one ciphertext in its metadata, this can give be a small perf. improvement when listing a lot of encrypted objects. *** The new HMAC API allows clients to compute a deterministic keyed checkusm (MAC) over some data without having direct access to the HMAC key. Clients may use this to verify that messages are authentic or generate the same pseudo-random secret on startup. Signed-off-by: Andreas Auernhammer --- api_test.go | 1 + internal/api/api.go | 1 + internal/api/request.go | 5 + internal/api/response.go | 5 + internal/{key => crypto}/ciphertext.go | 67 ++-- internal/crypto/key.go | 495 +++++++++++++++++++++++++ internal/crypto/key_test.go | 346 +++++++++++++++++ internal/key/key.go | 309 --------------- internal/key/key_test.go | 248 ------------- internal/key/version.go | 51 --- internal/keystore/azure/client.go | 5 +- internal/keystore/fortanix/keystore.go | 7 +- internal/protobuf/crypto.pb.go | 335 +++++++++++++++++ internal/protobuf/crypto.proto | 33 ++ internal/protobuf/proto.go | 85 +++++ keystore.go | 20 +- server.go | 97 ++++- state.go | 8 + 18 files changed, 1437 insertions(+), 681 deletions(-) rename internal/{key => crypto}/ciphertext.go (70%) create mode 100644 internal/crypto/key.go create mode 100644 internal/crypto/key_test.go delete mode 100644 internal/key/key.go delete mode 100644 internal/key/key_test.go delete mode 100644 internal/key/version.go create mode 100644 internal/protobuf/crypto.pb.go create mode 100644 internal/protobuf/crypto.proto create mode 100644 internal/protobuf/proto.go diff --git a/api_test.go b/api_test.go index db5c0085..676a1140 100644 --- a/api_test.go +++ b/api_test.go @@ -139,6 +139,7 @@ func testListAPIDefaults(t *testing.T) { "/v1/key/generate/": {Method: http.MethodPut, MaxBody: 1 * mem.MB, Timeout: 15 * time.Second}, "/v1/key/encrypt/": {Method: http.MethodPut, MaxBody: 1 * mem.MB, Timeout: 15 * time.Second}, "/v1/key/decrypt/": {Method: http.MethodPut, MaxBody: 1 * mem.MB, Timeout: 15 * time.Second}, + "/v1/key/hmac/": {Method: http.MethodPut, MaxBody: 1 * mem.MB, Timeout: 15 * time.Second}, "/v1/policy/describe/": {Method: http.MethodGet, MaxBody: 0, Timeout: 15 * time.Second}, "/v1/policy/read/": {Method: http.MethodGet, MaxBody: 0, Timeout: 15 * time.Second}, 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 70% rename from internal/key/ciphertext.go rename to internal/crypto/ciphertext.go index 0b1a39d5..3ed8d352 100644 --- a/internal/key/ciphertext.go +++ b/internal/crypto/ciphertext.go @@ -1,43 +1,50 @@ -// Copyright 2022 - MinIO, Inc. All rights reserved. +// 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 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 +58,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 +108,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..f814f77d 100644 --- a/keystore.go +++ b/keystore.go @@ -15,7 +15,7 @@ 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" ) @@ -236,7 +236,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,8 +253,8 @@ 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 := crypto.EncodeKeyVersion(key) if err != nil { return err } @@ -289,7 +289,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 +312,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 +327,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..fe2b7d4a 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 = crypto.AES256 } else { - algorithm = kes.ChaCha20 + cipher = crypto.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,