generated from amazon-archives/__template_Apache-2.0
- Notifications
You must be signed in to change notification settings - Fork 18
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
akareddy04 merged 46 commits into aniravk/ReEncrypt-feature from aniravk/reEncryptInstructionFileAPI Jul 15, 2025
Merged
Changes from 22 commits
Commits
Show all changes
46 commits Select commit Hold shift + click to select a range
cc6e593 Added ReEncryptInstructionFileRequest class
effe9bb Added the ReEncryptInstructionFileResponse class
8ac06bb Merge ReEncrypt-feature branch to get latest changes for instruction …
8b16a23 changed S3Keyring to RawKeyring and also added getter functions
f501b43 cleaned up ReEncryptInstructionFileResponse class
d16db4d Rough outline of reEncryptInstructionFile method (default suffix)
5a1f292 added support for custom instruction file suffix in getObject
8fb7167 added optional attribte for custom instruction file suffix in encode …
3ffc4b2 method overloading for putInstructionFile to be able to handle the cu…
6194b64 wrote 2 comprehensive test cases regarding re-encryption of instructi…
f6e43e5 appends a period in front of the instruction file suffix
5b7b3ae verified in test case testRsaKeyringReEncryptInstructionFile that the…
3bbb9fb Added examples of re-encryption of instruction files via RSA + AES ke…
24b804b added Amazon copyright notice
4164acf renamed ReEncryptionInstructionFileExample class to ReEncryptInstruct…
d45c026 Added test cases to test legacy wrapping algorithm upgrades + backwar…
d4cef6e Added javadoc comments for readability
0e2b8ce added more test cases to ensure build validation
e652052 added fix to retrieve the actual contents from the materials descript…
effb4ec small fixes
2b6dcc8 added one small fix (eliminated space between package and imports in …
7c015b2 removed unused import
07056fc initialized _secureRandom in builder in S3Keyring with a default value
2871210 Added example for standard RSA key rotation with default instruction …
ee4f448 Changed static constant INSTRUCTION_FILE_SUFFIX to DEFAULT_INSTRUCTIO…
239bc34 changed static constant INSTRUCTION_FILE_SUFFIX to DEFAULT_INSTRUCTIO…
5298412 renamed the getter in ContentMetadata to encryptedDataKeyMatDescOrCon…
0bbd9e7 Changed INSTRUCTION_FILE_SUFFIX to DEFAULT_INSTRUCTION_FILE_SUFFIX
3ae2fa4 refactored the encodeMetadata to be cleaner
3696fa9 updated getter for encryptionContext to be contentMetadata.encryptedD…
4e70b6a Changed INSTRUCTION_FILE_SUFFIX to DEFAULT_INSTRUCTION_FILE_SUFFIX
767c58b Made sure to modify the javadoc comments to mention that RSA keyrings…
9e8f502 Modified the names of all the getter functions to be lowercase
887ffd0 removed setting the secureRandom attribute for the Aes + RSA keyrings…
b6b59b6 removed setting the secureRandom attribute in the builder for AES + R…
8ce42fe rennamed getter to match the newly updated one: actualContentMetadata…
b4c1d57 added space between all of the test cases
66126e0 cleaned up the reEncryptInstructionFile method + added description fo…
cc35216 cleaned up some of the test-cases (will continue working on this)
b36455a added more test cases with RSA key rotation with default instruction …
7d2e3f0 Added a note to the javadoc comment for the setter for instructionFil…
3092657 fixed up javadoc comments
d1eecbf V2 (Default) to V3 is not a legacy upgrade for wrapping algorithms. M…
3a5c9a6 fixed some more test cases
a13917a modified simpleRsaKeyringReEncryptInstructionFileWithCustomSuffix tes…
dd202cf removed unused import
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
300 changes: 300 additions & 0 deletions 300 ...examples/java/software/amazon/encryption/s3/examples/ReEncryptInstructionFileExample.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,300 @@ | ||
| // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| package software.amazon.encryption.s3.examples; | ||
| | ||
| import software.amazon.awssdk.core.ResponseBytes; | ||
| import software.amazon.awssdk.core.sync.RequestBody; | ||
| import software.amazon.awssdk.services.s3.S3Client; | ||
| import software.amazon.awssdk.services.s3.model.GetObjectResponse; | ||
| import software.amazon.encryption.s3.S3EncryptionClient; | ||
| import software.amazon.encryption.s3.S3EncryptionClientException; | ||
| import software.amazon.encryption.s3.internal.InstructionFileConfig; | ||
| import software.amazon.encryption.s3.internal.ReEncryptInstructionFileRequest; | ||
| import software.amazon.encryption.s3.internal.ReEncryptInstructionFileResponse; | ||
| import software.amazon.encryption.s3.materials.AesKeyring; | ||
| import software.amazon.encryption.s3.materials.MaterialsDescription; | ||
| import software.amazon.encryption.s3.materials.PartialRsaKeyPair; | ||
| import software.amazon.encryption.s3.materials.RsaKeyring; | ||
| | ||
| import javax.crypto.KeyGenerator; | ||
| import javax.crypto.SecretKey; | ||
| import java.security.KeyPair; | ||
| import java.security.KeyPairGenerator; | ||
| import java.security.NoSuchAlgorithmException; | ||
| import java.security.PrivateKey; | ||
| import java.security.PublicKey; | ||
| import java.security.SecureRandom; | ||
| | ||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
| import static software.amazon.encryption.s3.S3EncryptionClient.withCustomInstructionFileSuffix; | ||
| import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.appendTestSuffix; | ||
| import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.deleteObject; | ||
| | ||
| public class ReEncryptInstructionFileExample { | ||
| | ||
| /** | ||
| * Generates a 256-bit AES key for encryption/decryption operations. | ||
| * | ||
| * @return A SecretKey instance for AES operations | ||
| * @throws NoSuchAlgorithmException if AES algorithm is not available | ||
| */ | ||
| private static SecretKey generateAesKey() throws NoSuchAlgorithmException { | ||
| KeyGenerator keyGen = KeyGenerator.getInstance("AES"); | ||
| keyGen.init(256); | ||
| return keyGen.generateKey(); | ||
| } | ||
| | ||
| /** | ||
| * Generates a 2048-bit RSA key pair for encryption/decryption operations. | ||
| * | ||
| * @return A KeyPair instance for RSA operations | ||
| * @throws NoSuchAlgorithmException if RSA algorithm is not available | ||
| */ | ||
| private static KeyPair generateRsaKeyPair() throws NoSuchAlgorithmException { | ||
| KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); | ||
| keyPairGen.initialize(2048); | ||
| return keyPairGen.generateKeyPair(); | ||
| } | ||
| | ||
| public static void main(final String[] args) throws NoSuchAlgorithmException { | ||
| final String bucket = args[0]; | ||
| simpleAesKeyringReEncryptInstructionFile(bucket); | ||
| simpleRsaKeyringReEncryptInstructionFile(bucket); | ||
| } | ||
| | ||
| /** | ||
| * This example demonstrates re-encrypting the encrypted data key in an instruction file with a new AES wrapping key. | ||
| * The other cryptographic parameters in the instruction file such as the IV and wrapping algorithm remain unchanged. | ||
| * | ||
| * @param bucket The name of the Amazon S3 bucket to perform operations on. | ||
| * @throws NoSuchAlgorithmException if AES algorithm is not available | ||
| */ | ||
| public static void simpleAesKeyringReEncryptInstructionFile(final String bucket) throws NoSuchAlgorithmException { | ||
| // Set up the S3 object key and content to be encrypted | ||
| final String objectKey = appendTestSuffix("aes-re-encrypt-instruction-file-test"); | ||
| final String input = "Testing re-encryption of instruction file with AES Keyring"; | ||
| | ||
| // Generate the original AES key for initial encryption | ||
| SecretKey originalAesKey = generateAesKey(); | ||
| | ||
| // Create the original AES keyring with materials description | ||
| AesKeyring oldKeyring = AesKeyring.builder() | ||
| .wrappingKey(originalAesKey) | ||
| .secureRandom(new SecureRandom()) | ||
| .materialsDescription(MaterialsDescription.builder() | ||
| .put("version", "1.0") | ||
| .put("rotated", "no") | ||
| .build()) | ||
| .build(); | ||
| | ||
| // Create a default S3 client for instruction file operations | ||
| S3Client wrappedClient = S3Client.create(); | ||
| | ||
| // Create the S3 Encryption Client with instruction file support enabled | ||
| // The client can perform both putObject and getObject operations using the original AES key | ||
| S3EncryptionClient originalClient = S3EncryptionClient.builder() | ||
| .keyring(oldKeyring) | ||
| .instructionFileConfig(InstructionFileConfig.builder() | ||
| .instructionFileClient(wrappedClient) | ||
| .enableInstructionFilePutObject(true) | ||
| .build()) | ||
| .build(); | ||
| | ||
| // Upload both the encrypted object and instruction file to the specified bucket in S3 | ||
| originalClient.putObject(builder -> builder | ||
| .bucket(bucket) | ||
| .key(objectKey) | ||
| .build(), RequestBody.fromString(input)); | ||
| | ||
| // Generate a new AES key for re-encryption (rotating wrapping key) | ||
| SecretKey newAesKey = generateAesKey(); | ||
| | ||
| // Create a new keyring with the new AES key and updated materials description | ||
| AesKeyring newKeyring = AesKeyring.builder() | ||
| .wrappingKey(newAesKey) | ||
| .secureRandom(new SecureRandom()) | ||
| .materialsDescription(MaterialsDescription.builder() | ||
| .put("version", "2.0") | ||
| .put("rotated", "yes") | ||
| .build()) | ||
| .build(); | ||
| | ||
| // Create the re-encryption of instruction file request to re-encrypt the encrypted data key with the new wrapping key | ||
| // This updates the instruction file without touching the encrypted object | ||
| ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = ReEncryptInstructionFileRequest.builder() | ||
| .bucket(bucket) | ||
| .key(objectKey) | ||
| .newKeyring(newKeyring) | ||
| .build(); | ||
| | ||
| // Perform the re-encryption of the instruction file | ||
| ReEncryptInstructionFileResponse response = originalClient.reEncryptInstructionFile(reEncryptInstructionFileRequest); | ||
| | ||
| // Verify that the original client can no longer decrypt the object | ||
| // This proves that the instruction file has been successfully re-encrypted | ||
| try { | ||
| originalClient.getObjectAsBytes(builder -> builder | ||
| .bucket(bucket) | ||
| .key(objectKey) | ||
| .build()); | ||
| throw new RuntimeException("Original client should not be able to decrypt the object in S3 post re-encryption of instruction file!"); | ||
| } catch (S3EncryptionClientException e) { | ||
| assertTrue(e.getMessage().contains("Unable to AES/GCM unwrap")); | ||
| } | ||
| | ||
| // Create a new client with the rotated AES key | ||
| S3EncryptionClient newClient = S3EncryptionClient.builder() | ||
| .keyring(newKeyring) | ||
| .instructionFileConfig(InstructionFileConfig.builder() | ||
| .instructionFileClient(wrappedClient) | ||
| .enableInstructionFilePutObject(true) | ||
| .build()) | ||
| .build(); | ||
| | ||
| // Verify that the new client can successfully decrypt the object | ||
| // This proves that the instruction file has been successfully re-encrypted | ||
| ResponseBytes<GetObjectResponse> decryptedObject = newClient.getObjectAsBytes(builder -> builder | ||
| .bucket(bucket) | ||
| .key(objectKey) | ||
| .build()); | ||
| | ||
| // Assert that the decrypted object's content matches the original input | ||
| assertEquals(input, decryptedObject.asUtf8String()); | ||
| | ||
| // Call deleteObject to delete the object from given S3 Bucket | ||
| deleteObject(bucket, objectKey, originalClient); | ||
| } | ||
| | ||
| /** | ||
| * This example demonstrates generating a custom instruction file to enable access to encrypted object by a third party. | ||
| * This enables secure sharing of encrypted objects without sharing private keys. | ||
| * | ||
| * @param bucket The name of the Amazon S3 bucket to perform operations on. | ||
| * @throws NoSuchAlgorithmException if RSA algorithm is not available | ||
| */ | ||
| public static void simpleRsaKeyringReEncryptInstructionFile(final String bucket) throws NoSuchAlgorithmException { | ||
| // Set up the S3 object key and content to be encrypted | ||
| final String objectKey = appendTestSuffix("rsa-re-encrypt-instruction-file-test"); | ||
| final String input = "Testing re-encryption of instruction file with RSA Keyring"; | ||
| | ||
| // Generate RSA key pair for the original client | ||
| KeyPair clientRsaKeyPair = generateRsaKeyPair(); | ||
| PublicKey clientPublicKey = clientRsaKeyPair.getPublic(); | ||
| PrivateKey clientPrivateKey = clientRsaKeyPair.getPrivate(); | ||
| | ||
| // Create a partial RSA key pair for the client's keyring | ||
| PartialRsaKeyPair clientPartialRsaKeyPair = PartialRsaKeyPair.builder() | ||
| .publicKey(clientPublicKey) | ||
| .privateKey(clientPrivateKey) | ||
| .build(); | ||
| | ||
| // Create the client's RSA keyring with materials description | ||
| RsaKeyring clientKeyring = RsaKeyring.builder() | ||
| .wrappingKeyPair(clientPartialRsaKeyPair) | ||
| .secureRandom(new SecureRandom()) | ||
| .materialsDescription(MaterialsDescription.builder() | ||
| .put("isOwner", "yes") | ||
| .put("access-level", "admin") | ||
| .build()) | ||
| .build(); | ||
| | ||
| // Create a default S3 client for instruction file operations | ||
| S3Client wrappedClient = S3Client.create(); | ||
| | ||
| // Create the S3 Encryption Client with instruction file support enabled | ||
| // The client can perform both putObject and getObject operations using RSA keyring | ||
| S3EncryptionClient client = S3EncryptionClient.builder() | ||
| .keyring(clientKeyring) | ||
| .instructionFileConfig(InstructionFileConfig.builder() | ||
| .instructionFileClient(wrappedClient) | ||
| .enableInstructionFilePutObject(true) | ||
| .build()) | ||
| .build(); | ||
| | ||
| // Upload both the encrypted object and instruction file to the specified bucket in S3 | ||
| client.putObject(builder -> builder | ||
| .bucket(bucket) | ||
| .key(objectKey) | ||
| .build(), RequestBody.fromString(input)); | ||
| | ||
| // Generate a new RSA key pair for the third party customer | ||
| KeyPair thirdPartyKeyPair = generateRsaKeyPair(); | ||
| PublicKey thirdPartyPublicKey = thirdPartyKeyPair.getPublic(); | ||
| PrivateKey thirdPartyPrivateKey = thirdPartyKeyPair.getPrivate(); | ||
| | ||
| // Create a partial RSA key pair for the third party's keyring | ||
| PartialRsaKeyPair thirdPartyPartialRsaKeyPair = PartialRsaKeyPair.builder() | ||
| .publicKey(thirdPartyPublicKey) | ||
| .privateKey(thirdPartyPrivateKey) | ||
| .build(); | ||
| | ||
| // Create the third party's RSA keyring with updated materials description | ||
| RsaKeyring thirdPartyKeyring = RsaKeyring.builder() | ||
| .wrappingKeyPair(thirdPartyPartialRsaKeyPair) | ||
| .secureRandom(new SecureRandom()) | ||
| .materialsDescription(MaterialsDescription.builder() | ||
| .put("isOwner", "no") | ||
| .put("access-level", "user") | ||
| .build()) | ||
| .build(); | ||
| | ||
| // Create the re-encryption request that will generate a new instruction file specifically for third party access | ||
| // This new instruction file will use a custom suffix and contain the data key encrypted with the third party's public key | ||
| ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = ReEncryptInstructionFileRequest.builder() | ||
| .bucket(bucket) | ||
| .key(objectKey) | ||
| .instructionFileSuffix("third-party-access-instruction-file") // Custom instruction file suffix for third party | ||
| .newKeyring(thirdPartyKeyring) | ||
| .build(); | ||
| | ||
| // Perform the re-encryption operation to create the new instruction file | ||
| // This creates a new instruction file without modifying the original encrypted object | ||
| ReEncryptInstructionFileResponse reEncryptInstructionFileResponse = client.reEncryptInstructionFile(reEncryptInstructionFileRequest); | ||
| | ||
| // Create the third party's S3 Encryption Client | ||
| S3EncryptionClient thirdPartyClient = S3EncryptionClient.builder() | ||
| .keyring(thirdPartyKeyring) | ||
| .secureRandom(new SecureRandom()) | ||
| .instructionFileConfig(InstructionFileConfig.builder() | ||
| .instructionFileClient(wrappedClient) | ||
| .enableInstructionFilePutObject(true) | ||
| .build()) | ||
| .build(); | ||
| | ||
| // Verify that the original client can still decrypt the object in the specified bucket in S3 using the default instruction file | ||
| ResponseBytes<GetObjectResponse> clientDecryptedObject = client.getObjectAsBytes(builder -> builder | ||
| .bucket(bucket) | ||
| .key(objectKey) | ||
| .build()); | ||
| | ||
| // Assert that the decrypted object's content matches the original input | ||
| assertEquals(input, clientDecryptedObject.asUtf8String()); | ||
| | ||
| // Verify that the third party cannot decrypt the object in the specified bucket in S3 using the default instruction file | ||
| try { | ||
| ResponseBytes<GetObjectResponse> thirdPartyDecryptObject = thirdPartyClient.getObjectAsBytes(builder -> builder | ||
| .bucket(bucket) | ||
| .key(objectKey) | ||
| .build()); | ||
| throw new RuntimeException("Third party client should not be able to decrypt the object in S3 using the default instruction file!"); | ||
| } catch (S3EncryptionClientException e) { | ||
| assertTrue(e.getMessage().contains("Unable to RSA-OAEP-SHA1 unwrap")); | ||
| } | ||
| | ||
| // Verify that the third party can decrypt the object in the specified bucket in S3 using their custom instruction file | ||
| // This demonstrates successful secure sharing of encrypted data | ||
| ResponseBytes<GetObjectResponse> thirdPartyDecryptedObject = thirdPartyClient.getObjectAsBytes(builder -> builder | ||
| .bucket(bucket) | ||
| .key(objectKey) | ||
| .overrideConfiguration(withCustomInstructionFileSuffix(".third-party-access-instruction-file")) | ||
| .build()); | ||
| | ||
| // Assert that the decrypted object's content matches the original input | ||
| assertEquals(input, thirdPartyDecryptedObject.asUtf8String()); | ||
| | ||
| // Call deleteObject to delete the object from given S3 Bucket | ||
| deleteObject(bucket, objectKey, client); | ||
| } | ||
| | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit. This suggestion is invalid because no changes were made to the code. Suggestions cannot be applied while the pull request is closed. Suggestions cannot be applied while viewing a subset of changes. Only one suggestion per line can be applied in a batch. Add this suggestion to a batch that can be applied as a single commit. Applying suggestions on deleted lines is not supported. You must change the existing code in this line in order to create a valid suggestion. Outdated suggestions cannot be applied. This suggestion has been applied or marked resolved. Suggestions cannot be applied from pending reviews. Suggestions cannot be applied on multi-line comments. Suggestions cannot be applied while the pull request is queued to merge. Suggestion cannot be applied right now. Please check back later.
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.
this is a bit nitty but I think worth it:
we should create two separate keyrings using the third party's key:
sharedKeyringthis is more accurate to the workflow