Skip to content

Commit f7e6fa5

Browse files
akareddy04Anirav Kareddy
andauthored
feat: reEncryptInstructionFile Implementation (#478)
Implemented the following feature: re-encrypting an instruction file with a new keyring while preserving the original encrypted object in S3. This enables: Key rotation by updating instruction file metadata without re-encrypting object content Sharing encrypted objects with partners by creating new instruction files with a custom suffix using their public keys --------- Co-authored-by: Anirav Kareddy <[email protected]>
1 parent 6d45ec5 commit f7e6fa5

22 files changed

+6340
-79
lines changed

.prettierignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Ignore artifacts:
2+
build
3+
coverage

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

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

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

Lines changed: 125 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,20 +47,30 @@
4747
import software.amazon.awssdk.services.s3.model.UploadPartRequest;
4848
import software.amazon.awssdk.services.s3.model.UploadPartResponse;
4949
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;
5053
import software.amazon.encryption.s3.internal.ConvertSDKRequests;
5154
import software.amazon.encryption.s3.internal.GetEncryptedObjectPipeline;
5255
import software.amazon.encryption.s3.internal.InstructionFileConfig;
5356
import software.amazon.encryption.s3.internal.MultiFileOutputStream;
5457
import software.amazon.encryption.s3.internal.MultipartUploadObjectPipeline;
5558
import software.amazon.encryption.s3.internal.PutEncryptedObjectPipeline;
59+
import software.amazon.encryption.s3.internal.ReEncryptInstructionFileRequest;
60+
import software.amazon.encryption.s3.internal.ReEncryptInstructionFileResponse;
5661
import software.amazon.encryption.s3.internal.UploadObjectObserver;
5762
import software.amazon.encryption.s3.materials.AesKeyring;
5863
import software.amazon.encryption.s3.materials.CryptographicMaterialsManager;
64+
import software.amazon.encryption.s3.materials.DecryptMaterialsRequest;
65+
import software.amazon.encryption.s3.materials.DecryptionMaterials;
5966
import software.amazon.encryption.s3.materials.DefaultCryptoMaterialsManager;
67+
import software.amazon.encryption.s3.materials.EncryptedDataKey;
68+
import software.amazon.encryption.s3.materials.EncryptionMaterials;
6069
import software.amazon.encryption.s3.materials.Keyring;
6170
import software.amazon.encryption.s3.materials.KmsKeyring;
6271
import software.amazon.encryption.s3.materials.MultipartConfiguration;
6372
import software.amazon.encryption.s3.materials.PartialRsaKeyPair;
73+
import software.amazon.encryption.s3.materials.RawKeyring;
6474
import software.amazon.encryption.s3.materials.RsaKeyring;
6575
import software.amazon.encryption.s3.materials.S3Keyring;
6676

@@ -71,6 +81,7 @@
7181
import java.security.Provider;
7282
import java.security.SecureRandom;
7383
import java.util.ArrayList;
84+
import java.util.Collections;
7485
import java.util.List;
7586
import java.util.Map;
7687
import java.util.Optional;
@@ -83,7 +94,8 @@
8394
import java.util.function.Consumer;
8495

8596
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;
8799
import static software.amazon.encryption.s3.S3EncryptionClientUtilities.MAX_ALLOWED_BUFFER_SIZE_BYTES;
88100
import static software.amazon.encryption.s3.S3EncryptionClientUtilities.MIN_ALLOWED_BUFFER_SIZE_BYTES;
89101
import 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())

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: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ public class ContentMetadata {
1515

1616
private final EncryptedDataKey _encryptedDataKey;
1717
private final String _encryptedDataKeyAlgorithm;
18-
private final Map<String, String> _encryptedDataKeyContext;
18+
19+
/**
20+
* This field stores either encryption context or material description.
21+
* We use a single field to store both in order to maintain backwards
22+
* compatibility with V2, which treated both as the same.
23+
*/
24+
private final Map<String, String> _encryptionContextOrMatDesc;
1925

2026
private final byte[] _contentIv;
2127
private final String _contentCipher;
@@ -27,7 +33,7 @@ private ContentMetadata(Builder builder) {
2733

2834
_encryptedDataKey = builder._encryptedDataKey;
2935
_encryptedDataKeyAlgorithm = builder._encryptedDataKeyAlgorithm;
30-
_encryptedDataKeyContext = builder._encryptedDataKeyContext;
36+
_encryptionContextOrMatDesc = builder._encryptionContextOrMatDesc;
3137

3238
_contentIv = builder._contentIv;
3339
_contentCipher = builder._contentCipher;
@@ -51,14 +57,15 @@ public String encryptedDataKeyAlgorithm() {
5157
return _encryptedDataKeyAlgorithm;
5258
}
5359

60+
5461
/**
5562
* Note that the underlying implementation uses a Collections.unmodifiableMap which is
5663
* immutable.
5764
*/
5865
@SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "False positive; underlying"
5966
+ " implementation is immutable")
60-
public Map<String, String> encryptedDataKeyContext() {
61-
return _encryptedDataKeyContext;
67+
public Map<String, String> encryptedDataKeyMatDescOrContext() {
68+
return _encryptionContextOrMatDesc;
6269
}
6370

6471
public byte[] contentIv() {
@@ -85,7 +92,7 @@ public static class Builder {
8592

8693
private EncryptedDataKey _encryptedDataKey;
8794
private String _encryptedDataKeyAlgorithm;
88-
private Map<String, String> _encryptedDataKeyContext;
95+
private Map<String, String> _encryptionContextOrMatDesc;
8996

9097
private byte[] _contentIv;
9198
private String _contentCipher;
@@ -111,8 +118,8 @@ public Builder encryptedDataKeyAlgorithm(String encryptedDataKeyAlgorithm) {
111118
return this;
112119
}
113120

114-
public Builder encryptedDataKeyContext(Map<String, String> encryptedDataKeyContext) {
115-
_encryptedDataKeyContext = Collections.unmodifiableMap(encryptedDataKeyContext);
121+
public Builder encryptionContextOrMatDesc(Map<String, String> encryptionContextOrMatDesc) {
122+
_encryptionContextOrMatDesc = Collections.unmodifiableMap(encryptionContextOrMatDesc);
116123
return this;
117124
}
118125

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

Lines changed: 11 additions & 6 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

@@ -136,8 +137,8 @@ private ContentMetadata readFromMap(Map<String, String> metadata, GetObjectRespo
136137
.keyProviderInfo(keyProviderInfo.getBytes(StandardCharsets.UTF_8))
137138
.build();
138139

139-
// Get encrypted data key encryption context
140-
final Map<String, String> encryptionContext = new HashMap<>();
140+
// Get encrypted data key encryption context or materials description (depending on the keyring)
141+
final Map<String, String> encryptionContextOrMatDesc = new HashMap<>();
141142
// The V2 client treats null value here as empty, do the same to avoid incompatibility
142143
String jsonEncryptionContext = metadata.getOrDefault(MetadataKeyConstants.ENCRYPTED_DATA_KEY_CONTEXT, "{}");
143144
// When the encryption context contains non-US-ASCII characters,
@@ -149,7 +150,7 @@ private ContentMetadata readFromMap(Map<String, String> metadata, GetObjectRespo
149150
JsonNode objectNode = parser.parse(decodedJsonEncryptionContext);
150151

151152
for (Map.Entry<String, JsonNode> entry : objectNode.asObject().entrySet()) {
152-
encryptionContext.put(entry.getKey(), entry.getValue().asString());
153+
encryptionContextOrMatDesc.put(entry.getKey(), entry.getValue().asString());
153154
}
154155
} catch (Exception e) {
155156
throw new RuntimeException(e);
@@ -161,7 +162,7 @@ private ContentMetadata readFromMap(Map<String, String> metadata, GetObjectRespo
161162
return ContentMetadata.builder()
162163
.algorithmSuite(algorithmSuite)
163164
.encryptedDataKey(edk)
164-
.encryptedDataKeyContext(encryptionContext)
165+
.encryptionContextOrMatDesc(encryptionContextOrMatDesc)
165166
.contentIv(iv)
166167
.contentRange(contentRange)
167168
.build();
@@ -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;

0 commit comments

Comments
 (0)