From 47e4330b5a2fb1d096d0037401d39e0900c7039d Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Mon, 22 Dec 2025 23:34:50 -0800 Subject: [PATCH 1/7] m --- DynamoDbEncryption/runtimes/java/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DynamoDbEncryption/runtimes/java/build.gradle.kts b/DynamoDbEncryption/runtimes/java/build.gradle.kts index 2bbd34588..08c1273e4 100644 --- a/DynamoDbEncryption/runtimes/java/build.gradle.kts +++ b/DynamoDbEncryption/runtimes/java/build.gradle.kts @@ -92,7 +92,7 @@ dependencies { testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.11.4") // For the DDB-EC v1 - implementation("com.amazonaws:aws-java-sdk-dynamodb:1.12.780") + compileOnly("com.amazonaws:aws-java-sdk-dynamodb:1.12.780") // https://mvnrepository.com/artifact/org.testng/testng testImplementation("org.testng:testng:7.5") // https://mvnrepository.com/artifact/com.amazonaws/DynamoDBLocal From fade525cd9aadf540fdb2bfe09c4d6c0b1395d4a Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Tue, 23 Dec 2025 09:54:32 -0800 Subject: [PATCH 2/7] comment v1 deps --- .../runtimes/java/Migration/DDBECToAWSDBE/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/runtimes/java/Migration/DDBECToAWSDBE/build.gradle.kts b/Examples/runtimes/java/Migration/DDBECToAWSDBE/build.gradle.kts index 8a610557c..aefadf8a4 100644 --- a/Examples/runtimes/java/Migration/DDBECToAWSDBE/build.gradle.kts +++ b/Examples/runtimes/java/Migration/DDBECToAWSDBE/build.gradle.kts @@ -75,8 +75,8 @@ dependencies { implementation("software.amazon.awssdk:kms") // To support legacy configuration - implementation("com.amazonaws:aws-java-sdk-dynamodb:1.12.780") - implementation("com.amazonaws:aws-java-sdk-kms") + // implementation("com.amazonaws:aws-java-sdk-dynamodb:1.12.780") + // implementation("com.amazonaws:aws-java-sdk-kms") // https://mvnrepository.com/artifact/org.testng/testng testImplementation("org.testng:testng:7.5") From 4ee923b374f698690003b639141348f1d7117664 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Tue, 23 Dec 2025 09:56:09 -0800 Subject: [PATCH 3/7] revert later: make sdk v1 back to required deps --- DynamoDbEncryption/runtimes/java/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DynamoDbEncryption/runtimes/java/build.gradle.kts b/DynamoDbEncryption/runtimes/java/build.gradle.kts index 08c1273e4..2bbd34588 100644 --- a/DynamoDbEncryption/runtimes/java/build.gradle.kts +++ b/DynamoDbEncryption/runtimes/java/build.gradle.kts @@ -92,7 +92,7 @@ dependencies { testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.11.4") // For the DDB-EC v1 - compileOnly("com.amazonaws:aws-java-sdk-dynamodb:1.12.780") + implementation("com.amazonaws:aws-java-sdk-dynamodb:1.12.780") // https://mvnrepository.com/artifact/org.testng/testng testImplementation("org.testng:testng:7.5") // https://mvnrepository.com/artifact/com.amazonaws/DynamoDBLocal From 77b88a71778916cbc32bd19ea4b4553edfd82f26 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Tue, 23 Dec 2025 10:33:27 -0800 Subject: [PATCH 4/7] Revert "revert later: make sdk v1 back to required deps" This reverts commit 4ee923b374f698690003b639141348f1d7117664. --- DynamoDbEncryption/runtimes/java/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DynamoDbEncryption/runtimes/java/build.gradle.kts b/DynamoDbEncryption/runtimes/java/build.gradle.kts index 2bbd34588..08c1273e4 100644 --- a/DynamoDbEncryption/runtimes/java/build.gradle.kts +++ b/DynamoDbEncryption/runtimes/java/build.gradle.kts @@ -92,7 +92,7 @@ dependencies { testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.11.4") // For the DDB-EC v1 - implementation("com.amazonaws:aws-java-sdk-dynamodb:1.12.780") + compileOnly("com.amazonaws:aws-java-sdk-dynamodb:1.12.780") // https://mvnrepository.com/artifact/org.testng/testng testImplementation("org.testng:testng:7.5") // https://mvnrepository.com/artifact/com.amazonaws/DynamoDBLocal From abcf94bfffda0351dc5c37dc7fcfe4482902baab Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Tue, 23 Dec 2025 10:34:09 -0800 Subject: [PATCH 5/7] Revert "comment v1 deps" This reverts commit fade525cd9aadf540fdb2bfe09c4d6c0b1395d4a. --- .../runtimes/java/Migration/DDBECToAWSDBE/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/runtimes/java/Migration/DDBECToAWSDBE/build.gradle.kts b/Examples/runtimes/java/Migration/DDBECToAWSDBE/build.gradle.kts index aefadf8a4..8a610557c 100644 --- a/Examples/runtimes/java/Migration/DDBECToAWSDBE/build.gradle.kts +++ b/Examples/runtimes/java/Migration/DDBECToAWSDBE/build.gradle.kts @@ -75,8 +75,8 @@ dependencies { implementation("software.amazon.awssdk:kms") // To support legacy configuration - // implementation("com.amazonaws:aws-java-sdk-dynamodb:1.12.780") - // implementation("com.amazonaws:aws-java-sdk-kms") + implementation("com.amazonaws:aws-java-sdk-dynamodb:1.12.780") + implementation("com.amazonaws:aws-java-sdk-kms") // https://mvnrepository.com/artifact/org.testng/testng testImplementation("org.testng:testng:7.5") From 664f188cce04f955e5bc40a9ca1f808cb278ec64 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Tue, 23 Dec 2025 13:05:02 -0800 Subject: [PATCH 6/7] sdk v2 --- .../runtimes/java/build.gradle.kts | 2 +- .../encryption/DelegatedKey.java | 146 +++++ .../encryption/DynamoDbEncryptor.java | 595 ++++++++++++++++++ .../encryption/DynamoDbSigner.java | 261 ++++++++ .../encryption/EncryptionContext.java | 187 ++++++ .../encryption/EncryptionFlags.java | 23 + .../DynamoDbEncryptionException.java | 47 ++ .../materials/AbstractRawMaterials.java | 73 +++ .../materials/AsymmetricRawMaterials.java | 49 ++ .../materials/CryptographicMaterials.java | 24 + .../materials/DecryptionMaterials.java | 27 + .../materials/EncryptionMaterials.java | 27 + .../materials/SymmetricRawMaterials.java | 58 ++ .../materials/WrappedRawMaterials.java | 212 +++++++ .../providers/AsymmetricStaticProvider.java | 46 ++ .../providers/CachingMostRecentProvider.java | 183 ++++++ .../providers/DirectKmsMaterialsProvider.java | 296 +++++++++ .../EncryptionMaterialsProvider.java | 71 +++ .../providers/KeyStoreMaterialsProvider.java | 199 ++++++ .../providers/SymmetricStaticProvider.java | 130 ++++ .../providers/WrappedMaterialsProvider.java | 163 +++++ .../encryption/providers/store/MetaStore.java | 434 +++++++++++++ .../providers/store/ProviderStore.java | 84 +++ .../utils/EncryptionContextOperators.java | 81 +++ .../internal/AttributeValueMarshaller.java | 331 ++++++++++ .../internal/Base64.java | 48 ++ .../internal/ByteBufferInputStream.java | 56 ++ .../internal/Hkdf.java | 316 ++++++++++ .../internal/LRUCache.java | 107 ++++ .../internal/MsClock.java | 19 + .../internal/TTLCache.java | 242 +++++++ .../internal/Utils.java | 39 ++ 32 files changed, 4575 insertions(+), 1 deletion(-) create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DelegatedKey.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbEncryptor.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbSigner.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/EncryptionContext.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/EncryptionFlags.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/exceptions/DynamoDbEncryptionException.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/AbstractRawMaterials.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/AsymmetricRawMaterials.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/CryptographicMaterials.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/DecryptionMaterials.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/EncryptionMaterials.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/SymmetricRawMaterials.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/WrappedRawMaterials.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/AsymmetricStaticProvider.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/CachingMostRecentProvider.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/DirectKmsMaterialsProvider.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/EncryptionMaterialsProvider.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/KeyStoreMaterialsProvider.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/SymmetricStaticProvider.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/WrappedMaterialsProvider.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/store/MetaStore.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/store/ProviderStore.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/utils/EncryptionContextOperators.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/AttributeValueMarshaller.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/Base64.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/ByteBufferInputStream.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/Hkdf.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/LRUCache.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/MsClock.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/TTLCache.java create mode 100644 DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/Utils.java diff --git a/DynamoDbEncryption/runtimes/java/build.gradle.kts b/DynamoDbEncryption/runtimes/java/build.gradle.kts index 08c1273e4..2bbd34588 100644 --- a/DynamoDbEncryption/runtimes/java/build.gradle.kts +++ b/DynamoDbEncryption/runtimes/java/build.gradle.kts @@ -92,7 +92,7 @@ dependencies { testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.11.4") // For the DDB-EC v1 - compileOnly("com.amazonaws:aws-java-sdk-dynamodb:1.12.780") + implementation("com.amazonaws:aws-java-sdk-dynamodb:1.12.780") // https://mvnrepository.com/artifact/org.testng/testng testImplementation("org.testng:testng:7.5") // https://mvnrepository.com/artifact/com.amazonaws/DynamoDBLocal diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DelegatedKey.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DelegatedKey.java new file mode 100644 index 000000000..52e02f2e8 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DelegatedKey.java @@ -0,0 +1,146 @@ +/* + * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption; + +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; + +/** + * Identifies keys which should not be used directly with {@link Cipher} but + * instead contain their own cryptographic logic. This can be used to wrap more + * complex logic, HSM integration, or service-calls. + * + *

+ * Most delegated keys will only support a subset of these operations. (For + * example, AES keys will generally not support {@link #sign(byte[], String)} or + * {@link #verify(byte[], byte[], String)} and HMAC keys will generally not + * support anything except sign and verify.) + * {@link UnsupportedOperationException} should be thrown in these cases. + * + * @author Greg Rubin + */ +public interface DelegatedKey extends SecretKey { + /** + * Encrypts the provided plaintext and returns a byte-array containing the ciphertext. + * + * @param plainText + * @param additionalAssociatedData + * Optional additional data which must then also be provided for successful + * decryption. Both null and arrays of length 0 are treated identically. + * Not all keys will support this parameter. + * @param algorithm + * the transformation to be used when encrypting the data + * @return ciphertext the ciphertext produced by this encryption operation + * @throws UnsupportedOperationException + * if encryption is not supported or if additionalAssociatedData is + * provided, but not supported. + */ + byte[] encrypt(byte[] plainText, byte[] additionalAssociatedData, String algorithm) + throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException, + NoSuchPaddingException; + + /** + * Decrypts the provided ciphertext and returns a byte-array containing the + * plaintext. + * + * @param cipherText + * @param additionalAssociatedData + * Optional additional data which was provided during encryption. + * Both null and arrays of length 0 are treated + * identically. Not all keys will support this parameter. + * @param algorithm + * the transformation to be used when decrypting the data + * @return plaintext the result of decrypting the input ciphertext + * @throws UnsupportedOperationException + * if decryption is not supported or if + * additionalAssociatedData is provided, but not + * supported. + */ + byte[] decrypt(byte[] cipherText, byte[] additionalAssociatedData, String algorithm) + throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException, + NoSuchPaddingException, InvalidAlgorithmParameterException; + + /** + * Wraps (encrypts) the provided key to make it safe for + * storage or transmission. + * + * @param key + * @param additionalAssociatedData + * Optional additional data which must then also be provided for + * successful unwrapping. Both null and arrays of + * length 0 are treated identically. Not all keys will support + * this parameter. + * @param algorithm + * the transformation to be used when wrapping the key + * @return the wrapped key + * @throws UnsupportedOperationException + * if wrapping is not supported or if + * additionalAssociatedData is provided, but not + * supported. + */ + byte[] wrap(Key key, byte[] additionalAssociatedData, String algorithm) throws InvalidKeyException, + NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException; + + /** + * Unwraps (decrypts) the provided wrappedKey to recover the + * original key. + * + * @param wrappedKey + * @param additionalAssociatedData + * Optional additional data which was provided during wrapping. + * Both null and arrays of length 0 are treated + * identically. Not all keys will support this parameter. + * @param algorithm + * the transformation to be used when unwrapping the key + * @return the unwrapped key + * @throws UnsupportedOperationException + * if wrapping is not supported or if + * additionalAssociatedData is provided, but not + * supported. + */ + Key unwrap(byte[] wrappedKey, String wrappedKeyAlgorithm, int wrappedKeyType, + byte[] additionalAssociatedData, String algorithm) throws NoSuchAlgorithmException, NoSuchPaddingException, + InvalidKeyException; + + /** + * Calculates and returns a signature for dataToSign. + * + * @param dataToSign + * @param algorithm + * @return the signature + * @throws UnsupportedOperationException if signing is not supported + */ + byte[] sign(byte[] dataToSign, String algorithm) throws GeneralSecurityException; + + /** + * Checks the provided signature for correctness. + * + * @param dataToSign + * @param signature + * @param algorithm + * @return true if and only if the signature matches the dataToSign. + * @throws UnsupportedOperationException if signature validation is not supported + */ + boolean verify(byte[] dataToSign, byte[] signature, String algorithm); +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbEncryptor.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbEncryptor.java new file mode 100644 index 000000000..95e6ec73c --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbEncryptor.java @@ -0,0 +1,595 @@ +/* + * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.SignatureException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; + +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.exceptions.DynamoDbEncryptionException; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.DecryptionMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.EncryptionMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.EncryptionMaterialsProvider; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.utils.EncryptionContextOperators; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.internal.AttributeValueMarshaller; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.internal.ByteBufferInputStream; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.internal.Utils; + +/** + * The low-level API for performing crypto operations on the record attributes. + * + * @author Greg Rubin + */ +public class DynamoDbEncryptor { + private static final String DEFAULT_SIGNATURE_ALGORITHM = "SHA256withRSA"; + private static final String DEFAULT_METADATA_FIELD = "*amzn-ddb-map-desc*"; + private static final String DEFAULT_SIGNATURE_FIELD = "*amzn-ddb-map-sig*"; + private static final String DEFAULT_DESCRIPTION_BASE = "amzn-ddb-map-"; // Same as the Mapper + private static final Charset UTF8 = Charset.forName("UTF-8"); + private static final String SYMMETRIC_ENCRYPTION_MODE = "/CBC/PKCS5Padding"; + private static final ConcurrentHashMap BLOCK_SIZE_CACHE = new ConcurrentHashMap<>(); + private static final Function BLOCK_SIZE_CALCULATOR = (transformation) -> { + try { + final Cipher c = Cipher.getInstance(transformation); + return c.getBlockSize(); + } catch (final GeneralSecurityException ex) { + throw new IllegalArgumentException("Algorithm does not exist", ex); + } + }; + + private static final int CURRENT_VERSION = 0; + + private String signatureFieldName = DEFAULT_SIGNATURE_FIELD; + private String materialDescriptionFieldName = DEFAULT_METADATA_FIELD; + + private EncryptionMaterialsProvider encryptionMaterialsProvider; + private final String descriptionBase; + private final String symmetricEncryptionModeHeader; + private final String signingAlgorithmHeader; + + static final String DEFAULT_SIGNING_ALGORITHM_HEADER = DEFAULT_DESCRIPTION_BASE + "signingAlg"; + + private Function encryptionContextOverrideOperator; + + protected DynamoDbEncryptor(EncryptionMaterialsProvider provider, String descriptionBase) { + this.encryptionMaterialsProvider = provider; + this.descriptionBase = descriptionBase; + symmetricEncryptionModeHeader = this.descriptionBase + "sym-mode"; + signingAlgorithmHeader = this.descriptionBase + "signingAlg"; + } + + public static DynamoDbEncryptor getInstance( + EncryptionMaterialsProvider provider, String descriptionbase) { + return new DynamoDbEncryptor(provider, descriptionbase); + } + + public static DynamoDbEncryptor getInstance(EncryptionMaterialsProvider provider) { + return getInstance(provider, DEFAULT_DESCRIPTION_BASE); + } + + /** + * Returns a decrypted version of the provided DynamoDb record. The signature is verified across + * all provided fields. All fields (except those listed in doNotEncrypt are + * decrypted. + * + * @param itemAttributes the DynamoDbRecord + * @param context additional information used to successfully select the encryption materials and + * decrypt the data. This should include (at least) the tableName and the materialDescription. + * @param doNotDecrypt those fields which should not be encrypted + * @return a plaintext version of the DynamoDb record + * @throws SignatureException if the signature is invalid or cannot be verified + * @throws GeneralSecurityException + */ + public Map decryptAllFieldsExcept( + Map itemAttributes, EncryptionContext context, String... doNotDecrypt) + throws GeneralSecurityException { + return decryptAllFieldsExcept(itemAttributes, context, Arrays.asList(doNotDecrypt)); + } + + /** @see #decryptAllFieldsExcept(Map, EncryptionContext, String...) */ + public Map decryptAllFieldsExcept( + Map itemAttributes, + EncryptionContext context, + Collection doNotDecrypt) + throws GeneralSecurityException { + Map> attributeFlags = + allDecryptionFlagsExcept(itemAttributes, doNotDecrypt); + return decryptRecord(itemAttributes, attributeFlags, context); + } + + /** + * Returns the decryption flags for all item attributes except for those explicitly specified to + * be excluded. + * + * @param doNotDecrypt fields to be excluded + */ + public Map> allDecryptionFlagsExcept( + Map itemAttributes, String... doNotDecrypt) { + return allDecryptionFlagsExcept(itemAttributes, Arrays.asList(doNotDecrypt)); + } + + /** + * Returns the decryption flags for all item attributes except for those explicitly specified to + * be excluded. + * + * @param doNotDecrypt fields to be excluded + */ + public Map> allDecryptionFlagsExcept( + Map itemAttributes, Collection doNotDecrypt) { + Map> attributeFlags = new HashMap>(); + + for (String fieldName : doNotDecrypt) { + attributeFlags.put(fieldName, EnumSet.of(EncryptionFlags.SIGN)); + } + + for (String fieldName : itemAttributes.keySet()) { + if (!attributeFlags.containsKey(fieldName) + && !fieldName.equals(getMaterialDescriptionFieldName()) + && !fieldName.equals(getSignatureFieldName())) { + attributeFlags.put(fieldName, EnumSet.of(EncryptionFlags.ENCRYPT, EncryptionFlags.SIGN)); + } + } + return attributeFlags; + } + + /** + * Returns an encrypted version of the provided DynamoDb record. All fields are signed. All fields + * (except those listed in doNotEncrypt) are encrypted. + * + * @param itemAttributes a DynamoDb Record + * @param context additional information used to successfully select the encryption materials and + * encrypt the data. This should include (at least) the tableName. + * @param doNotEncrypt those fields which should not be encrypted + * @return a ciphertext version of the DynamoDb record + * @throws GeneralSecurityException + */ + public Map encryptAllFieldsExcept( + Map itemAttributes, EncryptionContext context, String... doNotEncrypt) + throws GeneralSecurityException { + + return encryptAllFieldsExcept(itemAttributes, context, Arrays.asList(doNotEncrypt)); + } + + public Map encryptAllFieldsExcept( + Map itemAttributes, + EncryptionContext context, + Collection doNotEncrypt) + throws GeneralSecurityException { + Map> attributeFlags = + allEncryptionFlagsExcept(itemAttributes, doNotEncrypt); + return encryptRecord(itemAttributes, attributeFlags, context); + } + + /** + * Returns the encryption flags for all item attributes except for those explicitly specified to + * be excluded. + * + * @param doNotEncrypt fields to be excluded + */ + public Map> allEncryptionFlagsExcept( + Map itemAttributes, String... doNotEncrypt) { + return allEncryptionFlagsExcept(itemAttributes, Arrays.asList(doNotEncrypt)); + } + + /** + * Returns the encryption flags for all item attributes except for those explicitly specified to + * be excluded. + * + * @param doNotEncrypt fields to be excluded + */ + public Map> allEncryptionFlagsExcept( + Map itemAttributes, Collection doNotEncrypt) { + Map> attributeFlags = new HashMap>(); + for (String fieldName : doNotEncrypt) { + attributeFlags.put(fieldName, EnumSet.of(EncryptionFlags.SIGN)); + } + + for (String fieldName : itemAttributes.keySet()) { + if (!attributeFlags.containsKey(fieldName)) { + attributeFlags.put(fieldName, EnumSet.of(EncryptionFlags.ENCRYPT, EncryptionFlags.SIGN)); + } + } + return attributeFlags; + } + + public Map decryptRecord( + Map itemAttributes, + Map> attributeFlags, + EncryptionContext context) + throws GeneralSecurityException { + if (!itemContainsFieldsToDecryptOrSign(itemAttributes.keySet(), attributeFlags)) { + return itemAttributes; + } + // Copy to avoid changing anyone elses objects + itemAttributes = new HashMap(itemAttributes); + + Map materialDescription = Collections.emptyMap(); + DecryptionMaterials materials; + SecretKey decryptionKey; + + DynamoDbSigner signer = DynamoDbSigner.getInstance(DEFAULT_SIGNATURE_ALGORITHM, Utils.getRng()); + + if (itemAttributes.containsKey(materialDescriptionFieldName)) { + materialDescription = unmarshallDescription(itemAttributes.get(materialDescriptionFieldName)); + } + // Copy the material description and attribute values into the context + context = + new EncryptionContext.Builder(context) + .materialDescription(materialDescription) + .attributeValues(itemAttributes) + .build(); + + Function encryptionContextOverrideOperator = + getEncryptionContextOverrideOperator(); + if (encryptionContextOverrideOperator != null) { + context = encryptionContextOverrideOperator.apply(context); + } + + materials = encryptionMaterialsProvider.getDecryptionMaterials(context); + decryptionKey = materials.getDecryptionKey(); + if (materialDescription.containsKey(signingAlgorithmHeader)) { + String signingAlg = materialDescription.get(signingAlgorithmHeader); + signer = DynamoDbSigner.getInstance(signingAlg, Utils.getRng()); + } + + ByteBuffer signature; + if (!itemAttributes.containsKey(signatureFieldName) + || itemAttributes.get(signatureFieldName).b() == null) { + signature = ByteBuffer.allocate(0); + } else { + signature = itemAttributes.get(signatureFieldName).b().asByteBuffer().asReadOnlyBuffer(); + } + itemAttributes.remove(signatureFieldName); + + String associatedData = "TABLE>" + context.getTableName() + " attributeNamesToCheck, Map> attributeFlags) { + return attributeNamesToCheck.stream() + .filter(attributeFlags::containsKey) + .anyMatch(attributeName -> !attributeFlags.get(attributeName).isEmpty()); + } + + public Map encryptRecord( + Map itemAttributes, + Map> attributeFlags, + EncryptionContext context) { + if (attributeFlags.isEmpty()) { + return itemAttributes; + } + // Copy to avoid changing anyone elses objects + itemAttributes = new HashMap<>(itemAttributes); + + // Copy the attribute values into the context + context = context.toBuilder() + .attributeValues(itemAttributes) + .build(); + + Function encryptionContextOverrideOperator = + getEncryptionContextOverrideOperator(); + if (encryptionContextOverrideOperator != null) { + context = encryptionContextOverrideOperator.apply(context); + } + + EncryptionMaterials materials = encryptionMaterialsProvider.getEncryptionMaterials(context); + // We need to copy this because we modify it to record other encryption details + Map materialDescription = new HashMap<>( + materials.getMaterialDescription()); + SecretKey encryptionKey = materials.getEncryptionKey(); + + try { + actualEncryption(itemAttributes, attributeFlags, materialDescription, encryptionKey); + + // The description must be stored after encryption because its data + // is necessary for proper decryption. + final String signingAlgo = materialDescription.get(signingAlgorithmHeader); + DynamoDbSigner signer; + if (signingAlgo != null) { + signer = DynamoDbSigner.getInstance(signingAlgo, Utils.getRng()); + } else { + signer = DynamoDbSigner.getInstance(DEFAULT_SIGNATURE_ALGORITHM, Utils.getRng()); + } + + if (materials.getSigningKey() instanceof PrivateKey) { + materialDescription.put(signingAlgorithmHeader, signer.getSigningAlgorithm()); + } + if (! materialDescription.isEmpty()) { + itemAttributes.put(materialDescriptionFieldName, marshallDescription(materialDescription)); + } + + String associatedData = "TABLE>" + context.getTableName() + " itemAttributes, + Map> attributeFlags, SecretKey encryptionKey, + Map materialDescription) throws GeneralSecurityException { + final String encryptionMode = encryptionKey != null ? encryptionKey.getAlgorithm() + + materialDescription.get(symmetricEncryptionModeHeader) : null; + Cipher cipher = null; + int blockSize = -1; + + for (Map.Entry entry: itemAttributes.entrySet()) { + Set flags = attributeFlags.get(entry.getKey()); + if (flags != null && flags.contains(EncryptionFlags.ENCRYPT)) { + if (!flags.contains(EncryptionFlags.SIGN)) { + throw new IllegalArgumentException("All encrypted fields must be signed. Bad field: " + entry.getKey()); + } + ByteBuffer plainText; + ByteBuffer cipherText = entry.getValue().b().asByteBuffer(); + cipherText.rewind(); + if (encryptionKey instanceof DelegatedKey) { + plainText = ByteBuffer.wrap(((DelegatedKey)encryptionKey).decrypt(toByteArray(cipherText), null, encryptionMode)); + } else { + if (cipher == null) { + blockSize = getBlockSize(encryptionMode); + cipher = Cipher.getInstance(encryptionMode); + } + byte[] iv = new byte[blockSize]; + cipherText.get(iv); + cipher.init(Cipher.DECRYPT_MODE, encryptionKey, new IvParameterSpec(iv), Utils.getRng()); + plainText = ByteBuffer.allocate(cipher.getOutputSize(cipherText.remaining())); + cipher.doFinal(cipherText, plainText); + plainText.rewind(); + } + entry.setValue(AttributeValueMarshaller.unmarshall(plainText)); + } + } + } + + private static int getBlockSize(final String encryptionMode) { + return BLOCK_SIZE_CACHE.computeIfAbsent(encryptionMode, BLOCK_SIZE_CALCULATOR); + } + + /** + * This method has the side effect of replacing the plaintext + * attribute-values of "itemAttributes" with ciphertext attribute-values + * (which are always in the form of ByteBuffer) as per the corresponding + * attribute flags. + */ + private void actualEncryption(Map itemAttributes, + Map> attributeFlags, + Map materialDescription, + SecretKey encryptionKey) throws GeneralSecurityException { + String encryptionMode = null; + if (encryptionKey != null) { + materialDescription.put(this.symmetricEncryptionModeHeader, + SYMMETRIC_ENCRYPTION_MODE); + encryptionMode = encryptionKey.getAlgorithm() + SYMMETRIC_ENCRYPTION_MODE; + } + Cipher cipher = null; + int blockSize = -1; + + for (Map.Entry entry: itemAttributes.entrySet()) { + Set flags = attributeFlags.get(entry.getKey()); + if (flags != null && flags.contains(EncryptionFlags.ENCRYPT)) { + if (!flags.contains(EncryptionFlags.SIGN)) { + throw new IllegalArgumentException("All encrypted fields must be signed. Bad field: " + entry.getKey()); + } + ByteBuffer plainText = AttributeValueMarshaller.marshall(entry.getValue()); + plainText.rewind(); + ByteBuffer cipherText; + if (encryptionKey instanceof DelegatedKey) { + DelegatedKey dk = (DelegatedKey) encryptionKey; + cipherText = ByteBuffer.wrap( + dk.encrypt(toByteArray(plainText), null, encryptionMode)); + } else { + if (cipher == null) { + blockSize = getBlockSize(encryptionMode); + cipher = Cipher.getInstance(encryptionMode); + } + // Encryption format: + // Note a unique iv is generated per attribute + cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, Utils.getRng()); + cipherText = ByteBuffer.allocate(blockSize + cipher.getOutputSize(plainText.remaining())); + cipherText.position(blockSize); + cipher.doFinal(plainText, cipherText); + cipherText.flip(); + final byte[] iv = cipher.getIV(); + if (iv.length != blockSize) { + throw new IllegalStateException(String.format("Generated IV length (%d) not equal to block size (%d)", + iv.length, blockSize)); + } + cipherText.put(iv); + cipherText.rewind(); + } + // Replace the plaintext attribute value with the encrypted content + entry.setValue(AttributeValue.builder().b(SdkBytes.fromByteBuffer(cipherText)).build()); + } + } + } + + /** + * Get the name of the DynamoDB field used to store the signature. + * Defaults to {@link #DEFAULT_SIGNATURE_FIELD}. + * + * @return the name of the DynamoDB field used to store the signature + */ + String getSignatureFieldName() { + return signatureFieldName; + } + + /** + * Set the name of the DynamoDB field used to store the signature. + * + * @param signatureFieldName + */ + void setSignatureFieldName(final String signatureFieldName) { + this.signatureFieldName = signatureFieldName; + } + + /** + * Get the name of the DynamoDB field used to store metadata used by the + * DynamoDBEncryptedMapper. Defaults to {@link #DEFAULT_METADATA_FIELD}. + * + * @return the name of the DynamoDB field used to store metadata used by the + * DynamoDBEncryptedMapper + */ + String getMaterialDescriptionFieldName() { + return materialDescriptionFieldName; + } + + /** + * Set the name of the DynamoDB field used to store metadata used by the + * DynamoDBEncryptedMapper + * + * @param materialDescriptionFieldName + */ + void setMaterialDescriptionFieldName(final String materialDescriptionFieldName) { + this.materialDescriptionFieldName = materialDescriptionFieldName; + } + + /** + * Marshalls the description into a ByteBuffer by outputting + * each key (modified UTF-8) followed by its value (also in modified UTF-8). + * + * @param description + * @return the description encoded as an AttributeValue with a ByteBuffer value + * @see java.io.DataOutput#writeUTF(String) + */ + private static AttributeValue marshallDescription(Map description) { + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream out = new DataOutputStream(bos); + out.writeInt(CURRENT_VERSION); + for (Map.Entry entry : description.entrySet()) { + byte[] bytes = entry.getKey().getBytes(UTF8); + out.writeInt(bytes.length); + out.write(bytes); + bytes = entry.getValue().getBytes(UTF8); + out.writeInt(bytes.length); + out.write(bytes); + } + out.close(); + return AttributeValue.builder().b(SdkBytes.fromByteArray(bos.toByteArray())).build(); + } catch (IOException ex) { + // Due to the objects in use, an IOException is not possible. + throw new RuntimeException("Unexpected exception", ex); + } + } + + /** + * @see #marshallDescription(Map) + */ + private static Map unmarshallDescription(AttributeValue attributeValue) { + try (DataInputStream in = new DataInputStream( + new ByteBufferInputStream(attributeValue.b().asByteBuffer())) ) { + Map result = new HashMap<>(); + int version = in.readInt(); + if (version != CURRENT_VERSION) { + throw new IllegalArgumentException("Unsupported description version"); + } + + String key, value; + int keyLength, valueLength; + try { + while(in.available() > 0) { + keyLength = in.readInt(); + byte[] bytes = new byte[keyLength]; + if (in.read(bytes) != keyLength) { + throw new IllegalArgumentException("Malformed description"); + } + key = new String(bytes, UTF8); + valueLength = in.readInt(); + bytes = new byte[valueLength]; + if (in.read(bytes) != valueLength) { + throw new IllegalArgumentException("Malformed description"); + } + value = new String(bytes, UTF8); + result.put(key, value); + } + } catch (EOFException eof) { + throw new IllegalArgumentException("Malformed description", eof); + } + return result; + } catch (IOException ex) { + // Due to the objects in use, an IOException is not possible. + throw new RuntimeException("Unexpected exception", ex); + } + } + + /** + * @param encryptionContextOverrideOperator the nullable operator which will be used to override + * the EncryptionContext. + * @see EncryptionContextOperators + */ + void setEncryptionContextOverrideOperator( + Function encryptionContextOverrideOperator) { + this.encryptionContextOverrideOperator = encryptionContextOverrideOperator; + } + + /** + * @return the operator used to override the EncryptionContext + * @see #setEncryptionContextOverrideOperator(Function) + */ + private Function getEncryptionContextOverrideOperator() { + return encryptionContextOverrideOperator; + } + + private static byte[] toByteArray(ByteBuffer buffer) { + buffer = buffer.duplicate(); + // We can only return the array directly if: + // 1. The ByteBuffer exposes an array + // 2. The ByteBuffer starts at the beginning of the array + // 3. The ByteBuffer uses the entire array + if (buffer.hasArray() && buffer.arrayOffset() == 0) { + byte[] result = buffer.array(); + if (buffer.remaining() == result.length) { + return result; + } + } + + byte[] result = new byte[buffer.remaining()]; + buffer.get(result); + return result; + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbSigner.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbSigner.java new file mode 100644 index 000000000..d2998057b --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbSigner.java @@ -0,0 +1,261 @@ +/* + * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.SignatureException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import software.amazon.cryptools.dynamodbencryptionclientsdk2.internal.AttributeValueMarshaller; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.internal.Utils; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * @author Greg Rubin + */ +// NOTE: This class must remain thread-safe. +class DynamoDbSigner { + private static final ConcurrentHashMap cache = + new ConcurrentHashMap(); + + protected static final Charset UTF8 = Charset.forName("UTF-8"); + private final SecureRandom rnd; + private final SecretKey hmacComparisonKey; + private final String signingAlgorithm; + + /** + * @param signingAlgorithm is the algorithm used for asymmetric signing (ex: SHA256withRSA). This + * is ignored for symmetric HMACs as that algorithm is fully specified by the key. + */ + static DynamoDbSigner getInstance(String signingAlgorithm, SecureRandom rnd) { + DynamoDbSigner result = cache.get(signingAlgorithm); + if (result == null) { + result = new DynamoDbSigner(signingAlgorithm, rnd); + cache.putIfAbsent(signingAlgorithm, result); + } + return result; + } + + /** + * @param signingAlgorithm is the algorithm used for asymmetric signing (ex: SHA256withRSA). This + * is ignored for symmetric HMACs as that algorithm is fully specified by the key. + */ + private DynamoDbSigner(String signingAlgorithm, SecureRandom rnd) { + if (rnd == null) { + rnd = Utils.getRng(); + } + this.rnd = rnd; + this.signingAlgorithm = signingAlgorithm; + // Shorter than the output of SHA256 to avoid weak keys. + // http://cs.nyu.edu/~dodis/ps/h-of-h.pdf + // http://link.springer.com/chapter/10.1007%2F978-3-642-32009-5_21 + byte[] tmpKey = new byte[31]; + rnd.nextBytes(tmpKey); + hmacComparisonKey = new SecretKeySpec(tmpKey, "HmacSHA256"); + } + + void verifySignature( + Map itemAttributes, + Map> attributeFlags, + byte[] associatedData, + Key verificationKey, + ByteBuffer signature) + throws GeneralSecurityException { + if (verificationKey instanceof DelegatedKey) { + DelegatedKey dKey = (DelegatedKey) verificationKey; + byte[] stringToSign = calculateStringToSign(itemAttributes, attributeFlags, associatedData); + if (!dKey.verify(stringToSign, toByteArray(signature), dKey.getAlgorithm())) { + throw new SignatureException("Bad signature"); + } + } else if (verificationKey instanceof SecretKey) { + byte[] calculatedSig = + calculateSignature( + itemAttributes, attributeFlags, associatedData, (SecretKey) verificationKey); + if (!safeEquals(signature, calculatedSig)) { + throw new SignatureException("Bad signature"); + } + } else if (verificationKey instanceof PublicKey) { + PublicKey integrityKey = (PublicKey) verificationKey; + byte[] stringToSign = calculateStringToSign(itemAttributes, attributeFlags, associatedData); + Signature sig = Signature.getInstance(getSigningAlgorithm()); + sig.initVerify(integrityKey); + sig.update(stringToSign); + if (!sig.verify(toByteArray(signature))) { + throw new SignatureException("Bad signature"); + } + } else { + throw new IllegalArgumentException("No integrity key provided"); + } + } + + static byte[] calculateStringToSign( + Map itemAttributes, + Map> attributeFlags, + byte[] associatedData) + throws NoSuchAlgorithmException { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + List attrNames = new ArrayList(itemAttributes.keySet()); + Collections.sort(attrNames); + MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); + if (associatedData != null) { + out.write(sha256.digest(associatedData)); + } else { + out.write(sha256.digest()); + } + sha256.reset(); + + for (String name : attrNames) { + Set set = attributeFlags.get(name); + if (set != null && set.contains(EncryptionFlags.SIGN)) { + AttributeValue tmp = itemAttributes.get(name); + out.write(sha256.digest(name.getBytes(UTF8))); + sha256.reset(); + if (set.contains(EncryptionFlags.ENCRYPT)) { + sha256.update("ENCRYPTED".getBytes(UTF8)); + } else { + sha256.update("PLAINTEXT".getBytes(UTF8)); + } + out.write(sha256.digest()); + + sha256.reset(); + + sha256.update(AttributeValueMarshaller.marshall(tmp)); + out.write(sha256.digest()); + sha256.reset(); + } + } + return out.toByteArray(); + } catch (IOException ex) { + // Due to the objects in use, an IOException is not possible. + throw new RuntimeException("Unexpected exception", ex); + } + } + + /** The itemAttributes have already been encrypted, if necessary, before the signing. */ + byte[] calculateSignature( + Map itemAttributes, + Map> attributeFlags, + byte[] associatedData, + Key key) + throws GeneralSecurityException { + if (key instanceof DelegatedKey) { + return calculateSignature(itemAttributes, attributeFlags, associatedData, (DelegatedKey) key); + } else if (key instanceof SecretKey) { + return calculateSignature(itemAttributes, attributeFlags, associatedData, (SecretKey) key); + } else if (key instanceof PrivateKey) { + return calculateSignature(itemAttributes, attributeFlags, associatedData, (PrivateKey) key); + } else { + throw new IllegalArgumentException("No integrity key provided"); + } + } + + byte[] calculateSignature( + Map itemAttributes, + Map> attributeFlags, + byte[] associatedData, + DelegatedKey key) + throws GeneralSecurityException { + byte[] stringToSign = calculateStringToSign(itemAttributes, attributeFlags, associatedData); + return key.sign(stringToSign, key.getAlgorithm()); + } + + byte[] calculateSignature( + Map itemAttributes, + Map> attributeFlags, + byte[] associatedData, + SecretKey key) + throws GeneralSecurityException { + if (key instanceof DelegatedKey) { + return calculateSignature(itemAttributes, attributeFlags, associatedData, (DelegatedKey) key); + } + byte[] stringToSign = calculateStringToSign(itemAttributes, attributeFlags, associatedData); + Mac hmac = Mac.getInstance(key.getAlgorithm()); + hmac.init(key); + hmac.update(stringToSign); + return hmac.doFinal(); + } + + byte[] calculateSignature( + Map itemAttributes, + Map> attributeFlags, + byte[] associatedData, + PrivateKey key) + throws GeneralSecurityException { + byte[] stringToSign = calculateStringToSign(itemAttributes, attributeFlags, associatedData); + Signature sig = Signature.getInstance(signingAlgorithm); + sig.initSign(key, rnd); + sig.update(stringToSign); + return sig.sign(); + } + + String getSigningAlgorithm() { + return signingAlgorithm; + } + + /** Constant-time equality check. */ + private boolean safeEquals(ByteBuffer signature, byte[] calculatedSig) { + try { + signature.rewind(); + Mac hmac = Mac.getInstance(hmacComparisonKey.getAlgorithm()); + hmac.init(hmacComparisonKey); + hmac.update(signature); + byte[] signatureHash = hmac.doFinal(); + + hmac.reset(); + hmac.update(calculatedSig); + byte[] calculatedHash = hmac.doFinal(); + + return MessageDigest.isEqual(signatureHash, calculatedHash); + } catch (GeneralSecurityException ex) { + // We've hardcoded these algorithms, so the error should not be possible. + throw new RuntimeException("Unexpected exception", ex); + } + } + + private static byte[] toByteArray(ByteBuffer buffer) { + if (buffer.hasArray()) { + byte[] result = buffer.array(); + buffer.rewind(); + return result; + } else { + byte[] result = new byte[buffer.remaining()]; + buffer.get(result); + buffer.rewind(); + return result; + } + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/EncryptionContext.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/EncryptionContext.java new file mode 100644 index 000000000..9a78ad9b0 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/EncryptionContext.java @@ -0,0 +1,187 @@ +/* + * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.DecryptionMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.EncryptionMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.EncryptionMaterialsProvider; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * This class serves to provide additional useful data to + * {@link EncryptionMaterialsProvider}s so they can more intelligently select + * the proper {@link EncryptionMaterials} or {@link DecryptionMaterials} for + * use. Any of the methods are permitted to return null. + *

+ * For the simplest cases, all a developer needs to provide in the context are: + *

    + *
  • TableName
  • + *
  • HashKeyName
  • + *
  • RangeKeyName (if present)
  • + *
+ * + * This class is immutable. + * + * @author Greg Rubin + */ +public final class EncryptionContext { + private final String tableName; + private final Map attributeValues; + private final Object developerContext; + private final String hashKeyName; + private final String rangeKeyName; + private final Map materialDescription; + + /** + * Return a new builder that can be used to construct an {@link EncryptionContext} + * @return A newly initialized {@link EncryptionContext.Builder}. + */ + public static Builder builder() { + return new Builder(); + } + + private EncryptionContext(Builder builder) { + tableName = builder.tableName; + attributeValues = builder.attributeValues; + developerContext = builder.developerContext; + hashKeyName = builder.hashKeyName; + rangeKeyName = builder.rangeKeyName; + materialDescription = builder.materialDescription; + } + + /** + * Returns the name of the DynamoDB Table this record is associated with. + */ + public String getTableName() { + return tableName; + } + + /** + * Returns the DynamoDB record about to be encrypted/decrypted. + */ + public Map getAttributeValues() { + return attributeValues; + } + + /** + * This object has no meaning (and will not be set or examined) by any core libraries. + * It exists to allow custom object mappers and data access layers to pass + * data to {@link EncryptionMaterialsProvider}s through the {@link DynamoDbEncryptor}. + */ + public Object getDeveloperContext() { + return developerContext; + } + + /** + * Returns the name of the HashKey attribute for the record to be encrypted/decrypted. + */ + public String getHashKeyName() { + return hashKeyName; + } + + /** + * Returns the name of the RangeKey attribute for the record to be encrypted/decrypted. + */ + public String getRangeKeyName() { + return rangeKeyName; + } + + public Map getMaterialDescription() { + return materialDescription; + } + + /** + * Converts an existing {@link EncryptionContext} into a builder that can be used to mutate and make a new version. + * @return A new {@link EncryptionContext.Builder} with all the fields filled out to match the current object. + */ + public Builder toBuilder() { + return new Builder(this); + } + + /** + * Builder class for {@link EncryptionContext}. + * Mutable objects (other than developerContext) will undergo + * a defensive copy prior to being stored in the builder. + * + * This class is not thread-safe. + */ + public static final class Builder { + private String tableName = null; + private Map attributeValues = null; + private Object developerContext = null; + private String hashKeyName = null; + private String rangeKeyName = null; + private Map materialDescription = null; + + public Builder() { + } + + public Builder(EncryptionContext context) { + tableName = context.getTableName(); + attributeValues = context.getAttributeValues(); + hashKeyName = context.getHashKeyName(); + rangeKeyName = context.getRangeKeyName(); + developerContext = context.getDeveloperContext(); + materialDescription = context.getMaterialDescription(); + } + + public EncryptionContext build() { + return new EncryptionContext(this); + } + + public Builder tableName(String tableName) { + this.tableName = tableName; + return this; + } + + public Builder attributeValues(Map attributeValues) { + this.attributeValues = Collections.unmodifiableMap(new HashMap<>(attributeValues)); + return this; + } + + public Builder developerContext(Object developerContext) { + this.developerContext = developerContext; + return this; + } + + public Builder hashKeyName(String hashKeyName) { + this.hashKeyName = hashKeyName; + return this; + } + + public Builder rangeKeyName(String rangeKeyName) { + this.rangeKeyName = rangeKeyName; + return this; + } + + public Builder materialDescription(Map materialDescription) { + this.materialDescription = Collections.unmodifiableMap(new HashMap<>(materialDescription)); + return this; + } + } + + @Override + public String toString() { + return "EncryptionContext [tableName=" + tableName + ", attributeValues=" + attributeValues + + ", developerContext=" + developerContext + + ", hashKeyName=" + hashKeyName + ", rangeKeyName=" + rangeKeyName + + ", materialDescription=" + materialDescription + "]"; + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/EncryptionFlags.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/EncryptionFlags.java new file mode 100644 index 000000000..47329f712 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/EncryptionFlags.java @@ -0,0 +1,23 @@ +/* + * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption; + +/** + * @author Greg Rubin + */ +public enum EncryptionFlags { + ENCRYPT, + SIGN +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/exceptions/DynamoDbEncryptionException.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/exceptions/DynamoDbEncryptionException.java new file mode 100644 index 000000000..f245d66e3 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/exceptions/DynamoDbEncryptionException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.exceptions; + +/** + * Generic exception thrown for any problem the DynamoDB encryption client has performing tasks + */ +public class DynamoDbEncryptionException extends RuntimeException { + private static final long serialVersionUID = - 7565904179772520868L; + + /** + * Standard constructor + * @param cause exception cause + */ + public DynamoDbEncryptionException(Throwable cause) { + super(cause); + } + + /** + * Standard constructor + * @param message exception message + */ + public DynamoDbEncryptionException(String message) { + super(message); + } + + /** + * Standard constructor + * @param message exception message + * @param cause exception cause + */ + public DynamoDbEncryptionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/AbstractRawMaterials.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/AbstractRawMaterials.java new file mode 100644 index 000000000..5dfbb1970 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/AbstractRawMaterials.java @@ -0,0 +1,73 @@ +/* + * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials; + +import java.security.Key; +import java.security.KeyPair; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.crypto.SecretKey; + +/** + * @author Greg Rubin + */ +public abstract class AbstractRawMaterials implements DecryptionMaterials, EncryptionMaterials { + private Map description; + private final Key signingKey; + private final Key verificationKey; + + @SuppressWarnings("unchecked") + protected AbstractRawMaterials(KeyPair signingPair) { + this(signingPair, Collections.EMPTY_MAP); + } + + protected AbstractRawMaterials(KeyPair signingPair, Map description) { + this.signingKey = signingPair.getPrivate(); + this.verificationKey = signingPair.getPublic(); + setMaterialDescription(description); + } + + @SuppressWarnings("unchecked") + protected AbstractRawMaterials(SecretKey macKey) { + this(macKey, Collections.EMPTY_MAP); + } + + protected AbstractRawMaterials(SecretKey macKey, Map description) { + this.signingKey = macKey; + this.verificationKey = macKey; + this.description = Collections.unmodifiableMap(new HashMap<>(description)); + } + + @Override + public Map getMaterialDescription() { + return new HashMap<>(description); + } + + public void setMaterialDescription(Map description) { + this.description = Collections.unmodifiableMap(new HashMap<>(description)); + } + + @Override + public Key getSigningKey() { + return signingKey; + } + + @Override + public Key getVerificationKey() { + return verificationKey; + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/AsymmetricRawMaterials.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/AsymmetricRawMaterials.java new file mode 100644 index 000000000..003d0b60c --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/AsymmetricRawMaterials.java @@ -0,0 +1,49 @@ +/* + * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.util.Collections; +import java.util.Map; + +import javax.crypto.SecretKey; + +/** + * @author Greg Rubin + */ +public class AsymmetricRawMaterials extends WrappedRawMaterials { + @SuppressWarnings("unchecked") + public AsymmetricRawMaterials(KeyPair encryptionKey, KeyPair signingPair) + throws GeneralSecurityException { + this(encryptionKey, signingPair, Collections.EMPTY_MAP); + } + + public AsymmetricRawMaterials(KeyPair encryptionKey, KeyPair signingPair, Map description) + throws GeneralSecurityException { + super(encryptionKey.getPublic(), encryptionKey.getPrivate(), signingPair, description); + } + + @SuppressWarnings("unchecked") + public AsymmetricRawMaterials(KeyPair encryptionKey, SecretKey macKey) + throws GeneralSecurityException { + this(encryptionKey, macKey, Collections.EMPTY_MAP); + } + + public AsymmetricRawMaterials(KeyPair encryptionKey, SecretKey macKey, Map description) + throws GeneralSecurityException { + super(encryptionKey.getPublic(), encryptionKey.getPrivate(), macKey, description); + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/CryptographicMaterials.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/CryptographicMaterials.java new file mode 100644 index 000000000..033d331f5 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/CryptographicMaterials.java @@ -0,0 +1,24 @@ +/* + * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials; + +import java.util.Map; + +/** + * @author Greg Rubin + */ +public interface CryptographicMaterials { + Map getMaterialDescription(); +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/DecryptionMaterials.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/DecryptionMaterials.java new file mode 100644 index 000000000..00f8548bc --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/DecryptionMaterials.java @@ -0,0 +1,27 @@ +/* + * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials; + +import java.security.Key; + +import javax.crypto.SecretKey; + +/** + * @author Greg Rubin + */ +public interface DecryptionMaterials extends CryptographicMaterials { + SecretKey getDecryptionKey(); + Key getVerificationKey(); +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/EncryptionMaterials.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/EncryptionMaterials.java new file mode 100644 index 000000000..ecef9e9fc --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/EncryptionMaterials.java @@ -0,0 +1,27 @@ +/* + * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials; + +import java.security.Key; + +import javax.crypto.SecretKey; + +/** + * @author Greg Rubin + */ +public interface EncryptionMaterials extends CryptographicMaterials { + SecretKey getEncryptionKey(); + Key getSigningKey(); +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/SymmetricRawMaterials.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/SymmetricRawMaterials.java new file mode 100644 index 000000000..b3daab44b --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/SymmetricRawMaterials.java @@ -0,0 +1,58 @@ +/* + * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials; + +import java.security.KeyPair; +import java.util.Collections; +import java.util.Map; + +import javax.crypto.SecretKey; + +/** + * @author Greg Rubin + */ +public class SymmetricRawMaterials extends AbstractRawMaterials { + private final SecretKey cryptoKey; + + @SuppressWarnings("unchecked") + public SymmetricRawMaterials(SecretKey encryptionKey, KeyPair signingPair) { + this(encryptionKey, signingPair, Collections.EMPTY_MAP); + } + + public SymmetricRawMaterials(SecretKey encryptionKey, KeyPair signingPair, Map description) { + super(signingPair, description); + this.cryptoKey = encryptionKey; + } + + @SuppressWarnings("unchecked") + public SymmetricRawMaterials(SecretKey encryptionKey, SecretKey macKey) { + this(encryptionKey, macKey, Collections.EMPTY_MAP); + } + + public SymmetricRawMaterials(SecretKey encryptionKey, SecretKey macKey, Map description) { + super(macKey, description); + this.cryptoKey = encryptionKey; + } + + @Override + public SecretKey getEncryptionKey() { + return cryptoKey; + } + + @Override + public SecretKey getDecryptionKey() { + return cryptoKey; + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/WrappedRawMaterials.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/WrappedRawMaterials.java new file mode 100644 index 000000000..fd17521ca --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/WrappedRawMaterials.java @@ -0,0 +1,212 @@ +/* + * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials; + +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.Map; + +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; + +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.DelegatedKey; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.internal.Base64; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.internal.Utils; + +/** + * Represents cryptographic materials used to manage unique record-level keys. + * This class specifically implements Envelope Encryption where a unique content + * key is randomly generated each time this class is constructed which is then + * encrypted with the Wrapping Key and then persisted in the Description. If a + * wrapped key is present in the Description, then that content key is unwrapped + * and used to decrypt the actual data in the record. + * + * Other possibly implementations might use a Key-Derivation Function to derive + * a unique key per record. + * + * @author Greg Rubin + */ +public class WrappedRawMaterials extends AbstractRawMaterials { + /** + * The key-name in the Description which contains the algorithm use to wrap + * content key. Example values are "AESWrap", or + * "RSA/ECB/OAEPWithSHA-256AndMGF1Padding". + */ + public static final String KEY_WRAPPING_ALGORITHM = "amzn-ddb-wrap-alg"; + /** + * The key-name in the Description which contains the algorithm used by the + * content key. Example values are "AES", or "Blowfish". + */ + public static final String CONTENT_KEY_ALGORITHM = "amzn-ddb-env-alg"; + /** + * The key-name in the Description which which contains the wrapped content + * key. + */ + public static final String ENVELOPE_KEY = "amzn-ddb-env-key"; + + private static final String DEFAULT_ALGORITHM = "AES/256"; + + protected final Key wrappingKey; + protected final Key unwrappingKey; + private final SecretKey envelopeKey; + + public WrappedRawMaterials(Key wrappingKey, Key unwrappingKey, KeyPair signingPair) + throws GeneralSecurityException { + this(wrappingKey, unwrappingKey, signingPair, Collections.emptyMap()); + } + + public WrappedRawMaterials(Key wrappingKey, Key unwrappingKey, KeyPair signingPair, + Map description) throws GeneralSecurityException { + super(signingPair, description); + this.wrappingKey = wrappingKey; + this.unwrappingKey = unwrappingKey; + this.envelopeKey = initEnvelopeKey(); + } + + public WrappedRawMaterials(Key wrappingKey, Key unwrappingKey, SecretKey macKey) + throws GeneralSecurityException { + this(wrappingKey, unwrappingKey, macKey, Collections.emptyMap()); + } + + public WrappedRawMaterials(Key wrappingKey, Key unwrappingKey, SecretKey macKey, + Map description) throws GeneralSecurityException { + super(macKey, description); + this.wrappingKey = wrappingKey; + this.unwrappingKey = unwrappingKey; + this.envelopeKey = initEnvelopeKey(); + } + + @Override + public SecretKey getDecryptionKey() { + return envelopeKey; + } + + @Override + public SecretKey getEncryptionKey() { + return envelopeKey; + } + + /** + * Called by the constructors. If there is already a key associated with + * this record (usually signified by a value stored in the description in + * the key {@link #ENVELOPE_KEY}) it extracts it and returns it. Otherwise + * it generates a new key, stores a wrapped version in the Description, and + * returns the key to the caller. + * + * @return the content key (which is returned by both + * {@link #getDecryptionKey()} and {@link #getEncryptionKey()}. + * @throws GeneralSecurityException if there is a problem + */ + protected SecretKey initEnvelopeKey() throws GeneralSecurityException { + Map description = getMaterialDescription(); + if (description.containsKey(ENVELOPE_KEY)) { + if (unwrappingKey == null) { + throw new IllegalStateException("No private decryption key provided."); + } + byte[] encryptedKey = Base64.decode(description.get(ENVELOPE_KEY)); + String wrappingAlgorithm = unwrappingKey.getAlgorithm(); + if (description.containsKey(KEY_WRAPPING_ALGORITHM)) { + wrappingAlgorithm = description.get(KEY_WRAPPING_ALGORITHM); + } + return unwrapKey(description, encryptedKey, wrappingAlgorithm); + } else { + SecretKey key = description.containsKey(CONTENT_KEY_ALGORITHM) ? + generateContentKey(description.get(CONTENT_KEY_ALGORITHM)) : + generateContentKey(DEFAULT_ALGORITHM); + + String wrappingAlg = description.containsKey(KEY_WRAPPING_ALGORITHM) ? + description.get(KEY_WRAPPING_ALGORITHM) : + getTransformation(wrappingKey.getAlgorithm()); + byte[] encryptedKey = wrapKey(key, wrappingAlg); + description.put(ENVELOPE_KEY, Base64.encodeToString(encryptedKey)); + description.put(CONTENT_KEY_ALGORITHM, key.getAlgorithm()); + description.put(KEY_WRAPPING_ALGORITHM, wrappingAlg); + setMaterialDescription(description); + return key; + } + } + + public byte[] wrapKey(SecretKey key, String wrappingAlg) throws NoSuchAlgorithmException, NoSuchPaddingException, + InvalidKeyException, IllegalBlockSizeException { + if (wrappingKey instanceof DelegatedKey) { + return ((DelegatedKey)wrappingKey).wrap(key, null, wrappingAlg); + } else { + Cipher cipher = Cipher.getInstance(wrappingAlg); + cipher.init(Cipher.WRAP_MODE, wrappingKey, Utils.getRng()); + return cipher.wrap(key); + } + } + + protected SecretKey unwrapKey( + Map description, byte[] encryptedKey, String wrappingAlgorithm) + throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException { + if (unwrappingKey instanceof DelegatedKey) { + return (SecretKey) + ((DelegatedKey) unwrappingKey) + .unwrap( + encryptedKey, + description.get(CONTENT_KEY_ALGORITHM), + Cipher.SECRET_KEY, + null, + wrappingAlgorithm); + } else { + Cipher cipher = Cipher.getInstance(wrappingAlgorithm); + + // This can be of the form "AES/256" as well as "AES" e.g., + // but we want to set the SecretKey with just "AES" in either case + String[] algPieces = description.get(CONTENT_KEY_ALGORITHM).split("/", 2); + String contentKeyAlgorithm = algPieces[0]; + + cipher.init(Cipher.UNWRAP_MODE, unwrappingKey, Utils.getRng()); + return (SecretKey) cipher.unwrap(encryptedKey, contentKeyAlgorithm, Cipher.SECRET_KEY); + } + } + + protected SecretKey generateContentKey(final String algorithm) throws NoSuchAlgorithmException { + String[] pieces = algorithm.split("/", 2); + KeyGenerator kg = KeyGenerator.getInstance(pieces[0]); + int keyLen = 0; + if (pieces.length == 2) { + try { + keyLen = Integer.parseInt(pieces[1]); + } catch (NumberFormatException ignored) { + } + } + + if (keyLen > 0) { + kg.init(keyLen, Utils.getRng()); + } else { + kg.init(Utils.getRng()); + } + return kg.generateKey(); + } + + private static String getTransformation(final String algorithm) { + if (algorithm.equalsIgnoreCase("RSA")) { + return "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"; + } else if (algorithm.equalsIgnoreCase("AES")) { + return "AESWrap"; + } else { + return algorithm; + } + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/AsymmetricStaticProvider.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/AsymmetricStaticProvider.java new file mode 100644 index 000000000..b49e2b9a2 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/AsymmetricStaticProvider.java @@ -0,0 +1,46 @@ +/* + * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers; + +import java.security.KeyPair; +import java.util.Collections; +import java.util.Map; + +import javax.crypto.SecretKey; + +/** + * This is a thin wrapper around the {@link WrappedMaterialsProvider}, using + * the provided encryptionKey for wrapping and unwrapping the + * record key. Please see that class for detailed documentation. + * + * @author Greg Rubin + */ +public class AsymmetricStaticProvider extends WrappedMaterialsProvider { + public AsymmetricStaticProvider(KeyPair encryptionKey, KeyPair signingPair) { + this(encryptionKey, signingPair, Collections.emptyMap()); + } + + public AsymmetricStaticProvider(KeyPair encryptionKey, SecretKey macKey) { + this(encryptionKey, macKey, Collections.emptyMap()); + } + + public AsymmetricStaticProvider(KeyPair encryptionKey, KeyPair signingPair, Map description) { + super(encryptionKey.getPublic(), encryptionKey.getPrivate(), signingPair, description); + } + + public AsymmetricStaticProvider(KeyPair encryptionKey, SecretKey macKey, Map description) { + super(encryptionKey.getPublic(), encryptionKey.getPrivate(), macKey, description); + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/CachingMostRecentProvider.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/CachingMostRecentProvider.java new file mode 100644 index 000000000..653e754c2 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/CachingMostRecentProvider.java @@ -0,0 +1,183 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers; + +import io.netty.util.internal.ObjectUtil; + +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.EncryptionContext; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.DecryptionMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.EncryptionMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.store.ProviderStore; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.internal.TTLCache; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.internal.TTLCache.EntryLoader; +import java.util.concurrent.TimeUnit; + +/** + * This meta-Provider encrypts data with the most recent version of keying materials from a {@link + * ProviderStore} and decrypts using whichever version is appropriate. It also caches the results + * from the {@link ProviderStore} to avoid excessive load on the backing systems. + */ +public class CachingMostRecentProvider implements EncryptionMaterialsProvider { + private static final long INITIAL_VERSION = 0; + private static final String PROVIDER_CACHE_KEY_DELIM = "#"; + private static final int DEFAULT_CACHE_MAX_SIZE = 1000; + + private final long ttlInNanos; + private final ProviderStore keystore; + protected final String defaultMaterialName; + private final TTLCache providerCache; + private final TTLCache versionCache; + + private final EntryLoader versionLoader = + new EntryLoader() { + @Override + public Long load(String entryKey) { + return keystore.getMaxVersion(entryKey); + } + }; + private final EntryLoader providerLoader = + new EntryLoader() { + @Override + public EncryptionMaterialsProvider load(String entryKey) { + final String[] parts = entryKey.split(PROVIDER_CACHE_KEY_DELIM, 2); + if (parts.length != 2) { + throw new IllegalStateException("Invalid cache key for provider cache: " + entryKey); + } + return keystore.getProvider(parts[0], Long.parseLong(parts[1])); + } + }; + + /** + * Creates a new {@link CachingMostRecentProvider}. + * + * @param keystore The key store that this provider will use to determine which material and which + * version of material to use + * @param materialName The name of the materials associated with this provider + * @param ttlInMillis The length of time in milliseconds to cache the most recent provider + */ + public CachingMostRecentProvider( + final ProviderStore keystore, final String materialName, final long ttlInMillis) { + this(keystore, materialName, ttlInMillis, DEFAULT_CACHE_MAX_SIZE); + } + + /** + * Creates a new {@link CachingMostRecentProvider}. + * + * @param keystore The key store that this provider will use to determine which material and which + * version of material to use + * @param materialName The name of the materials associated with this provider + * @param ttlInMillis The length of time in milliseconds to cache the most recent provider + * @param maxCacheSize The maximum size of the underlying caches this provider uses. Entries will + * be evicted from the cache once this size is exceeded. + */ + public CachingMostRecentProvider( + final ProviderStore keystore, + final String materialName, + final long ttlInMillis, + final int maxCacheSize) { + this.keystore = ObjectUtil.checkNotNull(keystore, "keystore must not be null"); + this.defaultMaterialName = materialName; + this.ttlInNanos = TimeUnit.MILLISECONDS.toNanos(ttlInMillis); + + this.providerCache = new TTLCache<>(maxCacheSize, ttlInMillis, providerLoader); + this.versionCache = new TTLCache<>(maxCacheSize, ttlInMillis, versionLoader); + } + + @Override + public DecryptionMaterials getDecryptionMaterials(EncryptionContext context) { + final long version = + keystore.getVersionFromMaterialDescription(context.getMaterialDescription()); + final String materialName = getMaterialName(context); + final String cacheKey = buildCacheKey(materialName, version); + + EncryptionMaterialsProvider provider = providerCache.load(cacheKey); + return provider.getDecryptionMaterials(context); + } + + + + @Override + public EncryptionMaterials getEncryptionMaterials(EncryptionContext context) { + final String materialName = getMaterialName(context); + final long currentVersion = versionCache.load(materialName); + + if (currentVersion < 0) { + // The material hasn't been created yet, so specify a loading function + // to create the first version of materials and update both caches. + // We want this to be done as part of the cache load to ensure that this logic + // only happens once in a multithreaded environment, + // in order to limit calls to the keystore's dependencies. + final String cacheKey = buildCacheKey(materialName, INITIAL_VERSION); + EncryptionMaterialsProvider newProvider = + providerCache.load( + cacheKey, + s -> { + // Create the new material in the keystore + final String[] parts = s.split(PROVIDER_CACHE_KEY_DELIM, 2); + if (parts.length != 2) { + throw new IllegalStateException("Invalid cache key for provider cache: " + s); + } + EncryptionMaterialsProvider provider = + keystore.getOrCreate(parts[0], Long.parseLong(parts[1])); + + // We now should have version 0 in our keystore. + // Update the version cache for this material as a side effect + versionCache.put(materialName, INITIAL_VERSION); + + // Return the new materials to be put into the cache + return provider; + }); + + return newProvider.getEncryptionMaterials(context); + } else { + final String cacheKey = buildCacheKey(materialName, currentVersion); + return providerCache.load(cacheKey).getEncryptionMaterials(context); + } + } + + @Override + public void refresh() { + versionCache.clear(); + providerCache.clear(); + } + + public String getMaterialName() { + return defaultMaterialName; + } + + public long getTtlInMills() { + return TimeUnit.NANOSECONDS.toMillis(ttlInNanos); + } + + /** + * The current version of the materials being used for encryption. Returns -1 if we do not + * currently have a current version. + */ + public long getCurrentVersion() { + return versionCache.load(getMaterialName()); + } + + /** + * The last time the current version was updated. Returns 0 if we do not currently have a current + * version. + */ + public long getLastUpdated() { + // We cache a version of -1 to mean that there is not a current version + if (versionCache.load(getMaterialName()) < 0) { + return 0; + } + // Otherwise, return the last update time of that entry + return TimeUnit.NANOSECONDS.toMillis(versionCache.getLastUpdated(getMaterialName())); + } + + protected String getMaterialName(final EncryptionContext context) { + return defaultMaterialName; + } + + private static String buildCacheKey(final String materialName, final long version) { + StringBuilder result = new StringBuilder(materialName); + result.append(PROVIDER_CACHE_KEY_DELIM); + result.append(version); + return result.toString(); + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/DirectKmsMaterialsProvider.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/DirectKmsMaterialsProvider.java new file mode 100644 index 000000000..425a4119f --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/DirectKmsMaterialsProvider.java @@ -0,0 +1,296 @@ +/* + * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers; + +import static software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.WrappedRawMaterials.CONTENT_KEY_ALGORITHM; +import static software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.WrappedRawMaterials.ENVELOPE_KEY; +import static software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.WrappedRawMaterials.KEY_WRAPPING_ALGORITHM; + +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.EncryptionContext; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.exceptions.DynamoDbEncryptionException; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.DecryptionMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.EncryptionMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.SymmetricRawMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.WrappedRawMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.internal.Base64; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.internal.Hkdf; + +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.kms.KmsClient; +import software.amazon.awssdk.services.kms.model.DecryptRequest; +import software.amazon.awssdk.services.kms.model.DecryptResponse; +import software.amazon.awssdk.services.kms.model.GenerateDataKeyRequest; +import software.amazon.awssdk.services.kms.model.GenerateDataKeyResponse; + +/** + * Generates a unique data key for each record in DynamoDB and protects that key + * using {@link KmsClient}. Currently, the HashKey, RangeKey, and TableName will be + * included in the KMS EncryptionContext for wrapping/unwrapping the key. This + * means that records cannot be copied/moved between tables without re-encryption. + * + * @see KMS Encryption Context + */ +public class DirectKmsMaterialsProvider implements EncryptionMaterialsProvider { + private static final String COVERED_ATTR_CTX_KEY = "aws-kms-ec-attr"; + private static final String SIGNING_KEY_ALGORITHM = "amzn-ddb-sig-alg"; + private static final String TABLE_NAME_EC_KEY = "*aws-kms-table*"; + + private static final String DEFAULT_ENC_ALG = "AES/256"; + private static final String DEFAULT_SIG_ALG = "HmacSHA256/256"; + private static final String KEY_COVERAGE = "*keys*"; + private static final String KDF_ALG = "HmacSHA256"; + private static final String KDF_SIG_INFO = "Signing"; + private static final String KDF_ENC_INFO = "Encryption"; + + private final KmsClient kms; + private final String encryptionKeyId; + private final Map description; + private final String dataKeyAlg; + private final int dataKeyLength; + private final String dataKeyDesc; + private final String sigKeyAlg; + private final int sigKeyLength; + private final String sigKeyDesc; + + public DirectKmsMaterialsProvider(KmsClient kms) { + this(kms, null); + } + + public DirectKmsMaterialsProvider(KmsClient kms, String encryptionKeyId, Map materialDescription) { + this.kms = kms; + this.encryptionKeyId = encryptionKeyId; + this.description = materialDescription != null ? + Collections.unmodifiableMap(new HashMap<>(materialDescription)) : + Collections.emptyMap(); + + dataKeyDesc = description.getOrDefault(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, DEFAULT_ENC_ALG); + + String[] parts = dataKeyDesc.split("/", 2); + this.dataKeyAlg = parts[0]; + this.dataKeyLength = parts.length == 2 ? Integer.parseInt(parts[1]) : 256; + + sigKeyDesc = description.getOrDefault(SIGNING_KEY_ALGORITHM, DEFAULT_SIG_ALG); + + parts = sigKeyDesc.split("/", 2); + this.sigKeyAlg = parts[0]; + this.sigKeyLength = parts.length == 2 ? Integer.parseInt(parts[1]) : 256; + } + + public DirectKmsMaterialsProvider(KmsClient kms, String encryptionKeyId) { + this(kms, encryptionKeyId, Collections.emptyMap()); + } + + @Override + public DecryptionMaterials getDecryptionMaterials(EncryptionContext context) { + final Map materialDescription = context.getMaterialDescription(); + + final Map ec = new HashMap<>(); + final String providedEncAlg = materialDescription.get(CONTENT_KEY_ALGORITHM); + final String providedSigAlg = materialDescription.get(SIGNING_KEY_ALGORITHM); + + ec.put("*" + CONTENT_KEY_ALGORITHM + "*", providedEncAlg); + ec.put("*" + SIGNING_KEY_ALGORITHM + "*", providedSigAlg); + + populateKmsEcFromEc(context, ec); + + DecryptRequest.Builder request = DecryptRequest.builder(); + request.ciphertextBlob(SdkBytes.fromByteArray(Base64.decode(materialDescription.get(ENVELOPE_KEY)))); + request.encryptionContext(ec); + final DecryptResponse decryptResponse = decrypt(request.build(), context); + validateEncryptionKeyId(decryptResponse.keyId(), context); + + final Hkdf kdf; + try { + kdf = Hkdf.getInstance(KDF_ALG); + } catch (NoSuchAlgorithmException e) { + throw new DynamoDbEncryptionException(e); + } + kdf.init(decryptResponse.plaintext().asByteArray()); + + final String[] encAlgParts = providedEncAlg.split("/", 2); + int encLength = encAlgParts.length == 2 ? Integer.parseInt(encAlgParts[1]) : 256; + final String[] sigAlgParts = providedSigAlg.split("/", 2); + int sigLength = sigAlgParts.length == 2 ? Integer.parseInt(sigAlgParts[1]) : 256; + + final SecretKey encryptionKey = new SecretKeySpec(kdf.deriveKey(KDF_ENC_INFO, encLength / 8), encAlgParts[0]); + final SecretKey macKey = new SecretKeySpec(kdf.deriveKey(KDF_SIG_INFO, sigLength / 8), sigAlgParts[0]); + + return new SymmetricRawMaterials(encryptionKey, macKey, materialDescription); + } + + @Override + public EncryptionMaterials getEncryptionMaterials(EncryptionContext context) { + final Map ec = new HashMap<>(); + ec.put("*" + CONTENT_KEY_ALGORITHM + "*", dataKeyDesc); + ec.put("*" + SIGNING_KEY_ALGORITHM + "*", sigKeyDesc); + populateKmsEcFromEc(context, ec); + + final String keyId = selectEncryptionKeyId(context); + if (keyId == null || keyId.isEmpty()) { + throw new DynamoDbEncryptionException("Encryption key id is empty."); + } + + final GenerateDataKeyRequest.Builder req = GenerateDataKeyRequest.builder(); + req.keyId(keyId); + // NumberOfBytes parameter is used because we're not using this key as an AES-256 key, + // we're using it as an HKDF-SHA256 key. + req.numberOfBytes(256 / 8); + req.encryptionContext(ec); + + final GenerateDataKeyResponse dataKeyResult = generateDataKey(req.build(), context); + + final Map materialDescription = new HashMap<>(description); + materialDescription.put(COVERED_ATTR_CTX_KEY, KEY_COVERAGE); + materialDescription.put(KEY_WRAPPING_ALGORITHM, "kms"); + materialDescription.put(CONTENT_KEY_ALGORITHM, dataKeyDesc); + materialDescription.put(SIGNING_KEY_ALGORITHM, sigKeyDesc); + materialDescription.put(ENVELOPE_KEY, + Base64.encodeToString(dataKeyResult.ciphertextBlob().asByteArray())); + + final Hkdf kdf; + try { + kdf = Hkdf.getInstance(KDF_ALG); + } catch (NoSuchAlgorithmException e) { + throw new DynamoDbEncryptionException(e); + } + + kdf.init(dataKeyResult.plaintext().asByteArray()); + + final SecretKey encryptionKey = new SecretKeySpec(kdf.deriveKey(KDF_ENC_INFO, dataKeyLength / 8), dataKeyAlg); + final SecretKey signatureKey = new SecretKeySpec(kdf.deriveKey(KDF_SIG_INFO, sigKeyLength / 8), sigKeyAlg); + return new SymmetricRawMaterials(encryptionKey, signatureKey, materialDescription); + } + + /** + * Get encryption key id that is used to create the {@link EncryptionMaterials}. + * + * @return encryption key id. + */ + protected String getEncryptionKeyId() { + return this.encryptionKeyId; + } + + /** + * Select encryption key id to be used to generate data key. The default implementation of this method returns + * {@link DirectKmsMaterialsProvider#encryptionKeyId}. + * + * @param context encryption context. + * @return the encryptionKeyId. + * @throws DynamoDbEncryptionException when we fails to select a valid encryption key id. + */ + protected String selectEncryptionKeyId(EncryptionContext context) throws DynamoDbEncryptionException { + return getEncryptionKeyId(); + } + + /** + * Validate the encryption key id. The default implementation of this method does not validate + * encryption key id. + * + * @param encryptionKeyId encryption key id from {@link DecryptResponse}. + * @param context encryption context. + * @throws DynamoDbEncryptionException when encryptionKeyId is invalid. + */ + protected void validateEncryptionKeyId(String encryptionKeyId, EncryptionContext context) + throws DynamoDbEncryptionException { + // No action taken. + } + + /** + * Decrypts ciphertext. The default implementation calls KMS to decrypt the ciphertext using the parameters + * provided in the {@link DecryptRequest}. Subclass can override the default implementation to provide + * additional request parameters using attributes within the {@link EncryptionContext}. + * + * @param request request parameters to decrypt the given ciphertext. + * @param context additional useful data to decrypt the ciphertext. + * @return the decrypted plaintext for the given ciphertext. + */ + protected DecryptResponse decrypt(final DecryptRequest request, final EncryptionContext context) { + return kms.decrypt(request); + } + + /** + * Returns a data encryption key that you can use in your application to encrypt data locally. The default + * implementation calls KMS to generate the data key using the parameters provided in the + * {@link GenerateDataKeyRequest}. Subclass can override the default implementation to provide additional + * request parameters using attributes within the {@link EncryptionContext}. + * + * @param request request parameters to generate the data key. + * @param context additional useful data to generate the data key. + * @return the newly generated data key which includes both the plaintext and ciphertext. + */ + protected GenerateDataKeyResponse generateDataKey(final GenerateDataKeyRequest request, + final EncryptionContext context) { + return kms.generateDataKey(request); + } + + /** + * Extracts relevant information from {@code context} and uses it to populate fields in + * {@code kmsEc}. Currently, these fields are: + *
+ *
{@code HashKeyName}
+ *
{@code HashKeyValue}
+ *
{@code RangeKeyName}
+ *
{@code RangeKeyValue}
+ *
{@link #TABLE_NAME_EC_KEY}
+ *
{@code TableName}
+ */ + private static void populateKmsEcFromEc(EncryptionContext context, Map kmsEc) { + final String hashKeyName = context.getHashKeyName(); + if (hashKeyName != null) { + final AttributeValue hashKey = context.getAttributeValues().get(hashKeyName); + if (hashKey.n() != null) { + kmsEc.put(hashKeyName, hashKey.n()); + } else if (hashKey.s() != null) { + kmsEc.put(hashKeyName, hashKey.s()); + } else if (hashKey.b() != null) { + kmsEc.put(hashKeyName, Base64.encodeToString(hashKey.b().asByteArray())); + } else { + throw new UnsupportedOperationException("DirectKmsMaterialsProvider only supports String, Number, and Binary HashKeys"); + } + } + final String rangeKeyName = context.getRangeKeyName(); + if (rangeKeyName != null) { + final AttributeValue rangeKey = context.getAttributeValues().get(rangeKeyName); + if (rangeKey.n() != null) { + kmsEc.put(rangeKeyName, rangeKey.n()); + } else if (rangeKey.s() != null) { + kmsEc.put(rangeKeyName, rangeKey.s()); + } else if (rangeKey.b() != null) { + kmsEc.put(rangeKeyName, Base64.encodeToString(rangeKey.b().asByteArray())); + } else { + throw new UnsupportedOperationException("DirectKmsMaterialsProvider only supports String, Number, and Binary RangeKeys"); + } + } + + final String tableName = context.getTableName(); + if (tableName != null) { + kmsEc.put(TABLE_NAME_EC_KEY, tableName); + } + } + + @Override + public void refresh() { + // No action needed + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/EncryptionMaterialsProvider.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/EncryptionMaterialsProvider.java new file mode 100644 index 000000000..b60fee3ee --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/EncryptionMaterialsProvider.java @@ -0,0 +1,71 @@ +/* + * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers; + +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.EncryptionContext; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.DecryptionMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.EncryptionMaterials; + +/** + * Interface for providing encryption materials. + * Implementations are free to use any strategy for providing encryption + * materials, such as simply providing static material that doesn't change, + * or more complicated implementations, such as integrating with existing + * key management systems. + * + * @author Greg Rubin + */ +public interface EncryptionMaterialsProvider { + + /** + * Retrieves encryption materials matching the specified description from some source. + * + * @param context + * Information to assist in selecting a the proper return value. The implementation + * is free to determine the minimum necessary for successful processing. + * + * @return + * The encryption materials that match the description, or null if no matching encryption materials found. + */ + DecryptionMaterials getDecryptionMaterials(EncryptionContext context); + + /** + * Returns EncryptionMaterials which the caller can use for encryption. + * Each implementation of EncryptionMaterialsProvider can choose its own + * strategy for loading encryption material. For example, an + * implementation might load encryption material from an existing key + * management system, or load new encryption material when keys are + * rotated. + * + * @param context + * Information to assist in selecting a the proper return value. The implementation + * is free to determine the minimum necessary for successful processing. + * + * @return EncryptionMaterials which the caller can use to encrypt or + * decrypt data. + */ + EncryptionMaterials getEncryptionMaterials(EncryptionContext context); + + /** + * Forces this encryption materials provider to refresh its encryption + * material. For many implementations of encryption materials provider, + * this may simply be a no-op, such as any encryption materials provider + * implementation that vends static/non-changing encryption material. + * For other implementations that vend different encryption material + * throughout their lifetime, this method should force the encryption + * materials provider to refresh its encryption material. + */ + void refresh(); +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/KeyStoreMaterialsProvider.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/KeyStoreMaterialsProvider.java new file mode 100644 index 000000000..483b81b51 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/KeyStoreMaterialsProvider.java @@ -0,0 +1,199 @@ +/* + * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.KeyStore.Entry; +import java.security.KeyStore.PrivateKeyEntry; +import java.security.KeyStore.ProtectionParameter; +import java.security.KeyStore.SecretKeyEntry; +import java.security.KeyStore.TrustedCertificateEntry; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.UnrecoverableEntryException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.EncryptionContext; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.exceptions.DynamoDbEncryptionException; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.AsymmetricRawMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.DecryptionMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.EncryptionMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.SymmetricRawMaterials; + +/** + * @author Greg Rubin + */ +public class KeyStoreMaterialsProvider implements EncryptionMaterialsProvider { + private final Map description; + private final String encryptionAlias; + private final String signingAlias; + private final ProtectionParameter encryptionProtection; + private final ProtectionParameter signingProtection; + private final KeyStore keyStore; + private final AtomicReference currMaterials = + new AtomicReference<>(); + + public KeyStoreMaterialsProvider(KeyStore keyStore, String encryptionAlias, String signingAlias, Map description) + throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableEntryException { + this(keyStore, encryptionAlias, signingAlias, null, null, description); + } + + public KeyStoreMaterialsProvider(KeyStore keyStore, String encryptionAlias, String signingAlias, + ProtectionParameter encryptionProtection, ProtectionParameter signingProtection, + Map description) + throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableEntryException { + super(); + this.keyStore = keyStore; + this.encryptionAlias = encryptionAlias; + this.signingAlias = signingAlias; + this.encryptionProtection = encryptionProtection; + this.signingProtection = signingProtection; + this.description = Collections.unmodifiableMap(new HashMap<>(description)); + + validateKeys(); + loadKeys(); + } + + @Override + public DecryptionMaterials getDecryptionMaterials(EncryptionContext context) { + CurrentMaterials materials = currMaterials.get(); + if (context.getMaterialDescription().entrySet().containsAll(description.entrySet())) { + if (materials.encryptionEntry instanceof SecretKeyEntry) { + return materials.symRawMaterials; + } else { + try { + return makeAsymMaterials(materials, context.getMaterialDescription()); + } catch (GeneralSecurityException ex) { + throw new DynamoDbEncryptionException("Unable to decrypt envelope key", ex); + } + } + } else { + return null; + } + } + + @Override + public EncryptionMaterials getEncryptionMaterials(EncryptionContext context) { + CurrentMaterials materials = currMaterials.get(); + if (materials.encryptionEntry instanceof SecretKeyEntry) { + return materials.symRawMaterials; + } else { + try { + return makeAsymMaterials(materials, description); + } catch (GeneralSecurityException ex) { + throw new DynamoDbEncryptionException("Unable to encrypt envelope key", ex); + } + } + } + + private AsymmetricRawMaterials makeAsymMaterials(CurrentMaterials materials, + Map description) throws GeneralSecurityException { + KeyPair encryptionPair = entry2Pair(materials.encryptionEntry); + if (materials.signingEntry instanceof SecretKeyEntry) { + return new AsymmetricRawMaterials(encryptionPair, + ((SecretKeyEntry) materials.signingEntry).getSecretKey(), description); + } else { + return new AsymmetricRawMaterials(encryptionPair, entry2Pair(materials.signingEntry), + description); + } + } + + private static KeyPair entry2Pair(Entry entry) { + PublicKey pub = null; + PrivateKey priv = null; + + if (entry instanceof PrivateKeyEntry) { + PrivateKeyEntry pk = (PrivateKeyEntry) entry; + if (pk.getCertificate() != null) { + pub = pk.getCertificate().getPublicKey(); + } + priv = pk.getPrivateKey(); + } else if (entry instanceof TrustedCertificateEntry) { + TrustedCertificateEntry tc = (TrustedCertificateEntry) entry; + pub = tc.getTrustedCertificate().getPublicKey(); + } else { + throw new IllegalArgumentException( + "Only entry types PrivateKeyEntry and TrustedCertificateEntry are supported."); + } + return new KeyPair(pub, priv); + } + + /** + * Reloads the keys from the underlying keystore by calling + * {@link KeyStore#getEntry(String, ProtectionParameter)} again for each of them. + */ + @Override + public void refresh() { + try { + loadKeys(); + } catch (GeneralSecurityException ex) { + throw new DynamoDbEncryptionException("Unable to load keys from keystore", ex); + } + } + + private void validateKeys() throws KeyStoreException { + if (!keyStore.containsAlias(encryptionAlias)) { + throw new IllegalArgumentException("Keystore does not contain alias: " + + encryptionAlias); + } + if (!keyStore.containsAlias(signingAlias)) { + throw new IllegalArgumentException("Keystore does not contain alias: " + + signingAlias); + } + } + + private void loadKeys() throws NoSuchAlgorithmException, UnrecoverableEntryException, + KeyStoreException { + Entry encryptionEntry = keyStore.getEntry(encryptionAlias, encryptionProtection); + Entry signingEntry = keyStore.getEntry(signingAlias, signingProtection); + CurrentMaterials newMaterials = new CurrentMaterials(encryptionEntry, signingEntry); + currMaterials.set(newMaterials); + } + + private class CurrentMaterials { + public final Entry encryptionEntry; + public final Entry signingEntry; + public final SymmetricRawMaterials symRawMaterials; + + public CurrentMaterials(Entry encryptionEntry, Entry signingEntry) { + super(); + this.encryptionEntry = encryptionEntry; + this.signingEntry = signingEntry; + + if (encryptionEntry instanceof SecretKeyEntry) { + if (signingEntry instanceof SecretKeyEntry) { + this.symRawMaterials = new SymmetricRawMaterials( + ((SecretKeyEntry) encryptionEntry).getSecretKey(), + ((SecretKeyEntry) signingEntry).getSecretKey(), + description); + } else { + this.symRawMaterials = new SymmetricRawMaterials( + ((SecretKeyEntry) encryptionEntry).getSecretKey(), + entry2Pair(signingEntry), + description); + } + } else { + this.symRawMaterials = null; + } + } + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/SymmetricStaticProvider.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/SymmetricStaticProvider.java new file mode 100644 index 000000000..8a63a0328 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/SymmetricStaticProvider.java @@ -0,0 +1,130 @@ +/* + * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers; + +import java.security.KeyPair; +import java.util.Collections; +import java.util.Map; + +import javax.crypto.SecretKey; + +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.EncryptionContext; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.CryptographicMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.DecryptionMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.EncryptionMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.SymmetricRawMaterials; + +/** + * A provider which always returns the same provided symmetric + * encryption/decryption key and the same signing/verification key(s). + * + * @author Greg Rubin + */ +public class SymmetricStaticProvider implements EncryptionMaterialsProvider { + private final SymmetricRawMaterials materials; + + /** + * @param encryptionKey + * the value to be returned by + * {@link #getEncryptionMaterials(EncryptionContext)} and + * {@link #getDecryptionMaterials(EncryptionContext)} + * @param signingPair + * the keypair used to sign/verify the data stored in Dynamo. If + * only the public key is provided, then this provider may be + * used for decryption, but not encryption. + */ + public SymmetricStaticProvider(SecretKey encryptionKey, KeyPair signingPair) { + this(encryptionKey, signingPair, Collections.emptyMap()); + } + + /** + * @param encryptionKey + * the value to be returned by + * {@link #getEncryptionMaterials(EncryptionContext)} and + * {@link #getDecryptionMaterials(EncryptionContext)} + * @param signingPair + * the keypair used to sign/verify the data stored in Dynamo. If + * only the public key is provided, then this provider may be + * used for decryption, but not encryption. + * @param description + * the value to be returned by + * {@link CryptographicMaterials#getMaterialDescription()} for + * any {@link CryptographicMaterials} returned by this object. + */ + public SymmetricStaticProvider(SecretKey encryptionKey, + KeyPair signingPair, Map description) { + materials = new SymmetricRawMaterials(encryptionKey, signingPair, + description); + } + + /** + * @param encryptionKey + * the value to be returned by + * {@link #getEncryptionMaterials(EncryptionContext)} and + * {@link #getDecryptionMaterials(EncryptionContext)} + * @param macKey + * the key used to sign/verify the data stored in Dynamo. + */ + public SymmetricStaticProvider(SecretKey encryptionKey, SecretKey macKey) { + this(encryptionKey, macKey, Collections.emptyMap()); + } + + /** + * @param encryptionKey + * the value to be returned by + * {@link #getEncryptionMaterials(EncryptionContext)} and + * {@link #getDecryptionMaterials(EncryptionContext)} + * @param macKey + * the key used to sign/verify the data stored in Dynamo. + * @param description + * the value to be returned by + * {@link CryptographicMaterials#getMaterialDescription()} for + * any {@link CryptographicMaterials} returned by this object. + */ + public SymmetricStaticProvider(SecretKey encryptionKey, SecretKey macKey, Map description) { + materials = new SymmetricRawMaterials(encryptionKey, macKey, description); + } + + /** + * Returns the encryptionKey provided to the constructor if and only if + * materialDescription is a super-set (may be equal) to the + * description provided to the constructor. + */ + @Override + public DecryptionMaterials getDecryptionMaterials(EncryptionContext context) { + if (context.getMaterialDescription().entrySet().containsAll(materials.getMaterialDescription().entrySet())) { + return materials; + } + else { + return null; + } + } + + /** + * Returns the encryptionKey provided to the constructor. + */ + @Override + public EncryptionMaterials getEncryptionMaterials(EncryptionContext context) { + return materials; + } + + /** + * Does nothing. + */ + @Override + public void refresh() { + // Do Nothing + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/WrappedMaterialsProvider.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/WrappedMaterialsProvider.java new file mode 100644 index 000000000..1c92fb3f4 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/WrappedMaterialsProvider.java @@ -0,0 +1,163 @@ +/* + * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers; + +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyPair; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.crypto.SecretKey; + +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.EncryptionContext; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.exceptions.DynamoDbEncryptionException; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.CryptographicMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.DecryptionMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.EncryptionMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.WrappedRawMaterials; + +/** + * This provider will use create a unique (random) symmetric key upon each call to + * {@link #getEncryptionMaterials(EncryptionContext)}. Practically, this means each record in DynamoDB will be + * encrypted under a unique record key. A wrapped/encrypted copy of this record key is stored in the + * MaterialsDescription field of that record and is unwrapped/decrypted upon reading that record. + * + * This is generally a more secure way of encrypting data than with the + * {@link SymmetricStaticProvider}. + * + * @see WrappedRawMaterials + * + * @author Greg Rubin + */ +public class WrappedMaterialsProvider implements EncryptionMaterialsProvider { + private final Key wrappingKey; + private final Key unwrappingKey; + private final KeyPair sigPair; + private final SecretKey macKey; + private final Map description; + + /** + * @param wrappingKey + * The key used to wrap/encrypt the symmetric record key. (May be the same as the + * unwrappingKey.) + * @param unwrappingKey + * The key used to unwrap/decrypt the symmetric record key. (May be the same as the + * wrappingKey.) If null, then this provider may only be used for + * decryption, but not encryption. + * @param signingPair + * the keypair used to sign/verify the data stored in Dynamo. If only the public key + * is provided, then this provider may only be used for decryption, but not + * encryption. + */ + public WrappedMaterialsProvider(Key wrappingKey, Key unwrappingKey, KeyPair signingPair) { + this(wrappingKey, unwrappingKey, signingPair, Collections.emptyMap()); + } + + /** + * @param wrappingKey + * The key used to wrap/encrypt the symmetric record key. (May be the same as the + * unwrappingKey.) + * @param unwrappingKey + * The key used to unwrap/decrypt the symmetric record key. (May be the same as the + * wrappingKey.) If null, then this provider may only be used for + * decryption, but not encryption. + * @param signingPair + * the keypair used to sign/verify the data stored in Dynamo. If only the public key + * is provided, then this provider may only be used for decryption, but not + * encryption. + * @param description + * description the value to be returned by + * {@link CryptographicMaterials#getMaterialDescription()} for any + * {@link CryptographicMaterials} returned by this object. + */ + public WrappedMaterialsProvider(Key wrappingKey, Key unwrappingKey, KeyPair signingPair, Map description) { + this.wrappingKey = wrappingKey; + this.unwrappingKey = unwrappingKey; + this.sigPair = signingPair; + this.macKey = null; + this.description = Collections.unmodifiableMap(new HashMap<>(description)); + } + + /** + * @param wrappingKey + * The key used to wrap/encrypt the symmetric record key. (May be the same as the + * unwrappingKey.) + * @param unwrappingKey + * The key used to unwrap/decrypt the symmetric record key. (May be the same as the + * wrappingKey.) If null, then this provider may only be used for + * decryption, but not encryption. + * @param macKey + * the key used to sign/verify the data stored in Dynamo. + */ + public WrappedMaterialsProvider(Key wrappingKey, Key unwrappingKey, SecretKey macKey) { + this(wrappingKey, unwrappingKey, macKey, Collections.emptyMap()); + } + + /** + * @param wrappingKey + * The key used to wrap/encrypt the symmetric record key. (May be the same as the + * unwrappingKey.) + * @param unwrappingKey + * The key used to unwrap/decrypt the symmetric record key. (May be the same as the + * wrappingKey.) If null, then this provider may only be used for + * decryption, but not encryption. + * @param macKey + * the key used to sign/verify the data stored in Dynamo. + * @param description + * description the value to be returned by + * {@link CryptographicMaterials#getMaterialDescription()} for any + * {@link CryptographicMaterials} returned by this object. + */ + public WrappedMaterialsProvider(Key wrappingKey, Key unwrappingKey, SecretKey macKey, Map description) { + this.wrappingKey = wrappingKey; + this.unwrappingKey = unwrappingKey; + this.sigPair = null; + this.macKey = macKey; + this.description = Collections.unmodifiableMap(new HashMap<>(description)); + } + + @Override + public DecryptionMaterials getDecryptionMaterials(EncryptionContext context) { + try { + if (macKey != null) { + return new WrappedRawMaterials(wrappingKey, unwrappingKey, macKey, context.getMaterialDescription()); + } else { + return new WrappedRawMaterials(wrappingKey, unwrappingKey, sigPair, context.getMaterialDescription()); + } + } catch (GeneralSecurityException ex) { + throw new DynamoDbEncryptionException("Unable to decrypt envelope key", ex); + } + } + + @Override + public EncryptionMaterials getEncryptionMaterials(EncryptionContext context) { + try { + if (macKey != null) { + return new WrappedRawMaterials(wrappingKey, unwrappingKey, macKey, description); + } else { + return new WrappedRawMaterials(wrappingKey, unwrappingKey, sigPair, description); + } + } catch (GeneralSecurityException ex) { + throw new DynamoDbEncryptionException("Unable to encrypt envelope key", ex); + } + } + + @Override + public void refresh() { + // Do nothing + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/store/MetaStore.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/store/MetaStore.java new file mode 100644 index 000000000..c0fbe5e06 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/store/MetaStore.java @@ -0,0 +1,434 @@ +/* + * Copyright 2015-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.store; + +import java.security.GeneralSecurityException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ComparisonOperator; +import software.amazon.awssdk.services.dynamodb.model.Condition; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; +import software.amazon.awssdk.services.dynamodb.model.CreateTableResponse; +import software.amazon.awssdk.services.dynamodb.model.ExpectedAttributeValue; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; +import software.amazon.awssdk.services.dynamodb.model.KeyType; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.QueryRequest; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.DynamoDbEncryptor; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.EncryptionContext; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.EncryptionMaterialsProvider; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.WrappedMaterialsProvider; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.internal.Utils; + + +/** + * Provides a simple collection of EncryptionMaterialProviders backed by an encrypted DynamoDB + * table. This can be used to build key hierarchies or meta providers. + * + * Currently, this only supports AES-256 in AESWrap mode and HmacSHA256 for the providers persisted + * in the table. + * + * @author rubin + */ +public class MetaStore extends ProviderStore { + private static final String INTEGRITY_ALGORITHM_FIELD = "intAlg"; + private static final String INTEGRITY_KEY_FIELD = "int"; + private static final String ENCRYPTION_ALGORITHM_FIELD = "encAlg"; + private static final String ENCRYPTION_KEY_FIELD = "enc"; + private static final Pattern COMBINED_PATTERN = Pattern.compile("([^#]+)#(\\d*)"); + private static final String DEFAULT_INTEGRITY = "HmacSHA256"; + private static final String DEFAULT_ENCRYPTION = "AES"; + private static final String MATERIAL_TYPE_VERSION = "t"; + private static final String META_ID = "amzn-ddb-meta-id"; + + private static final String DEFAULT_HASH_KEY = "N"; + private static final String DEFAULT_RANGE_KEY = "V"; + + /** Default no-op implementation of {@link ExtraDataSupplier}. */ + private static final EmptyExtraDataSupplier EMPTY_EXTRA_DATA_SUPPLIER + = new EmptyExtraDataSupplier(); + + /** DDB fields that must be encrypted. */ + private static final Set ENCRYPTED_FIELDS; + static { + final Set tempEncryptedFields = new HashSet<>(); + tempEncryptedFields.add(MATERIAL_TYPE_VERSION); + tempEncryptedFields.add(ENCRYPTION_KEY_FIELD); + tempEncryptedFields.add(ENCRYPTION_ALGORITHM_FIELD); + tempEncryptedFields.add(INTEGRITY_KEY_FIELD); + tempEncryptedFields.add(INTEGRITY_ALGORITHM_FIELD); + ENCRYPTED_FIELDS = tempEncryptedFields; + } + + private final Map doesNotExist; + private final Set doNotEncrypt; +// private final DynamoDbEncryptionConfiguration encryptionConfiguration; + private final String tableName; + private final DynamoDbClient ddb; + private final DynamoDbEncryptor encryptor; + private final EncryptionContext ddbCtx; + private final ExtraDataSupplier extraDataSupplier; + + /** + * Provides extra data that should be persisted along with the standard material data. + */ + public interface ExtraDataSupplier { + + /** + * Gets the extra data attributes for the specified material name. + * + * @param materialName material name. + * @param version version number. + * @return plain text of the extra data. + */ + Map getAttributes(final String materialName, final long version); + + /** + * Gets the extra data field names that should be signed only but not encrypted. + * + * @return signed only fields. + */ + Set getSignedOnlyFieldNames(); + } + + /** + * Create a new MetaStore with specified table name. + * + * @param ddb Interface for accessing DynamoDB. + * @param tableName DynamoDB table name for this {@link MetaStore}. + * @param encryptor used to perform crypto operations on the record attributes. + */ + public MetaStore(final DynamoDbClient ddb, final String tableName, + final DynamoDbEncryptor encryptor) { + this(ddb, tableName, encryptor, EMPTY_EXTRA_DATA_SUPPLIER); + } + + /** + * Create a new MetaStore with specified table name and extra data supplier. + * + * @param ddb Interface for accessing DynamoDB. + * @param tableName DynamoDB table name for this {@link MetaStore}. + * @param encryptor used to perform crypto operations on the record attributes + * @param extraDataSupplier provides extra data that should be stored along with the material. + */ + public MetaStore(final DynamoDbClient ddb, final String tableName, + final DynamoDbEncryptor encryptor, final ExtraDataSupplier extraDataSupplier) { + this.ddb = checkNotNull(ddb, "ddb must not be null"); + this.tableName = checkNotNull(tableName, "tableName must not be null"); + this.encryptor = checkNotNull(encryptor, "encryptor must not be null"); + this.extraDataSupplier = checkNotNull(extraDataSupplier, "extraDataSupplier must not be null"); + this.ddbCtx = + new EncryptionContext.Builder() + .tableName(this.tableName) + .hashKeyName(DEFAULT_HASH_KEY) + .rangeKeyName(DEFAULT_RANGE_KEY) + .build(); + + final Map tmpExpected = new HashMap<>(); + tmpExpected.put(DEFAULT_HASH_KEY, ExpectedAttributeValue.builder().exists(false).build()); + tmpExpected.put(DEFAULT_RANGE_KEY, ExpectedAttributeValue.builder().exists(false).build()); + doesNotExist = Collections.unmodifiableMap(tmpExpected); + + this.doNotEncrypt = getSignedOnlyFields(extraDataSupplier); + } + + @Override + public EncryptionMaterialsProvider getProvider(final String materialName, final long version) { + final Map item = getMaterialItem(materialName, version); + return decryptProvider(item); + } + + @Override + public EncryptionMaterialsProvider getOrCreate(final String materialName, final long nextId) { + final Map plaintext = createMaterialItem(materialName, nextId); + final Map ciphertext = conditionalPut(getEncryptedText(plaintext)); + return decryptProvider(ciphertext); + } + + @Override + public long getMaxVersion(final String materialName) { + + final List> items = + ddb.query( + QueryRequest.builder() + .tableName(tableName) + .consistentRead(Boolean.TRUE) + .keyConditions( + Collections.singletonMap( + DEFAULT_HASH_KEY, + Condition.builder() + .comparisonOperator(ComparisonOperator.EQ) + .attributeValueList(AttributeValue.builder().s(materialName).build()) + .build())) + .limit(1) + .scanIndexForward(false) + .attributesToGet(DEFAULT_RANGE_KEY) + .build()) + .items(); + + if (items.isEmpty()) { + return -1L; + } else { + return Long.parseLong(items.get(0).get(DEFAULT_RANGE_KEY).n()); + } + } + + @Override + public long getVersionFromMaterialDescription(final Map description) { + final Matcher m = COMBINED_PATTERN.matcher(description.get(META_ID)); + if (m.matches()) { + return Long.parseLong(m.group(2)); + } else { + throw new IllegalArgumentException("No meta id found"); + } + } + + /** + * This API retrieves the intermediate keys from the source region and replicates it in the target region. + * + * @param materialName material name of the encryption material. + * @param version version of the encryption material. + * @param targetMetaStore target MetaStore where the encryption material to be stored. + */ + public void replicate(final String materialName, final long version, final MetaStore targetMetaStore) { + try { + final Map item = getMaterialItem(materialName, version); + + final Map plainText = getPlainText(item); + final Map encryptedText = targetMetaStore.getEncryptedText(plainText); + final PutItemRequest put = PutItemRequest.builder() + .tableName(targetMetaStore.tableName) + .item(encryptedText) + .expected(doesNotExist) + .build(); + targetMetaStore.ddb.putItem(put); + } catch (ConditionalCheckFailedException e) { + //Item already present. + } + } + + /** + * Creates a DynamoDB Table with the correct properties to be used with a ProviderStore. + * + * @param ddb interface for accessing DynamoDB + * @param tableName name of table that stores the meta data of the material. + * @param provisionedThroughput required provisioned throughput of the this table. + * @return result of create table request. + */ + public static CreateTableResponse createTable(final DynamoDbClient ddb, final String tableName, + final ProvisionedThroughput provisionedThroughput) { + return ddb.createTable( + CreateTableRequest.builder() + .tableName(tableName) + .attributeDefinitions(Arrays.asList( + AttributeDefinition.builder() + .attributeName(DEFAULT_HASH_KEY) + .attributeType(ScalarAttributeType.S) + .build(), + AttributeDefinition.builder() + .attributeName(DEFAULT_RANGE_KEY) + .attributeType(ScalarAttributeType.N).build())) + .keySchema(Arrays.asList( + KeySchemaElement.builder() + .attributeName(DEFAULT_HASH_KEY) + .keyType(KeyType.HASH) + .build(), + KeySchemaElement.builder() + .attributeName(DEFAULT_RANGE_KEY) + .keyType(KeyType.RANGE) + .build())) + .provisionedThroughput(provisionedThroughput).build()); + } + + private Map getMaterialItem(final String materialName, final long version) { + final Map ddbKey = new HashMap<>(); + ddbKey.put(DEFAULT_HASH_KEY, AttributeValue.builder().s(materialName).build()); + ddbKey.put(DEFAULT_RANGE_KEY, AttributeValue.builder().n(Long.toString(version)).build()); + final Map item = ddbGet(ddbKey); + if (item == null || item.isEmpty()) { + throw new IndexOutOfBoundsException("No material found: " + materialName + "#" + version); + } + return item; + } + + + /** + * Empty extra data supplier. This default class is intended to simplify the default + * implementation of {@link MetaStore}. + */ + private static class EmptyExtraDataSupplier implements ExtraDataSupplier { + @Override + public Map getAttributes(String materialName, long version) { + return Collections.emptyMap(); + } + + @Override + public Set getSignedOnlyFieldNames() { + return Collections.emptySet(); + } + } + + /** + * Get a set of fields that must be signed but not encrypted. + * + * @param extraDataSupplier extra data supplier that is used to return sign only field names. + * @return fields that must be signed. + */ + private static Set getSignedOnlyFields(final ExtraDataSupplier extraDataSupplier) { + final Set signedOnlyFields = extraDataSupplier.getSignedOnlyFieldNames(); + for (final String signedOnlyField : signedOnlyFields) { + if (ENCRYPTED_FIELDS.contains(signedOnlyField)) { + throw new IllegalArgumentException(signedOnlyField + " must be encrypted"); + } + } + + // fields that should not be encrypted + final Set doNotEncryptFields = new HashSet<>(); + doNotEncryptFields.add(DEFAULT_HASH_KEY); + doNotEncryptFields.add(DEFAULT_RANGE_KEY); + doNotEncryptFields.addAll(signedOnlyFields); + return Collections.unmodifiableSet(doNotEncryptFields); + } + + private Map conditionalPut(final Map item) { + try { + final PutItemRequest put = PutItemRequest.builder().tableName(tableName).item(item) + .expected(doesNotExist).build(); + ddb.putItem(put); + return item; + } catch (final ConditionalCheckFailedException ex) { + final Map ddbKey = new HashMap<>(); + ddbKey.put(DEFAULT_HASH_KEY, item.get(DEFAULT_HASH_KEY)); + ddbKey.put(DEFAULT_RANGE_KEY, item.get(DEFAULT_RANGE_KEY)); + return ddbGet(ddbKey); + } + } + + private Map ddbGet(final Map ddbKey) { + return ddb.getItem( + GetItemRequest.builder().tableName(tableName).consistentRead(true) + .key(ddbKey).build()).item(); + } + + /** + * Build an material item for a given material name and version with newly generated + * encryption and integrity keys. + * + * @param materialName material name. + * @param version version of the material. + * @return newly generated plaintext material item. + */ + private Map createMaterialItem(final String materialName, final long version) { + final SecretKeySpec encryptionKey = new SecretKeySpec(Utils.getRandom(32), DEFAULT_ENCRYPTION); + final SecretKeySpec integrityKey = new SecretKeySpec(Utils.getRandom(32), DEFAULT_INTEGRITY); + + final Map plaintext = new HashMap<>(); + plaintext.put(DEFAULT_HASH_KEY, AttributeValue.builder().s(materialName).build()); + plaintext.put(DEFAULT_RANGE_KEY, AttributeValue.builder().n(Long.toString(version)).build()); + plaintext.put(MATERIAL_TYPE_VERSION, AttributeValue.builder().s("0").build()); + plaintext.put(ENCRYPTION_KEY_FIELD, + AttributeValue.builder().b(SdkBytes.fromByteArray(encryptionKey.getEncoded())).build()); + plaintext.put(ENCRYPTION_ALGORITHM_FIELD, AttributeValue.builder().s(encryptionKey.getAlgorithm()).build()); + plaintext.put(INTEGRITY_KEY_FIELD, + AttributeValue.builder().b(SdkBytes.fromByteArray(integrityKey.getEncoded())).build()); + plaintext.put(INTEGRITY_ALGORITHM_FIELD, AttributeValue.builder().s(integrityKey.getAlgorithm()).build()); + plaintext.putAll(extraDataSupplier.getAttributes(materialName, version)); + + return plaintext; + } + + private EncryptionMaterialsProvider decryptProvider(final Map item) { + final Map plaintext = getPlainText(item); + + final String type = plaintext.get(MATERIAL_TYPE_VERSION).s(); + final SecretKey encryptionKey; + final SecretKey integrityKey; + // This switch statement is to make future extensibility easier and more obvious + switch (type) { + case "0": // Only currently supported type + encryptionKey = new SecretKeySpec(plaintext.get(ENCRYPTION_KEY_FIELD).b().asByteArray(), + plaintext.get(ENCRYPTION_ALGORITHM_FIELD).s()); + integrityKey = new SecretKeySpec(plaintext.get(INTEGRITY_KEY_FIELD).b().asByteArray(), plaintext + .get(INTEGRITY_ALGORITHM_FIELD).s()); + break; + default: + throw new IllegalStateException("Unsupported material type: " + type); + } + return new WrappedMaterialsProvider(encryptionKey, encryptionKey, integrityKey, + buildDescription(plaintext)); + } + + /** + * Decrypts attributes in the ciphertext item using {@link DynamoDbEncryptor}. except the + * attribute names specified in doNotEncrypt. + * + * @param ciphertext the ciphertext to be decrypted. + * @throws SdkClientException when failed to decrypt material item. + * @return decrypted item. + */ + private Map getPlainText(final Map ciphertext) { + try { + return encryptor.decryptAllFieldsExcept(ciphertext, ddbCtx, doNotEncrypt); + } catch (final GeneralSecurityException e) { + throw SdkClientException.create("Error retrieving PlainText", e); + } + } + + /** + * Encrypts attributes in the plaintext item using {@link DynamoDbEncryptor}. except the attribute + * names specified in doNotEncrypt. + * + * @throws SdkClientException when failed to encrypt material item. + * @param plaintext plaintext to be encrypted. + */ + private Map getEncryptedText(Map plaintext) { + try { + return encryptor.encryptAllFieldsExcept(plaintext, ddbCtx, doNotEncrypt); + } catch (final GeneralSecurityException e) { + throw SdkClientException.create("Error retrieving PlainText", e); + } + } + + private Map buildDescription(final Map plaintext) { + return Collections.singletonMap(META_ID, plaintext.get(DEFAULT_HASH_KEY).s() + "#" + + plaintext.get(DEFAULT_RANGE_KEY).n()); + } + + private static V checkNotNull(final V ref, final String errMsg) { + if (ref == null) { + throw new NullPointerException(errMsg); + } else { + return ref; + } + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/store/ProviderStore.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/store/ProviderStore.java new file mode 100644 index 000000000..a29fe9b34 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/store/ProviderStore.java @@ -0,0 +1,84 @@ +/* + * Copyright 2015-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.store; + +import java.util.Map; + +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.EncryptionMaterialsProvider; + +/** + * Provides a standard way to retrieve and optionally create {@link EncryptionMaterialsProvider}s + * backed by some form of persistent storage. + * + * @author rubin + * + */ +public abstract class ProviderStore { + + /** + * Returns the most recent provider with the specified name. If there are no providers with this + * name, it will create one with version 0. + */ + public EncryptionMaterialsProvider getProvider(final String materialName) { + final long currVersion = getMaxVersion(materialName); + if (currVersion >= 0) { + return getProvider(materialName, currVersion); + } else { + return getOrCreate(materialName, 0); + } + } + + /** + * Returns the provider with the specified name and version. + * + * @throws IndexOutOfBoundsException + * if {@code version} is not a valid version + */ + public abstract EncryptionMaterialsProvider getProvider(final String materialName, final long version); + + /** + * Creates a new provider with a version one greater than the current max version. If multiple + * clients attempt to create a provider with this same version simultaneously, they will + * properly coordinate and the result will be that a single provider is created and that all + * ProviderStores return the same one. + */ + public EncryptionMaterialsProvider newProvider(final String materialName) { + final long nextId = getMaxVersion(materialName) + 1; + return getOrCreate(materialName, nextId); + } + + /** + * Returns the provider with the specified name and version and creates it if it doesn't exist. + * + * @throws UnsupportedOperationException + * if a new provider cannot be created + */ + public EncryptionMaterialsProvider getOrCreate(final String materialName, final long nextId) { + try { + return getProvider(materialName, nextId); + } catch (final IndexOutOfBoundsException ex) { + throw new UnsupportedOperationException("This ProviderStore does not support creation.", ex); + } + } + + /** + * Returns the maximum version number associated with {@code materialName}. If there are no + * versions, returns -1. + */ + public abstract long getMaxVersion(final String materialName); + + /** + * Extracts the material version from {@code description}. + */ + public abstract long getVersionFromMaterialDescription(final Map description); +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/utils/EncryptionContextOperators.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/utils/EncryptionContextOperators.java new file mode 100644 index 000000000..d29bb818c --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/utils/EncryptionContextOperators.java @@ -0,0 +1,81 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.utils; + +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.EncryptionContext; + +import java.util.Map; +import java.util.function.UnaryOperator; + +/** + * Implementations of common operators for overriding the EncryptionContext + */ +public class EncryptionContextOperators { + + // Prevent instantiation + private EncryptionContextOperators() { + } + + /** + * An operator for overriding EncryptionContext's table name for a specific DynamoDbEncryptor. If any table names or + * the encryption context itself is null, then it returns the original EncryptionContext. + * + * @param originalTableName the name of the table that should be overridden in the Encryption Context + * @param newTableName the table name that should be used in the Encryption Context + * @return A UnaryOperator that produces a new EncryptionContext with the supplied table name + */ + public static UnaryOperator overrideEncryptionContextTableName( + String originalTableName, + String newTableName) { + return encryptionContext -> { + if (encryptionContext == null + || encryptionContext.getTableName() == null + || originalTableName == null + || newTableName == null) { + return encryptionContext; + } + if (originalTableName.equals(encryptionContext.getTableName())) { + return encryptionContext.toBuilder().tableName(newTableName).build(); + } else { + return encryptionContext; + } + }; + } + + /** + * An operator for mapping multiple table names in the Encryption Context to a new table name. If the table name for + * a given EncryptionContext is missing, then it returns the original EncryptionContext. Similarly, it returns the + * original EncryptionContext if the value it is overridden to is null, or if the original table name is null. + * + * @param tableNameOverrideMap a map specifying the names of tables that should be overridden, + * and the values to which they should be overridden. If the given table name + * corresponds to null, or isn't in the map, then the table name won't be overridden. + * @return A UnaryOperator that produces a new EncryptionContext with the supplied table name + */ + public static UnaryOperator overrideEncryptionContextTableNameUsingMap( + Map tableNameOverrideMap) { + return encryptionContext -> { + if (tableNameOverrideMap == null || encryptionContext == null || encryptionContext.getTableName() == null) { + return encryptionContext; + } + String newTableName = tableNameOverrideMap.get(encryptionContext.getTableName()); + if (newTableName != null) { + return encryptionContext.toBuilder().tableName(newTableName).build(); + } else { + return encryptionContext; + } + }; + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/AttributeValueMarshaller.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/AttributeValueMarshaller.java new file mode 100644 index 000000000..e9348af05 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/AttributeValueMarshaller.java @@ -0,0 +1,331 @@ +/* + * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.internal; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import software.amazon.awssdk.core.BytesWrapper; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.util.DefaultSdkAutoConstructList; +import software.amazon.awssdk.core.util.DefaultSdkAutoConstructMap; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + + +/** + * @author Greg Rubin + */ +public class AttributeValueMarshaller { + private static final Charset UTF8 = Charset.forName("UTF-8"); + private static final int TRUE_FLAG = 1; + private static final int FALSE_FLAG = 0; + + private AttributeValueMarshaller() { + // Prevent instantiation + } + + /** + * Marshalls the data using a TLV (Tag-Length-Value) encoding. The tag may be 'b', 'n', 's', + * '?', '\0' to represent a ByteBuffer, Number, String, Boolean, or Null respectively. The tag + * may also be capitalized (for 'b', 'n', and 's',) to represent an array of that type. If an + * array is stored, then a four-byte big-endian integer is written representing the number of + * array elements. If a ByteBuffer is stored, the length of the buffer is stored as a four-byte + * big-endian integer and the buffer then copied directly. Both Numbers and Strings are treated + * identically and are stored as UTF8 encoded Unicode, proceeded by the length of the encoded + * string (in bytes) as a four-byte big-endian integer. Boolean is encoded as a single byte, 0 + * for false and 1 for true (and so has no Length parameter). The + * Null tag ('\0') takes neither a Length nor a Value parameter. + * + * The tags 'L' and 'M' are for the document types List and Map respectively. These are encoded + * recursively with the Length being the size of the collection. In the case of List, the value + * is a Length number of marshalled AttributeValues. If the case of Map, the value is a Length + * number of AttributeValue Pairs where the first must always have a String value. + * + * This implementation does not recognize loops. If an AttributeValue contains itself + * (even indirectly) this code will recurse infinitely. + * + * @param attributeValue an AttributeValue instance + * @return the serialized AttributeValue + * @see java.io.DataInput + */ + public static ByteBuffer marshall(final AttributeValue attributeValue) { + try (ByteArrayOutputStream resultBytes = new ByteArrayOutputStream(); + DataOutputStream out = new DataOutputStream(resultBytes);) { + marshall(attributeValue, out); + out.close(); + resultBytes.close(); + return ByteBuffer.wrap(resultBytes.toByteArray()); + } catch (final IOException ex) { + // Due to the objects in use, an IOException is not possible. + throw new RuntimeException("Unexpected exception", ex); + } + } + + private static void marshall(final AttributeValue attributeValue, final DataOutputStream out) + throws IOException { + + if (attributeValue.b() != null) { + out.writeChar('b'); + writeBytes(attributeValue.b().asByteBuffer(), out); + } else if (hasAttributeValueSet(attributeValue.bs())) { + out.writeChar('B'); + writeBytesList(attributeValue.bs().stream() + .map(BytesWrapper::asByteBuffer).collect(Collectors.toList()), out); + } else if (attributeValue.n() != null) { + out.writeChar('n'); + writeString(trimZeros(attributeValue.n()), out); + } else if (hasAttributeValueSet(attributeValue.ns())) { + out.writeChar('N'); + + final List ns = new ArrayList<>(attributeValue.ns().size()); + for (final String n : attributeValue.ns()) { + ns.add(trimZeros(n)); + } + writeStringList(ns, out); + } else if (attributeValue.s() != null) { + out.writeChar('s'); + writeString(attributeValue.s(), out); + } else if (hasAttributeValueSet(attributeValue.ss())) { + out.writeChar('S'); + writeStringList(attributeValue.ss(), out); + } else if (attributeValue.bool() != null) { + out.writeChar('?'); + out.writeByte((attributeValue.bool() ? TRUE_FLAG : FALSE_FLAG)); + } else if (Boolean.TRUE.equals(attributeValue.nul())) { + out.writeChar('\0'); + } else if (hasAttributeValueSet(attributeValue.l())) { + final List l = attributeValue.l(); + out.writeChar('L'); + out.writeInt(l.size()); + for (final AttributeValue attr : l) { + if (attr == null) { + throw new NullPointerException( + "Encountered null list entry value while marshalling attribute value " + + attributeValue); + } + marshall(attr, out); + } + } else if (hasAttributeValueMap(attributeValue.m())) { + final Map m = attributeValue.m(); + final List mKeys = new ArrayList<>(m.keySet()); + Collections.sort(mKeys); + out.writeChar('M'); + out.writeInt(m.size()); + for (final String mKey : mKeys) { + marshall(AttributeValue.builder().s(mKey).build(), out); + + final AttributeValue mValue = m.get(mKey); + + if (mValue == null) { + throw new NullPointerException( + "Encountered null map value for key " + + mKey + + " while marshalling attribute value " + + attributeValue); + } + marshall(mValue, out); + } + } else { + throw new IllegalArgumentException("A seemingly empty AttributeValue is indicative of invalid input or potential errors"); + } + } + + /** + * @see #marshall(AttributeValue) + */ + public static AttributeValue unmarshall(final ByteBuffer plainText) { + try (final DataInputStream in = new DataInputStream( + new ByteBufferInputStream(plainText.asReadOnlyBuffer()))) { + return unmarshall(in); + } catch (IOException ex) { + // Due to the objects in use, an IOException is not possible. + throw new RuntimeException("Unexpected exception", ex); + } + } + + private static AttributeValue unmarshall(final DataInputStream in) throws IOException { + char type = in.readChar(); + AttributeValue.Builder result = AttributeValue.builder(); + switch (type) { + case '\0': + result.nul(Boolean.TRUE); + break; + case 'b': + result.b(SdkBytes.fromByteBuffer(readBytes(in))); + break; + case 'B': + result.bs(readBytesList(in).stream().map(SdkBytes::fromByteBuffer).collect(Collectors.toList())); + break; + case 'n': + result.n(readString(in)); + break; + case 'N': + result.ns(readStringList(in)); + break; + case 's': + result.s(readString(in)); + break; + case 'S': + result.ss(readStringList(in)); + break; + case '?': + final byte boolValue = in.readByte(); + + if (boolValue == TRUE_FLAG) { + result.bool(Boolean.TRUE); + } else if (boolValue == FALSE_FLAG) { + result.bool(Boolean.FALSE); + } else { + throw new IllegalArgumentException("Improperly formatted data"); + } + break; + case 'L': + final int lCount = in.readInt(); + final List l = new ArrayList<>(lCount); + for (int lIdx = 0; lIdx < lCount; lIdx++) { + l.add(unmarshall(in)); + } + result.l(l); + break; + case 'M': + final int mCount = in.readInt(); + final Map m = new HashMap<>(); + for (int mIdx = 0; mIdx < mCount; mIdx++) { + final AttributeValue key = unmarshall(in); + if (key.s() == null) { + throw new IllegalArgumentException("Improperly formatted data"); + } + AttributeValue value = unmarshall(in); + m.put(key.s(), value); + } + result.m(m); + break; + default: + throw new IllegalArgumentException("Unsupported data encoding"); + } + + return result.build(); + } + + private static String trimZeros(final String n) { + BigDecimal number = new BigDecimal(n); + if (number.compareTo(BigDecimal.ZERO) == 0) { + return "0"; + } + return number.stripTrailingZeros().toPlainString(); + } + + private static void writeStringList(List values, final DataOutputStream out) throws IOException { + final List sorted = new ArrayList<>(values); + Collections.sort(sorted); + out.writeInt(sorted.size()); + for (final String v : sorted) { + writeString(v, out); + } + } + + private static List readStringList(final DataInputStream in) throws IOException, + IllegalArgumentException { + final int nCount = in.readInt(); + List ns = new ArrayList<>(nCount); + for (int nIdx = 0; nIdx < nCount; nIdx++) { + ns.add(readString(in)); + } + return ns; + } + + private static void writeString(String value, final DataOutputStream out) throws IOException { + final byte[] bytes = value.getBytes(UTF8); + out.writeInt(bytes.length); + out.write(bytes); + } + + private static String readString(final DataInputStream in) throws IOException, + IllegalArgumentException { + byte[] bytes; + int length; + length = in.readInt(); + bytes = new byte[length]; + if(in.read(bytes) != length) { + throw new IllegalArgumentException("Improperly formatted data"); + } + return new String(bytes, UTF8); + } + + private static void writeBytesList(List values, final DataOutputStream out) throws IOException { + final List sorted = new ArrayList<>(values); + Collections.sort(sorted); + out.writeInt(sorted.size()); + for (final ByteBuffer v : sorted) { + writeBytes(v, out); + } + } + + private static List readBytesList(final DataInputStream in) throws IOException { + final int bCount = in.readInt(); + List bs = new ArrayList<>(bCount); + for (int bIdx = 0; bIdx < bCount; bIdx++) { + bs.add(readBytes(in)); + } + return bs; + } + + private static void writeBytes(ByteBuffer value, final DataOutputStream out) throws IOException { + value = value.asReadOnlyBuffer(); + value.rewind(); + out.writeInt(value.remaining()); + while (value.hasRemaining()) { + out.writeByte(value.get()); + } + } + + private static ByteBuffer readBytes(final DataInputStream in) throws IOException { + final int length = in.readInt(); + final byte[] buf = new byte[length]; + in.readFully(buf); + return ByteBuffer.wrap(buf); + } + + /** + * Determines if the value of a 'set' type AttributeValue (various S types) has been explicitly set or not. + * @param value the actual value portion of an AttributeValue of the appropriate type + * @return true if the value of this type field has been explicitly set, false if it has not + */ + private static boolean hasAttributeValueSet(Collection value) { + return value != null && value != DefaultSdkAutoConstructList.getInstance(); + } + + /** + * Determines if the value of a 'map' type AttributeValue (M type) has been explicitly set or not. + * @param value the actual value portion of a AttributeValue of the appropriate type + * @return true if the value of this type field has been explicitly set, false if it has not + */ + private static boolean hasAttributeValueMap(Map value) { + return value != null && value != DefaultSdkAutoConstructMap.getInstance(); + } + +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/Base64.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/Base64.java new file mode 100644 index 000000000..ee94a86a0 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/Base64.java @@ -0,0 +1,48 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.internal; + +import static java.util.Base64.*; + +/** + * A class for decoding Base64 strings and encoding bytes as Base64 strings. + */ +public class Base64 { + private static final Decoder DECODER = getMimeDecoder(); + private static final Encoder ENCODER = getEncoder(); + + private Base64() { } + + /** + * Encode the bytes as a Base64 string. + *

+ * See the Basic encoder in {@link java.util.Base64} + */ + public static String encodeToString(byte[] bytes) { + return ENCODER.encodeToString(bytes); + } + + /** + * Decode the Base64 string as bytes, ignoring illegal characters. + *

+ * See the Mime Decoder in {@link java.util.Base64} + */ + public static byte[] decode(String str) { + if(str == null) { + return null; + } + return DECODER.decode(str); + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/ByteBufferInputStream.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/ByteBufferInputStream.java new file mode 100644 index 000000000..ff7030684 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/ByteBufferInputStream.java @@ -0,0 +1,56 @@ +/* + * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.internal; + +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * @author Greg Rubin + */ +public class ByteBufferInputStream extends InputStream { + private final ByteBuffer buffer; + + public ByteBufferInputStream(ByteBuffer buffer) { + this.buffer = buffer; + } + + @Override + public int read() { + if (buffer.hasRemaining()) { + int tmp = buffer.get(); + if (tmp < 0) { + tmp += 256; + } + return tmp; + } else { + return -1; + } + } + + @Override + public int read(byte[] b, int off, int len) { + if (available() < len) { + len = available(); + } + buffer.get(b, off, len); + return len; + } + + @Override + public int available() { + return buffer.remaining(); + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/Hkdf.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/Hkdf.java new file mode 100644 index 000000000..15422aaab --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/Hkdf.java @@ -0,0 +1,316 @@ +/* + * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.internal; + +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Provider; +import java.util.Arrays; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.SecretKeySpec; + +/** + * HMAC-based Key Derivation Function. + * + * @see RFC 5869 + */ +public final class Hkdf { + private static final byte[] EMPTY_ARRAY = new byte[0]; + private final String algorithm; + private final Provider provider; + + private SecretKey prk = null; + + /** + * Returns an Hkdf object using the specified algorithm. + * + * @param algorithm + * the standard name of the requested MAC algorithm. See the Mac + * section in the Java Cryptography Architecture Standard Algorithm Name + * Documentation for information about standard algorithm + * names. + * @return the new Hkdf object + * @throws NoSuchAlgorithmException + * if no Provider supports a MacSpi implementation for the + * specified algorithm. + */ + public static Hkdf getInstance(final String algorithm) + throws NoSuchAlgorithmException { + // Constructed specifically to sanity-test arguments. + Mac mac = Mac.getInstance(algorithm); + return new Hkdf(algorithm, mac.getProvider()); + } + + /** + * Returns an Hkdf object using the specified algorithm. + * + * @param algorithm + * the standard name of the requested MAC algorithm. See the Mac + * section in the Java Cryptography Architecture Standard Algorithm Name + * Documentation for information about standard algorithm + * names. + * @param provider + * the name of the provider + * @return the new Hkdf object + * @throws NoSuchAlgorithmException + * if a MacSpi implementation for the specified algorithm is not + * available from the specified provider. + * @throws NoSuchProviderException + * if the specified provider is not registered in the security + * provider list. + */ + public static Hkdf getInstance(final String algorithm, final String provider) + throws NoSuchAlgorithmException, NoSuchProviderException { + // Constructed specifically to sanity-test arguments. + Mac mac = Mac.getInstance(algorithm, provider); + return new Hkdf(algorithm, mac.getProvider()); + } + + /** + * Returns an Hkdf object using the specified algorithm. + * + * @param algorithm + * the standard name of the requested MAC algorithm. See the Mac + * section in the Java Cryptography Architecture Standard Algorithm Name + * Documentation for information about standard algorithm + * names. + * @param provider + * the provider + * @return the new Hkdf object + * @throws NoSuchAlgorithmException + * if a MacSpi implementation for the specified algorithm is not + * available from the specified provider. + */ + public static Hkdf getInstance(final String algorithm, + final Provider provider) throws NoSuchAlgorithmException { + // Constructed specifically to sanity-test arguments. + Mac mac = Mac.getInstance(algorithm, provider); + return new Hkdf(algorithm, mac.getProvider()); + } + + /** + * Initializes this Hkdf with input keying material. A default salt of + * HashLen zeros will be used (where HashLen is the length of the return + * value of the supplied algorithm). + * + * @param ikm + * the Input Keying Material + */ + public void init(final byte[] ikm) { + init(ikm, null); + } + + /** + * Initializes this Hkdf with input keying material and a salt. If + * salt is null or of length 0, then a default salt of + * HashLen zeros will be used (where HashLen is the length of the return + * value of the supplied algorithm). + * + * @param salt + * the salt used for key extraction (optional) + * @param ikm + * the Input Keying Material + */ + public void init(final byte[] ikm, final byte[] salt) { + byte[] realSalt = (salt == null) ? EMPTY_ARRAY : salt.clone(); + byte[] rawKeyMaterial = EMPTY_ARRAY; + try { + Mac extractionMac = Mac.getInstance(algorithm, provider); + if (realSalt.length == 0) { + realSalt = new byte[extractionMac.getMacLength()]; + Arrays.fill(realSalt, (byte) 0); + } + extractionMac.init(new SecretKeySpec(realSalt, algorithm)); + rawKeyMaterial = extractionMac.doFinal(ikm); + SecretKeySpec key = new SecretKeySpec(rawKeyMaterial, algorithm); + Arrays.fill(rawKeyMaterial, (byte) 0); // Zeroize temporary array + unsafeInitWithoutKeyExtraction(key); + } catch (GeneralSecurityException e) { + // We've already checked all of the parameters so no exceptions + // should be possible here. + throw new RuntimeException("Unexpected exception", e); + } finally { + Arrays.fill(rawKeyMaterial, (byte) 0); // Zeroize temporary array + } + } + + /** + * Initializes this Hkdf to use the provided key directly for creation of + * new keys. If rawKey is not securely generated and uniformly + * distributed over the total key-space, then this will result in an + * insecure key derivation function (KDF). DO NOT USE THIS UNLESS YOU + * ARE ABSOLUTELY POSITIVE THIS IS THE CORRECT THING TO DO. + * + * @param rawKey + * the pseudorandom key directly used to derive keys + * @throws InvalidKeyException + * if the algorithm for rawKey does not match the + * algorithm this Hkdf was created with + */ + public void unsafeInitWithoutKeyExtraction(final SecretKey rawKey) + throws InvalidKeyException { + if (!rawKey.getAlgorithm().equals(algorithm)) { + throw new InvalidKeyException( + "Algorithm for the provided key must match the algorithm for this Hkdf. Expected " + + algorithm + " but found " + rawKey.getAlgorithm()); + } + + this.prk = rawKey; + } + + private Hkdf(final String algorithm, final Provider provider) { + if (!algorithm.startsWith("Hmac")) { + throw new IllegalArgumentException("Invalid algorithm " + algorithm + + ". Hkdf may only be used with Hmac algorithms."); + } + this.algorithm = algorithm; + this.provider = provider; + } + + /** + * Returns a pseudorandom key of length bytes. + * + * @param info + * optional context and application specific information (can be + * a zero-length string). This will be treated as UTF-8. + * @param length + * the length of the output key in bytes + * @return a pseudorandom key of length bytes. + * @throws IllegalStateException + * if this object has not been initialized + */ + public byte[] deriveKey(final String info, final int length) throws IllegalStateException { + return deriveKey((info != null ? info.getBytes(StandardCharsets.UTF_8) : null), length); + } + + /** + * Returns a pseudorandom key of length bytes. + * + * @param info + * optional context and application specific information (can be + * a zero-length array). + * @param length + * the length of the output key in bytes + * @return a pseudorandom key of length bytes. + * @throws IllegalStateException + * if this object has not been initialized + */ + public byte[] deriveKey(final byte[] info, final int length) throws IllegalStateException { + byte[] result = new byte[length]; + try { + deriveKey(info, length, result, 0); + } catch (ShortBufferException ex) { + // This exception is impossible as we ensure the buffer is long + // enough + throw new RuntimeException(ex); + } + return result; + } + + /** + * Derives a pseudorandom key of length bytes and stores the + * result in output. + * + * @param info + * optional context and application specific information (can be + * a zero-length array). + * @param length + * the length of the output key in bytes + * @param output + * the buffer where the pseudorandom key will be stored + * @param offset + * the offset in output where the key will be stored + * @throws ShortBufferException + * if the given output buffer is too small to hold the result + * @throws IllegalStateException + * if this object has not been initialized + */ + public void deriveKey(final byte[] info, final int length, + final byte[] output, final int offset) throws ShortBufferException, + IllegalStateException { + assertInitialized(); + if (length < 0) { + throw new IllegalArgumentException("Length must be a non-negative value."); + } + if (output.length < offset + length) { + throw new ShortBufferException(); + } + Mac mac = createMac(); + + if (length > 255 * mac.getMacLength()) { + throw new IllegalArgumentException( + "Requested keys may not be longer than 255 times the underlying HMAC length."); + } + + byte[] t = EMPTY_ARRAY; + try { + int loc = 0; + byte i = 1; + while (loc < length) { + mac.update(t); + mac.update(info); + mac.update(i); + t = mac.doFinal(); + + for (int x = 0; x < t.length && loc < length; x++, loc++) { + output[loc] = t[x]; + } + + i++; + } + } finally { + Arrays.fill(t, (byte) 0); // Zeroize temporary array + } + } + + private Mac createMac() { + try { + Mac mac = Mac.getInstance(algorithm, provider); + mac.init(prk); + return mac; + } catch (NoSuchAlgorithmException ex) { + // We've already validated that this algorithm is correct. + throw new RuntimeException(ex); + } catch (InvalidKeyException ex) { + // We've already validated that this key is correct. + throw new RuntimeException(ex); + } + } + + /** + * Throws an IllegalStateException if this object has not been + * initialized. + * + * @throws IllegalStateException + * if this object has not been initialized + */ + private void assertInitialized() throws IllegalStateException { + if (prk == null) { + throw new IllegalStateException("Hkdf has not been initialized"); + } + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/LRUCache.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/LRUCache.java new file mode 100644 index 000000000..e191a8421 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/LRUCache.java @@ -0,0 +1,107 @@ +/* + * Copyright 2015-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.internal; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +import software.amazon.awssdk.annotations.ThreadSafe; + +/** + * A bounded cache that has a LRU eviction policy when the cache is full. + * + * @param + * value type + */ +@ThreadSafe +public final class LRUCache { + /** + * Used for the internal cache. + */ + private final Map map; + + /** + * Maximum size of the cache. + */ + private final int maxSize; + + /** + * @param maxSize + * the maximum number of entries of the cache + */ + public LRUCache(final int maxSize) { + if (maxSize < 1) { + throw new IllegalArgumentException("maxSize " + maxSize + " must be at least 1"); + } + this.maxSize = maxSize; + map = Collections.synchronizedMap(new LRUHashMap(maxSize)); + } + + /** + * Adds an entry to the cache, evicting the earliest entry if necessary. + */ + public T add(final String key, final T value) { + return map.put(key, value); + } + + /** Returns the value of the given key; or null of no such entry exists. */ + public T get(final String key) { + return map.get(key); + } + + /** + * Returns the current size of the cache. + */ + public int size() { + return map.size(); + } + + /** + * Returns the maximum size of the cache. + */ + public int getMaxSize() { + return maxSize; + } + + public void clear() { + map.clear(); + } + + public T remove(String key) { + return map.remove(key); + } + + @Override + public String toString() { + return map.toString(); + } + + @SuppressWarnings("serial") + private static class LRUHashMap extends LinkedHashMap { + private final int maxSize; + + private LRUHashMap(final int maxSize) { + super(10, 0.75F, true); + this.maxSize = maxSize; + } + + @Override + protected boolean removeEldestEntry(final Entry eldest) { + return size() > maxSize; + } + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/MsClock.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/MsClock.java new file mode 100644 index 000000000..3d776c0dc --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/MsClock.java @@ -0,0 +1,19 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.internal; + +interface MsClock { + MsClock WALLCLOCK = System::nanoTime; + + public long timestampNano(); +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/TTLCache.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/TTLCache.java new file mode 100644 index 000000000..f529047c8 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/TTLCache.java @@ -0,0 +1,242 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.cryptools.dynamodbencryptionclientsdk2.internal; + +import io.netty.util.internal.ObjectUtil; +import software.amazon.awssdk.annotations.ThreadSafe; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; + +/** + * A cache, backed by an LRUCache, that uses a loader to calculate values on cache miss or expired + * TTL. + * + *

Note that this cache does not proactively evict expired entries, however will immediately + * evict entries discovered to be expired on load. + * + * @param value type + */ +@ThreadSafe +public final class TTLCache { + /** Used for the internal cache. */ + private final LRUCache> cache; + + /** Time to live for entries in the cache. */ + private final long ttlInNanos; + + /** Used for loading new values into the cache on cache miss or expiration. */ + private final EntryLoader defaultLoader; + + // Mockable time source, to allow us to test TTL behavior. + // package access for tests + MsClock clock = MsClock.WALLCLOCK; + + private static final long TTL_GRACE_IN_NANO = TimeUnit.MILLISECONDS.toNanos(500); + + /** + * @param maxSize the maximum number of entries of the cache + * @param ttlInMillis the time to live value for entries of the cache, in milliseconds + */ + public TTLCache(final int maxSize, final long ttlInMillis, final EntryLoader loader) { + if (maxSize < 1) { + throw new IllegalArgumentException("maxSize " + maxSize + " must be at least 1"); + } + if (ttlInMillis < 1) { + throw new IllegalArgumentException("ttlInMillis " + maxSize + " must be at least 1"); + } + this.ttlInNanos = TimeUnit.MILLISECONDS.toNanos(ttlInMillis); + this.cache = new LRUCache<>(maxSize); + this.defaultLoader = ObjectUtil.checkNotNull(loader, "loader must not be null"); + } + + /** + * Uses the default loader to calculate the value at key and insert it into the cache, if it + * doesn't already exist or is expired according to the TTL. + * + *

This immediately evicts entries past the TTL such that a load failure results in the removal + * of the entry. + * + *

Entries that are not expired according to the TTL are returned without recalculating the + * value. + * + *

Within a grace period past the TTL, the cache may either return the cached value without + * recalculating or use the loader to recalculate the value. This is implemented such that, in a + * multi-threaded environment, only one thread per cache key uses the loader to recalculate the + * value at one time. + * + * @param key The cache key to load the value at + * @return The value of the given value (already existing or re-calculated). + */ + public T load(final String key) { + return load(key, defaultLoader::load); + } + + /** + * Uses the inputted function to calculate the value at key and insert it into the cache, if it + * doesn't already exist or is expired according to the TTL. + * + *

This immediately evicts entries past the TTL such that a load failure results in the removal + * of the entry. + * + *

Entries that are not expired according to the TTL are returned without recalculating the + * value. + * + *

Within a grace period past the TTL, the cache may either return the cached value without + * recalculating or use the loader to recalculate the value. This is implemented such that, in a + * multi-threaded environment, only one thread per cache key uses the loader to recalculate the + * value at one time. + * + *

Returns the value of the given key (already existing or re-calculated). + * + * @param key The cache key to load the value at + * @param f The function to use to load the value, given key as input + * @return The value of the given value (already existing or re-calculated). + */ + public T load(final String key, Function f) { + final LockedState ls = cache.get(key); + + if (ls == null) { + // The entry doesn't exist yet, so load a new one. + return loadNewEntryIfAbsent(key, f); + } else if (clock.timestampNano() - ls.getState().lastUpdatedNano + > ttlInNanos + TTL_GRACE_IN_NANO) { + // The data has expired past the grace period. + // Evict the old entry and load a new entry. + cache.remove(key); + return loadNewEntryIfAbsent(key, f); + } else if (clock.timestampNano() - ls.getState().lastUpdatedNano <= ttlInNanos) { + // The data hasn't expired. Return as-is from the cache. + return ls.getState().data; + } else if (!ls.tryLock()) { + // We are in the TTL grace period. If we couldn't grab the lock, then some other + // thread is currently loading the new value. Because we are in the grace period, + // use the cached data instead of waiting for the lock. + return ls.getState().data; + } + + // We are in the grace period and have acquired a lock. + // Update the cache with the value determined by the loading function. + try { + T loadedData = f.apply(key); + ls.update(loadedData, clock.timestampNano()); + return ls.getState().data; + } finally { + ls.unlock(); + } + } + + // Synchronously calculate the value for a new entry in the cache if it doesn't already exist. + // Otherwise return the cached value. + // It is important that this is the only place where we use the loader for a new entry, + // given that we don't have the entry yet to lock on. + // This ensures that the loading function is only called once if multiple threads + // attempt to add a new entry for the same key at the same time. + private synchronized T loadNewEntryIfAbsent(final String key, Function f) { + // If the entry already exists in the cache, return it + final LockedState cachedState = cache.get(key); + if (cachedState != null) { + return cachedState.getState().data; + } + + // Otherwise, load the data and create a new entry + T loadedData = f.apply(key); + LockedState ls = new LockedState<>(loadedData, clock.timestampNano()); + cache.add(key, ls); + return loadedData; + } + + + /** + * Put a new entry in the cache. Returns the value previously at that key in the cache, or null if + * the entry previously didn't exist or is expired. + */ + public synchronized T put(final String key, final T value) { + LockedState ls = new LockedState<>(value, clock.timestampNano()); + LockedState oldLockedState = cache.add(key, ls); + if (oldLockedState == null + || clock.timestampNano() - oldLockedState.getState().lastUpdatedNano + > ttlInNanos + TTL_GRACE_IN_NANO) { + return null; + } + return oldLockedState.getState().data; + } + + /** + * Get when the entry at this key was last updated. Returns 0 if the entry doesn't exist at key. + */ + public long getLastUpdated(String key) { + LockedState ls = cache.get(key); + if (ls == null) { + return 0; + } + return ls.getState().lastUpdatedNano; + } + + /** Returns the current size of the cache. */ + public int size() { + return cache.size(); + } + + /** Returns the maximum size of the cache. */ + public int getMaxSize() { + return cache.getMaxSize(); + } + + /** Clears all entries from the cache. */ + public void clear() { + cache.clear(); + } + + @Override + public String toString() { + return cache.toString(); + } + + public interface EntryLoader { + T load(String entryKey); + } + + // An object which stores a state alongside a lock, + // and performs updates to that state atomically. + // The state may only be updated if the lock is acquired by the current thread. + private static class LockedState { + private final ReentrantLock lock = new ReentrantLock(true); + private final AtomicReference> state; + + public LockedState(T data, long createTimeNano) { + state = new AtomicReference<>(new State<>(data, createTimeNano)); + } + + public State getState() { + return state.get(); + } + + public void unlock() { + lock.unlock(); + } + + public boolean tryLock() { + return lock.tryLock(); + } + + public void update(T data, long createTimeNano) { + if (!lock.isHeldByCurrentThread()) { + throw new IllegalStateException("Lock not held by current thread"); + } + state.set(new State<>(data, createTimeNano)); + } + } + + // An object that holds some data and the time at which this object was created + private static class State { + public final T data; + public final long lastUpdatedNano; + + public State(T data, long lastUpdatedNano) { + this.data = data; + this.lastUpdatedNano = lastUpdatedNano; + } + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/Utils.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/Utils.java new file mode 100644 index 000000000..6d092cc06 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/Utils.java @@ -0,0 +1,39 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.internal; + +import java.security.SecureRandom; + +public class Utils { + private static final ThreadLocal RND = ThreadLocal.withInitial(() -> { + final SecureRandom result = new SecureRandom(); + result.nextBoolean(); // Force seeding + return result; + }); + + private Utils() { + // Prevent instantiation + } + + public static SecureRandom getRng() { + return RND.get(); + } + + public static byte[] getRandom(int len) { + final byte[] result = new byte[len]; + getRng().nextBytes(result); + return result; + } +} From 9cb306c775673c2b72cb738b8dbc4266df3a2441 Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Tue, 23 Dec 2025 13:06:16 -0800 Subject: [PATCH 7/7] auto commit --- DynamoDbEncryption/runtimes/java/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DynamoDbEncryption/runtimes/java/build.gradle.kts b/DynamoDbEncryption/runtimes/java/build.gradle.kts index 2bbd34588..08c1273e4 100644 --- a/DynamoDbEncryption/runtimes/java/build.gradle.kts +++ b/DynamoDbEncryption/runtimes/java/build.gradle.kts @@ -92,7 +92,7 @@ dependencies { testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.11.4") // For the DDB-EC v1 - implementation("com.amazonaws:aws-java-sdk-dynamodb:1.12.780") + compileOnly("com.amazonaws:aws-java-sdk-dynamodb:1.12.780") // https://mvnrepository.com/artifact/org.testng/testng testImplementation("org.testng:testng:7.5") // https://mvnrepository.com/artifact/com.amazonaws/DynamoDBLocal