diff --git a/cmd/crypto/error.go b/cmd/crypto/error.go index 8dda24935..fb244d460 100644 --- a/cmd/crypto/error.go +++ b/cmd/crypto/error.go @@ -16,6 +16,13 @@ package crypto import "errors" +// Error is the generic type for any error happening during decrypting +// an object. It indicates that the object itself or its metadata was +// modified accidentally or maliciously. +type Error struct{ msg string } + +func (e Error) Error() string { return e.msg } + var ( // ErrInvalidEncryptionMethod indicates that the specified SSE encryption method // is not supported. diff --git a/cmd/crypto/key.go b/cmd/crypto/key.go index edef2923e..72039e243 100644 --- a/cmd/crypto/key.go +++ b/cmd/crypto/key.go @@ -21,8 +21,9 @@ import ( "crypto/rand" "encoding/binary" "errors" + "fmt" "io" - "path/filepath" + "path" "github.com/minio/minio/cmd/logger" sha256 "github.com/minio/sha256-simd" @@ -51,33 +52,69 @@ func GenerateKey(extKey [32]byte, random io.Reader) (key ObjectKey) { return } +// SealedKey represents a sealed object key. It can be stored +// at an untrusted location. +type SealedKey struct { + Key [64]byte // The encrypted and authenticted object-key. + IV [32]byte // The random IV used to encrypt the object-key. + Algorithm string // The sealing algorithm used to encrypt the object key. +} + // Seal encrypts the ObjectKey using the 256 bit external key and IV. The sealed -// key is also cryptographically bound to the object's path (bucket/object). -func (key ObjectKey) Seal(extKey, iv [32]byte, bucket, object string) []byte { - var sealedKey bytes.Buffer +// key is also cryptographically bound to the object's path (bucket/object) and the +// domain (SSE-C or SSE-S3). +func (key ObjectKey) Seal(extKey, iv [32]byte, domain, bucket, object string) SealedKey { + var ( + sealingKey [32]byte + encryptedKey bytes.Buffer + ) mac := hmac.New(sha256.New, extKey[:]) mac.Write(iv[:]) - mac.Write([]byte(filepath.Join(bucket, object))) + mac.Write([]byte(domain)) + mac.Write([]byte(SealAlgorithm)) + mac.Write([]byte(path.Join(bucket, object))) // use path.Join for canonical 'bucket/object' + mac.Sum(sealingKey[:0]) - if n, err := sio.Encrypt(&sealedKey, bytes.NewReader(key[:]), sio.Config{Key: mac.Sum(nil)}); n != 64 || err != nil { + if n, err := sio.Encrypt(&encryptedKey, bytes.NewReader(key[:]), sio.Config{Key: sealingKey[:]}); n != 64 || err != nil { logger.CriticalIf(context.Background(), errors.New("Unable to generate sealed key")) } - return sealedKey.Bytes() + sealedKey := SealedKey{ + IV: iv, + Algorithm: SealAlgorithm, + } + copy(sealedKey.Key[:], encryptedKey.Bytes()) + return sealedKey } -// Unseal decrypts a sealed key using the 256 bit external key and IV. Since the sealed key -// is cryptographically bound to the object's path the same bucket/object as during sealing +// Unseal decrypts a sealed key using the 256 bit external key. Since the sealed key +// may be cryptographically bound to the object's path the same bucket/object as during sealing // must be provided. On success the ObjectKey contains the decrypted sealed key. -func (key *ObjectKey) Unseal(sealedKey []byte, extKey, iv [32]byte, bucket, object string) error { - var unsealedKey bytes.Buffer - mac := hmac.New(sha256.New, extKey[:]) - mac.Write(iv[:]) - mac.Write([]byte(filepath.Join(bucket, object))) +func (key *ObjectKey) Unseal(extKey [32]byte, sealedKey SealedKey, domain, bucket, object string) error { + var ( + unsealConfig sio.Config + decryptedKey bytes.Buffer + ) + switch sealedKey.Algorithm { + default: + return Error{fmt.Sprintf("The sealing algorithm '%s' is not supported", sealedKey.Algorithm)} + case SealAlgorithm: + mac := hmac.New(sha256.New, extKey[:]) + mac.Write(sealedKey.IV[:]) + mac.Write([]byte(domain)) + mac.Write([]byte(SealAlgorithm)) + mac.Write([]byte(path.Join(bucket, object))) // use path.Join for canonical 'bucket/object' + unsealConfig = sio.Config{MinVersion: sio.Version20, Key: mac.Sum(nil)} + case InsecureSealAlgorithm: + sha := sha256.New() + sha.Write(extKey[:]) + sha.Write(sealedKey.IV[:]) + unsealConfig = sio.Config{MinVersion: sio.Version10, Key: sha.Sum(nil)} + } - if n, err := sio.Decrypt(&unsealedKey, bytes.NewReader(sealedKey), sio.Config{Key: mac.Sum(nil)}); n != 32 || err != nil { + if n, err := sio.Decrypt(&decryptedKey, bytes.NewReader(sealedKey.Key[:]), unsealConfig); n != 32 || err != nil { return err // TODO(aead): upgrade sio to use sio.Error } - copy(key[:], unsealedKey.Bytes()) + copy(key[:], decryptedKey.Bytes()) return nil } diff --git a/cmd/crypto/key_test.go b/cmd/crypto/key_test.go index c91415edf..93d4e079d 100644 --- a/cmd/crypto/key_test.go +++ b/cmd/crypto/key_test.go @@ -20,6 +20,8 @@ import ( "encoding/hex" "io" "testing" + + "github.com/minio/minio/cmd/logger" ) var shortRandom = func(limit int64) io.Reader { return io.LimitReader(rand.Reader, limit) } @@ -37,14 +39,18 @@ var generateKeyTests = []struct { Random io.Reader ShouldPass bool }{ - {ExtKey: [32]byte{}, Random: nil, ShouldPass: true}, // 0 - {ExtKey: [32]byte{}, Random: rand.Reader, ShouldPass: true}, // 1 - {ExtKey: [32]byte{}, Random: shortRandom(32), ShouldPass: true}, // 2 - // {ExtKey: [32]byte{}, Random: shortRandom(31), ShouldPass: false}, // 3 See: https://github.com/minio/minio/issues/6064 + {ExtKey: [32]byte{}, Random: nil, ShouldPass: true}, // 0 + {ExtKey: [32]byte{}, Random: rand.Reader, ShouldPass: true}, // 1 + {ExtKey: [32]byte{}, Random: shortRandom(32), ShouldPass: true}, // 2 + {ExtKey: [32]byte{}, Random: shortRandom(31), ShouldPass: false}, // 3 } func TestGenerateKey(t *testing.T) { + defer func(disableLog bool) { logger.Disable = disableLog }(logger.Disable) + logger.Disable = true + for i, test := range generateKeyTests { + i, test := i, test func() { defer recoverTest(i, test.ShouldPass, t) key := GenerateKey(test.ExtKey, test.Random) @@ -56,37 +62,37 @@ func TestGenerateKey(t *testing.T) { } var sealUnsealKeyTests = []struct { - SealExtKey, SealIV [32]byte - SealBucket, SealObject string + SealExtKey, SealIV [32]byte + SealDomain, SealBucket, SealObject string - UnsealExtKey, UnsealIV [32]byte - UnsealBucket, UnsealObject string + UnsealExtKey [32]byte + UnsealDomain, UnsealBucket, UnsealObject string ShouldPass bool }{ { - SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealBucket: "bucket", SealObject: "object", - UnsealExtKey: [32]byte{}, UnsealIV: [32]byte{}, UnsealBucket: "bucket", UnsealObject: "object", + SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealDomain: "SSE-C", SealBucket: "bucket", SealObject: "object", + UnsealExtKey: [32]byte{}, UnsealDomain: "SSE-C", UnsealBucket: "bucket", UnsealObject: "object", ShouldPass: true, }, // 0 { - SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealBucket: "bucket", SealObject: "object", - UnsealExtKey: [32]byte{1}, UnsealIV: [32]byte{0}, UnsealBucket: "bucket", UnsealObject: "object", + SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealDomain: "SSE-C", SealBucket: "bucket", SealObject: "object", + UnsealExtKey: [32]byte{1}, UnsealDomain: "SSE-C", UnsealBucket: "bucket", UnsealObject: "object", // different ext-key ShouldPass: false, }, // 1 { - SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealBucket: "bucket", SealObject: "object", - UnsealExtKey: [32]byte{}, UnsealIV: [32]byte{1}, UnsealBucket: "bucket", UnsealObject: "object", + SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealDomain: "SSE-S3", SealBucket: "bucket", SealObject: "object", + UnsealExtKey: [32]byte{}, UnsealDomain: "SSE-C", UnsealBucket: "bucket", UnsealObject: "object", // different domain ShouldPass: false, }, // 2 { - SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealBucket: "bucket", SealObject: "object", - UnsealExtKey: [32]byte{}, UnsealIV: [32]byte{}, UnsealBucket: "Bucket", UnsealObject: "object", + SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealDomain: "SSE-C", SealBucket: "bucket", SealObject: "object", + UnsealExtKey: [32]byte{}, UnsealDomain: "SSE-C", UnsealBucket: "Bucket", UnsealObject: "object", // different bucket ShouldPass: false, }, // 3 { - SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealBucket: "bucket", SealObject: "object", - UnsealExtKey: [32]byte{}, UnsealIV: [32]byte{}, UnsealBucket: "bucket", UnsealObject: "Object", + SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealDomain: "SSE-C", SealBucket: "bucket", SealObject: "object", + UnsealExtKey: [32]byte{}, UnsealDomain: "SSE-C", UnsealBucket: "bucket", UnsealObject: "Object", // different object ShouldPass: false, }, // 4 } @@ -94,13 +100,22 @@ var sealUnsealKeyTests = []struct { func TestSealUnsealKey(t *testing.T) { for i, test := range sealUnsealKeyTests { key := GenerateKey(test.SealExtKey, rand.Reader) - sealedKey := key.Seal(test.SealExtKey, test.SealIV, test.SealBucket, test.SealObject) - if err := key.Unseal(sealedKey, test.UnsealExtKey, test.UnsealIV, test.UnsealBucket, test.UnsealObject); err == nil && !test.ShouldPass { + sealedKey := key.Seal(test.SealExtKey, test.SealIV, test.SealDomain, test.SealBucket, test.SealObject) + if err := key.Unseal(test.UnsealExtKey, sealedKey, test.UnsealDomain, test.UnsealBucket, test.UnsealObject); err == nil && !test.ShouldPass { t.Errorf("Test %d should fail but passed successfully", i) } else if err != nil && test.ShouldPass { t.Errorf("Test %d should pass put failed: %v", i, err) } } + + // Test legacy InsecureSealAlgorithm + var extKey, iv [32]byte + key := GenerateKey(extKey, rand.Reader) + sealedKey := key.Seal(extKey, iv, "SSE-S3", "bucket", "object") + sealedKey.Algorithm = InsecureSealAlgorithm + if err := key.Unseal(extKey, sealedKey, "SSE-S3", "bucket", "object"); err == nil { + t.Errorf("'%s' test succeeded but it should fail because the legacy algorithm was used", sealedKey.Algorithm) + } } var derivePartKeyTest = []struct { diff --git a/cmd/crypto/sse.go b/cmd/crypto/sse.go index 0cc1584fd..09130e1f8 100644 --- a/cmd/crypto/sse.go +++ b/cmd/crypto/sse.go @@ -35,6 +35,18 @@ const ( S3KMSSealedKey = "X-Minio-Internal-Server-Side-Encryption-S3-Kms-Sealed-Key" ) +const ( + // SealAlgorithm is the encryption/sealing algorithm used to derive & seal + // the key-encryption-key and to en/decrypt the object data. + SealAlgorithm = "DAREv2-HMAC-SHA256" + + // InsecureSealAlgorithm is the legacy encryption/sealing algorithm used + // to derive & seal the key-encryption-key and to en/decrypt the object data. + // This algorithm should not be used for new objects because its key derivation + // is not optimal. See: https://github.com/minio/minio/pull/6121 + InsecureSealAlgorithm = "DARE-SHA256" +) + // EncryptSinglePart encrypts an io.Reader which must be the // the body of a single-part PUT request. func EncryptSinglePart(r io.Reader, key ObjectKey) io.Reader {