diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..1b8ac8894 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +# Ignore artifacts: +build +coverage diff --git a/src/examples/java/software/amazon/encryption/s3/examples/ReEncryptInstructionFileExample.java b/src/examples/java/software/amazon/encryption/s3/examples/ReEncryptInstructionFileExample.java new file mode 100644 index 000000000..a29aa33b3 --- /dev/null +++ b/src/examples/java/software/amazon/encryption/s3/examples/ReEncryptInstructionFileExample.java @@ -0,0 +1,512 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.encryption.s3.examples; + +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; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +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; + +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); + simpleRsaKeyringReEncryptInstructionFileWithCustomSuffix(bucket); + } + + /** + * This example demonstrates re-encrypting the encrypted data key in an instruction file with a new AES wrapping key. + * + * @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) + .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) + .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) + .enforceRotation(true) + .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 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 re-encrypting the encrypted data key in an instruction file with a new RSA wrapping key. + * + * @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 the original RSA key pair for initial encryption + KeyPair originalRsaKeyPair = generateRsaKeyPair(); + PublicKey originalPublicKey = originalRsaKeyPair.getPublic(); + PrivateKey originalPrivateKey = originalRsaKeyPair.getPrivate(); + + // Create a partial RSA key pair for the original keyring + PartialRsaKeyPair originalPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(originalPublicKey) + .privateKey(originalPrivateKey) + .build(); + + // Create the original RSA keyring with materials description + RsaKeyring originalKeyring = RsaKeyring + .builder() + .wrappingKeyPair(originalPartialRsaKeyPair) + .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 RSA keyring + S3EncryptionClient originalClient = S3EncryptionClient + .builder() + .keyring(originalKeyring) + .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 RSA key pair for the new RSA keyring + KeyPair newKeyPair = generateRsaKeyPair(); + PublicKey newPublicKey = newKeyPair.getPublic(); + PrivateKey newPrivateKey = newKeyPair.getPrivate(); + + // Create a partial RSA key pair for the new RSA keyring + PartialRsaKeyPair newPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(newPublicKey) + .privateKey(newPrivateKey) + .build(); + + // Create the new RSA keyring with updated materials description + RsaKeyring newKeyring = RsaKeyring + .builder() + .wrappingKeyPair(newPartialRsaKeyPair) + .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) + .enforceRotation(true) + .build(); + + // Perform the re-encryption of the instruction file + ReEncryptInstructionFileResponse reEncryptInstructionFileResponse = + 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 RSA-OAEP-SHA1 unwrap")); + } + + // Create a new client with the rotated RSA 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 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 simpleRsaKeyringReEncryptInstructionFileWithCustomSuffix( + 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-with-custom-suffix" + ); + 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) + .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 decryption keyring + PartialRsaKeyPair thirdPartyPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(thirdPartyPublicKey) + .privateKey(thirdPartyPrivateKey) + .build(); + + // Create RSA keyring with third party's public key and updated materials description for re-encryption request + RsaKeyring sharedKeyring = RsaKeyring + .builder() + .wrappingKeyPair( + PartialRsaKeyPair.builder().publicKey(thirdPartyPublicKey).build() + ) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "no") + .put("access-level", "user") + .build() + ) + .build(); + + // Create RSA keyring with third party's public and private keys for decryption purposes with updated materials description + RsaKeyring thirdPartyKeyring = RsaKeyring + .builder() + .wrappingKeyPair(thirdPartyPartialRsaKeyPair) + .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(sharedKeyring) + .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) + .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 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 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 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); + } +} diff --git a/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java b/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java index f00e69a83..8b44bcda3 100644 --- a/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java +++ b/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java @@ -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> ENCRYPTION_CONTEXT = new ExecutionAttribute<>("EncryptionContext"); public static final ExecutionAttribute CONFIGURATION = new ExecutionAttribute<>("MultipartConfiguration"); + //Used for specifying custom instruction file suffix on a per-request basis + public static final ExecutionAttribute 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 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 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 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 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 + *

+ * 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 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 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 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) { + 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)}. *

@@ -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()) diff --git a/src/main/java/software/amazon/encryption/s3/S3EncryptionClientUtilities.java b/src/main/java/software/amazon/encryption/s3/S3EncryptionClientUtilities.java index 3abc60ab0..3d45849df 100644 --- a/src/main/java/software/amazon/encryption/s3/S3EncryptionClientUtilities.java +++ b/src/main/java/software/amazon/encryption/s3/S3EncryptionClientUtilities.java @@ -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(); @@ -32,7 +32,7 @@ public class S3EncryptionClientUtilities { */ static List 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()); } } diff --git a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadata.java b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadata.java index 270258f15..4d61a2248 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadata.java +++ b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadata.java @@ -15,7 +15,13 @@ public class ContentMetadata { private final EncryptedDataKey _encryptedDataKey; private final String _encryptedDataKeyAlgorithm; - private final Map _encryptedDataKeyContext; + + /** + * This field stores either encryption context or material description. + * We use a single field to store both in order to maintain backwards + * compatibility with V2, which treated both as the same. + */ + private final Map _encryptionContextOrMatDesc; private final byte[] _contentIv; private final String _contentCipher; @@ -27,7 +33,7 @@ private ContentMetadata(Builder builder) { _encryptedDataKey = builder._encryptedDataKey; _encryptedDataKeyAlgorithm = builder._encryptedDataKeyAlgorithm; - _encryptedDataKeyContext = builder._encryptedDataKeyContext; + _encryptionContextOrMatDesc = builder._encryptionContextOrMatDesc; _contentIv = builder._contentIv; _contentCipher = builder._contentCipher; @@ -51,14 +57,15 @@ public String encryptedDataKeyAlgorithm() { return _encryptedDataKeyAlgorithm; } + /** * Note that the underlying implementation uses a Collections.unmodifiableMap which is * immutable. */ @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "False positive; underlying" + " implementation is immutable") - public Map encryptedDataKeyContext() { - return _encryptedDataKeyContext; + public Map encryptedDataKeyMatDescOrContext() { + return _encryptionContextOrMatDesc; } public byte[] contentIv() { @@ -85,7 +92,7 @@ public static class Builder { private EncryptedDataKey _encryptedDataKey; private String _encryptedDataKeyAlgorithm; - private Map _encryptedDataKeyContext; + private Map _encryptionContextOrMatDesc; private byte[] _contentIv; private String _contentCipher; @@ -111,8 +118,8 @@ public Builder encryptedDataKeyAlgorithm(String encryptedDataKeyAlgorithm) { return this; } - public Builder encryptedDataKeyContext(Map encryptedDataKeyContext) { - _encryptedDataKeyContext = Collections.unmodifiableMap(encryptedDataKeyContext); + public Builder encryptionContextOrMatDesc(Map encryptionContextOrMatDesc) { + _encryptionContextOrMatDesc = Collections.unmodifiableMap(encryptionContextOrMatDesc); return this; } diff --git a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java index 0ab055873..3d1e4edd9 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java +++ b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java @@ -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; @@ -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 { @@ -136,8 +137,8 @@ private ContentMetadata readFromMap(Map metadata, GetObjectRespo .keyProviderInfo(keyProviderInfo.getBytes(StandardCharsets.UTF_8)) .build(); - // Get encrypted data key encryption context - final Map encryptionContext = new HashMap<>(); + // Get encrypted data key encryption context or materials description (depending on the keyring) + final Map encryptionContextOrMatDesc = new HashMap<>(); // The V2 client treats null value here as empty, do the same to avoid incompatibility String jsonEncryptionContext = metadata.getOrDefault(MetadataKeyConstants.ENCRYPTED_DATA_KEY_CONTEXT, "{}"); // When the encryption context contains non-US-ASCII characters, @@ -149,7 +150,7 @@ private ContentMetadata readFromMap(Map metadata, GetObjectRespo JsonNode objectNode = parser.parse(decodedJsonEncryptionContext); for (Map.Entry entry : objectNode.asObject().entrySet()) { - encryptionContext.put(entry.getKey(), entry.getValue().asString()); + encryptionContextOrMatDesc.put(entry.getKey(), entry.getValue().asString()); } } catch (Exception e) { throw new RuntimeException(e); @@ -161,7 +162,7 @@ private ContentMetadata readFromMap(Map metadata, GetObjectRespo return ContentMetadata.builder() .algorithmSuite(algorithmSuite) .encryptedDataKey(edk) - .encryptedDataKeyContext(encryptionContext) + .encryptionContextOrMatDesc(encryptionContextOrMatDesc) .contentIv(iv) .contentRange(contentRange) .build(); @@ -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 instruction; diff --git a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataEncodingStrategy.java b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataEncodingStrategy.java index cf4e86fa7..dd7eb3e00 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataEncodingStrategy.java +++ b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataEncodingStrategy.java @@ -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(); @@ -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 newMetadata = addMetadataToMap(putObjectRequest.metadata(), materials, iv); return putObjectRequest.toBuilder() - .metadata(newMetadata) - .build(); + .metadata(newMetadata) + .build(); } } @@ -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 @@ -80,11 +87,16 @@ private Map addMetadataToMap(Map map, Encryption try (JsonWriter jsonWriter = JsonWriter.create()) { jsonWriter.writeStartObject(); - for (Map.Entry entry : materials.encryptionContext().entrySet()) { - jsonWriter.writeFieldName(entry.getKey()).writeValue(entry.getValue()); + if (!materials.encryptionContext().isEmpty() && materials.materialsDescription().isEmpty()) { + for (Map.Entry entry : materials.encryptionContext().entrySet()) { + jsonWriter.writeFieldName(entry.getKey()).writeValue(entry.getValue()); + } + } else if (materials.encryptionContext().isEmpty() && !materials.materialsDescription().isEmpty()) { + for (Map.Entry entry : materials.materialsDescription().entrySet()) { + jsonWriter.writeFieldName(entry.getKey()).writeValue(entry.getValue()); + } } jsonWriter.writeEndObject(); - String jsonEncryptionContext = new String(jsonWriter.getBytes(), StandardCharsets.UTF_8); metadata.put(MetadataKeyConstants.ENCRYPTED_DATA_KEY_CONTEXT, jsonEncryptionContext); } catch (JsonWriter.JsonGenerationException e) { diff --git a/src/main/java/software/amazon/encryption/s3/internal/GetEncryptedObjectPipeline.java b/src/main/java/software/amazon/encryption/s3/internal/GetEncryptedObjectPipeline.java index b0805a6ac..bc3563688 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/GetEncryptedObjectPipeline.java +++ b/src/main/java/software/amazon/encryption/s3/internal/GetEncryptedObjectPipeline.java @@ -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(); diff --git a/src/main/java/software/amazon/encryption/s3/internal/InstructionFileConfig.java b/src/main/java/software/amazon/encryption/s3/internal/InstructionFileConfig.java index 59cccc788..fe89287fc 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/InstructionFileConfig.java +++ b/src/main/java/software/amazon/encryption/s3/internal/InstructionFileConfig.java @@ -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; /** @@ -44,11 +45,15 @@ public enum InstructionFileClientType { ASYNC } - boolean isInstructionFilePutEnabled() { + public boolean isInstructionFilePutEnabled() { return _enableInstructionFilePut; } 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) { @@ -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)); diff --git a/src/main/java/software/amazon/encryption/s3/internal/ReEncryptInstructionFileRequest.java b/src/main/java/software/amazon/encryption/s3/internal/ReEncryptInstructionFileRequest.java new file mode 100644 index 000000000..a1fdd5bbc --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/internal/ReEncryptInstructionFileRequest.java @@ -0,0 +1,175 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.encryption.s3.internal; + +import static software.amazon.encryption.s3.S3EncryptionClientUtilities.DEFAULT_INSTRUCTION_FILE_SUFFIX; + +import software.amazon.encryption.s3.S3EncryptionClientException; +import software.amazon.encryption.s3.materials.RawKeyring; +import software.amazon.encryption.s3.materials.RsaKeyring; + +/** + * Request object for re-encrypting instruction files in S3. + * This request supports re-encryption operations using either AES or RSA keyrings. + * For AES keyrings, only the default instruction file suffix is supported. + * For RSA keyrings, both the default and custom instruction file suffixes are supported. + */ +public class ReEncryptInstructionFileRequest { + + private final String bucket; + private final String key; + private final RawKeyring newKeyring; + private final String instructionFileSuffix; + private final boolean enforceRotation; + + private ReEncryptInstructionFileRequest(Builder builder) { + bucket = builder.bucket; + key = builder.key; + newKeyring = builder.newKeyring; + instructionFileSuffix = builder.instructionFileSuffix; + enforceRotation = builder.enforceRotation; + } + + /** + * @return the S3 bucket name that contains the encrypted object and instruction file to re-encrypt + */ + public String bucket() { + return bucket; + } + + /** + * @return the S3 object key of the encrypted object whose instruction file will be re-encrypted + */ + public String key() { + return key; + } + + /** + * @return the new keyring (AES or RSA) that will be used to re-encrypt the instruction file + */ + public RawKeyring newKeyring() { + return newKeyring; + } + + /** + * @return the suffix to use for the instruction file. The default instruction file suffix is ".instruction" + */ + public String instructionFileSuffix() { + return instructionFileSuffix; + } + + /** + * @return whether to enforce rotation for the re-encrypted instruction file + */ + public boolean enforceRotation() { + return enforceRotation; + } + + /** + * Creates a builder that can be used to configure and create a {@link ReEncryptInstructionFileRequest} + * + * @return a new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for ReEncryptInstructionFileRequest. + */ + public static class Builder { + + private String bucket; + private String key; + private RawKeyring newKeyring; + private String instructionFileSuffix = DEFAULT_INSTRUCTION_FILE_SUFFIX; + private boolean enforceRotation = false; + + /** + * Sets the S3 bucket name for the re-encryption of instruction file. + * + * @param bucket the S3 bucket name + * @return a reference to this object so that method calls can be chained together. + */ + public Builder bucket(String bucket) { + this.bucket = bucket; + return this; + } + + /** + * Sets the S3 object key for the re-encryption of instruction file. + * + * @param key the S3 object key + * @return a reference to this object so that method calls can be chained together. + */ + public Builder key(String key) { + this.key = key; + return this; + } + + /** + * Sets the new keyring for re-encryption of instruction file. + * + * @param newKeyring the new keyring for re-encryption + * @return a reference to this object so that method calls can be chained together. + */ + public Builder newKeyring(RawKeyring newKeyring) { + this.newKeyring = newKeyring; + return this; + } + + /** + * Sets a custom instruction file suffix for the re-encrypted instruction file. + * For AES keyrings, only the default instruction file suffix is allowed. + * For RSA keyrings, both the default and custom instruction file suffixes are allowed. + * Note: The "." prefix is automatically added to the suffix + * + * @param instructionFileSuffix the instruction file suffix + * @return a reference to this object so that method calls can be chained together. + */ + public Builder instructionFileSuffix(String instructionFileSuffix) { + this.instructionFileSuffix = "." + instructionFileSuffix; + return this; + } + + /** + * Sets whether to enforce rotation for the re-encrypted instruction file. + * When enabled, the client will attempt to decrypt the re-encrypted instruction file with the old key material and + * throw an exception when decryption succeeds. This is a stronger level of validation that the wrapping key has been + * rotated than the standard assertion that the materials descriptions are different. + * + * @param enforceRotation whether to enforce rotation + * @return a reference to this object so that method calls can be chained together. + */ + public Builder enforceRotation(boolean enforceRotation) { + this.enforceRotation = enforceRotation; + return this; + } + + /** + * Validates and builds the ReEncryptInstructionFileRequest according + * to the configuration options passed to the Builder object. + * + * @return an instance of the ReEncryptInstructionFileRequest + */ + public ReEncryptInstructionFileRequest build() { + if (bucket == null || bucket.isEmpty()) { + throw new S3EncryptionClientException("Bucket must be provided!"); + } + if (key == null || key.isEmpty()) { + throw new S3EncryptionClientException("Key must be provided!"); + } + if (newKeyring == null) { + throw new S3EncryptionClientException("New keyring must be provided!"); + } + if (!(newKeyring instanceof RsaKeyring)) { + if (!instructionFileSuffix.equals(DEFAULT_INSTRUCTION_FILE_SUFFIX)) { + throw new S3EncryptionClientException( + "Custom Instruction file suffix is only applicable for RSA keyring!" + ); + } + } + return new ReEncryptInstructionFileRequest(this); + } + } +} diff --git a/src/main/java/software/amazon/encryption/s3/internal/ReEncryptInstructionFileResponse.java b/src/main/java/software/amazon/encryption/s3/internal/ReEncryptInstructionFileResponse.java new file mode 100644 index 000000000..630abe500 --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/internal/ReEncryptInstructionFileResponse.java @@ -0,0 +1,63 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.encryption.s3.internal; + +/** + * Response object returned after re-encrypting an instruction file in S3. + * Contains the S3 bucket name, object key, instruction file suffix, and rotation enforcement status for the re-encrypted instruction file + */ +public class ReEncryptInstructionFileResponse { + + private final String bucket; + private final String key; + private final String instructionFileSuffix; + private final boolean enforceRotation; + + /** + * Creates a new ReEncryptInstructionFileResponse object with the specified parameters. + * + * @param bucket the S3 bucket containing the re-encrypted instruction file + * @param key the S3 object key of the encrypted object in S3 + * @param instructionFileSuffix the suffix used for the instruction file + * @param enforceRotation whether rotation was enforced for the re-encrypted instruction file + */ + public ReEncryptInstructionFileResponse( + String bucket, + String key, + String instructionFileSuffix, + boolean enforceRotation + ) { + this.bucket = bucket; + this.key = key; + this.instructionFileSuffix = instructionFileSuffix.substring(1); + this.enforceRotation = enforceRotation; + } + + /** + * @return the S3 bucket containing the re-encrypted instruction file + */ + public String bucket() { + return bucket; + } + + /** + * @return the S3 object key of the encrypted object in S3 + */ + public String key() { + return key; + } + + /** + * @return whether rotation was enforced for the re-encrypted instruction file + */ + public boolean enforceRotation() { + return enforceRotation; + } + + /** + * @return the instruction file suffix used for the instruction file + */ + public String instructionFileSuffix() { + return instructionFileSuffix; + } +} diff --git a/src/main/java/software/amazon/encryption/s3/materials/AesKeyring.java b/src/main/java/software/amazon/encryption/s3/materials/AesKeyring.java index e2fbaac08..ec2e474df 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/AesKeyring.java +++ b/src/main/java/software/amazon/encryption/s3/materials/AesKeyring.java @@ -20,7 +20,7 @@ * This keyring can wrap keys with the active keywrap algorithm and * unwrap with the active and legacy algorithms for AES keys. */ -public class AesKeyring extends S3Keyring { +public class AesKeyring extends RawKeyring { private static final String KEY_ALGORITHM = "AES"; @@ -88,6 +88,11 @@ public boolean isLegacy() { return false; } + @Override + public EncryptionMaterials modifyMaterials(EncryptionMaterials materials) { + return modifyMaterialsForRawKeyring(materials); + } + @Override public String keyProviderInfo() { return KEY_PROVIDER_INFO; @@ -98,13 +103,6 @@ public EncryptionMaterials generateDataKey(EncryptionMaterials materials) { return defaultGenerateDataKey(materials); } - @Override - public EncryptionMaterials modifyMaterials(EncryptionMaterials materials) { - warnIfEncryptionContextIsPresent(materials); - - return materials; - } - @Override public byte[] encryptDataKey(SecureRandom secureRandom, EncryptionMaterials materials) @@ -177,7 +175,7 @@ protected Map decryptDataKeyStrategies() { return decryptDataKeyStrategies; } - public static class Builder extends S3Keyring.Builder { + public static class Builder extends RawKeyring.Builder { private SecretKey _wrappingKey; private Builder() { @@ -199,7 +197,6 @@ public Builder wrappingKey(final SecretKey wrappingKey) { _wrappingKey = wrappingKey; return builder(); } - public AesKeyring build() { return new AesKeyring(this); } diff --git a/src/main/java/software/amazon/encryption/s3/materials/EncryptionMaterials.java b/src/main/java/software/amazon/encryption/s3/materials/EncryptionMaterials.java index 8a358ed72..ff54785b8 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/EncryptionMaterials.java +++ b/src/main/java/software/amazon/encryption/s3/materials/EncryptionMaterials.java @@ -4,6 +4,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import software.amazon.awssdk.services.s3.model.S3Request; +import software.amazon.encryption.s3.S3EncryptionClientException; import software.amazon.encryption.s3.algorithms.AlgorithmSuite; import software.amazon.encryption.s3.internal.CipherMode; import software.amazon.encryption.s3.internal.CipherProvider; @@ -27,6 +28,7 @@ final public class EncryptionMaterials implements CryptographicMaterials { // Additional information passed into encrypted that is required on decryption as well // Should NOT contain sensitive information private final Map _encryptionContext; + private final MaterialsDescription _materialsDescription; private final List _encryptedDataKeys; private final byte[] _plaintextDataKey; @@ -43,6 +45,7 @@ private EncryptionMaterials(Builder builder) { this._cryptoProvider = builder._cryptoProvider; this._plaintextLength = builder._plaintextLength; this._ciphertextLength = _plaintextLength + _algorithmSuite.cipherTagLengthBytes(); + this._materialsDescription = builder._materialsDescription; } static public Builder builder() { @@ -101,6 +104,9 @@ public Provider cryptoProvider() { return _cryptoProvider; } + public MaterialsDescription materialsDescription() { + return _materialsDescription; + } @Override public CipherMode cipherMode() { return CipherMode.ENCRYPT; @@ -119,6 +125,7 @@ public Builder toBuilder() { .encryptedDataKeys(_encryptedDataKeys) .plaintextDataKey(_plaintextDataKey) .cryptoProvider(_cryptoProvider) + .materialsDescription(_materialsDescription) .plaintextLength(_plaintextLength); } @@ -132,6 +139,7 @@ static public class Builder { private byte[] _plaintextDataKey = null; private long _plaintextLength = -1; private Provider _cryptoProvider = null; + private MaterialsDescription _materialsDescription = MaterialsDescription.builder().build(); private Builder() { } @@ -145,7 +153,12 @@ public Builder algorithmSuite(AlgorithmSuite algorithmSuite) { _algorithmSuite = algorithmSuite; return this; } - + public Builder materialsDescription(MaterialsDescription materialsDescription) { + _materialsDescription = materialsDescription == null + ? MaterialsDescription.builder().build() + : materialsDescription; + return this; + } public Builder encryptionContext(Map encryptionContext) { _encryptionContext = encryptionContext == null ? Collections.emptyMap() @@ -175,6 +188,9 @@ public Builder plaintextLength(long plaintextLength) { } public EncryptionMaterials build() { + if (!_materialsDescription.isEmpty() && !_encryptionContext.isEmpty()) { + throw new S3EncryptionClientException("MaterialsDescription and EncryptionContext cannot both be set!"); + } return new EncryptionMaterials(this); } } diff --git a/src/main/java/software/amazon/encryption/s3/materials/MaterialsDescription.java b/src/main/java/software/amazon/encryption/s3/materials/MaterialsDescription.java new file mode 100644 index 000000000..148b20c49 --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/materials/MaterialsDescription.java @@ -0,0 +1,140 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.encryption.s3.materials; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * This class is used to provide key-value pairs that describe the key material used with the Keyring, specifically for AES and RSA Keyrings. + * This will be useful during the re-encryption of instruction file. + * The stored Materials Description is immutable once created. + */ +public class MaterialsDescription implements Map { + + private final Map materialsDescription; + + private MaterialsDescription(Builder builder) { + this.materialsDescription = + Collections.unmodifiableMap(new HashMap<>(builder.materialsDescription)); + } + + /** + * @return a new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * @return the materials description map + */ + public Map getMaterialsDescription() { + return this.materialsDescription; + } + + @Override + public int size() { + return materialsDescription.size(); + } + + @Override + public boolean isEmpty() { + return materialsDescription.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return materialsDescription.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return materialsDescription.containsValue(value); + } + + @Override + public String get(Object key) { + return materialsDescription.get(key); + } + + @Override + public String put(String key, String value) { + throw new UnsupportedOperationException("This map is immutable"); + } + + @Override + public String remove(Object key) { + return materialsDescription.remove(key); + } + + @Override + public void putAll(Map m) { + throw new UnsupportedOperationException("This map is immutable"); + } + + @Override + public void clear() { + materialsDescription.clear(); + } + + @Override + public Set keySet() { + return materialsDescription.keySet(); + } + + @Override + public Collection values() { + return materialsDescription.values(); + } + + @Override + public Set> entrySet() { + return materialsDescription.entrySet(); + } + + /** + * Builder for MaterialsDescription. + */ + public static class Builder { + + private final Map materialsDescription = new HashMap<>(); + + /** + * @param key the key to add + * @param value the value to add + * @return a reference to this object so that method calls can be chained together. + * @throws IllegalArgumentException if key or value is null + */ + public Builder put(String key, String value) { + if (key == null || value == null) { + throw new IllegalArgumentException("Key and value must not be null"); + } + materialsDescription.put(key, value); + return this; + } + + /** + * @param description the map of key-value pairs to add + * @return a reference to this object so that method calls can be chained together. + * @throws IllegalArgumentException if description is null + */ + public Builder putAll(Map description) { + if (description == null) { + throw new IllegalArgumentException("Description must not be null"); + } + materialsDescription.putAll(description); + return this; + } + + /** + * @return the built MaterialsDescription + */ + public MaterialsDescription build() { + return new MaterialsDescription(this); + } + } +} diff --git a/src/main/java/software/amazon/encryption/s3/materials/RawKeyring.java b/src/main/java/software/amazon/encryption/s3/materials/RawKeyring.java new file mode 100644 index 000000000..9ad240322 --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/materials/RawKeyring.java @@ -0,0 +1,105 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.encryption.s3.materials; + +import org.apache.commons.logging.LogFactory; +import software.amazon.encryption.s3.S3EncryptionClient; + +/** + * This is an abstract base class for keyrings that use raw cryptographic keys (AES + RSA) + */ +public abstract class RawKeyring extends S3Keyring { + + protected final MaterialsDescription _materialsDescription; + + protected RawKeyring(Builder builder) { + super(builder); + _materialsDescription = builder._materialsDescription; + } + + /** + * Modifies encryption materials with the keyring's materials description if present. + * Issues a warning if encryption context is found, as it provides no security benefit for raw keyrings. + * + * @param materials the encryption materials to modify + * @return modified encryption materials with the keyring's materials description or original encryption materials if no materials description is set + */ + public EncryptionMaterials modifyMaterialsForRawKeyring( + EncryptionMaterials materials + ) { + warnIfEncryptionContextIsPresent(materials); + if (_materialsDescription != null && !_materialsDescription.isEmpty()) { + materials = + materials + .toBuilder() + .materialsDescription(_materialsDescription) + .build(); + } + return materials; + } + + /** + * Checks if an encryption context is present in the EncryptionMaterials and issues a warning + * if an encryption context is found. + *

+ * Encryption context is not recommended for use with + * non-KMS keyrings as it does not provide additional security benefits and is not stored. + * + * @param materials EncryptionMaterials + */ + + public void warnIfEncryptionContextIsPresent(EncryptionMaterials materials) { + materials + .s3Request() + .overrideConfiguration() + .flatMap(overrideConfiguration -> + overrideConfiguration + .executionAttributes() + .getOptionalAttribute(S3EncryptionClient.ENCRYPTION_CONTEXT) + ) + .ifPresent(ctx -> + LogFactory + .getLog(getClass()) + .warn( + "Usage of Encryption Context provides no " + + "security benefit in " + + getClass().getSimpleName() + + ".Additionally, this Encryption Context WILL NOT be " + + "stored in the material description. Provide a MaterialDescription in the Keyring's builder instead." + ) + ); + } + + /** + * Abstract builder for RawKeyring implementations. + * Provides common functionality for setting materials description on raw keyrings. + * + * @param the type of keyring being built + * @param the type of builder + */ + public abstract static class Builder< + KeyringT extends RawKeyring, BuilderT extends Builder + > + extends S3Keyring.Builder { + + protected MaterialsDescription _materialsDescription; + + protected Builder() { + super(); + } + + /** + * Sets the materials description for this keyring. + * Materials description provides additional metadata for raw keyrings. + * + * @param materialsDescription the materials description to associate with this keyring. + * @return a reference to this object so that method calls can be chained together. + */ + public BuilderT materialsDescription( + MaterialsDescription materialsDescription + ) { + _materialsDescription = materialsDescription; + return builder(); + } + } +} diff --git a/src/main/java/software/amazon/encryption/s3/materials/RsaKeyring.java b/src/main/java/software/amazon/encryption/s3/materials/RsaKeyring.java index da40d4c1b..91563b14c 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/RsaKeyring.java +++ b/src/main/java/software/amazon/encryption/s3/materials/RsaKeyring.java @@ -23,7 +23,7 @@ * This keyring can wrap keys with the active keywrap algorithm and * unwrap with the active and legacy algorithms for RSA keys. */ -public class RsaKeyring extends S3Keyring { +public class RsaKeyring extends RawKeyring { private final PartialRsaKeyPair _partialRsaKeyPair; @@ -93,6 +93,11 @@ public boolean isLegacy() { return false; } + @Override + public EncryptionMaterials modifyMaterials(EncryptionMaterials materials) { + return modifyMaterialsForRawKeyring(materials); + } + @Override public String keyProviderInfo() { return KEY_PROVIDER_INFO; @@ -103,13 +108,6 @@ public EncryptionMaterials generateDataKey(EncryptionMaterials materials) { return defaultGenerateDataKey(materials); } - @Override - public EncryptionMaterials modifyMaterials(EncryptionMaterials materials) { - warnIfEncryptionContextIsPresent(materials); - - return materials; - } - @Override public byte[] encryptDataKey(SecureRandom secureRandom, EncryptionMaterials materials) throws GeneralSecurityException { @@ -197,7 +195,7 @@ protected Map decryptDataKeyStrategies() { return decryptDataKeyStrategies; } - public static class Builder extends S3Keyring.Builder { + public static class Builder extends RawKeyring.Builder { private PartialRsaKeyPair _partialRsaKeyPair; private Builder() { @@ -217,6 +215,7 @@ public Builder wrappingKeyPair(final PartialRsaKeyPair partialRsaKeyPair) { public RsaKeyring build() { return new RsaKeyring(this); } - } + + } } diff --git a/src/main/java/software/amazon/encryption/s3/materials/S3Keyring.java b/src/main/java/software/amazon/encryption/s3/materials/S3Keyring.java index 6cb5d7b2a..ebf3a6105 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/S3Keyring.java +++ b/src/main/java/software/amazon/encryption/s3/materials/S3Keyring.java @@ -3,7 +3,6 @@ package software.amazon.encryption.s3.materials; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import software.amazon.encryption.s3.S3EncryptionClient; import software.amazon.encryption.s3.S3EncryptionClientException; import java.nio.charset.StandardCharsets; @@ -14,8 +13,6 @@ import java.util.Map; import javax.crypto.SecretKey; -import org.apache.commons.logging.LogFactory; - /** * This serves as the base class for all the keyrings in the S3 encryption client. * Shared functionality is all performed here. @@ -132,30 +129,11 @@ public DecryptionMaterials onDecrypt(final DecryptionMaterials materials, List decryptDataKeyStrategies(); - /** - * Checks if an encryption context is present in the EncryptionMaterials and issues a warning - * if an encryption context is found. - *

- * Encryption context is not recommended for use with - * non-KMS keyrings as it may not provide additional security benefits. - * - * @param materials EncryptionMaterials - */ - public void warnIfEncryptionContextIsPresent(EncryptionMaterials materials) { - materials.s3Request().overrideConfiguration() - .flatMap(overrideConfiguration -> - overrideConfiguration.executionAttributes() - .getOptionalAttribute(S3EncryptionClient.ENCRYPTION_CONTEXT)) - .ifPresent(ctx -> LogFactory.getLog(getClass()).warn("Usage of Encryption Context provides no security benefit in " + getClass().getSimpleName())); - - } - abstract public static class Builder> { private boolean _enableLegacyWrappingAlgorithms = false; - private SecureRandom _secureRandom; + private SecureRandom _secureRandom = new SecureRandom(); private DataKeyGenerator _dataKeyGenerator = new DefaultDataKeyGenerator(); - protected Builder() {} protected abstract BuilderT builder(); @@ -185,7 +163,6 @@ public BuilderT dataKeyGenerator(final DataKeyGenerator dataKeyGenerator) { _dataKeyGenerator = dataKeyGenerator; return builder(); } - abstract public KeyringT build(); } } diff --git a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientMatDescTest.java b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientMatDescTest.java new file mode 100644 index 000000000..b94ffd3d1 --- /dev/null +++ b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientMatDescTest.java @@ -0,0 +1,370 @@ +package software.amazon.encryption.s3; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static software.amazon.encryption.s3.S3EncryptionClient.withAdditionalConfiguration; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.BUCKET; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.appendTestSuffix; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.deleteObject; + +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; +import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.ServerSideEncryption; +import software.amazon.encryption.s3.internal.InstructionFileConfig; +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; + +public class S3EncryptionClientMatDescTest { + + private static SecretKey AES_KEY; + private static KeyPair RSA_KEY_PAIR; + + @BeforeAll + public static void setUp() throws NoSuchAlgorithmException { + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(256); + AES_KEY = keyGen.generateKey(); + + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(2048); + RSA_KEY_PAIR = keyPairGen.generateKeyPair(); + } + + @Test + public void testAesMaterialsDescriptionInObjectMetadata() { + AesKeyring aesKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY) + .materialsDescription( + MaterialsDescription.builder().put("version", "1.0").build() + ) + .build(); + S3EncryptionClient client = S3EncryptionClient + .builder() + .keyring(aesKeyring) + .build(); + final String input = "Testing Materials Description in Object Metadata!"; + final String objectKey = appendTestSuffix( + "test-aes-materials-description-in-object-metadata" + ); + + client.putObject( + builder -> builder.bucket(BUCKET).key(objectKey).build(), + RequestBody.fromString(input) + ); + ResponseBytes responseBytes = + client.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey).build() + ); + assertEquals(input, responseBytes.asUtf8String()); + + JsonNodeParser parser = JsonNodeParser.create(); + JsonNode matDescNode = parser.parse( + responseBytes.response().metadata().get("x-amz-matdesc") + ); + assertEquals("1.0", matDescNode.asObject().get("version").asString()); + + deleteObject(BUCKET, objectKey, client); + } + + @Test + public void testRsaMaterialsDescriptionInObjectMetadata() { + PartialRsaKeyPair keyPair = new PartialRsaKeyPair( + RSA_KEY_PAIR.getPrivate(), + RSA_KEY_PAIR.getPublic() + ); + RsaKeyring rsaKeyring = RsaKeyring + .builder() + .wrappingKeyPair(keyPair) + .materialsDescription( + MaterialsDescription + .builder() + .put("version", "1.0") + .put("admin", "yes") + .build() + ) + .build(); + S3EncryptionClient client = S3EncryptionClient + .builder() + .keyring(rsaKeyring) + .build(); + final String input = "Testing Materials Description in Instruction File!"; + final String objectKey = appendTestSuffix( + "test-rsa-materials-description-in-instruction-file" + ); + + client.putObject( + builder -> builder.bucket(BUCKET).key(objectKey).build(), + RequestBody.fromString(input) + ); + ResponseBytes responseBytes = + client.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey).build() + ); + assertEquals(input, responseBytes.asUtf8String()); + + JsonNodeParser parser = JsonNodeParser.create(); + JsonNode matDescNode = parser.parse( + responseBytes.response().metadata().get("x-amz-matdesc") + ); + assertEquals("1.0", matDescNode.asObject().get("version").asString()); + assertEquals("yes", matDescNode.asObject().get("admin").asString()); + + deleteObject(BUCKET, objectKey, client); + } + + @Test + public void testAesMaterialsDescriptionInInstructionFile() { + AesKeyring aesKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY) + .materialsDescription( + MaterialsDescription.builder().put("version", "1.0").build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient client = S3EncryptionClient + .builder() + .keyring(aesKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .enableInstructionFilePutObject(true) + .instructionFileClient(wrappedClient) + .build() + ) + .build(); + + final String input = "Testing Materials Description in Instruction File!"; + final String objectKey = appendTestSuffix( + "test-aes-materials-description-in-instruction-file" + ); + + client.putObject( + builder -> builder.bucket(BUCKET).key(objectKey).build(), + RequestBody.fromString(input) + ); + ResponseBytes responseBytes = + client.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey).build() + ); + assertEquals(input, responseBytes.asUtf8String()); + + S3Client defaultClient = S3Client.create(); + + ResponseBytes directInstGetResponse = + defaultClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey + ".instruction").build() + ); + + String instructionFileContent = directInstGetResponse.asUtf8String(); + JsonNodeParser parser = JsonNodeParser.create(); + JsonNode instructionFileNode = parser.parse(instructionFileContent); + + JsonNode matDescNode = parser.parse( + instructionFileNode.asObject().get("x-amz-matdesc").asString() + ); + assertEquals("1.0", matDescNode.asObject().get("version").asString()); + + deleteObject(BUCKET, objectKey, client); + } + + @Test + public void testRsaMaterialsDescriptionInInstructionFile() { + PartialRsaKeyPair keyPair = new PartialRsaKeyPair( + RSA_KEY_PAIR.getPrivate(), + RSA_KEY_PAIR.getPublic() + ); + + RsaKeyring rsaKeyring = RsaKeyring + .builder() + .wrappingKeyPair(keyPair) + .materialsDescription( + MaterialsDescription + .builder() + .put("version", "1.0") + .put("admin", "yes") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient client = S3EncryptionClient + .builder() + .keyring(rsaKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .enableInstructionFilePutObject(true) + .instructionFileClient(wrappedClient) + .build() + ) + .build(); + + final String input = "Testing Materials Description in Instruction File!"; + final String objectKey = appendTestSuffix( + "test-rsa-materials-description-in-instruction-file" + ); + + client.putObject( + builder -> builder.bucket(BUCKET).key(objectKey).build(), + RequestBody.fromString(input) + ); + ResponseBytes responseBytes = + client.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey).build() + ); + assertEquals(input, responseBytes.asUtf8String()); + + S3Client defaultClient = S3Client.create(); + + ResponseBytes directInstGetResponse = + defaultClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey + ".instruction").build() + ); + + String instructionFileContent = directInstGetResponse.asUtf8String(); + JsonNodeParser parser = JsonNodeParser.create(); + JsonNode instructionFileNode = parser.parse(instructionFileContent); + + JsonNode matDescNode = parser.parse( + instructionFileNode.asObject().get("x-amz-matdesc").asString() + ); + assertEquals("1.0", matDescNode.asObject().get("version").asString()); + assertEquals("yes", matDescNode.asObject().get("admin").asString()); + + deleteObject(BUCKET, objectKey, client); + } + + @Test + public void testAesKeyringMatDescOverridesPutObjectEncryptionContext() { + AesKeyring aesKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY) + .materialsDescription( + MaterialsDescription.builder().put("version", "1.0").build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient client = S3EncryptionClient + .builder() + .keyring(aesKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .enableInstructionFilePutObject(true) + .instructionFileClient(wrappedClient) + .build() + ) + .build(); + + final String input = + "Testing Materials Description in Instruction File and not Encryption Context!"; + final String objectKey = appendTestSuffix( + "test-aes-materials-description-in-instruction-file-and-not-encryption-context" + ); + final Map encryptionContext = new HashMap(); + encryptionContext.put("admin", "yes"); + + client.putObject( + builder -> + builder + .bucket(BUCKET) + .key(objectKey) + .overrideConfiguration(withAdditionalConfiguration(encryptionContext)) + .build(), + RequestBody.fromString(input) + ); + ResponseBytes responseBytes = + client.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey).build() + ); + assertEquals(input, responseBytes.asUtf8String()); + + S3Client defaultClient = S3Client.create(); + + ResponseBytes directInstGetResponse = + defaultClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey + ".instruction").build() + ); + + String instructionFileContent = directInstGetResponse.asUtf8String(); + JsonNodeParser parser = JsonNodeParser.create(); + JsonNode instructionFileNode = parser.parse(instructionFileContent); + + JsonNode matDescNode = parser.parse( + instructionFileNode.asObject().get("x-amz-matdesc").asString() + ); + assertEquals("1.0", matDescNode.asObject().get("version").asString()); + assertNull(matDescNode.asObject().get("admin")); + } + + @Test + public void testRsaKeyringMatDescOverridesPutObjectEncryptionContext() { + PartialRsaKeyPair keyPair = new PartialRsaKeyPair( + RSA_KEY_PAIR.getPrivate(), + RSA_KEY_PAIR.getPublic() + ); + RsaKeyring rsaKeyring = RsaKeyring + .builder() + .wrappingKeyPair(keyPair) + .materialsDescription( + MaterialsDescription.builder().put("version", "1.0").build() + ) + .build(); + S3EncryptionClient client = S3EncryptionClient + .builder() + .keyring(rsaKeyring) + .build(); + final String input = + "Testing Materials Description in Instruction File and not Encryption Context!"; + final String objectKey = appendTestSuffix( + "test-rsa-materials-description-in-instruction-file-and-not-encryption-context" + ); + final Map encryptionContext = new HashMap(); + encryptionContext.put("admin", "yes"); + + client.putObject( + builder -> + builder + .bucket(BUCKET) + .key(objectKey) + .overrideConfiguration(withAdditionalConfiguration(encryptionContext)) + .build(), + RequestBody.fromString(input) + ); + ResponseBytes responseBytes = + client.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey).build() + ); + assertEquals(input, responseBytes.asUtf8String()); + + JsonNodeParser parser = JsonNodeParser.create(); + JsonNode matDescNode = parser.parse( + responseBytes.response().metadata().get("x-amz-matdesc") + ); + assertEquals("1.0", matDescNode.asObject().get("version").asString()); + assertNull(matDescNode.asObject().get("admin")); + } +} diff --git a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientReEncryptInstructionFileTest.java b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientReEncryptInstructionFileTest.java new file mode 100644 index 000000000..dbf07c654 --- /dev/null +++ b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientReEncryptInstructionFileTest.java @@ -0,0 +1,4616 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.encryption.s3; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static software.amazon.encryption.s3.S3EncryptionClient.withCustomInstructionFileSuffix; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.BUCKET; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.appendTestSuffix; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.deleteObject; + +import com.amazonaws.AmazonClientException; +import com.amazonaws.services.s3.AmazonS3Encryption; +import com.amazonaws.services.s3.AmazonS3EncryptionClient; +import com.amazonaws.services.s3.AmazonS3EncryptionClientV2; +import com.amazonaws.services.s3.AmazonS3EncryptionV2; +import com.amazonaws.services.s3.model.CryptoConfiguration; +import com.amazonaws.services.s3.model.CryptoConfigurationV2; +import com.amazonaws.services.s3.model.CryptoMode; +import com.amazonaws.services.s3.model.CryptoStorageMode; +import com.amazonaws.services.s3.model.EncryptedGetObjectRequest; +import com.amazonaws.services.s3.model.EncryptionMaterials; +import com.amazonaws.services.s3.model.EncryptionMaterialsProvider; +import com.amazonaws.services.s3.model.StaticEncryptionMaterialsProvider; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; +import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +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; + +public class S3EncryptionClientReEncryptInstructionFileTest { + + private static SecretKey AES_KEY; + private static SecretKey AES_KEY_TWO; + private static KeyPair RSA_KEY_PAIR; + private static KeyPair RSA_KEY_PAIR_TWO; + + @BeforeAll + public static void setUp() throws NoSuchAlgorithmException { + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(256); + AES_KEY = keyGen.generateKey(); + AES_KEY_TWO = keyGen.generateKey(); + + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(2048); + RSA_KEY_PAIR = keyPairGen.generateKeyPair(); + RSA_KEY_PAIR_TWO = keyPairGen.generateKeyPair(); + } + + @Test + public void testAesReEncryptInstructionFileFailsWithSameMaterialsDescription() { + AesKeyring oldKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "no").build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient client = S3EncryptionClient + .builder() + .keyring(oldKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + final String objectKey = appendTestSuffix( + "aes-re-encrypt-instruction-file-with-same-materials-description-test" + ); + final String input = + "Testing re-encryption of instruction file with AES Keyring"; + + client.putObject( + builder -> builder.bucket(BUCKET).key(objectKey).build(), + RequestBody.fromString(input) + ); + + AesKeyring newKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY_TWO) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "no").build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .build(); + + try { + client.reEncryptInstructionFile(reEncryptInstructionFileRequest); + throw new RuntimeException("Expected failure"); + } catch (S3EncryptionClientException e) { + assertTrue( + e + .getMessage() + .contains("New keyring must have new materials description!") + ); + } + + deleteObject(BUCKET, objectKey, client); + } + + @Test + public void testRsaReEncryptInstructionFileWithCustomSuffixFailsWithSameMaterialsDescription() { + PublicKey clientPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey clientPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair clientPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(clientPublicKey) + .privateKey(clientPrivateKey) + .build(); + + RsaKeyring clientKeyring = RsaKeyring + .builder() + .wrappingKeyPair(clientPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "yes") + .put("access-level", "admin") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient client = S3EncryptionClient + .builder() + .keyring(clientKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + final String objectKey = appendTestSuffix( + "rsa-re-encrypt-instruction-file-with-custom-suffix-and-same-materials-description-test" + ); + final String input = + "Testing re-encryption of instruction file with RSA Keyring"; + + client.putObject( + builder -> builder.bucket(BUCKET).key(objectKey).build(), + RequestBody.fromString(input) + ); + + PublicKey thirdPartyPublicKey = RSA_KEY_PAIR_TWO.getPublic(); + PrivateKey thirdPartyPrivateKey = RSA_KEY_PAIR_TWO.getPrivate(); + + PartialRsaKeyPair thirdPartyPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(thirdPartyPublicKey) + .privateKey(thirdPartyPrivateKey) + .build(); + + RsaKeyring thirdPartyKeyring = RsaKeyring + .builder() + .wrappingKeyPair(thirdPartyPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "yes") + .put("access-level", "admin") + .build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .instructionFileSuffix("third-party-access-instruction-file") + .newKeyring(thirdPartyKeyring) + .build(); + + try { + client.reEncryptInstructionFile(reEncryptInstructionFileRequest); + throw new RuntimeException("Expected failure"); + } catch (S3EncryptionClientException e) { + assertTrue( + e + .getMessage() + .contains("New keyring must have new materials description!") + ); + } + + deleteObject(BUCKET, objectKey, client); + } + + @Test + public void testAesReEncryptInstructionFileRejectsCustomInstructionFileSuffix() { + AesKeyring oldKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "no").build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient client = S3EncryptionClient + .builder() + .keyring(oldKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + final String objectKey = appendTestSuffix( + "aes-re-encrypt-instruction-file-with-custom-suffix-test" + ); + + AesKeyring newKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY_TWO) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "yes").build() + ) + .build(); + + try { + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .instructionFileSuffix("custom-suffix") + .build(); + } catch (S3EncryptionClientException e) { + assertTrue( + e + .getMessage() + .contains( + "Custom Instruction file suffix is only applicable for RSA keyring!" + ) + ); + } + + deleteObject(BUCKET, objectKey, client); + } + + @Test + public void testReEncryptInstructionFileFailsWhenInstructionFilePutNotEnabled() { + PublicKey originalPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey originalPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair originalPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(originalPublicKey) + .privateKey(originalPrivateKey) + .build(); + + RsaKeyring oldKeyring = RsaKeyring + .builder() + .wrappingKeyPair(originalPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "no").build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient client = S3EncryptionClient + .builder() + .keyring(oldKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .build() + ) + .build(); + + final String objectKey = appendTestSuffix( + "rsa-re-encrypt-instruction-file" + ); + + PublicKey newPublicKey = RSA_KEY_PAIR_TWO.getPublic(); + PrivateKey newPrivateKey = RSA_KEY_PAIR_TWO.getPrivate(); + + PartialRsaKeyPair newPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(newPublicKey) + .privateKey(newPrivateKey) + .build(); + + RsaKeyring newKeyring = RsaKeyring + .builder() + .wrappingKeyPair(newPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "yes").build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .build(); + + try { + ReEncryptInstructionFileResponse response = + client.reEncryptInstructionFile(reEncryptInstructionFileRequest); + } catch (S3EncryptionClientException e) { + assertTrue( + e + .getMessage() + .contains( + "Instruction file put operations must be enabled to re-encrypt instruction files" + ) + ); + } + } + + @Test + public void testAesKeyringReEncryptInstructionFile() { + AesKeyring oldKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "no").build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient client = S3EncryptionClient + .builder() + .keyring(oldKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + final String objectKey = appendTestSuffix( + "aes-re-encrypt-instruction-file-test" + ); + final String input = + "Testing re-encryption of instruction file with AES Keyring"; + + client.putObject( + builder -> builder.bucket(BUCKET).key(objectKey).build(), + RequestBody.fromString(input) + ); + + ResponseBytes instructionFile = + wrappedClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey + ".instruction").build() + ); + + String instructionFileContent = instructionFile.asUtf8String(); + JsonNodeParser parser = JsonNodeParser.create(); + JsonNode instructionFileNode = parser.parse(instructionFileContent); + + String originalIv = instructionFileNode + .asObject() + .get("x-amz-iv") + .asString(); + String originalEncryptedDataKeyAlgorithm = instructionFileNode + .asObject() + .get("x-amz-wrap-alg") + .asString(); + String originalEncryptedDataKey = instructionFileNode + .asObject() + .get("x-amz-key-v2") + .asString(); + JsonNode originalMatDescNode = parser.parse( + instructionFileNode.asObject().get("x-amz-matdesc").asString() + ); + assertEquals( + "no", + originalMatDescNode.asObject().get("rotated").asString() + ); + + AesKeyring newKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY_TWO) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "yes").build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .build(); + + ReEncryptInstructionFileResponse response = client.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + + assertEquals(BUCKET, response.bucket()); + assertEquals(objectKey, response.key()); + assertEquals("instruction", response.instructionFileSuffix()); + assertFalse(response.enforceRotation()); + + S3Client rotatedWrappedClient = S3Client.create(); + + S3EncryptionClient rotatedClient = S3EncryptionClient + .builder() + .keyring(newKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(rotatedWrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + try { + client.getObjectAsBytes( + GetObjectRequest.builder().bucket(BUCKET).key(objectKey).build() + ); + throw new RuntimeException("Expected exception"); + } catch (S3EncryptionClientException e) { + assertTrue(e.getMessage().contains("Unable to AES/GCM unwrap")); + } + + ResponseBytes getResponse = + rotatedClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey).build() + ); + + assertEquals(input, getResponse.asUtf8String()); + + ResponseBytes reEncryptedInstructionFile = + rotatedWrappedClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey + ".instruction").build() + ); + + String newInstructionFileContent = + reEncryptedInstructionFile.asUtf8String(); + JsonNode newInstructionFileNode = parser.parse(newInstructionFileContent); + + String postReEncryptionIv = newInstructionFileNode + .asObject() + .get("x-amz-iv") + .asString(); + String postReEncryptionEncryptedDataKeyAlgorithm = newInstructionFileNode + .asObject() + .get("x-amz-wrap-alg") + .asString(); + String postReEncryptionEncryptedDataKey = newInstructionFileNode + .asObject() + .get("x-amz-key-v2") + .asString(); + JsonNode postReEncryptionMatDescNode = parser.parse( + newInstructionFileNode.asObject().get("x-amz-matdesc").asString() + ); + + assertEquals( + "yes", + postReEncryptionMatDescNode.asObject().get("rotated").asString() + ); + assertEquals(originalIv, postReEncryptionIv); + assertEquals( + originalEncryptedDataKeyAlgorithm, + postReEncryptionEncryptedDataKeyAlgorithm + ); + assertNotEquals(originalEncryptedDataKey, postReEncryptionEncryptedDataKey); + + deleteObject(BUCKET, objectKey, client); + } + + @Test + public void testRsaKeyringReEncryptInstructionFile() { + PublicKey originalPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey originalPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair originalPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(originalPublicKey) + .privateKey(originalPrivateKey) + .build(); + + RsaKeyring oldKeyring = RsaKeyring + .builder() + .wrappingKeyPair(originalPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "no").build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient client = S3EncryptionClient + .builder() + .keyring(oldKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + final String objectKey = appendTestSuffix( + "rsa-re-encrypt-instruction-file-test" + ); + final String input = + "Testing re-encryption of instruction file with RSA Keyring"; + + client.putObject( + builder -> builder.bucket(BUCKET).key(objectKey).build(), + RequestBody.fromString(input) + ); + + ResponseBytes instructionFile = + wrappedClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey + ".instruction").build() + ); + + String instructionFileContent = instructionFile.asUtf8String(); + JsonNodeParser parser = JsonNodeParser.create(); + JsonNode instructionFileNode = parser.parse(instructionFileContent); + + String originalIv = instructionFileNode + .asObject() + .get("x-amz-iv") + .asString(); + String originalEncryptedDataKeyAlgorithm = instructionFileNode + .asObject() + .get("x-amz-wrap-alg") + .asString(); + String originalEncryptedDataKey = instructionFileNode + .asObject() + .get("x-amz-key-v2") + .asString(); + JsonNode originalMatDescNode = parser.parse( + instructionFileNode.asObject().get("x-amz-matdesc").asString() + ); + assertEquals( + "no", + originalMatDescNode.asObject().get("rotated").asString() + ); + + PublicKey newPublicKey = RSA_KEY_PAIR_TWO.getPublic(); + PrivateKey newPrivateKey = RSA_KEY_PAIR_TWO.getPrivate(); + + PartialRsaKeyPair newPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(newPublicKey) + .privateKey(newPrivateKey) + .build(); + + RsaKeyring newKeyring = RsaKeyring + .builder() + .wrappingKeyPair(newPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "yes").build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .build(); + + ReEncryptInstructionFileResponse response = client.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + + assertEquals(BUCKET, response.bucket()); + assertEquals(objectKey, response.key()); + assertEquals("instruction", response.instructionFileSuffix()); + assertFalse(response.enforceRotation()); + + S3Client rotatedWrappedClient = S3Client.create(); + + S3EncryptionClient rotatedClient = S3EncryptionClient + .builder() + .keyring(newKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(rotatedWrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + try { + client.getObjectAsBytes( + GetObjectRequest.builder().bucket(BUCKET).key(objectKey).build() + ); + throw new RuntimeException("Expected exception"); + } catch (S3EncryptionClientException e) { + assertTrue(e.getMessage().contains("Unable to RSA-OAEP-SHA1 unwrap")); + } + + ResponseBytes getResponse = + rotatedClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey).build() + ); + + assertEquals(input, getResponse.asUtf8String()); + + ResponseBytes reEncryptedInstructionFile = + rotatedWrappedClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey + ".instruction").build() + ); + + String newInstructionFileContent = + reEncryptedInstructionFile.asUtf8String(); + JsonNode newInstructionFileNode = parser.parse(newInstructionFileContent); + + String postReEncryptionIv = newInstructionFileNode + .asObject() + .get("x-amz-iv") + .asString(); + String postReEncryptionEncryptedDataKeyAlgorithm = newInstructionFileNode + .asObject() + .get("x-amz-wrap-alg") + .asString(); + String postReEncryptionEncryptedDataKey = newInstructionFileNode + .asObject() + .get("x-amz-key-v2") + .asString(); + JsonNode postReEncryptionMatDescNode = parser.parse( + newInstructionFileNode.asObject().get("x-amz-matdesc").asString() + ); + + assertEquals( + "yes", + postReEncryptionMatDescNode.asObject().get("rotated").asString() + ); + assertEquals(originalIv, postReEncryptionIv); + assertEquals( + originalEncryptedDataKeyAlgorithm, + postReEncryptionEncryptedDataKeyAlgorithm + ); + assertNotEquals(originalEncryptedDataKey, postReEncryptionEncryptedDataKey); + + deleteObject(BUCKET, objectKey, client); + } + + @Test + public void testRsaKeyringReEncryptInstructionFileWithCustomSuffix() { + PublicKey clientPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey clientPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair clientPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(clientPublicKey) + .privateKey(clientPrivateKey) + .build(); + + RsaKeyring clientKeyring = RsaKeyring + .builder() + .wrappingKeyPair(clientPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "yes") + .put("access-level", "admin") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient client = S3EncryptionClient + .builder() + .keyring(clientKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + final String objectKey = appendTestSuffix( + "rsa-re-encrypt-instruction-file-with-custom-suffix-test" + ); + final String input = + "Testing re-encryption of instruction file with RSA Keyring"; + + client.putObject( + builder -> builder.bucket(BUCKET).key(objectKey).build(), + RequestBody.fromString(input) + ); + + PublicKey thirdPartyPublicKey = RSA_KEY_PAIR_TWO.getPublic(); + PrivateKey thirdPartyPrivateKey = RSA_KEY_PAIR_TWO.getPrivate(); + + PartialRsaKeyPair thirdPartyPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(thirdPartyPublicKey) + .privateKey(thirdPartyPrivateKey) + .build(); + + RsaKeyring thirdPartyKeyring = RsaKeyring + .builder() + .wrappingKeyPair(thirdPartyPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "no") + .put("access-level", "user") + .build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .instructionFileSuffix("third-party-access-instruction-file") + .newKeyring(thirdPartyKeyring) + .build(); + + S3EncryptionClient thirdPartyClient = S3EncryptionClient + .builder() + .keyring(thirdPartyKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + ReEncryptInstructionFileResponse reEncryptInstructionFileResponse = + client.reEncryptInstructionFile(reEncryptInstructionFileRequest); + + assertEquals(BUCKET, reEncryptInstructionFileResponse.bucket()); + assertEquals(objectKey, reEncryptInstructionFileResponse.key()); + assertEquals( + "third-party-access-instruction-file", + reEncryptInstructionFileResponse.instructionFileSuffix() + ); + assertFalse(reEncryptInstructionFileResponse.enforceRotation()); + + ResponseBytes clientInstructionFile = + wrappedClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey + ".instruction").build() + ); + + JsonNodeParser parser = JsonNodeParser.create(); + + String clientInstructionFileContent = clientInstructionFile.asUtf8String(); + + JsonNode clientInstructionFileNode = parser.parse( + clientInstructionFileContent + ); + String clientIv = clientInstructionFileNode + .asObject() + .get("x-amz-iv") + .asString(); + String clientEncryptedDataKeyAlgorithm = clientInstructionFileNode + .asObject() + .get("x-amz-wrap-alg") + .asString(); + String clientEncryptedDataKey = clientInstructionFileNode + .asObject() + .get("x-amz-key-v2") + .asString(); + JsonNode clientMatDescNode = parser.parse( + clientInstructionFileNode.asObject().get("x-amz-matdesc").asString() + ); + + assertEquals("yes", clientMatDescNode.asObject().get("isOwner").asString()); + assertEquals( + "admin", + clientMatDescNode.asObject().get("access-level").asString() + ); + + ResponseBytes thirdPartyInstFile = + wrappedClient.getObjectAsBytes(builder -> + builder + .bucket(BUCKET) + .key(objectKey + ".third-party-access-instruction-file") + .build() + ); + + String thirdPartyInstructionFileContent = thirdPartyInstFile.asUtf8String(); + JsonNode thirdPartyInstructionFileNode = parser.parse( + thirdPartyInstructionFileContent + ); + String thirdPartyIv = thirdPartyInstructionFileNode + .asObject() + .get("x-amz-iv") + .asString(); + String thirdPartyEncryptedDataKeyAlgorithm = thirdPartyInstructionFileNode + .asObject() + .get("x-amz-wrap-alg") + .asString(); + String thirdPartyEncryptedDataKey = thirdPartyInstructionFileNode + .asObject() + .get("x-amz-key-v2") + .asString(); + JsonNode thirdPartyMatDescNode = parser.parse( + thirdPartyInstructionFileNode.asObject().get("x-amz-matdesc").asString() + ); + assertEquals( + "no", + thirdPartyMatDescNode.asObject().get("isOwner").asString() + ); + assertEquals( + "user", + thirdPartyMatDescNode.asObject().get("access-level").asString() + ); + + assertEquals(clientIv, thirdPartyIv); + assertEquals( + clientEncryptedDataKeyAlgorithm, + thirdPartyEncryptedDataKeyAlgorithm + ); + assertNotEquals(clientEncryptedDataKey, thirdPartyEncryptedDataKey); + + try { + ResponseBytes thirdPartyDecryptObject = + thirdPartyClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey).build() + ); + throw new RuntimeException("Expected exception"); + } catch (S3EncryptionClientException e) { + assertTrue(e.getMessage().contains("Unable to RSA-OAEP-SHA1 unwrap")); + } + + ResponseBytes thirdPartyDecryptedObject = + thirdPartyClient.getObjectAsBytes(builder -> + builder + .bucket(BUCKET) + .key(objectKey) + .overrideConfiguration( + withCustomInstructionFileSuffix( + ".third-party-access-instruction-file" + ) + ) + .build() + ); + + assertEquals(input, thirdPartyDecryptedObject.asUtf8String()); + + ResponseBytes clientDecryptedObject = + client.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey).build() + ); + + assertEquals(input, clientDecryptedObject.asUtf8String()); + + deleteObject(BUCKET, objectKey, client); + } + + @Test + public void testReEncryptInstructionFileV2AesToV3() { + final String input = + "Testing re-encryption of instruction file with AES keyrings from V2 to V3"; + final String objectKey = appendTestSuffix( + "v2-aes-to-v3-re-encrypt-instruction-file-test" + ); + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(AES_KEY).addDescription("rotated", "no") + ); + + CryptoConfigurationV2 cryptoConfig = new CryptoConfigurationV2( + CryptoMode.StrictAuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3EncryptionV2 v2OriginalClient = AmazonS3EncryptionClientV2 + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterialsProvider(materialsProvider) + .build(); + + v2OriginalClient.putObject(BUCKET, objectKey, input); + + AesKeyring oldKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "no").build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(oldKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + AesKeyring newKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY_TWO) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "yes").build() + ) + .build(); + + S3EncryptionClient v3RotatedClient = S3EncryptionClient + .builder() + .keyring(newKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + EncryptionMaterialsProvider newMaterialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(AES_KEY_TWO).addDescription("rotated", "yes") + ); + + CryptoConfigurationV2 newCryptoConfig = new CryptoConfigurationV2( + CryptoMode.StrictAuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3EncryptionV2 v2RotatedClient = AmazonS3EncryptionClientV2 + .encryptionBuilder() + .withCryptoConfiguration(newCryptoConfig) + .withEncryptionMaterialsProvider(newMaterialsProvider) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .build(); + + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + + ResponseBytes v3DecryptObject = + v3RotatedClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey).build() + ); + + assertEquals(input, v3DecryptObject.asUtf8String()); + + String v2DecryptObject = v2RotatedClient.getObjectAsString( + BUCKET, + objectKey + ); + assertEquals(input, v2DecryptObject); + + deleteObject(BUCKET, objectKey, v3RotatedClient); + } + + @Test + public void testReEncryptInstructionFileWithCustomSuffixV2RsaToV3() + throws IOException { + final String input = + "Testing re-encryption of instruction file with RSA keyrings from V2 to V3"; + final String objectKey = appendTestSuffix( + "v2-rsa-to-v3-re-encrypt-instruction-file-with-custom-suffix-test" + ); + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR) + .addDescription("isOwner", "yes") + .addDescription("access-level", "admin") + ); + CryptoConfigurationV2 cryptoConfig = new CryptoConfigurationV2( + CryptoMode.AuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3EncryptionV2 v2OriginalClient = AmazonS3EncryptionClientV2 + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterialsProvider(materialsProvider) + .build(); + + v2OriginalClient.putObject(BUCKET, objectKey, input); + + PublicKey clientPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey clientPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair clientPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(clientPublicKey) + .privateKey(clientPrivateKey) + .build(); + + RsaKeyring clientKeyring = RsaKeyring + .builder() + .wrappingKeyPair(clientPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "yes") + .put("access-level", "admin") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(clientKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + PublicKey thirdPartyPublicKey = RSA_KEY_PAIR_TWO.getPublic(); + PrivateKey thirdPartyPrivateKey = RSA_KEY_PAIR_TWO.getPrivate(); + + PartialRsaKeyPair thirdPartyPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(thirdPartyPublicKey) + .privateKey(thirdPartyPrivateKey) + .build(); + + RsaKeyring thirdPartyKeyring = RsaKeyring + .builder() + .wrappingKeyPair(thirdPartyPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "no") + .put("access-level", "user") + .build() + ) + .build(); + + S3EncryptionClient v3ThirdPartyClient = S3EncryptionClient + .builder() + .keyring(thirdPartyKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + EncryptionMaterialsProvider thirdPartyMaterialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR_TWO) + .addDescription("isOwner", "no") + .addDescription("access-level", "user") + ); + + CryptoConfigurationV2 thirdPartyCryptoConfig = new CryptoConfigurationV2( + CryptoMode.AuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3EncryptionV2 v2ThirdPartyRotatedClient = AmazonS3EncryptionClientV2 + .encryptionBuilder() + .withCryptoConfiguration(thirdPartyCryptoConfig) + .withEncryptionMaterialsProvider(thirdPartyMaterialsProvider) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(thirdPartyKeyring) + .instructionFileSuffix("third-party-access-instruction-file") + .build(); + + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + + ResponseBytes v3DecryptObject = + v3OriginalClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey).build() + ); + + assertEquals(input, v3DecryptObject.asUtf8String()); + + String v2DecryptObject = v2OriginalClient.getObjectAsString( + BUCKET, + objectKey + ); + assertEquals(input, v2DecryptObject); + + ResponseBytes thirdPartyDecryptedObject = + v3ThirdPartyClient.getObjectAsBytes(builder -> + builder + .bucket(BUCKET) + .key(objectKey) + .overrideConfiguration( + withCustomInstructionFileSuffix( + ".third-party-access-instruction-file" + ) + ) + .build() + ); + + assertEquals(input, thirdPartyDecryptedObject.asUtf8String()); + + EncryptedGetObjectRequest request = new EncryptedGetObjectRequest( + BUCKET, + objectKey + ) + .withInstructionFileSuffix("third-party-access-instruction-file"); + + String v2ThirdPartyDecryptObject = IOUtils.toString( + v2ThirdPartyRotatedClient.getObject(request).getObjectContent(), + StandardCharsets.UTF_8 + ); + assertEquals(input, v2ThirdPartyDecryptObject); + + deleteObject(BUCKET, objectKey, v3OriginalClient); + } + + @Test + public void testReEncryptInstructionFileV2RsaToV3() { + final String input = + "Testing re-encryption of instruction file with RSA keyrings from V2 to V3"; + final String objectKey = appendTestSuffix( + "v2-rsa-to-v3-re-encrypt-instruction-file-test" + ); + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR).addDescription("rotated", "no") + ); + CryptoConfigurationV2 cryptoConfig = new CryptoConfigurationV2( + CryptoMode.AuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3EncryptionV2 v2OriginalClient = AmazonS3EncryptionClientV2 + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterialsProvider(materialsProvider) + .build(); + + v2OriginalClient.putObject(BUCKET, objectKey, input); + + PublicKey originalPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey originalPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair originalPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(originalPublicKey) + .privateKey(originalPrivateKey) + .build(); + + RsaKeyring clientKeyring = RsaKeyring + .builder() + .wrappingKeyPair(originalPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "no").build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(clientKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + PublicKey newPublicKey = RSA_KEY_PAIR_TWO.getPublic(); + PrivateKey newPrivateKey = RSA_KEY_PAIR_TWO.getPrivate(); + + PartialRsaKeyPair newPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(newPublicKey) + .privateKey(newPrivateKey) + .build(); + + RsaKeyring newKeyring = RsaKeyring + .builder() + .wrappingKeyPair(newPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "yes").build() + ) + .build(); + + S3EncryptionClient v3RotatedClient = S3EncryptionClient + .builder() + .keyring(newKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + EncryptionMaterialsProvider newMaterialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR_TWO) + .addDescription("rotated", "yes") + ); + + CryptoConfigurationV2 newCryptoConfig = new CryptoConfigurationV2( + CryptoMode.AuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3EncryptionV2 v2RotatedClient = AmazonS3EncryptionClientV2 + .encryptionBuilder() + .withCryptoConfiguration(newCryptoConfig) + .withEncryptionMaterialsProvider(newMaterialsProvider) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .build(); + + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + + ResponseBytes v3DecryptObject = + v3RotatedClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey).build() + ); + + assertEquals(input, v3DecryptObject.asUtf8String()); + + String v2DecryptObject = v2RotatedClient.getObjectAsString( + BUCKET, + objectKey + ); + assertEquals(input, v2DecryptObject); + + deleteObject(BUCKET, objectKey, v3OriginalClient); + } + + @Test + public void testReEncryptInstructionFileUpgradesV1AesToV3() { + final String input = + "Testing re-encryption of instruction file, upgrading legacy V1 AES to V3"; + final String objectKey = appendTestSuffix( + "v1-aes-to-v3-re-encrypt-instruction-file-test" + ); + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(AES_KEY) + .addDescription("rotated", "no") + .addDescription("isLegacy", "yes") + ); + + CryptoConfiguration cryptoConfig = new CryptoConfiguration( + CryptoMode.AuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1Client = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterials(materialsProvider) + .build(); + + v1Client.putObject(BUCKET, objectKey, input); + + AesKeyring oldKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "no") + .put("isLegacy", "yes") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(oldKeyring) + .enableLegacyUnauthenticatedModes(true) + .enableLegacyWrappingAlgorithms(true) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + AesKeyring newKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY_TWO) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "yes") + .put("isLegacy", "no") + .build() + ) + .build(); + + S3EncryptionClient v3RotatedClient = S3EncryptionClient + .builder() + .keyring(newKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + EncryptionMaterialsProvider newMaterialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(AES_KEY_TWO) + .addDescription("rotated", "yes") + .addDescription("isLegacy", "no") + ); + + CryptoConfiguration newCryptoConfig = new CryptoConfiguration( + CryptoMode.AuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1RotatedClient = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(newCryptoConfig) + .withEncryptionMaterials(newMaterialsProvider) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .build(); + + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + + ResponseBytes v3DecryptObject = + v3RotatedClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey).build() + ); + assertEquals(input, v3DecryptObject.asUtf8String()); + + ResponseBytes instructionFile = + wrappedClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey + ".instruction").build() + ); + + JsonNodeParser parser = JsonNodeParser.create(); + JsonNode instructionFileNode = parser.parse(instructionFile.asUtf8String()); + String wrappingAlgorithm = instructionFileNode + .asObject() + .get("x-amz-wrap-alg") + .asString(); + assertEquals("AES/GCM", wrappingAlgorithm); + + String v1DecryptObject = v1RotatedClient.getObjectAsString( + BUCKET, + objectKey + ); + assertEquals(input, v1DecryptObject); + + deleteObject(BUCKET, objectKey, v3RotatedClient); + } + + @Test + public void testReEncryptInstructionFileWithCustomSuffixUpgradesV1RsaToV3() + throws IOException { + final String input = + "Testing re-encryption of instruction file, upgrading legacy V1 RSA to V3"; + final String objectKey = appendTestSuffix( + "v1-rsa-to-v3-re-encrypt-instruction-file-with-custom-suffix-test" + ); + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR) + .addDescription("isOwner", "yes") + .addDescription("access-level", "admin") + ); + CryptoConfiguration cryptoConfig = new CryptoConfiguration( + CryptoMode.StrictAuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1OriginalClient = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterials(materialsProvider) + .build(); + + v1OriginalClient.putObject(BUCKET, objectKey, input); + + PublicKey clientPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey clientPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair clientPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(clientPublicKey) + .privateKey(clientPrivateKey) + .build(); + + RsaKeyring clientKeyring = RsaKeyring + .builder() + .wrappingKeyPair(clientPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "yes") + .put("access-level", "admin") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(clientKeyring) + .enableLegacyWrappingAlgorithms(true) + .enableLegacyUnauthenticatedModes(true) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + PublicKey thirdPartyPublicKey = RSA_KEY_PAIR_TWO.getPublic(); + PrivateKey thirdPartyPrivateKey = RSA_KEY_PAIR_TWO.getPrivate(); + + PartialRsaKeyPair thirdPartyPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(thirdPartyPublicKey) + .privateKey(thirdPartyPrivateKey) + .build(); + + RsaKeyring thirdPartyKeyring = RsaKeyring + .builder() + .wrappingKeyPair(thirdPartyPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "no") + .put("access-level", "user") + .build() + ) + .build(); + + S3EncryptionClient v3ThirdPartyClient = S3EncryptionClient + .builder() + .keyring(thirdPartyKeyring) + .enableLegacyWrappingAlgorithms(true) + .enableLegacyUnauthenticatedModes(true) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + EncryptionMaterialsProvider thirdPartyMaterialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR_TWO) + .addDescription("isOwner", "no") + .addDescription("access-level", "user") + ); + + CryptoConfiguration thirdPartyCryptoConfig = new CryptoConfiguration( + CryptoMode.StrictAuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1ThirdPartyRotatedClient = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(thirdPartyCryptoConfig) + .withEncryptionMaterials(thirdPartyMaterialsProvider) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(thirdPartyKeyring) + .instructionFileSuffix("third-party-access-instruction-file") + .build(); + + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + + ResponseBytes v3DecryptObject = + v3OriginalClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey).build() + ); + assertEquals(input, v3DecryptObject.asUtf8String()); + + String v2DecryptObject = v1OriginalClient.getObjectAsString( + BUCKET, + objectKey + ); + assertEquals(input, v2DecryptObject); + + ResponseBytes thirdPartyDecryptedObject = + v3ThirdPartyClient.getObjectAsBytes(builder -> + builder + .bucket(BUCKET) + .key(objectKey) + .overrideConfiguration( + withCustomInstructionFileSuffix( + ".third-party-access-instruction-file" + ) + ) + .build() + ); + + assertEquals(input, thirdPartyDecryptedObject.asUtf8String()); + + EncryptedGetObjectRequest request = new EncryptedGetObjectRequest( + BUCKET, + objectKey + ) + .withInstructionFileSuffix("third-party-access-instruction-file"); + + String v1ThirdPartyDecryptObject = IOUtils.toString( + v1ThirdPartyRotatedClient.getObject(request).getObjectContent(), + StandardCharsets.UTF_8 + ); + assertEquals(input, v1ThirdPartyDecryptObject); + + deleteObject(BUCKET, objectKey, v3OriginalClient); + } + + @Test + public void testReEncryptInstructionFileUpgradesV1RsaToV3() { + final String input = + "Testing re-encryption of instruction file, upgrading legacy V1 RSA to V3"; + final String objectKey = appendTestSuffix( + "v1-rsa-to-v3-re-encrypt-instruction-file-test" + ); + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR) + .addDescription("rotated", "no") + .addDescription("isLegacy", "yes") + ); + CryptoConfiguration cryptoConfig = new CryptoConfiguration( + CryptoMode.StrictAuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1OriginalClient = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterials(materialsProvider) + .build(); + + v1OriginalClient.putObject(BUCKET, objectKey, input); + + PublicKey originalPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey originalPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair originalPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(originalPublicKey) + .privateKey(originalPrivateKey) + .build(); + + RsaKeyring originalKeyring = RsaKeyring + .builder() + .wrappingKeyPair(originalPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "no") + .put("isLegacy", "yes") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(originalKeyring) + .enableLegacyWrappingAlgorithms(true) + .enableLegacyUnauthenticatedModes(true) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + PublicKey newPublicKey = RSA_KEY_PAIR_TWO.getPublic(); + PrivateKey newPrivateKey = RSA_KEY_PAIR_TWO.getPrivate(); + + PartialRsaKeyPair newPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(newPublicKey) + .privateKey(newPrivateKey) + .build(); + + RsaKeyring newKeyring = RsaKeyring + .builder() + .wrappingKeyPair(newPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "yes") + .put("isLegacy", "no") + .build() + ) + .build(); + + S3EncryptionClient v3RotatedClient = S3EncryptionClient + .builder() + .keyring(newKeyring) + .enableLegacyWrappingAlgorithms(true) + .enableLegacyUnauthenticatedModes(true) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + EncryptionMaterialsProvider newMaterialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR_TWO) + .addDescription("rotated", "yes") + .addDescription("isLegacy", "no") + ); + + CryptoConfiguration newCryptoConfig = new CryptoConfiguration( + CryptoMode.StrictAuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1RotatedClient = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(newCryptoConfig) + .withEncryptionMaterials(newMaterialsProvider) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .build(); + + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + + ResponseBytes v3DecryptObject = + v3RotatedClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey).build() + ); + + assertEquals(input, v3DecryptObject.asUtf8String()); + + String v1DecryptObject = v1RotatedClient.getObjectAsString( + BUCKET, + objectKey + ); + assertEquals(input, v1DecryptObject); + + ResponseBytes instructionFile = + wrappedClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey + ".instruction").build() + ); + + JsonNodeParser parser = JsonNodeParser.create(); + JsonNode instructionFileNode = parser.parse(instructionFile.asUtf8String()); + String wrappingAlgorithm = instructionFileNode + .asObject() + .get("x-amz-wrap-alg") + .asString(); + assertEquals("RSA-OAEP-SHA1", wrappingAlgorithm); + + deleteObject(BUCKET, objectKey, v3OriginalClient); + } + + @Test + public void testReEncryptInstructionFileUpgradesV1AesEncryptionOnlyToV3() { + final String input = + "Testing re-encryption of instruction file, upgrading legacy V1 Encryption Only AES to V3"; + final String objectKey = appendTestSuffix( + "v1-aes-encryption-only-to-v3-re-encrypt-instruction-file-test" + ); + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(AES_KEY) + .addDescription("rotated", "no") + .addDescription("isLegacy", "yes") + ); + + CryptoConfiguration cryptoConfig = new CryptoConfiguration( + CryptoMode.EncryptionOnly + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1OriginalClient = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterials(materialsProvider) + .build(); + + v1OriginalClient.putObject(BUCKET, objectKey, input); + + AesKeyring oldKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "no") + .put("isLegacy", "yes") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(oldKeyring) + .enableLegacyUnauthenticatedModes(true) + .enableLegacyWrappingAlgorithms(true) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + AesKeyring newKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY_TWO) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "yes") + .put("isLegacy", "no") + .build() + ) + .build(); + + S3EncryptionClient v3RotatedClient = S3EncryptionClient + .builder() + .keyring(newKeyring) + .enableLegacyWrappingAlgorithms(true) + .enableLegacyUnauthenticatedModes(true) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + EncryptionMaterialsProvider newMaterialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(AES_KEY_TWO) + .addDescription("rotated", "yes") + .addDescription("isLegacy", "no") + ); + + CryptoConfiguration newCryptoConfig = new CryptoConfiguration( + CryptoMode.EncryptionOnly + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1RotatedClient = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(newCryptoConfig) + .withEncryptionMaterials(newMaterialsProvider) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .build(); + + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + + ResponseBytes v3DecryptObject = + v3RotatedClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey).build() + ); + assertEquals(input, v3DecryptObject.asUtf8String()); + + ResponseBytes instructionFile = + wrappedClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey + ".instruction").build() + ); + + JsonNodeParser parser = JsonNodeParser.create(); + JsonNode instructionFileNode = parser.parse(instructionFile.asUtf8String()); + String wrappingAlgorithm = instructionFileNode + .asObject() + .get("x-amz-wrap-alg") + .asString(); + assertEquals("AES/GCM", wrappingAlgorithm); + + try { + String v1DecryptObject = v1RotatedClient.getObjectAsString( + BUCKET, + objectKey + ); + throw new RuntimeException( + "V1 client with EncryptionOnly cannot decrypt content after V3 re-encryption due to AES/GCM algorithm upgrade" + ); + } catch (AmazonClientException e) { + assertTrue( + e + .getMessage() + .contains( + "An exception was thrown when attempting to decrypt the Content Encryption Key" + ) + ); + } + + deleteObject(BUCKET, objectKey, v3RotatedClient); + } + + @Test + public void testReEncryptInstructionFileWithCustomSuffixUpgradesV1RsaEncryptionOnlyToV3() + throws IOException { + final String input = + "Testing re-encryption of instruction file, upgrading legacy V1 Encryption Only RSA to V3"; + final String objectKey = appendTestSuffix( + "v1-rsa-encryption-only-to-v3-re-encrypt-instruction-file-with-custom-suffix-test" + ); + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR) + .addDescription("isOwner", "yes") + .addDescription("access-level", "admin") + ); + CryptoConfiguration cryptoConfig = new CryptoConfiguration( + CryptoMode.EncryptionOnly + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1OriginalClient = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterials(materialsProvider) + .build(); + + v1OriginalClient.putObject(BUCKET, objectKey, input); + + PublicKey clientPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey clientPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair clientPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(clientPublicKey) + .privateKey(clientPrivateKey) + .build(); + + RsaKeyring clientKeyring = RsaKeyring + .builder() + .wrappingKeyPair(clientPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "yes") + .put("access-level", "admin") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(clientKeyring) + .enableLegacyWrappingAlgorithms(true) + .enableLegacyUnauthenticatedModes(true) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + PublicKey thirdPartyPublicKey = RSA_KEY_PAIR_TWO.getPublic(); + PrivateKey thirdPartyPrivateKey = RSA_KEY_PAIR_TWO.getPrivate(); + + PartialRsaKeyPair thirdPartyPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(thirdPartyPublicKey) + .privateKey(thirdPartyPrivateKey) + .build(); + + RsaKeyring thirdPartyKeyring = RsaKeyring + .builder() + .wrappingKeyPair(thirdPartyPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "no") + .put("access-level", "user") + .build() + ) + .build(); + + S3EncryptionClient v3ThirdPartyClient = S3EncryptionClient + .builder() + .keyring(thirdPartyKeyring) + .enableLegacyWrappingAlgorithms(true) + .enableLegacyUnauthenticatedModes(true) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + EncryptionMaterialsProvider thirdPartyMaterialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR_TWO) + .addDescription("isOwner", "no") + .addDescription("access-level", "user") + ); + + CryptoConfiguration thirdPartyCryptoConfig = new CryptoConfiguration( + CryptoMode.EncryptionOnly + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1ThirdPartyRotatedClient = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(thirdPartyCryptoConfig) + .withEncryptionMaterials(thirdPartyMaterialsProvider) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(thirdPartyKeyring) + .instructionFileSuffix("third-party-access-instruction-file") + .build(); + + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + + ResponseBytes v3DecryptObject = + v3OriginalClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey).build() + ); + assertEquals(input, v3DecryptObject.asUtf8String()); + + String v1DecryptObject = v1OriginalClient.getObjectAsString( + BUCKET, + objectKey + ); + assertEquals(input, v1DecryptObject); + + ResponseBytes thirdPartyDecryptedObject = + v3ThirdPartyClient.getObjectAsBytes(builder -> + builder + .bucket(BUCKET) + .key(objectKey) + .overrideConfiguration( + withCustomInstructionFileSuffix( + ".third-party-access-instruction-file" + ) + ) + .build() + ); + + assertEquals(input, thirdPartyDecryptedObject.asUtf8String()); + + EncryptedGetObjectRequest request = new EncryptedGetObjectRequest( + BUCKET, + objectKey + ) + .withInstructionFileSuffix("third-party-access-instruction-file"); + + try { + String v1ThirdPartyDecryptObject = IOUtils.toString( + v1ThirdPartyRotatedClient.getObject(request).getObjectContent(), + StandardCharsets.UTF_8 + ); + throw new RuntimeException( + "V1 client with EncryptionOnly cannot decrypt content after V3 re-encryption due to RSA algorithm upgrade" + ); + } catch (SecurityException e) { + assertTrue( + e + .getMessage() + .contains( + "The content encryption algorithm used at encryption time does not match the algorithm stored for decryption time. The object may be altered or corrupted." + ) + ); + } + + deleteObject(BUCKET, objectKey, v3OriginalClient); + } + + @Test + public void testReEncryptInstructionFileUpgradesV1RsaEncryptionOnlyToV3() + throws IOException { + final String input = + "Testing re-encryption of instruction file, upgrading legacy V1 Encryption Only RSA to V3"; + final String objectKey = appendTestSuffix( + "v1-rsa-encryption-only-to-v3-re-encrypt-instruction-file-test" + ); + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR) + .addDescription("rotated", "no") + .addDescription("isLegacy", "yes") + ); + CryptoConfiguration cryptoConfig = new CryptoConfiguration( + CryptoMode.EncryptionOnly + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1OriginalClient = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterials(materialsProvider) + .build(); + + v1OriginalClient.putObject(BUCKET, objectKey, input); + + PublicKey originalPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey originalPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair originalPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(originalPublicKey) + .privateKey(originalPrivateKey) + .build(); + + RsaKeyring originalKeyring = RsaKeyring + .builder() + .wrappingKeyPair(originalPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "no") + .put("isLegacy", "yes") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(originalKeyring) + .enableLegacyWrappingAlgorithms(true) + .enableLegacyUnauthenticatedModes(true) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + PublicKey newPublicKey = RSA_KEY_PAIR_TWO.getPublic(); + PrivateKey newPrivateKey = RSA_KEY_PAIR_TWO.getPrivate(); + + PartialRsaKeyPair newPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(newPublicKey) + .privateKey(newPrivateKey) + .build(); + + RsaKeyring newKeyring = RsaKeyring + .builder() + .wrappingKeyPair(newPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "yes") + .put("isLegacy", "no") + .build() + ) + .build(); + + S3EncryptionClient v3RotatedClient = S3EncryptionClient + .builder() + .keyring(newKeyring) + .enableLegacyWrappingAlgorithms(true) + .enableLegacyUnauthenticatedModes(true) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + EncryptionMaterialsProvider newMaterialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR_TWO) + .addDescription("rotated", "yes") + .addDescription("isLegacy", "no") + ); + + CryptoConfiguration newCryptoConfig = new CryptoConfiguration( + CryptoMode.EncryptionOnly + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1RotatedClient = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(newCryptoConfig) + .withEncryptionMaterials(newMaterialsProvider) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .build(); + + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + + ResponseBytes v3DecryptObject = + v3RotatedClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey).build() + ); + + assertEquals(input, v3DecryptObject.asUtf8String()); + + try { + String v1DecryptObject = v1RotatedClient.getObjectAsString( + BUCKET, + objectKey + ); + throw new RuntimeException( + "V1 client with EncryptionOnly cannot decrypt content after V3 re-encryption due to RSA algorithm upgrade" + ); + } catch (SecurityException e) { + assertTrue( + e + .getMessage() + .contains( + "The content encryption algorithm used at encryption time does not match the algorithm stored for decryption time. The object may be altered or corrupted." + ) + ); + } + + ResponseBytes instructionFile = + wrappedClient.getObjectAsBytes(builder -> + builder.bucket(BUCKET).key(objectKey + ".instruction").build() + ); + + JsonNodeParser parser = JsonNodeParser.create(); + JsonNode instructionFileNode = parser.parse(instructionFile.asUtf8String()); + String wrappingAlgorithm = instructionFileNode + .asObject() + .get("x-amz-wrap-alg") + .asString(); + assertEquals("RSA-OAEP-SHA1", wrappingAlgorithm); + + deleteObject(BUCKET, objectKey, v3OriginalClient); + } + + @Test + public void testAesKeyringReEncryptInstructionFileEnforceRotation() { + AesKeyring oldKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "no").build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient client = S3EncryptionClient + .builder() + .keyring(oldKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + final String objectKey = appendTestSuffix( + "aes-re-encrypt-instruction-file-enforce-rotation-test" + ); + final String input = + "Testing re-encryption of instruction file with AES Keyring and enforce rotation enabled"; + + client.putObject( + builder -> builder.bucket(BUCKET).key(objectKey).build(), + RequestBody.fromString(input) + ); + + AesKeyring newKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY_TWO) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "yes").build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .enforceRotation(true) + .build(); + + try { + ReEncryptInstructionFileResponse response = + client.reEncryptInstructionFile(reEncryptInstructionFileRequest); + assertTrue(response.enforceRotation()); + } catch (S3EncryptionClientException e) { + fail("Enforce rotation should not throw exception"); + } + + deleteObject(BUCKET, objectKey, client); + } + + @Test + public void testAesKeyringReEncryptInstructionFileEnforceRotationWithSameKey() { + AesKeyring oldKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "no").build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient client = S3EncryptionClient + .builder() + .keyring(oldKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + final String objectKey = appendTestSuffix( + "aes-re-encrypt-instruction-file-enforce-rotation-with-same-key-test" + ); + final String input = + "Testing re-encryption of instruction file with AES keyring and enforce rotation enabled"; + + client.putObject( + builder -> builder.bucket(BUCKET).key(objectKey).build(), + RequestBody.fromString(input) + ); + + AesKeyring newKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "yes").build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .enforceRotation(true) + .build(); + + try { + ReEncryptInstructionFileResponse response = + client.reEncryptInstructionFile(reEncryptInstructionFileRequest); + fail("Enforce rotation should throw exception"); + } catch (S3EncryptionClientException e) { + assertTrue( + e + .getMessage() + .contains( + "Re-encryption failed due to enforced rotation! Old keyring is still able to decrypt the newly encrypted data key" + ) + ); + } + + deleteObject(BUCKET, objectKey, client); + } + + @Test + public void testRsaKeyringReEncryptInstructionFileEnforceRotation() { + PublicKey originalPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey originalPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair originalPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(originalPublicKey) + .privateKey(originalPrivateKey) + .build(); + + RsaKeyring oldKeyring = RsaKeyring + .builder() + .wrappingKeyPair(originalPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "no").build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient client = S3EncryptionClient + .builder() + .keyring(oldKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + final String objectKey = appendTestSuffix( + "rsa-re-encrypt-instruction-file-enforce-rotation-test" + ); + final String input = + "Testing re-encryption of instruction file with RSA Keyring and enforce rotation enabled"; + + client.putObject( + builder -> builder.bucket(BUCKET).key(objectKey).build(), + RequestBody.fromString(input) + ); + + PublicKey newPublicKey = RSA_KEY_PAIR_TWO.getPublic(); + PrivateKey newPrivateKey = RSA_KEY_PAIR_TWO.getPrivate(); + + PartialRsaKeyPair newPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(newPublicKey) + .privateKey(newPrivateKey) + .build(); + + RsaKeyring newKeyring = RsaKeyring + .builder() + .wrappingKeyPair(newPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "yes").build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .enforceRotation(true) + .build(); + + try { + ReEncryptInstructionFileResponse response = + client.reEncryptInstructionFile(reEncryptInstructionFileRequest); + assertTrue(response.enforceRotation()); + } catch (S3EncryptionClientException e) { + fail("Enforce rotation should not throw exception"); + } + + deleteObject(BUCKET, objectKey, client); + } + + @Test + public void testRsaKeyringReEncryptInstructionFileEnforceRotationWithSameKey() { + PublicKey originalPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey originalPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair originalPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(originalPublicKey) + .privateKey(originalPrivateKey) + .build(); + + RsaKeyring oldKeyring = RsaKeyring + .builder() + .wrappingKeyPair(originalPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "no").build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient client = S3EncryptionClient + .builder() + .keyring(oldKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + final String objectKey = appendTestSuffix( + "rsa-re-encrypt-instruction-file-enforce-rotation-with-same-key-test" + ); + final String input = + "Testing re-encryption of instruction file with RSA Keyring and enforce rotation enabled"; + + client.putObject( + builder -> builder.bucket(BUCKET).key(objectKey).build(), + RequestBody.fromString(input) + ); + + RsaKeyring newKeyring = RsaKeyring + .builder() + .wrappingKeyPair(originalPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "yes").build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .enforceRotation(true) + .build(); + + try { + ReEncryptInstructionFileResponse response = + client.reEncryptInstructionFile(reEncryptInstructionFileRequest); + fail("Enforce rotation should throw exception"); + } catch (S3EncryptionClientException e) { + assertTrue( + e + .getMessage() + .contains( + "Re-encryption failed due to enforced rotation! Old keyring is still able to decrypt the newly encrypted data key" + ) + ); + } + + deleteObject(BUCKET, objectKey, client); + } + + @Test + public void testRsaKeyringReEncryptInstructionFileWithCustomSuffixEnforceRotation() { + PublicKey clientPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey clientPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair clientPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(clientPublicKey) + .privateKey(clientPrivateKey) + .build(); + + RsaKeyring clientKeyring = RsaKeyring + .builder() + .wrappingKeyPair(clientPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "yes") + .put("access-level", "admin") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient client = S3EncryptionClient + .builder() + .keyring(clientKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + final String objectKey = appendTestSuffix( + "rsa-re-encrypt-instruction-file-enforce-rotation-with-custom-suffix-test" + ); + final String input = + "Testing re-encryption of instruction file with RSA Keyring and enforce rotation enabled"; + + client.putObject( + builder -> builder.bucket(BUCKET).key(objectKey).build(), + RequestBody.fromString(input) + ); + + PublicKey thirdPartyPublicKey = RSA_KEY_PAIR_TWO.getPublic(); + PrivateKey thirdPartyPrivateKey = RSA_KEY_PAIR_TWO.getPrivate(); + + PartialRsaKeyPair thirdPartyPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(thirdPartyPublicKey) + .privateKey(thirdPartyPrivateKey) + .build(); + + RsaKeyring thirdPartyKeyring = RsaKeyring + .builder() + .wrappingKeyPair(thirdPartyPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "no") + .put("access-level", "user") + .build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(thirdPartyKeyring) + .enforceRotation(true) + .instructionFileSuffix("third-party-access-instruction-file") + .build(); + + try { + ReEncryptInstructionFileResponse response = + client.reEncryptInstructionFile(reEncryptInstructionFileRequest); + assertTrue(response.enforceRotation()); + } catch (S3EncryptionClientException e) { + fail("Enforce rotation should not throw exception"); + } + + deleteObject(BUCKET, objectKey, client); + } + + @Test + public void testRsaKeyringReEncryptInstructionFileWithCustomSuffixEnforceRotationWithSameKey() { + PublicKey clientPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey clientPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair clientPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(clientPublicKey) + .privateKey(clientPrivateKey) + .build(); + + RsaKeyring clientKeyring = RsaKeyring + .builder() + .wrappingKeyPair(clientPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "yes") + .put("access-level", "admin") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient client = S3EncryptionClient + .builder() + .keyring(clientKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + final String objectKey = appendTestSuffix( + "rsa-re-encrypt-instruction-file-enforce-rotation-with-custom-suffix-and-same-key-test" + ); + final String input = + "Testing re-encryption of instruction file with RSA Keyring and enforce rotation enabled"; + + client.putObject( + builder -> builder.bucket(BUCKET).key(objectKey).build(), + RequestBody.fromString(input) + ); + + RsaKeyring thirdPartyKeyring = RsaKeyring + .builder() + .wrappingKeyPair(clientPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "no") + .put("access-level", "user") + .build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(thirdPartyKeyring) + .enforceRotation(true) + .instructionFileSuffix("third-party-access-instruction-file") + .build(); + + try { + ReEncryptInstructionFileResponse response = + client.reEncryptInstructionFile(reEncryptInstructionFileRequest); + fail("Enforce rotation should throw exception"); + } catch (S3EncryptionClientException e) { + assertTrue( + e + .getMessage() + .contains( + "Re-encryption failed due to enforced rotation! Old keyring is still able to decrypt the newly encrypted data key" + ) + ); + } + + deleteObject(BUCKET, objectKey, client); + } + + @Test + public void testAesKeyringReEncryptInstructionFileV1ToV3UpgradeEnforceRotation() { + final String objectKey = appendTestSuffix( + "v1-aes-to-v3-re-encrypt-instruction-file-with-enforce-rotation-test" + ); + final String input = + "Testing re-encryption of instruction file from V1 to V3 with AES Keyring and enforce rotation enabled"; + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(AES_KEY) + .addDescription("rotated", "no") + .addDescription("isLegacy", "yes") + ); + + CryptoConfiguration cryptoConfig = new CryptoConfiguration( + CryptoMode.AuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1Client = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterials(materialsProvider) + .build(); + + v1Client.putObject(BUCKET, objectKey, input); + + AesKeyring oldKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "no") + .put("isLegacy", "yes") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(oldKeyring) + .enableLegacyUnauthenticatedModes(true) + .enableLegacyWrappingAlgorithms(true) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + AesKeyring newKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY_TWO) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "yes") + .put("isLegacy", "no") + .build() + ) + .build(); + + S3EncryptionClient v3RotatedClient = S3EncryptionClient + .builder() + .keyring(newKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .enforceRotation(true) + .build(); + + try { + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + assertTrue(response.enforceRotation()); + } catch (S3EncryptionClientException e) { + fail("Enforce rotation should not throw exception"); + } + + deleteObject(BUCKET, objectKey, v3RotatedClient); + } + + @Test + public void testAesKeyringReEncryptInstructionFileV1ToV3UpgradeEnforceRotationWithSameKey() { + final String objectKey = appendTestSuffix( + "v1-aes-to-v3-re-encrypt-instruction-file-with-enforce-rotation-same-key-test" + ); + final String input = + "Testing re-encryption of instruction file from V1 to V3 with AES Keyring and enforce rotation enabled"; + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(AES_KEY) + .addDescription("rotated", "no") + .addDescription("isLegacy", "yes") + ); + + CryptoConfiguration cryptoConfig = new CryptoConfiguration( + CryptoMode.StrictAuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1Client = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterials(materialsProvider) + .build(); + + v1Client.putObject(BUCKET, objectKey, input); + + AesKeyring oldKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "no") + .put("isLegacy", "yes") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(oldKeyring) + .enableLegacyUnauthenticatedModes(true) + .enableLegacyWrappingAlgorithms(true) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + AesKeyring newKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "yes") + .put("isLegacy", "no") + .build() + ) + .build(); + + S3EncryptionClient v3RotatedClient = S3EncryptionClient + .builder() + .keyring(newKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .enforceRotation(true) + .build(); + + try { + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + fail("Enforce rotation should throw exception"); + } catch (S3EncryptionClientException e) { + assertTrue( + e + .getMessage() + .contains( + "Re-encryption failed due to enforced rotation! Old keyring is still able to decrypt the newly encrypted data key" + ) + ); + } + + deleteObject(BUCKET, objectKey, v3RotatedClient); + } + + @Test + public void testAesKeyringReEncryptInstructionFileV2ToV3EnforceRotationWithSameKey() { + final String objectKey = appendTestSuffix( + "v2-aes-to-v3-re-encrypt-instruction-file-with-enforce-rotation-same-key-test" + ); + final String input = + "Testing re-encryption of instruction file from V2 to V3 with AES Keyring and enforce rotation enabled"; + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(AES_KEY).addDescription("rotated", "no") + ); + + CryptoConfigurationV2 cryptoConfig = new CryptoConfigurationV2( + CryptoMode.AuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3EncryptionV2 v2OriginalClient = AmazonS3EncryptionClientV2 + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterialsProvider(materialsProvider) + .build(); + + v2OriginalClient.putObject(BUCKET, objectKey, input); + + AesKeyring oldKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "no").build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(oldKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + AesKeyring newKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "yes").build() + ) + .build(); + + S3EncryptionClient v3RotatedClient = S3EncryptionClient + .builder() + .keyring(newKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .enforceRotation(true) + .build(); + + try { + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + fail("Enforce rotation should throw exception"); + } catch (S3EncryptionClientException e) { + assertTrue( + e + .getMessage() + .contains( + "Re-encryption failed due to enforced rotation! Old keyring is still able to decrypt the newly encrypted data key" + ) + ); + } + + deleteObject(BUCKET, objectKey, v3RotatedClient); + } + + @Test + public void testAesKeyringReEncryptInstructionFileV2ToV3EnforceRotation() { + final String objectKey = appendTestSuffix( + "v2-aes-to-v3-re-encrypt-instruction-file-with-enforce-rotation-test" + ); + final String input = + "Testing re-encryption of instruction file from V2 to V3 with AES Keyring and enforce rotation enabled"; + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(AES_KEY).addDescription("rotated", "no") + ); + + CryptoConfigurationV2 cryptoConfig = new CryptoConfigurationV2( + CryptoMode.StrictAuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3EncryptionV2 v2OriginalClient = AmazonS3EncryptionClientV2 + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterialsProvider(materialsProvider) + .build(); + + v2OriginalClient.putObject(BUCKET, objectKey, input); + + AesKeyring oldKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "no").build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(oldKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + AesKeyring newKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY_TWO) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "yes").build() + ) + .build(); + + S3EncryptionClient v3RotatedClient = S3EncryptionClient + .builder() + .keyring(newKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .enforceRotation(true) + .build(); + + try { + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + assertTrue(response.enforceRotation()); + } catch (S3EncryptionClientException e) { + fail("Enforce rotation should not throw exception"); + } + + deleteObject(BUCKET, objectKey, v3RotatedClient); + } + + @Test + public void testRsaKeyringReEncryptInstructionFileWithCustomSuffixV2ToV3EnforceRotation() { + final String input = + "Testing re-encryption of instruction file with RSA keyrings from V2 to V3"; + final String objectKey = appendTestSuffix( + "v2-rsa-to-v3-re-encrypt-instruction-file-with-custom-suffix-enforce-rotation-test" + ); + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR) + .addDescription("isOwner", "yes") + .addDescription("access-level", "admin") + ); + CryptoConfigurationV2 cryptoConfig = new CryptoConfigurationV2( + CryptoMode.AuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3EncryptionV2 v2OriginalClient = AmazonS3EncryptionClientV2 + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterialsProvider(materialsProvider) + .build(); + + v2OriginalClient.putObject(BUCKET, objectKey, input); + + PublicKey clientPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey clientPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair clientPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(clientPublicKey) + .privateKey(clientPrivateKey) + .build(); + + RsaKeyring clientKeyring = RsaKeyring + .builder() + .wrappingKeyPair(clientPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "yes") + .put("access-level", "admin") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(clientKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + PublicKey thirdPartyPublicKey = RSA_KEY_PAIR_TWO.getPublic(); + PrivateKey thirdPartyPrivateKey = RSA_KEY_PAIR_TWO.getPrivate(); + + PartialRsaKeyPair thirdPartyPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(thirdPartyPublicKey) + .privateKey(thirdPartyPrivateKey) + .build(); + + RsaKeyring thirdPartyKeyring = RsaKeyring + .builder() + .wrappingKeyPair(thirdPartyPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "no") + .put("access-level", "user") + .build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(thirdPartyKeyring) + .instructionFileSuffix("third-party-access-instruction-file") + .enforceRotation(true) + .build(); + + try { + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + assertTrue(response.enforceRotation()); + } catch (S3EncryptionClientException e) { + fail("Enforce rotation should not throw exception"); + } + + deleteObject(BUCKET, objectKey, v3OriginalClient); + } + + @Test + public void testRsaKeyringReEncryptInstructionFileWithCustomSuffixV2ToV3EnforceRotationWithSameKey() { + final String input = + "Testing re-encryption of instruction file with RSA keyrings from V2 to V3"; + final String objectKey = appendTestSuffix( + "v2-rsa-to-v3-re-encrypt-instruction-file-with-custom-suffix-enforce-rotation-same-key-test" + ); + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR) + .addDescription("isOwner", "yes") + .addDescription("access-level", "admin") + ); + CryptoConfigurationV2 cryptoConfig = new CryptoConfigurationV2( + CryptoMode.StrictAuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3EncryptionV2 v2OriginalClient = AmazonS3EncryptionClientV2 + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterialsProvider(materialsProvider) + .build(); + + v2OriginalClient.putObject(BUCKET, objectKey, input); + + PublicKey clientPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey clientPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair clientPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(clientPublicKey) + .privateKey(clientPrivateKey) + .build(); + + RsaKeyring clientKeyring = RsaKeyring + .builder() + .wrappingKeyPair(clientPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "yes") + .put("access-level", "admin") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(clientKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + RsaKeyring thirdPartyKeyring = RsaKeyring + .builder() + .wrappingKeyPair(clientPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "no") + .put("access-level", "user") + .build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(thirdPartyKeyring) + .instructionFileSuffix("third-party-access-instruction-file") + .enforceRotation(true) + .build(); + + try { + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + fail("Enforce rotation should throw exception"); + } catch (S3EncryptionClientException e) { + assertTrue( + e + .getMessage() + .contains( + "Re-encryption failed due to enforced rotation! Old keyring is still able to decrypt the newly encrypted data key" + ) + ); + } + + deleteObject(BUCKET, objectKey, v3OriginalClient); + } + + @Test + public void testRsaKeyringReEncryptInstructionFileV2ToV3EnforceRotation() { + final String input = + "Testing re-encryption of instruction file with RSA keyrings from V2 to V3"; + final String objectKey = appendTestSuffix( + "v2-rsa-to-v3-re-encrypt-instruction-file-enforce-rotation-test" + ); + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR).addDescription("rotated", "no") + ); + CryptoConfigurationV2 cryptoConfig = new CryptoConfigurationV2( + CryptoMode.AuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3EncryptionV2 v2OriginalClient = AmazonS3EncryptionClientV2 + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterialsProvider(materialsProvider) + .build(); + + v2OriginalClient.putObject(BUCKET, objectKey, input); + + PublicKey originalPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey originalPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair originalPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(originalPublicKey) + .privateKey(originalPrivateKey) + .build(); + + RsaKeyring clientKeyring = RsaKeyring + .builder() + .wrappingKeyPair(originalPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "no").build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(clientKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + PublicKey newPublicKey = RSA_KEY_PAIR_TWO.getPublic(); + PrivateKey newPrivateKey = RSA_KEY_PAIR_TWO.getPrivate(); + + PartialRsaKeyPair newPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(newPublicKey) + .privateKey(newPrivateKey) + .build(); + + RsaKeyring newKeyring = RsaKeyring + .builder() + .wrappingKeyPair(newPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "yes").build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .enforceRotation(true) + .build(); + + try { + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + assertTrue(response.enforceRotation()); + } catch (S3EncryptionClientException e) { + fail("Enforce rotation should not throw exception"); + } + + deleteObject(BUCKET, objectKey, v3OriginalClient); + } + + @Test + public void testRsaKeyringReEncryptInstructionFileV2ToV3EnforceRotationWithSameKey() { + final String input = + "Testing re-encryption of instruction file with RSA keyrings from V2 to V3"; + final String objectKey = appendTestSuffix( + "v2-rsa-to-v3-re-encrypt-instruction-file-enforce-rotation-same-key-test" + ); + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR).addDescription("rotated", "no") + ); + CryptoConfigurationV2 cryptoConfig = new CryptoConfigurationV2( + CryptoMode.AuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3EncryptionV2 v2OriginalClient = AmazonS3EncryptionClientV2 + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterialsProvider(materialsProvider) + .build(); + + v2OriginalClient.putObject(BUCKET, objectKey, input); + + PublicKey originalPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey originalPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair originalPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(originalPublicKey) + .privateKey(originalPrivateKey) + .build(); + + RsaKeyring clientKeyring = RsaKeyring + .builder() + .wrappingKeyPair(originalPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "no").build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(clientKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + RsaKeyring newKeyring = RsaKeyring + .builder() + .wrappingKeyPair(originalPartialRsaKeyPair) + .materialsDescription( + MaterialsDescription.builder().put("rotated", "yes").build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .enforceRotation(true) + .build(); + + try { + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + fail("Enforce rotation should throw exception"); + } catch (S3EncryptionClientException e) { + assertTrue( + e + .getMessage() + .contains( + "Re-encryption failed due to enforced rotation! Old keyring is still able to decrypt the newly encrypted data key" + ) + ); + } + + deleteObject(BUCKET, objectKey, v3OriginalClient); + } + + @Test + public void testRsaKeyringReEncryptInstructionFileV1ToV3EnforceRotation() { + final String input = + "Testing re-encryption of instruction file with RSA keyrings from V1 to V3"; + final String objectKey = appendTestSuffix( + "v1-rsa-to-v3-re-encrypt-instruction-file-enforce-rotation-test" + ); + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR) + .addDescription("rotated", "no") + .addDescription("isLegacy", "yes") + ); + CryptoConfiguration cryptoConfig = new CryptoConfiguration( + CryptoMode.AuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1OriginalClient = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterials(materialsProvider) + .build(); + + v1OriginalClient.putObject(BUCKET, objectKey, input); + + PublicKey originalPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey originalPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair originalPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(originalPublicKey) + .privateKey(originalPrivateKey) + .build(); + + RsaKeyring originalKeyring = RsaKeyring + .builder() + .wrappingKeyPair(originalPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "no") + .put("isLegacy", "yes") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(originalKeyring) + .enableLegacyWrappingAlgorithms(true) + .enableLegacyUnauthenticatedModes(true) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + PublicKey newPublicKey = RSA_KEY_PAIR_TWO.getPublic(); + PrivateKey newPrivateKey = RSA_KEY_PAIR_TWO.getPrivate(); + + PartialRsaKeyPair newPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(newPublicKey) + .privateKey(newPrivateKey) + .build(); + + RsaKeyring newKeyring = RsaKeyring + .builder() + .wrappingKeyPair(newPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "yes") + .put("isLegacy", "no") + .build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .enforceRotation(true) + .build(); + + try { + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + assertTrue(response.enforceRotation()); + } catch (S3EncryptionClientException e) { + fail("Enforce rotation should not throw exception"); + } + + deleteObject(BUCKET, objectKey, v3OriginalClient); + } + + @Test + public void testRsaKeyringReEncryptInstructionFileV1ToV3EnforceRotationWithSameKey() { + final String input = + "Testing re-encryption of instruction file with RSA keyrings from V1 to V3"; + final String objectKey = appendTestSuffix( + "v1-rsa-to-v3-re-encrypt-instruction-file-enforce-rotation-same-key-test" + ); + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR) + .addDescription("rotated", "no") + .addDescription("isLegacy", "yes") + ); + CryptoConfiguration cryptoConfig = new CryptoConfiguration( + CryptoMode.StrictAuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1OriginalClient = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterials(materialsProvider) + .build(); + + v1OriginalClient.putObject(BUCKET, objectKey, input); + + PublicKey originalPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey originalPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair originalPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(originalPublicKey) + .privateKey(originalPrivateKey) + .build(); + + RsaKeyring originalKeyring = RsaKeyring + .builder() + .wrappingKeyPair(originalPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "no") + .put("isLegacy", "yes") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(originalKeyring) + .enableLegacyWrappingAlgorithms(true) + .enableLegacyUnauthenticatedModes(true) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + RsaKeyring newKeyring = RsaKeyring + .builder() + .wrappingKeyPair(originalPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "yes") + .put("isLegacy", "no") + .build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .enforceRotation(true) + .build(); + + try { + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + fail("Enforce rotation should throw exception"); + } catch (S3EncryptionClientException e) { + assertTrue( + e + .getMessage() + .contains( + "Re-encryption failed due to enforced rotation! Old keyring is still able to decrypt the newly encrypted data key" + ) + ); + } + + deleteObject(BUCKET, objectKey, v3OriginalClient); + } + + @Test + public void testRsaKeyringReEncryptInstructionFileWithCustomSuffixV1ToV3EnforceRotation() { + final String input = + "Testing re-encryption of instruction file with RSA keyrings from V1 to V3"; + final String objectKey = appendTestSuffix( + "v1-rsa-to-v3-re-encrypt-instruction-file-with-custom-suffix-enforce-rotation-test" + ); + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR) + .addDescription("isOwner", "yes") + .addDescription("access-level", "admin") + ); + CryptoConfiguration cryptoConfig = new CryptoConfiguration( + CryptoMode.AuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1OriginalClient = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterials(materialsProvider) + .build(); + + v1OriginalClient.putObject(BUCKET, objectKey, input); + + PublicKey clientPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey clientPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair clientPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(clientPublicKey) + .privateKey(clientPrivateKey) + .build(); + + RsaKeyring clientKeyring = RsaKeyring + .builder() + .wrappingKeyPair(clientPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "yes") + .put("access-level", "admin") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(clientKeyring) + .enableLegacyWrappingAlgorithms(true) + .enableLegacyUnauthenticatedModes(true) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + PublicKey thirdPartyPublicKey = RSA_KEY_PAIR_TWO.getPublic(); + PrivateKey thirdPartyPrivateKey = RSA_KEY_PAIR_TWO.getPrivate(); + + PartialRsaKeyPair thirdPartyPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(thirdPartyPublicKey) + .privateKey(thirdPartyPrivateKey) + .build(); + + RsaKeyring thirdPartyKeyring = RsaKeyring + .builder() + .wrappingKeyPair(thirdPartyPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "no") + .put("access-level", "user") + .build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(thirdPartyKeyring) + .instructionFileSuffix("third-party-access-instruction-file") + .enforceRotation(true) + .build(); + + try { + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + assertTrue(response.enforceRotation()); + } catch (S3EncryptionClientException e) { + fail("Enforce rotation should not throw exception"); + } + + deleteObject(BUCKET, objectKey, v3OriginalClient); + } + + @Test + public void testRsaKeyringReEncryptInstructionFileWithCustomSuffixV1ToV3EnforceRotationWithSameKey() { + final String input = + "Testing re-encryption of instruction file with RSA keyrings from V1 to V3"; + final String objectKey = appendTestSuffix( + "v1-rsa-to-v3-re-encrypt-instruction-file-with-custom-suffix-enforce-rotation-same-key-test" + ); + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR) + .addDescription("isOwner", "yes") + .addDescription("access-level", "admin") + ); + CryptoConfiguration cryptoConfig = new CryptoConfiguration( + CryptoMode.StrictAuthenticatedEncryption + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1OriginalClient = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterials(materialsProvider) + .build(); + + v1OriginalClient.putObject(BUCKET, objectKey, input); + + PublicKey clientPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey clientPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair clientPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(clientPublicKey) + .privateKey(clientPrivateKey) + .build(); + + RsaKeyring clientKeyring = RsaKeyring + .builder() + .wrappingKeyPair(clientPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "yes") + .put("access-level", "admin") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(clientKeyring) + .enableLegacyWrappingAlgorithms(true) + .enableLegacyUnauthenticatedModes(true) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + RsaKeyring thirdPartyKeyring = RsaKeyring + .builder() + .wrappingKeyPair(clientPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "no") + .put("access-level", "user") + .build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(thirdPartyKeyring) + .instructionFileSuffix("third-party-access-instruction-file") + .enforceRotation(true) + .build(); + + try { + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + fail("Enforce rotation should throw exception"); + } catch (S3EncryptionClientException e) { + assertTrue( + e + .getMessage() + .contains( + "Re-encryption failed due to enforced rotation! Old keyring is still able to decrypt the newly encrypted data key" + ) + ); + } + + deleteObject(BUCKET, objectKey, v3OriginalClient); + } + + @Test + public void testRsaKeyringReEncryptInstructionFileWithCustomSuffixV1ToV3EnforceRotationEncryptionOnly() { + final String input = + "Testing re-encryption of instruction file with RSA keyrings from V1 to V3"; + final String objectKey = appendTestSuffix( + "v1-rsa-to-v3-re-encrypt-instruction-file-with-custom-suffix-enforce-rotation-encryption-only-test" + ); + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR) + .addDescription("isOwner", "yes") + .addDescription("access-level", "admin") + ); + CryptoConfiguration cryptoConfig = new CryptoConfiguration( + CryptoMode.EncryptionOnly + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1OriginalClient = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterials(materialsProvider) + .build(); + + v1OriginalClient.putObject(BUCKET, objectKey, input); + + PublicKey clientPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey clientPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair clientPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(clientPublicKey) + .privateKey(clientPrivateKey) + .build(); + + RsaKeyring clientKeyring = RsaKeyring + .builder() + .wrappingKeyPair(clientPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "yes") + .put("access-level", "admin") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(clientKeyring) + .enableLegacyWrappingAlgorithms(true) + .enableLegacyUnauthenticatedModes(true) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + PublicKey thirdPartyPublicKey = RSA_KEY_PAIR_TWO.getPublic(); + PrivateKey thirdPartyPrivateKey = RSA_KEY_PAIR_TWO.getPrivate(); + + PartialRsaKeyPair thirdPartyPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(thirdPartyPublicKey) + .privateKey(thirdPartyPrivateKey) + .build(); + + RsaKeyring thirdPartyKeyring = RsaKeyring + .builder() + .wrappingKeyPair(thirdPartyPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "no") + .put("access-level", "user") + .build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(thirdPartyKeyring) + .instructionFileSuffix("third-party-access-instruction-file") + .enforceRotation(true) + .build(); + + try { + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + assertTrue(response.enforceRotation()); + } catch (S3EncryptionClientException e) { + fail("Enforce rotation should not throw exception"); + } + + deleteObject(BUCKET, objectKey, v3OriginalClient); + } + + @Test + public void testRsaKeyringReEncryptInstructionFileWithCustomSuffixV1ToV3EnforceRotationWithSameKeyEncryptionOnly() { + final String input = + "Testing re-encryption of instruction file with RSA keyrings from V1 to V3"; + final String objectKey = appendTestSuffix( + "v1-rsa-to-v3-re-encrypt-instruction-file-with-custom-suffix-enforce-rotation-same-key-encryption-only-test" + ); + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR) + .addDescription("isOwner", "yes") + .addDescription("access-level", "admin") + ); + CryptoConfiguration cryptoConfig = new CryptoConfiguration( + CryptoMode.EncryptionOnly + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1OriginalClient = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterials(materialsProvider) + .build(); + + v1OriginalClient.putObject(BUCKET, objectKey, input); + + PublicKey clientPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey clientPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair clientPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(clientPublicKey) + .privateKey(clientPrivateKey) + .build(); + + RsaKeyring clientKeyring = RsaKeyring + .builder() + .wrappingKeyPair(clientPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "yes") + .put("access-level", "admin") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(clientKeyring) + .enableLegacyWrappingAlgorithms(true) + .enableLegacyUnauthenticatedModes(true) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + RsaKeyring thirdPartyKeyring = RsaKeyring + .builder() + .wrappingKeyPair(clientPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("isOwner", "no") + .put("access-level", "user") + .build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(thirdPartyKeyring) + .instructionFileSuffix("third-party-access-instruction-file") + .enforceRotation(true) + .build(); + + try { + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + fail("Enforce rotation should throw exception"); + } catch (S3EncryptionClientException e) { + assertTrue( + e + .getMessage() + .contains( + "Re-encryption failed due to enforced rotation! Old keyring is still able to decrypt the newly encrypted data key" + ) + ); + } + + deleteObject(BUCKET, objectKey, v3OriginalClient); + } + + @Test + public void testRsaKeyringReEncryptInstructionFileV1ToV3EnforceRotationEncryptionOnly() { + final String input = + "Testing re-encryption of instruction file with RSA keyrings from V1 to V3"; + final String objectKey = appendTestSuffix( + "v1-rsa-to-v3-re-encrypt-instruction-file-enforce-rotation-encryption-only-test" + ); + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR) + .addDescription("rotated", "no") + .addDescription("isLegacy", "yes") + ); + CryptoConfiguration cryptoConfig = new CryptoConfiguration( + CryptoMode.EncryptionOnly + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1OriginalClient = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterials(materialsProvider) + .build(); + + v1OriginalClient.putObject(BUCKET, objectKey, input); + + PublicKey originalPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey originalPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair originalPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(originalPublicKey) + .privateKey(originalPrivateKey) + .build(); + + RsaKeyring originalKeyring = RsaKeyring + .builder() + .wrappingKeyPair(originalPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "no") + .put("isLegacy", "yes") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(originalKeyring) + .enableLegacyWrappingAlgorithms(true) + .enableLegacyUnauthenticatedModes(true) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + PublicKey newPublicKey = RSA_KEY_PAIR_TWO.getPublic(); + PrivateKey newPrivateKey = RSA_KEY_PAIR_TWO.getPrivate(); + + PartialRsaKeyPair newPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(newPublicKey) + .privateKey(newPrivateKey) + .build(); + + RsaKeyring newKeyring = RsaKeyring + .builder() + .wrappingKeyPair(newPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "yes") + .put("isLegacy", "no") + .build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .enforceRotation(true) + .build(); + + try { + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + assertTrue(response.enforceRotation()); + } catch (S3EncryptionClientException e) { + fail("Enforce rotation should not throw exception"); + } + + deleteObject(BUCKET, objectKey, v3OriginalClient); + } + + @Test + public void testRsaKeyringReEncryptInstructionFileV1ToV3EnforceRotationWithSameKeyEncryptionOnly() { + final String input = + "Testing re-encryption of instruction file with RSA keyrings from V1 to V3"; + final String objectKey = appendTestSuffix( + "v1-rsa-to-v3-re-encrypt-instruction-file-enforce-rotation-same-key-encryption-only-test" + ); + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(RSA_KEY_PAIR) + .addDescription("rotated", "no") + .addDescription("isLegacy", "yes") + ); + CryptoConfiguration cryptoConfig = new CryptoConfiguration( + CryptoMode.EncryptionOnly + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1OriginalClient = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterials(materialsProvider) + .build(); + + v1OriginalClient.putObject(BUCKET, objectKey, input); + + PublicKey originalPublicKey = RSA_KEY_PAIR.getPublic(); + PrivateKey originalPrivateKey = RSA_KEY_PAIR.getPrivate(); + + PartialRsaKeyPair originalPartialRsaKeyPair = PartialRsaKeyPair + .builder() + .publicKey(originalPublicKey) + .privateKey(originalPrivateKey) + .build(); + + RsaKeyring originalKeyring = RsaKeyring + .builder() + .wrappingKeyPair(originalPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "no") + .put("isLegacy", "yes") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(originalKeyring) + .enableLegacyWrappingAlgorithms(true) + .enableLegacyUnauthenticatedModes(true) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + RsaKeyring newKeyring = RsaKeyring + .builder() + .wrappingKeyPair(originalPartialRsaKeyPair) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "yes") + .put("isLegacy", "no") + .build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .enforceRotation(true) + .build(); + + try { + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + fail("Enforce rotation should throw exception"); + } catch (S3EncryptionClientException e) { + assertTrue( + e + .getMessage() + .contains( + "Re-encryption failed due to enforced rotation! Old keyring is still able to decrypt the newly encrypted data key" + ) + ); + } + + deleteObject(BUCKET, objectKey, v3OriginalClient); + } + + @Test + public void testAesKeyringReEncryptInstructionFileV1ToV3UpgradeEnforceRotationEncryptionOnly() { + final String objectKey = appendTestSuffix( + "v1-aes-to-v3-re-encrypt-instruction-file-with-enforce-rotation-encryption-only-test" + ); + final String input = + "Testing re-encryption of instruction file from V1 to V3 with AES Keyring and enforce rotation enabled"; + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(AES_KEY) + .addDescription("rotated", "no") + .addDescription("isLegacy", "yes") + ); + + CryptoConfiguration cryptoConfig = new CryptoConfiguration( + CryptoMode.EncryptionOnly + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1Client = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterials(materialsProvider) + .build(); + + v1Client.putObject(BUCKET, objectKey, input); + + AesKeyring oldKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "no") + .put("isLegacy", "yes") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(oldKeyring) + .enableLegacyUnauthenticatedModes(true) + .enableLegacyWrappingAlgorithms(true) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + AesKeyring newKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY_TWO) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "yes") + .put("isLegacy", "no") + .build() + ) + .build(); + + S3EncryptionClient v3RotatedClient = S3EncryptionClient + .builder() + .keyring(newKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .enforceRotation(true) + .build(); + + try { + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + assertTrue(response.enforceRotation()); + } catch (S3EncryptionClientException e) { + fail("Enforce rotation should not throw exception"); + } + + deleteObject(BUCKET, objectKey, v3RotatedClient); + } + + @Test + public void testAesKeyringReEncryptInstructionFileV1ToV3UpgradeEnforceRotationWithSameKeyEncryptionOnly() { + final String objectKey = appendTestSuffix( + "v1-aes-to-v3-re-encrypt-instruction-file-with-enforce-rotation-same-key-encryption-only-test" + ); + final String input = + "Testing re-encryption of instruction file from V1 to V3 with AES Keyring and enforce rotation enabled"; + + EncryptionMaterialsProvider materialsProvider = + new StaticEncryptionMaterialsProvider( + new EncryptionMaterials(AES_KEY) + .addDescription("rotated", "no") + .addDescription("isLegacy", "yes") + ); + + CryptoConfiguration cryptoConfig = new CryptoConfiguration( + CryptoMode.EncryptionOnly + ) + .withStorageMode(CryptoStorageMode.InstructionFile); + + AmazonS3Encryption v1Client = AmazonS3EncryptionClient + .encryptionBuilder() + .withCryptoConfiguration(cryptoConfig) + .withEncryptionMaterials(materialsProvider) + .build(); + + v1Client.putObject(BUCKET, objectKey, input); + + AesKeyring oldKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY) + .enableLegacyWrappingAlgorithms(true) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "no") + .put("isLegacy", "yes") + .build() + ) + .build(); + + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient v3OriginalClient = S3EncryptionClient + .builder() + .keyring(oldKeyring) + .enableLegacyUnauthenticatedModes(true) + .enableLegacyWrappingAlgorithms(true) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + AesKeyring newKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY) + .materialsDescription( + MaterialsDescription + .builder() + .put("rotated", "yes") + .put("isLegacy", "no") + .build() + ) + .build(); + + S3EncryptionClient v3RotatedClient = S3EncryptionClient + .builder() + .keyring(newKeyring) + .instructionFileConfig( + InstructionFileConfig + .builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + ReEncryptInstructionFileRequest reEncryptInstructionFileRequest = + ReEncryptInstructionFileRequest + .builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .enforceRotation(true) + .build(); + + try { + ReEncryptInstructionFileResponse response = + v3OriginalClient.reEncryptInstructionFile( + reEncryptInstructionFileRequest + ); + fail("Enforce rotation should throw exception"); + } catch (S3EncryptionClientException e) { + assertTrue( + e + .getMessage() + .contains( + "Re-encryption failed due to enforced rotation! Old keyring is still able to decrypt the newly encrypted data key" + ) + ); + } + + deleteObject(BUCKET, objectKey, v3RotatedClient); + } +} diff --git a/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataStrategyTest.java b/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataStrategyTest.java index 3e7f09e87..43f6a979b 100644 --- a/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataStrategyTest.java +++ b/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataStrategyTest.java @@ -49,7 +49,7 @@ public void decodeWithObjectMetadata() { expectedContentMetadata = ContentMetadata.builder() .algorithmSuite(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) .encryptedDataKeyAlgorithm(null) - .encryptedDataKeyContext(new HashMap()) + .encryptionContextOrMatDesc(new HashMap()) .contentIv(bytes) .build(); diff --git a/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataTest.java b/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataTest.java index 73a38f23b..d40233ca0 100644 --- a/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataTest.java +++ b/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataTest.java @@ -35,7 +35,7 @@ public void setUp() { .encryptedDataKey(encryptedDataKey) .contentIv(contentIv) .encryptedDataKeyAlgorithm(encryptedDataKeyAlgorithm) - .encryptedDataKeyContext(encryptedDataKeyContext) + .encryptionContextOrMatDesc(encryptedDataKeyContext) .build(); } @@ -57,7 +57,7 @@ public void testEncryptedDataKeyAlgorithm() { @Test public void testEncryptedDataKeyContext() { - assertEquals(encryptedDataKeyContext, actualContentMetadata.encryptedDataKeyContext()); + assertEquals(encryptedDataKeyContext, actualContentMetadata.encryptedDataKeyMatDescOrContext()); } @Test diff --git a/src/test/java/software/amazon/encryption/s3/materials/MaterialsDescriptionTest.java b/src/test/java/software/amazon/encryption/s3/materials/MaterialsDescriptionTest.java new file mode 100644 index 000000000..6a5230718 --- /dev/null +++ b/src/test/java/software/amazon/encryption/s3/materials/MaterialsDescriptionTest.java @@ -0,0 +1,121 @@ +package software.amazon.encryption.s3.materials; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class MaterialsDescriptionTest { + + private static SecretKey AES_KEY; + private static KeyPair RSA_KEY_PAIR; + + @BeforeAll + public static void setUp() throws NoSuchAlgorithmException { + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(256); + AES_KEY = keyGen.generateKey(); + + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(2048); + RSA_KEY_PAIR = keyPairGen.generateKeyPair(); + } + + @Test + public void testSimpleMaterialsDescription() { + MaterialsDescription materialsDescription = MaterialsDescription + .builder() + .put("version", "1.0") + .build(); + assertEquals( + "1.0", + materialsDescription.getMaterialsDescription().get("version") + ); + assertEquals(1, materialsDescription.getMaterialsDescription().size()); + try { + materialsDescription.getMaterialsDescription().put("version", "2.0"); + fail("Expected UnsupportedOperationException!"); + } catch (UnsupportedOperationException e) { + assertNull(e.getMessage()); + } + try { + materialsDescription.getMaterialsDescription().clear(); + fail("Expected UnsupportedOperationException!"); + } catch (UnsupportedOperationException e) { + assertNull(e.getMessage()); + } + } + + @Test + public void testMaterialsDescriptionPutAll() { + Map description = new HashMap<>(); + description.put("version", "1.0"); + description.put("next-version", "2.0"); + MaterialsDescription materialsDescription = MaterialsDescription + .builder() + .putAll(description) + .build(); + assertEquals(2, materialsDescription.getMaterialsDescription().size()); + assertTrue( + materialsDescription.getMaterialsDescription().containsKey("version") + ); + assertTrue( + materialsDescription.getMaterialsDescription().containsKey("next-version") + ); + assertEquals( + "1.0", + materialsDescription.getMaterialsDescription().get("version") + ); + assertEquals( + "2.0", + materialsDescription.getMaterialsDescription().get("next-version") + ); + } + + @Test + public void testMaterialsDescriptionAesKeyring() { + AesKeyring aesKeyring = AesKeyring + .builder() + .wrappingKey(AES_KEY) + .materialsDescription( + MaterialsDescription + .builder() + .put("version", "1.0") + .put("admin", "yes") + .build() + ) + .build(); + assertNotNull(aesKeyring); + } + + @Test + public void testMaterialsDescriptionRsaKeyring() { + PartialRsaKeyPair keyPair = new PartialRsaKeyPair( + RSA_KEY_PAIR.getPrivate(), + RSA_KEY_PAIR.getPublic() + ); + RsaKeyring rsaKeyring = RsaKeyring + .builder() + .wrappingKeyPair(keyPair) + .materialsDescription( + MaterialsDescription + .builder() + .put("version", "1.0") + .put("admin", "yes") + .build() + ) + .build(); + assertNotNull(rsaKeyring); + } +}