From 128706199d2ae478812fbae7bb80c819b5b95c9c Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 25 Aug 2025 14:44:39 -0700 Subject: [PATCH 1/3] feat: allow raw keyrings to decrypt with multiple wrapping keys --- .../encryption/s3/S3EncryptionClient.java | 5 +- .../s3/internal/ContentMetadata.java | 41 +- .../ContentMetadataDecodingStrategy.java | 27 +- .../internal/GetEncryptedObjectPipeline.java | 3 +- .../s3/materials/AesKeyMaterial.java | 67 ++ .../encryption/s3/materials/AesKeyring.java | 65 +- .../s3/materials/DecryptMaterialsRequest.java | 18 + .../s3/materials/DecryptionMaterials.java | 21 + .../DefaultCryptoMaterialsManager.java | 1 + .../s3/materials/MaterialsDescription.java | 18 + .../s3/materials/RawKeyMaterial.java | 91 ++ .../encryption/s3/materials/RawKeyring.java | 54 +- .../s3/materials/RsaKeyMaterial.java | 65 ++ .../encryption/s3/materials/RsaKeyring.java | 55 +- .../AdditionalDecryptionKeyMaterialTest.java | 923 ++++++++++++++++++ .../S3EncryptionClientCompatibilityTest.java | 217 +++- .../internal/ContentMetadataStrategyTest.java | 2 +- .../s3/internal/ContentMetadataTest.java | 4 +- .../s3/materials/KeyMaterialTest.java | 178 ++++ .../materials/MaterialsDescriptionTest.java | 57 ++ 20 files changed, 1837 insertions(+), 75 deletions(-) create mode 100644 src/main/java/software/amazon/encryption/s3/materials/AesKeyMaterial.java create mode 100644 src/main/java/software/amazon/encryption/s3/materials/RawKeyMaterial.java create mode 100644 src/main/java/software/amazon/encryption/s3/materials/RsaKeyMaterial.java create mode 100644 src/test/java/software/amazon/encryption/s3/AdditionalDecryptionKeyMaterialTest.java create mode 100644 src/test/java/software/amazon/encryption/s3/materials/KeyMaterialTest.java diff --git a/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java b/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java index e1c275bd5..9ebeb9b98 100644 --- a/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java +++ b/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java @@ -68,6 +68,7 @@ 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.MaterialsDescription; import software.amazon.encryption.s3.materials.MultipartConfiguration; import software.amazon.encryption.s3.materials.PartialRsaKeyPair; import software.amazon.encryption.s3.materials.RawKeyring; @@ -246,7 +247,7 @@ public ReEncryptInstructionFileResponse reEncryptInstructionFile(ReEncryptInstru //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 MaterialsDescription currentKeyringMaterialsDescription = contentMetadata.materialsDescription(); final byte[] iv = contentMetadata.contentIv(); //Decrypt the data key using the current keyring @@ -277,7 +278,7 @@ public ReEncryptInstructionFileResponse reEncryptInstructionFile(ReEncryptInstru RawKeyring newKeyring = reEncryptInstructionFileRequest.newKeyring(); EncryptionMaterials encryptedMaterials = newKeyring.onEncrypt(encryptionMaterials); - final Map newMaterialsDescription = encryptedMaterials.materialsDescription().getMaterialsDescription(); + final MaterialsDescription newMaterialsDescription = encryptedMaterials.materialsDescription(); //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!"); 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 4d61a2248..8a47a6f17 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadata.java +++ b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadata.java @@ -5,6 +5,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import software.amazon.encryption.s3.algorithms.AlgorithmSuite; import software.amazon.encryption.s3.materials.EncryptedDataKey; +import software.amazon.encryption.s3.materials.MaterialsDescription; import java.util.Collections; import java.util.Map; @@ -17,11 +18,14 @@ public class ContentMetadata { private final String _encryptedDataKeyAlgorithm; /** - * 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. + * This field stores the encryption context. */ - private final Map _encryptionContextOrMatDesc; + private final Map _encryptionContext; + + /** + * This field stores the materials description used for RSA and AES keyrings. + */ + private final MaterialsDescription _materialsDescription; private final byte[] _contentIv; private final String _contentCipher; @@ -33,7 +37,8 @@ private ContentMetadata(Builder builder) { _encryptedDataKey = builder._encryptedDataKey; _encryptedDataKeyAlgorithm = builder._encryptedDataKeyAlgorithm; - _encryptionContextOrMatDesc = builder._encryptionContextOrMatDesc; + _encryptionContext = builder._encryptionContext; + _materialsDescription = builder._materialsDescription; _contentIv = builder._contentIv; _contentCipher = builder._contentCipher; @@ -64,8 +69,16 @@ public String encryptedDataKeyAlgorithm() { */ @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "False positive; underlying" + " implementation is immutable") - public Map encryptedDataKeyMatDescOrContext() { - return _encryptionContextOrMatDesc; + public Map encryptionContext() { + return _encryptionContext; + } + + /** + * Returns the materials description used for RSA and AES keyrings. + * @return the materials description + */ + public MaterialsDescription materialsDescription() { + return _materialsDescription; } public byte[] contentIv() { @@ -92,7 +105,8 @@ public static class Builder { private EncryptedDataKey _encryptedDataKey; private String _encryptedDataKeyAlgorithm; - private Map _encryptionContextOrMatDesc; + private Map _encryptionContext; + private MaterialsDescription _materialsDescription = MaterialsDescription.builder().build(); private byte[] _contentIv; private String _contentCipher; @@ -118,8 +132,15 @@ public Builder encryptedDataKeyAlgorithm(String encryptedDataKeyAlgorithm) { return this; } - public Builder encryptionContextOrMatDesc(Map encryptionContextOrMatDesc) { - _encryptionContextOrMatDesc = Collections.unmodifiableMap(encryptionContextOrMatDesc); + public Builder encryptionContext(Map encryptionContext) { + _encryptionContext = Collections.unmodifiableMap(encryptionContext); + return this; + } + + public Builder materialsDescription(MaterialsDescription materialsDescription) { + _materialsDescription = materialsDescription == null + ? MaterialsDescription.builder().build() + : materialsDescription; 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 3d1e4edd9..77e6eabc5 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java +++ b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java @@ -13,6 +13,7 @@ import software.amazon.encryption.s3.S3EncryptionClientException; import software.amazon.encryption.s3.algorithms.AlgorithmSuite; import software.amazon.encryption.s3.materials.EncryptedDataKey; +import software.amazon.encryption.s3.materials.MaterialsDescription; import software.amazon.encryption.s3.materials.S3Keyring; import java.io.ByteArrayOutputStream; @@ -137,8 +138,8 @@ private ContentMetadata readFromMap(Map metadata, GetObjectRespo .keyProviderInfo(keyProviderInfo.getBytes(StandardCharsets.UTF_8)) .build(); - // Get encrypted data key encryption context or materials description (depending on the keyring) - final Map encryptionContextOrMatDesc = new HashMap<>(); + // Parse the JSON materials description or encryption context + final Map matDescMap = 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, @@ -150,19 +151,37 @@ private ContentMetadata readFromMap(Map metadata, GetObjectRespo JsonNode objectNode = parser.parse(decodedJsonEncryptionContext); for (Map.Entry entry : objectNode.asObject().entrySet()) { - encryptionContextOrMatDesc.put(entry.getKey(), entry.getValue().asString()); + matDescMap.put(entry.getKey(), entry.getValue().asString()); } } catch (Exception e) { throw new RuntimeException(e); } + // By default, assume the context is a materials description unless it's a KMS keyring + Map encryptionContext; + MaterialsDescription materialsDescription; + + if (keyProviderInfo.contains("kms")) { + // For KMS keyrings, use the map as encryption context + encryptionContext = matDescMap; + materialsDescription = MaterialsDescription.builder().build(); + } else { + // For all other keyrings (AES, RSA), use the map as materials description + materialsDescription = MaterialsDescription.builder() + .putAll(matDescMap) + .build(); + // Set an empty encryption context + encryptionContext = new HashMap<>(); + } + // Get content iv byte[] iv = DECODER.decode(metadata.get(MetadataKeyConstants.CONTENT_IV)); return ContentMetadata.builder() .algorithmSuite(algorithmSuite) .encryptedDataKey(edk) - .encryptionContextOrMatDesc(encryptionContextOrMatDesc) + .encryptionContext(encryptionContext) + .materialsDescription(materialsDescription) .contentIv(iv) .contentRange(contentRange) .build(); 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 b002b42b8..ddd80c392 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/GetEncryptedObjectPipeline.java +++ b/src/main/java/software/amazon/encryption/s3/internal/GetEncryptedObjectPipeline.java @@ -91,7 +91,8 @@ private DecryptionMaterials prepareMaterialsFromRequest(final GetObjectRequest g .s3Request(getObjectRequest) .algorithmSuite(algorithmSuite) .encryptedDataKeys(encryptedDataKeys) - .encryptionContext(contentMetadata.encryptedDataKeyMatDescOrContext()) + .encryptionContext(contentMetadata.encryptionContext()) + .materialsDescription(contentMetadata.materialsDescription()) .ciphertextLength(getObjectResponse.contentLength()) .contentRange(getObjectRequest.range()) .build(); diff --git a/src/main/java/software/amazon/encryption/s3/materials/AesKeyMaterial.java b/src/main/java/software/amazon/encryption/s3/materials/AesKeyMaterial.java new file mode 100644 index 000000000..a5af68785 --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/materials/AesKeyMaterial.java @@ -0,0 +1,67 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.encryption.s3.materials; + +import javax.crypto.SecretKey; + +/** + * A concrete implementation of RawKeyMaterial for AES keys. + * This class provides a more convenient way to create key material for AES keyrings + * without having to specify the generic type parameter. + */ +public class AesKeyMaterial extends RawKeyMaterial { + + /** + * Creates a new AesKeyMaterial with the specified materials description and key material. + * + * @param materialsDescription the materials description + * @param keyMaterial the AES key material + */ + public AesKeyMaterial(MaterialsDescription materialsDescription, SecretKey keyMaterial) { + super(materialsDescription, keyMaterial); + } + + /** + * @return a new builder instance for AesKeyMaterial + */ + public static Builder aesBuilder() { + return new Builder(); + } + + /** + * Builder for AesKeyMaterial. + */ + public static class Builder { + private MaterialsDescription _materialsDescription; + private SecretKey _keyMaterial; + + /** + * Sets the materials description for this AES key material. + * + * @param materialsDescription the materials description + * @return a reference to this object so that method calls can be chained together. + */ + public Builder materialsDescription(MaterialsDescription materialsDescription) { + this._materialsDescription = materialsDescription; + return this; + } + + /** + * Sets the AES key material. + * + * @param keyMaterial the AES key material + * @return a reference to this object so that method calls can be chained together. + */ + public Builder keyMaterial(SecretKey keyMaterial) { + this._keyMaterial = keyMaterial; + return this; + } + + /** + * @return the built AesKeyMaterial + */ + public AesKeyMaterial build() { + return new AesKeyMaterial(_materialsDescription, _keyMaterial); + } + } +} 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 ec2e474df..f09acc5b4 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 RawKeyring { +public class AesKeyring extends RawKeyring { private static final String KEY_ALGORITHM = "AES"; @@ -41,13 +41,16 @@ public String keyProviderInfo() { return KEY_PROVIDER_INFO; } - @Override - public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { - final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); - cipher.init(Cipher.DECRYPT_MODE, _wrappingKey); + @Override + public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { + // Find the appropriate key material to use for decryption + SecretKey keyToUse = findKeyMaterialForDecryption(materials, _wrappingKey); - return cipher.doFinal(encryptedDataKey); - } + final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); + cipher.init(Cipher.DECRYPT_MODE, keyToUse); + + return cipher.doFinal(encryptedDataKey); + } }; private final DecryptDataKeyStrategy _aesWrapStrategy = new DecryptDataKeyStrategy() { @@ -65,14 +68,17 @@ public String keyProviderInfo() { return KEY_PROVIDER_INFO; } - @Override - public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { - final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); - cipher.init(Cipher.UNWRAP_MODE, _wrappingKey); + @Override + public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { + // Find the appropriate key material to use for decryption + SecretKey keyToUse = findKeyMaterialForDecryption(materials, _wrappingKey); - Key plaintextKey = cipher.unwrap(encryptedDataKey, CIPHER_ALGORITHM, Cipher.SECRET_KEY); - return plaintextKey.getEncoded(); - } + final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); + cipher.init(Cipher.UNWRAP_MODE, keyToUse); + + Key plaintextKey = cipher.unwrap(encryptedDataKey, CIPHER_ALGORITHM, Cipher.SECRET_KEY); + return plaintextKey.getEncoded(); + } }; private final DataKeyStrategy _aesGcmStrategy = new DataKeyStrategy() { @@ -126,22 +132,25 @@ public byte[] encryptDataKey(SecureRandom secureRandom, return encodedBytes; } - @Override - public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { - byte[] iv = new byte[IV_LENGTH_BYTES]; - byte[] ciphertext = new byte[encryptedDataKey.length - iv.length]; + @Override + public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { + byte[] iv = new byte[IV_LENGTH_BYTES]; + byte[] ciphertext = new byte[encryptedDataKey.length - iv.length]; - System.arraycopy(encryptedDataKey, 0, iv, 0, iv.length); - System.arraycopy(encryptedDataKey, iv.length, ciphertext, 0, ciphertext.length); + System.arraycopy(encryptedDataKey, 0, iv, 0, iv.length); + System.arraycopy(encryptedDataKey, iv.length, ciphertext, 0, ciphertext.length); - GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(TAG_LENGTH_BITS, iv); - final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); - cipher.init(Cipher.DECRYPT_MODE, _wrappingKey, gcmParameterSpec); + // Find the appropriate key material to use for decryption + SecretKey keyToUse = findKeyMaterialForDecryption(materials, _wrappingKey); - final byte[] aADBytes = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherName().getBytes(StandardCharsets.UTF_8); - cipher.updateAAD(aADBytes); - return cipher.doFinal(ciphertext); - } + GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(TAG_LENGTH_BITS, iv); + final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); + cipher.init(Cipher.DECRYPT_MODE, keyToUse, gcmParameterSpec); + + final byte[] aADBytes = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherName().getBytes(StandardCharsets.UTF_8); + cipher.updateAAD(aADBytes); + return cipher.doFinal(ciphertext); + } }; private final Map decryptDataKeyStrategies = new HashMap<>(); @@ -175,7 +184,7 @@ protected Map decryptDataKeyStrategies() { return decryptDataKeyStrategies; } - public static class Builder extends RawKeyring.Builder { + public static class Builder extends RawKeyring.Builder { private SecretKey _wrappingKey; private Builder() { diff --git a/src/main/java/software/amazon/encryption/s3/materials/DecryptMaterialsRequest.java b/src/main/java/software/amazon/encryption/s3/materials/DecryptMaterialsRequest.java index 0b7097120..33b02a186 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/DecryptMaterialsRequest.java +++ b/src/main/java/software/amazon/encryption/s3/materials/DecryptMaterialsRequest.java @@ -16,6 +16,7 @@ public class DecryptMaterialsRequest { private final AlgorithmSuite _algorithmSuite; private final List _encryptedDataKeys; private final Map _encryptionContext; + private final MaterialsDescription _materialsDescription; private final long _ciphertextLength; private final String _contentRange; @@ -24,6 +25,7 @@ private DecryptMaterialsRequest(Builder builder) { this._algorithmSuite = builder._algorithmSuite; this._encryptedDataKeys = builder._encryptedDataKeys; this._encryptionContext = builder._encryptionContext; + this._materialsDescription = builder._materialsDescription; this._ciphertextLength = builder._ciphertextLength; this._contentRange = builder._contentRange; } @@ -60,6 +62,14 @@ public Map encryptionContext() { return _encryptionContext; } + /** + * Returns the materials description used for RSA and AES keyrings. + * @return the materials description + */ + public MaterialsDescription materialsDescription() { + return _materialsDescription; + } + public long ciphertextLength() { return _ciphertextLength; } @@ -73,6 +83,7 @@ static public class Builder { public GetObjectRequest _s3Request = null; private AlgorithmSuite _algorithmSuite = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF; private Map _encryptionContext = Collections.emptyMap(); + private MaterialsDescription _materialsDescription = MaterialsDescription.builder().build(); private List _encryptedDataKeys = Collections.emptyList(); private long _ciphertextLength = -1; private String _contentRange = null; @@ -97,6 +108,13 @@ public Builder encryptionContext(Map encryptionContext) { return this; } + public Builder materialsDescription(MaterialsDescription materialsDescription) { + _materialsDescription = materialsDescription == null + ? MaterialsDescription.builder().build() + : materialsDescription; + return this; + } + public Builder encryptedDataKeys(List encryptedDataKeys) { _encryptedDataKeys = encryptedDataKeys == null ? Collections.emptyList() diff --git a/src/main/java/software/amazon/encryption/s3/materials/DecryptionMaterials.java b/src/main/java/software/amazon/encryption/s3/materials/DecryptionMaterials.java index b2df93a09..1977a2a0b 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/DecryptionMaterials.java +++ b/src/main/java/software/amazon/encryption/s3/materials/DecryptionMaterials.java @@ -27,6 +27,9 @@ final public class DecryptionMaterials implements CryptographicMaterials { // Should NOT contain sensitive information private final Map _encryptionContext; + // Materials description used for RSA and AES keyrings + private final MaterialsDescription _materialsDescription; + private final byte[] _plaintextDataKey; private long _ciphertextLength; @@ -37,6 +40,7 @@ private DecryptionMaterials(Builder builder) { this._s3Request = builder._s3Request; this._algorithmSuite = builder._algorithmSuite; this._encryptionContext = builder._encryptionContext; + this._materialsDescription = builder._materialsDescription; this._plaintextDataKey = builder._plaintextDataKey; this._ciphertextLength = builder._ciphertextLength; this._cryptoProvider = builder._cryptoProvider; @@ -65,6 +69,14 @@ public Map encryptionContext() { return _encryptionContext; } + /** + * Returns the materials description used for RSA and AES keyrings. + * @return the materials description + */ + public MaterialsDescription materialsDescription() { + return _materialsDescription; + } + public byte[] plaintextDataKey() { if (_plaintextDataKey == null) { return null; @@ -103,6 +115,7 @@ public Builder toBuilder() { .s3Request(_s3Request) .algorithmSuite(_algorithmSuite) .encryptionContext(_encryptionContext) + .materialsDescription(_materialsDescription) .plaintextDataKey(_plaintextDataKey) .ciphertextLength(_ciphertextLength) .cryptoProvider(_cryptoProvider) @@ -115,6 +128,7 @@ static public class Builder { private Provider _cryptoProvider = null; private AlgorithmSuite _algorithmSuite = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF; private Map _encryptionContext = Collections.emptyMap(); + private MaterialsDescription _materialsDescription = MaterialsDescription.builder().build(); private byte[] _plaintextDataKey = null; private long _ciphertextLength = -1; private String _contentRange = null; @@ -139,6 +153,13 @@ public Builder encryptionContext(Map encryptionContext) { return this; } + public Builder materialsDescription(MaterialsDescription materialsDescription) { + _materialsDescription = materialsDescription == null + ? MaterialsDescription.builder().build() + : materialsDescription; + return this; + } + public Builder plaintextDataKey(byte[] plaintextDataKey) { _plaintextDataKey = plaintextDataKey == null ? null : plaintextDataKey.clone(); return this; diff --git a/src/main/java/software/amazon/encryption/s3/materials/DefaultCryptoMaterialsManager.java b/src/main/java/software/amazon/encryption/s3/materials/DefaultCryptoMaterialsManager.java index 96d163a4a..3e5d3c4b3 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/DefaultCryptoMaterialsManager.java +++ b/src/main/java/software/amazon/encryption/s3/materials/DefaultCryptoMaterialsManager.java @@ -36,6 +36,7 @@ public DecryptionMaterials decryptMaterials(DecryptMaterialsRequest request) { .s3Request(request.s3Request()) .algorithmSuite(request.algorithmSuite()) .encryptionContext(request.encryptionContext()) + .materialsDescription(request.materialsDescription()) .ciphertextLength(request.ciphertextLength()) .cryptoProvider(_cryptoProvider) .contentRange(request.contentRange()) diff --git a/src/main/java/software/amazon/encryption/s3/materials/MaterialsDescription.java b/src/main/java/software/amazon/encryption/s3/materials/MaterialsDescription.java index 148b20c49..0ac80282f 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/MaterialsDescription.java +++ b/src/main/java/software/amazon/encryption/s3/materials/MaterialsDescription.java @@ -96,6 +96,24 @@ public Set> entrySet() { return materialsDescription.entrySet(); } + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + MaterialsDescription other = (MaterialsDescription) obj; + return getMaterialsDescription().equals(other.getMaterialsDescription()); + } + + @Override + public int hashCode() { + return materialsDescription.hashCode(); + } + /** * Builder for MaterialsDescription. */ diff --git a/src/main/java/software/amazon/encryption/s3/materials/RawKeyMaterial.java b/src/main/java/software/amazon/encryption/s3/materials/RawKeyMaterial.java new file mode 100644 index 000000000..161519a61 --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/materials/RawKeyMaterial.java @@ -0,0 +1,91 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.encryption.s3.materials; + +/** + * This class represents raw key material used by keyrings. + * It contains a materials description and the actual key material. + * + * @param the type of key material + */ +public class RawKeyMaterial { + + protected final MaterialsDescription _materialsDescription; + protected final T _keyMaterial; + + private RawKeyMaterial(Builder builder) { + this._materialsDescription = builder._materialsDescription; + this._keyMaterial = builder._keyMaterial; + } + + /** + * Protected constructor for subclasses. + * + * @param materialsDescription the materials description + * @param keyMaterial the key material + */ + protected RawKeyMaterial(MaterialsDescription materialsDescription, T keyMaterial) { + this._materialsDescription = materialsDescription; + this._keyMaterial = keyMaterial; + } + + /** + * @return a new builder instance + */ + public static Builder builder() { + return new Builder<>(); + } + + /** + * @return the materials description + */ + public MaterialsDescription getMaterialsDescription() { + return _materialsDescription; + } + + /** + * @return the key material + */ + public T getKeyMaterial() { + return _keyMaterial; + } + + /** + * Builder for RawKeyMaterial. + * + * @param the type of key material + */ + public static class Builder { + private MaterialsDescription _materialsDescription; + private T _keyMaterial; + + /** + * Sets the materials description for this raw key material. + * + * @param materialsDescription the materials description + * @return a reference to this object so that method calls can be chained together. + */ + public Builder materialsDescription(MaterialsDescription materialsDescription) { + this._materialsDescription = materialsDescription; + return this; + } + + /** + * Sets the key material. + * + * @param keyMaterial the key material + * @return a reference to this object so that method calls can be chained together. + */ + public Builder keyMaterial(T keyMaterial) { + this._keyMaterial = keyMaterial; + return this; + } + + /** + * @return the built RawKeyMaterial + */ + public RawKeyMaterial build() { + return new RawKeyMaterial<>(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 index 9ad240322..74f1d89cd 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/RawKeyring.java +++ b/src/main/java/software/amazon/encryption/s3/materials/RawKeyring.java @@ -5,16 +5,46 @@ import org.apache.commons.logging.LogFactory; import software.amazon.encryption.s3.S3EncryptionClient; +import java.util.Map; + /** * This is an abstract base class for keyrings that use raw cryptographic keys (AES + RSA) + * + * @param the type of key material used by this keyring */ -public abstract class RawKeyring extends S3Keyring { +public abstract class RawKeyring extends S3Keyring { protected final MaterialsDescription _materialsDescription; + protected final Map> _additionalDecryptionKeyMaterial; - protected RawKeyring(Builder builder) { + protected RawKeyring(Builder builder) { super(builder); _materialsDescription = builder._materialsDescription; + _additionalDecryptionKeyMaterial = builder._additionalDecryptionKeyMaterial; + } + + /** + * Finds the appropriate key material to use for decryption based on the materials description. + * If a matching key material is found in the additionalDecryptionKeyMaterial map, it is returned. + * Otherwise, the default key material is returned. + * + * @param materials the decryption materials containing the materials description + * @param defaultKeyMaterial the default key material to use if no matching key material is found + * @return the key material to use for decryption + */ + protected T findKeyMaterialForDecryption(DecryptionMaterials materials, T defaultKeyMaterial) { + if (_additionalDecryptionKeyMaterial != null && !_additionalDecryptionKeyMaterial.isEmpty()) { + // Get the materials description from the decryption materials + MaterialsDescription materialsDescription = materials.materialsDescription(); + + // Check if there's a matching entry in the additionalDecryptionKeyMaterial map + RawKeyMaterial matchingKeyMaterial = _additionalDecryptionKeyMaterial.get(materialsDescription); + if (matchingKeyMaterial != null) { + return matchingKeyMaterial.getKeyMaterial(); + } + } + + return defaultKeyMaterial; } /** @@ -76,13 +106,17 @@ public void warnIfEncryptionContextIsPresent(EncryptionMaterials materials) { * * @param the type of keyring being built * @param the type of builder + * @param the type of key material used by this keyring */ public abstract static class Builder< - KeyringT extends RawKeyring, BuilderT extends Builder + KeyringT extends RawKeyring, + BuilderT extends Builder, + T > extends S3Keyring.Builder { protected MaterialsDescription _materialsDescription; + protected Map> _additionalDecryptionKeyMaterial; protected Builder() { super(); @@ -101,5 +135,19 @@ public BuilderT materialsDescription( _materialsDescription = materialsDescription; return builder(); } + + /** + * Sets the map of keys for which to use for decryption. + * + * @param additionalDecryptionKeyMaterial the map of additional key material for decryption, + * where the key is the materials description and the value is the key material + * @return a reference to this object so that method calls can be chained together. + */ + public BuilderT additionalDecryptionKeyMaterial( + Map> additionalDecryptionKeyMaterial + ) { + _additionalDecryptionKeyMaterial = additionalDecryptionKeyMaterial; + return builder(); + } } } diff --git a/src/main/java/software/amazon/encryption/s3/materials/RsaKeyMaterial.java b/src/main/java/software/amazon/encryption/s3/materials/RsaKeyMaterial.java new file mode 100644 index 000000000..76ea89a2f --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/materials/RsaKeyMaterial.java @@ -0,0 +1,65 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.encryption.s3.materials; + +/** + * A concrete implementation of RawKeyMaterial for RSA keys. + * This class provides a more convenient way to create key material for RSA keyrings + * without having to specify the generic type parameter. + */ +public class RsaKeyMaterial extends RawKeyMaterial { + + /** + * Creates a new RsaKeyMaterial with the specified materials description and key material. + * + * @param materialsDescription the materials description + * @param keyMaterial the RSA key material + */ + public RsaKeyMaterial(MaterialsDescription materialsDescription, PartialRsaKeyPair keyMaterial) { + super(materialsDescription, keyMaterial); + } + + /** + * @return a new builder instance for RsaKeyMaterial + */ + public static Builder rsaBuilder() { + return new Builder(); + } + + /** + * Builder for RsaKeyMaterial. + */ + public static class Builder { + private MaterialsDescription _materialsDescription; + private PartialRsaKeyPair _keyMaterial; + + /** + * Sets the materials description for this RSA key material. + * + * @param materialsDescription the materials description + * @return a reference to this object so that method calls can be chained together. + */ + public Builder materialsDescription(MaterialsDescription materialsDescription) { + this._materialsDescription = materialsDescription; + return this; + } + + /** + * Sets the RSA key material. + * + * @param keyMaterial the RSA key material + * @return a reference to this object so that method calls can be chained together. + */ + public Builder keyMaterial(PartialRsaKeyPair keyMaterial) { + this._keyMaterial = keyMaterial; + return this; + } + + /** + * @return the built RsaKeyMaterial + */ + public RsaKeyMaterial build() { + return new RsaKeyMaterial(_materialsDescription, _keyMaterial); + } + } +} 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 91563b14c..ac9469327 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 RawKeyring { +public class RsaKeyring extends RawKeyring { private final PartialRsaKeyPair _partialRsaKeyPair; @@ -42,13 +42,16 @@ public String keyProviderInfo() { return KEY_PROVIDER_INFO; } - @Override - public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { - final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); - cipher.init(Cipher.DECRYPT_MODE, _partialRsaKeyPair.getPrivateKey()); + @Override + public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { + // Find the appropriate key material to use for decryption + PartialRsaKeyPair keyPairToUse = findKeyMaterialForDecryption(materials, _partialRsaKeyPair); - return cipher.doFinal(encryptedDataKey); - } + final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); + cipher.init(Cipher.DECRYPT_MODE, keyPairToUse.getPrivateKey()); + + return cipher.doFinal(encryptedDataKey); + } }; private final DecryptDataKeyStrategy _rsaEcbStrategy = new DecryptDataKeyStrategy() { @@ -65,15 +68,18 @@ public String keyProviderInfo() { return KEY_PROVIDER_INFO; } - @Override - public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { - final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); - cipher.init(Cipher.UNWRAP_MODE, _partialRsaKeyPair.getPrivateKey()); + @Override + public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { + // Find the appropriate key material to use for decryption + PartialRsaKeyPair keyPairToUse = findKeyMaterialForDecryption(materials, _partialRsaKeyPair); - Key plaintextKey = cipher.unwrap(encryptedDataKey, CIPHER_ALGORITHM, Cipher.SECRET_KEY); + final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); + cipher.init(Cipher.UNWRAP_MODE, keyPairToUse.getPrivateKey()); - return plaintextKey.getEncoded(); - } + Key plaintextKey = cipher.unwrap(encryptedDataKey, CIPHER_ALGORITHM, Cipher.SECRET_KEY); + + return plaintextKey.getEncoded(); + } }; private final DataKeyStrategy _rsaOaepStrategy = new DataKeyStrategy() { @@ -127,16 +133,19 @@ public byte[] encryptDataKey(SecureRandom secureRandom, return ciphertext; } - @Override - public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { - final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); - cipher.init(Cipher.UNWRAP_MODE, _partialRsaKeyPair.getPrivateKey(), OAEP_PARAMETER_SPEC); + @Override + public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { + // Find the appropriate key material to use for decryption + PartialRsaKeyPair keyPairToUse = findKeyMaterialForDecryption(materials, _partialRsaKeyPair); - String dataKeyAlgorithm = materials.algorithmSuite().dataKeyAlgorithm(); - Key pseudoDataKey = cipher.unwrap(encryptedDataKey, dataKeyAlgorithm, Cipher.SECRET_KEY); + final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); + cipher.init(Cipher.UNWRAP_MODE, keyPairToUse.getPrivateKey(), OAEP_PARAMETER_SPEC); - return parsePseudoDataKey(materials, pseudoDataKey.getEncoded()); - } + String dataKeyAlgorithm = materials.algorithmSuite().dataKeyAlgorithm(); + Key pseudoDataKey = cipher.unwrap(encryptedDataKey, dataKeyAlgorithm, Cipher.SECRET_KEY); + + return parsePseudoDataKey(materials, pseudoDataKey.getEncoded()); + } private byte[] parsePseudoDataKey(DecryptionMaterials materials, byte[] pseudoDataKey) { int dataKeyLengthBytes = pseudoDataKey[0]; @@ -195,7 +204,7 @@ protected Map decryptDataKeyStrategies() { return decryptDataKeyStrategies; } - public static class Builder extends RawKeyring.Builder { + public static class Builder extends RawKeyring.Builder { private PartialRsaKeyPair _partialRsaKeyPair; private Builder() { diff --git a/src/test/java/software/amazon/encryption/s3/AdditionalDecryptionKeyMaterialTest.java b/src/test/java/software/amazon/encryption/s3/AdditionalDecryptionKeyMaterialTest.java new file mode 100644 index 000000000..5b51ec8d8 --- /dev/null +++ b/src/test/java/software/amazon/encryption/s3/AdditionalDecryptionKeyMaterialTest.java @@ -0,0 +1,923 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.encryption.s3; + +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.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.encryption.s3.materials.AesKeyring; +import software.amazon.encryption.s3.materials.MaterialsDescription; +import software.amazon.encryption.s3.materials.RawKeyMaterial; +import software.amazon.encryption.s3.materials.RsaKeyring; +import software.amazon.encryption.s3.materials.PartialRsaKeyPair; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +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; + +/** + * This class is an integration test for verifying the additionalDecryptionKeyMaterial feature + * in the S3EncryptionClient. + */ +public class AdditionalDecryptionKeyMaterialTest { + + private static SecretKey AES_KEY_1; + private static SecretKey AES_KEY_2; + private static SecretKey AES_KEY_3; + private static KeyPair RSA_KEY_PAIR_1; + private static KeyPair RSA_KEY_PAIR_2; + private static KeyPair RSA_KEY_PAIR_3; + + @BeforeAll + public static void setUp() throws NoSuchAlgorithmException { + // Generate AES keys + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(256); + AES_KEY_1 = keyGen.generateKey(); + AES_KEY_2 = keyGen.generateKey(); + AES_KEY_3 = keyGen.generateKey(); + + // Generate RSA key pairs + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(2048); + RSA_KEY_PAIR_1 = keyPairGen.generateKeyPair(); + RSA_KEY_PAIR_2 = keyPairGen.generateKeyPair(); + RSA_KEY_PAIR_3 = keyPairGen.generateKeyPair(); + } + + /** + * Test AES keyring with null additionalDecryptionKeyMaterial map. + * This tests the default behavior when no additional key material is provided. + */ + @Test + public void testAesKeyringWithNullAdditionalKeyMaterial() { + final String objectKey = appendTestSuffix("aes-null-additional-key-material"); + final String input = "AES with null additional key material"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an AES keyring with the first key and materials description + AesKeyring encryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_1) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create an AES keyring with the same key but null additionalDecryptionKeyMaterial + AesKeyring decryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_1) + .materialsDescription(materialsDescription) + .additionalDecryptionKeyMaterial(null) // Explicitly set to null + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Decrypt the object + ResponseBytes objectResponse = decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build()); + + // Verify the decrypted content + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } + + /** + * Test AES keyring with empty additionalDecryptionKeyMaterial map. + * This tests the behavior when an empty map is provided. + */ + @Test + public void testAesKeyringWithEmptyAdditionalKeyMaterial() { + final String objectKey = appendTestSuffix("aes-empty-additional-key-material"); + final String input = "AES with empty additional key material"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an AES keyring with the first key and materials description + AesKeyring encryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_1) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create an AES keyring with the same key but empty additionalDecryptionKeyMaterial + Map> emptyMap = new HashMap<>(); + AesKeyring decryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_1) + .materialsDescription(materialsDescription) + .additionalDecryptionKeyMaterial(emptyMap) // Empty map + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Decrypt the object + ResponseBytes objectResponse = decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build()); + + // Verify the decrypted content + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } + + /** + * Test AES keyring with a singleton additionalDecryptionKeyMaterial map. + * This tests the behavior when a single additional key material is provided. + */ + @Test + public void testAesKeyringWithSingletonAdditionalKeyMaterial() { + final String objectKey = appendTestSuffix("aes-singleton-additional-key-material"); + final String input = "AES with singleton additional key material"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an AES keyring with the first key and materials description + AesKeyring encryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_1) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create a singleton map with the matching materials description and the same key used for encryption + Map> singletonMap = new HashMap<>(); + singletonMap.put(materialsDescription, RawKeyMaterial.builder() + .materialsDescription(materialsDescription) + .keyMaterial(AES_KEY_1) // Use the same key that was used for encryption + .build()); + + // Create an AES keyring with a different key but with additionalDecryptionKeyMaterial containing the original key + AesKeyring decryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_3) // Different key than what was used for encryption + .materialsDescription(MaterialsDescription.builder().put("different", "description").build()) + .additionalDecryptionKeyMaterial(singletonMap) // Contains the key that matches the materials description + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Decrypt the object + ResponseBytes objectResponse = decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build()); + + // Verify the decrypted content + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } + + /** + * Test AES keyring with multiple entries in the additionalDecryptionKeyMaterial map. + * This tests the behavior when multiple additional key materials are provided. + */ + @Test + public void testAesKeyringWithMultipleAdditionalKeyMaterials() { + final String objectKey = appendTestSuffix("aes-multiple-additional-key-materials"); + final String input = "AES with multiple additional key materials"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an AES keyring with the first key and materials description + AesKeyring encryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_1) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create a map with multiple entries + Map> multipleMap = new HashMap<>(); + + // Add an entry that doesn't match + MaterialsDescription nonMatchingDesc = MaterialsDescription.builder() + .put("purpose", "different") + .put("version", "2") + .build(); + multipleMap.put(nonMatchingDesc, RawKeyMaterial.builder() + .materialsDescription(nonMatchingDesc) + .keyMaterial(AES_KEY_2) + .build()); + + // Add the matching entry + multipleMap.put(materialsDescription, RawKeyMaterial.builder() + .materialsDescription(materialsDescription) + .keyMaterial(AES_KEY_1) + .build()); + + // Create an AES keyring with a different key but with additionalDecryptionKeyMaterial containing the original key + AesKeyring decryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_3) // Different key than what was used for encryption + .materialsDescription(MaterialsDescription.builder().put("different", "description").build()) + .additionalDecryptionKeyMaterial(multipleMap) // Contains the key that matches the materials description + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Decrypt the object + ResponseBytes objectResponse = decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build()); + + // Verify the decrypted content + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } + + /** + * Test AES keyring with additionalDecryptionKeyMaterial that doesn't match. + * This tests the behavior when no matching key material is found and it should fall back to the default key. + */ + @Test + public void testAesKeyringWithNonMatchingAdditionalKeyMaterial() { + final String objectKey = appendTestSuffix("aes-non-matching-additional-key-material"); + final String input = "AES with non-matching additional key material"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an AES keyring with the first key and materials description + AesKeyring encryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_1) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create a map with a non-matching entry + Map> nonMatchingMap = new HashMap<>(); + MaterialsDescription nonMatchingDesc = MaterialsDescription.builder() + .put("purpose", "different") + .put("version", "2") + .build(); + nonMatchingMap.put(nonMatchingDesc, RawKeyMaterial.builder() + .materialsDescription(nonMatchingDesc) + .keyMaterial(AES_KEY_2) + .build()); + + // Create an AES keyring with the correct key as the default but with non-matching additionalDecryptionKeyMaterial + AesKeyring decryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_1) // Same key as used for encryption + .materialsDescription(materialsDescription) + .additionalDecryptionKeyMaterial(nonMatchingMap) // Contains a key that doesn't match + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Decrypt the object + ResponseBytes objectResponse = decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build()); + + // Verify the decrypted content + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } + + /** + * Test AES keyring with additionalDecryptionKeyMaterial that doesn't match and a wrong default key. + * This tests the behavior when no matching key material is found and the default key is also wrong. + */ + @Test + public void testAesKeyringWithNonMatchingAdditionalKeyMaterialAndWrongDefaultKey() { + final String objectKey = appendTestSuffix("aes-non-matching-additional-key-material-wrong-default"); + final String input = "AES with non-matching additional key material and wrong default key"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an AES keyring with the first key and materials description + AesKeyring encryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_1) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create a map with a non-matching entry + Map> nonMatchingMap = new HashMap<>(); + MaterialsDescription nonMatchingDesc = MaterialsDescription.builder() + .put("purpose", "different") + .put("version", "2") + .build(); + nonMatchingMap.put(nonMatchingDesc, RawKeyMaterial.builder() + .materialsDescription(nonMatchingDesc) + .keyMaterial(AES_KEY_2) + .build()); + + // Create an AES keyring with a wrong default key and non-matching additionalDecryptionKeyMaterial + AesKeyring decryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_3) // Different key than what was used for encryption + .materialsDescription(materialsDescription) + .additionalDecryptionKeyMaterial(nonMatchingMap) // Contains a key that doesn't match + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Attempt to decrypt the object, which should fail + assertThrows(S3EncryptionClientException.class, () -> decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build())); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } + + /** + * Test RSA keyring with null additionalDecryptionKeyMaterial map. + * This tests the default behavior when no additional key material is provided. + */ + @Test + public void testRsaKeyringWithNullAdditionalKeyMaterial() { + final String objectKey = appendTestSuffix("rsa-null-additional-key-material"); + final String input = "RSA with null additional key material"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an RSA keyring with the first key pair and materials description + RsaKeyring encryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build()) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create an RSA keyring with the same key pair but null additionalDecryptionKeyMaterial + RsaKeyring decryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build()) + .materialsDescription(materialsDescription) + .additionalDecryptionKeyMaterial(null) // Explicitly set to null + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Decrypt the object + ResponseBytes objectResponse = decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build()); + + // Verify the decrypted content + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } + + /** + * Test RSA keyring with empty additionalDecryptionKeyMaterial map. + * This tests the behavior when an empty map is provided. + */ + @Test + public void testRsaKeyringWithEmptyAdditionalKeyMaterial() { + final String objectKey = appendTestSuffix("rsa-empty-additional-key-material"); + final String input = "RSA with empty additional key material"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an RSA keyring with the first key pair and materials description + RsaKeyring encryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build()) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create an RSA keyring with the same key pair but empty additionalDecryptionKeyMaterial + Map> emptyMap = new HashMap<>(); + RsaKeyring decryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build()) + .materialsDescription(materialsDescription) + .additionalDecryptionKeyMaterial(emptyMap) // Empty map + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Decrypt the object + ResponseBytes objectResponse = decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build()); + + // Verify the decrypted content + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } + + /** + * Test RSA keyring with a singleton additionalDecryptionKeyMaterial map. + * This tests the behavior when a single additional key material is provided. + */ + @Test + public void testRsaKeyringWithSingletonAdditionalKeyMaterial() { + final String objectKey = appendTestSuffix("rsa-singleton-additional-key-material"); + final String input = "RSA with singleton additional key material"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an RSA keyring with the first key pair and materials description + RsaKeyring encryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build()) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create a singleton map with the matching materials description and the same key pair used for encryption + Map> singletonMap = new HashMap<>(); + singletonMap.put(materialsDescription, RawKeyMaterial.builder() + .materialsDescription(materialsDescription) + .keyMaterial(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build()) + .build()); + + // Create an RSA keyring with a different key pair but with additionalDecryptionKeyMaterial containing the original key pair + RsaKeyring decryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_3.getPublic()) + .privateKey(RSA_KEY_PAIR_3.getPrivate()) + .build()) + .materialsDescription(MaterialsDescription.builder().put("different", "description").build()) + .additionalDecryptionKeyMaterial(singletonMap) // Contains the key pair that matches the materials description + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Decrypt the object + ResponseBytes objectResponse = decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build()); + + // Verify the decrypted content + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } + + /** + * Test RSA keyring with multiple entries in the additionalDecryptionKeyMaterial map. + * This tests the behavior when multiple additional key materials are provided. + */ + @Test + public void testRsaKeyringWithMultipleAdditionalKeyMaterials() { + final String objectKey = appendTestSuffix("rsa-multiple-additional-key-materials"); + final String input = "RSA with multiple additional key materials"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an RSA keyring with the first key pair and materials description + RsaKeyring encryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build()) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create a map with multiple entries + Map> multipleMap = new HashMap<>(); + + // Add an entry that doesn't match + MaterialsDescription nonMatchingDesc = MaterialsDescription.builder() + .put("purpose", "different") + .put("version", "2") + .build(); + multipleMap.put(nonMatchingDesc, RawKeyMaterial.builder() + .materialsDescription(nonMatchingDesc) + .keyMaterial(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_2.getPublic()) + .privateKey(RSA_KEY_PAIR_2.getPrivate()) + .build()) + .build()); + + // Add the matching entry + multipleMap.put(materialsDescription, RawKeyMaterial.builder() + .materialsDescription(materialsDescription) + .keyMaterial(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build()) + .build()); + + // Create an RSA keyring with a different key pair but with additionalDecryptionKeyMaterial containing the original key pair + RsaKeyring decryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_3.getPublic()) + .privateKey(RSA_KEY_PAIR_3.getPrivate()) + .build()) + .materialsDescription(MaterialsDescription.builder().put("different", "description").build()) + .additionalDecryptionKeyMaterial(multipleMap) // Contains the key pair that matches the materials description + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Decrypt the object + ResponseBytes objectResponse = decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build()); + + // Verify the decrypted content + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } + + /** + * Test RSA keyring with additionalDecryptionKeyMaterial that doesn't match. + * This tests the behavior when no matching key material is found and it should fall back to the default key. + */ + @Test + public void testRsaKeyringWithNonMatchingAdditionalKeyMaterial() { + final String objectKey = appendTestSuffix("rsa-non-matching-additional-key-material"); + final String input = "RSA with non-matching additional key material"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an RSA keyring with the first key pair and materials description + RsaKeyring encryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build()) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create a map with a non-matching entry + Map> nonMatchingMap = new HashMap<>(); + MaterialsDescription nonMatchingDesc = MaterialsDescription.builder() + .put("purpose", "different") + .put("version", "2") + .build(); + nonMatchingMap.put(nonMatchingDesc, RawKeyMaterial.builder() + .materialsDescription(nonMatchingDesc) + .keyMaterial(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_2.getPublic()) + .privateKey(RSA_KEY_PAIR_2.getPrivate()) + .build()) + .build()); + + // Create an RSA keyring with the correct key pair as the default but with non-matching additionalDecryptionKeyMaterial + RsaKeyring decryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build()) + .materialsDescription(materialsDescription) + .additionalDecryptionKeyMaterial(nonMatchingMap) // Contains a key pair that doesn't match + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Decrypt the object + ResponseBytes objectResponse = decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build()); + + // Verify the decrypted content + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } + + /** + * Test RSA keyring with additionalDecryptionKeyMaterial that doesn't match and a wrong default key. + * This tests the behavior when no matching key material is found and the default key is also wrong. + */ + @Test + public void testRsaKeyringWithNonMatchingAdditionalKeyMaterialAndWrongDefaultKey() { + final String objectKey = appendTestSuffix("rsa-non-matching-additional-key-material-wrong-default"); + final String input = "RSA with non-matching additional key material and wrong default key"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an RSA keyring with the first key pair and materials description + RsaKeyring encryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build()) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create a map with a non-matching entry + Map> nonMatchingMap = new HashMap<>(); + MaterialsDescription nonMatchingDesc = MaterialsDescription.builder() + .put("purpose", "different") + .put("version", "2") + .build(); + nonMatchingMap.put(nonMatchingDesc, RawKeyMaterial.builder() + .materialsDescription(nonMatchingDesc) + .keyMaterial(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_2.getPublic()) + .privateKey(RSA_KEY_PAIR_2.getPrivate()) + .build()) + .build()); + + // Create an RSA keyring with a wrong default key pair and non-matching additionalDecryptionKeyMaterial + RsaKeyring decryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_3.getPublic()) + .privateKey(RSA_KEY_PAIR_3.getPrivate()) + .build()) + .materialsDescription(materialsDescription) + .additionalDecryptionKeyMaterial(nonMatchingMap) // Contains a key pair that doesn't match + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Attempt to decrypt the object, which should fail + assertThrows(S3EncryptionClientException.class, () -> decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build())); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } +} diff --git a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientCompatibilityTest.java b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientCompatibilityTest.java index 5f701abbd..589707bef 100644 --- a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientCompatibilityTest.java +++ b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientCompatibilityTest.java @@ -17,6 +17,7 @@ import com.amazonaws.services.s3.model.EncryptionMaterialsProvider; import com.amazonaws.services.s3.model.KMSEncryptionMaterials; import com.amazonaws.services.s3.model.KMSEncryptionMaterialsProvider; +import com.amazonaws.services.s3.model.SimpleMaterialProvider; import com.amazonaws.services.s3.model.StaticEncryptionMaterialsProvider; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -28,6 +29,8 @@ import software.amazon.awssdk.services.s3.model.MetadataDirective; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.encryption.s3.internal.InstructionFileConfig; +import software.amazon.encryption.s3.materials.AesKeyring; +import software.amazon.encryption.s3.materials.MaterialsDescription; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; @@ -42,7 +45,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; - 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.KMS_KEY_ID; @@ -274,6 +276,219 @@ public void AesGcmV3toV2() { deleteObject(BUCKET, objectKey, v3Client); v3Client.close(); } + @Test + public void AesGcmV2toV3MatDescValidation() { + final String objectKey = appendTestSuffix("aes-gcm-v2-to-v3-matdesc-validation"); + + // V2 Client + EncryptionMaterials aesKeyOneMats = new EncryptionMaterials(AES_KEY); + aesKeyOneMats.addDescription("key", "one-or-is-it???"); + SimpleMaterialProvider materialsProvider = new SimpleMaterialProvider(); + materialsProvider.addMaterial(aesKeyOneMats); + // Encrypt with this one + materialsProvider.withLatest(aesKeyOneMats); + + AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder() + .withEncryptionMaterialsProvider(materialsProvider) + .build(); + + // V3 Client + AesKeyring aesKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY) + // Same key, different MatDesc + .materialsDescription(MaterialsDescription.builder().put("key", "one").build()) + .build(); + S3Client v3Client = S3EncryptionClient.builder() + .keyring(aesKeyring) + .build(); + final String input = "AesGcmV3toV2"; + + // V2 encrypt, V3 decrypt + v2Client.putObject(BUCKET, objectKey, input); + ResponseBytes v3resp = v3Client.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build()); + assertEquals(input, v3resp.asUtf8String()); + + // Cleanup + deleteObject(BUCKET, objectKey, v3Client); + v3Client.close(); + } + + @Test + public void AesGcmV2toV3MatDescValidationNoMatDesc() { + final String objectKey = appendTestSuffix("aes-gcm-v2-to-v3-matdesc-validation-no-mat-desc"); + + // V2 Client + EncryptionMaterials aesKeyOneMats = new EncryptionMaterials(AES_KEY); + aesKeyOneMats.addDescription("key", "one-or-is-it???"); + SimpleMaterialProvider materialsProvider = new SimpleMaterialProvider(); + materialsProvider.addMaterial(aesKeyOneMats); + // Encrypt with this one + materialsProvider.withLatest(aesKeyOneMats); + + AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder() + .withEncryptionMaterialsProvider(materialsProvider) + .build(); + + // V3 Client + AesKeyring aesKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY) + .build(); + S3Client v3Client = S3EncryptionClient.builder() + .keyring(aesKeyring) + .build(); + final String input = "AesGcmV3toV2"; + + // V2 encrypt, V3 decrypt + v2Client.putObject(BUCKET, objectKey, input); + ResponseBytes v3resp = v3Client.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build()); + assertEquals(input, v3resp.asUtf8String()); + + // Cleanup + deleteObject(BUCKET, objectKey, v3Client); + v3Client.close(); + } + + @Test + public void AesGcmV3toV2MatDescValidation() { + final String objectKey = appendTestSuffix("aes-gcm-v3-to-v2-matdesc-validation"); + + // V2 Client + EncryptionMaterials aesKeyOneMats = new EncryptionMaterials(AES_KEY); + aesKeyOneMats.addDescription("key", "one-or-is-it???"); + SimpleMaterialProvider materialsProvider = new SimpleMaterialProvider(); + materialsProvider.addMaterial(aesKeyOneMats); + // Encrypt with this one + materialsProvider.withLatest(aesKeyOneMats); + + AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder() + .withEncryptionMaterialsProvider(materialsProvider) + .build(); + + // V3 Client + AesKeyring aesKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY) + // Same key, different MatDesc + .materialsDescription(MaterialsDescription.builder().put("key", "one").build()) + .build(); + S3Client v3Client = S3EncryptionClient.builder() + .keyring(aesKeyring) + .build(); + final String input = "AesGcmV2toV3"; + + // V3 encrypt, V2 decrypt + v3Client.putObject(builder -> builder + .bucket(BUCKET) + .key(objectKey), RequestBody.fromString(input)); + + String output = v2Client.getObjectAsString(BUCKET, objectKey); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, v3Client); + v3Client.close(); + } + + + @Test + public void AesGcmV3toV2MatDescValidationNoMatDesc() { + final String objectKey = appendTestSuffix("aes-gcm-v3-to-v2-matdesc-validation-no-mat-desc"); + + // V2 Client + EncryptionMaterials aesKeyOneMats = new EncryptionMaterials(AES_KEY); + aesKeyOneMats.addDescription("key", "one-or-is-it???"); + SimpleMaterialProvider materialsProvider = new SimpleMaterialProvider(); + materialsProvider.addMaterial(aesKeyOneMats); + // Encrypt with this one + materialsProvider.withLatest(aesKeyOneMats); + + AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder() + .withEncryptionMaterialsProvider(materialsProvider) + .build(); + + // V3 Client + AesKeyring aesKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY) + .build(); + S3Client v3Client = S3EncryptionClient.builder() + .keyring(aesKeyring) + .build(); + final String input = "AesGcmV3toV2"; + v3Client.putObject(builder -> builder + .bucket(BUCKET) + .key(objectKey), RequestBody.fromString(input)); + + String output = v2Client.getObjectAsString(BUCKET, objectKey); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, v3Client); + v3Client.close(); + } + @Test + public void AesGcmV3toV2ManyKeys() throws NoSuchAlgorithmException { + final String objectKey = appendTestSuffix("aes-gcm-v3-to-v2-many-keys"); + + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(256); + SecretKey aesKeyTwo = keyGen.generateKey(); + SecretKey aesKeyThree = keyGen.generateKey(); + + // V2 Client + EncryptionMaterials aesKeyOneMats = new EncryptionMaterials(AES_KEY); + aesKeyOneMats.addDescription("key", "one-or-is-it???"); +// EncryptionMaterials aesKeyTwoMats = new EncryptionMaterials(aesKeyTwo); +// aesKeyTwoMats.addDescription("key", "two"); +// EncryptionMaterials aesKeyThreeMats = new EncryptionMaterials(aesKeyThree); +// aesKeyThreeMats.addDescription("key", "three"); + SimpleMaterialProvider materialsProvider = new SimpleMaterialProvider(); + materialsProvider.addMaterial(aesKeyOneMats); +// materialsProvider.addMaterial(aesKeyTwoMats); +// materialsProvider.addMaterial(aesKeyThreeMats); + // Specify latest + materialsProvider.withLatest(aesKeyOneMats); +// materialsProvider.withLatest(aesKeyTwoMats); + + AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder() + .withEncryptionMaterialsProvider(materialsProvider) + .build(); + + // V3 Client + AesKeyring aesKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY) + .materialsDescription(MaterialsDescription.builder().put("key", "one").build()) + .build(); + S3Client v3Client = S3EncryptionClient.builder() + .keyring(aesKeyring) + .build(); + final String input = "AesGcmV3toV2"; + +// V3 encrypt, V2 decrypt +// v3Client.putObject(builder -> builder +// .bucket(BUCKET) +// .key(objectKey), RequestBody.fromString(input)); +// +// String output = v2Client.getObjectAsString(BUCKET, objectKey); +// assertEquals(input, output); + + // V2 encrypt, V3 decrypt + String objectKey2 = appendTestSuffix("v2-many-mats"); + v2Client.putObject(BUCKET, objectKey2, input); + ResponseBytes v3resp = v3Client.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey2) + .build()); + assertEquals(input, v3resp.asUtf8String()); + + // Cleanup + deleteObject(BUCKET, objectKey, v3Client); + v3Client.close(); + } @Test public void AesGcmV3toV3() { 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 43f6a979b..32a2a60cf 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) - .encryptionContextOrMatDesc(new HashMap()) + .encryptionContext(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 d40233ca0..f14e74227 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) - .encryptionContextOrMatDesc(encryptedDataKeyContext) + .encryptionContext(encryptedDataKeyContext) .build(); } @@ -57,7 +57,7 @@ public void testEncryptedDataKeyAlgorithm() { @Test public void testEncryptedDataKeyContext() { - assertEquals(encryptedDataKeyContext, actualContentMetadata.encryptedDataKeyMatDescOrContext()); + assertEquals(encryptedDataKeyContext, actualContentMetadata.encryptionContext()); } @Test diff --git a/src/test/java/software/amazon/encryption/s3/materials/KeyMaterialTest.java b/src/test/java/software/amazon/encryption/s3/materials/KeyMaterialTest.java new file mode 100644 index 000000000..d4de47d05 --- /dev/null +++ b/src/test/java/software/amazon/encryption/s3/materials/KeyMaterialTest.java @@ -0,0 +1,178 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.encryption.s3.materials; + +import org.junit.jupiter.api.Test; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Tests for the AesKeyMaterial and RsaKeyMaterial classes. + */ +public class KeyMaterialTest { + + /** + * Test creating AesKeyMaterial using the builder. + */ + @Test + public void testAesKeyMaterial() throws NoSuchAlgorithmException { + // Generate an AES key + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(256); + SecretKey aesKey = keyGen.generateKey(); + + // Create a materials description + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create AesKeyMaterial using the builder + AesKeyMaterial aesKeyMaterial = AesKeyMaterial.aesBuilder() + .materialsDescription(materialsDescription) + .keyMaterial(aesKey) + .build(); + + // Verify the key material + assertEquals(materialsDescription, aesKeyMaterial.getMaterialsDescription()); + assertEquals(aesKey, aesKeyMaterial.getKeyMaterial()); + } + + /** + * Test creating RsaKeyMaterial using the builder. + */ + @Test + public void testRsaKeyMaterial() throws NoSuchAlgorithmException { + // Generate an RSA key pair + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(2048); + KeyPair rsaKeyPair = keyPairGen.generateKeyPair(); + + // Create a PartialRsaKeyPair + PartialRsaKeyPair partialRsaKeyPair = PartialRsaKeyPair.builder() + .publicKey(rsaKeyPair.getPublic()) + .privateKey(rsaKeyPair.getPrivate()) + .build(); + + // Create a materials description + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create RsaKeyMaterial using the builder + RsaKeyMaterial rsaKeyMaterial = RsaKeyMaterial.rsaBuilder() + .materialsDescription(materialsDescription) + .keyMaterial(partialRsaKeyPair) + .build(); + + // Verify the key material + assertEquals(materialsDescription, rsaKeyMaterial.getMaterialsDescription()); + assertEquals(partialRsaKeyPair, rsaKeyMaterial.getKeyMaterial()); + } + + /** + * Test using AesKeyMaterial with additionalDecryptionKeyMaterial. + */ + @Test + public void testAesKeyMaterialWithAdditionalDecryptionKeyMaterial() throws NoSuchAlgorithmException { + // Generate AES keys + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(256); + SecretKey aesKey1 = keyGen.generateKey(); + SecretKey aesKey2 = keyGen.generateKey(); + + // Create materials descriptions + MaterialsDescription materialsDescription1 = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + MaterialsDescription materialsDescription2 = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "2") + .build(); + + // Create a map with AesKeyMaterial + Map> additionalKeyMaterial = new HashMap<>(); + + // Old way (with explicit type parameters) + additionalKeyMaterial.put(materialsDescription1, RawKeyMaterial.builder() + .materialsDescription(materialsDescription1) + .keyMaterial(aesKey1) + .build()); + + // New way (with concrete type) + additionalKeyMaterial.put(materialsDescription2, AesKeyMaterial.aesBuilder() + .materialsDescription(materialsDescription2) + .keyMaterial(aesKey2) + .build()); + + // Verify the map entries + assertNotNull(additionalKeyMaterial.get(materialsDescription1)); + assertNotNull(additionalKeyMaterial.get(materialsDescription2)); + assertEquals(aesKey1, additionalKeyMaterial.get(materialsDescription1).getKeyMaterial()); + assertEquals(aesKey2, additionalKeyMaterial.get(materialsDescription2).getKeyMaterial()); + } + + /** + * Test using RsaKeyMaterial with additionalDecryptionKeyMaterial. + */ + @Test + public void testRsaKeyMaterialWithAdditionalDecryptionKeyMaterial() throws NoSuchAlgorithmException { + // Generate RSA key pairs + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(2048); + KeyPair rsaKeyPair1 = keyPairGen.generateKeyPair(); + KeyPair rsaKeyPair2 = keyPairGen.generateKeyPair(); + + // Create PartialRsaKeyPairs + PartialRsaKeyPair partialRsaKeyPair1 = PartialRsaKeyPair.builder() + .publicKey(rsaKeyPair1.getPublic()) + .privateKey(rsaKeyPair1.getPrivate()) + .build(); + PartialRsaKeyPair partialRsaKeyPair2 = PartialRsaKeyPair.builder() + .publicKey(rsaKeyPair2.getPublic()) + .privateKey(rsaKeyPair2.getPrivate()) + .build(); + + // Create materials descriptions + MaterialsDescription materialsDescription1 = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + MaterialsDescription materialsDescription2 = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "2") + .build(); + + // Create a map with RsaKeyMaterial + Map> additionalKeyMaterial = new HashMap<>(); + + // Old way (with explicit type parameters) + additionalKeyMaterial.put(materialsDescription1, RawKeyMaterial.builder() + .materialsDescription(materialsDescription1) + .keyMaterial(partialRsaKeyPair1) + .build()); + + // New way (with concrete type) + additionalKeyMaterial.put(materialsDescription2, RsaKeyMaterial.rsaBuilder() + .materialsDescription(materialsDescription2) + .keyMaterial(partialRsaKeyPair2) + .build()); + + // Verify the map entries + assertNotNull(additionalKeyMaterial.get(materialsDescription1)); + assertNotNull(additionalKeyMaterial.get(materialsDescription2)); + assertEquals(partialRsaKeyPair1, additionalKeyMaterial.get(materialsDescription1).getKeyMaterial()); + assertEquals(partialRsaKeyPair2, additionalKeyMaterial.get(materialsDescription2).getKeyMaterial()); + } +} diff --git a/src/test/java/software/amazon/encryption/s3/materials/MaterialsDescriptionTest.java b/src/test/java/software/amazon/encryption/s3/materials/MaterialsDescriptionTest.java index 6a5230718..06073538e 100644 --- a/src/test/java/software/amazon/encryption/s3/materials/MaterialsDescriptionTest.java +++ b/src/test/java/software/amazon/encryption/s3/materials/MaterialsDescriptionTest.java @@ -1,6 +1,7 @@ package software.amazon.encryption.s3.materials; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -118,4 +119,60 @@ public void testMaterialsDescriptionRsaKeyring() { .build(); assertNotNull(rsaKeyring); } + + @Test + public void testEquals() { + // Create two identical MaterialsDescription objects + MaterialsDescription desc1 = MaterialsDescription.builder() + .put("key1", "value1") + .put("key2", "value2") + .build(); + + MaterialsDescription desc2 = MaterialsDescription.builder() + .put("key1", "value1") + .put("key2", "value2") + .build(); + + // Create a MaterialsDescription with different values + MaterialsDescription desc3 = MaterialsDescription.builder() + .put("key1", "value1") + .put("key2", "different") + .build(); + + // Create a MaterialsDescription with different keys + MaterialsDescription desc4 = MaterialsDescription.builder() + .put("key1", "value1") + .put("different", "value2") + .build(); + + // Create a MaterialsDescription with different number of entries + MaterialsDescription desc5 = MaterialsDescription.builder() + .put("key1", "value1") + .build(); + + // Test reflexivity + assertEquals(desc1, desc1); + + // Test symmetry + assertEquals(desc1, desc2); + assertEquals(desc2, desc1); + + // Test with different values + assertNotEquals(desc1, desc3); + + // Test with different keys + assertNotEquals(desc1, desc4); + + // Test with different number of entries + assertNotEquals(desc1, desc5); + + // Test with null + assertNotEquals(desc1, null); + + // Test with different type + assertNotEquals(desc1, "not a MaterialsDescription"); + + // Test hashCode + assertEquals(desc1.hashCode(), desc2.hashCode()); + } } From 790ae2bc23a778fee9f0eb03414733f094fb936b Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Tue, 26 Aug 2025 11:16:35 -0700 Subject: [PATCH 2/3] remove exploratory tests in Compatibility tests class --- .../S3EncryptionClientCompatibilityTest.java | 213 ------------------ 1 file changed, 213 deletions(-) diff --git a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientCompatibilityTest.java b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientCompatibilityTest.java index 589707bef..8559d2fa8 100644 --- a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientCompatibilityTest.java +++ b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientCompatibilityTest.java @@ -276,219 +276,6 @@ public void AesGcmV3toV2() { deleteObject(BUCKET, objectKey, v3Client); v3Client.close(); } - @Test - public void AesGcmV2toV3MatDescValidation() { - final String objectKey = appendTestSuffix("aes-gcm-v2-to-v3-matdesc-validation"); - - // V2 Client - EncryptionMaterials aesKeyOneMats = new EncryptionMaterials(AES_KEY); - aesKeyOneMats.addDescription("key", "one-or-is-it???"); - SimpleMaterialProvider materialsProvider = new SimpleMaterialProvider(); - materialsProvider.addMaterial(aesKeyOneMats); - // Encrypt with this one - materialsProvider.withLatest(aesKeyOneMats); - - AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder() - .withEncryptionMaterialsProvider(materialsProvider) - .build(); - - // V3 Client - AesKeyring aesKeyring = AesKeyring.builder() - .wrappingKey(AES_KEY) - // Same key, different MatDesc - .materialsDescription(MaterialsDescription.builder().put("key", "one").build()) - .build(); - S3Client v3Client = S3EncryptionClient.builder() - .keyring(aesKeyring) - .build(); - final String input = "AesGcmV3toV2"; - - // V2 encrypt, V3 decrypt - v2Client.putObject(BUCKET, objectKey, input); - ResponseBytes v3resp = v3Client.getObjectAsBytes(GetObjectRequest.builder() - .bucket(BUCKET) - .key(objectKey) - .build()); - assertEquals(input, v3resp.asUtf8String()); - - // Cleanup - deleteObject(BUCKET, objectKey, v3Client); - v3Client.close(); - } - - @Test - public void AesGcmV2toV3MatDescValidationNoMatDesc() { - final String objectKey = appendTestSuffix("aes-gcm-v2-to-v3-matdesc-validation-no-mat-desc"); - - // V2 Client - EncryptionMaterials aesKeyOneMats = new EncryptionMaterials(AES_KEY); - aesKeyOneMats.addDescription("key", "one-or-is-it???"); - SimpleMaterialProvider materialsProvider = new SimpleMaterialProvider(); - materialsProvider.addMaterial(aesKeyOneMats); - // Encrypt with this one - materialsProvider.withLatest(aesKeyOneMats); - - AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder() - .withEncryptionMaterialsProvider(materialsProvider) - .build(); - - // V3 Client - AesKeyring aesKeyring = AesKeyring.builder() - .wrappingKey(AES_KEY) - .build(); - S3Client v3Client = S3EncryptionClient.builder() - .keyring(aesKeyring) - .build(); - final String input = "AesGcmV3toV2"; - - // V2 encrypt, V3 decrypt - v2Client.putObject(BUCKET, objectKey, input); - ResponseBytes v3resp = v3Client.getObjectAsBytes(GetObjectRequest.builder() - .bucket(BUCKET) - .key(objectKey) - .build()); - assertEquals(input, v3resp.asUtf8String()); - - // Cleanup - deleteObject(BUCKET, objectKey, v3Client); - v3Client.close(); - } - - @Test - public void AesGcmV3toV2MatDescValidation() { - final String objectKey = appendTestSuffix("aes-gcm-v3-to-v2-matdesc-validation"); - - // V2 Client - EncryptionMaterials aesKeyOneMats = new EncryptionMaterials(AES_KEY); - aesKeyOneMats.addDescription("key", "one-or-is-it???"); - SimpleMaterialProvider materialsProvider = new SimpleMaterialProvider(); - materialsProvider.addMaterial(aesKeyOneMats); - // Encrypt with this one - materialsProvider.withLatest(aesKeyOneMats); - - AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder() - .withEncryptionMaterialsProvider(materialsProvider) - .build(); - - // V3 Client - AesKeyring aesKeyring = AesKeyring.builder() - .wrappingKey(AES_KEY) - // Same key, different MatDesc - .materialsDescription(MaterialsDescription.builder().put("key", "one").build()) - .build(); - S3Client v3Client = S3EncryptionClient.builder() - .keyring(aesKeyring) - .build(); - final String input = "AesGcmV2toV3"; - - // V3 encrypt, V2 decrypt - v3Client.putObject(builder -> builder - .bucket(BUCKET) - .key(objectKey), RequestBody.fromString(input)); - - String output = v2Client.getObjectAsString(BUCKET, objectKey); - assertEquals(input, output); - - // Cleanup - deleteObject(BUCKET, objectKey, v3Client); - v3Client.close(); - } - - - @Test - public void AesGcmV3toV2MatDescValidationNoMatDesc() { - final String objectKey = appendTestSuffix("aes-gcm-v3-to-v2-matdesc-validation-no-mat-desc"); - - // V2 Client - EncryptionMaterials aesKeyOneMats = new EncryptionMaterials(AES_KEY); - aesKeyOneMats.addDescription("key", "one-or-is-it???"); - SimpleMaterialProvider materialsProvider = new SimpleMaterialProvider(); - materialsProvider.addMaterial(aesKeyOneMats); - // Encrypt with this one - materialsProvider.withLatest(aesKeyOneMats); - - AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder() - .withEncryptionMaterialsProvider(materialsProvider) - .build(); - - // V3 Client - AesKeyring aesKeyring = AesKeyring.builder() - .wrappingKey(AES_KEY) - .build(); - S3Client v3Client = S3EncryptionClient.builder() - .keyring(aesKeyring) - .build(); - final String input = "AesGcmV3toV2"; - v3Client.putObject(builder -> builder - .bucket(BUCKET) - .key(objectKey), RequestBody.fromString(input)); - - String output = v2Client.getObjectAsString(BUCKET, objectKey); - assertEquals(input, output); - - // Cleanup - deleteObject(BUCKET, objectKey, v3Client); - v3Client.close(); - } - @Test - public void AesGcmV3toV2ManyKeys() throws NoSuchAlgorithmException { - final String objectKey = appendTestSuffix("aes-gcm-v3-to-v2-many-keys"); - - KeyGenerator keyGen = KeyGenerator.getInstance("AES"); - keyGen.init(256); - SecretKey aesKeyTwo = keyGen.generateKey(); - SecretKey aesKeyThree = keyGen.generateKey(); - - // V2 Client - EncryptionMaterials aesKeyOneMats = new EncryptionMaterials(AES_KEY); - aesKeyOneMats.addDescription("key", "one-or-is-it???"); -// EncryptionMaterials aesKeyTwoMats = new EncryptionMaterials(aesKeyTwo); -// aesKeyTwoMats.addDescription("key", "two"); -// EncryptionMaterials aesKeyThreeMats = new EncryptionMaterials(aesKeyThree); -// aesKeyThreeMats.addDescription("key", "three"); - SimpleMaterialProvider materialsProvider = new SimpleMaterialProvider(); - materialsProvider.addMaterial(aesKeyOneMats); -// materialsProvider.addMaterial(aesKeyTwoMats); -// materialsProvider.addMaterial(aesKeyThreeMats); - // Specify latest - materialsProvider.withLatest(aesKeyOneMats); -// materialsProvider.withLatest(aesKeyTwoMats); - - AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder() - .withEncryptionMaterialsProvider(materialsProvider) - .build(); - - // V3 Client - AesKeyring aesKeyring = AesKeyring.builder() - .wrappingKey(AES_KEY) - .materialsDescription(MaterialsDescription.builder().put("key", "one").build()) - .build(); - S3Client v3Client = S3EncryptionClient.builder() - .keyring(aesKeyring) - .build(); - final String input = "AesGcmV3toV2"; - -// V3 encrypt, V2 decrypt -// v3Client.putObject(builder -> builder -// .bucket(BUCKET) -// .key(objectKey), RequestBody.fromString(input)); -// -// String output = v2Client.getObjectAsString(BUCKET, objectKey); -// assertEquals(input, output); - - // V2 encrypt, V3 decrypt - String objectKey2 = appendTestSuffix("v2-many-mats"); - v2Client.putObject(BUCKET, objectKey2, input); - ResponseBytes v3resp = v3Client.getObjectAsBytes(GetObjectRequest.builder() - .bucket(BUCKET) - .key(objectKey2) - .build()); - assertEquals(input, v3resp.asUtf8String()); - - // Cleanup - deleteObject(BUCKET, objectKey, v3Client); - v3Client.close(); - } @Test public void AesGcmV3toV3() { From ec5887ead6d46be0c3428e81c5991dd836f9adfb Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 27 Aug 2025 17:18:43 -0700 Subject: [PATCH 3/3] add tests for ReEncrypt --- .../encryption/s3/S3EncryptionClient.java | 1 + ...WithAdditionalDecryptionMaterialsTest.java | 599 ++++++++++++++++++ 2 files changed, 600 insertions(+) create mode 100644 src/test/java/software/amazon/encryption/s3/S3EncryptionClientReEncryptInstructionFileWithAdditionalDecryptionMaterialsTest.java diff --git a/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java b/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java index 9ebeb9b98..0b17a4911 100644 --- a/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java +++ b/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java @@ -258,6 +258,7 @@ public ReEncryptInstructionFileResponse reEncryptInstructionFile(ReEncryptInstru DecryptMaterialsRequest.builder() .algorithmSuite(algorithmSuite) .encryptedDataKeys(Collections.singletonList(originalEncryptedDataKey)) + .materialsDescription(contentMetadata.materialsDescription()) .s3Request(request) .build() ); diff --git a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientReEncryptInstructionFileWithAdditionalDecryptionMaterialsTest.java b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientReEncryptInstructionFileWithAdditionalDecryptionMaterialsTest.java new file mode 100644 index 000000000..77a3810b3 --- /dev/null +++ b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientReEncryptInstructionFileWithAdditionalDecryptionMaterialsTest.java @@ -0,0 +1,599 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.encryption.s3; + +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.AesKeyMaterial; +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.RawKeyMaterial; +import software.amazon.encryption.s3.materials.RsaKeyMaterial; +import software.amazon.encryption.s3.materials.RsaKeyring; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +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; + +/** + * This class tests the ReEncryptInstructionFile operation with additionalDecryptionMaterials. + * It tests scenarios where the client is configured with additionalDecryptionMaterials and uses + * those materials to decrypt the instruction file during the re-encryption process. + */ +public class S3EncryptionClientReEncryptInstructionFileWithAdditionalDecryptionMaterialsTest { + + private static SecretKey AES_KEY_1; + private static SecretKey AES_KEY_2; + private static SecretKey AES_KEY_3; + private static KeyPair RSA_KEY_PAIR_1; + private static KeyPair RSA_KEY_PAIR_2; + private static KeyPair RSA_KEY_PAIR_3; + + @BeforeAll + public static void setUp() throws NoSuchAlgorithmException { + // Generate AES keys + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(256); + AES_KEY_1 = keyGen.generateKey(); + AES_KEY_2 = keyGen.generateKey(); + AES_KEY_3 = keyGen.generateKey(); + + // Generate RSA key pairs + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(2048); + RSA_KEY_PAIR_1 = keyPairGen.generateKeyPair(); + RSA_KEY_PAIR_2 = keyPairGen.generateKeyPair(); + RSA_KEY_PAIR_3 = keyPairGen.generateKeyPair(); + } + + /** + * Test AES keyring with additionalDecryptionMaterials for ReEncryptInstructionFile. + * This test encrypts an object with AES_KEY_1, then uses a client with AES_KEY_2 as the primary key + * but with additionalDecryptionMaterials containing AES_KEY_1 to re-encrypt the instruction file. + */ + @Test + public void testAesKeyringReEncryptInstructionFileWithAdditionalDecryptionMaterials() { + // Create materials descriptions + MaterialsDescription originalMatDesc = MaterialsDescription.builder() + .put("purpose", "original") + .put("version", "1") + .build(); + + MaterialsDescription newMatDesc = MaterialsDescription.builder() + .put("purpose", "rotated") + .put("version", "2") + .build(); + + MaterialsDescription otherMatDesc = MaterialsDescription.builder() + .put("purpose", "testing") + .put("do not use", "just for testing multi-key") + .build(); + + // Create a map of additional decryption key materials containing all the keys + Map> additionalDecryptionKeyMaterial = new HashMap<>(); + additionalDecryptionKeyMaterial.put(originalMatDesc, RawKeyMaterial.builder() + .materialsDescription(originalMatDesc) + .keyMaterial(AES_KEY_1) + .build()); + additionalDecryptionKeyMaterial.put(newMatDesc, RawKeyMaterial.builder() + .materialsDescription(newMatDesc) + .keyMaterial(AES_KEY_2) + .build()); + additionalDecryptionKeyMaterial.put(otherMatDesc, AesKeyMaterial.aesBuilder() + .materialsDescription(otherMatDesc) + .keyMaterial(AES_KEY_3) + .build()); + + // Create an AES keyring with the first key and original materials description + AesKeyring originalKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_1) + .materialsDescription(originalMatDesc) + .build(); + + // Create an S3 client for the original encryption + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient originalClient = S3EncryptionClient.builder() + .keyring(originalKeyring) + .instructionFileConfig( + InstructionFileConfig.builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + // Create a test object key and content + final String objectKey = appendTestSuffix("aes-re-encrypt-instruction-file-with-additional-decryption-materials"); + final String input = "Testing re-encryption of instruction file with AES Keyring and additional decryption materials"; + + // Encrypt and upload the object with the original keyring + originalClient.putObject( + builder -> builder.bucket(BUCKET).key(objectKey).build(), + RequestBody.fromString(input) + ); + + // Get the original instruction file to verify its contents + ResponseBytes originalInstructionFile = wrappedClient.getObjectAsBytes( + builder -> builder.bucket(BUCKET).key(objectKey + ".instruction").build() + ); + + // Parse the original instruction file + String originalInstructionFileContent = originalInstructionFile.asUtf8String(); + JsonNodeParser parser = JsonNodeParser.create(); + JsonNode originalInstructionFileNode = parser.parse(originalInstructionFileContent); + + String originalIv = originalInstructionFileNode.asObject().get("x-amz-iv").asString(); + String originalEncryptedDataKeyAlgorithm = originalInstructionFileNode.asObject().get("x-amz-wrap-alg").asString(); + String originalEncryptedDataKey = originalInstructionFileNode.asObject().get("x-amz-key-v2").asString(); + JsonNode originalMatDescNode = parser.parse(originalInstructionFileNode.asObject().get("x-amz-matdesc").asString()); + + assertEquals("original", originalMatDescNode.asObject().get("purpose").asString()); + assertEquals("1", originalMatDescNode.asObject().get("version").asString()); + + // Create a new AES keyring with a different key as primary but with additionalDecryptionKeyMaterial containing the original key + AesKeyring newKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_2) // Key used to ReEncrypt + .materialsDescription(newMatDesc) + .additionalDecryptionKeyMaterial(additionalDecryptionKeyMaterial) // contains the original key + .build(); + + // Create a client with the new keyring + S3EncryptionClient newClient = S3EncryptionClient.builder() + .keyring(newKeyring) + .instructionFileConfig( + InstructionFileConfig.builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + // Re-encrypt the instruction file with the new keyring + ReEncryptInstructionFileRequest reEncryptRequest = ReEncryptInstructionFileRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .build(); + + // The re-encryption should succeed because the new client contains the original key in additionalDecryptionMaterials + ReEncryptInstructionFileResponse response = newClient.reEncryptInstructionFile(reEncryptRequest); + + // Verify the response + assertEquals(BUCKET, response.bucket()); + assertEquals(objectKey, response.key()); + assertEquals("instruction", response.instructionFileSuffix()); + + // Get the re-encrypted instruction file to verify its contents + ResponseBytes reEncryptedInstructionFile = wrappedClient.getObjectAsBytes( + builder -> builder.bucket(BUCKET).key(objectKey + ".instruction").build() + ); + + // Parse the re-encrypted instruction file + String reEncryptedInstructionFileContent = reEncryptedInstructionFile.asUtf8String(); + JsonNode reEncryptedInstructionFileNode = parser.parse(reEncryptedInstructionFileContent); + + String reEncryptedIv = reEncryptedInstructionFileNode.asObject().get("x-amz-iv").asString(); + String reEncryptedDataKeyAlgorithm = reEncryptedInstructionFileNode.asObject().get("x-amz-wrap-alg").asString(); + String reEncryptedDataKey = reEncryptedInstructionFileNode.asObject().get("x-amz-key-v2").asString(); + JsonNode reEncryptedMatDescNode = parser.parse(reEncryptedInstructionFileNode.asObject().get("x-amz-matdesc").asString()); + + // Verify the re-encrypted instruction file has the new materials description + assertEquals("rotated", reEncryptedMatDescNode.asObject().get("purpose").asString()); + assertEquals("2", reEncryptedMatDescNode.asObject().get("version").asString()); + + // Verify the IV is preserved but the encrypted data key is different + assertEquals(originalIv, reEncryptedIv); + assertEquals(originalEncryptedDataKeyAlgorithm, reEncryptedDataKeyAlgorithm); + assertNotEquals(originalEncryptedDataKey, reEncryptedDataKey); + + // Verify decryption works with the new client (already created above) + + // Verify the object can be decrypted with the new key + ResponseBytes decryptedObject = newClient.getObjectAsBytes( + GetObjectRequest.builder().bucket(BUCKET).key(objectKey).build() + ); + assertEquals(input, decryptedObject.asUtf8String()); + + // Verify the original client can no longer decrypt the object with the original keyring + try { + originalClient.getObjectAsBytes( + GetObjectRequest.builder().bucket(BUCKET).key(objectKey).build() + ); + assertTrue(false, "Original client should not be able to decrypt the re-encrypted object"); + } catch (S3EncryptionClientException e) { + // Expected exception + assertTrue(e.getMessage().contains("Unable to AES/GCM unwrap")); + } + + // Cleanup + deleteObject(BUCKET, objectKey, newClient); + originalClient.close(); + newClient.close(); + } + + /** + * Test RSA keyring with additionalDecryptionMaterials for ReEncryptInstructionFile. + * This test encrypts an object with RSA_KEY_PAIR_1, then uses a client with RSA_KEY_PAIR_2 as the primary key + * but with additionalDecryptionMaterials containing RSA_KEY_PAIR_1 to re-encrypt the instruction file. + */ + @Test + public void testRsaKeyringReEncryptInstructionFileWithAdditionalDecryptionMaterials() { + // Create materials descriptions + MaterialsDescription originalMatDesc = MaterialsDescription.builder() + .put("purpose", "original") + .put("version", "1") + .build(); + + MaterialsDescription newMatDesc = MaterialsDescription.builder() + .put("purpose", "rotated") + .put("version", "2") + .build(); + + MaterialsDescription otherMatDesc = MaterialsDescription.builder() + .put("purpose", "testing") + .put("do not use", "just for testing multi-key") + .build(); + + // Create RSA key pairs for the test + PartialRsaKeyPair originalKeyPair = PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build(); + + PartialRsaKeyPair newKeyPair = PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_2.getPublic()) + .privateKey(RSA_KEY_PAIR_2.getPrivate()) + .build(); + + PartialRsaKeyPair otherKeyPair = PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_3.getPublic()) + .privateKey(RSA_KEY_PAIR_3.getPrivate()) + .build(); + + // Create a map of additional decryption key materials containing the original key pair + Map> additionalDecryptionKeyMaterial = new HashMap<>(); + additionalDecryptionKeyMaterial.put(originalMatDesc, RawKeyMaterial.builder() + .materialsDescription(originalMatDesc) + .keyMaterial(originalKeyPair) + .build()); + additionalDecryptionKeyMaterial.put(newMatDesc, RsaKeyMaterial.rsaBuilder() + .materialsDescription(newMatDesc) + .keyMaterial(newKeyPair) + .build()); + additionalDecryptionKeyMaterial.put(otherMatDesc, RsaKeyMaterial.rsaBuilder() + .materialsDescription(otherMatDesc) + .keyMaterial(otherKeyPair) + .build()); + + // Create an RSA keyring with the first key pair and original materials description + RsaKeyring originalKeyring = RsaKeyring.builder() + .wrappingKeyPair(originalKeyPair) + .materialsDescription(originalMatDesc) + .build(); + + // Create an S3 client for the original encryption + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient originalClient = S3EncryptionClient.builder() + .keyring(originalKeyring) + .instructionFileConfig( + InstructionFileConfig.builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + // Create a test object key and content + final String objectKey = appendTestSuffix("rsa-re-encrypt-instruction-file-with-additional-decryption-materials"); + final String input = "Testing re-encryption of instruction file with RSA Keyring and additional decryption materials"; + + // Encrypt and upload the object with the original keyring + originalClient.putObject( + builder -> builder.bucket(BUCKET).key(objectKey).build(), + RequestBody.fromString(input) + ); + + // Get the original instruction file to verify its contents + ResponseBytes originalInstructionFile = wrappedClient.getObjectAsBytes( + builder -> builder.bucket(BUCKET).key(objectKey + ".instruction").build() + ); + + // Parse the original instruction file + String originalInstructionFileContent = originalInstructionFile.asUtf8String(); + JsonNodeParser parser = JsonNodeParser.create(); + JsonNode originalInstructionFileNode = parser.parse(originalInstructionFileContent); + + String originalIv = originalInstructionFileNode.asObject().get("x-amz-iv").asString(); + String originalEncryptedDataKeyAlgorithm = originalInstructionFileNode.asObject().get("x-amz-wrap-alg").asString(); + String originalEncryptedDataKey = originalInstructionFileNode.asObject().get("x-amz-key-v2").asString(); + JsonNode originalMatDescNode = parser.parse(originalInstructionFileNode.asObject().get("x-amz-matdesc").asString()); + + assertEquals("original", originalMatDescNode.asObject().get("purpose").asString()); + assertEquals("1", originalMatDescNode.asObject().get("version").asString()); + + // Create a new RSA keyring with a different key pair + RsaKeyring newKeyring = RsaKeyring.builder() + .wrappingKeyPair(newKeyPair) // Different key pair than what was used for original encryption + .materialsDescription(newMatDesc) + .additionalDecryptionKeyMaterial(additionalDecryptionKeyMaterial) + .build(); + + // Create a client with the new keyring + S3EncryptionClient newClient = S3EncryptionClient.builder() + .keyring(newKeyring) + .instructionFileConfig( + InstructionFileConfig.builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + // Re-encrypt the instruction file with the new keyring + ReEncryptInstructionFileRequest reEncryptRequest = ReEncryptInstructionFileRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .build(); + + // The re-encryption should succeed because the new client contains the original key in additionalDecryptionMaterials + ReEncryptInstructionFileResponse response = newClient.reEncryptInstructionFile(reEncryptRequest); + + // Verify the response + assertEquals(BUCKET, response.bucket()); + assertEquals(objectKey, response.key()); + assertEquals("instruction", response.instructionFileSuffix()); + + // Get the re-encrypted instruction file to verify its contents + ResponseBytes reEncryptedInstructionFile = wrappedClient.getObjectAsBytes( + builder -> builder.bucket(BUCKET).key(objectKey + ".instruction").build() + ); + + // Parse the re-encrypted instruction file + String reEncryptedInstructionFileContent = reEncryptedInstructionFile.asUtf8String(); + JsonNode reEncryptedInstructionFileNode = parser.parse(reEncryptedInstructionFileContent); + + String reEncryptedIv = reEncryptedInstructionFileNode.asObject().get("x-amz-iv").asString(); + String reEncryptedDataKeyAlgorithm = reEncryptedInstructionFileNode.asObject().get("x-amz-wrap-alg").asString(); + String reEncryptedDataKey = reEncryptedInstructionFileNode.asObject().get("x-amz-key-v2").asString(); + JsonNode reEncryptedMatDescNode = parser.parse(reEncryptedInstructionFileNode.asObject().get("x-amz-matdesc").asString()); + + // Verify the re-encrypted instruction file has the new materials description + assertEquals("rotated", reEncryptedMatDescNode.asObject().get("purpose").asString()); + assertEquals("2", reEncryptedMatDescNode.asObject().get("version").asString()); + + // Verify the IV is preserved but the encrypted data key is different + assertEquals(originalIv, reEncryptedIv); + assertEquals(originalEncryptedDataKeyAlgorithm, reEncryptedDataKeyAlgorithm); + assertNotEquals(originalEncryptedDataKey, reEncryptedDataKey); + + // Verify decryption works with the new client (already created above) + + // Verify the object can be decrypted with the new key + ResponseBytes decryptedObject = newClient.getObjectAsBytes( + GetObjectRequest.builder().bucket(BUCKET).key(objectKey).build() + ); + assertEquals(input, decryptedObject.asUtf8String()); + + // Verify the original client can no longer decrypt the object with the original keyring + try { + originalClient.getObjectAsBytes( + GetObjectRequest.builder().bucket(BUCKET).key(objectKey).build() + ); + assertTrue(false, "Original client should not be able to decrypt the re-encrypted object"); + } catch (S3EncryptionClientException e) { + // Expected exception + assertTrue(e.getMessage().contains("Unable to RSA-OAEP-SHA1 unwrap")); + } + + // Cleanup + deleteObject(BUCKET, objectKey, newClient); + originalClient.close(); + newClient.close(); + } + + /** + * Test RSA keyring with custom suffix and additionalDecryptionMaterials for ReEncryptInstructionFile. + * This test encrypts an object with RSA_KEY_PAIR_1, then uses a client with RSA_KEY_PAIR_2 as the primary key + * but with additionalDecryptionMaterials containing RSA_KEY_PAIR_1 to re-encrypt the instruction file with a custom suffix. + */ + @Test + public void testRsaKeyringReEncryptInstructionFileWithCustomSuffixAndAdditionalDecryptionMaterials() { + // Create materials descriptions + MaterialsDescription originalMatDesc = MaterialsDescription.builder() + .put("purpose", "original") + .put("access", "owner") + .build(); + + MaterialsDescription newMatDesc = MaterialsDescription.builder() + .put("purpose", "shared") + .put("access", "partner") + .build(); + + MaterialsDescription otherMatDesc = MaterialsDescription.builder() + .put("purpose", "testing") + .put("do not use", "just for testing multi-key") + .build(); + + // Create RSA key pairs for the test + PartialRsaKeyPair originalKeyPair = PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build(); + + PartialRsaKeyPair newKeyPair = PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_2.getPublic()) + .privateKey(RSA_KEY_PAIR_2.getPrivate()) + .build(); + + PartialRsaKeyPair otherKeyPair = PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_3.getPublic()) + .privateKey(RSA_KEY_PAIR_3.getPrivate()) + .build(); + + // Create a map of additional decryption key materials containing the original key pair + Map> additionalDecryptionKeyMaterial = new HashMap<>(); + additionalDecryptionKeyMaterial.put(originalMatDesc, RawKeyMaterial.builder() + .materialsDescription(originalMatDesc) + .keyMaterial(originalKeyPair) + .build()); + additionalDecryptionKeyMaterial.put(newMatDesc, RsaKeyMaterial.rsaBuilder() + .materialsDescription(newMatDesc) + .keyMaterial(newKeyPair) + .build()); + additionalDecryptionKeyMaterial.put(otherMatDesc, RsaKeyMaterial.rsaBuilder() + .materialsDescription(otherMatDesc) + .keyMaterial(otherKeyPair) + .build()); + + // Create an RSA keyring with the first key pair and original materials description + RsaKeyring originalKeyring = RsaKeyring.builder() + .wrappingKeyPair(originalKeyPair) + .materialsDescription(originalMatDesc) + .build(); + + // Create an S3 client for the original encryption + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient originalClient = S3EncryptionClient.builder() + .keyring(originalKeyring) + .instructionFileConfig( + InstructionFileConfig.builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + // Create a test object key and content + final String objectKey = appendTestSuffix("rsa-re-encrypt-instruction-file-with-custom-suffix-and-additional-decryption-materials"); + final String input = "Testing re-encryption of instruction file with RSA Keyring, custom suffix, and additional decryption materials"; + final String customSuffix = "partner-access"; + + // Encrypt and upload the object with the original keyring + originalClient.putObject( + builder -> builder.bucket(BUCKET).key(objectKey).build(), + RequestBody.fromString(input) + ); + + // Get the original instruction file to verify its contents + ResponseBytes originalInstructionFile = wrappedClient.getObjectAsBytes( + builder -> builder.bucket(BUCKET).key(objectKey + ".instruction").build() + ); + + // Parse the original instruction file + String originalInstructionFileContent = originalInstructionFile.asUtf8String(); + JsonNodeParser parser = JsonNodeParser.create(); + JsonNode originalInstructionFileNode = parser.parse(originalInstructionFileContent); + + String originalIv = originalInstructionFileNode.asObject().get("x-amz-iv").asString(); + String originalEncryptedDataKeyAlgorithm = originalInstructionFileNode.asObject().get("x-amz-wrap-alg").asString(); + String originalEncryptedDataKey = originalInstructionFileNode.asObject().get("x-amz-key-v2").asString(); + JsonNode originalMatDescNode = parser.parse(originalInstructionFileNode.asObject().get("x-amz-matdesc").asString()); + + assertEquals("original", originalMatDescNode.asObject().get("purpose").asString()); + assertEquals("owner", originalMatDescNode.asObject().get("access").asString()); + + // Create a new RSA keyring with a different key pair + RsaKeyring newKeyring = RsaKeyring.builder() + .wrappingKeyPair(newKeyPair) // Different key pair than what was used for original encryption + .materialsDescription(newMatDesc) + .additionalDecryptionKeyMaterial(additionalDecryptionKeyMaterial) + .build(); + + // Create a client with the new keyring + S3EncryptionClient newClient = S3EncryptionClient.builder() + .keyring(newKeyring) + .instructionFileConfig( + InstructionFileConfig.builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + // Re-encrypt the instruction file with the new keyring and custom suffix + ReEncryptInstructionFileRequest reEncryptRequest = ReEncryptInstructionFileRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .instructionFileSuffix(customSuffix) + .build(); + + // The re-encryption should succeed because the new client contains the original key in additionalDecryptionMaterials + ReEncryptInstructionFileResponse response = newClient.reEncryptInstructionFile(reEncryptRequest); + + // Verify the response + assertEquals(BUCKET, response.bucket()); + assertEquals(objectKey, response.key()); + assertEquals(customSuffix, response.instructionFileSuffix()); + + // Get the re-encrypted instruction file with custom suffix to verify its contents + ResponseBytes reEncryptedInstructionFile = wrappedClient.getObjectAsBytes( + builder -> builder.bucket(BUCKET).key(objectKey + "." + customSuffix).build() + ); + + // Parse the re-encrypted instruction file + String reEncryptedInstructionFileContent = reEncryptedInstructionFile.asUtf8String(); + JsonNode reEncryptedInstructionFileNode = parser.parse(reEncryptedInstructionFileContent); + + String reEncryptedIv = reEncryptedInstructionFileNode.asObject().get("x-amz-iv").asString(); + String reEncryptedDataKeyAlgorithm = reEncryptedInstructionFileNode.asObject().get("x-amz-wrap-alg").asString(); + String reEncryptedDataKey = reEncryptedInstructionFileNode.asObject().get("x-amz-key-v2").asString(); + JsonNode reEncryptedMatDescNode = parser.parse(reEncryptedInstructionFileNode.asObject().get("x-amz-matdesc").asString()); + + // Verify the re-encrypted instruction file has the new materials description + assertEquals("shared", reEncryptedMatDescNode.asObject().get("purpose").asString()); + assertEquals("partner", reEncryptedMatDescNode.asObject().get("access").asString()); + + // Verify the IV is preserved but the encrypted data key is different + assertEquals(originalIv, reEncryptedIv); + assertEquals(originalEncryptedDataKeyAlgorithm, reEncryptedDataKeyAlgorithm); + assertNotEquals(originalEncryptedDataKey, reEncryptedDataKey); + + // Verify decryption works with the new client (already created above) + + // Verify the object can be decrypted with the new key and custom suffix + ResponseBytes decryptedObject = newClient.getObjectAsBytes( + GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .overrideConfiguration(withCustomInstructionFileSuffix("." + customSuffix)) + .build() + ); + assertEquals(input, decryptedObject.asUtf8String()); + + // Verify the original client can still decrypt using the default instruction file + ResponseBytes originalDecryptedObject = originalClient.getObjectAsBytes( + GetObjectRequest.builder().bucket(BUCKET).key(objectKey).build() + ); + assertEquals(input, originalDecryptedObject.asUtf8String()); + + // Cleanup + deleteObject(BUCKET, objectKey, newClient); + originalClient.close(); + newClient.close(); + } +}