Skip to content

Commit 675256f

Browse files
akareddy04Anirav Kareddy
andauthored
feat: ReEncryptInstructionFile Implementation (#470)
--------- Co-authored-by: Anirav Kareddy <aniravk@amazon.com>
1 parent b643bc8 commit 675256f

18 files changed

+2511
-112
lines changed

src/examples/java/software/amazon/encryption/s3/examples/ReEncryptInstructionFileExample.java

Lines changed: 421 additions & 0 deletions
Large diffs are not rendered by default.

src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,20 +46,30 @@
4646
import software.amazon.awssdk.services.s3.model.UploadPartRequest;
4747
import software.amazon.awssdk.services.s3.model.UploadPartResponse;
4848
import 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;
4952
import software.amazon.encryption.s3.internal.ConvertSDKRequests;
5053
import software.amazon.encryption.s3.internal.GetEncryptedObjectPipeline;
5154
import software.amazon.encryption.s3.internal.InstructionFileConfig;
5255
import software.amazon.encryption.s3.internal.MultiFileOutputStream;
5356
import software.amazon.encryption.s3.internal.MultipartUploadObjectPipeline;
5457
import software.amazon.encryption.s3.internal.PutEncryptedObjectPipeline;
58+
import software.amazon.encryption.s3.internal.ReEncryptInstructionFileRequest;
59+
import software.amazon.encryption.s3.internal.ReEncryptInstructionFileResponse;
5560
import software.amazon.encryption.s3.internal.UploadObjectObserver;
5661
import software.amazon.encryption.s3.materials.AesKeyring;
5762
import software.amazon.encryption.s3.materials.CryptographicMaterialsManager;
63+
import software.amazon.encryption.s3.materials.DecryptMaterialsRequest;
64+
import software.amazon.encryption.s3.materials.DecryptionMaterials;
5865
import software.amazon.encryption.s3.materials.DefaultCryptoMaterialsManager;
66+
import software.amazon.encryption.s3.materials.EncryptedDataKey;
67+
import software.amazon.encryption.s3.materials.EncryptionMaterials;
5968
import software.amazon.encryption.s3.materials.Keyring;
6069
import software.amazon.encryption.s3.materials.KmsKeyring;
6170
import software.amazon.encryption.s3.materials.MultipartConfiguration;
6271
import software.amazon.encryption.s3.materials.PartialRsaKeyPair;
72+
import software.amazon.encryption.s3.materials.RawKeyring;
6373
import software.amazon.encryption.s3.materials.RsaKeyring;
6474

6575
import javax.crypto.SecretKey;
@@ -69,6 +79,7 @@
6979
import java.security.Provider;
7080
import java.security.SecureRandom;
7181
import java.util.ArrayList;
82+
import java.util.Collections;
7283
import java.util.List;
7384
import java.util.Map;
7485
import java.util.Optional;
@@ -81,7 +92,8 @@
8192
import java.util.function.Consumer;
8293

8394
import 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;
8597
import static software.amazon.encryption.s3.S3EncryptionClientUtilities.MAX_ALLOWED_BUFFER_SIZE_BYTES;
8698
import static software.amazon.encryption.s3.S3EncryptionClientUtilities.MIN_ALLOWED_BUFFER_SIZE_BYTES;
8799
import 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())

src/main/java/software/amazon/encryption/s3/S3EncryptionClientUtilities.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
public class S3EncryptionClientUtilities {
1717

18-
public static final String INSTRUCTION_FILE_SUFFIX = ".instruction";
18+
public static final String DEFAULT_INSTRUCTION_FILE_SUFFIX = ".instruction";
1919
public static final long MIN_ALLOWED_BUFFER_SIZE_BYTES = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherBlockSizeBytes();
2020
public static final long MAX_ALLOWED_BUFFER_SIZE_BYTES = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherMaxContentLengthBytes();
2121

@@ -32,7 +32,7 @@ public class S3EncryptionClientUtilities {
3232
*/
3333
static List<ObjectIdentifier> instructionFileKeysToDelete(final DeleteObjectsRequest request) {
3434
return request.delete().objects().stream()
35-
.map(o -> o.toBuilder().key(o.key() + INSTRUCTION_FILE_SUFFIX).build())
35+
.map(o -> o.toBuilder().key(o.key() + DEFAULT_INSTRUCTION_FILE_SUFFIX).build())
3636
.collect(Collectors.toList());
3737
}
3838
}

src/main/java/software/amazon/encryption/s3/internal/ContentMetadata.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public String encryptedDataKeyAlgorithm() {
6464
*/
6565
@SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "False positive; underlying"
6666
+ " implementation is immutable")
67-
public Map<String, String> encryptedDataKeyContext() {
67+
public Map<String, String> encryptedDataKeyMatDescOrContext() {
6868
return _encryptionContextOrMatDesc;
6969
}
7070

src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
1010
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
1111
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
12+
import software.amazon.encryption.s3.S3EncryptionClient;
1213
import software.amazon.encryption.s3.S3EncryptionClientException;
1314
import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
1415
import software.amazon.encryption.s3.materials.EncryptedDataKey;
@@ -24,7 +25,7 @@
2425
import java.util.Map;
2526
import java.util.concurrent.CompletionException;
2627

27-
import static software.amazon.encryption.s3.S3EncryptionClientUtilities.INSTRUCTION_FILE_SUFFIX;
28+
import static software.amazon.encryption.s3.S3EncryptionClientUtilities.DEFAULT_INSTRUCTION_FILE_SUFFIX;
2829

2930
public class ContentMetadataDecodingStrategy {
3031

@@ -224,9 +225,13 @@ private ContentMetadata decodeFromObjectMetadata(GetObjectRequest request, GetOb
224225
}
225226

226227
private ContentMetadata decodeFromInstructionFile(GetObjectRequest request, GetObjectResponse response) {
228+
String instructionFileSuffix = request.overrideConfiguration()
229+
.flatMap(config -> config.executionAttributes().getOptionalAttribute(S3EncryptionClient.CUSTOM_INSTRUCTION_FILE_SUFFIX))
230+
.orElse(DEFAULT_INSTRUCTION_FILE_SUFFIX);
231+
227232
GetObjectRequest instructionGetObjectRequest = GetObjectRequest.builder()
228233
.bucket(request.bucket())
229-
.key(request.key() + INSTRUCTION_FILE_SUFFIX)
234+
.key(request.key() + instructionFileSuffix)
230235
.build();
231236

232237
ResponseInputStream<GetObjectResponse> instruction;

src/main/java/software/amazon/encryption/s3/internal/ContentMetadataEncodingStrategy.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import java.util.HashMap;
1515
import java.util.Map;
1616

17+
import static software.amazon.encryption.s3.S3EncryptionClientUtilities.DEFAULT_INSTRUCTION_FILE_SUFFIX;
18+
1719
public class ContentMetadataEncodingStrategy {
1820

1921
private static final Base64.Encoder ENCODER = Base64.getEncoder();
@@ -24,16 +26,20 @@ public ContentMetadataEncodingStrategy(InstructionFileConfig instructionFileConf
2426
}
2527

2628
public PutObjectRequest encodeMetadata(EncryptionMaterials materials, byte[] iv, PutObjectRequest putObjectRequest) {
29+
return encodeMetadata(materials, iv, putObjectRequest, DEFAULT_INSTRUCTION_FILE_SUFFIX);
30+
}
31+
32+
public PutObjectRequest encodeMetadata(EncryptionMaterials materials, byte[] iv, PutObjectRequest putObjectRequest, String instructionFileSuffix) {
2733
if (_instructionFileConfig.isInstructionFilePutEnabled()) {
2834
final String metadataString = metadataToString(materials, iv);
29-
_instructionFileConfig.putInstructionFile(putObjectRequest, metadataString);
35+
_instructionFileConfig.putInstructionFile(putObjectRequest, metadataString, instructionFileSuffix);
3036
// the original request object is returned as-is
3137
return putObjectRequest;
3238
} else {
3339
Map<String, String> newMetadata = addMetadataToMap(putObjectRequest.metadata(), materials, iv);
3440
return putObjectRequest.toBuilder()
35-
.metadata(newMetadata)
36-
.build();
41+
.metadata(newMetadata)
42+
.build();
3743
}
3844
}
3945

@@ -51,6 +57,7 @@ public CreateMultipartUploadRequest encodeMetadata(EncryptionMaterials materials
5157
.build();
5258
}
5359
}
60+
5461
private String metadataToString(EncryptionMaterials materials, byte[] iv) {
5562
// this is just the metadata map serialized as JSON
5663
// so first get the Map

src/main/java/software/amazon/encryption/s3/internal/GetEncryptedObjectPipeline.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ private DecryptionMaterials prepareMaterialsFromRequest(final GetObjectRequest g
8585
.s3Request(getObjectRequest)
8686
.algorithmSuite(algorithmSuite)
8787
.encryptedDataKeys(encryptedDataKeys)
88-
.encryptionContext(contentMetadata.encryptedDataKeyContext())
88+
.encryptionContext(contentMetadata.encryptedDataKeyMatDescOrContext())
8989
.ciphertextLength(getObjectResponse.contentLength())
9090
.contentRange(getObjectRequest.range())
9191
.build();

src/main/java/software/amazon/encryption/s3/internal/InstructionFileConfig.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
import java.util.HashMap;
1616
import java.util.Map;
1717

18-
import static software.amazon.encryption.s3.S3EncryptionClientUtilities.INSTRUCTION_FILE_SUFFIX;
18+
19+
import static software.amazon.encryption.s3.S3EncryptionClientUtilities.DEFAULT_INSTRUCTION_FILE_SUFFIX;
1920
import static software.amazon.encryption.s3.internal.MetadataKeyConstants.INSTRUCTION_FILE;
2021

2122
/**
@@ -49,6 +50,10 @@ boolean isInstructionFilePutEnabled() {
4950
}
5051

5152
PutObjectResponse putInstructionFile(PutObjectRequest request, String instructionFileContent) {
53+
return putInstructionFile(request, instructionFileContent, DEFAULT_INSTRUCTION_FILE_SUFFIX);
54+
}
55+
56+
PutObjectResponse putInstructionFile(PutObjectRequest request, String instructionFileContent, String instructionFileSuffix) {
5257
// This shouldn't happen in practice because the metadata strategy will evaluate
5358
// if instruction file Puts are enabled before calling this method; check again anyway for robustness
5459
if (!_enableInstructionFilePut) {
@@ -60,12 +65,11 @@ PutObjectResponse putInstructionFile(PutObjectRequest request, String instructio
6065
// It contains a key with no value identifying it as an instruction file
6166
instFileMetadata.put(INSTRUCTION_FILE, "");
6267

63-
// In a future release, non-default suffixes will be supported.
6468
// Use toBuilder to keep all other fields the same as the actual request
6569
final PutObjectRequest instPutRequest = request.toBuilder()
66-
.key(request.key() + INSTRUCTION_FILE_SUFFIX)
67-
.metadata(instFileMetadata)
68-
.build();
70+
.key(request.key() + instructionFileSuffix)
71+
.metadata(instFileMetadata)
72+
.build();
6973
switch (_clientType) {
7074
case SYNCHRONOUS:
7175
return _s3Client.putObject(instPutRequest, RequestBody.fromString(instructionFileContent));

0 commit comments

Comments
 (0)