diff options
Diffstat (limited to 'weed/s3api/s3api_key_rotation.go')
| -rw-r--r-- | weed/s3api/s3api_key_rotation.go | 291 |
1 files changed, 291 insertions, 0 deletions
diff --git a/weed/s3api/s3api_key_rotation.go b/weed/s3api/s3api_key_rotation.go new file mode 100644 index 000000000..682f47807 --- /dev/null +++ b/weed/s3api/s3api_key_rotation.go @@ -0,0 +1,291 @@ +package s3api + +import ( + "bytes" + "crypto/rand" + "fmt" + "io" + "net/http" + + "github.com/seaweedfs/seaweedfs/weed/glog" + "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" +) + +// rotateSSECKey handles SSE-C key rotation for same-object copies +func (s3a *S3ApiServer) rotateSSECKey(entry *filer_pb.Entry, r *http.Request) ([]*filer_pb.FileChunk, error) { + // Parse source and destination SSE-C keys + sourceKey, err := ParseSSECCopySourceHeaders(r) + if err != nil { + return nil, fmt.Errorf("parse SSE-C copy source headers: %w", err) + } + + destKey, err := ParseSSECHeaders(r) + if err != nil { + return nil, fmt.Errorf("parse SSE-C destination headers: %w", err) + } + + // Validate that we have both keys + if sourceKey == nil { + return nil, fmt.Errorf("source SSE-C key required for key rotation") + } + + if destKey == nil { + return nil, fmt.Errorf("destination SSE-C key required for key rotation") + } + + // Check if keys are actually different + if sourceKey.KeyMD5 == destKey.KeyMD5 { + glog.V(2).Infof("SSE-C key rotation: keys are identical, using direct copy") + return entry.GetChunks(), nil + } + + glog.V(2).Infof("SSE-C key rotation: rotating from key %s to key %s", + sourceKey.KeyMD5[:8], destKey.KeyMD5[:8]) + + // For SSE-C key rotation, we need to re-encrypt all chunks + // This cannot be a metadata-only operation because the encryption key changes + return s3a.rotateSSECChunks(entry, sourceKey, destKey) +} + +// rotateSSEKMSKey handles SSE-KMS key rotation for same-object copies +func (s3a *S3ApiServer) rotateSSEKMSKey(entry *filer_pb.Entry, r *http.Request) ([]*filer_pb.FileChunk, error) { + // Get source and destination key IDs + srcKeyID, srcEncrypted := GetSourceSSEKMSInfo(entry.Extended) + if !srcEncrypted { + return nil, fmt.Errorf("source object is not SSE-KMS encrypted") + } + + dstKeyID := r.Header.Get(s3_constants.AmzServerSideEncryptionAwsKmsKeyId) + if dstKeyID == "" { + // Use default key if not specified + dstKeyID = "default" + } + + // Check if keys are actually different + if srcKeyID == dstKeyID { + glog.V(2).Infof("SSE-KMS key rotation: keys are identical, using direct copy") + return entry.GetChunks(), nil + } + + glog.V(2).Infof("SSE-KMS key rotation: rotating from key %s to key %s", srcKeyID, dstKeyID) + + // For SSE-KMS, we can potentially do metadata-only rotation + // if the KMS service supports key aliasing and the data encryption key can be re-wrapped + if s3a.canDoMetadataOnlyKMSRotation(srcKeyID, dstKeyID) { + return s3a.rotateSSEKMSMetadataOnly(entry, srcKeyID, dstKeyID) + } + + // Fallback to full re-encryption + return s3a.rotateSSEKMSChunks(entry, srcKeyID, dstKeyID, r) +} + +// canDoMetadataOnlyKMSRotation determines if KMS key rotation can be done metadata-only +func (s3a *S3ApiServer) canDoMetadataOnlyKMSRotation(srcKeyID, dstKeyID string) bool { + // For now, we'll be conservative and always re-encrypt + // In a full implementation, this would check if: + // 1. Both keys are in the same KMS instance + // 2. The KMS supports key re-wrapping + // 3. The user has permissions for both keys + return false +} + +// rotateSSEKMSMetadataOnly performs metadata-only SSE-KMS key rotation +func (s3a *S3ApiServer) rotateSSEKMSMetadataOnly(entry *filer_pb.Entry, srcKeyID, dstKeyID string) ([]*filer_pb.FileChunk, error) { + // This would re-wrap the data encryption key with the new KMS key + // For now, return an error since we don't support this yet + return nil, fmt.Errorf("metadata-only KMS key rotation not yet implemented") +} + +// rotateSSECChunks re-encrypts all chunks with new SSE-C key +func (s3a *S3ApiServer) rotateSSECChunks(entry *filer_pb.Entry, sourceKey, destKey *SSECustomerKey) ([]*filer_pb.FileChunk, error) { + // Get IV from entry metadata + iv, err := GetIVFromMetadata(entry.Extended) + if err != nil { + return nil, fmt.Errorf("get IV from metadata: %w", err) + } + + var rotatedChunks []*filer_pb.FileChunk + + for _, chunk := range entry.GetChunks() { + rotatedChunk, err := s3a.rotateSSECChunk(chunk, sourceKey, destKey, iv) + if err != nil { + return nil, fmt.Errorf("rotate SSE-C chunk: %w", err) + } + rotatedChunks = append(rotatedChunks, rotatedChunk) + } + + // Generate new IV for the destination and store it in entry metadata + newIV := make([]byte, AESBlockSize) + if _, err := io.ReadFull(rand.Reader, newIV); err != nil { + return nil, fmt.Errorf("generate new IV: %w", err) + } + + // Update entry metadata with new IV and SSE-C headers + if entry.Extended == nil { + entry.Extended = make(map[string][]byte) + } + StoreIVInMetadata(entry.Extended, newIV) + entry.Extended[s3_constants.AmzServerSideEncryptionCustomerAlgorithm] = []byte("AES256") + entry.Extended[s3_constants.AmzServerSideEncryptionCustomerKeyMD5] = []byte(destKey.KeyMD5) + + return rotatedChunks, nil +} + +// rotateSSEKMSChunks re-encrypts all chunks with new SSE-KMS key +func (s3a *S3ApiServer) rotateSSEKMSChunks(entry *filer_pb.Entry, srcKeyID, dstKeyID string, r *http.Request) ([]*filer_pb.FileChunk, error) { + var rotatedChunks []*filer_pb.FileChunk + + // Parse encryption context and bucket key settings + _, encryptionContext, bucketKeyEnabled, err := ParseSSEKMSCopyHeaders(r) + if err != nil { + return nil, fmt.Errorf("parse SSE-KMS copy headers: %w", err) + } + + for _, chunk := range entry.GetChunks() { + rotatedChunk, err := s3a.rotateSSEKMSChunk(chunk, srcKeyID, dstKeyID, encryptionContext, bucketKeyEnabled) + if err != nil { + return nil, fmt.Errorf("rotate SSE-KMS chunk: %w", err) + } + rotatedChunks = append(rotatedChunks, rotatedChunk) + } + + return rotatedChunks, nil +} + +// rotateSSECChunk rotates a single SSE-C encrypted chunk +func (s3a *S3ApiServer) rotateSSECChunk(chunk *filer_pb.FileChunk, sourceKey, destKey *SSECustomerKey, iv []byte) (*filer_pb.FileChunk, error) { + // Create new chunk with same properties + newChunk := &filer_pb.FileChunk{ + Offset: chunk.Offset, + Size: chunk.Size, + ModifiedTsNs: chunk.ModifiedTsNs, + ETag: chunk.ETag, + } + + // Assign new volume for the rotated chunk + assignResult, err := s3a.assignNewVolume("") + if err != nil { + return nil, fmt.Errorf("assign new volume: %w", err) + } + + // Set file ID on new chunk + if err := s3a.setChunkFileId(newChunk, assignResult); err != nil { + return nil, err + } + + // Get source chunk data + srcUrl, err := s3a.lookupVolumeUrl(chunk.GetFileIdString()) + if err != nil { + return nil, fmt.Errorf("lookup source volume: %w", err) + } + + // Download encrypted data + encryptedData, err := s3a.downloadChunkData(srcUrl, 0, int64(chunk.Size)) + if err != nil { + return nil, fmt.Errorf("download chunk data: %w", err) + } + + // Decrypt with source key using provided IV + decryptedReader, err := CreateSSECDecryptedReader(bytes.NewReader(encryptedData), sourceKey, iv) + if err != nil { + return nil, fmt.Errorf("create decrypted reader: %w", err) + } + + decryptedData, err := io.ReadAll(decryptedReader) + if err != nil { + return nil, fmt.Errorf("decrypt data: %w", err) + } + + // Re-encrypt with destination key + encryptedReader, _, err := CreateSSECEncryptedReader(bytes.NewReader(decryptedData), destKey) + if err != nil { + return nil, fmt.Errorf("create encrypted reader: %w", err) + } + + // Note: IV will be handled at the entry level by the calling function + + reencryptedData, err := io.ReadAll(encryptedReader) + if err != nil { + return nil, fmt.Errorf("re-encrypt data: %w", err) + } + + // Update chunk size to include new IV + newChunk.Size = uint64(len(reencryptedData)) + + // Upload re-encrypted data + if err := s3a.uploadChunkData(reencryptedData, assignResult); err != nil { + return nil, fmt.Errorf("upload re-encrypted data: %w", err) + } + + return newChunk, nil +} + +// rotateSSEKMSChunk rotates a single SSE-KMS encrypted chunk +func (s3a *S3ApiServer) rotateSSEKMSChunk(chunk *filer_pb.FileChunk, srcKeyID, dstKeyID string, encryptionContext map[string]string, bucketKeyEnabled bool) (*filer_pb.FileChunk, error) { + // Create new chunk with same properties + newChunk := &filer_pb.FileChunk{ + Offset: chunk.Offset, + Size: chunk.Size, + ModifiedTsNs: chunk.ModifiedTsNs, + ETag: chunk.ETag, + } + + // Assign new volume for the rotated chunk + assignResult, err := s3a.assignNewVolume("") + if err != nil { + return nil, fmt.Errorf("assign new volume: %w", err) + } + + // Set file ID on new chunk + if err := s3a.setChunkFileId(newChunk, assignResult); err != nil { + return nil, err + } + + // Get source chunk data + srcUrl, err := s3a.lookupVolumeUrl(chunk.GetFileIdString()) + if err != nil { + return nil, fmt.Errorf("lookup source volume: %w", err) + } + + // Download data (this would be encrypted with the old KMS key) + chunkData, err := s3a.downloadChunkData(srcUrl, 0, int64(chunk.Size)) + if err != nil { + return nil, fmt.Errorf("download chunk data: %w", err) + } + + // For now, we'll just re-upload the data as-is + // In a full implementation, this would: + // 1. Decrypt with old KMS key + // 2. Re-encrypt with new KMS key + // 3. Update metadata accordingly + + // Upload data with new key (placeholder implementation) + if err := s3a.uploadChunkData(chunkData, assignResult); err != nil { + return nil, fmt.Errorf("upload rotated data: %w", err) + } + + return newChunk, nil +} + +// IsSameObjectCopy determines if this is a same-object copy operation +func IsSameObjectCopy(r *http.Request, srcBucket, srcObject, dstBucket, dstObject string) bool { + return srcBucket == dstBucket && srcObject == dstObject +} + +// NeedsKeyRotation determines if the copy operation requires key rotation +func NeedsKeyRotation(entry *filer_pb.Entry, r *http.Request) bool { + // Check for SSE-C key rotation + if IsSSECEncrypted(entry.Extended) && IsSSECRequest(r) { + return true // Assume different keys for safety + } + + // Check for SSE-KMS key rotation + if IsSSEKMSEncrypted(entry.Extended) && IsSSEKMSRequest(r) { + srcKeyID, _ := GetSourceSSEKMSInfo(entry.Extended) + dstKeyID := r.Header.Get(s3_constants.AmzServerSideEncryptionAwsKmsKeyId) + return srcKeyID != dstKeyID + } + + return false +} |
