4646import software .amazon .awssdk .services .s3 .model .UploadPartRequest ;
4747import software .amazon .awssdk .services .s3 .model .UploadPartResponse ;
4848import software .amazon .encryption .s3 .algorithms .AlgorithmSuite ;
49+ import software .amazon .encryption .s3 .internal .ContentMetadata ;
50+ import software .amazon .encryption .s3 .internal .ContentMetadataDecodingStrategy ;
51+ import software .amazon .encryption .s3 .internal .ContentMetadataEncodingStrategy ;
4952import software .amazon .encryption .s3 .internal .ConvertSDKRequests ;
5053import software .amazon .encryption .s3 .internal .GetEncryptedObjectPipeline ;
5154import software .amazon .encryption .s3 .internal .InstructionFileConfig ;
5255import software .amazon .encryption .s3 .internal .MultiFileOutputStream ;
5356import software .amazon .encryption .s3 .internal .MultipartUploadObjectPipeline ;
5457import software .amazon .encryption .s3 .internal .PutEncryptedObjectPipeline ;
58+ import software .amazon .encryption .s3 .internal .ReEncryptInstructionFileRequest ;
59+ import software .amazon .encryption .s3 .internal .ReEncryptInstructionFileResponse ;
5560import software .amazon .encryption .s3 .internal .UploadObjectObserver ;
5661import software .amazon .encryption .s3 .materials .AesKeyring ;
5762import software .amazon .encryption .s3 .materials .CryptographicMaterialsManager ;
63+ import software .amazon .encryption .s3 .materials .DecryptMaterialsRequest ;
64+ import software .amazon .encryption .s3 .materials .DecryptionMaterials ;
5865import software .amazon .encryption .s3 .materials .DefaultCryptoMaterialsManager ;
66+ import software .amazon .encryption .s3 .materials .EncryptedDataKey ;
67+ import software .amazon .encryption .s3 .materials .EncryptionMaterials ;
5968import software .amazon .encryption .s3 .materials .Keyring ;
6069import software .amazon .encryption .s3 .materials .KmsKeyring ;
6170import software .amazon .encryption .s3 .materials .MultipartConfiguration ;
6271import software .amazon .encryption .s3 .materials .PartialRsaKeyPair ;
72+ import software .amazon .encryption .s3 .materials .RawKeyring ;
6373import software .amazon .encryption .s3 .materials .RsaKeyring ;
6474
6575import javax .crypto .SecretKey ;
6979import java .security .Provider ;
7080import java .security .SecureRandom ;
7181import java .util .ArrayList ;
82+ import java .util .Collections ;
7283import java .util .List ;
7384import java .util .Map ;
7485import java .util .Optional ;
8192import java .util .function .Consumer ;
8293
8394import static software .amazon .encryption .s3 .S3EncryptionClientUtilities .DEFAULT_BUFFER_SIZE_BYTES ;
84- import static software .amazon .encryption .s3 .S3EncryptionClientUtilities .INSTRUCTION_FILE_SUFFIX ;
95+
96+ import static software .amazon .encryption .s3 .S3EncryptionClientUtilities .DEFAULT_INSTRUCTION_FILE_SUFFIX ;
8597import static software .amazon .encryption .s3 .S3EncryptionClientUtilities .MAX_ALLOWED_BUFFER_SIZE_BYTES ;
8698import static software .amazon .encryption .s3 .S3EncryptionClientUtilities .MIN_ALLOWED_BUFFER_SIZE_BYTES ;
8799import static software .amazon .encryption .s3 .S3EncryptionClientUtilities .instructionFileKeysToDelete ;
@@ -97,6 +109,9 @@ public class S3EncryptionClient extends DelegatingS3Client {
97109 public static final ExecutionAttribute <Map <String , String >> ENCRYPTION_CONTEXT = new ExecutionAttribute <>("EncryptionContext" );
98110 public static final ExecutionAttribute <MultipartConfiguration > CONFIGURATION = new ExecutionAttribute <>("MultipartConfiguration" );
99111
112+ //Used for specifying custom instruction file suffix on a per-request basis
113+ public static final ExecutionAttribute <String > CUSTOM_INSTRUCTION_FILE_SUFFIX = new ExecutionAttribute <>("CustomInstructionFileSuffix" );
114+
100115 private final S3Client _wrappedClient ;
101116 private final S3AsyncClient _wrappedAsyncClient ;
102117 private final CryptographicMaterialsManager _cryptoMaterialsManager ;
@@ -143,6 +158,18 @@ public static Consumer<AwsRequestOverrideConfiguration.Builder> withAdditionalCo
143158 builder .putExecutionAttribute (S3EncryptionClient .ENCRYPTION_CONTEXT , encryptionContext );
144159 }
145160
161+ /**
162+ * Attaches a custom instruction file suffix to a request. Must be used as a parameter to
163+ * {@link S3Request#overrideConfiguration()} in the request.
164+ * This allows specifying a custom suffix for the instruction file on a per-request basis.
165+ * @param customInstructionFileSuffix the custom suffix to use for the instruction file.
166+ * @return Consumer for use in overrideConfiguration()
167+ */
168+ public static Consumer <AwsRequestOverrideConfiguration .Builder > withCustomInstructionFileSuffix (String customInstructionFileSuffix ) {
169+ return builder ->
170+ builder .putExecutionAttribute (S3EncryptionClient .CUSTOM_INSTRUCTION_FILE_SUFFIX , customInstructionFileSuffix );
171+ }
172+
146173 /**
147174 * Attaches multipart configuration to a request. Must be used as a parameter to
148175 * {@link S3Request#overrideConfiguration()} in the request.
@@ -154,7 +181,6 @@ public static Consumer<AwsRequestOverrideConfiguration.Builder> withAdditionalCo
154181 builder .putExecutionAttribute (S3EncryptionClient .CONFIGURATION , multipartConfiguration );
155182 }
156183
157-
158184 /**
159185 * Attaches encryption context and multipart configuration to a request.
160186 * * Must be used as a parameter to
@@ -172,6 +198,77 @@ public static Consumer<AwsRequestOverrideConfiguration.Builder> withAdditionalCo
172198 .putExecutionAttribute (S3EncryptionClient .CONFIGURATION , multipartConfiguration );
173199 }
174200
201+ /**
202+ * Re-encrypts an instruction file with a new keyring while preserving the original encrypted object in S3.
203+ * This enables:
204+ * 1. Key rotation by updating instruction file metadata without re-encrypting object content
205+ * 2. Sharing encrypted objects with partners by creating new instruction files with a custom suffix using their public keys
206+ * <p>
207+ * Key rotation scenarios:
208+ * - Legacy to V3: Can rotate same wrapping key from legacy wrapping algorithms to fully supported wrapping algorithms
209+ * - Within V3: When rotating the wrapping key, the new keyring must be different from the current keyring
210+ *
211+ * @param reEncryptInstructionFileRequest the request containing bucket, object key, new keyring, and optional instruction file suffix
212+ * @return ReEncryptInstructionFileResponse containing the bucket, object key, and instruction file suffix used
213+ * @throws S3EncryptionClientException if the new keyring has the same materials description as the current one
214+ */
215+ public ReEncryptInstructionFileResponse reEncryptInstructionFile (ReEncryptInstructionFileRequest reEncryptInstructionFileRequest ) {
216+ //Build request to retrieve the encrypted object and its associated instruction file
217+ final GetObjectRequest request = GetObjectRequest .builder ()
218+ .bucket (reEncryptInstructionFileRequest .bucket ())
219+ .key (reEncryptInstructionFileRequest .key ())
220+ .build ();
221+
222+ ResponseInputStream <GetObjectResponse > response = this .getObject (request );
223+ ContentMetadataDecodingStrategy decodingStrategy = new ContentMetadataDecodingStrategy (_instructionFileConfig );
224+ ContentMetadata contentMetadata = decodingStrategy .decode (request , response .response ());
225+
226+ //Extract cryptographic parameters from the current instruction file that MUST be preserved during re-encryption
227+ final AlgorithmSuite algorithmSuite = contentMetadata .algorithmSuite ();
228+ final EncryptedDataKey originalEncryptedDataKey = contentMetadata .encryptedDataKey ();
229+ final Map <String , String > currentKeyringMaterialsDescription = contentMetadata .encryptedDataKeyMatDescOrContext ();
230+ final byte [] iv = contentMetadata .contentIv ();
231+
232+ //Decrypt the data key using the current keyring
233+ DecryptionMaterials decryptedMaterials = this ._cryptoMaterialsManager .decryptMaterials (
234+ DecryptMaterialsRequest .builder ()
235+ .algorithmSuite (algorithmSuite )
236+ .encryptedDataKeys (Collections .singletonList (originalEncryptedDataKey ))
237+ .s3Request (request )
238+ .build ()
239+ );
240+
241+ final byte [] plaintextDataKey = decryptedMaterials .plaintextDataKey ();
242+
243+ //Prepare encryption materials with the decrypted data key
244+ EncryptionMaterials encryptionMaterials = EncryptionMaterials .builder ()
245+ .algorithmSuite (algorithmSuite )
246+ .plaintextDataKey (plaintextDataKey )
247+ .s3Request (request )
248+ .build ();
249+
250+ //Re-encrypt the data key with the new keyring while preserving other cryptographic parameters
251+ RawKeyring newKeyring = reEncryptInstructionFileRequest .newKeyring ();
252+ EncryptionMaterials encryptedMaterials = newKeyring .onEncrypt (encryptionMaterials );
253+
254+ final Map <String , String > newMaterialsDescription = encryptedMaterials .materialsDescription ().getMaterialsDescription ();
255+ //Validate that the new keyring has different materials description than the old keyring
256+ if (newMaterialsDescription .equals (currentKeyringMaterialsDescription )) {
257+ throw new S3EncryptionClientException ("New keyring must have new materials description!" );
258+ }
259+
260+ //Create or update instruction file with the re-encrypted metadata while preserving IV
261+ ContentMetadataEncodingStrategy encodeStrategy = new ContentMetadataEncodingStrategy (_instructionFileConfig );
262+ encodeStrategy .encodeMetadata (encryptedMaterials , iv , PutObjectRequest .builder ()
263+ .bucket (reEncryptInstructionFileRequest .bucket ())
264+ .key (reEncryptInstructionFileRequest .key ())
265+ .build (), reEncryptInstructionFileRequest .instructionFileSuffix ());
266+
267+ return new ReEncryptInstructionFileResponse (reEncryptInstructionFileRequest .bucket (),
268+ reEncryptInstructionFileRequest .key (), reEncryptInstructionFileRequest .instructionFileSuffix ());
269+
270+ }
271+
175272 /**
176273 * See {@link S3EncryptionClient#putObject(PutObjectRequest, RequestBody)}.
177274 * <p>
@@ -380,7 +477,7 @@ public DeleteObjectResponse deleteObject(DeleteObjectRequest deleteObjectRequest
380477 // Delete the object
381478 DeleteObjectResponse deleteObjectResponse = _wrappedAsyncClient .deleteObject (actualRequest ).join ();
382479 // If Instruction file exists, delete the instruction file as well.
383- String instructionObjectKey = deleteObjectRequest .key () + INSTRUCTION_FILE_SUFFIX ;
480+ String instructionObjectKey = deleteObjectRequest .key () + DEFAULT_INSTRUCTION_FILE_SUFFIX ;
384481 _wrappedAsyncClient .deleteObject (builder -> builder
385482 .overrideConfiguration (API_NAME_INTERCEPTOR )
386483 .bucket (deleteObjectRequest .bucket ())
0 commit comments