|
|
|
|
@@ -24,7 +24,6 @@ import (
|
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
|
|
|
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
|
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/util/mem"
|
|
|
|
|
|
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
|
|
|
)
|
|
|
|
|
@@ -2394,45 +2393,6 @@ func (s3a *S3ApiServer) HeadObjectHandler(w http.ResponseWriter, r *http.Request
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func captureCORSHeaders(w http.ResponseWriter, headersToCapture []string) map[string]string {
|
|
|
|
|
captured := make(map[string]string)
|
|
|
|
|
for _, corsHeader := range headersToCapture {
|
|
|
|
|
if value := w.Header().Get(corsHeader); value != "" {
|
|
|
|
|
captured[corsHeader] = value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return captured
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func restoreCORSHeaders(w http.ResponseWriter, capturedCORSHeaders map[string]string) {
|
|
|
|
|
for corsHeader, value := range capturedCORSHeaders {
|
|
|
|
|
w.Header().Set(corsHeader, value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// writeFinalResponse handles the common response writing logic shared between
|
|
|
|
|
// passThroughResponse and handleSSECResponse
|
|
|
|
|
func writeFinalResponse(w http.ResponseWriter, proxyResponse *http.Response, bodyReader io.Reader, capturedCORSHeaders map[string]string) (statusCode int, bytesTransferred int64) {
|
|
|
|
|
// Restore CORS headers that were set by middleware
|
|
|
|
|
restoreCORSHeaders(w, capturedCORSHeaders)
|
|
|
|
|
|
|
|
|
|
if proxyResponse.Header.Get("Content-Range") != "" && proxyResponse.StatusCode == 200 {
|
|
|
|
|
statusCode = http.StatusPartialContent
|
|
|
|
|
} else {
|
|
|
|
|
statusCode = proxyResponse.StatusCode
|
|
|
|
|
}
|
|
|
|
|
w.WriteHeader(statusCode)
|
|
|
|
|
|
|
|
|
|
// Stream response data
|
|
|
|
|
buf := mem.Allocate(128 * 1024)
|
|
|
|
|
defer mem.Free(buf)
|
|
|
|
|
bytesTransferred, err := io.CopyBuffer(w, bodyReader, buf)
|
|
|
|
|
if err != nil {
|
|
|
|
|
glog.V(1).Infof("response read %d bytes: %v", bytesTransferred, err)
|
|
|
|
|
}
|
|
|
|
|
return statusCode, bytesTransferred
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// fetchObjectEntry fetches the filer entry for an object
|
|
|
|
|
// Returns nil if not found (not an error), or propagates other errors
|
|
|
|
|
func (s3a *S3ApiServer) fetchObjectEntry(bucket, object string) (*filer_pb.Entry, error) {
|
|
|
|
|
@@ -2458,187 +2418,6 @@ func (s3a *S3ApiServer) fetchObjectEntryRequired(bucket, object string) (*filer_
|
|
|
|
|
return fetchedEntry, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// copyResponseHeaders copies headers from proxy response to the response writer,
|
|
|
|
|
// excluding internal SeaweedFS headers and optionally excluding body-related headers
|
|
|
|
|
func copyResponseHeaders(w http.ResponseWriter, proxyResponse *http.Response, excludeBodyHeaders bool) {
|
|
|
|
|
for k, v := range proxyResponse.Header {
|
|
|
|
|
// Always exclude internal SeaweedFS headers
|
|
|
|
|
if s3_constants.IsSeaweedFSInternalHeader(k) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
// Optionally exclude body-related headers that might change after decryption
|
|
|
|
|
if excludeBodyHeaders && (k == "Content-Length" || k == "Content-Encoding") {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
w.Header()[k] = v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func passThroughResponse(proxyResponse *http.Response, w http.ResponseWriter) (statusCode int, bytesTransferred int64) {
|
|
|
|
|
// Capture existing CORS headers that may have been set by middleware
|
|
|
|
|
capturedCORSHeaders := captureCORSHeaders(w, corsHeaders)
|
|
|
|
|
|
|
|
|
|
// Copy headers from proxy response (excluding internal SeaweedFS headers)
|
|
|
|
|
copyResponseHeaders(w, proxyResponse, false)
|
|
|
|
|
|
|
|
|
|
return writeFinalResponse(w, proxyResponse, proxyResponse.Body, capturedCORSHeaders)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handleSSECResponse handles SSE-C decryption and response processing
|
|
|
|
|
func (s3a *S3ApiServer) handleSSECResponse(r *http.Request, proxyResponse *http.Response, w http.ResponseWriter, entry *filer_pb.Entry) (statusCode int, bytesTransferred int64) {
|
|
|
|
|
// Check if the object has SSE-C metadata
|
|
|
|
|
sseAlgorithm := proxyResponse.Header.Get(s3_constants.AmzServerSideEncryptionCustomerAlgorithm)
|
|
|
|
|
sseKeyMD5 := proxyResponse.Header.Get(s3_constants.AmzServerSideEncryptionCustomerKeyMD5)
|
|
|
|
|
isObjectEncrypted := sseAlgorithm != "" && sseKeyMD5 != ""
|
|
|
|
|
|
|
|
|
|
// Parse SSE-C headers from request once (avoid duplication)
|
|
|
|
|
customerKey, err := ParseSSECHeaders(r)
|
|
|
|
|
if err != nil {
|
|
|
|
|
errCode := MapSSECErrorToS3Error(err)
|
|
|
|
|
s3err.WriteErrorResponse(w, r, errCode)
|
|
|
|
|
return http.StatusBadRequest, 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if isObjectEncrypted {
|
|
|
|
|
// This object was encrypted with SSE-C, validate customer key
|
|
|
|
|
if customerKey == nil {
|
|
|
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrSSECustomerKeyMissing)
|
|
|
|
|
return http.StatusBadRequest, 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SSE-C MD5 is base64 and case-sensitive
|
|
|
|
|
if customerKey.KeyMD5 != sseKeyMD5 {
|
|
|
|
|
// For GET/HEAD requests, AWS S3 returns 403 Forbidden for a key mismatch.
|
|
|
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
|
|
|
|
return http.StatusForbidden, 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SSE-C encrypted objects support HTTP Range requests
|
|
|
|
|
// The IV is stored in metadata and CTR mode allows seeking to any offset
|
|
|
|
|
// Range requests will be handled by the filer layer with proper offset-based decryption
|
|
|
|
|
|
|
|
|
|
// Check if this is a chunked or small content SSE-C object
|
|
|
|
|
// Use the entry parameter passed from the caller (avoids redundant lookup)
|
|
|
|
|
if entry != nil {
|
|
|
|
|
// Check for SSE-C chunks
|
|
|
|
|
sseCChunks := 0
|
|
|
|
|
for _, chunk := range entry.GetChunks() {
|
|
|
|
|
if chunk.GetSseType() == filer_pb.SSEType_SSE_C {
|
|
|
|
|
sseCChunks++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if sseCChunks >= 1 {
|
|
|
|
|
|
|
|
|
|
// Handle chunked SSE-C objects - each chunk needs independent decryption
|
|
|
|
|
multipartReader, decErr := s3a.createMultipartSSECDecryptedReader(r, proxyResponse, entry)
|
|
|
|
|
if decErr != nil {
|
|
|
|
|
glog.Errorf("Failed to create multipart SSE-C decrypted reader: %v", decErr)
|
|
|
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
|
|
|
|
return http.StatusInternalServerError, 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Capture existing CORS headers
|
|
|
|
|
capturedCORSHeaders := captureCORSHeaders(w, corsHeaders)
|
|
|
|
|
|
|
|
|
|
// Copy headers from proxy response (excluding internal SeaweedFS headers)
|
|
|
|
|
copyResponseHeaders(w, proxyResponse, false)
|
|
|
|
|
|
|
|
|
|
// Set proper headers for range requests
|
|
|
|
|
rangeHeader := r.Header.Get("Range")
|
|
|
|
|
if rangeHeader != "" {
|
|
|
|
|
|
|
|
|
|
// Parse range header (e.g., "bytes=0-99")
|
|
|
|
|
if len(rangeHeader) > 6 && rangeHeader[:6] == "bytes=" {
|
|
|
|
|
rangeSpec := rangeHeader[6:]
|
|
|
|
|
parts := strings.Split(rangeSpec, "-")
|
|
|
|
|
if len(parts) == 2 {
|
|
|
|
|
startOffset, endOffset := int64(0), int64(-1)
|
|
|
|
|
if parts[0] != "" {
|
|
|
|
|
startOffset, _ = strconv.ParseInt(parts[0], 10, 64)
|
|
|
|
|
}
|
|
|
|
|
if parts[1] != "" {
|
|
|
|
|
endOffset, _ = strconv.ParseInt(parts[1], 10, 64)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if endOffset >= startOffset {
|
|
|
|
|
// Specific range - set proper Content-Length and Content-Range headers
|
|
|
|
|
rangeLength := endOffset - startOffset + 1
|
|
|
|
|
totalSize := proxyResponse.Header.Get("Content-Length")
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Content-Length", strconv.FormatInt(rangeLength, 10))
|
|
|
|
|
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%s", startOffset, endOffset, totalSize))
|
|
|
|
|
// writeFinalResponse will set status to 206 if Content-Range is present
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return writeFinalResponse(w, proxyResponse, multipartReader, capturedCORSHeaders)
|
|
|
|
|
} else if len(entry.GetChunks()) == 0 && len(entry.Content) > 0 {
|
|
|
|
|
// Small content SSE-C object stored directly in entry.Content
|
|
|
|
|
|
|
|
|
|
// Fall through to traditional single-object SSE-C handling below
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Single-part SSE-C object: Get IV from proxy response headers (stored during upload)
|
|
|
|
|
ivBase64 := proxyResponse.Header.Get(s3_constants.SeaweedFSSSEIVHeader)
|
|
|
|
|
if ivBase64 == "" {
|
|
|
|
|
glog.Errorf("SSE-C encrypted single-part object missing IV in metadata")
|
|
|
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
|
|
|
|
return http.StatusInternalServerError, 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
iv, err := base64.StdEncoding.DecodeString(ivBase64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
glog.Errorf("Failed to decode IV from metadata: %v", err)
|
|
|
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
|
|
|
|
return http.StatusInternalServerError, 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create decrypted reader with IV from metadata
|
|
|
|
|
decryptedReader, decErr := CreateSSECDecryptedReader(proxyResponse.Body, customerKey, iv)
|
|
|
|
|
if decErr != nil {
|
|
|
|
|
glog.Errorf("Failed to create SSE-C decrypted reader: %v", decErr)
|
|
|
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
|
|
|
|
return http.StatusInternalServerError, 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Capture existing CORS headers that may have been set by middleware
|
|
|
|
|
capturedCORSHeaders := captureCORSHeaders(w, corsHeaders)
|
|
|
|
|
|
|
|
|
|
// Copy headers from proxy response (excluding body-related headers that might change and internal SeaweedFS headers)
|
|
|
|
|
copyResponseHeaders(w, proxyResponse, true)
|
|
|
|
|
|
|
|
|
|
// Set correct Content-Length for SSE-C (only for full object requests)
|
|
|
|
|
// With IV stored in metadata, the encrypted length equals the original length
|
|
|
|
|
if proxyResponse.Header.Get("Content-Range") == "" {
|
|
|
|
|
// Full object request: encrypted length equals original length (IV not in stream)
|
|
|
|
|
if contentLengthStr := proxyResponse.Header.Get("Content-Length"); contentLengthStr != "" {
|
|
|
|
|
// Content-Length is already correct since IV is stored in metadata, not in data stream
|
|
|
|
|
w.Header().Set("Content-Length", contentLengthStr)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// For range requests, let the actual bytes transferred determine the response length
|
|
|
|
|
|
|
|
|
|
// Add SSE-C response headers
|
|
|
|
|
w.Header().Set(s3_constants.AmzServerSideEncryptionCustomerAlgorithm, sseAlgorithm)
|
|
|
|
|
w.Header().Set(s3_constants.AmzServerSideEncryptionCustomerKeyMD5, sseKeyMD5)
|
|
|
|
|
|
|
|
|
|
return writeFinalResponse(w, proxyResponse, decryptedReader, capturedCORSHeaders)
|
|
|
|
|
} else {
|
|
|
|
|
// Object is not encrypted, but check if customer provided SSE-C headers unnecessarily
|
|
|
|
|
if customerKey != nil {
|
|
|
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrSSECustomerKeyNotNeeded)
|
|
|
|
|
return http.StatusBadRequest, 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Normal pass-through response
|
|
|
|
|
return passThroughResponse(proxyResponse, w)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// addObjectLockHeadersToResponse extracts object lock metadata from entry Extended attributes
|
|
|
|
|
// and adds the appropriate S3 headers to the response
|
|
|
|
|
func (s3a *S3ApiServer) addObjectLockHeadersToResponse(w http.ResponseWriter, entry *filer_pb.Entry) {
|
|
|
|
|
@@ -2680,54 +2459,6 @@ func (s3a *S3ApiServer) addObjectLockHeadersToResponse(w http.ResponseWriter, en
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// addSSEHeadersToResponse converts stored SSE metadata from entry.Extended to HTTP response headers
|
|
|
|
|
// Uses intelligent prioritization: only set headers for the PRIMARY encryption type to avoid conflicts
|
|
|
|
|
func (s3a *S3ApiServer) addSSEHeadersToResponse(proxyResponse *http.Response, entry *filer_pb.Entry) {
|
|
|
|
|
if entry == nil || entry.Extended == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Determine the primary encryption type by examining chunks (most reliable)
|
|
|
|
|
primarySSEType := s3a.detectPrimarySSEType(entry)
|
|
|
|
|
|
|
|
|
|
// Only set headers for the PRIMARY encryption type
|
|
|
|
|
switch primarySSEType {
|
|
|
|
|
case s3_constants.SSETypeC:
|
|
|
|
|
// Add only SSE-C headers
|
|
|
|
|
if algorithmBytes, exists := entry.Extended[s3_constants.AmzServerSideEncryptionCustomerAlgorithm]; exists && len(algorithmBytes) > 0 {
|
|
|
|
|
proxyResponse.Header.Set(s3_constants.AmzServerSideEncryptionCustomerAlgorithm, string(algorithmBytes))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if keyMD5Bytes, exists := entry.Extended[s3_constants.AmzServerSideEncryptionCustomerKeyMD5]; exists && len(keyMD5Bytes) > 0 {
|
|
|
|
|
proxyResponse.Header.Set(s3_constants.AmzServerSideEncryptionCustomerKeyMD5, string(keyMD5Bytes))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ivBytes, exists := entry.Extended[s3_constants.SeaweedFSSSEIV]; exists && len(ivBytes) > 0 {
|
|
|
|
|
ivBase64 := base64.StdEncoding.EncodeToString(ivBytes)
|
|
|
|
|
proxyResponse.Header.Set(s3_constants.SeaweedFSSSEIVHeader, ivBase64)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case s3_constants.SSETypeKMS:
|
|
|
|
|
// Add only SSE-KMS headers
|
|
|
|
|
if sseAlgorithm, exists := entry.Extended[s3_constants.AmzServerSideEncryption]; exists && len(sseAlgorithm) > 0 {
|
|
|
|
|
proxyResponse.Header.Set(s3_constants.AmzServerSideEncryption, string(sseAlgorithm))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if kmsKeyID, exists := entry.Extended[s3_constants.AmzServerSideEncryptionAwsKmsKeyId]; exists && len(kmsKeyID) > 0 {
|
|
|
|
|
proxyResponse.Header.Set(s3_constants.AmzServerSideEncryptionAwsKmsKeyId, string(kmsKeyID))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case s3_constants.SSETypeS3:
|
|
|
|
|
// Add only SSE-S3 headers
|
|
|
|
|
proxyResponse.Header.Set(s3_constants.AmzServerSideEncryption, SSES3Algorithm)
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
// Unencrypted or unknown - don't set any SSE headers
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
glog.V(3).Infof("addSSEHeadersToResponse: processed %d extended metadata entries", len(entry.Extended))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// detectPrimarySSEType determines the primary SSE type by examining chunk metadata
|
|
|
|
|
func (s3a *S3ApiServer) detectPrimarySSEType(entry *filer_pb.Entry) string {
|
|
|
|
|
// Safety check: handle nil entry
|
|
|
|
|
@@ -3183,140 +2914,6 @@ func (r *SSERangeReader) Read(p []byte) (n int, err error) {
|
|
|
|
|
return n, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// createMultipartSSECDecryptedReader creates a decrypted reader for multipart SSE-C objects
|
|
|
|
|
// Each chunk has its own IV and encryption key from the original multipart parts
|
|
|
|
|
func (s3a *S3ApiServer) createMultipartSSECDecryptedReader(r *http.Request, proxyResponse *http.Response, entry *filer_pb.Entry) (io.Reader, error) {
|
|
|
|
|
ctx := r.Context()
|
|
|
|
|
|
|
|
|
|
// Parse SSE-C headers from the request for decryption key
|
|
|
|
|
customerKey, err := ParseSSECHeaders(r)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("invalid SSE-C headers for multipart decryption: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Entry is passed from caller to avoid redundant filer lookup
|
|
|
|
|
|
|
|
|
|
// Sort chunks by offset to ensure correct order
|
|
|
|
|
chunks := entry.GetChunks()
|
|
|
|
|
sort.Slice(chunks, func(i, j int) bool {
|
|
|
|
|
return chunks[i].GetOffset() < chunks[j].GetOffset()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Check for Range header to optimize chunk processing
|
|
|
|
|
var startOffset, endOffset int64 = 0, -1
|
|
|
|
|
rangeHeader := r.Header.Get("Range")
|
|
|
|
|
if rangeHeader != "" {
|
|
|
|
|
// Parse range header (e.g., "bytes=0-99")
|
|
|
|
|
if len(rangeHeader) > 6 && rangeHeader[:6] == "bytes=" {
|
|
|
|
|
rangeSpec := rangeHeader[6:]
|
|
|
|
|
parts := strings.Split(rangeSpec, "-")
|
|
|
|
|
if len(parts) == 2 {
|
|
|
|
|
if parts[0] != "" {
|
|
|
|
|
startOffset, _ = strconv.ParseInt(parts[0], 10, 64)
|
|
|
|
|
}
|
|
|
|
|
if parts[1] != "" {
|
|
|
|
|
endOffset, _ = strconv.ParseInt(parts[1], 10, 64)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filter chunks to only those needed for the range request
|
|
|
|
|
var neededChunks []*filer_pb.FileChunk
|
|
|
|
|
for _, chunk := range chunks {
|
|
|
|
|
chunkStart := chunk.GetOffset()
|
|
|
|
|
chunkEnd := chunkStart + int64(chunk.GetSize()) - 1
|
|
|
|
|
|
|
|
|
|
// Check if this chunk overlaps with the requested range
|
|
|
|
|
if endOffset == -1 {
|
|
|
|
|
// No end specified, take all chunks from startOffset
|
|
|
|
|
if chunkEnd >= startOffset {
|
|
|
|
|
neededChunks = append(neededChunks, chunk)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Specific range: check for overlap
|
|
|
|
|
if chunkStart <= endOffset && chunkEnd >= startOffset {
|
|
|
|
|
neededChunks = append(neededChunks, chunk)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create readers for only the needed chunks
|
|
|
|
|
var readers []io.Reader
|
|
|
|
|
|
|
|
|
|
for _, chunk := range neededChunks {
|
|
|
|
|
|
|
|
|
|
// Get this chunk's encrypted data
|
|
|
|
|
chunkReader, err := s3a.createEncryptedChunkReader(ctx, chunk)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to create chunk reader: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if chunk.GetSseType() == filer_pb.SSEType_SSE_C {
|
|
|
|
|
// For SSE-C chunks, extract the IV from the stored per-chunk metadata (unified approach)
|
|
|
|
|
if len(chunk.GetSseMetadata()) > 0 {
|
|
|
|
|
// Deserialize the SSE-C metadata stored in the unified metadata field
|
|
|
|
|
ssecMetadata, decErr := DeserializeSSECMetadata(chunk.GetSseMetadata())
|
|
|
|
|
if decErr != nil {
|
|
|
|
|
chunkReader.Close()
|
|
|
|
|
return nil, fmt.Errorf("failed to deserialize SSE-C metadata for chunk %s: %v", chunk.GetFileIdString(), decErr)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Decode the IV from the metadata
|
|
|
|
|
iv, ivErr := base64.StdEncoding.DecodeString(ssecMetadata.IV)
|
|
|
|
|
if ivErr != nil {
|
|
|
|
|
chunkReader.Close()
|
|
|
|
|
return nil, fmt.Errorf("failed to decode IV for SSE-C chunk %s: %v", chunk.GetFileIdString(), ivErr)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
partOffset := ssecMetadata.PartOffset
|
|
|
|
|
if partOffset < 0 {
|
|
|
|
|
chunkReader.Close()
|
|
|
|
|
return nil, fmt.Errorf("invalid SSE-C part offset %d for chunk %s", partOffset, chunk.GetFileIdString())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Use stored IV and advance CTR stream by PartOffset within the encrypted stream
|
|
|
|
|
decryptedReader, decErr := CreateSSECDecryptedReaderWithOffset(chunkReader, customerKey, iv, uint64(partOffset))
|
|
|
|
|
if decErr != nil {
|
|
|
|
|
chunkReader.Close()
|
|
|
|
|
return nil, fmt.Errorf("failed to create SSE-C decrypted reader for chunk %s: %v", chunk.GetFileIdString(), decErr)
|
|
|
|
|
}
|
|
|
|
|
readers = append(readers, decryptedReader)
|
|
|
|
|
} else {
|
|
|
|
|
chunkReader.Close()
|
|
|
|
|
return nil, fmt.Errorf("SSE-C chunk %s missing required metadata", chunk.GetFileIdString())
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Non-SSE-C chunk, use as-is
|
|
|
|
|
readers = append(readers, chunkReader)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
multiReader := NewMultipartSSEReader(readers)
|
|
|
|
|
|
|
|
|
|
// Apply range logic if a range was requested
|
|
|
|
|
if rangeHeader != "" && startOffset >= 0 {
|
|
|
|
|
if endOffset == -1 {
|
|
|
|
|
// Open-ended range (e.g., "bytes=100-")
|
|
|
|
|
return &SSERangeReader{
|
|
|
|
|
reader: multiReader,
|
|
|
|
|
offset: startOffset,
|
|
|
|
|
remaining: -1, // Read until EOF
|
|
|
|
|
}, nil
|
|
|
|
|
} else {
|
|
|
|
|
// Specific range (e.g., "bytes=0-99")
|
|
|
|
|
rangeLength := endOffset - startOffset + 1
|
|
|
|
|
return &SSERangeReader{
|
|
|
|
|
reader: multiReader,
|
|
|
|
|
offset: startOffset,
|
|
|
|
|
remaining: rangeLength,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return multiReader, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PartBoundaryInfo holds information about a part's chunk boundaries
|
|
|
|
|
type PartBoundaryInfo struct {
|
|
|
|
|
PartNumber int `json:"part"`
|
|
|
|
|
|