Skip to content

feat: ReEncryptInstructionFile Implementation #470

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
cc6e593
Added ReEncryptInstructionFileRequest class
Jun 17, 2025
effe9bb
Added the ReEncryptInstructionFileResponse class
Jun 17, 2025
8ac06bb
Merge ReEncrypt-feature branch to get latest changes for instruction …
Jun 25, 2025
8b16a23
changed S3Keyring to RawKeyring and also added getter functions
Jun 25, 2025
f501b43
cleaned up ReEncryptInstructionFileResponse class
Jun 25, 2025
d16db4d
Rough outline of reEncryptInstructionFile method (default suffix)
Jun 26, 2025
5a1f292
added support for custom instruction file suffix in getObject
Jun 30, 2025
8fb7167
added optional attribte for custom instruction file suffix in encode …
Jun 30, 2025
3ffc4b2
method overloading for putInstructionFile to be able to handle the cu…
Jun 30, 2025
6194b64
wrote 2 comprehensive test cases regarding re-encryption of instructi…
Jul 1, 2025
f6e43e5
appends a period in front of the instruction file suffix
Jul 1, 2025
5b7b3ae
verified in test case testRsaKeyringReEncryptInstructionFile that the…
Jul 1, 2025
3bbb9fb
Added examples of re-encryption of instruction files via RSA + AES ke…
Jul 3, 2025
24b804b
added Amazon copyright notice
Jul 3, 2025
4164acf
renamed ReEncryptionInstructionFileExample class to ReEncryptInstruct…
Jul 3, 2025
d45c026
Added test cases to test legacy wrapping algorithm upgrades + backwar…
Jul 7, 2025
d4cef6e
Added javadoc comments for readability
Jul 7, 2025
0e2b8ce
added more test cases to ensure build validation
Jul 7, 2025
e652052
added fix to retrieve the actual contents from the materials descript…
Jul 7, 2025
effb4ec
small fixes
Jul 7, 2025
2b6dcc8
added one small fix (eliminated space between package and imports in …
Jul 7, 2025
7c015b2
removed unused import
Jul 7, 2025
07056fc
initialized _secureRandom in builder in S3Keyring with a default value
Jul 11, 2025
2871210
Added example for standard RSA key rotation with default instruction …
Jul 11, 2025
ee4f448
Changed static constant INSTRUCTION_FILE_SUFFIX to DEFAULT_INSTRUCTIO…
Jul 11, 2025
239bc34
changed static constant INSTRUCTION_FILE_SUFFIX to DEFAULT_INSTRUCTIO…
Jul 11, 2025
5298412
renamed the getter in ContentMetadata to encryptedDataKeyMatDescOrCon…
Jul 11, 2025
0bbd9e7
Changed INSTRUCTION_FILE_SUFFIX to DEFAULT_INSTRUCTION_FILE_SUFFIX
Jul 11, 2025
3ae2fa4
refactored the encodeMetadata to be cleaner
Jul 11, 2025
3696fa9
updated getter for encryptionContext to be contentMetadata.encryptedD…
Jul 11, 2025
4e70b6a
Changed INSTRUCTION_FILE_SUFFIX to DEFAULT_INSTRUCTION_FILE_SUFFIX
Jul 11, 2025
767c58b
Made sure to modify the javadoc comments to mention that RSA keyrings…
Jul 11, 2025
9e8f502
Modified the names of all the getter functions to be lowercase
Jul 11, 2025
887ffd0
removed setting the secureRandom attribute for the Aes + RSA keyrings…
Jul 11, 2025
b6b59b6
removed setting the secureRandom attribute in the builder for AES + R…
Jul 11, 2025
8ce42fe
rennamed getter to match the newly updated one: actualContentMetadata…
Jul 11, 2025
b4c1d57
added space between all of the test cases
Jul 11, 2025
66126e0
cleaned up the reEncryptInstructionFile method + added description fo…
Jul 11, 2025
cc35216
cleaned up some of the test-cases (will continue working on this)
Jul 11, 2025
b36455a
added more test cases with RSA key rotation with default instruction …
Jul 11, 2025
7d2e3f0
Added a note to the javadoc comment for the setter for instructionFil…
Jul 11, 2025
3092657
fixed up javadoc comments
Jul 14, 2025
d1eecbf
V2 (Default) to V3 is not a legacy upgrade for wrapping algorithms. M…
Jul 14, 2025
3a5c9a6
fixed some more test cases
Jul 14, 2025
a13917a
modified simpleRsaKeyringReEncryptInstructionFileWithCustomSuffix tes…
Jul 15, 2025
dd202cf
removed unused import
Jul 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

103 changes: 100 additions & 3 deletions src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,30 @@
import software.amazon.awssdk.services.s3.model.UploadPartRequest;
import software.amazon.awssdk.services.s3.model.UploadPartResponse;
import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
import software.amazon.encryption.s3.internal.ContentMetadata;
import software.amazon.encryption.s3.internal.ContentMetadataDecodingStrategy;
import software.amazon.encryption.s3.internal.ContentMetadataEncodingStrategy;
import software.amazon.encryption.s3.internal.ConvertSDKRequests;
import software.amazon.encryption.s3.internal.GetEncryptedObjectPipeline;
import software.amazon.encryption.s3.internal.InstructionFileConfig;
import software.amazon.encryption.s3.internal.MultiFileOutputStream;
import software.amazon.encryption.s3.internal.MultipartUploadObjectPipeline;
import software.amazon.encryption.s3.internal.PutEncryptedObjectPipeline;
import software.amazon.encryption.s3.internal.ReEncryptInstructionFileRequest;
import software.amazon.encryption.s3.internal.ReEncryptInstructionFileResponse;
import software.amazon.encryption.s3.internal.UploadObjectObserver;
import software.amazon.encryption.s3.materials.AesKeyring;
import software.amazon.encryption.s3.materials.CryptographicMaterialsManager;
import software.amazon.encryption.s3.materials.DecryptMaterialsRequest;
import software.amazon.encryption.s3.materials.DecryptionMaterials;
import software.amazon.encryption.s3.materials.DefaultCryptoMaterialsManager;
import software.amazon.encryption.s3.materials.EncryptedDataKey;
import software.amazon.encryption.s3.materials.EncryptionMaterials;
import software.amazon.encryption.s3.materials.Keyring;
import software.amazon.encryption.s3.materials.KmsKeyring;
import software.amazon.encryption.s3.materials.MultipartConfiguration;
import software.amazon.encryption.s3.materials.PartialRsaKeyPair;
import software.amazon.encryption.s3.materials.RawKeyring;
import software.amazon.encryption.s3.materials.RsaKeyring;

import javax.crypto.SecretKey;
Expand All @@ -69,6 +79,7 @@
import java.security.Provider;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand All @@ -81,7 +92,8 @@
import java.util.function.Consumer;

import static software.amazon.encryption.s3.S3EncryptionClientUtilities.DEFAULT_BUFFER_SIZE_BYTES;
import static software.amazon.encryption.s3.S3EncryptionClientUtilities.INSTRUCTION_FILE_SUFFIX;

import static software.amazon.encryption.s3.S3EncryptionClientUtilities.DEFAULT_INSTRUCTION_FILE_SUFFIX;
import static software.amazon.encryption.s3.S3EncryptionClientUtilities.MAX_ALLOWED_BUFFER_SIZE_BYTES;
import static software.amazon.encryption.s3.S3EncryptionClientUtilities.MIN_ALLOWED_BUFFER_SIZE_BYTES;
import static software.amazon.encryption.s3.S3EncryptionClientUtilities.instructionFileKeysToDelete;
Expand All @@ -97,6 +109,9 @@ public class S3EncryptionClient extends DelegatingS3Client {
public static final ExecutionAttribute<Map<String, String>> ENCRYPTION_CONTEXT = new ExecutionAttribute<>("EncryptionContext");
public static final ExecutionAttribute<MultipartConfiguration> CONFIGURATION = new ExecutionAttribute<>("MultipartConfiguration");

//Used for specifying custom instruction file suffix on a per-request basis
public static final ExecutionAttribute<String> CUSTOM_INSTRUCTION_FILE_SUFFIX = new ExecutionAttribute<>("CustomInstructionFileSuffix");

private final S3Client _wrappedClient;
private final S3AsyncClient _wrappedAsyncClient;
private final CryptographicMaterialsManager _cryptoMaterialsManager;
Expand Down Expand Up @@ -143,6 +158,18 @@ public static Consumer<AwsRequestOverrideConfiguration.Builder> withAdditionalCo
builder.putExecutionAttribute(S3EncryptionClient.ENCRYPTION_CONTEXT, encryptionContext);
}

/**
* Attaches a custom instruction file suffix to a request. Must be used as a parameter to
* {@link S3Request#overrideConfiguration()} in the request.
* This allows specifying a custom suffix for the instruction file on a per-request basis.
* @param customInstructionFileSuffix the custom suffix to use for the instruction file.
* @return Consumer for use in overrideConfiguration()
*/
public static Consumer<AwsRequestOverrideConfiguration.Builder> withCustomInstructionFileSuffix(String customInstructionFileSuffix) {
return builder ->
builder.putExecutionAttribute(S3EncryptionClient.CUSTOM_INSTRUCTION_FILE_SUFFIX, customInstructionFileSuffix);
}

/**
* Attaches multipart configuration to a request. Must be used as a parameter to
* {@link S3Request#overrideConfiguration()} in the request.
Expand All @@ -154,7 +181,6 @@ public static Consumer<AwsRequestOverrideConfiguration.Builder> withAdditionalCo
builder.putExecutionAttribute(S3EncryptionClient.CONFIGURATION, multipartConfiguration);
}


/**
* Attaches encryption context and multipart configuration to a request.
* * Must be used as a parameter to
Expand All @@ -172,6 +198,77 @@ public static Consumer<AwsRequestOverrideConfiguration.Builder> withAdditionalCo
.putExecutionAttribute(S3EncryptionClient.CONFIGURATION, multipartConfiguration);
}

/**
* Re-encrypts an instruction file with a new keyring while preserving the original encrypted object in S3.
* This enables:
* 1. Key rotation by updating instruction file metadata without re-encrypting object content
* 2. Sharing encrypted objects with partners by creating new instruction files with a custom suffix using their public keys
* <p>
* Key rotation scenarios:
* - Legacy to V3: Can rotate same wrapping key from legacy wrapping algorithms to fully supported wrapping algorithms
* - Within V3: When rotating the wrapping key, the new keyring must be different from the current keyring
*
* @param reEncryptInstructionFileRequest the request containing bucket, object key, new keyring, and optional instruction file suffix
* @return ReEncryptInstructionFileResponse containing the bucket, object key, and instruction file suffix used
* @throws S3EncryptionClientException if the new keyring has the same materials description as the current one
*/
public ReEncryptInstructionFileResponse reEncryptInstructionFile(ReEncryptInstructionFileRequest reEncryptInstructionFileRequest) {
//Build request to retrieve the encrypted object and its associated instruction file
final GetObjectRequest request = GetObjectRequest.builder()
.bucket(reEncryptInstructionFileRequest.bucket())
.key(reEncryptInstructionFileRequest.key())
.build();

ResponseInputStream<GetObjectResponse> response = this.getObject(request);
ContentMetadataDecodingStrategy decodingStrategy = new ContentMetadataDecodingStrategy(_instructionFileConfig);
ContentMetadata contentMetadata = decodingStrategy.decode(request, response.response());

//Extract cryptographic parameters from the current instruction file that MUST be preserved during re-encryption
final AlgorithmSuite algorithmSuite = contentMetadata.algorithmSuite();
final EncryptedDataKey originalEncryptedDataKey = contentMetadata.encryptedDataKey();
final Map<String, String> currentKeyringMaterialsDescription = contentMetadata.encryptedDataKeyMatDescOrContext();
final byte[] iv = contentMetadata.contentIv();

//Decrypt the data key using the current keyring
DecryptionMaterials decryptedMaterials = this._cryptoMaterialsManager.decryptMaterials(
DecryptMaterialsRequest.builder()
.algorithmSuite(algorithmSuite)
.encryptedDataKeys(Collections.singletonList(originalEncryptedDataKey))
.s3Request(request)
.build()
);

final byte[] plaintextDataKey = decryptedMaterials.plaintextDataKey();

//Prepare encryption materials with the decrypted data key
EncryptionMaterials encryptionMaterials = EncryptionMaterials.builder()
.algorithmSuite(algorithmSuite)
.plaintextDataKey(plaintextDataKey)
.s3Request(request)
.build();

//Re-encrypt the data key with the new keyring while preserving other cryptographic parameters
RawKeyring newKeyring = reEncryptInstructionFileRequest.newKeyring();
EncryptionMaterials encryptedMaterials = newKeyring.onEncrypt(encryptionMaterials);

final Map<String, String> newMaterialsDescription = encryptedMaterials.materialsDescription().getMaterialsDescription();
//Validate that the new keyring has different materials description than the old keyring
if (newMaterialsDescription.equals(currentKeyringMaterialsDescription)) {
throw new S3EncryptionClientException("New keyring must have new materials description!");
}

//Create or update instruction file with the re-encrypted metadata while preserving IV
ContentMetadataEncodingStrategy encodeStrategy = new ContentMetadataEncodingStrategy(_instructionFileConfig);
encodeStrategy.encodeMetadata(encryptedMaterials, iv, PutObjectRequest.builder()
.bucket(reEncryptInstructionFileRequest.bucket())
.key(reEncryptInstructionFileRequest.key())
.build(), reEncryptInstructionFileRequest.instructionFileSuffix());

return new ReEncryptInstructionFileResponse(reEncryptInstructionFileRequest.bucket(),
reEncryptInstructionFileRequest.key(), reEncryptInstructionFileRequest.instructionFileSuffix());

}

/**
* See {@link S3EncryptionClient#putObject(PutObjectRequest, RequestBody)}.
* <p>
Expand Down Expand Up @@ -380,7 +477,7 @@ public DeleteObjectResponse deleteObject(DeleteObjectRequest deleteObjectRequest
// Delete the object
DeleteObjectResponse deleteObjectResponse = _wrappedAsyncClient.deleteObject(actualRequest).join();
// If Instruction file exists, delete the instruction file as well.
String instructionObjectKey = deleteObjectRequest.key() + INSTRUCTION_FILE_SUFFIX;
String instructionObjectKey = deleteObjectRequest.key() + DEFAULT_INSTRUCTION_FILE_SUFFIX;
_wrappedAsyncClient.deleteObject(builder -> builder
.overrideConfiguration(API_NAME_INTERCEPTOR)
.bucket(deleteObjectRequest.bucket())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/
public class S3EncryptionClientUtilities {

public static final String INSTRUCTION_FILE_SUFFIX = ".instruction";
public static final String DEFAULT_INSTRUCTION_FILE_SUFFIX = ".instruction";
public static final long MIN_ALLOWED_BUFFER_SIZE_BYTES = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherBlockSizeBytes();
public static final long MAX_ALLOWED_BUFFER_SIZE_BYTES = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherMaxContentLengthBytes();

Expand All @@ -32,7 +32,7 @@ public class S3EncryptionClientUtilities {
*/
static List<ObjectIdentifier> instructionFileKeysToDelete(final DeleteObjectsRequest request) {
return request.delete().objects().stream()
.map(o -> o.toBuilder().key(o.key() + INSTRUCTION_FILE_SUFFIX).build())
.map(o -> o.toBuilder().key(o.key() + DEFAULT_INSTRUCTION_FILE_SUFFIX).build())
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public String encryptedDataKeyAlgorithm() {
*/
@SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "False positive; underlying"
+ " implementation is immutable")
public Map<String, String> encryptedDataKeyContext() {
public Map<String, String> encryptedDataKeyMatDescOrContext() {
return _encryptionContextOrMatDesc;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
import software.amazon.encryption.s3.S3EncryptionClient;
import software.amazon.encryption.s3.S3EncryptionClientException;
import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
import software.amazon.encryption.s3.materials.EncryptedDataKey;
Expand All @@ -24,7 +25,7 @@
import java.util.Map;
import java.util.concurrent.CompletionException;

import static software.amazon.encryption.s3.S3EncryptionClientUtilities.INSTRUCTION_FILE_SUFFIX;
import static software.amazon.encryption.s3.S3EncryptionClientUtilities.DEFAULT_INSTRUCTION_FILE_SUFFIX;

public class ContentMetadataDecodingStrategy {

Expand Down Expand Up @@ -224,9 +225,13 @@ private ContentMetadata decodeFromObjectMetadata(GetObjectRequest request, GetOb
}

private ContentMetadata decodeFromInstructionFile(GetObjectRequest request, GetObjectResponse response) {
String instructionFileSuffix = request.overrideConfiguration()
.flatMap(config -> config.executionAttributes().getOptionalAttribute(S3EncryptionClient.CUSTOM_INSTRUCTION_FILE_SUFFIX))
.orElse(DEFAULT_INSTRUCTION_FILE_SUFFIX);

GetObjectRequest instructionGetObjectRequest = GetObjectRequest.builder()
.bucket(request.bucket())
.key(request.key() + INSTRUCTION_FILE_SUFFIX)
.key(request.key() + instructionFileSuffix)
.build();

ResponseInputStream<GetObjectResponse> instruction;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import java.util.HashMap;
import java.util.Map;

import static software.amazon.encryption.s3.S3EncryptionClientUtilities.DEFAULT_INSTRUCTION_FILE_SUFFIX;

public class ContentMetadataEncodingStrategy {

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

public PutObjectRequest encodeMetadata(EncryptionMaterials materials, byte[] iv, PutObjectRequest putObjectRequest) {
return encodeMetadata(materials, iv, putObjectRequest, DEFAULT_INSTRUCTION_FILE_SUFFIX);
}

public PutObjectRequest encodeMetadata(EncryptionMaterials materials, byte[] iv, PutObjectRequest putObjectRequest, String instructionFileSuffix) {
if (_instructionFileConfig.isInstructionFilePutEnabled()) {
final String metadataString = metadataToString(materials, iv);
_instructionFileConfig.putInstructionFile(putObjectRequest, metadataString);
_instructionFileConfig.putInstructionFile(putObjectRequest, metadataString, instructionFileSuffix);
// the original request object is returned as-is
return putObjectRequest;
} else {
Map<String, String> newMetadata = addMetadataToMap(putObjectRequest.metadata(), materials, iv);
return putObjectRequest.toBuilder()
.metadata(newMetadata)
.build();
.metadata(newMetadata)
.build();
}
}

Expand All @@ -51,6 +57,7 @@ public CreateMultipartUploadRequest encodeMetadata(EncryptionMaterials materials
.build();
}
}

private String metadataToString(EncryptionMaterials materials, byte[] iv) {
// this is just the metadata map serialized as JSON
// so first get the Map
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ private DecryptionMaterials prepareMaterialsFromRequest(final GetObjectRequest g
.s3Request(getObjectRequest)
.algorithmSuite(algorithmSuite)
.encryptedDataKeys(encryptedDataKeys)
.encryptionContext(contentMetadata.encryptedDataKeyContext())
.encryptionContext(contentMetadata.encryptedDataKeyMatDescOrContext())
.ciphertextLength(getObjectResponse.contentLength())
.contentRange(getObjectRequest.range())
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
import java.util.HashMap;
import java.util.Map;

import static software.amazon.encryption.s3.S3EncryptionClientUtilities.INSTRUCTION_FILE_SUFFIX;

import static software.amazon.encryption.s3.S3EncryptionClientUtilities.DEFAULT_INSTRUCTION_FILE_SUFFIX;
import static software.amazon.encryption.s3.internal.MetadataKeyConstants.INSTRUCTION_FILE;

/**
Expand Down Expand Up @@ -49,6 +50,10 @@ boolean isInstructionFilePutEnabled() {
}

PutObjectResponse putInstructionFile(PutObjectRequest request, String instructionFileContent) {
return putInstructionFile(request, instructionFileContent, DEFAULT_INSTRUCTION_FILE_SUFFIX);
}

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

// In a future release, non-default suffixes will be supported.
// Use toBuilder to keep all other fields the same as the actual request
final PutObjectRequest instPutRequest = request.toBuilder()
.key(request.key() + INSTRUCTION_FILE_SUFFIX)
.metadata(instFileMetadata)
.build();
.key(request.key() + instructionFileSuffix)
.metadata(instFileMetadata)
.build();
switch (_clientType) {
case SYNCHRONOUS:
return _s3Client.putObject(instPutRequest, RequestBody.fromString(instructionFileContent));
Expand Down
Loading