From 644c2ce326ff5b44beef29db8c152ae6fe1c0663 Mon Sep 17 00:00:00 2001 From: Andreas Auernhammer Date: Wed, 25 Jul 2018 22:35:54 +0200 Subject: [PATCH] crypto: add support for parsing/creating SSE-C/SSE-S3 metadata (#6169) * crypto: add support for parsing SSE-C/SSE-S3 metadata This commit adds support for detecting and parsing SSE-C/SSE-S3 object metadata. With the `IsEncrypted` functions it is possible to determine whether an object seems to be encrypted. With the `ParseMetadata` functions it is possible to validate such metadata and extract the SSE-C/SSE-S3 related values. It also fixes some naming issues. * crypto: add functions for creating SSE object metadata This commit adds functions for creating SSE-S3 and SSE-C metadata. It also adds a `CreateMultipartMetadata` for creating multipart metadata. For all functions unit tests are included. --- cmd/crypto/error.go | 8 + cmd/crypto/header.go | 12 +- cmd/crypto/header_test.go | 6 +- cmd/crypto/kms.go | 17 +- cmd/crypto/kms_test.go | 2 +- cmd/crypto/metadata.go | 213 +++++++++++++++++++++ cmd/crypto/metadata_test.go | 361 ++++++++++++++++++++++++++++++++++++ cmd/crypto/sse.go | 9 +- 8 files changed, 607 insertions(+), 21 deletions(-) create mode 100644 cmd/crypto/metadata.go create mode 100644 cmd/crypto/metadata_test.go diff --git a/cmd/crypto/error.go b/cmd/crypto/error.go index 49121f8a2..01acc452e 100644 --- a/cmd/crypto/error.go +++ b/cmd/crypto/error.go @@ -49,6 +49,14 @@ var ( ErrCustomerKeyMD5Mismatch = errors.New("The provided SSE-C key MD5 does not match the computed MD5 of the SSE-C key") ) +var ( + errMissingInternalIV = Error{"The object metadata is missing the internal encryption IV"} + errMissingInternalSealAlgorithm = Error{"The object metadata is missing the internal seal algorithm"} + + errInvalidInternalIV = Error{"The internal encryption IV is malformed"} + errInvalidInternalSealAlgorithm = Error{"The internal seal algorithm is invalid and not supported"} +) + var ( // errOutOfEntropy indicates that the a source of randomness (PRNG) wasn't able // to produce enough random data. This is fatal error and should cause a panic. diff --git a/cmd/crypto/header.go b/cmd/crypto/header.go index a326e1d3b..4bdec840a 100644 --- a/cmd/crypto/header.go +++ b/cmd/crypto/header.go @@ -70,9 +70,9 @@ func (s3) IsRequested(h http.Header) bool { return ok } -// Parse parses the SSE-S3 related HTTP headers and checks +// ParseHTTP parses the SSE-S3 related HTTP headers and checks // whether they contain valid values. -func (s3) Parse(h http.Header) (err error) { +func (s3) ParseHTTP(h http.Header) (err error) { if h.Get(SSEHeader) != SSEAlgorithmAES256 { err = ErrInvalidEncryptionMethod } @@ -123,9 +123,9 @@ func (ssecCopy) IsRequested(h http.Header) bool { return false } -// Parse parses the SSE-C headers and returns the SSE-C client key +// ParseHTTP parses the SSE-C headers and returns the SSE-C client key // on success. SSE-C copy headers are ignored. -func (ssec) Parse(h http.Header) (key [32]byte, err error) { +func (ssec) ParseHTTP(h http.Header) (key [32]byte, err error) { defer h.Del(SSECKey) // remove SSE-C key from headers after parsing if h.Get(SSECAlgorithm) != SSEAlgorithmAES256 { return key, ErrInvalidCustomerAlgorithm @@ -149,9 +149,9 @@ func (ssec) Parse(h http.Header) (key [32]byte, err error) { return key, nil } -// Parse parses the SSE-C copy headers and returns the SSE-C client key +// ParseHTTP parses the SSE-C copy headers and returns the SSE-C client key // on success. Regular SSE-C headers are ignored. -func (ssecCopy) Parse(h http.Header) (key [32]byte, err error) { +func (ssecCopy) ParseHTTP(h http.Header) (key [32]byte, err error) { defer h.Del(SSECopyKey) // remove SSE-C copy key of source object from headers after parsing if h.Get(SSECopyAlgorithm) != SSEAlgorithmAES256 { return key, ErrInvalidCustomerAlgorithm diff --git a/cmd/crypto/header_test.go b/cmd/crypto/header_test.go index 6a9538b3d..eb6fae313 100644 --- a/cmd/crypto/header_test.go +++ b/cmd/crypto/header_test.go @@ -49,7 +49,7 @@ var s3ParseTests = []struct { func TestS3Parse(t *testing.T) { for i, test := range s3ParseTests { - if err := S3.Parse(test.Header); err != test.ExpectedErr { + if err := S3.ParseHTTP(test.Header); err != test.ExpectedErr { t.Errorf("Test %d: Wanted '%v' but got '%v'", i, test.ExpectedErr, err) } } @@ -204,7 +204,7 @@ var ssecParseTests = []struct { func TestSSECParse(t *testing.T) { var zeroKey [32]byte for i, test := range ssecParseTests { - key, err := SSEC.Parse(test.Header) + key, err := SSEC.ParseHTTP(test.Header) if err != test.ExpectedErr { t.Errorf("Test %d: want error '%v' but got '%v'", i, test.ExpectedErr, err) } @@ -286,7 +286,7 @@ var ssecCopyParseTests = []struct { func TestSSECopyParse(t *testing.T) { var zeroKey [32]byte for i, test := range ssecCopyParseTests { - key, err := SSECopy.Parse(test.Header) + key, err := SSECopy.ParseHTTP(test.Header) if err != test.ExpectedErr { t.Errorf("Test %d: want error '%v' but got '%v'", i, test.ExpectedErr, err) } diff --git a/cmd/crypto/kms.go b/cmd/crypto/kms.go index 05c3b5e2e..fed56eb44 100644 --- a/cmd/crypto/kms.go +++ b/cmd/crypto/kms.go @@ -80,12 +80,12 @@ type KMS interface { // The context is cryptographically bound to the // generated key. The same context must be provided // again to unseal the generated key. - GenerateKey(keyID string, context Context) (key, sealedKey []byte, err error) + GenerateKey(keyID string, context Context) (key [32]byte, sealedKey []byte, err error) // UnsealKey unseals the sealedKey using the master key // referenced by the keyID. The provided context must // match the context used to generate the sealed key. - UnsealKey(keyID string, sealedKey []byte, context Context) (key []byte, err error) + UnsealKey(keyID string, sealedKey []byte, context Context) (key [32]byte, err error) } type masterKeyKMS struct { @@ -98,9 +98,8 @@ type masterKeyKMS struct { // to the generated keys. func NewKMS(key [32]byte) KMS { return &masterKeyKMS{masterKey: key} } -func (kms *masterKeyKMS) GenerateKey(keyID string, ctx Context) (key, sealedKey []byte, err error) { - key = make([]byte, 32) - if _, err = io.ReadFull(rand.Reader, key); err != nil { +func (kms *masterKeyKMS) GenerateKey(keyID string, ctx Context) (key [32]byte, sealedKey []byte, err error) { + if _, err = io.ReadFull(rand.Reader, key[:]); err != nil { logger.CriticalIf(context.Background(), errOutOfEntropy) } @@ -108,22 +107,22 @@ func (kms *masterKeyKMS) GenerateKey(keyID string, ctx Context) (key, sealedKey buffer bytes.Buffer derivedKey = kms.deriveKey(keyID, ctx) ) - if n, err := sio.Encrypt(&buffer, bytes.NewReader(key), sio.Config{Key: derivedKey[:]}); err != nil || n != 64 { + if n, err := sio.Encrypt(&buffer, bytes.NewReader(key[:]), sio.Config{Key: derivedKey[:]}); err != nil || n != 64 { logger.CriticalIf(context.Background(), errors.New("KMS: unable to encrypt data key")) } sealedKey = buffer.Bytes() return key, sealedKey, nil } -func (kms *masterKeyKMS) UnsealKey(keyID string, sealedKey []byte, ctx Context) (key []byte, err error) { +func (kms *masterKeyKMS) UnsealKey(keyID string, sealedKey []byte, ctx Context) (key [32]byte, err error) { var ( buffer bytes.Buffer derivedKey = kms.deriveKey(keyID, ctx) ) if n, err := sio.Decrypt(&buffer, bytes.NewReader(sealedKey), sio.Config{Key: derivedKey[:]}); err != nil || n != 32 { - return nil, err // TODO(aead): upgrade sio to use sio.Error + return key, err // TODO(aead): upgrade sio to use sio.Error } - key = buffer.Bytes() + copy(key[:], buffer.Bytes()) return key, nil } diff --git a/cmd/crypto/kms_test.go b/cmd/crypto/kms_test.go index c5a1cd2ff..9b411c1ee 100644 --- a/cmd/crypto/kms_test.go +++ b/cmd/crypto/kms_test.go @@ -53,7 +53,7 @@ func TestMasterKeyKMS(t *testing.T) { if err == nil && test.ShouldFail { t.Errorf("Test %d: KMS unsealed the generated successfully but should have failed", i) } - if !test.ShouldFail && !bytes.Equal(key, unsealedKey) { + if !test.ShouldFail && !bytes.Equal(key[:], unsealedKey[:]) { t.Errorf("Test %d: The generated and unsealed key differ", i) } } diff --git a/cmd/crypto/metadata.go b/cmd/crypto/metadata.go new file mode 100644 index 000000000..3a274968b --- /dev/null +++ b/cmd/crypto/metadata.go @@ -0,0 +1,213 @@ +// Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crypto + +import ( + "context" + "encoding/base64" + "fmt" + + "github.com/minio/minio/cmd/logger" +) + +// IsMultiPart returns true if the object metadata indicates +// that it was uploaded using some form of server-side-encryption +// and the S3 multipart API. +func IsMultiPart(metadata map[string]string) bool { + if _, ok := metadata[SSEMultipart]; ok { + return true + } + return false +} + +// IsEncrypted returns true if the object metadata indicates +// that it was uploaded using some form of server-side-encryption. +// +// IsEncrypted only checks whether the metadata contains at least +// one entry indicating SSE-C or SSE-S3. +func IsEncrypted(metadata map[string]string) bool { + if _, ok := metadata[SSEIV]; ok { + return true + } + if _, ok := metadata[SSESealAlgorithm]; ok { + return true + } + if IsMultiPart(metadata) { + return true + } + if S3.IsEncrypted(metadata) { + return true + } + if SSEC.IsEncrypted(metadata) { + return true + } + return false +} + +// IsEncrypted returns true if the object metadata indicates +// that the object was uploaded using SSE-S3. +func (s3) IsEncrypted(metadata map[string]string) bool { + if _, ok := metadata[S3SealedKey]; ok { + return true + } + if _, ok := metadata[S3KMSKeyID]; ok { + return true + } + if _, ok := metadata[S3KMSSealedKey]; ok { + return true + } + return false +} + +// IsEncrypted returns true if the object metadata indicates +// that the object was uploaded using SSE-C. +func (ssec) IsEncrypted(metadata map[string]string) bool { + if _, ok := metadata[SSECSealedKey]; ok { + return true + } + return false +} + +// CreateMultipartMetadata adds the multipart flag entry to metadata +// and returns modifed metadata. It allocates a new metadata map if +// metadata is nil. +func CreateMultipartMetadata(metadata map[string]string) map[string]string { + if metadata == nil { + metadata = map[string]string{} + } + metadata[SSEMultipart] = "" + return metadata +} + +// CreateMetadata encodes the keyID, the sealed kms data key and the sealed key +// into the metadata and returns the modified metadata. It allocates a new +// metadata map if metadata is nil. +func (s3) CreateMetadata(metadata map[string]string, keyID string, kmsKey []byte, sealedKey SealedKey) map[string]string { + if sealedKey.Algorithm != SealAlgorithm { + logger.CriticalIf(context.Background(), fmt.Errorf("The seal algorithm '%s' is invalid for SSE-S3", sealedKey.Algorithm)) + } + + if metadata == nil { + metadata = map[string]string{} + } + metadata[S3KMSKeyID] = keyID + metadata[SSESealAlgorithm] = sealedKey.Algorithm + metadata[SSEIV] = base64.StdEncoding.EncodeToString(sealedKey.IV[:]) + metadata[S3SealedKey] = base64.StdEncoding.EncodeToString(sealedKey.Key[:]) + metadata[S3KMSSealedKey] = base64.StdEncoding.EncodeToString(kmsKey) + return metadata +} + +// ParseMetadata extracts all SSE-S3 related values from the object metadata +// and checks whether they are well-formed. It returns the KMS key-ID, the +// sealed KMS key and the sealed object key on success. +func (s3) ParseMetadata(metadata map[string]string) (keyID string, kmsKey []byte, sealedKey SealedKey, err error) { + // Extract all required values from object metadata + b64IV, ok := metadata[SSEIV] + if !ok { + return keyID, kmsKey, sealedKey, errMissingInternalIV + } + algorithm, ok := metadata[SSESealAlgorithm] + if !ok { + return keyID, kmsKey, sealedKey, errMissingInternalSealAlgorithm + } + b64SealedKey, ok := metadata[S3SealedKey] + if !ok { + return keyID, kmsKey, sealedKey, Error{"The object metadata is missing the internal sealed key for SSE-S3"} + } + keyID, ok = metadata[S3KMSKeyID] + if !ok { + return keyID, kmsKey, sealedKey, Error{"The object metadata is missing the internal KMS key-ID for SSE-S3"} + } + b64KMSSealedKey, ok := metadata[S3KMSSealedKey] + if !ok { + return keyID, kmsKey, sealedKey, Error{"The object metadata is missing the internal sealed KMS data key for SSE-S3"} + } + + // Check whether all extracted values are well-formed + iv, err := base64.StdEncoding.DecodeString(b64IV) + if err != nil || len(iv) != 32 { + return keyID, kmsKey, sealedKey, errInvalidInternalIV + } + if algorithm != SealAlgorithm { + return keyID, kmsKey, sealedKey, errInvalidInternalSealAlgorithm + } + encryptedKey, err := base64.StdEncoding.DecodeString(b64SealedKey) + if err != nil || len(encryptedKey) != 64 { + return keyID, kmsKey, sealedKey, Error{"The internal sealed key for SSE-S3 is invalid"} + } + kmsKey, err = base64.StdEncoding.DecodeString(b64KMSSealedKey) + if err != nil { + return keyID, kmsKey, sealedKey, Error{"The internal sealed KMS data key for SSE-S3 is invalid"} + } + + sealedKey.Algorithm = algorithm + copy(sealedKey.IV[:], iv) + copy(sealedKey.Key[:], encryptedKey) + return keyID, kmsKey, sealedKey, nil +} + +// CreateMetadata encodes the sealed key into the metadata and returns the modified metadata. +// It allocates a new metadata map if metadata is nil. +func (ssec) CreateMetadata(metadata map[string]string, sealedKey SealedKey) map[string]string { + if sealedKey.Algorithm != SealAlgorithm { + logger.CriticalIf(context.Background(), fmt.Errorf("The seal algorithm '%s' is invalid for SSE-C", sealedKey.Algorithm)) + } + + if metadata == nil { + metadata = map[string]string{} + } + metadata[SSESealAlgorithm] = SealAlgorithm + metadata[SSEIV] = base64.StdEncoding.EncodeToString(sealedKey.IV[:]) + metadata[SSECSealedKey] = base64.StdEncoding.EncodeToString(sealedKey.Key[:]) + return metadata +} + +// ParseMetadata extracts all SSE-C related values from the object metadata +// and checks whether they are well-formed. It returns the sealed object key +// on success. +func (ssec) ParseMetadata(metadata map[string]string) (sealedKey SealedKey, err error) { + // Extract all required values from object metadata + b64IV, ok := metadata[SSEIV] + if !ok { + return sealedKey, errMissingInternalIV + } + algorithm, ok := metadata[SSESealAlgorithm] + if !ok { + return sealedKey, errMissingInternalSealAlgorithm + } + b64SealedKey, ok := metadata[SSECSealedKey] + if !ok { + return sealedKey, Error{"The object metadata is missing the internal sealed key for SSE-C"} + } + + // Check whether all extracted values are well-formed + iv, err := base64.StdEncoding.DecodeString(b64IV) + if err != nil || len(iv) != 32 { + return sealedKey, errInvalidInternalIV + } + if algorithm != SealAlgorithm && algorithm != InsecureSealAlgorithm { + return sealedKey, errInvalidInternalSealAlgorithm + } + encryptedKey, err := base64.StdEncoding.DecodeString(b64SealedKey) + if err != nil || len(encryptedKey) != 64 { + return sealedKey, Error{"The internal sealed key for SSE-C is invalid"} + } + + sealedKey.Algorithm = algorithm + copy(sealedKey.IV[:], iv) + copy(sealedKey.Key[:], encryptedKey) + return sealedKey, nil +} diff --git a/cmd/crypto/metadata_test.go b/cmd/crypto/metadata_test.go new file mode 100644 index 000000000..918d8d862 --- /dev/null +++ b/cmd/crypto/metadata_test.go @@ -0,0 +1,361 @@ +// Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crypto + +import ( + "bytes" + "encoding/base64" + "testing" + + "github.com/minio/minio/cmd/logger" +) + +var isMultipartTests = []struct { + Metadata map[string]string + Multipart bool +}{ + {Multipart: true, Metadata: map[string]string{SSEMultipart: ""}}, // 0 + {Multipart: true, Metadata: map[string]string{"X-Minio-Internal-Encrypted-Multipart": ""}}, // 1 + {Multipart: true, Metadata: map[string]string{SSEMultipart: "some-value"}}, // 2 + {Multipart: false, Metadata: map[string]string{"": ""}}, // 3 + {Multipart: false, Metadata: map[string]string{"X-Minio-Internal-EncryptedMultipart": ""}}, // 4 +} + +func TestIsMultipart(t *testing.T) { + for i, test := range isMultipartTests { + if isMultipart := IsMultiPart(test.Metadata); isMultipart != test.Multipart { + t.Errorf("Test %d: got '%v' - want '%v'", i, isMultipart, test.Multipart) + } + } +} + +var isEncryptedTests = []struct { + Metadata map[string]string + Encrypted bool +}{ + {Encrypted: true, Metadata: map[string]string{SSEMultipart: ""}}, // 0 + {Encrypted: true, Metadata: map[string]string{SSEIV: ""}}, // 1 + {Encrypted: true, Metadata: map[string]string{SSESealAlgorithm: ""}}, // 2 + {Encrypted: true, Metadata: map[string]string{SSECSealedKey: ""}}, // 3 + {Encrypted: true, Metadata: map[string]string{S3SealedKey: ""}}, // 4 + {Encrypted: true, Metadata: map[string]string{S3KMSKeyID: ""}}, // 5 + {Encrypted: true, Metadata: map[string]string{S3KMSSealedKey: ""}}, // 6 + {Encrypted: false, Metadata: map[string]string{"": ""}}, // 7 + {Encrypted: false, Metadata: map[string]string{"X-Minio-Internal-Server-Side-Encryption": ""}}, // 8 +} + +func TestIsEncrypted(t *testing.T) { + for i, test := range isEncryptedTests { + if isEncrypted := IsEncrypted(test.Metadata); isEncrypted != test.Encrypted { + t.Errorf("Test %d: got '%v' - want '%v'", i, isEncrypted, test.Encrypted) + } + } +} + +var s3IsEncryptedTests = []struct { + Metadata map[string]string + Encrypted bool +}{ + {Encrypted: false, Metadata: map[string]string{SSEMultipart: ""}}, // 0 + {Encrypted: false, Metadata: map[string]string{SSEIV: ""}}, // 1 + {Encrypted: false, Metadata: map[string]string{SSESealAlgorithm: ""}}, // 2 + {Encrypted: false, Metadata: map[string]string{SSECSealedKey: ""}}, // 3 + {Encrypted: true, Metadata: map[string]string{S3SealedKey: ""}}, // 4 + {Encrypted: true, Metadata: map[string]string{S3KMSKeyID: ""}}, // 5 + {Encrypted: true, Metadata: map[string]string{S3KMSSealedKey: ""}}, // 6 + {Encrypted: false, Metadata: map[string]string{"": ""}}, // 7 + {Encrypted: false, Metadata: map[string]string{"X-Minio-Internal-Server-Side-Encryption": ""}}, // 8 +} + +func TestS3IsEncrypted(t *testing.T) { + for i, test := range s3IsEncryptedTests { + if isEncrypted := S3.IsEncrypted(test.Metadata); isEncrypted != test.Encrypted { + t.Errorf("Test %d: got '%v' - want '%v'", i, isEncrypted, test.Encrypted) + } + } +} + +var ssecIsEncryptedTests = []struct { + Metadata map[string]string + Encrypted bool +}{ + {Encrypted: false, Metadata: map[string]string{SSEMultipart: ""}}, // 0 + {Encrypted: false, Metadata: map[string]string{SSEIV: ""}}, // 1 + {Encrypted: false, Metadata: map[string]string{SSESealAlgorithm: ""}}, // 2 + {Encrypted: true, Metadata: map[string]string{SSECSealedKey: ""}}, // 3 + {Encrypted: false, Metadata: map[string]string{S3SealedKey: ""}}, // 4 + {Encrypted: false, Metadata: map[string]string{S3KMSKeyID: ""}}, // 5 + {Encrypted: false, Metadata: map[string]string{S3KMSSealedKey: ""}}, // 6 + {Encrypted: false, Metadata: map[string]string{"": ""}}, // 7 + {Encrypted: false, Metadata: map[string]string{"X-Minio-Internal-Server-Side-Encryption": ""}}, // 8 +} + +func TestSSECIsEncrypted(t *testing.T) { + for i, test := range ssecIsEncryptedTests { + if isEncrypted := SSEC.IsEncrypted(test.Metadata); isEncrypted != test.Encrypted { + t.Errorf("Test %d: got '%v' - want '%v'", i, isEncrypted, test.Encrypted) + } + } +} + +var s3ParseMetadataTests = []struct { + Metadata map[string]string + ExpectedErr error + + DataKey []byte + KeyID string + SealedKey SealedKey +}{ + {ExpectedErr: errMissingInternalIV, Metadata: map[string]string{}, DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{}}, // 0 + { + ExpectedErr: errMissingInternalSealAlgorithm, Metadata: map[string]string{SSEIV: ""}, + DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{}, + }, // 1 + { + ExpectedErr: Error{"The object metadata is missing the internal sealed key for SSE-S3"}, + Metadata: map[string]string{SSEIV: "", SSESealAlgorithm: ""}, DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{}, + }, // 2 + { + ExpectedErr: Error{"The object metadata is missing the internal KMS key-ID for SSE-S3"}, + Metadata: map[string]string{SSEIV: "", SSESealAlgorithm: "", S3SealedKey: ""}, DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{}, + }, // 3 + { + ExpectedErr: Error{"The object metadata is missing the internal sealed KMS data key for SSE-S3"}, + Metadata: map[string]string{SSEIV: "", SSESealAlgorithm: "", S3SealedKey: "", S3KMSKeyID: ""}, + DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{}, + }, // 4 + { + ExpectedErr: errInvalidInternalIV, + Metadata: map[string]string{SSEIV: "", SSESealAlgorithm: "", S3SealedKey: "", S3KMSKeyID: "", S3KMSSealedKey: ""}, + DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{}, + }, // 5 + { + ExpectedErr: errInvalidInternalSealAlgorithm, + Metadata: map[string]string{ + SSEIV: base64.StdEncoding.EncodeToString(make([]byte, 32)), SSESealAlgorithm: "", S3SealedKey: "", S3KMSKeyID: "", S3KMSSealedKey: "", + }, + DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{}, + }, // 6 + { + ExpectedErr: Error{"The internal sealed key for SSE-S3 is invalid"}, + Metadata: map[string]string{ + SSEIV: base64.StdEncoding.EncodeToString(make([]byte, 32)), SSESealAlgorithm: SealAlgorithm, S3SealedKey: "", + S3KMSKeyID: "", S3KMSSealedKey: "", + }, + DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{}, + }, // 7 + { + ExpectedErr: Error{"The internal sealed KMS data key for SSE-S3 is invalid"}, + Metadata: map[string]string{ + SSEIV: base64.StdEncoding.EncodeToString(make([]byte, 32)), SSESealAlgorithm: SealAlgorithm, + S3SealedKey: base64.StdEncoding.EncodeToString(make([]byte, 64)), S3KMSKeyID: "key-1", + S3KMSSealedKey: ".MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=", // invalid base64 + }, + DataKey: []byte{}, KeyID: "key-1", SealedKey: SealedKey{}, + }, // 8 + { + ExpectedErr: nil, + Metadata: map[string]string{ + SSEIV: base64.StdEncoding.EncodeToString(make([]byte, 32)), SSESealAlgorithm: SealAlgorithm, + S3SealedKey: base64.StdEncoding.EncodeToString(make([]byte, 64)), S3KMSKeyID: "", S3KMSSealedKey: "", + }, + DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{Algorithm: SealAlgorithm}, + }, // 9 + { + ExpectedErr: nil, + Metadata: map[string]string{ + SSEIV: base64.StdEncoding.EncodeToString(append([]byte{1}, make([]byte, 31)...)), SSESealAlgorithm: SealAlgorithm, + S3SealedKey: base64.StdEncoding.EncodeToString(append([]byte{1}, make([]byte, 63)...)), S3KMSKeyID: "key-1", + S3KMSSealedKey: base64.StdEncoding.EncodeToString(make([]byte, 48)), + }, + DataKey: make([]byte, 48), KeyID: "key-1", SealedKey: SealedKey{Algorithm: SealAlgorithm, Key: [64]byte{1}, IV: [32]byte{1}}, + }, // 10 +} + +func TestS3ParseMetadata(t *testing.T) { + for i, test := range s3ParseMetadataTests { + keyID, dataKey, sealedKey, err := S3.ParseMetadata(test.Metadata) + if err != test.ExpectedErr { + t.Errorf("Test %d: got error '%v' - want error '%v'", i, err, test.ExpectedErr) + } + if !bytes.Equal(dataKey, test.DataKey) { + t.Errorf("Test %d: got data key '%v' - want data key '%v'", i, dataKey, test.DataKey) + } + if keyID != test.KeyID { + t.Errorf("Test %d: got key-ID '%v' - want key-ID '%v'", i, keyID, test.KeyID) + } + if sealedKey.Algorithm != test.SealedKey.Algorithm { + t.Errorf("Test %d: got sealed key algorithm '%v' - want sealed key algorithm '%v'", i, sealedKey.Algorithm, test.SealedKey.Algorithm) + } + if !bytes.Equal(sealedKey.Key[:], test.SealedKey.Key[:]) { + t.Errorf("Test %d: got sealed key '%v' - want sealed key '%v'", i, sealedKey.Key, test.SealedKey.Key) + } + if !bytes.Equal(sealedKey.IV[:], test.SealedKey.IV[:]) { + t.Errorf("Test %d: got sealed key IV '%v' - want sealed key IV '%v'", i, sealedKey.IV, test.SealedKey.IV) + } + } +} + +var ssecParseMetadataTests = []struct { + Metadata map[string]string + ExpectedErr error + + SealedKey SealedKey +}{ + {ExpectedErr: errMissingInternalIV, Metadata: map[string]string{}, SealedKey: SealedKey{}}, // 0 + {ExpectedErr: errMissingInternalSealAlgorithm, Metadata: map[string]string{SSEIV: ""}, SealedKey: SealedKey{}}, // 1 + { + ExpectedErr: Error{"The object metadata is missing the internal sealed key for SSE-C"}, + Metadata: map[string]string{SSEIV: "", SSESealAlgorithm: ""}, SealedKey: SealedKey{}, + }, // 2 + { + ExpectedErr: errInvalidInternalIV, + Metadata: map[string]string{SSEIV: "", SSESealAlgorithm: "", SSECSealedKey: ""}, SealedKey: SealedKey{}, + }, // 3 + { + ExpectedErr: errInvalidInternalSealAlgorithm, + Metadata: map[string]string{ + SSEIV: base64.StdEncoding.EncodeToString(make([]byte, 32)), SSESealAlgorithm: "", SSECSealedKey: "", + }, + SealedKey: SealedKey{}, + }, // 4 + { + ExpectedErr: Error{"The internal sealed key for SSE-C is invalid"}, + Metadata: map[string]string{ + SSEIV: base64.StdEncoding.EncodeToString(make([]byte, 32)), SSESealAlgorithm: SealAlgorithm, SSECSealedKey: "", + }, + SealedKey: SealedKey{}, + }, // 5 + { + ExpectedErr: nil, + Metadata: map[string]string{ + SSEIV: base64.StdEncoding.EncodeToString(make([]byte, 32)), SSESealAlgorithm: SealAlgorithm, + SSECSealedKey: base64.StdEncoding.EncodeToString(make([]byte, 64)), + }, + SealedKey: SealedKey{Algorithm: SealAlgorithm}, + }, // 6 + { + ExpectedErr: nil, + Metadata: map[string]string{ + SSEIV: base64.StdEncoding.EncodeToString(append([]byte{1}, make([]byte, 31)...)), SSESealAlgorithm: InsecureSealAlgorithm, + SSECSealedKey: base64.StdEncoding.EncodeToString(append([]byte{1}, make([]byte, 63)...)), + }, + SealedKey: SealedKey{Algorithm: InsecureSealAlgorithm, Key: [64]byte{1}, IV: [32]byte{1}}, + }, // 7 +} + +func TestCreateMultipartMetadata(t *testing.T) { + metadata := CreateMultipartMetadata(nil) + if v, ok := metadata[SSEMultipart]; !ok || v != "" { + t.Errorf("Metadata is missing the correct value for '%s': got '%s' - want '%s'", SSEMultipart, v, "") + } +} + +func TestSSECParseMetadata(t *testing.T) { + for i, test := range ssecParseMetadataTests { + sealedKey, err := SSEC.ParseMetadata(test.Metadata) + if err != test.ExpectedErr { + t.Errorf("Test %d: got error '%v' - want error '%v'", i, err, test.ExpectedErr) + } + if sealedKey.Algorithm != test.SealedKey.Algorithm { + t.Errorf("Test %d: got sealed key algorithm '%v' - want sealed key algorithm '%v'", i, sealedKey.Algorithm, test.SealedKey.Algorithm) + } + if !bytes.Equal(sealedKey.Key[:], test.SealedKey.Key[:]) { + t.Errorf("Test %d: got sealed key '%v' - want sealed key '%v'", i, sealedKey.Key, test.SealedKey.Key) + } + if !bytes.Equal(sealedKey.IV[:], test.SealedKey.IV[:]) { + t.Errorf("Test %d: got sealed key IV '%v' - want sealed key IV '%v'", i, sealedKey.IV, test.SealedKey.IV) + } + } +} + +var s3CreateMetadataTests = []struct { + KeyID string + SealedDataKey []byte + SealedKey SealedKey +}{ + {KeyID: "", SealedDataKey: make([]byte, 48), SealedKey: SealedKey{Algorithm: SealAlgorithm}}, + {KeyID: "cafebabe", SealedDataKey: make([]byte, 48), SealedKey: SealedKey{Algorithm: SealAlgorithm}}, + {KeyID: "deadbeef", SealedDataKey: make([]byte, 32), SealedKey: SealedKey{IV: [32]byte{0xf7}, Key: [64]byte{0xea}, Algorithm: SealAlgorithm}}, +} + +func TestS3CreateMetadata(t *testing.T) { + defer func(disableLog bool) { logger.Disable = disableLog }(logger.Disable) + logger.Disable = true + for i, test := range s3CreateMetadataTests { + metadata := S3.CreateMetadata(nil, test.KeyID, test.SealedDataKey, test.SealedKey) + keyID, kmsKey, sealedKey, err := S3.ParseMetadata(metadata) + if err != nil { + t.Errorf("Test %d: failed to parse metadata: %v", i, err) + continue + } + if keyID != test.KeyID { + t.Errorf("Test %d: Key-ID mismatch: got '%s' - want '%s'", i, keyID, test.KeyID) + } + if !bytes.Equal(kmsKey, test.SealedDataKey) { + t.Errorf("Test %d: sealed KMS data mismatch: got '%v' - want '%v'", i, kmsKey, test.SealedDataKey) + } + if sealedKey.Algorithm != test.SealedKey.Algorithm { + t.Errorf("Test %d: seal algorithm mismatch: got '%s' - want '%s'", i, sealedKey.Algorithm, test.SealedKey.Algorithm) + } + if !bytes.Equal(sealedKey.IV[:], test.SealedKey.IV[:]) { + t.Errorf("Test %d: IV mismatch: got '%v' - want '%v'", i, sealedKey.IV, test.SealedKey.IV) + } + if !bytes.Equal(sealedKey.Key[:], test.SealedKey.Key[:]) { + t.Errorf("Test %d: sealed key mismatch: got '%v' - want '%v'", i, sealedKey.Key, test.SealedKey.Key) + } + } + + defer func() { + if err := recover(); err == nil || err != logger.ErrCritical { + t.Errorf("Expected '%s' panic for invalid seal algorithm but got '%s'", logger.ErrCritical, err) + } + }() + _ = S3.CreateMetadata(nil, "", []byte{}, SealedKey{Algorithm: InsecureSealAlgorithm}) +} + +var ssecCreateMetadataTests = []SealedKey{ + {Algorithm: SealAlgorithm}, + {IV: [32]byte{0xff}, Key: [64]byte{0x7e}, Algorithm: SealAlgorithm}, +} + +func TestSSECCreateMetadata(t *testing.T) { + defer func(disableLog bool) { logger.Disable = disableLog }(logger.Disable) + logger.Disable = true + for i, test := range s3CreateMetadataTests { + metadata := SSEC.CreateMetadata(nil, test.SealedKey) + sealedKey, err := SSEC.ParseMetadata(metadata) + if err != nil { + t.Errorf("Test %d: failed to parse metadata: %v", i, err) + continue + } + if sealedKey.Algorithm != test.SealedKey.Algorithm { + t.Errorf("Test %d: seal algorithm mismatch: got '%s' - want '%s'", i, sealedKey.Algorithm, test.SealedKey.Algorithm) + } + if !bytes.Equal(sealedKey.IV[:], test.SealedKey.IV[:]) { + t.Errorf("Test %d: IV mismatch: got '%v' - want '%v'", i, sealedKey.IV, test.SealedKey.IV) + } + if !bytes.Equal(sealedKey.Key[:], test.SealedKey.Key[:]) { + t.Errorf("Test %d: sealed key mismatch: got '%v' - want '%v'", i, sealedKey.Key, test.SealedKey.Key) + } + } + + defer func() { + if err := recover(); err == nil || err != logger.ErrCritical { + t.Errorf("Expected '%s' panic for invalid seal algorithm but got '%s'", logger.ErrCritical, err) + } + }() + _ = SSEC.CreateMetadata(nil, SealedKey{Algorithm: InsecureSealAlgorithm}) +} diff --git a/cmd/crypto/sse.go b/cmd/crypto/sse.go index 955fadc73..d1621783f 100644 --- a/cmd/crypto/sse.go +++ b/cmd/crypto/sse.go @@ -25,6 +25,11 @@ import ( ) const ( + // SSEMultipart is the metadata key indicating that the object + // was uploaded using the S3 multipart API and stored using + // some from of server-side-encryption. + SSEMultipart = "X-Minio-Internal-Encrypted-Multipart" + // SSEIV is the metadata key referencing the random initialization // vector (IV) used for SSE-S3 and SSE-C key derivation. SSEIV = "X-Minio-Internal-Server-Side-Encryption-Iv" @@ -33,8 +38,8 @@ const ( // used by SSE-C and SSE-S3 to encrypt the object. SSESealAlgorithm = "X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm" - // SSECSealKey is the metadata key referencing the sealed object-key for SSE-C. - SSECSealKey = "X-Minio-Internal-Server-Side-Encryption-Sealed-Key" + // SSECSealedKey is the metadata key referencing the sealed object-key for SSE-C. + SSECSealedKey = "X-Minio-Internal-Server-Side-Encryption-Sealed-Key" // S3SealedKey is the metadata key referencing the sealed object-key for SSE-S3. S3SealedKey = "X-Minio-Internal-Server-Side-Encryption-S3-Sealed-Key"