From 758a80e39bd708283b7180b7621ce90cc5809695 Mon Sep 17 00:00:00 2001 From: Andreas Auernhammer Date: Wed, 18 Jul 2018 19:49:26 +0200 Subject: [PATCH] crypto: add basic functionality for parsing SSE-C headers (#6148) This commit adds basic support for SSE-C / SSE-C copy. This includes functions for determining whether SSE-C is requested by the S3 client and functions for parsing such HTTP headers. All S3 SSE-C parsing errors are exported such that callers can pattern-match to forward the correct error to S3 clients. Further the SSE-C related internal metadata entry-keys are added by this commit. --- cmd/crypto/error.go | 20 +++ cmd/crypto/header.go | 127 +++++++++++++++++++ cmd/crypto/header_test.go | 256 +++++++++++++++++++++++++++++++++++++- cmd/crypto/sse.go | 21 ++++ 4 files changed, 420 insertions(+), 4 deletions(-) diff --git a/cmd/crypto/error.go b/cmd/crypto/error.go index d5834e4ff..49121f8a2 100644 --- a/cmd/crypto/error.go +++ b/cmd/crypto/error.go @@ -27,6 +27,26 @@ var ( // ErrInvalidEncryptionMethod indicates that the specified SSE encryption method // is not supported. ErrInvalidEncryptionMethod = errors.New("The encryption method is not supported") + + // ErrInvalidCustomerAlgorithm indicates that the specified SSE-C algorithm + // is not supported. + ErrInvalidCustomerAlgorithm = errors.New("The SSE-C algorithm is not supported") + + // ErrMissingCustomerKey indicates that the HTTP headers contains no SSE-C client key. + ErrMissingCustomerKey = errors.New("The SSE-C request is missing the customer key") + + // ErrMissingCustomerKeyMD5 indicates that the HTTP headers contains no SSE-C client key + // MD5 checksum. + ErrMissingCustomerKeyMD5 = errors.New("The SSE-C request is missing the customer key MD5") + + // ErrInvalidCustomerKey indicates that the SSE-C client key is not valid - e.g. not a + // base64-encoded string or not 256 bits long. + ErrInvalidCustomerKey = errors.New("The SSE-C client key is invalid") + + // ErrCustomerKeyMD5Mismatch indicates that the SSE-C key MD5 does not match the + // computed MD5 sum. This means that the client provided either the wrong key for + // a certain MD5 checksum or the wrong MD5 for a certain key. + ErrCustomerKeyMD5Mismatch = errors.New("The provided SSE-C key MD5 does not match the computed MD5 of the SSE-C key") ) var ( diff --git a/cmd/crypto/header.go b/cmd/crypto/header.go index 9227c246b..a326e1d3b 100644 --- a/cmd/crypto/header.go +++ b/cmd/crypto/header.go @@ -15,12 +15,43 @@ package crypto import ( + "bytes" + "crypto/md5" + "encoding/base64" "net/http" ) // SSEHeader is the general AWS SSE HTTP header key. const SSEHeader = "X-Amz-Server-Side-Encryption" +const ( + // SSECAlgorithm is the HTTP header key referencing + // the SSE-C algorithm. + SSECAlgorithm = SSEHeader + "-Customer-Algorithm" + + // SSECKey is the HTTP header key referencing the + // SSE-C client-provided key.. + SSECKey = SSEHeader + "-Customer-Key" + + // SSECKeyMD5 is the HTTP header key referencing + // the MD5 sum of the client-provided key. + SSECKeyMD5 = SSEHeader + "-Customer-Key-Md5" +) + +const ( + // SSECopyAlgorithm is the HTTP header key referencing + // the SSE-C algorithm for SSE-C copy requests. + SSECopyAlgorithm = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm" + + // SSECopyKey is the HTTP header key referencing the SSE-C + // client-provided key for SSE-C copy requests. + SSECopyKey = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key" + + // SSECopyKeyMD5 is the HTTP header key referencing the + // MD5 sum of the client key for SSE-C copy requests. + SSECopyKeyMD5 = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5" +) + // SSEAlgorithmAES256 is the only supported value for the SSE-S3 or SSE-C algorithm header. // For SSE-S3 see: https://docs.aws.amazon.com/AmazonS3/latest/dev/SSEUsingRESTAPI.html // For SSE-C see: https://docs.aws.amazon.com/AmazonS3/latest/dev/ServerSideEncryptionCustomerKeys.html @@ -47,3 +78,99 @@ func (s3) Parse(h http.Header) (err error) { } return } + +var ( + // SSEC represents AWS SSE-C. It provides functionality to handle + // SSE-C requests. + SSEC = ssec{} + + // SSECopy represents AWS SSE-C for copy requests. It provides + // functionality to handle SSE-C copy requests. + SSECopy = ssecCopy{} +) + +type ssec struct{} +type ssecCopy struct{} + +// IsRequested returns true if the HTTP headers contains +// at least one SSE-C header. SSE-C copy headers are ignored. +func (ssec) IsRequested(h http.Header) bool { + if _, ok := h[SSECAlgorithm]; ok { + return true + } + if _, ok := h[SSECKey]; ok { + return true + } + if _, ok := h[SSECKeyMD5]; ok { + return true + } + return false +} + +// IsRequested returns true if the HTTP headers contains +// at least one SSE-C copy header. Regular SSE-C headers +// are ignored. +func (ssecCopy) IsRequested(h http.Header) bool { + if _, ok := h[SSECopyAlgorithm]; ok { + return true + } + if _, ok := h[SSECopyKey]; ok { + return true + } + if _, ok := h[SSECopyKeyMD5]; ok { + return true + } + return false +} + +// Parse 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) { + defer h.Del(SSECKey) // remove SSE-C key from headers after parsing + if h.Get(SSECAlgorithm) != SSEAlgorithmAES256 { + return key, ErrInvalidCustomerAlgorithm + } + if h.Get(SSECKey) == "" { + return key, ErrMissingCustomerKey + } + if h.Get(SSECKeyMD5) == "" { + return key, ErrMissingCustomerKeyMD5 + } + + clientKey, err := base64.StdEncoding.DecodeString(h.Get(SSECKey)) + if err != nil || len(clientKey) != 32 { // The client key must be 256 bits long + return key, ErrInvalidCustomerKey + } + keyMD5, err := base64.StdEncoding.DecodeString(h.Get(SSECKeyMD5)) + if md5Sum := md5.Sum(clientKey); err != nil || !bytes.Equal(md5Sum[:], keyMD5) { + return key, ErrCustomerKeyMD5Mismatch + } + copy(key[:], clientKey) + return key, nil +} + +// Parse 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) { + defer h.Del(SSECopyKey) // remove SSE-C copy key of source object from headers after parsing + if h.Get(SSECopyAlgorithm) != SSEAlgorithmAES256 { + return key, ErrInvalidCustomerAlgorithm + } + if h.Get(SSECopyKey) == "" { + return key, ErrMissingCustomerKey + } + if h.Get(SSECopyKeyMD5) == "" { + return key, ErrMissingCustomerKeyMD5 + } + + clientKey, err := base64.StdEncoding.DecodeString(h.Get(SSECopyKey)) + if err != nil || len(clientKey) != 32 { // The client key must be 256 bits long + return key, ErrInvalidCustomerKey + } + keyMD5, err := base64.StdEncoding.DecodeString(h.Get(SSECopyKeyMD5)) + if md5Sum := md5.Sum(clientKey); err != nil || !bytes.Equal(md5Sum[:], keyMD5) { + return key, ErrCustomerKeyMD5Mismatch + } + copy(key[:], clientKey) + return key, nil +} diff --git a/cmd/crypto/header_test.go b/cmd/crypto/header_test.go index eedc35c2b..6a9538b3d 100644 --- a/cmd/crypto/header_test.go +++ b/cmd/crypto/header_test.go @@ -19,7 +19,7 @@ import ( "testing" ) -var isRequestedTests = []struct { +var s3IsRequestedTests = []struct { Header http.Header Expected bool }{ @@ -30,14 +30,14 @@ var isRequestedTests = []struct { } func TestS3IsRequested(t *testing.T) { - for i, test := range isRequestedTests { + for i, test := range s3IsRequestedTests { if got := S3.IsRequested(test.Header); got != test.Expected { t.Errorf("Test %d: Wanted %v but got %v", i, test.Expected, got) } } } -var parseTests = []struct { +var s3ParseTests = []struct { Header http.Header ExpectedErr error }{ @@ -48,9 +48,257 @@ var parseTests = []struct { } func TestS3Parse(t *testing.T) { - for i, test := range parseTests { + for i, test := range s3ParseTests { if err := S3.Parse(test.Header); err != test.ExpectedErr { t.Errorf("Test %d: Wanted '%v' but got '%v'", i, test.ExpectedErr, err) } } } + +var ssecIsRequestedTests = []struct { + Header http.Header + Expected bool +}{ + {Header: http.Header{}, Expected: false}, // 0 + {Header: http.Header{"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}}, Expected: true}, // 1 + {Header: http.Header{"X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}}, Expected: true}, // 2 + {Header: http.Header{"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}}, Expected: true}, // 3 + { + Header: http.Header{ + "X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{""}, + "X-Amz-Server-Side-Encryption-Customer-Key": []string{""}, + "X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{""}, + }, + Expected: true, + }, // 4 + { + Header: http.Header{ + "X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}, + "X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}, + "X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}, + }, + Expected: true, + }, // 5 + { + Header: http.Header{ + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}, + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}, + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}, + }, + Expected: false, + }, // 6 +} + +func TestSSECIsRequested(t *testing.T) { + for i, test := range ssecIsRequestedTests { + if got := SSEC.IsRequested(test.Header); got != test.Expected { + t.Errorf("Test %d: Wanted %v but got %v", i, test.Expected, got) + } + } +} + +var ssecCopyIsRequestedTests = []struct { + Header http.Header + Expected bool +}{ + {Header: http.Header{}, Expected: false}, // 0 + {Header: http.Header{"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}}, Expected: true}, // 1 + {Header: http.Header{"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}}, Expected: true}, // 2 + {Header: http.Header{"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}}, Expected: true}, // 3 + { + Header: http.Header{ + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{""}, + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{""}, + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{""}, + }, + Expected: true, + }, // 4 + { + Header: http.Header{ + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}, + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}, + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}, + }, + Expected: true, + }, // 5 + { + Header: http.Header{ + "X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}, + "X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}, + "X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}, + }, + Expected: false, + }, // 6 +} + +func TestSSECopyIsRequested(t *testing.T) { + for i, test := range ssecCopyIsRequestedTests { + if got := SSECopy.IsRequested(test.Header); got != test.Expected { + t.Errorf("Test %d: Wanted %v but got %v", i, test.Expected, got) + } + } +} + +var ssecParseTests = []struct { + Header http.Header + ExpectedErr error +}{ + { + Header: http.Header{ + "X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}, + "X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}, + "X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}, + }, + ExpectedErr: nil, // 0 + }, + { + Header: http.Header{ + "X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES-256"}, // invalid algorithm + "X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}, + "X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}, + }, + ExpectedErr: ErrInvalidCustomerAlgorithm, // 1 + }, + { + Header: http.Header{ + "X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}, + "X-Amz-Server-Side-Encryption-Customer-Key": []string{""}, // no client key + "X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}, + }, + ExpectedErr: ErrMissingCustomerKey, // 2 + }, + { + Header: http.Header{ + "X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}, + "X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRr.ZXltdXN0cHJvdmlkZWQ="}, // invalid key + "X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}, + }, + ExpectedErr: ErrInvalidCustomerKey, // 3 + }, + { + Header: http.Header{ + "X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}, + "X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}, + "X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{""}, // no key MD5 + }, + ExpectedErr: ErrMissingCustomerKeyMD5, // 4 + }, + { + Header: http.Header{ + "X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}, + "X-Amz-Server-Side-Encryption-Customer-Key": []string{"DzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}, // wrong client key + "X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}, + }, + ExpectedErr: ErrCustomerKeyMD5Mismatch, // 5 + }, + { + Header: http.Header{ + "X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}, + "X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}, + "X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{".7PpPLAK26ONlVUGOWlusfg=="}, // wrong key MD5 + }, + ExpectedErr: ErrCustomerKeyMD5Mismatch, // 6 + }, +} + +func TestSSECParse(t *testing.T) { + var zeroKey [32]byte + for i, test := range ssecParseTests { + key, err := SSEC.Parse(test.Header) + if err != test.ExpectedErr { + t.Errorf("Test %d: want error '%v' but got '%v'", i, test.ExpectedErr, err) + } + + if err != nil && key != zeroKey { + t.Errorf("Test %d: parsing failed and client key is not zero key", i) + } + if err == nil && key == zeroKey { + t.Errorf("Test %d: parsed client key is zero key", i) + } + if _, ok := test.Header[SSECKey]; ok { + t.Errorf("Test %d: client key is not removed from HTTP headers after parsing", i) + } + } +} + +var ssecCopyParseTests = []struct { + Header http.Header + ExpectedErr error +}{ + { + Header: http.Header{ + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}, + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}, + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}, + }, + ExpectedErr: nil, // 0 + }, + { + Header: http.Header{ + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES-256"}, // invalid algorithm + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}, + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}, + }, + ExpectedErr: ErrInvalidCustomerAlgorithm, // 1 + }, + { + Header: http.Header{ + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}, + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{""}, // no client key + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}, + }, + ExpectedErr: ErrMissingCustomerKey, // 2 + }, + { + Header: http.Header{ + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}, + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRr.ZXltdXN0cHJvdmlkZWQ="}, // invalid key + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}, + }, + ExpectedErr: ErrInvalidCustomerKey, // 3 + }, + { + Header: http.Header{ + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}, + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}, + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{""}, // no key MD5 + }, + ExpectedErr: ErrMissingCustomerKeyMD5, // 4 + }, + { + Header: http.Header{ + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}, + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"DzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}, // wrong client key + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}, + }, + ExpectedErr: ErrCustomerKeyMD5Mismatch, // 5 + }, + { + Header: http.Header{ + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}, + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}, + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{".7PpPLAK26ONlVUGOWlusfg=="}, // wrong key MD5 + }, + ExpectedErr: ErrCustomerKeyMD5Mismatch, // 6 + }, +} + +func TestSSECopyParse(t *testing.T) { + var zeroKey [32]byte + for i, test := range ssecCopyParseTests { + key, err := SSECopy.Parse(test.Header) + if err != test.ExpectedErr { + t.Errorf("Test %d: want error '%v' but got '%v'", i, test.ExpectedErr, err) + } + + if err != nil && key != zeroKey { + t.Errorf("Test %d: parsing failed and client key is not zero key", i) + } + if err == nil && key == zeroKey { + t.Errorf("Test %d: parsed client key is zero key", i) + } + if _, ok := test.Header[SSECKey]; ok { + t.Errorf("Test %d: client key is not removed from HTTP headers after parsing", i) + } + } +} diff --git a/cmd/crypto/sse.go b/cmd/crypto/sse.go index 09130e1f8..955fadc73 100644 --- a/cmd/crypto/sse.go +++ b/cmd/crypto/sse.go @@ -25,11 +25,24 @@ import ( ) const ( + // 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" + + // SSESealAlgorithm is the metadata key referencing the algorithm + // 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" + // S3SealedKey is the metadata key referencing the sealed object-key for SSE-S3. S3SealedKey = "X-Minio-Internal-Server-Side-Encryption-S3-Sealed-Key" + // S3KMSKeyID is the metadata key referencing the KMS key-id used to // generate/decrypt the S3-KMS-Sealed-Key. It is only used for SSE-S3 + KMS. S3KMSKeyID = "X-Minio-Internal-Server-Side-Encryption-S3-Kms-Key-Id" + // S3KMSSealedKey is the metadata key referencing the encrypted key generated // by KMS. It is only used for SSE-S3 + KMS. S3KMSSealedKey = "X-Minio-Internal-Server-Side-Encryption-S3-Kms-Sealed-Key" @@ -57,6 +70,14 @@ func EncryptSinglePart(r io.Reader, key ObjectKey) io.Reader { return r } +// EncryptMultiPart encrypts an io.Reader which must be the body of +// multi-part PUT request. It derives an unique encryption key from +// the partID and the object key. +func EncryptMultiPart(r io.Reader, partID int, key ObjectKey) io.Reader { + partKey := key.DerivePartKey(uint32(partID)) + return EncryptSinglePart(r, ObjectKey(partKey)) +} + // DecryptSinglePart decrypts an io.Writer which must an object // uploaded with the single-part PUT API. The offset and length // specify the requested range.