diff --git a/api/data/info.go b/api/data/info.go index 182793b37..8088b57cb 100644 --- a/api/data/info.go +++ b/api/data/info.go @@ -36,7 +36,6 @@ type ( CID cid.ID IsDir bool IsDeleteMarker bool - EncryptionInfo EncryptionInfo Bucket string Name string @@ -48,14 +47,6 @@ type ( Headers map[string]string } - // EncryptionInfo store parsed object encryption headers. - EncryptionInfo struct { - Enabled bool - Algorithm string - HMACKey string - HMACSalt string - } - // NotificationInfo store info to send s3 notification. NotificationInfo struct { Name string @@ -122,11 +113,6 @@ func (o *ObjectInfo) Address() oid.Address { return addr } -// IsEncrypted returns true if object is encrypted. -func (o ObjectInfo) IsEncrypted() bool { - return o.EncryptionInfo.Enabled -} - func (b BucketSettings) Unversioned() bool { return b.Versioning == VersioningUnversioned } diff --git a/api/errors/errors.go b/api/errors/errors.go index b7225fd6a..5d2709b4c 100644 --- a/api/errors/errors.go +++ b/api/errors/errors.go @@ -140,7 +140,6 @@ const ( ErrSSEMultipartEncrypted ErrSSEEncryptedObject ErrInvalidEncryptionParameters - ErrInvalidSSECustomerAlgorithm ErrInvalidEncryptionAlgorithm ErrInvalidSSECustomerKey ErrMissingSSECustomerKey @@ -1006,12 +1005,6 @@ var errorCodes = errorCodeMap{ Description: "The encryption parameters are not applicable to this object.", HTTPStatusCode: http.StatusBadRequest, }, - ErrInvalidSSECustomerAlgorithm: { - ErrCode: ErrInvalidSSECustomerAlgorithm, - Code: "InvalidArgument", - Description: "Requests specifying Server Side Encryption with Customer provided keys must provide a valid encryption algorithm.", - HTTPStatusCode: http.StatusBadRequest, - }, ErrInvalidEncryptionAlgorithm: { ErrCode: ErrInvalidEncryptionAlgorithm, Code: "InvalidArgument", diff --git a/api/handler/attributes.go b/api/handler/attributes.go index e0837318f..3710675c0 100644 --- a/api/handler/attributes.go +++ b/api/handler/attributes.go @@ -94,13 +94,13 @@ func (h *handler) GetObjectAttributesHandler(w http.ResponseWriter, r *http.Requ } info := extendedInfo.ObjectInfo - encryption, err := h.formEncryptionParams(r.Header) + encryptionParams, err := h.formEncryptionParams(r.Header) if err != nil { h.logAndSendError(w, "invalid sse headers", reqInfo, err) return } - if err = encryption.MatchObjectEncryption(info.EncryptionInfo); err != nil { + if err = encryptionParams.MatchObjectEncryption(layer.FormEncryptionInfo(info.Headers)); err != nil { h.logAndSendError(w, "encryption doesn't match object", reqInfo, errors.GetAPIError(errors.ErrBadRequest), zap.Error(err)) return } @@ -214,23 +214,14 @@ func formUploadAttributes(info *data.ObjectInfo, maxParts, marker int) (*ObjectP partInfos := strings.Split(completedParts, ",") parts := make([]Part, len(partInfos)) for i, p := range partInfos { - // partInfo[0] -- part number, partInfo[1] -- part size, partInfo[2] -- checksum - partInfo := strings.Split(p, "-") - if len(partInfo) != 3 { - return nil, fmt.Errorf("invalid completed parts header") - } - num, err := strconv.Atoi(partInfo[0]) + part, err := layer.ParseCompletedPartHeader(p) if err != nil { - return nil, err - } - size, err := strconv.Atoi(partInfo[1]) - if err != nil { - return nil, err + return nil, fmt.Errorf("invalid competed part: %w", err) } parts[i] = Part{ - PartNumber: num, - Size: size, - ChecksumSHA256: partInfo[2], + PartNumber: part.PartNumber, + Size: int(part.Size), + ChecksumSHA256: part.ETag, } } diff --git a/api/handler/copy.go b/api/handler/copy.go index 7b46d9f33..9df58ccf6 100644 --- a/api/handler/copy.go +++ b/api/handler/copy.go @@ -96,13 +96,13 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) { return } - encryption, err := h.formEncryptionParams(r.Header) + encryptionParams, err := h.formEncryptionParams(r.Header) if err != nil { h.logAndSendError(w, "invalid sse headers", reqInfo, err) return } - if err = encryption.MatchObjectEncryption(objInfo.EncryptionInfo); err != nil { + if err = encryptionParams.MatchObjectEncryption(layer.FormEncryptionInfo(objInfo.Headers)); err != nil { h.logAndSendError(w, "encryption doesn't match object", reqInfo, errors.GetAPIError(errors.ErrBadRequest), zap.Error(err)) return } @@ -128,7 +128,7 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) { DstObject: reqInfo.ObjectName, SrcSize: objInfo.Size, Header: metadata, - Encryption: encryption, + Encryption: encryptionParams, } settings, err := h.obj.GetBucketSettings(r.Context(), dstBktInfo) @@ -186,7 +186,7 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) { h.log.Error("couldn't send notification: %w", zap.Error(err)) } - if encryption.Enabled() { + if encryptionParams.Enabled() { addSSECHeaders(w.Header(), r.Header) } } diff --git a/api/handler/get.go b/api/handler/get.go index 0cb7393f7..9cf1ed9df 100644 --- a/api/handler/get.go +++ b/api/handler/get.go @@ -84,7 +84,7 @@ func writeHeaders(h http.Header, requestHeader http.Header, extendedInfo *data.E } h.Set(api.LastModified, info.Created.UTC().Format(http.TimeFormat)) - if info.IsEncrypted() { + if len(info.Headers[layer.AttributeEncryptionAlgorithm]) > 0 { h.Set(api.ContentLength, info.Headers[layer.AttributeDecryptedSize]) addSSECHeaders(h, requestHeader) } else { @@ -150,19 +150,19 @@ func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) { return } - encryption, err := h.formEncryptionParams(r.Header) + encryptionParams, err := h.formEncryptionParams(r.Header) if err != nil { h.logAndSendError(w, "invalid sse headers", reqInfo, err) return } - if err = encryption.MatchObjectEncryption(info.EncryptionInfo); err != nil { + if err = encryptionParams.MatchObjectEncryption(layer.FormEncryptionInfo(info.Headers)); err != nil { h.logAndSendError(w, "encryption doesn't match object", reqInfo, errors.GetAPIError(errors.ErrBadRequest), zap.Error(err)) return } fullSize := info.Size - if encryption.Enabled() { + if encryptionParams.Enabled() { if fullSize, err = strconv.ParseInt(info.Headers[layer.AttributeDecryptedSize], 10, 64); err != nil { h.logAndSendError(w, "invalid decrypted size header", reqInfo, errors.GetAPIError(errors.ErrBadRequest)) return @@ -213,7 +213,7 @@ func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) { Writer: w, Range: params, BucketInfo: bktInfo, - Encryption: encryption, + Encryption: encryptionParams, } if err = h.obj.GetObject(r.Context(), getParams); err != nil { h.logAndSendError(w, "could not get object", reqInfo, err) diff --git a/api/handler/head.go b/api/handler/head.go index 5b22c341a..846109334 100644 --- a/api/handler/head.go +++ b/api/handler/head.go @@ -53,13 +53,13 @@ func (h *handler) HeadObjectHandler(w http.ResponseWriter, r *http.Request) { } info := extendedInfo.ObjectInfo - encryption, err := h.formEncryptionParams(r.Header) + encryptionParams, err := h.formEncryptionParams(r.Header) if err != nil { h.logAndSendError(w, "invalid sse headers", reqInfo, err) return } - if err = encryption.MatchObjectEncryption(info.EncryptionInfo); err != nil { + if err = encryptionParams.MatchObjectEncryption(layer.FormEncryptionInfo(info.Headers)); err != nil { h.logAndSendError(w, "encryption doesn't match object", reqInfo, errors.GetAPIError(errors.ErrBadRequest), zap.Error(err)) return } diff --git a/api/handler/multipart_upload.go b/api/handler/multipart_upload.go index db6f3d484..050d56a0f 100644 --- a/api/handler/multipart_upload.go +++ b/api/handler/multipart_upload.go @@ -327,7 +327,7 @@ func (h *handler) UploadPartCopy(w http.ResponseWriter, r *http.Request) { return } - if err = p.Info.Encryption.MatchObjectEncryption(srcInfo.EncryptionInfo); err != nil { + if err = p.Info.Encryption.MatchObjectEncryption(layer.FormEncryptionInfo(srcInfo.Headers)); err != nil { h.logAndSendError(w, "encryption doesn't match object", reqInfo, errors.GetAPIError(errors.ErrBadRequest), zap.Error(err)) return } diff --git a/api/handler/put.go b/api/handler/put.go index 182f7ea71..ed9def2e3 100644 --- a/api/handler/put.go +++ b/api/handler/put.go @@ -21,6 +21,7 @@ import ( "github.com/nspcc-dev/neofs-s3-gw/api/data" "github.com/nspcc-dev/neofs-s3-gw/api/errors" "github.com/nspcc-dev/neofs-s3-gw/api/layer" + "github.com/nspcc-dev/neofs-s3-gw/api/layer/encryption" "github.com/nspcc-dev/neofs-s3-gw/creds/accessbox" "github.com/nspcc-dev/neofs-sdk-go/eacl" "github.com/nspcc-dev/neofs-sdk-go/session" @@ -297,7 +298,7 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) { api.WriteSuccessResponseHeadersOnly(w) } -func (h handler) formEncryptionParams(header http.Header) (enc layer.EncryptionParams, err error) { +func (h handler) formEncryptionParams(header http.Header) (enc encryption.Params, err error) { sseCustomerAlgorithm := header.Get(api.AmzServerSideEncryptionCustomerAlgorithm) sseCustomerKey := header.Get(api.AmzServerSideEncryptionCustomerKey) sseCustomerKeyMD5 := header.Get(api.AmzServerSideEncryptionCustomerKeyMD5) @@ -333,10 +334,7 @@ func (h handler) formEncryptionParams(header http.Header) (enc layer.EncryptionP return enc, errors.GetAPIError(errors.ErrSSECustomerKeyMD5Mismatch) } - var aesKey layer.AES256Key - copy(aesKey[:], key) - - return layer.NewEncryptionParams(aesKey), nil + return encryption.NewParams(key) } func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) { diff --git a/api/layer/encryption/encryption.go b/api/layer/encryption/encryption.go new file mode 100644 index 000000000..6c7379f79 --- /dev/null +++ b/api/layer/encryption/encryption.go @@ -0,0 +1,344 @@ +package encryption + +import ( + "bytes" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + errorsStd "errors" + "fmt" + "io" + + "github.com/minio/sio" +) + +// Params contains encryption key info. +type Params struct { + enabled bool + customerKey []byte +} + +// ObjectEncryption stores parsed object encryption headers. +type ObjectEncryption struct { + Enabled bool + Algorithm string + HMACKey string + HMACSalt string +} + +type encryptedPart struct { + size uint64 + encryptedSize uint64 +} + +// Range stores payload interval. +type Range struct { + Start uint64 + End uint64 +} + +// Decrypter allows decrypt payload of encrypted object. +type Decrypter struct { + reader io.Reader + decReader io.Reader + parts []encryptedPart + currentPart int + encryption Params + + rangeParam *Range + + partDataRemain uint64 + encPartRangeLen uint64 + + seqNumber uint64 + decLen uint64 + skipLen uint64 + + ln uint64 + off uint64 +} + +const ( + blockSize = 1 << 16 // 64KB + fullBlockSize = blockSize + 32 + aes256KeySize = 32 +) + +// NewParams creates new params to encrypt with provided key. +func NewParams(key []byte) (Params, error) { + var p Params + if len(key) != aes256KeySize { + return p, fmt.Errorf("invalid key size: %d", len(key)) + } + p.enabled = true + p.customerKey = make([]byte, aes256KeySize) + copy(p.customerKey, key) + return p, nil +} + +// Key returns encryption key as slice. +func (p Params) Key() []byte { + return p.customerKey[:] +} + +// Enabled returns true if key isn't empty. +func (p Params) Enabled() bool { + return p.enabled +} + +// HMAC computes salted HMAC. +func (p Params) HMAC() ([]byte, []byte, error) { + mac := hmac.New(sha256.New, p.Key()) + + salt := make([]byte, 16) + if _, err := rand.Read(salt); err != nil { + return nil, nil, errorsStd.New("failed to init create salt") + } + + mac.Write(salt) + return mac.Sum(nil), salt, nil +} + +// MatchObjectEncryption checks if encryption params are valid for provided object. +func (p Params) MatchObjectEncryption(encInfo ObjectEncryption) error { + if p.Enabled() != encInfo.Enabled { + return errorsStd.New("invalid encryption view") + } + + if !encInfo.Enabled { + return nil + } + + hmacSalt, err := hex.DecodeString(encInfo.HMACSalt) + if err != nil { + return fmt.Errorf("invalid hmacSalt '%s': %w", encInfo.HMACSalt, err) + } + + hmacKey, err := hex.DecodeString(encInfo.HMACKey) + if err != nil { + return fmt.Errorf("invalid hmacKey '%s': %w", encInfo.HMACKey, err) + } + + mac := hmac.New(sha256.New, p.Key()) + mac.Write(hmacSalt) + expectedHmacKey := mac.Sum(nil) + if !bytes.Equal(expectedHmacKey, hmacKey) { + return errorsStd.New("mismatched hmac key") + } + + return nil +} + +// NewMultipartDecrypter creates new decrypted that can decrypt multipart object +// that contains concatenation of encrypted parts. +func NewMultipartDecrypter(p Params, decryptedObjectSize uint64, partsSizes []uint64, r *Range) (*Decrypter, error) { + parts := make([]encryptedPart, len(partsSizes)) + + for i, size := range partsSizes { + encPartSize, err := sio.EncryptedSize(size) + if err != nil { + return nil, fmt.Errorf("compute encrypted size: %w", err) + } + + parts[i] = encryptedPart{ + size: size, + encryptedSize: encPartSize, + } + } + + rangeParam := r + if rangeParam == nil { + rangeParam = &Range{ + End: decryptedObjectSize - 1, + } + } + + return newDecrypter(p, parts, rangeParam) +} + +// NewDecrypter creates decrypter for regular encrypted object. +func NewDecrypter(p Params, encryptedObjectSize uint64, r *Range) (*Decrypter, error) { + decSize, err := sio.DecryptedSize(encryptedObjectSize) + if err != nil { + return nil, fmt.Errorf("compute decrypted size: %w", err) + } + + parts := []encryptedPart{{ + size: decSize, + encryptedSize: encryptedObjectSize, + }} + + return newDecrypter(p, parts, r) +} + +func newDecrypter(p Params, parts []encryptedPart, r *Range) (*Decrypter, error) { + if !p.Enabled() { + return nil, errorsStd.New("couldn't create decrypter with disabled encryption") + } + + if r != nil && r.Start > r.End { + return nil, fmt.Errorf("invalid range: %d %d", r.Start, r.End) + } + + decReader := &Decrypter{ + parts: parts, + rangeParam: r, + encryption: p, + } + + decReader.initRangeParams() + + return decReader, nil +} + +// DecryptedLength is actual (decrypted) length of data. +func (d Decrypter) DecryptedLength() uint64 { + return d.decLen +} + +// EncryptedLength is size of encrypted data that should be read for successful decryption. +func (d Decrypter) EncryptedLength() uint64 { + return d.ln +} + +// EncryptedOffset is offset of encrypted payload for successful decryption. +func (d Decrypter) EncryptedOffset() uint64 { + return d.off +} + +func (d *Decrypter) initRangeParams() { + d.partDataRemain = d.parts[d.currentPart].size + d.encPartRangeLen = d.parts[d.currentPart].encryptedSize + if d.rangeParam == nil { + d.decLen = d.partDataRemain + d.ln = d.encPartRangeLen + return + } + + start, end := d.rangeParam.Start, d.rangeParam.End + + var sum, encSum uint64 + var partStart int + for i, part := range d.parts { + if start < sum+part.size { + partStart = i + break + } + sum += part.size + encSum += part.encryptedSize + } + + d.skipLen = (start - sum) % blockSize + d.seqNumber = (start - sum) / blockSize + encOffPart := d.seqNumber * fullBlockSize + d.off = encSum + encOffPart + d.encPartRangeLen = d.encPartRangeLen - encOffPart + d.partDataRemain = d.partDataRemain + sum - start + + var partEnd int + for i, part := range d.parts[partStart:] { + index := partStart + i + if end < sum+part.size { + partEnd = index + break + } + sum += part.size + encSum += part.encryptedSize + } + + payloadPartEnd := (end - sum) / blockSize + endEnc := encSum + (payloadPartEnd+1)*fullBlockSize + + endPartEnc := encSum + d.parts[partEnd].encryptedSize + if endPartEnc < endEnc { + endEnc = endPartEnc + } + d.ln = endEnc - d.off + d.decLen = end - start + 1 + + if d.ln < d.encPartRangeLen { + d.encPartRangeLen = d.ln + } + if d.decLen < d.partDataRemain { + d.partDataRemain = d.decLen + } +} + +func (d *Decrypter) updateRangeParams() { + d.partDataRemain = d.parts[d.currentPart].size + d.encPartRangeLen = d.parts[d.currentPart].encryptedSize + d.seqNumber = 0 + d.skipLen = 0 +} + +// Read implements io.Reader. +func (d *Decrypter) Read(p []byte) (int, error) { + if uint64(len(p)) < d.partDataRemain { + n, err := d.decReader.Read(p) + if err != nil { + return n, err + } + d.partDataRemain -= uint64(n) + return n, nil + } + + n1, err := io.ReadFull(d.decReader, p[:d.partDataRemain]) + if err != nil { + return n1, err + } + + d.currentPart++ + if d.currentPart == len(d.parts) { + return n1, io.EOF + } + + d.updateRangeParams() + + err = d.initNextDecReader() + if err != nil { + return n1, err + } + + n2, err := d.decReader.Read(p[n1:]) + if err != nil { + return n1 + n2, err + } + + d.partDataRemain -= uint64(n2) + + return n1 + n2, nil +} + +// SetReader sets encrypted payload reader that should be decrypted. +// Must be invoked before any read. +func (d *Decrypter) SetReader(r io.Reader) error { + d.reader = r + return d.initNextDecReader() +} + +func (d *Decrypter) initNextDecReader() error { + if d.reader == nil { + return errorsStd.New("reader isn't set") + } + + r, err := sio.DecryptReader(io.LimitReader(d.reader, int64(d.encPartRangeLen)), + sio.Config{ + MinVersion: sio.Version20, + SequenceNumber: uint32(d.seqNumber), + Key: d.encryption.Key(), + CipherSuites: []byte{sio.AES_256_GCM}, + }) + if err != nil { + return fmt.Errorf("couldn't create decrypter: %w", err) + } + + if d.skipLen > 0 { + if _, err = io.CopyN(io.Discard, r, int64(d.skipLen)); err != nil { + return fmt.Errorf("couldn't skip some bytes: %w", err) + } + } + d.decReader = r + + return nil +} diff --git a/api/layer/encryption_test.go b/api/layer/encryption/encryption_test.go similarity index 71% rename from api/layer/encryption_test.go rename to api/layer/encryption/encryption_test.go index b76b29846..41ca53057 100644 --- a/api/layer/encryption_test.go +++ b/api/layer/encryption/encryption_test.go @@ -1,11 +1,10 @@ -package layer +package encryption import ( "encoding/hex" "strconv" "testing" - "github.com/nspcc-dev/neofs-s3-gw/api/data" "github.com/stretchr/testify/require" ) @@ -13,19 +12,20 @@ const ( aes256Key = "1234567890qwertyuiopasdfghjklzxc" ) -func getAES256Key() AES256Key { - var key AES256Key +func getAES256Key() []byte { + key := make([]byte, 32) copy(key[:], aes256Key) return key } func TestHMAC(t *testing.T) { - encParam := NewEncryptionParams(getAES256Key()) + encParam, err := NewParams(getAES256Key()) + require.NoError(t, err) hmacKey, hmacSalt, err := encParam.HMAC() require.NoError(t, err) - encInfo := data.EncryptionInfo{ + encInfo := ObjectEncryption{ Enabled: true, Algorithm: "", HMACKey: hex.EncodeToString(hmacKey), @@ -44,33 +44,34 @@ const ( encPartSize = 5245440 // partSize + enc headers ) -func getDecrypter() *decrypter { - parts := make([]EncryptedPart, partNum) +func getDecrypter(t *testing.T) *Decrypter { + parts := make([]encryptedPart, partNum) for i := range parts { - parts[i] = EncryptedPart{ - Part: Part{ - PartNumber: i + 1, - Size: int64(partSize), - }, - EncryptedSize: encPartSize, + parts[i] = encryptedPart{ + size: partSize, + encryptedSize: encPartSize, } } - return &decrypter{ + + params, err := NewParams(getAES256Key()) + require.NoError(t, err) + + return &Decrypter{ parts: parts, - encryption: NewEncryptionParams(getAES256Key()), + encryption: params, } } func TestDecrypterInitParams(t *testing.T) { - decReader := getDecrypter() + decReader := getDecrypter(t) for i, tc := range []struct { - rng *RangeParams + rng *Range expSkipLen, expLn, expOff, expSeqNumber uint64 - expDecLen, expDataRemain, expEncPartRange int64 + expDecLen, expDataRemain, expEncPartRange uint64 }{ { - rng: &RangeParams{End: objSize - 1}, + rng: &Range{End: objSize - 1}, expSkipLen: 0, expLn: encObjSize, expOff: 0, @@ -80,7 +81,7 @@ func TestDecrypterInitParams(t *testing.T) { expEncPartRange: encPartSize, }, { - rng: &RangeParams{End: 999999}, + rng: &Range{End: 999999}, expSkipLen: 0, expLn: 1049088, expOff: 0, @@ -90,7 +91,7 @@ func TestDecrypterInitParams(t *testing.T) { expEncPartRange: 1049088, }, { - rng: &RangeParams{Start: 1000000, End: 1999999}, + rng: &Range{Start: 1000000, End: 1999999}, expSkipLen: 16960, expLn: 1049088, expOff: 983520, diff --git a/api/layer/layer.go b/api/layer/layer.go index ac46411a9..683b1ba8c 100644 --- a/api/layer/layer.go +++ b/api/layer/layer.go @@ -1,14 +1,9 @@ package layer import ( - "bytes" "context" "crypto/ecdsa" - "crypto/hmac" "crypto/rand" - "crypto/sha256" - "encoding/hex" - errorsStd "errors" "fmt" "io" "net/url" @@ -16,7 +11,8 @@ import ( "strings" "time" - "github.com/minio/sio" + "github.com/nspcc-dev/neofs-s3-gw/api/layer/encryption" + "github.com/nats-io/nats.go" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neofs-s3-gw/api" @@ -89,7 +85,7 @@ type ( ObjectInfo *data.ObjectInfo BucketInfo *data.BucketInfo Writer io.Writer - Encryption EncryptionParams + Encryption encryption.Params } // HeadObjectParams stores object head request parameters. @@ -113,14 +109,6 @@ type ( End uint64 } - // AES256Key is a key for encryption. - AES256Key [32]byte - - EncryptionParams struct { - enabled bool - customerKey AES256Key - } - // PutObjectParams stores object put request parameters. PutObjectParams struct { BktInfo *data.BucketInfo @@ -129,7 +117,7 @@ type ( Reader io.Reader Header map[string]string Lock *data.ObjectLock - Encryption EncryptionParams + Encryption encryption.Params } DeleteObjectParams struct { @@ -160,7 +148,7 @@ type ( Header map[string]string Range *RangeParams Lock *data.ObjectLock - Encryption EncryptionParams + Encryption encryption.Params } // CreateBucketParams stores bucket create request parameters. CreateBucketParams struct { @@ -285,72 +273,6 @@ func (f MsgHandlerFunc) HandleMessage(ctx context.Context, msg *nats.Msg) error return f(ctx, msg) } -// NewEncryptionParams create new params to encrypt with provided key. -func NewEncryptionParams(key AES256Key) EncryptionParams { - return EncryptionParams{ - enabled: true, - customerKey: key, - } -} - -// Key returns encryption key as slice. -func (p EncryptionParams) Key() []byte { - return p.customerKey[:] -} - -// AESKey returns encryption key. -func (p EncryptionParams) AESKey() AES256Key { - return p.customerKey -} - -// Enabled returns true if key isn't empty. -func (p EncryptionParams) Enabled() bool { - return p.enabled -} - -// HMAC compute salted HMAC. -func (p EncryptionParams) HMAC() ([]byte, []byte, error) { - mac := hmac.New(sha256.New, p.Key()) - - salt := make([]byte, 16) - if _, err := rand.Read(salt); err != nil { - return nil, nil, errorsStd.New("failed to init create salt") - } - - mac.Write(salt) - return mac.Sum(nil), salt, nil -} - -// MatchObjectEncryption check if encryption params are valid for provided object. -func (p EncryptionParams) MatchObjectEncryption(encInfo data.EncryptionInfo) error { - if p.Enabled() != encInfo.Enabled { - return errorsStd.New("invalid encryption view") - } - - if !encInfo.Enabled { - return nil - } - - hmacSalt, err := hex.DecodeString(encInfo.HMACSalt) - if err != nil { - return fmt.Errorf("invalid hmacSalt '%s': %w", encInfo.HMACSalt, err) - } - - hmacKey, err := hex.DecodeString(encInfo.HMACKey) - if err != nil { - return fmt.Errorf("invalid hmacKey '%s': %w", encInfo.HMACKey, err) - } - - mac := hmac.New(sha256.New, p.Key()) - mac.Write(hmacSalt) - expectedHmacKey := mac.Sum(nil) - if !bytes.Equal(expectedHmacKey, hmacKey) { - return errorsStd.New("mismatched hmac key") - } - - return nil -} - // DefaultCachesConfigs returns filled configs. func DefaultCachesConfigs(logger *zap.Logger) *CachesConfig { return &CachesConfig{ @@ -473,253 +395,6 @@ func (n *layer) ListBuckets(ctx context.Context) ([]*data.BucketInfo, error) { return n.containerList(ctx) } -func formEncryptedParts(header string) ([]EncryptedPart, error) { - partInfos := strings.Split(header, ",") - result := make([]EncryptedPart, len(partInfos)) - - for i, partInfo := range partInfos { - part, err := parseCompletedPartHeader(partInfo) - if err != nil { - return nil, err - } - - encPartSize, err := sio.EncryptedSize(uint64(part.Size)) - if err != nil { - return nil, fmt.Errorf("compute encrypted size: %w", err) - } - - result[i] = EncryptedPart{ - Part: *part, - EncryptedSize: int64(encPartSize), - } - } - - return result, nil -} - -type decrypter struct { - reader io.Reader - decReader io.Reader - parts []EncryptedPart - currentPart int - encryption EncryptionParams - - rangeParam *RangeParams - - partDataRemain int64 - encPartRangeLen int64 - - seqNumber uint64 - decLen int64 - skipLen uint64 - - ln uint64 - off uint64 -} - -func (d decrypter) decLength() int64 { - return d.decLen -} - -func (d decrypter) encLength() uint64 { - return d.ln -} - -func (d decrypter) encOffset() uint64 { - return d.off -} - -func getDecryptReader(p *GetObjectParams) (*decrypter, error) { - if !p.Encryption.Enabled() { - return nil, errorsStd.New("couldn't create decrypter with disabled encryption") - } - - rangeParam := p.Range - - var err error - var parts []EncryptedPart - header := p.ObjectInfo.Headers[UploadCompletedParts] - if len(header) != 0 { - parts, err = formEncryptedParts(header) - if err != nil { - return nil, fmt.Errorf("form parts: %w", err) - } - if rangeParam == nil { - decSizeHeader := p.ObjectInfo.Headers[AttributeDecryptedSize] - size, err := strconv.ParseUint(decSizeHeader, 10, 64) - if err != nil { - return nil, fmt.Errorf("parse dec size header '%s': %w", decSizeHeader, err) - } - rangeParam = &RangeParams{ - Start: 0, - End: size - 1, - } - } - } else { - decSize, err := sio.DecryptedSize(uint64(p.ObjectInfo.Size)) - if err != nil { - return nil, fmt.Errorf("compute decrypted size: %w", err) - } - - parts = []EncryptedPart{{ - Part: Part{Size: int64(decSize)}, - EncryptedSize: p.ObjectInfo.Size, - }} - } - - if rangeParam != nil && rangeParam.Start > rangeParam.End { - return nil, fmt.Errorf("invalid range: %d %d", rangeParam.Start, rangeParam.End) - } - - decReader := &decrypter{ - parts: parts, - rangeParam: rangeParam, - encryption: p.Encryption, - } - - decReader.initRangeParams() - - return decReader, nil -} - -const ( - blockSize = 1 << 16 // 64KB - fullBlockSize = blockSize + 32 -) - -func (d *decrypter) initRangeParams() { - d.partDataRemain = d.parts[d.currentPart].Size - d.encPartRangeLen = d.parts[d.currentPart].EncryptedSize - if d.rangeParam == nil { - d.decLen = d.partDataRemain - d.ln = uint64(d.encPartRangeLen) - return - } - - start, end := d.rangeParam.Start, d.rangeParam.End - - var sum, encSum uint64 - var partStart int - for i, part := range d.parts { - if start < sum+uint64(part.Size) { - partStart = i - break - } - sum += uint64(part.Size) - encSum += uint64(part.EncryptedSize) - } - - d.skipLen = (start - sum) % blockSize - d.seqNumber = (start - sum) / blockSize - encOffPart := d.seqNumber * fullBlockSize - d.off = encSum + encOffPart - d.encPartRangeLen = d.encPartRangeLen - int64(encOffPart) - d.partDataRemain = d.partDataRemain + int64(sum-start) - - var partEnd int - for i, part := range d.parts[partStart:] { - index := partStart + i - if end < sum+uint64(part.Size) { - partEnd = index - break - } - sum += uint64(part.Size) - encSum += uint64(part.EncryptedSize) - } - - payloadPartEnd := (end - sum) / blockSize - endEnc := encSum + (payloadPartEnd+1)*fullBlockSize - - endPartEnc := encSum + uint64(d.parts[partEnd].EncryptedSize) - if endPartEnc < endEnc { - endEnc = endPartEnc - } - d.ln = endEnc - d.off - d.decLen = int64(end - start + 1) - - if int64(d.ln) < d.encPartRangeLen { - d.encPartRangeLen = int64(d.ln) - } - if d.decLen < d.partDataRemain { - d.partDataRemain = d.decLen - } -} - -func (d *decrypter) updateRangeParams() { - d.partDataRemain = d.parts[d.currentPart].Size - d.encPartRangeLen = d.parts[d.currentPart].EncryptedSize - d.seqNumber = 0 - d.skipLen = 0 -} - -func (d *decrypter) Read(p []byte) (int, error) { - if int64(len(p)) < d.partDataRemain { - n, err := d.decReader.Read(p) - if err != nil { - return n, err - } - d.partDataRemain -= int64(n) - return n, nil - } - - n1, err := io.ReadFull(d.decReader, p[:d.partDataRemain]) - if err != nil { - return n1, err - } - - d.currentPart++ - if d.currentPart == len(d.parts) { - return n1, io.EOF - } - - d.updateRangeParams() - - err = d.initNextDecReader() - if err != nil { - return n1, err - } - - n2, err := d.decReader.Read(p[n1:]) - if err != nil { - return n1 + n2, err - } - - d.partDataRemain -= int64(n2) - - return n1 + n2, nil -} - -func (d *decrypter) SetReader(r io.Reader) error { - d.reader = r - return d.initNextDecReader() -} - -func (d *decrypter) initNextDecReader() error { - if d.reader == nil { - return errorsStd.New("reader isn't set") - } - - r, err := sio.DecryptReader(io.LimitReader(d.reader, d.encPartRangeLen), - sio.Config{ - MinVersion: sio.Version20, - SequenceNumber: uint32(d.seqNumber), - Key: d.encryption.Key(), - CipherSuites: []byte{sio.AES_256_GCM}, - }) - if err != nil { - return fmt.Errorf("couldn't create decrypter: %w", err) - } - - if d.skipLen > 0 { - if _, err = io.CopyN(io.Discard, r, int64(d.skipLen)); err != nil { - return fmt.Errorf("couldn't skip some bytes: %w", err) - } - } - d.decReader = r - - return nil -} - // GetObject from storage. func (n *layer) GetObject(ctx context.Context, p *GetObjectParams) error { var params getParams @@ -727,15 +402,15 @@ func (n *layer) GetObject(ctx context.Context, p *GetObjectParams) error { params.oid = p.ObjectInfo.ID params.bktInfo = p.BucketInfo - var decReader *decrypter + var decReader *encryption.Decrypter if p.Encryption.Enabled() { var err error - decReader, err = getDecryptReader(p) + decReader, err = getDecrypter(p) if err != nil { return fmt.Errorf("creating decrypter: %w", err) } - params.off = decReader.encOffset() - params.ln = decReader.encLength() + params.off = decReader.EncryptedOffset() + params.ln = decReader.EncryptedLength() } else { if p.Range != nil { if p.Range.Start > p.Range.End { @@ -764,18 +439,47 @@ func (n *layer) GetObject(ctx context.Context, p *GetObjectParams) error { if err = decReader.SetReader(payload); err != nil { return fmt.Errorf("set reader to decrypter: %w", err) } - r = io.LimitReader(decReader, decReader.decLength()) + r = io.LimitReader(decReader, int64(decReader.DecryptedLength())) } // copy full payload written, err := io.CopyBuffer(p.Writer, r, buf) if err != nil { - return fmt.Errorf("copy object payload written: '%d', decLength: '%d', params.ln: '%d' : %w", written, decReader.decLength(), params.ln, err) + return fmt.Errorf("copy object payload written: '%d', decLength: '%d', params.ln: '%d' : %w", written, decReader.DecryptedLength(), params.ln, err) } return nil } +func getDecrypter(p *GetObjectParams) (*encryption.Decrypter, error) { + var encRange *encryption.Range + if p.Range != nil { + encRange = &encryption.Range{Start: p.Range.Start, End: p.Range.End} + } + + header := p.ObjectInfo.Headers[UploadCompletedParts] + if len(header) == 0 { + return encryption.NewDecrypter(p.Encryption, uint64(p.ObjectInfo.Size), encRange) + } + + decryptedObjectSize, err := strconv.ParseUint(p.ObjectInfo.Headers[AttributeDecryptedSize], 10, 64) + if err != nil { + return nil, fmt.Errorf("parse decrypted size: %w", err) + } + + splits := strings.Split(header, ",") + sizes := make([]uint64, len(splits)) + for i, splitInfo := range splits { + part, err := ParseCompletedPartHeader(splitInfo) + if err != nil { + return nil, fmt.Errorf("parse completed part: %w", err) + } + sizes[i] = uint64(part.Size) + } + + return encryption.NewMultipartDecrypter(p.Encryption, decryptedObjectSize, sizes, encRange) +} + // GetObjectInfo returns meta information about the object. func (n *layer) GetObjectInfo(ctx context.Context, p *HeadObjectParams) (*data.ObjectInfo, error) { extendedObjectInfo, err := n.GetExtendedObjectInfo(ctx, p) diff --git a/api/layer/multipart_upload.go b/api/layer/multipart_upload.go index 27f664968..f2d5cf7d8 100644 --- a/api/layer/multipart_upload.go +++ b/api/layer/multipart_upload.go @@ -14,6 +14,7 @@ import ( "github.com/minio/sio" "github.com/nspcc-dev/neofs-s3-gw/api/data" "github.com/nspcc-dev/neofs-s3-gw/api/errors" + "github.com/nspcc-dev/neofs-s3-gw/api/layer/encryption" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" "github.com/nspcc-dev/neofs-sdk-go/user" "go.uber.org/zap" @@ -40,7 +41,7 @@ type ( UploadID string Bkt *data.BucketInfo Key string - Encryption EncryptionParams + Encryption encryption.Params } CreateMultipartParams struct { @@ -190,7 +191,7 @@ func (n *layer) UploadPart(ctx context.Context, p *UploadPartParams) (string, er } func (n *layer) uploadPart(ctx context.Context, multipartInfo *data.MultipartInfo, p *UploadPartParams) (*data.ObjectInfo, error) { - encInfo := formEncryptionInfo(multipartInfo.Meta) + encInfo := FormEncryptionInfo(multipartInfo.Meta) if err := p.Info.Encryption.MatchObjectEncryption(encInfo); err != nil { n.log.Warn("mismatched obj encryptionInfo", zap.Error(err)) return nil, errors.GetAPIError(errors.ErrInvalidEncryptionParameters) @@ -356,7 +357,7 @@ func (n *layer) CompleteMultipartUpload(ctx context.Context, p *CompleteMultipar if err != nil { return nil, nil, err } - encInfo := formEncryptionInfo(multipartInfo.Meta) + encInfo := FormEncryptionInfo(multipartInfo.Meta) if len(partsInfo) < len(p.Parts) { return nil, nil, errors.GetAPIError(errors.ErrInvalidPart) @@ -545,7 +546,7 @@ func (n *layer) ListParts(ctx context.Context, p *ListPartsParams) (*ListPartsIn return nil, err } - encInfo := formEncryptionInfo(multipartInfo.Meta) + encInfo := FormEncryptionInfo(multipartInfo.Meta) if err = p.Info.Encryption.MatchObjectEncryption(encInfo); err != nil { n.log.Warn("mismatched obj encryptionInfo", zap.Error(err)) return nil, errors.GetAPIError(errors.ErrInvalidEncryptionParameters) diff --git a/api/layer/object.go b/api/layer/object.go index b5ce2d934..38795d354 100644 --- a/api/layer/object.go +++ b/api/layer/object.go @@ -161,7 +161,7 @@ func encryptionReader(r io.Reader, size uint64, key []byte) (io.Reader, uint64, return r, encSize, nil } -func parseCompletedPartHeader(hdr string) (*Part, error) { +func ParseCompletedPartHeader(hdr string) (*Part, error) { // partInfo[0] -- part number, partInfo[1] -- part size, partInfo[2] -- checksum partInfo := strings.Split(hdr, "-") if len(partInfo) != 3 { @@ -268,9 +268,8 @@ func (n *layer) PutObject(ctx context.Context, p *PutObjectParams) (*data.Object n.listsCache.CleanCacheEntriesContainingObject(p.Object, p.BktInfo.CID) objInfo := &data.ObjectInfo{ - ID: id, - CID: p.BktInfo.CID, - EncryptionInfo: formEncryptionInfo(p.Header), + ID: id, + CID: p.BktInfo.CID, Owner: own, Bucket: p.BktInfo.Name, diff --git a/api/layer/util.go b/api/layer/util.go index d26ba9761..d5b85648a 100644 --- a/api/layer/util.go +++ b/api/layer/util.go @@ -9,6 +9,8 @@ import ( "strings" "time" + "github.com/nspcc-dev/neofs-s3-gw/api/layer/encryption" + "github.com/nspcc-dev/neofs-s3-gw/api" "github.com/nspcc-dev/neofs-s3-gw/api/data" "github.com/nspcc-dev/neofs-s3-gw/creds/accessbox" @@ -83,10 +85,9 @@ func objectInfoFromMeta(bkt *data.BucketInfo, meta *object.Object) *data.ObjectI objID, _ := meta.ID() payloadChecksum, _ := meta.PayloadChecksum() return &data.ObjectInfo{ - ID: objID, - CID: bkt.CID, - IsDir: false, - EncryptionInfo: formEncryptionInfo(headers), + ID: objID, + CID: bkt.CID, + IsDir: false, Bucket: bkt.Name, Name: filenameFromObject(meta), @@ -99,9 +100,9 @@ func objectInfoFromMeta(bkt *data.BucketInfo, meta *object.Object) *data.ObjectI } } -func formEncryptionInfo(headers map[string]string) data.EncryptionInfo { +func FormEncryptionInfo(headers map[string]string) encryption.ObjectEncryption { algorithm := headers[AttributeEncryptionAlgorithm] - return data.EncryptionInfo{ + return encryption.ObjectEncryption{ Enabled: len(algorithm) > 0, Algorithm: algorithm, HMACKey: headers[AttributeHMACKey], @@ -109,7 +110,7 @@ func formEncryptionInfo(headers map[string]string) data.EncryptionInfo { } } -func addEncryptionHeaders(meta map[string]string, enc EncryptionParams) error { +func addEncryptionHeaders(meta map[string]string, enc encryption.Params) error { meta[AttributeEncryptionAlgorithm] = AESEncryptionAlgorithm hmacKey, hmacSalt, err := enc.HMAC() if err != nil {