Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 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

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

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:

  • one with just the public key (used in ReEncrypt) maybe called sharedKeyring
  • second with the public+private key (used to show decryption works)

this is more accurate to the workflow

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);
}

}
Loading