47
47
import software .amazon .awssdk .services .s3 .model .UploadPartRequest ;
48
48
import software .amazon .awssdk .services .s3 .model .UploadPartResponse ;
49
49
import software .amazon .encryption .s3 .algorithms .AlgorithmSuite ;
50
+ import software .amazon .encryption .s3 .internal .ContentMetadata ;
51
+ import software .amazon .encryption .s3 .internal .ContentMetadataDecodingStrategy ;
52
+ import software .amazon .encryption .s3 .internal .ContentMetadataEncodingStrategy ;
50
53
import software .amazon .encryption .s3 .internal .ConvertSDKRequests ;
51
54
import software .amazon .encryption .s3 .internal .GetEncryptedObjectPipeline ;
52
55
import software .amazon .encryption .s3 .internal .InstructionFileConfig ;
53
56
import software .amazon .encryption .s3 .internal .MultiFileOutputStream ;
54
57
import software .amazon .encryption .s3 .internal .MultipartUploadObjectPipeline ;
55
58
import software .amazon .encryption .s3 .internal .PutEncryptedObjectPipeline ;
59
+ import software .amazon .encryption .s3 .internal .ReEncryptInstructionFileRequest ;
60
+ import software .amazon .encryption .s3 .internal .ReEncryptInstructionFileResponse ;
56
61
import software .amazon .encryption .s3 .internal .UploadObjectObserver ;
57
62
import software .amazon .encryption .s3 .materials .AesKeyring ;
58
63
import software .amazon .encryption .s3 .materials .CryptographicMaterialsManager ;
64
+ import software .amazon .encryption .s3 .materials .DecryptMaterialsRequest ;
65
+ import software .amazon .encryption .s3 .materials .DecryptionMaterials ;
59
66
import software .amazon .encryption .s3 .materials .DefaultCryptoMaterialsManager ;
67
+ import software .amazon .encryption .s3 .materials .EncryptedDataKey ;
68
+ import software .amazon .encryption .s3 .materials .EncryptionMaterials ;
60
69
import software .amazon .encryption .s3 .materials .Keyring ;
61
70
import software .amazon .encryption .s3 .materials .KmsKeyring ;
62
71
import software .amazon .encryption .s3 .materials .MultipartConfiguration ;
63
72
import software .amazon .encryption .s3 .materials .PartialRsaKeyPair ;
73
+ import software .amazon .encryption .s3 .materials .RawKeyring ;
64
74
import software .amazon .encryption .s3 .materials .RsaKeyring ;
65
75
import software .amazon .encryption .s3 .materials .S3Keyring ;
66
76
71
81
import java .security .Provider ;
72
82
import java .security .SecureRandom ;
73
83
import java .util .ArrayList ;
84
+ import java .util .Collections ;
74
85
import java .util .List ;
75
86
import java .util .Map ;
76
87
import java .util .Optional ;
83
94
import java .util .function .Consumer ;
84
95
85
96
import static software .amazon .encryption .s3 .S3EncryptionClientUtilities .DEFAULT_BUFFER_SIZE_BYTES ;
86
- import static software .amazon .encryption .s3 .S3EncryptionClientUtilities .INSTRUCTION_FILE_SUFFIX ;
97
+
98
+ import static software .amazon .encryption .s3 .S3EncryptionClientUtilities .DEFAULT_INSTRUCTION_FILE_SUFFIX ;
87
99
import static software .amazon .encryption .s3 .S3EncryptionClientUtilities .MAX_ALLOWED_BUFFER_SIZE_BYTES ;
88
100
import static software .amazon .encryption .s3 .S3EncryptionClientUtilities .MIN_ALLOWED_BUFFER_SIZE_BYTES ;
89
101
import static software .amazon .encryption .s3 .S3EncryptionClientUtilities .instructionFileKeysToDelete ;
@@ -99,6 +111,9 @@ public class S3EncryptionClient extends DelegatingS3Client {
99
111
public static final ExecutionAttribute <Map <String , String >> ENCRYPTION_CONTEXT = new ExecutionAttribute <>("EncryptionContext" );
100
112
public static final ExecutionAttribute <MultipartConfiguration > CONFIGURATION = new ExecutionAttribute <>("MultipartConfiguration" );
101
113
114
+ //Used for specifying custom instruction file suffix on a per-request basis
115
+ public static final ExecutionAttribute <String > CUSTOM_INSTRUCTION_FILE_SUFFIX = new ExecutionAttribute <>("CustomInstructionFileSuffix" );
116
+
102
117
private final S3Client _wrappedClient ;
103
118
private final S3AsyncClient _wrappedAsyncClient ;
104
119
private final CryptographicMaterialsManager _cryptoMaterialsManager ;
@@ -145,6 +160,18 @@ public static Consumer<AwsRequestOverrideConfiguration.Builder> withAdditionalCo
145
160
builder .putExecutionAttribute (S3EncryptionClient .ENCRYPTION_CONTEXT , encryptionContext );
146
161
}
147
162
163
+ /**
164
+ * Attaches a custom instruction file suffix to a request. Must be used as a parameter to
165
+ * {@link S3Request#overrideConfiguration()} in the request.
166
+ * This allows specifying a custom suffix for the instruction file on a per-request basis.
167
+ * @param customInstructionFileSuffix the custom suffix to use for the instruction file.
168
+ * @return Consumer for use in overrideConfiguration()
169
+ */
170
+ public static Consumer <AwsRequestOverrideConfiguration .Builder > withCustomInstructionFileSuffix (String customInstructionFileSuffix ) {
171
+ return builder ->
172
+ builder .putExecutionAttribute (S3EncryptionClient .CUSTOM_INSTRUCTION_FILE_SUFFIX , customInstructionFileSuffix );
173
+ }
174
+
148
175
/**
149
176
* Attaches multipart configuration to a request. Must be used as a parameter to
150
177
* {@link S3Request#overrideConfiguration()} in the request.
@@ -156,7 +183,6 @@ public static Consumer<AwsRequestOverrideConfiguration.Builder> withAdditionalCo
156
183
builder .putExecutionAttribute (S3EncryptionClient .CONFIGURATION , multipartConfiguration );
157
184
}
158
185
159
-
160
186
/**
161
187
* Attaches encryption context and multipart configuration to a request.
162
188
* * Must be used as a parameter to
@@ -174,6 +200,102 @@ public static Consumer<AwsRequestOverrideConfiguration.Builder> withAdditionalCo
174
200
.putExecutionAttribute (S3EncryptionClient .CONFIGURATION , multipartConfiguration );
175
201
}
176
202
203
+ /**
204
+ * Re-encrypts an instruction file with a new keyring while preserving the original encrypted object in S3.
205
+ * This enables:
206
+ * 1. Key rotation by updating instruction file metadata without re-encrypting object content
207
+ * 2. Sharing encrypted objects with partners by creating new instruction files with a custom suffix using their public keys
208
+ * <p>
209
+ * Key rotation scenarios:
210
+ * - Legacy to V3: Can rotate same wrapping key from legacy wrapping algorithms to fully supported wrapping algorithms
211
+ * - Within V3: When rotating the wrapping key, the new keyring must be different from the current keyring
212
+ * - Enforce Rotation: When enabled, ensures old keyring cannot decrypt data encrypted by new keyring
213
+ *
214
+ * @param reEncryptInstructionFileRequest the request containing bucket, object key, new keyring, and optional instruction file suffix
215
+ * @return ReEncryptInstructionFileResponse containing the bucket, object key, and instruction file suffix used
216
+ * @throws S3EncryptionClientException if the new keyring has the same materials description as the current one
217
+ */
218
+ public ReEncryptInstructionFileResponse reEncryptInstructionFile (ReEncryptInstructionFileRequest reEncryptInstructionFileRequest ) {
219
+ if (!_instructionFileConfig .isInstructionFilePutEnabled ()) {
220
+ throw new S3EncryptionClientException ("Instruction file put operations must be enabled to re-encrypt instruction files" );
221
+ }
222
+
223
+ //Build request to retrieve the encrypted object and its associated instruction file
224
+ final GetObjectRequest request = GetObjectRequest .builder ()
225
+ .bucket (reEncryptInstructionFileRequest .bucket ())
226
+ .key (reEncryptInstructionFileRequest .key ())
227
+ .build ();
228
+
229
+ ResponseInputStream <GetObjectResponse > response = this .getObject (request );
230
+ ContentMetadataDecodingStrategy decodingStrategy = new ContentMetadataDecodingStrategy (_instructionFileConfig );
231
+ ContentMetadata contentMetadata = decodingStrategy .decode (request , response .response ());
232
+
233
+ //Extract cryptographic parameters from the current instruction file that MUST be preserved during re-encryption
234
+ final AlgorithmSuite algorithmSuite = contentMetadata .algorithmSuite ();
235
+ final EncryptedDataKey originalEncryptedDataKey = contentMetadata .encryptedDataKey ();
236
+ final Map <String , String > currentKeyringMaterialsDescription = contentMetadata .encryptedDataKeyMatDescOrContext ();
237
+ final byte [] iv = contentMetadata .contentIv ();
238
+
239
+ //Decrypt the data key using the current keyring
240
+ DecryptionMaterials decryptedMaterials = this ._cryptoMaterialsManager .decryptMaterials (
241
+ DecryptMaterialsRequest .builder ()
242
+ .algorithmSuite (algorithmSuite )
243
+ .encryptedDataKeys (Collections .singletonList (originalEncryptedDataKey ))
244
+ .s3Request (request )
245
+ .build ()
246
+ );
247
+
248
+ final byte [] plaintextDataKey = decryptedMaterials .plaintextDataKey ();
249
+
250
+ //Prepare encryption materials with the decrypted data key
251
+ EncryptionMaterials encryptionMaterials = EncryptionMaterials .builder ()
252
+ .algorithmSuite (algorithmSuite )
253
+ .plaintextDataKey (plaintextDataKey )
254
+ .s3Request (request )
255
+ .build ();
256
+
257
+ //Re-encrypt the data key with the new keyring while preserving other cryptographic parameters
258
+ RawKeyring newKeyring = reEncryptInstructionFileRequest .newKeyring ();
259
+ EncryptionMaterials encryptedMaterials = newKeyring .onEncrypt (encryptionMaterials );
260
+
261
+ final Map <String , String > newMaterialsDescription = encryptedMaterials .materialsDescription ().getMaterialsDescription ();
262
+ //Validate that the new keyring has different materials description than the old keyring
263
+ if (newMaterialsDescription .equals (currentKeyringMaterialsDescription )) {
264
+ throw new S3EncryptionClientException ("New keyring must have new materials description!" );
265
+ }
266
+
267
+ // If enforceRotation is set to true, ensure that the old keyring cannot decrypt the newly encrypted data key
268
+ if (reEncryptInstructionFileRequest .enforceRotation ()) {
269
+ enforceRotation (encryptedMaterials , request );
270
+ }
271
+
272
+ //Create or update instruction file with the re-encrypted metadata while preserving IV
273
+ ContentMetadataEncodingStrategy encodeStrategy = new ContentMetadataEncodingStrategy (_instructionFileConfig );
274
+ encodeStrategy .encodeMetadata (encryptedMaterials , iv , PutObjectRequest .builder ()
275
+ .bucket (reEncryptInstructionFileRequest .bucket ())
276
+ .key (reEncryptInstructionFileRequest .key ())
277
+ .build (), reEncryptInstructionFileRequest .instructionFileSuffix ());
278
+
279
+ return new ReEncryptInstructionFileResponse (reEncryptInstructionFileRequest .bucket (),
280
+ reEncryptInstructionFileRequest .key (), reEncryptInstructionFileRequest .instructionFileSuffix (), reEncryptInstructionFileRequest .enforceRotation ());
281
+
282
+ }
283
+
284
+ private void enforceRotation (EncryptionMaterials newEncryptionMaterials , GetObjectRequest request ) {
285
+ try {
286
+ DecryptionMaterials decryptedMaterials = this ._cryptoMaterialsManager .decryptMaterials (
287
+ DecryptMaterialsRequest .builder ()
288
+ .algorithmSuite (newEncryptionMaterials .algorithmSuite ())
289
+ .encryptedDataKeys (newEncryptionMaterials .encryptedDataKeys ())
290
+ .s3Request (request )
291
+ .build ()
292
+ );
293
+ } catch (S3EncryptionClientException e ) {
294
+ return ;
295
+ }
296
+ throw new S3EncryptionClientException ("Re-encryption failed due to enforced rotation! Old keyring is still able to decrypt the newly encrypted data key" );
297
+ }
298
+
177
299
/**
178
300
* See {@link S3EncryptionClient#putObject(PutObjectRequest, RequestBody)}.
179
301
* <p>
@@ -382,7 +504,7 @@ public DeleteObjectResponse deleteObject(DeleteObjectRequest deleteObjectRequest
382
504
// Delete the object
383
505
DeleteObjectResponse deleteObjectResponse = _wrappedAsyncClient .deleteObject (actualRequest ).join ();
384
506
// If Instruction file exists, delete the instruction file as well.
385
- String instructionObjectKey = deleteObjectRequest .key () + INSTRUCTION_FILE_SUFFIX ;
507
+ String instructionObjectKey = deleteObjectRequest .key () + DEFAULT_INSTRUCTION_FILE_SUFFIX ;
386
508
_wrappedAsyncClient .deleteObject (builder -> builder
387
509
.overrideConfiguration (API_NAME_INTERCEPTOR )
388
510
.bucket (deleteObjectRequest .bucket ())
0 commit comments