4747import software .amazon .awssdk .services .s3 .model .UploadPartRequest ;
4848import software .amazon .awssdk .services .s3 .model .UploadPartResponse ;
4949import 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 ;
5053import software .amazon .encryption .s3 .internal .ConvertSDKRequests ;
5154import software .amazon .encryption .s3 .internal .GetEncryptedObjectPipeline ;
5255import software .amazon .encryption .s3 .internal .InstructionFileConfig ;
5356import software .amazon .encryption .s3 .internal .MultiFileOutputStream ;
5457import software .amazon .encryption .s3 .internal .MultipartUploadObjectPipeline ;
5558import software .amazon .encryption .s3 .internal .PutEncryptedObjectPipeline ;
59+ import software .amazon .encryption .s3 .internal .ReEncryptInstructionFileRequest ;
60+ import software .amazon .encryption .s3 .internal .ReEncryptInstructionFileResponse ;
5661import software .amazon .encryption .s3 .internal .UploadObjectObserver ;
5762import software .amazon .encryption .s3 .materials .AesKeyring ;
5863import software .amazon .encryption .s3 .materials .CryptographicMaterialsManager ;
64+ import software .amazon .encryption .s3 .materials .DecryptMaterialsRequest ;
65+ import software .amazon .encryption .s3 .materials .DecryptionMaterials ;
5966import software .amazon .encryption .s3 .materials .DefaultCryptoMaterialsManager ;
67+ import software .amazon .encryption .s3 .materials .EncryptedDataKey ;
68+ import software .amazon .encryption .s3 .materials .EncryptionMaterials ;
6069import software .amazon .encryption .s3 .materials .Keyring ;
6170import software .amazon .encryption .s3 .materials .KmsKeyring ;
6271import software .amazon .encryption .s3 .materials .MultipartConfiguration ;
6372import software .amazon .encryption .s3 .materials .PartialRsaKeyPair ;
73+ import software .amazon .encryption .s3 .materials .RawKeyring ;
6474import software .amazon .encryption .s3 .materials .RsaKeyring ;
6575import software .amazon .encryption .s3 .materials .S3Keyring ;
6676
7181import java .security .Provider ;
7282import java .security .SecureRandom ;
7383import java .util .ArrayList ;
84+ import java .util .Collections ;
7485import java .util .List ;
7586import java .util .Map ;
7687import java .util .Optional ;
8394import java .util .function .Consumer ;
8495
8596import 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 ;
8799import static software .amazon .encryption .s3 .S3EncryptionClientUtilities .MAX_ALLOWED_BUFFER_SIZE_BYTES ;
88100import static software .amazon .encryption .s3 .S3EncryptionClientUtilities .MIN_ALLOWED_BUFFER_SIZE_BYTES ;
89101import static software .amazon .encryption .s3 .S3EncryptionClientUtilities .instructionFileKeysToDelete ;
@@ -99,6 +111,9 @@ public class S3EncryptionClient extends DelegatingS3Client {
99111 public static final ExecutionAttribute <Map <String , String >> ENCRYPTION_CONTEXT = new ExecutionAttribute <>("EncryptionContext" );
100112 public static final ExecutionAttribute <MultipartConfiguration > CONFIGURATION = new ExecutionAttribute <>("MultipartConfiguration" );
101113
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+
102117 private final S3Client _wrappedClient ;
103118 private final S3AsyncClient _wrappedAsyncClient ;
104119 private final CryptographicMaterialsManager _cryptoMaterialsManager ;
@@ -145,6 +160,18 @@ public static Consumer<AwsRequestOverrideConfiguration.Builder> withAdditionalCo
145160 builder .putExecutionAttribute (S3EncryptionClient .ENCRYPTION_CONTEXT , encryptionContext );
146161 }
147162
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+
148175 /**
149176 * Attaches multipart configuration to a request. Must be used as a parameter to
150177 * {@link S3Request#overrideConfiguration()} in the request.
@@ -156,7 +183,6 @@ public static Consumer<AwsRequestOverrideConfiguration.Builder> withAdditionalCo
156183 builder .putExecutionAttribute (S3EncryptionClient .CONFIGURATION , multipartConfiguration );
157184 }
158185
159-
160186 /**
161187 * Attaches encryption context and multipart configuration to a request.
162188 * * Must be used as a parameter to
@@ -174,6 +200,102 @@ public static Consumer<AwsRequestOverrideConfiguration.Builder> withAdditionalCo
174200 .putExecutionAttribute (S3EncryptionClient .CONFIGURATION , multipartConfiguration );
175201 }
176202
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+
177299 /**
178300 * See {@link S3EncryptionClient#putObject(PutObjectRequest, RequestBody)}.
179301 * <p>
@@ -382,7 +504,7 @@ public DeleteObjectResponse deleteObject(DeleteObjectRequest deleteObjectRequest
382504 // Delete the object
383505 DeleteObjectResponse deleteObjectResponse = _wrappedAsyncClient .deleteObject (actualRequest ).join ();
384506 // 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 ;
386508 _wrappedAsyncClient .deleteObject (builder -> builder
387509 .overrideConfiguration (API_NAME_INTERCEPTOR )
388510 .bucket (deleteObjectRequest .bucket ())
0 commit comments