Skip to content

Commit 2c2a4bc

Browse files
cuppettCopilot
andcommitted
feat(objectstore): add DSSE-KMS support and unify S3 sse configuration
Follow-up to #57623 (SSE-KMS). Adds a single `sse` objectstore argument (none/sse-s3/sse-c/sse-kms/sse-kms-dsse) so DSSE-KMS, encryption context, and S3 Bucket Keys can be configured without ambiguous or silently conflicting combinations of booleans. `sse_c_key` and `sse_kms_enabled` remain as deprecated aliases so existing configs keep working unchanged. Conflicting sse* arguments resolve deterministically instead of throwing: sse_c_key + sse_kms_enabled both set falls back to SSE-C, matching the behaviour released in Nextcloud 34, and an explicit `sse` argument always overrides the deprecated keys, including `'sse' => ''` as an explicit opt-out. Every resolution is logged once via getConnection(), with a warning on any SSE-C usage. sse_kms_bucket_key with sse-kms-dsse is a warning rather than an exception: S3 accepts the request and simply does not apply the bucket key. Encryption headers are now split by request type. Write requests keep getServerSideEncryptionParameters(); requests that don't create an object (GetObject, HeadObject, ListParts, UploadPart, CompleteMultipartUpload, the source side of a copy) use getReadServerSideEncryptionParameters(), which only carries the SSE-C customer key and is gated on the resolved mode rather than the raw `sse_c_key` argument. Adds S3ServerSideEncryptionTest for parsing, precedence and logging, and S3EncryptionModesLiveTest, an opt-in (NC_TEST_S3_KMS_KEY_ID) mode matrix and cross-mode read suite against a real AWS bucket and KMS key. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Assisted-by: ClaudeCode:claude-opus-5 Assisted-by: ClaudeCode:claude-sonnet-5-5 Signed-off-by: Stephen Cuppett <steve@cuppett.com>
1 parent 366041b commit 2c2a4bc

13 files changed

Lines changed: 1079 additions & 67 deletions

File tree

‎apps/files_external/lib/Lib/Storage/AmazonS3.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ private function headObject(string $key): array|false {
122122
$this->objectCache[$key] = $this->getConnection()->headObject([
123123
'Bucket' => $this->bucket,
124124
'Key' => $key
125-
] + $this->getServerSideEncryptionParameters())->toArray();
125+
] + $this->getReadServerSideEncryptionParameters())->toArray();
126126
} catch (S3Exception $e) {
127127
if ($e->getStatusCode() >= 500) {
128128
throw $e;

‎config/config.sample.php‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2179,6 +2179,33 @@
21792179
// using Amazon S3 (or any other implementation that supports it) we recommend enabling it by using "when_supported".
21802180
'request_checksum_calculation' => 'when_required',
21812181
'response_checksum_validation' => 'when_required',
2182+
// optional: Server-side encryption of objects at rest.
2183+
// Valid values are:
2184+
// - '' (default): no server-side encryption requested by Nextcloud
2185+
// - 'sse-s3': S3-managed AES256 key
2186+
// - 'sse-c': customer-provided key, see 'sse_c_key' below (deprecated, see below)
2187+
// - 'sse-kms': single layer of AWS KMS-managed encryption
2188+
// - 'sse-kms-dsse': dual layer of AWS KMS-managed encryption (DSSE-KMS), higher cost
2189+
// and latency than 'sse-kms' but required by some compliance standards
2190+
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html
2191+
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingDSSEncryption.html
2192+
'sse' => '',
2193+
// optional: customer-provided key for 'sse' => 'sse-c', base64 encoded.
2194+
// Deprecated since Nextcloud 34: Amazon S3 disabled SSE-C by default for new
2195+
// buckets in April 2026. Use 'sse' => 'sse-kms' or 'sse-kms-dsse' instead.
2196+
'sse_c_key' => '',
2197+
// optional: ARN of the AWS KMS key to use for 'sse-kms' / 'sse-kms-dsse'.
2198+
// If omitted, the bucket's default KMS key (or the AWS-managed 'aws/s3' key) is used.
2199+
// The key must be a symmetric key in the same AWS region as the bucket.
2200+
'sse_kms_key_id' => '',
2201+
// optional: 'sse-kms' / 'sse-kms-dsse' only. Additional AWS KMS encryption context,
2202+
// e.g. to scope key usage to a tenant via a matching kms:EncryptionContext condition
2203+
// on the key policy. This is stored alongside the object and visible in AWS CloudTrail,
2204+
// so it must not contain secrets.
2205+
'sse_kms_encryption_context' => [],
2206+
// optional: 'sse-kms' only, not supported for 'sse-kms-dsse'. Enables S3 Bucket Keys,
2207+
// which can reduce AWS KMS request costs by up to 99% for SSE-KMS.
2208+
'sse_kms_bucket_key' => false,
21822209
],
21832210
],
21842211

‎lib/composer/composer/autoload_classmap.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1965,6 +1965,7 @@
19651965
'OC\\Files\\ObjectStore\\S3' => $baseDir . '/lib/private/Files/ObjectStore/S3.php',
19661966
'OC\\Files\\ObjectStore\\S3ConfigTrait' => $baseDir . '/lib/private/Files/ObjectStore/S3ConfigTrait.php',
19671967
'OC\\Files\\ObjectStore\\S3ConnectionTrait' => $baseDir . '/lib/private/Files/ObjectStore/S3ConnectionTrait.php',
1968+
'OC\\Files\\ObjectStore\\S3EncryptionMode' => $baseDir . '/lib/private/Files/ObjectStore/S3EncryptionMode.php',
19681969
'OC\\Files\\ObjectStore\\S3ObjectTrait' => $baseDir . '/lib/private/Files/ObjectStore/S3ObjectTrait.php',
19691970
'OC\\Files\\ObjectStore\\S3Signature' => $baseDir . '/lib/private/Files/ObjectStore/S3Signature.php',
19701971
'OC\\Files\\ObjectStore\\StorageObjectStore' => $baseDir . '/lib/private/Files/ObjectStore/StorageObjectStore.php',

‎lib/composer/composer/autoload_static.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2006,6 +2006,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
20062006
'OC\\Files\\ObjectStore\\S3' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/S3.php',
20072007
'OC\\Files\\ObjectStore\\S3ConfigTrait' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/S3ConfigTrait.php',
20082008
'OC\\Files\\ObjectStore\\S3ConnectionTrait' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/S3ConnectionTrait.php',
2009+
'OC\\Files\\ObjectStore\\S3EncryptionMode' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/S3EncryptionMode.php',
20092010
'OC\\Files\\ObjectStore\\S3ObjectTrait' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/S3ObjectTrait.php',
20102011
'OC\\Files\\ObjectStore\\S3Signature' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/S3Signature.php',
20112012
'OC\\Files\\ObjectStore\\StorageObjectStore' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/StorageObjectStore.php',

‎lib/private/Files/ObjectStore/S3.php‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public function uploadMultipartPart(string $urn, string $uploadId, int $partId,
5353
'ContentLength' => $size,
5454
'PartNumber' => $partId,
5555
'UploadId' => $uploadId,
56-
] + $this->getServerSideEncryptionParameters());
56+
] + $this->getReadServerSideEncryptionParameters());
5757
}
5858

5959
#[\Override]
@@ -69,7 +69,7 @@ public function getMultipartUploads(string $urn, string $uploadId): array {
6969
'UploadId' => $uploadId,
7070
'MaxParts' => 1000,
7171
'PartNumberMarker' => $partNumberMarker,
72-
] + $this->getServerSideEncryptionParameters());
72+
] + $this->getReadServerSideEncryptionParameters());
7373
$parts = array_merge($parts, $result->get('Parts') ?? []);
7474
$isTruncated = $result->get('IsTruncated');
7575
$partNumberMarker = $result->get('NextPartNumberMarker');
@@ -85,11 +85,11 @@ public function completeMultipartUpload(string $urn, string $uploadId, array $re
8585
'Key' => $urn,
8686
'UploadId' => $uploadId,
8787
'MultipartUpload' => ['Parts' => $result],
88-
] + $this->getServerSideEncryptionParameters());
88+
] + $this->getReadServerSideEncryptionParameters());
8989
$stat = $this->getConnection()->headObject([
9090
'Bucket' => $this->bucket,
9191
'Key' => $urn,
92-
] + $this->getServerSideEncryptionParameters());
92+
] + $this->getReadServerSideEncryptionParameters());
9393
return (int)$stat->get('ContentLength');
9494
}
9595

@@ -120,7 +120,7 @@ public function getObjectMetaData(string $urn): array {
120120
$object = $this->getConnection()->headObject([
121121
'Bucket' => $this->bucket,
122122
'Key' => $urn
123-
] + $this->getServerSideEncryptionParameters())->toArray();
123+
] + $this->getReadServerSideEncryptionParameters())->toArray();
124124
return [
125125
'mtime' => $object['LastModified'],
126126
'etag' => trim($object['ETag'], '"'),
@@ -133,7 +133,7 @@ public function listObjects(string $prefix = ''): \Iterator {
133133
$results = $this->getConnection()->getPaginator('ListObjectsV2', [
134134
'Bucket' => $this->bucket,
135135
'Prefix' => $prefix,
136-
] + $this->getServerSideEncryptionParameters());
136+
]);
137137

138138
foreach ($results as $result) {
139139
if (is_array($result['Contents'])) {

‎lib/private/Files/ObjectStore/S3ConfigTrait.php‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,16 @@ trait S3ConfigTrait {
4141
private bool $useMultipartCopy = true;
4242

4343
protected int $retriesMaxAttempts;
44+
45+
protected S3EncryptionMode $sseMode = S3EncryptionMode::None;
46+
47+
protected ?string $sseKmsKeyId = null;
48+
49+
/** Already base64-encoded JSON, ready to hand to the AWS SDK */
50+
protected ?string $sseKmsEncryptionContext = null;
51+
52+
protected bool $sseKmsBucketKey = false;
53+
54+
/** @var string[] Non-fatal `sse*` configuration issues found by parseEncryptionParams(), logged by logEncryptionWarnings() */
55+
protected array $encryptionWarnings = [];
4456
}

‎lib/private/Files/ObjectStore/S3ConnectionTrait.php‎

Lines changed: 167 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ trait S3ConnectionTrait {
3838
private ?ICache $existingBucketsCache = null;
3939
private bool $usePresignedUrl = false;
4040

41+
/** @var array<string, true> Deduplicates encryption warnings across instances/calls, keyed by message */
42+
private static array $loggedEncryptionWarnings = [];
43+
4144
protected function parseParams($params) {
4245
if (empty($params['bucket'])) {
4346
throw new \Exception('Bucket has to be configured.');
@@ -62,6 +65,7 @@ protected function parseParams($params) {
6265
$this->copySizeLimit = $params['copySizeLimit'] ?? 5242880000;
6366
$this->useMultipartCopy = (bool)($params['useMultipartCopy'] ?? true);
6467
$this->retriesMaxAttempts = $params['retriesMaxAttempts'] ?? 5;
68+
$this->parseEncryptionParams($params);
6569
$params['region'] = empty($params['region']) ? 'eu-west-1' : $params['region'];
6670
$params['hostname'] = empty($params['hostname']) ? 's3.' . $params['region'] . '.amazonaws.com' : $params['hostname'];
6771
$params['key'] = $params['key'] ?? '';
@@ -98,6 +102,8 @@ public function getConnection() {
98102
return $this->connection;
99103
}
100104

105+
$this->logEncryptionWarnings();
106+
101107
if ($this->existingBucketsCache === null) {
102108
$this->existingBucketsCache = Server::get(ICacheFactory::class)
103109
->createLocal('s3-bucket-exists-cache');
@@ -337,77 +343,194 @@ protected function getSSECParameters(bool $copy = false): array {
337343
}
338344

339345
/**
340-
* Get SSE-KMS key ID from configuration
341-
* @return string|null KMS key ARN/ID or null for bucket default key
346+
* Resolve and validate the `sse*` objectstore arguments into `$this->sseMode` and friends.
347+
*
348+
* `sse_c_key` and `sse_kms_enabled` are kept as deprecated aliases for `sse` set to
349+
* `sse-c` / `sse-kms` respectively, so existing configurations keep working unchanged. An
350+
* explicit `sse` always wins over both. If neither is set and both deprecated keys are,
351+
* `sse_c_key` (SSE-C) takes precedence, matching the behaviour Nextcloud 34 shipped with.
352+
*
353+
* Configuration that can be deterministically resolved never blocks boot: it is instead
354+
* recorded in `$this->encryptionWarnings` for `logEncryptionWarnings()` to log once a
355+
* connection is actually made. Only configuration with no reasonable resolution throws.
356+
*
357+
* @throws \Exception on `sse*` configuration with no reasonable resolution
342358
*/
343-
protected function getSSEKMSKeyId(): ?string {
344-
if (isset($this->params['sse_kms_key_id']) && !empty($this->params['sse_kms_key_id'])) {
345-
return $this->params['sse_kms_key_id'];
359+
private function parseEncryptionParams(array $params): void {
360+
$this->encryptionWarnings = [];
361+
362+
$explicitMode = null;
363+
$hasExplicitSse = array_key_exists('sse', $params);
364+
if ($hasExplicitSse) {
365+
// 'sse' => '' is an explicit opt-out and also wins over the deprecated aliases
366+
$explicitMode = S3EncryptionMode::tryFrom((string)$params['sse']);
367+
if ($explicitMode === null) {
368+
$valid = implode(', ', array_map(
369+
static fn (S3EncryptionMode $mode): string => "'{$mode->value}'",
370+
array_filter(S3EncryptionMode::cases(), static fn (S3EncryptionMode $mode): bool => $mode !== S3EncryptionMode::None)
371+
));
372+
throw new \Exception("Invalid 'sse' objectstore argument '{$params['sse']}', expected one of: {$valid}.");
373+
}
346374
}
347-
return null;
375+
376+
$hasLegacySseC = !empty($params['sse_c_key']);
377+
$hasLegacySseKms = !empty($params['sse_kms_enabled']);
378+
379+
if ($hasExplicitSse) {
380+
$mode = $explicitMode;
381+
if ($hasLegacySseC && $mode !== S3EncryptionMode::SseC) {
382+
$this->encryptionWarnings[] = "Ignoring the 'sse_c_key' objectstore argument: 'sse' is explicitly set to '{$mode->value}'.";
383+
}
384+
if ($hasLegacySseKms && !$mode->isKms()) {
385+
$this->encryptionWarnings[] = "Ignoring the 'sse_kms_enabled' objectstore argument: 'sse' is explicitly set to '{$mode->value}'.";
386+
}
387+
} elseif ($hasLegacySseC || $hasLegacySseKms) {
388+
if ($hasLegacySseC && $hasLegacySseKms) {
389+
// This is the Nextcloud 34 behaviour: SSE-C silently took precedence over
390+
// SSE-KMS whenever both were configured. Kept for backward compatibility,
391+
// but no longer silent.
392+
$this->encryptionWarnings[] = "Both 'sse_c_key' and 'sse_kms_enabled' are set; 'sse_c_key' (SSE-C) takes precedence, as it did in Nextcloud 34. Set the 'sse' objectstore argument to select a single encryption mode explicitly.";
393+
}
394+
$mode = $hasLegacySseC ? S3EncryptionMode::SseC : S3EncryptionMode::SseKms;
395+
$deprecatedKey = $hasLegacySseC ? 'sse_c_key' : 'sse_kms_enabled';
396+
$this->encryptionWarnings[] = "The '{$deprecatedKey}' objectstore argument is deprecated, use 'sse' => '{$mode->value}' instead.";
397+
} else {
398+
$mode = S3EncryptionMode::None;
399+
}
400+
401+
if ($mode === S3EncryptionMode::SseC) {
402+
// SSE-C itself is deprecated since Nextcloud 34, not just the legacy 'sse_c_key'
403+
// argument shape, so this warning is emitted regardless of how 'sse-c' was
404+
// selected. Amazon S3 disabled SSE-C by default for new buckets in April 2026
405+
// (https://aws.amazon.com/blogs/storage/advanced-notice-amazon-s3-to-disable-the-use-of-sse-c-encryption-by-default-for-all-new-buckets-and-select-existing-buckets-in-april-2026/),
406+
// which is why this also requires managing and distributing the key yourself.
407+
$this->encryptionWarnings[] = "SSE-C ('sse' => 'sse-c') is deprecated since Nextcloud 34: Amazon S3 disabled SSE-C by default for new buckets in April 2026, and it requires you to manage the encryption key yourself. Use 'sse' => 'sse-kms' or 'sse-kms-dsse' instead, which let AWS KMS manage the key.";
408+
if (empty($params['sse_c_key'])) {
409+
// Unlike a merely redundant option, this cannot be resolved: silently falling
410+
// back to no encryption would defeat an explicit request for SSE-C.
411+
throw new \Exception("The 'sse' objectstore argument is set to 'sse-c' but no 'sse_c_key' is configured.");
412+
}
413+
}
414+
415+
$this->sseMode = $mode;
416+
417+
$keyId = (isset($params['sse_kms_key_id']) && $params['sse_kms_key_id'] !== '') ? (string)$params['sse_kms_key_id'] : null;
418+
if ($keyId !== null && !$mode->isKms()) {
419+
$this->encryptionWarnings[] = "Ignoring the 'sse_kms_key_id' objectstore argument: it only applies when 'sse' is 'sse-kms' or 'sse-kms-dsse'.";
420+
$keyId = null;
421+
}
422+
$this->sseKmsKeyId = $keyId;
423+
424+
$context = $params['sse_kms_encryption_context'] ?? null;
425+
if ($context !== null) {
426+
// A structurally invalid context can never be resolved, regardless of mode.
427+
if (!is_array($context)) {
428+
throw new \Exception("The 'sse_kms_encryption_context' objectstore argument must be an array of string key-value pairs.");
429+
}
430+
foreach ($context as $contextKey => $contextValue) {
431+
if (!is_string($contextKey) || !is_string($contextValue)) {
432+
throw new \Exception("The 'sse_kms_encryption_context' objectstore argument must be an array of string key-value pairs.");
433+
}
434+
}
435+
if (!$mode->isKms()) {
436+
$this->encryptionWarnings[] = "Ignoring the 'sse_kms_encryption_context' objectstore argument: it only applies when 'sse' is 'sse-kms' or 'sse-kms-dsse'.";
437+
$context = null;
438+
}
439+
}
440+
$this->sseKmsEncryptionContext = $context !== null ? base64_encode(json_encode($context, JSON_THROW_ON_ERROR)) : null;
441+
442+
$bucketKey = (bool)($params['sse_kms_bucket_key'] ?? false);
443+
if ($bucketKey && $mode !== S3EncryptionMode::SseKms) {
444+
// Merely redundant, not impossible: verified against a real bucket that S3
445+
// accepts BucketKeyEnabled alongside 'aws:kms:dsse' and simply does not apply
446+
// it (HeadObject afterwards shows no BucketKeyEnabled), rather than rejecting
447+
// the request outright, despite AWS's own documentation describing S3 Bucket
448+
// Keys as unsupported for DSSE-KMS.
449+
$reason = $mode === S3EncryptionMode::SseKmsDsse
450+
? "S3 Bucket Keys are not applied for DSSE-KMS ('sse' => 'sse-kms-dsse')"
451+
: "it only applies when 'sse' is 'sse-kms'";
452+
$this->encryptionWarnings[] = "Ignoring the 'sse_kms_bucket_key' objectstore argument: {$reason}.";
453+
$bucketKey = false;
454+
}
455+
$this->sseKmsBucketKey = $bucketKey;
348456
}
349457

350458
/**
351-
* Check if SSE-KMS is enabled
352-
* @return bool
459+
* Log any warnings recorded by `parseEncryptionParams()`, once a connection is actually
460+
* made, deduplicated by message so a long-lived request/worker doesn't repeat the same
461+
* warning on every call.
353462
*/
354-
protected function isSSEKMSEnabled(): bool {
355-
return !empty($this->params['sse_kms_enabled']) && $this->params['sse_kms_enabled'] === true;
463+
private function logEncryptionWarnings(): void {
464+
if ($this->encryptionWarnings === []) {
465+
return;
466+
}
467+
468+
$logger = Server::get(LoggerInterface::class);
469+
foreach ($this->encryptionWarnings as $warning) {
470+
if (isset(self::$loggedEncryptionWarnings[$warning])) {
471+
continue;
472+
}
473+
self::$loggedEncryptionWarnings[$warning] = true;
474+
$logger->warning($warning, ['app' => 'objectstore']);
475+
}
356476
}
357477

358478
/**
359-
* Get SSE-KMS parameters for S3 operations
360-
*
361-
* When SSE-KMS is enabled, AWS S3 encrypts objects server-side using
362-
* AWS Key Management Service (KMS) keys. This provides:
363-
* - Centralized key management via AWS KMS
364-
* - Audit trail of key usage
365-
* - No client-side encryption overhead
366-
* - Automatic key rotation support
479+
* Get SSE-KMS / DSSE-KMS parameters for S3 operations.
367480
*
368-
* @param bool $copy Whether this is for a copy operation (unused for KMS)
369481
* @return array Parameters to merge into S3 API calls
370482
*/
371-
protected function getSSEKMSParameters(bool $copy = false): array {
372-
if (!$this->isSSEKMSEnabled()) {
373-
return [];
374-
}
375-
483+
private function getSSEKMSParameters(): array {
376484
$params = [
377-
'ServerSideEncryption' => 'aws:kms',
485+
'ServerSideEncryption' => $this->sseMode === S3EncryptionMode::SseKmsDsse ? 'aws:kms:dsse' : 'aws:kms',
378486
];
379487

380-
// Add specific KMS key if configured, otherwise use bucket default key
381-
$keyId = $this->getSSEKMSKeyId();
382-
if ($keyId !== null) {
383-
$params['SSEKMSKeyId'] = $keyId;
488+
// Add specific KMS key if configured, otherwise use the bucket default key
489+
if ($this->sseKmsKeyId !== null) {
490+
$params['SSEKMSKeyId'] = $this->sseKmsKeyId;
384491
}
385492

386-
// Note: For copy operations, S3 re-encrypts with the destination key
387-
// No special source parameters needed (unlike SSE-C)
493+
if ($this->sseKmsEncryptionContext !== null) {
494+
$params['SSEKMSEncryptionContext'] = $this->sseKmsEncryptionContext;
495+
}
496+
497+
if ($this->sseKmsBucketKey) {
498+
$params['BucketKeyEnabled'] = true;
499+
}
388500

389501
return $params;
390502
}
391503

392504
/**
393-
* Get unified server-side encryption parameters
505+
* Get server-side encryption parameters for S3 operations, for whichever `sse` mode is configured.
394506
*
395-
* Supports both SSE-C (customer-provided keys) and SSE-KMS (AWS-managed keys).
396-
* SSE-C takes precedence if both are configured (for backward compatibility
397-
* during migration from SSE-C to SSE-KMS).
398-
*
399-
* @param bool $copy Whether this is for a copy operation
507+
* @param bool $copy Whether this is for a copy operation (only relevant for SSE-C)
400508
* @return array Encryption parameters to merge into S3 API calls
401509
*/
402510
protected function getServerSideEncryptionParameters(bool $copy = false): array {
403-
// SSE-C takes precedence for backward compatibility during migration
404-
$sseC = $this->getSSECParameters($copy);
405-
if (!empty($sseC)) {
406-
return $sseC;
407-
}
511+
return match ($this->sseMode) {
512+
S3EncryptionMode::None => [],
513+
S3EncryptionMode::SseS3 => ['ServerSideEncryption' => 'AES256'],
514+
S3EncryptionMode::SseC => $this->getSSECParameters($copy),
515+
// Unlike SSE-C, KMS/DSSE-KMS have no CopySource* variant: on CopyObject these
516+
// headers describe the destination object only, so $copy is not needed here.
517+
S3EncryptionMode::SseKms, S3EncryptionMode::SseKmsDsse => $this->getSSEKMSParameters(),
518+
};
519+
}
408520

409-
// Fall back to SSE-KMS if enabled
410-
return $this->getSSEKMSParameters($copy);
521+
/**
522+
* Get the server-side encryption parameters for requests that do not create an object
523+
* (GetObject, HeadObject, ListParts, UploadPart, CompleteMultipartUpload, ...).
524+
*
525+
* Only SSE-C needs parameters here, as the customer key must accompany every request
526+
* touching the object. ServerSideEncryption/SSEKMS* are write-only directives, and
527+
* SSE-S3/SSE-KMS objects are decrypted by S3 without client-provided key material.
528+
*
529+
* @param bool $copySource Whether the object is the source of a copy
530+
* @return array Encryption parameters to merge into S3 API calls
531+
*/
532+
protected function getReadServerSideEncryptionParameters(bool $copySource = false): array {
533+
return $this->sseMode === S3EncryptionMode::SseC ? $this->getSSECParameters($copySource) : [];
411534
}
412535

413536
public function isUsePresignedUrl(): bool {

0 commit comments

Comments
 (0)