* fix: decrypt SSE-encrypted objects in S3 replication sink
* fix: add SSE decryption support to GCS, Azure, B2, Local sinks
* fix: return error instead of warning for SSE-C objects during replication
* fix: close readers after upload to prevent resource leaks
* fix: return error for unknown SSE types instead of passing through ciphertext
* refactor(repl_util): extract CloseReader/CloseMaybeDecryptedReader helpers
The io.Closer close-on-error and defer-close pattern was duplicated in
copyWithDecryption and the S3 sink. Extract exported helpers to keep a
single implementation and prevent future divergence.
* fix(repl_util): warn on mixed SSE types across chunks in detectSSEType
detectSSEType previously returned the SSE type of the first encrypted
chunk without inspecting the rest. If an entry somehow has chunks with
different SSE types, only the first type's decryption would be applied.
Now scans all chunks and logs a warning on mismatch.
* fix(repl_util): decrypt inline SSE objects during replication
Small SSE-encrypted objects stored in entry.Content were being copied
as ciphertext because:
1. detectSSEType only checked chunk metadata, but inline objects have
no chunks — now falls back to checking entry.Extended for SSE keys
2. Non-S3 sinks short-circuited on len(entry.Content)>0, bypassing
the decryption path — now call MaybeDecryptContent before writing
Adds MaybeDecryptContent helper for decrypting inline byte content.
* fix(repl_util): add KMS initialization for replication SSE decryption
SSE-KMS decryption was not wired up for filer.backup — the only
initialization was for SSE-S3 key manager. CreateSSEKMSDecryptedReader
requires a global KMS provider which is only loaded by the S3 API
auth-config path.
Add InitializeSSEForReplication helper that initializes both SSE-S3
(from filer KEK) and SSE-KMS (from Viper config [kms] section /
WEED_KMS_* env vars). Replace the SSE-S3-only init in filer_backup.go.
* fix(replicator): initialize SSE decryption for filer.replicate
The SSE decryption setup was only added to filer_backup.go, but the
notification-based replicator (filer.replicate) uses the same sinks
and was missing the required initialization. Add SSE init in
NewReplicator so filer.replicate can decrypt SSE objects.
* refactor(repl_util): fold entry param into CopyFromChunkViews
Remove the CopyFromChunkViewsWithEntry wrapper and add the entry
parameter directly to CopyFromChunkViews, since all callers already
pass it.
* fix(repl_util): guard SSE init with sync.Once, error on mixed SSE types
InitializeWithFiler overwrites the global superKey on every call.
Wrap InitializeSSEForReplication with sync.Once so repeated calls
(e.g. from NewReplicator) are safe.
detectSSEType now returns an error instead of logging a warning when
chunks have inconsistent SSE types, so replication aborts rather than
silently applying the wrong decryption to some chunks.
* fix(repl_util): allow SSE init retry, detect conflicting metadata, add tests
- Replace sync.Once with mutex+bool so transient failures (e.g. filer
unreachable) don't permanently prevent initialization. Only successful
init flips the flag; failed attempts allow retries.
- Remove v.IsSet("kms") guard that prevented env-only KMS configs
(WEED_KMS_*) from being detected. Always attempt KMS loading and let
LoadConfigurations handle "no config found".
- detectSSEType now checks for conflicting extended metadata keys
(e.g. both SeaweedFSSSES3Key and SeaweedFSSSEKMSKey present) and
returns an error instead of silently picking the first match.
- Add table-driven tests for detectSSEType, MaybeDecryptReader, and
MaybeDecryptContent covering plaintext, uniform SSE, mixed chunks,
inline SSE via extended metadata, conflicting metadata, and SSE-C.
* test(repl_util): add SSE-S3 and SSE-KMS integration tests
Add round-trip encryption/decryption tests:
- SSE-S3: encrypt with CreateSSES3EncryptedReader, decrypt with
CreateSSES3DecryptedReader, verify plaintext matches
- SSE-KMS: encrypt with AES-CTR, wire a mock KMSProvider via
SetGlobalKMSProvider, build serialized KMS metadata, verify
MaybeDecryptReader and MaybeDecryptContent produce correct plaintext
Fix existing tests to check io.ReadAll errors.
* test(repl_util): exercise full SSE-S3 path through MaybeDecryptReader
Replace direct CreateSSES3DecryptedReader calls with end-to-end tests
that go through MaybeDecryptReader → decryptSSES3 →
DeserializeSSES3Metadata → GetSSES3IV → CreateSSES3DecryptedReader.
Uses WEED_S3_SSE_KEK env var + a mock filer client to initialize the
global key manager with a test KEK, then SerializeSSES3Metadata to
build proper envelope-encrypted metadata. Cleanup restores the key
manager state.
* fix(localsink): write to temp file to prevent truncated replicas
The local sink truncated the destination file before writing content.
If decryption or chunk copy failed, the file was left empty/truncated,
destroying the previous replica.
Write to a temp file in the same directory and atomically rename on
success. On any error the temp file is cleaned up and the existing
replica is untouched.
---------
Co-authored-by: Chris Lu <chris.lu@gmail.com>
535 lines
14 KiB
Go
535 lines
14 KiB
Go
package repl_util
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/kms"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
func TestDetectSSEType(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
entry *filer_pb.Entry
|
|
wantType filer_pb.SSEType
|
|
wantError bool
|
|
}{
|
|
{
|
|
name: "no chunks no extended",
|
|
entry: &filer_pb.Entry{},
|
|
wantType: filer_pb.SSEType_NONE,
|
|
},
|
|
{
|
|
name: "plaintext chunks",
|
|
entry: &filer_pb.Entry{
|
|
Chunks: []*filer_pb.FileChunk{
|
|
{SseType: filer_pb.SSEType_NONE},
|
|
{SseType: filer_pb.SSEType_NONE},
|
|
},
|
|
},
|
|
wantType: filer_pb.SSEType_NONE,
|
|
},
|
|
{
|
|
name: "uniform SSE-S3 chunks",
|
|
entry: &filer_pb.Entry{
|
|
Chunks: []*filer_pb.FileChunk{
|
|
{SseType: filer_pb.SSEType_SSE_S3},
|
|
{SseType: filer_pb.SSEType_SSE_S3},
|
|
},
|
|
},
|
|
wantType: filer_pb.SSEType_SSE_S3,
|
|
},
|
|
{
|
|
name: "uniform SSE-KMS chunks",
|
|
entry: &filer_pb.Entry{
|
|
Chunks: []*filer_pb.FileChunk{
|
|
{SseType: filer_pb.SSEType_SSE_KMS},
|
|
},
|
|
},
|
|
wantType: filer_pb.SSEType_SSE_KMS,
|
|
},
|
|
{
|
|
name: "mixed chunk SSE types",
|
|
entry: &filer_pb.Entry{
|
|
Chunks: []*filer_pb.FileChunk{
|
|
{SseType: filer_pb.SSEType_SSE_S3},
|
|
{SseType: filer_pb.SSEType_SSE_KMS},
|
|
},
|
|
},
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "inline SSE-S3 via extended",
|
|
entry: &filer_pb.Entry{
|
|
Extended: map[string][]byte{
|
|
s3_constants.SeaweedFSSSES3Key: {0x01},
|
|
},
|
|
},
|
|
wantType: filer_pb.SSEType_SSE_S3,
|
|
},
|
|
{
|
|
name: "inline SSE-KMS via extended",
|
|
entry: &filer_pb.Entry{
|
|
Extended: map[string][]byte{
|
|
s3_constants.SeaweedFSSSEKMSKey: {0x01},
|
|
},
|
|
},
|
|
wantType: filer_pb.SSEType_SSE_KMS,
|
|
},
|
|
{
|
|
name: "inline SSE-C via extended",
|
|
entry: &filer_pb.Entry{
|
|
Extended: map[string][]byte{
|
|
s3_constants.SeaweedFSSSEIV: {0x01},
|
|
},
|
|
},
|
|
wantType: filer_pb.SSEType_SSE_C,
|
|
},
|
|
{
|
|
name: "conflicting extended metadata",
|
|
entry: &filer_pb.Entry{
|
|
Extended: map[string][]byte{
|
|
s3_constants.SeaweedFSSSES3Key: {0x01},
|
|
s3_constants.SeaweedFSSSEKMSKey: {0x02},
|
|
},
|
|
},
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "chunks take precedence over extended",
|
|
entry: &filer_pb.Entry{
|
|
Chunks: []*filer_pb.FileChunk{
|
|
{SseType: filer_pb.SSEType_SSE_S3},
|
|
},
|
|
Extended: map[string][]byte{
|
|
s3_constants.SeaweedFSSSEKMSKey: {0x01},
|
|
},
|
|
},
|
|
wantType: filer_pb.SSEType_SSE_S3,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := detectSSEType(tt.entry)
|
|
if tt.wantError {
|
|
if err == nil {
|
|
t.Fatalf("expected error, got type %v", got)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != tt.wantType {
|
|
t.Errorf("got %v, want %v", got, tt.wantType)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMaybeDecryptReader_Plaintext(t *testing.T) {
|
|
content := []byte("hello world")
|
|
entry := &filer_pb.Entry{}
|
|
reader := bytes.NewReader(content)
|
|
|
|
got, err := MaybeDecryptReader(reader, entry)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
result, err := io.ReadAll(got)
|
|
if err != nil {
|
|
t.Fatalf("ReadAll error: %v", err)
|
|
}
|
|
if !bytes.Equal(result, content) {
|
|
t.Errorf("got %q, want %q", result, content)
|
|
}
|
|
}
|
|
|
|
func TestMaybeDecryptReader_NilEntry(t *testing.T) {
|
|
content := []byte("hello")
|
|
reader := bytes.NewReader(content)
|
|
|
|
got, err := MaybeDecryptReader(reader, nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
result, err := io.ReadAll(got)
|
|
if err != nil {
|
|
t.Fatalf("ReadAll error: %v", err)
|
|
}
|
|
if !bytes.Equal(result, content) {
|
|
t.Errorf("got %q, want %q", result, content)
|
|
}
|
|
}
|
|
|
|
func TestMaybeDecryptReader_SSEC_Error(t *testing.T) {
|
|
entry := &filer_pb.Entry{
|
|
Extended: map[string][]byte{
|
|
s3_constants.SeaweedFSSSEIV: {0x01},
|
|
},
|
|
}
|
|
reader := bytes.NewReader([]byte("data"))
|
|
|
|
_, err := MaybeDecryptReader(reader, entry)
|
|
if err == nil {
|
|
t.Fatal("expected error for SSE-C")
|
|
}
|
|
}
|
|
|
|
func TestMaybeDecryptContent_Plaintext(t *testing.T) {
|
|
content := []byte("hello world")
|
|
entry := &filer_pb.Entry{}
|
|
|
|
got, err := MaybeDecryptContent(content, entry)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !bytes.Equal(got, content) {
|
|
t.Errorf("got %q, want %q", got, content)
|
|
}
|
|
}
|
|
|
|
func TestMaybeDecryptContent_NilEntry(t *testing.T) {
|
|
content := []byte("data")
|
|
got, err := MaybeDecryptContent(content, nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !bytes.Equal(got, content) {
|
|
t.Errorf("got %q, want %q", got, content)
|
|
}
|
|
}
|
|
|
|
func TestMaybeDecryptContent_Empty(t *testing.T) {
|
|
got, err := MaybeDecryptContent(nil, &filer_pb.Entry{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != nil {
|
|
t.Errorf("expected nil, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestMaybeDecryptContent_SSEC_Error(t *testing.T) {
|
|
entry := &filer_pb.Entry{
|
|
Extended: map[string][]byte{
|
|
s3_constants.SeaweedFSSSEIV: {0x01},
|
|
},
|
|
}
|
|
|
|
_, err := MaybeDecryptContent([]byte("data"), entry)
|
|
if err == nil {
|
|
t.Fatal("expected error for SSE-C")
|
|
}
|
|
}
|
|
|
|
func TestMaybeDecryptContent_MixedExtended_Error(t *testing.T) {
|
|
entry := &filer_pb.Entry{
|
|
Extended: map[string][]byte{
|
|
s3_constants.SeaweedFSSSES3Key: {0x01},
|
|
s3_constants.SeaweedFSSSEKMSKey: {0x02},
|
|
},
|
|
}
|
|
|
|
_, err := MaybeDecryptContent([]byte("data"), entry)
|
|
if err == nil {
|
|
t.Fatal("expected error for conflicting SSE metadata")
|
|
}
|
|
}
|
|
|
|
// --- SSE-S3 integration tests ---
|
|
// These tests exercise the full MaybeDecryptReader/MaybeDecryptContent path
|
|
// for SSE-S3: detectSSEType → decryptSSES3 → DeserializeSSES3Metadata →
|
|
// GetSSES3IV → CreateSSES3DecryptedReader. A test KEK is injected via
|
|
// WEED_S3_SSE_KEK env var and a mock filer client.
|
|
|
|
// testFilerClient is a minimal filer_pb.FilerClient mock that returns
|
|
// ErrNotFound for all lookups (no KEK on filer — we use env var instead).
|
|
type testFilerClient struct{}
|
|
|
|
func (c *testFilerClient) WithFilerClient(_ bool, fn func(filer_pb.SeaweedFilerClient) error) error {
|
|
return fmt.Errorf("%w", filer_pb.ErrNotFound)
|
|
}
|
|
func (c *testFilerClient) AdjustedUrl(loc *filer_pb.Location) string { return loc.Url }
|
|
func (c *testFilerClient) GetDataCenter() string { return "" }
|
|
|
|
// setupTestSSES3 initializes the global SSE-S3 key manager with a test KEK
|
|
// via the WEED_S3_SSE_KEK env var and returns the KEK bytes + cleanup func.
|
|
func setupTestSSES3(t *testing.T) (kek []byte, cleanup func()) {
|
|
t.Helper()
|
|
|
|
kek = make([]byte, 32)
|
|
if _, err := io.ReadFull(rand.Reader, kek); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Force Viper to pick up the new env var
|
|
os.Setenv("WEED_S3_SSE_KEK", hex.EncodeToString(kek))
|
|
|
|
// Reset Viper cache so it reads the new env var
|
|
v := util.GetViper()
|
|
v.AutomaticEnv()
|
|
|
|
// Re-initialize the global key manager with the KEK from env
|
|
km := s3api.GetSSES3KeyManager()
|
|
if err := km.InitializeWithFiler(&testFilerClient{}); err != nil {
|
|
os.Unsetenv("WEED_S3_SSE_KEK")
|
|
t.Fatalf("InitializeWithFiler: %v", err)
|
|
}
|
|
|
|
return kek, func() {
|
|
os.Unsetenv("WEED_S3_SSE_KEK")
|
|
// Re-initialize with no KEK to clear the super key
|
|
km.InitializeWithFiler(&testFilerClient{})
|
|
}
|
|
}
|
|
|
|
func TestMaybeDecryptReader_SSES3(t *testing.T) {
|
|
_, cleanup := setupTestSSES3(t)
|
|
defer cleanup()
|
|
|
|
plaintext := []byte("SSE-S3 encrypted content for testing round-trip decryption")
|
|
|
|
// Generate a DEK and encrypt
|
|
sseKey, err := s3api.GenerateSSES3Key()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
encReader, encIV, err := s3api.CreateSSES3EncryptedReader(bytes.NewReader(plaintext), sseKey)
|
|
if err != nil {
|
|
t.Fatalf("encrypt: %v", err)
|
|
}
|
|
ciphertext, err := io.ReadAll(encReader)
|
|
if err != nil {
|
|
t.Fatalf("read ciphertext: %v", err)
|
|
}
|
|
|
|
// Build serialized SSE-S3 metadata (uses the global key manager to
|
|
// envelope-encrypt the DEK with the test KEK)
|
|
sseKey.IV = encIV
|
|
metadataBytes, err := s3api.SerializeSSES3Metadata(sseKey)
|
|
if err != nil {
|
|
t.Fatalf("serialize metadata: %v", err)
|
|
}
|
|
|
|
entry := &filer_pb.Entry{
|
|
Extended: map[string][]byte{
|
|
s3_constants.SeaweedFSSSES3Key: metadataBytes,
|
|
},
|
|
}
|
|
|
|
// Test full path: MaybeDecryptReader → decryptSSES3 → DeserializeSSES3Metadata → CreateSSES3DecryptedReader
|
|
decrypted, err := MaybeDecryptReader(bytes.NewReader(ciphertext), entry)
|
|
if err != nil {
|
|
t.Fatalf("MaybeDecryptReader: %v", err)
|
|
}
|
|
result, err := io.ReadAll(decrypted)
|
|
if err != nil {
|
|
t.Fatalf("ReadAll: %v", err)
|
|
}
|
|
if !bytes.Equal(result, plaintext) {
|
|
t.Errorf("SSE-S3 round-trip failed: got %q, want %q", result, plaintext)
|
|
}
|
|
}
|
|
|
|
func TestMaybeDecryptContent_SSES3(t *testing.T) {
|
|
_, cleanup := setupTestSSES3(t)
|
|
defer cleanup()
|
|
|
|
plaintext := []byte("inline SSE-S3 content")
|
|
|
|
// Generate a DEK and encrypt inline content
|
|
sseKey, err := s3api.GenerateSSES3Key()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
encReader, encIV, err := s3api.CreateSSES3EncryptedReader(bytes.NewReader(plaintext), sseKey)
|
|
if err != nil {
|
|
t.Fatalf("encrypt: %v", err)
|
|
}
|
|
ciphertext, err := io.ReadAll(encReader)
|
|
if err != nil {
|
|
t.Fatalf("read ciphertext: %v", err)
|
|
}
|
|
|
|
sseKey.IV = encIV
|
|
metadataBytes, err := s3api.SerializeSSES3Metadata(sseKey)
|
|
if err != nil {
|
|
t.Fatalf("serialize metadata: %v", err)
|
|
}
|
|
|
|
entry := &filer_pb.Entry{
|
|
Extended: map[string][]byte{
|
|
s3_constants.SeaweedFSSSES3Key: metadataBytes,
|
|
},
|
|
}
|
|
|
|
// Test full path: MaybeDecryptContent → MaybeDecryptReader → decryptSSES3
|
|
result, err := MaybeDecryptContent(ciphertext, entry)
|
|
if err != nil {
|
|
t.Fatalf("MaybeDecryptContent: %v", err)
|
|
}
|
|
if !bytes.Equal(result, plaintext) {
|
|
t.Errorf("SSE-S3 round-trip failed: got %q, want %q", result, plaintext)
|
|
}
|
|
}
|
|
|
|
// --- SSE-KMS integration tests ---
|
|
|
|
// testKMSProvider is a minimal KMSProvider mock for testing.
|
|
type testKMSProvider struct {
|
|
keyID string
|
|
plaintext []byte // the DEK plaintext returned by Decrypt
|
|
}
|
|
|
|
func (p *testKMSProvider) GenerateDataKey(_ context.Context, _ *kms.GenerateDataKeyRequest) (*kms.GenerateDataKeyResponse, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (p *testKMSProvider) Decrypt(_ context.Context, _ *kms.DecryptRequest) (*kms.DecryptResponse, error) {
|
|
return &kms.DecryptResponse{
|
|
KeyID: p.keyID,
|
|
Plaintext: append([]byte(nil), p.plaintext...), // return a copy
|
|
}, nil
|
|
}
|
|
|
|
func (p *testKMSProvider) DescribeKey(_ context.Context, _ *kms.DescribeKeyRequest) (*kms.DescribeKeyResponse, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (p *testKMSProvider) GetKeyID(_ context.Context, keyIdentifier string) (string, error) {
|
|
return p.keyID, nil
|
|
}
|
|
|
|
func (p *testKMSProvider) Close() error { return nil }
|
|
|
|
func TestMaybeDecryptReader_SSEKMS(t *testing.T) {
|
|
plaintext := []byte("SSE-KMS encrypted content for testing")
|
|
|
|
// Generate a random DEK and IV
|
|
dek := make([]byte, 32)
|
|
iv := make([]byte, aes.BlockSize)
|
|
if _, err := io.ReadFull(rand.Reader, dek); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Encrypt with AES-CTR (same cipher mode as SSE-KMS)
|
|
block, err := aes.NewCipher(dek)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ciphertext := make([]byte, len(plaintext))
|
|
cipher.NewCTR(block, iv).XORKeyStream(ciphertext, plaintext)
|
|
|
|
// Set up a mock KMS provider that returns our DEK
|
|
keyID := "test-kms-key-1"
|
|
encryptedDEK := []byte("fake-encrypted-dek") // mock doesn't validate
|
|
kms.SetGlobalKMSProvider(&testKMSProvider{
|
|
keyID: keyID,
|
|
plaintext: dek,
|
|
})
|
|
defer kms.SetGlobalKMSProvider(nil)
|
|
|
|
// Build serialized KMS metadata
|
|
kmsMetadata := s3api.SSEKMSMetadata{
|
|
Algorithm: s3_constants.SSEAlgorithmKMS,
|
|
KeyID: keyID,
|
|
EncryptedDataKey: base64.StdEncoding.EncodeToString(encryptedDEK),
|
|
IV: base64.StdEncoding.EncodeToString(iv),
|
|
}
|
|
metadataBytes, err := json.Marshal(kmsMetadata)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
entry := &filer_pb.Entry{
|
|
Extended: map[string][]byte{
|
|
s3_constants.SeaweedFSSSEKMSKey: metadataBytes,
|
|
},
|
|
}
|
|
|
|
// Test MaybeDecryptReader
|
|
reader := bytes.NewReader(ciphertext)
|
|
decrypted, err := MaybeDecryptReader(reader, entry)
|
|
if err != nil {
|
|
t.Fatalf("MaybeDecryptReader: %v", err)
|
|
}
|
|
result, err := io.ReadAll(decrypted)
|
|
if err != nil {
|
|
t.Fatalf("ReadAll: %v", err)
|
|
}
|
|
if !bytes.Equal(result, plaintext) {
|
|
t.Errorf("SSE-KMS round-trip failed: got %q, want %q", result, plaintext)
|
|
}
|
|
}
|
|
|
|
func TestMaybeDecryptContent_SSEKMS(t *testing.T) {
|
|
plaintext := []byte("inline SSE-KMS content")
|
|
|
|
dek := make([]byte, 32)
|
|
iv := make([]byte, aes.BlockSize)
|
|
if _, err := io.ReadFull(rand.Reader, dek); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
block, err := aes.NewCipher(dek)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ciphertext := make([]byte, len(plaintext))
|
|
cipher.NewCTR(block, iv).XORKeyStream(ciphertext, plaintext)
|
|
|
|
keyID := "test-kms-key-2"
|
|
kms.SetGlobalKMSProvider(&testKMSProvider{
|
|
keyID: keyID,
|
|
plaintext: dek,
|
|
})
|
|
defer kms.SetGlobalKMSProvider(nil)
|
|
|
|
kmsMetadata := s3api.SSEKMSMetadata{
|
|
Algorithm: s3_constants.SSEAlgorithmKMS,
|
|
KeyID: keyID,
|
|
EncryptedDataKey: base64.StdEncoding.EncodeToString([]byte("fake-encrypted-dek")),
|
|
IV: base64.StdEncoding.EncodeToString(iv),
|
|
}
|
|
metadataBytes, err := json.Marshal(kmsMetadata)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
entry := &filer_pb.Entry{
|
|
Extended: map[string][]byte{
|
|
s3_constants.SeaweedFSSSEKMSKey: metadataBytes,
|
|
},
|
|
}
|
|
|
|
result, err := MaybeDecryptContent(ciphertext, entry)
|
|
if err != nil {
|
|
t.Fatalf("MaybeDecryptContent: %v", err)
|
|
}
|
|
if !bytes.Equal(result, plaintext) {
|
|
t.Errorf("SSE-KMS round-trip failed: got %q, want %q", result, plaintext)
|
|
}
|
|
}
|