-
Notifications
You must be signed in to change notification settings - Fork 17
feat: reEncryptInstructionFile Implementation #478
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
Changes from all commits
b643bc8
675256f
5e41470
dc29afb
3ed8dde
b50ee55
a0bf49b
4a17e3b
1a4359d
7092675
65c2787
dd77804
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Ignore artifacts: | ||
build | ||
coverage |
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,20 +47,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 software.amazon.encryption.s3.materials.S3Keyring; | ||
|
||
|
@@ -71,6 +81,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; | ||
|
@@ -83,7 +94,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; | ||
|
@@ -99,6 +111,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; | ||
|
@@ -145,6 +160,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. | ||
|
@@ -156,7 +183,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 | ||
|
@@ -174,6 +200,102 @@ 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 | ||
* - Enforce Rotation: When enabled, ensures old keyring cannot decrypt data encrypted by new 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) { | ||
if (!_instructionFileConfig.isInstructionFilePutEnabled()) { | ||
throw new S3EncryptionClientException("Instruction file put operations must be enabled to re-encrypt instruction files"); | ||
} | ||
|
||
//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!"); | ||
} | ||
|
||
// If enforceRotation is set to true, ensure that the old keyring cannot decrypt the newly encrypted data key | ||
if (reEncryptInstructionFileRequest.enforceRotation()) { | ||
enforceRotation(encryptedMaterials, request); | ||
} | ||
|
||
//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(), reEncryptInstructionFileRequest.enforceRotation()); | ||
|
||
} | ||
|
||
private void enforceRotation(EncryptionMaterials newEncryptionMaterials, GetObjectRequest request) { | ||
try { | ||
DecryptionMaterials decryptedMaterials = this._cryptoMaterialsManager.decryptMaterials( | ||
DecryptMaterialsRequest.builder() | ||
.algorithmSuite(newEncryptionMaterials.algorithmSuite()) | ||
.encryptedDataKeys(newEncryptionMaterials.encryptedDataKeys()) | ||
.s3Request(request) | ||
.build() | ||
); | ||
} catch (S3EncryptionClientException e) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The catch won't be too general and it would fail closed. This is because we already validated the DecryptionMaterials after we decrypted the original keyring in the CMM and so in "onDecrypt" all that will be checked is whether the old keyring can decrypt the data key and if it is able to, then "key rotation" did not happen. |
||
return; | ||
} | ||
throw new S3EncryptionClientException("Re-encryption failed due to enforced rotation! Old keyring is still able to decrypt the newly encrypted data key"); | ||
} | ||
|
||
/** | ||
* See {@link S3EncryptionClient#putObject(PutObjectRequest, RequestBody)}. | ||
* <p> | ||
|
@@ -382,7 +504,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()) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: IMO,
enforce
doesn't seem right -- to me, it implies this method is taking some action to perform the rotation. But it's really just validating that the rotation was done correctly.I think
verify
orvalidate
would have been better, but it seems like a fair amount of work to change. I'll leave it up to you/Kess to decide if this rename makes sense and/or is worth the effort to change.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kess and I discussed this and we decided to keep enforceRotation since it is an optional attribute in the reEncryptInstructionFile request and customers are only interacting with it by setting the boolean value. The internal method "enforceRotation" is not being called by the client and so the API isn't being affected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant that we should change
enforce
anywhere it exists, including in the request object, since it isn't a great word to describe the effect of setting that flag. But this is fine as-is.