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 0000000000..52e02f2e8e --- /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 0000000000..95e6ec73c7 --- /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 0000000000..d2998057b0 --- /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 0000000000..9a78ad9b04 --- /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: + *

+ * + * 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 0000000000..47329f7128 --- /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 0000000000..f245d66e31 --- /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 0000000000..5dfbb19709 --- /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 0000000000..003d0b60cc --- /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 0000000000..033d331f5b --- /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 0000000000..00f8548bc7 --- /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 0000000000..ecef9e9fc8 --- /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 0000000000..b3daab44ba --- /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 0000000000..fd17521ca1 --- /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 0000000000..b49e2b9a20 --- /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 0000000000..653e754c26 --- /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 0000000000..425a4119f2 --- /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 0000000000..b60fee3ee0 --- /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 0000000000..483b81b51a --- /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 0000000000..8a63a0328c --- /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 0000000000..1c92fb3f4a --- /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 0000000000..c0fbe5e06f --- /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 0000000000..a29fe9b34d --- /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 0000000000..d29bb818cb --- /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 0000000000..e9348af05d --- /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 0000000000..ee94a86a02 --- /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 0000000000..ff70306841 --- /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 0000000000..15422aaab7 --- /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 0000000000..e191a84215 --- /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 0000000000..3d776c0dc8 --- /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 0000000000..f529047c8a --- /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 0000000000..6d092cc06b --- /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; + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/HolisticIT.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/HolisticIT.java new file mode 100644 index 0000000000..b9906bade0 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/HolisticIT.java @@ -0,0 +1,932 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.cryptools.dynamodbencryptionclientsdk2; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertTrue; + +import com.amazonaws.util.Base64; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.*; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.*; +import software.amazon.awssdk.services.kms.KmsClient; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.DynamoDbEncryptor; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.EncryptionContext; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.EncryptionFlags; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.AsymmetricStaticProvider; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.CachingMostRecentProvider; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.DirectKmsMaterialsProvider; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.EncryptionMaterialsProvider; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.SymmetricStaticProvider; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.WrappedMaterialsProvider; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.store.MetaStore; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.store.ProviderStore; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.testing.*; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.testing.ScenarioManifest.KeyData; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.testing.ScenarioManifest.Keys; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.testing.ScenarioManifest.Scenario; + +public class HolisticIT { + + private static final SecretKey aesKey = + new SecretKeySpec(new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, "AES"); + private static final SecretKey hmacKey = + new SecretKeySpec(new byte[] {0, 1, 2, 3, 4, 5, 6, 7}, "HmacSHA256"); + private static final String rsaEncPub = + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtiNSLSvT9cExXOcD0dGZ" + + "9DFEMHw8895gAZcCdSppDrxbD7XgZiQYTlgt058i5fS+l11guAUJtKt5sZ2u8Fx0" + + "K9pxMdlczGtvQJdx/LQETEnLnfzAijvHisJ8h6dQOVczM7t01KIkS24QZElyO+kY" + + "qMWLytUV4RSHnrnIuUtPHCe6LieDWT2+1UBguxgtFt1xdXlquACLVv/Em3wp40Xc" + + "bIwzhqLitb98rTY/wqSiGTz1uvvBX46n+f2j3geZKCEDGkWcXYw3dH4lRtDWTbqw" + + "eRcaNDT/MJswQlBk/Up9KCyN7gjX67gttiCO6jMoTNDejGeJhG4Dd2o0vmn8WJlr" + + "5wIDAQAB"; + private static final String rsaEncPriv = + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC2I1ItK9P1wTFc" + + "5wPR0Zn0MUQwfDzz3mABlwJ1KmkOvFsPteBmJBhOWC3TnyLl9L6XXWC4BQm0q3mx" + + "na7wXHQr2nEx2VzMa29Al3H8tARMScud/MCKO8eKwnyHp1A5VzMzu3TUoiRLbhBk" + + "SXI76RioxYvK1RXhFIeeuci5S08cJ7ouJ4NZPb7VQGC7GC0W3XF1eWq4AItW/8Sb" + + "fCnjRdxsjDOGouK1v3ytNj/CpKIZPPW6+8Ffjqf5/aPeB5koIQMaRZxdjDd0fiVG" + + "0NZNurB5Fxo0NP8wmzBCUGT9Sn0oLI3uCNfruC22II7qMyhM0N6MZ4mEbgN3ajS+" + + "afxYmWvnAgMBAAECggEBAIIU293zDWDZZ73oJ+w0fHXQsdjHAmlRitPX3CN99KZX" + + "k9m2ldudL9bUV3Zqk2wUzgIg6LDEuFfWmAVojsaP4VBopKtriEFfAYfqIbjPgLpT" + + "gh8FoyWW6D6MBJCFyGALjUAHQ7uRScathvt5ESMEqV3wKJTmdsfX97w/B8J+rLN3" + + "3fT3ZJUck5duZ8XKD+UtX1Y3UE1hTWo3Ae2MFND964XyUqy+HaYXjH0x6dhZzqyJ" + + "/OJ/MPGeMJgxp+nUbMWerwxrLQceNFVgnQgHj8e8k4fd04rkowkkPua912gNtmz7" + + "DuIEvcMnY64z585cn+cnXUPJwtu3JbAmn/AyLsV9FLECgYEA798Ut/r+vORB16JD" + + "KFu38pQCgIbdCPkXeI0DC6u1cW8JFhgRqi+AqSrEy5SzY3IY7NVMSRsBI9Y026Bl" + + "R9OQwTrOzLRAw26NPSDvbTkeYXlY9+hX7IovHjGkho/OxyTJ7bKRDYLoNCz56BC1" + + "khIWvECpcf/fZU0nqOFVFqF3H/UCgYEAwmJ4rjl5fksTNtNRL6ivkqkHIPKXzk5w" + + "C+L90HKNicic9bqyX8K4JRkGKSNYN3mkjrguAzUlEld390qNBw5Lu7PwATv0e2i+" + + "6hdwJsjTKNpj7Nh4Mieq6d7lWe1L8FLyHEhxgIeQ4BgqrVtPPOH8IBGpuzVZdWwI" + + "dgOvEvAi/usCgYBdfk3NB/+SEEW5jn0uldE0s4vmHKq6fJwxWIT/X4XxGJ4qBmec" + + "NbeoOAtMbkEdWbNtXBXHyMbA+RTRJctUG5ooNou0Le2wPr6+PMAVilXVGD8dIWpj" + + "v9htpFvENvkZlbU++IKhCY0ICR++3ARpUrOZ3Hou/NRN36y9nlZT48tSoQKBgES2" + + "Bi6fxmBsLUiN/f64xAc1lH2DA0I728N343xRYdK4hTMfYXoUHH+QjurvwXkqmI6S" + + "cEFWAdqv7IoPYjaCSSb6ffYRuWP+LK4WxuAO0QV53SSViDdCalntHmlhRhyXVVnG" + + "CckDIqT0JfHNev7savDzDWpNe2fUXlFJEBPDqrstAoGBAOpd5+QBHF/tP5oPILH4" + + "aD/zmqMH7VtB+b/fOPwtIM+B/WnU7hHLO5t2lJYu18Be3amPkfoQIB7bpkM3Cer2" + + "G7Jw+TcHrY+EtIziDB5vwau1fl4VcbA9SfWpBojJ5Ifo9ELVxGiK95WxeQNSmLUy" + + "7AJzhK1Gwey8a/v+xfqiu9sE"; + private static final PrivateKey rsaPriv; + private static final PublicKey rsaPub; + private static final KeyPair rsaPair; + private static final EncryptionMaterialsProvider symProv; + private static final EncryptionMaterialsProvider asymProv; + private static final EncryptionMaterialsProvider symWrappedProv; + private static final String HASH_KEY = "hashKey"; + private static final String RANGE_KEY = "rangeKey"; + private static final String RSA = "RSA"; + private static final String tableName = "TableName"; + final EnumSet signOnly = EnumSet.of(EncryptionFlags.SIGN); + final EnumSet encryptAndSign = + EnumSet.of(EncryptionFlags.ENCRYPT, EncryptionFlags.SIGN); + + private final LocalDynamoDb localDynamoDb = new LocalDynamoDb(); + private DynamoDbClient client; + private static KmsClient kmsClient = KmsClient.builder().build(); + + private static Map keyDataMap = new HashMap<>(); + + private static final Map ENCRYPTED_TEST_VALUE = new HashMap<>(); + private static final Map MIXED_TEST_VALUE = new HashMap<>(); + private static final Map SIGNED_TEST_VALUE = new HashMap<>(); + private static final Map UNTOUCHED_TEST_VALUE = new HashMap<>(); + + private static final Map ENCRYPTED_TEST_VALUE_2 = new HashMap<>(); + private static final Map MIXED_TEST_VALUE_2 = new HashMap<>(); + private static final Map SIGNED_TEST_VALUE_2 = new HashMap<>(); + private static final Map UNTOUCHED_TEST_VALUE_2 = new HashMap<>(); + + private static final String TEST_VECTOR_MANIFEST_DIR = "/vectors/encrypted_item/"; + private static final String SCENARIO_MANIFEST_PATH = TEST_VECTOR_MANIFEST_DIR + "scenarios.json"; + private static final String JAVA_DIR = "java"; + + static { + try { + KeyFactory rsaFact = KeyFactory.getInstance("RSA"); + rsaPub = rsaFact.generatePublic(new X509EncodedKeySpec(Base64.decode(rsaEncPub))); + rsaPriv = rsaFact.generatePrivate(new PKCS8EncodedKeySpec(Base64.decode(rsaEncPriv))); + rsaPair = new KeyPair(rsaPub, rsaPriv); + } catch (GeneralSecurityException ex) { + throw new RuntimeException(ex); + } + symProv = new SymmetricStaticProvider(aesKey, hmacKey); + asymProv = new AsymmetricStaticProvider(rsaPair, rsaPair); + symWrappedProv = new WrappedMaterialsProvider(aesKey, aesKey, hmacKey); + + ENCRYPTED_TEST_VALUE.put("hashKey", AttributeValue.builder().n("5").build()); + ENCRYPTED_TEST_VALUE.put("rangeKey", AttributeValue.builder().n("7").build()); + ENCRYPTED_TEST_VALUE.put("version", AttributeValue.builder().n("0").build()); + ENCRYPTED_TEST_VALUE.put("intValue", AttributeValue.builder().n("123").build()); + ENCRYPTED_TEST_VALUE.put("stringValue", AttributeValue.builder().s("Hello world!").build()); + ENCRYPTED_TEST_VALUE.put( + "byteArrayValue", + AttributeValue.builder().b(SdkBytes.fromByteArray(new byte[] {0, 1, 2, 3, 4, 5})).build()); + ENCRYPTED_TEST_VALUE.put( + "stringSet", + AttributeValue.builder() + .ss(new HashSet<>(Arrays.asList("Goodbye", "Cruel", "World", "?"))) + .build()); + ENCRYPTED_TEST_VALUE.put( + "intSet", + AttributeValue.builder() + .ns(new HashSet<>(Arrays.asList("1", "200", "10", "15", "0"))) + .build()); + + MIXED_TEST_VALUE.put("hashKey", AttributeValue.builder().n("6").build()); + MIXED_TEST_VALUE.put("rangeKey", AttributeValue.builder().n("8").build()); + MIXED_TEST_VALUE.put("version", AttributeValue.builder().n("0").build()); + MIXED_TEST_VALUE.put("intValue", AttributeValue.builder().n("123").build()); + MIXED_TEST_VALUE.put("stringValue", AttributeValue.builder().s("Hello world!").build()); + MIXED_TEST_VALUE.put( + "byteArrayValue", + AttributeValue.builder().b(SdkBytes.fromByteArray(new byte[] {0, 1, 2, 3, 4, 5})).build()); + MIXED_TEST_VALUE.put( + "stringSet", + AttributeValue.builder() + .ss(new HashSet<>(Arrays.asList("Goodbye", "Cruel", "World", "?"))) + .build()); + MIXED_TEST_VALUE.put( + "intSet", + AttributeValue.builder() + .ns(new HashSet<>(Arrays.asList("1", "200", "10", "15", "0"))) + .build()); + + SIGNED_TEST_VALUE.put("hashKey", AttributeValue.builder().n("8").build()); + SIGNED_TEST_VALUE.put("rangeKey", AttributeValue.builder().n("10").build()); + SIGNED_TEST_VALUE.put("version", AttributeValue.builder().n("0").build()); + SIGNED_TEST_VALUE.put("intValue", AttributeValue.builder().n("123").build()); + SIGNED_TEST_VALUE.put("stringValue", AttributeValue.builder().s("Hello world!").build()); + SIGNED_TEST_VALUE.put( + "byteArrayValue", + AttributeValue.builder().b(SdkBytes.fromByteArray(new byte[] {0, 1, 2, 3, 4, 5})).build()); + SIGNED_TEST_VALUE.put( + "stringSet", + AttributeValue.builder() + .ss(new HashSet<>(Arrays.asList("Goodbye", "Cruel", "World", "?"))) + .build()); + SIGNED_TEST_VALUE.put( + "intSet", + AttributeValue.builder() + .ns(new HashSet<>(Arrays.asList("1", "200", "10", "15", "0"))) + .build()); + + UNTOUCHED_TEST_VALUE.put("hashKey", AttributeValue.builder().n("7").build()); + UNTOUCHED_TEST_VALUE.put("rangeKey", AttributeValue.builder().n("9").build()); + UNTOUCHED_TEST_VALUE.put("version", AttributeValue.builder().n("0").build()); + UNTOUCHED_TEST_VALUE.put("intValue", AttributeValue.builder().n("123").build()); + UNTOUCHED_TEST_VALUE.put("stringValue", AttributeValue.builder().s("Hello world!").build()); + UNTOUCHED_TEST_VALUE.put( + "byteArrayValue", + AttributeValue.builder().b(SdkBytes.fromByteArray(new byte[] {0, 1, 2, 3, 4, 5})).build()); + UNTOUCHED_TEST_VALUE.put( + "stringSet", + AttributeValue.builder() + .ss(new HashSet(Arrays.asList("Goodbye", "Cruel", "World", "?"))) + .build()); + UNTOUCHED_TEST_VALUE.put( + "intSet", + AttributeValue.builder() + .ns(new HashSet(Arrays.asList("1", "200", "10", "15", "0"))) + .build()); + + // STORING DOUBLES + ENCRYPTED_TEST_VALUE_2.put("hashKey", AttributeValue.builder().n("5").build()); + ENCRYPTED_TEST_VALUE_2.put("rangeKey", AttributeValue.builder().n("7").build()); + ENCRYPTED_TEST_VALUE_2.put("version", AttributeValue.builder().n("0").build()); + ENCRYPTED_TEST_VALUE_2.put("intValue", AttributeValue.builder().n("123").build()); + ENCRYPTED_TEST_VALUE_2.put("stringValue", AttributeValue.builder().s("Hello world!").build()); + ENCRYPTED_TEST_VALUE_2.put( + "byteArrayValue", + AttributeValue.builder().b(SdkBytes.fromByteArray(new byte[] {0, 1, 2, 3, 4, 5})).build()); + ENCRYPTED_TEST_VALUE_2.put( + "stringSet", + AttributeValue.builder() + .ss(new HashSet<>(Arrays.asList("Goodbye", "Cruel", "World", "?"))) + .build()); + ENCRYPTED_TEST_VALUE_2.put( + "intSet", + AttributeValue.builder() + .ns(new HashSet<>(Arrays.asList("1", "200", "10", "15", "0"))) + .build()); + ENCRYPTED_TEST_VALUE_2.put( + "doubleValue", AttributeValue.builder().n(String.valueOf(15)).build()); + ENCRYPTED_TEST_VALUE_2.put( + "doubleSet", + AttributeValue.builder() + .ns(new HashSet(Arrays.asList("15", "7.6", "-3", "-34.2", "0"))) + .build()); + + MIXED_TEST_VALUE_2.put("hashKey", AttributeValue.builder().n("6").build()); + MIXED_TEST_VALUE_2.put("rangeKey", AttributeValue.builder().n("8").build()); + MIXED_TEST_VALUE_2.put("version", AttributeValue.builder().n("0").build()); + MIXED_TEST_VALUE_2.put("intValue", AttributeValue.builder().n("123").build()); + MIXED_TEST_VALUE_2.put("stringValue", AttributeValue.builder().s("Hello world!").build()); + MIXED_TEST_VALUE_2.put( + "byteArrayValue", + AttributeValue.builder().b(SdkBytes.fromByteArray(new byte[] {0, 1, 2, 3, 4, 5})).build()); + MIXED_TEST_VALUE_2.put( + "stringSet", + AttributeValue.builder() + .ss(new HashSet<>(Arrays.asList("Goodbye", "Cruel", "World", "?"))) + .build()); + MIXED_TEST_VALUE_2.put( + "intSet", + AttributeValue.builder() + .ns(new HashSet<>(Arrays.asList("1", "200", "10", "15", "0"))) + .build()); + MIXED_TEST_VALUE_2.put("doubleValue", AttributeValue.builder().n(String.valueOf(15)).build()); + MIXED_TEST_VALUE_2.put( + "doubleSet", + AttributeValue.builder() + .ns(new HashSet(Arrays.asList("15", "7.6", "-3", "-34.2", "0"))) + .build()); + + SIGNED_TEST_VALUE_2.put("hashKey", AttributeValue.builder().n("8").build()); + SIGNED_TEST_VALUE_2.put("rangeKey", AttributeValue.builder().n("10").build()); + SIGNED_TEST_VALUE_2.put("version", AttributeValue.builder().n("0").build()); + SIGNED_TEST_VALUE_2.put("intValue", AttributeValue.builder().n("123").build()); + SIGNED_TEST_VALUE_2.put("stringValue", AttributeValue.builder().s("Hello world!").build()); + SIGNED_TEST_VALUE_2.put( + "byteArrayValue", + AttributeValue.builder().b(SdkBytes.fromByteArray(new byte[] {0, 1, 2, 3, 4, 5})).build()); + SIGNED_TEST_VALUE_2.put( + "stringSet", + AttributeValue.builder() + .ss(new HashSet<>(Arrays.asList("Goodbye", "Cruel", "World", "?"))) + .build()); + SIGNED_TEST_VALUE_2.put( + "intSet", + AttributeValue.builder() + .ns(new HashSet<>(Arrays.asList("1", "200", "10", "15", "0"))) + .build()); + SIGNED_TEST_VALUE_2.put("doubleValue", AttributeValue.builder().n(String.valueOf(15)).build()); + SIGNED_TEST_VALUE_2.put( + "doubleSet", + AttributeValue.builder() + .ns(new HashSet(Arrays.asList("15", "7.6", "-3", "-34.2", "0"))) + .build()); + + UNTOUCHED_TEST_VALUE_2.put("hashKey", AttributeValue.builder().n("7").build()); + UNTOUCHED_TEST_VALUE_2.put("rangeKey", AttributeValue.builder().n("9").build()); + UNTOUCHED_TEST_VALUE_2.put("version", AttributeValue.builder().n("0").build()); + UNTOUCHED_TEST_VALUE_2.put("intValue", AttributeValue.builder().n("123").build()); + UNTOUCHED_TEST_VALUE_2.put("stringValue", AttributeValue.builder().s("Hello world!").build()); + UNTOUCHED_TEST_VALUE_2.put( + "byteArrayValue", + AttributeValue.builder().b(SdkBytes.fromByteArray(new byte[] {0, 1, 2, 3, 4, 5})).build()); + UNTOUCHED_TEST_VALUE_2.put( + "stringSet", + AttributeValue.builder() + .ss(new HashSet(Arrays.asList("Goodbye", "Cruel", "World", "?"))) + .build()); + UNTOUCHED_TEST_VALUE_2.put( + "intSet", + AttributeValue.builder() + .ns(new HashSet(Arrays.asList("1", "200", "10", "15", "0"))) + .build()); + UNTOUCHED_TEST_VALUE_2.put( + "doubleValue", AttributeValue.builder().n(String.valueOf(15)).build()); + UNTOUCHED_TEST_VALUE_2.put( + "doubleSet", + AttributeValue.builder() + .ns(new HashSet(Arrays.asList("15", "7.6", "-3", "-34.2", "0"))) + .build()); + } + + @DataProvider(name = "getEncryptTestVectors") + public static Object[][] getEncryptTestVectors() throws IOException { + ScenarioManifest scenarioManifest = + getManifestFromFile(SCENARIO_MANIFEST_PATH, new TypeReference() {}); + loadKeyData(scenarioManifest.keyDataPath); + + // Only use Java generated test vectors to dedupe the scenarios for encrypt, + // we only care that we are able to generate data using the different provider configurations + return scenarioManifest.scenarios.stream() + .filter(s -> s.ciphertextPath.contains(JAVA_DIR)) + .map(s -> new Object[] {s}) + .toArray(Object[][]::new); + } + + @DataProvider(name = "getDecryptTestVectors") + public static Object[][] getDecryptTestVectors() throws IOException { + ScenarioManifest scenarioManifest = + getManifestFromFile(SCENARIO_MANIFEST_PATH, new TypeReference() {}); + loadKeyData(scenarioManifest.keyDataPath); + + return scenarioManifest.scenarios.stream().map(s -> new Object[] {s}).toArray(Object[][]::new); + } + + @Test(dataProvider = "getDecryptTestVectors") + public void decryptTestVector(Scenario scenario) throws IOException, GeneralSecurityException { + localDynamoDb.start(); + client = localDynamoDb.createLimitedWrappedClient(); + + // load data into ciphertext tables + createCiphertextTables(client); + + // load data from vector file + putDataFromFile(client, scenario.ciphertextPath); + + // create and load metastore table if necessary + ProviderStore metastore = null; + if (scenario.metastore != null) { + MetaStore.createTable( + client, + scenario.metastore.tableName, + ProvisionedThroughput.builder().readCapacityUnits(100L).writeCapacityUnits(100L).build()); + putDataFromFile(client, scenario.metastore.path); + EncryptionMaterialsProvider metaProvider = + createProvider( + scenario.metastore.providerName, + scenario.materialName, + scenario.metastore.keys, + null); + metastore = + new MetaStore( + client, scenario.metastore.tableName, DynamoDbEncryptor.getInstance(metaProvider)); + } + + // Create the mapper with the provider under test + EncryptionMaterialsProvider provider = + createProvider(scenario.providerName, scenario.materialName, scenario.keys, metastore); + + // Verify successful decryption + switch (scenario.version) { + case "v0": + assertVersionCompatibility(provider, tableName); + break; + case "v1": + assertVersionCompatibility_2(provider, tableName); + break; + default: + throw new IllegalStateException( + "Version " + scenario.version + " not yet implemented in test vector runner"); + } + client.close(); + localDynamoDb.stop(); + } + + @Test(dataProvider = "getEncryptTestVectors") + public void encryptWithTestVector(Scenario scenario) throws IOException { + localDynamoDb.start(); + client = localDynamoDb.createLimitedWrappedClient(); + + // load data into ciphertext tables + createCiphertextTables(client); + + // create and load metastore table if necessary + ProviderStore metastore = null; + if (scenario.metastore != null) { + MetaStore.createTable( + client, + scenario.metastore.tableName, + ProvisionedThroughput.builder().readCapacityUnits(100L).writeCapacityUnits(100L).build()); + putDataFromFile(client, scenario.metastore.path); + EncryptionMaterialsProvider metaProvider = + createProvider( + scenario.metastore.providerName, + scenario.materialName, + scenario.metastore.keys, + null); + metastore = + new MetaStore( + client, scenario.metastore.tableName, DynamoDbEncryptor.getInstance(metaProvider)); + } + + // Encrypt data with the provider under test, only ensure that no exception is thrown + EncryptionMaterialsProvider provider = + createProvider(scenario.providerName, scenario.materialName, scenario.keys, metastore); + generateStandardData(provider); + client.close(); + localDynamoDb.stop(); + } + + private EncryptionMaterialsProvider createProvider( + String providerName, String materialName, Keys keys, ProviderStore metastore) { + switch (providerName) { + case ScenarioManifest.MOST_RECENT_PROVIDER_NAME: + return new CachingMostRecentProvider(metastore, materialName, 1000); + case ScenarioManifest.STATIC_PROVIDER_NAME: + KeyData decryptKeyData = keyDataMap.get(keys.decryptName); + KeyData verifyKeyData = keyDataMap.get(keys.verifyName); + SecretKey decryptKey = + new SecretKeySpec(Base64.decode(decryptKeyData.material), decryptKeyData.algorithm); + SecretKey verifyKey = + new SecretKeySpec(Base64.decode(verifyKeyData.material), verifyKeyData.algorithm); + return new SymmetricStaticProvider(decryptKey, verifyKey); + case ScenarioManifest.WRAPPED_PROVIDER_NAME: + decryptKeyData = keyDataMap.get(keys.decryptName); + verifyKeyData = keyDataMap.get(keys.verifyName); + + // This can be either the asymmetric provider, where we should test using it's explicit + // constructor, + // or a wrapped symmetric where we use the wrapped materials constructor. + if (decryptKeyData.keyType.equals(ScenarioManifest.SYMMETRIC_KEY_TYPE)) { + decryptKey = + new SecretKeySpec(Base64.decode(decryptKeyData.material), decryptKeyData.algorithm); + verifyKey = + new SecretKeySpec(Base64.decode(verifyKeyData.material), verifyKeyData.algorithm); + return new WrappedMaterialsProvider(decryptKey, decryptKey, verifyKey); + } else { + KeyData encryptKeyData = keyDataMap.get(keys.encryptName); + KeyData signKeyData = keyDataMap.get(keys.signName); + try { + // Hardcoded to use RSA for asymmetric keys. If we include vectors with a different + // asymmetric scheme this will need to be updated. + KeyFactory rsaFact = KeyFactory.getInstance(RSA); + + PublicKey encryptMaterial = + rsaFact.generatePublic( + new X509EncodedKeySpec(Base64.decode(encryptKeyData.material))); + PrivateKey decryptMaterial = + rsaFact.generatePrivate( + new PKCS8EncodedKeySpec(Base64.decode(decryptKeyData.material))); + KeyPair decryptPair = new KeyPair(encryptMaterial, decryptMaterial); + + PublicKey verifyMaterial = + rsaFact.generatePublic( + new X509EncodedKeySpec(Base64.decode(verifyKeyData.material))); + PrivateKey signingMaterial = + rsaFact.generatePrivate( + new PKCS8EncodedKeySpec(Base64.decode(signKeyData.material))); + KeyPair sigPair = new KeyPair(verifyMaterial, signingMaterial); + + return new AsymmetricStaticProvider(decryptPair, sigPair); + } catch (GeneralSecurityException ex) { + throw new RuntimeException(ex); + } + } + case ScenarioManifest.AWS_KMS_PROVIDER_NAME: + return new DirectKmsMaterialsProvider(kmsClient, keyDataMap.get(keys.decryptName).keyId); + default: + throw new IllegalStateException( + "Provider " + providerName + " not yet implemented in test vector runner"); + } + } + + // Create empty tables for the ciphertext. + // The underlying structure to these tables is hardcoded, + // and we run all test vectors assuming the ciphertext matches the key schema for these tables. + private void createCiphertextTables(DynamoDbClient localDynamoDb) { + // TableName Setup + ArrayList attrDef = new ArrayList<>(); + attrDef.add( + AttributeDefinition.builder() + .attributeName(HASH_KEY) + .attributeType(ScalarAttributeType.N) + .build()); + + attrDef.add( + AttributeDefinition.builder() + .attributeName(RANGE_KEY) + .attributeType(ScalarAttributeType.N) + .build()); + ArrayList keySchema = new ArrayList<>(); + keySchema.add(KeySchemaElement.builder().attributeName(HASH_KEY).keyType(KeyType.HASH).build()); + keySchema.add( + KeySchemaElement.builder().attributeName(RANGE_KEY).keyType(KeyType.RANGE).build()); + + localDynamoDb.createTable( + CreateTableRequest.builder() + .tableName("TableName") + .attributeDefinitions(attrDef) + .keySchema(keySchema) + .provisionedThroughput( + ProvisionedThroughput.builder() + .readCapacityUnits(100L) + .writeCapacityUnits(100L) + .build()) + .build()); + + // HashKeyOnly SetUp + attrDef = new ArrayList<>(); + attrDef.add( + AttributeDefinition.builder() + .attributeName(HASH_KEY) + .attributeType(ScalarAttributeType.S) + .build()); + + keySchema = new ArrayList<>(); + keySchema.add(KeySchemaElement.builder().attributeName(HASH_KEY).keyType(KeyType.HASH).build()); + + localDynamoDb.createTable( + CreateTableRequest.builder() + .tableName("HashKeyOnly") + .attributeDefinitions(attrDef) + .keySchema(keySchema) + .provisionedThroughput( + ProvisionedThroughput.builder() + .readCapacityUnits(100L) + .writeCapacityUnits(100L) + .build()) + .build()); + + // DeterministicTable SetUp + attrDef = new ArrayList<>(); + attrDef.add( + AttributeDefinition.builder() + .attributeName(HASH_KEY) + .attributeType(ScalarAttributeType.B) + .build()); + attrDef.add( + AttributeDefinition.builder() + .attributeName(RANGE_KEY) + .attributeType(ScalarAttributeType.N) + .build()); + + keySchema = new ArrayList<>(); + keySchema.add(KeySchemaElement.builder().attributeName(HASH_KEY).keyType(KeyType.HASH).build()); + keySchema.add( + KeySchemaElement.builder().attributeName(RANGE_KEY).keyType(KeyType.RANGE).build()); + + localDynamoDb.createTable( + CreateTableRequest.builder() + .tableName("DeterministicTable") + .attributeDefinitions(attrDef) + .keySchema(keySchema) + .provisionedThroughput( + ProvisionedThroughput.builder() + .readCapacityUnits(100L) + .writeCapacityUnits(100L) + .build()) + .build()); + } + + // Given a file in the test vector ciphertext format, put those entries into their tables. + // This assumes the expected tables have already been created. + private void putDataFromFile(DynamoDbClient localDynamoDb, String filename) throws IOException { + Map>> manifest = + getCiphertextManifestFromFile(filename); + for (String tableName : manifest.keySet()) { + for (Map attributes : manifest.get(tableName)) { + localDynamoDb.putItem( + PutItemRequest.builder().tableName(tableName).item(attributes).build()); + } + } + } + + private Map>> getCiphertextManifestFromFile( + String filename) throws IOException { + return getManifestFromFile( + TEST_VECTOR_MANIFEST_DIR + stripFilePath(filename), + new TypeReference>>>() {}); + } + + private static T getManifestFromFile(String filename, TypeReference typeRef) + throws IOException { + final URL url = HolisticIT.class.getResource(filename); + if (url == null) { + throw new IllegalStateException("Missing file " + filename + " in src/test/resources."); + } + final File manifestFile = new File(url.getPath()); + final ObjectMapper manifestMapper = new ObjectMapper(); + return (T) manifestMapper.readValue(manifestFile, typeRef); + } + + private static void loadKeyData(String filename) throws IOException { + keyDataMap = + getManifestFromFile( + TEST_VECTOR_MANIFEST_DIR + stripFilePath(filename), + new TypeReference>() {}); + } + + public void generateStandardData(EncryptionMaterialsProvider prov) { + DynamoDbEncryptor encryptor = DynamoDbEncryptor.getInstance(prov); + Map encryptedRecord; + Map> actions; + EncryptionContext encryptionContext = + EncryptionContext.builder() + .tableName(tableName) + .hashKeyName("hashKey") + .rangeKeyName("rangeKey") + .build(); + Map hashKey1 = new HashMap<>(); + Map hashKey2 = new HashMap<>(); + Map hashKey3 = new HashMap<>(); + + hashKey1.put("hashKey", AttributeValue.builder().s("Foo").build()); + hashKey2.put("hashKey", AttributeValue.builder().s("Bar").build()); + hashKey3.put("hashKey", AttributeValue.builder().s("Baz").build()); + + // encrypted record + actions = new HashMap<>(); + for (final String attr : ENCRYPTED_TEST_VALUE_2.keySet()) { + switch (attr) { + case "hashKey": + case "rangeKey": + case "version": + actions.put(attr, signOnly); + break; + default: + actions.put(attr, encryptAndSign); + break; + } + } + encryptedRecord = encryptor.encryptRecord(ENCRYPTED_TEST_VALUE_2, actions, encryptionContext); + putItems(encryptedRecord, tableName); + + // mixed test record + actions = new HashMap<>(); + for (final String attr : MIXED_TEST_VALUE_2.keySet()) { + switch (attr) { + case "rangeKey": + case "hashKey": + case "version": + case "stringValue": + case "doubleValue": + case "doubleSet": + actions.put(attr, signOnly); + break; + case "intValue": + break; + default: + actions.put(attr, encryptAndSign); + break; + } + } + encryptedRecord = encryptor.encryptRecord(MIXED_TEST_VALUE_2, actions, encryptionContext); + putItems(encryptedRecord, tableName); + + // sign only record + actions = new HashMap<>(); + for (final String attr : SIGNED_TEST_VALUE_2.keySet()) { + actions.put(attr, signOnly); + } + encryptedRecord = encryptor.encryptRecord(SIGNED_TEST_VALUE_2, actions, encryptionContext); + putItems(encryptedRecord, tableName); + + // untouched record + putItems(UNTOUCHED_TEST_VALUE_2, tableName); + } + + private void putItems(Map map, String tableName) { + PutItemRequest request = PutItemRequest.builder().item(map).tableName(tableName).build(); + client.putItem(request); + } + + private Map getItems(Map map, String tableName) { + GetItemRequest request = GetItemRequest.builder().key(map).tableName(tableName).build(); + return client.getItem(request).item(); + } + + private void assertVersionCompatibility(EncryptionMaterialsProvider provider, String tableName) + throws GeneralSecurityException { + DynamoDbEncryptor encryptor = DynamoDbEncryptor.getInstance(provider); + Map response; + Map decryptedRecord; + EncryptionContext encryptionContext = + EncryptionContext.builder() + .tableName(tableName) + .hashKeyName("hashKey") + .rangeKeyName("rangeKey") + .build(); + + // Set up maps for table items + HashMap untouched = new HashMap<>(); + HashMap signed = new HashMap<>(); + HashMap mixed = new HashMap<>(); + HashMap encrypted = new HashMap<>(); + HashMap hashKey1 = new HashMap<>(); + HashMap hashKey2 = new HashMap<>(); + HashMap hashKey3 = new HashMap<>(); + untouched.put("hashKey", UNTOUCHED_TEST_VALUE.get("hashKey")); + untouched.put("rangeKey", UNTOUCHED_TEST_VALUE.get("rangeKey")); + + signed.put("hashKey", SIGNED_TEST_VALUE.get("hashKey")); + signed.put("rangeKey", SIGNED_TEST_VALUE.get("rangeKey")); + + mixed.put("hashKey", MIXED_TEST_VALUE.get("hashKey")); + mixed.put("rangeKey", MIXED_TEST_VALUE.get("rangeKey")); + + encrypted.put("hashKey", ENCRYPTED_TEST_VALUE.get("hashKey")); + encrypted.put("rangeKey", ENCRYPTED_TEST_VALUE.get("rangeKey")); + + hashKey1.put("hashKey", AttributeValue.builder().s("Foo").build()); + hashKey2.put("hashKey", AttributeValue.builder().s("Bar").build()); + hashKey3.put("hashKey", AttributeValue.builder().s("Baz").build()); + + // check untouched attr + assertTrue( + new DdbRecordMatcher(UNTOUCHED_TEST_VALUE, false).matches(getItems(untouched, tableName))); + + // check signed attr + // Describe what actions need to be taken for each attribute + Map> actions = new HashMap<>(); + for (final String attr : SIGNED_TEST_VALUE.keySet()) { + actions.put(attr, signOnly); + } + response = getItems(signed, tableName); + decryptedRecord = encryptor.decryptRecord(response, actions, encryptionContext); + assertTrue(new DdbRecordMatcher(SIGNED_TEST_VALUE, false).matches(decryptedRecord)); + + // check mixed attr + actions = new HashMap<>(); + for (final String attr : MIXED_TEST_VALUE.keySet()) { + switch (attr) { + case "rangeKey": + case "hashKey": + case "version": + case "stringValue": + actions.put(attr, signOnly); + break; + case "intValue": + break; + default: + actions.put(attr, encryptAndSign); + break; + } + } + response = getItems(mixed, tableName); + decryptedRecord = encryptor.decryptRecord(response, actions, encryptionContext); + assertTrue(new DdbRecordMatcher(MIXED_TEST_VALUE, false).matches(decryptedRecord)); + + // check encrypted attr + actions = new HashMap<>(); + for (final String attr : ENCRYPTED_TEST_VALUE.keySet()) { + switch (attr) { + case "hashKey": + case "rangeKey": + case "version": + actions.put(attr, signOnly); + break; + default: + actions.put(attr, encryptAndSign); + break; + } + } + response = getItems(encrypted, tableName); + decryptedRecord = encryptor.decryptRecord(response, actions, encryptionContext); + assertTrue(new DdbRecordMatcher(ENCRYPTED_TEST_VALUE, false).matches(decryptedRecord)); + + assertEquals("Foo", getItems(hashKey1, "HashKeyOnly").get("hashKey").s()); + assertEquals("Bar", getItems(hashKey2, "HashKeyOnly").get("hashKey").s()); + assertEquals("Baz", getItems(hashKey3, "HashKeyOnly").get("hashKey").s()); + + Map key = new HashMap<>(); + for (int i = 1; i <= 3; ++i) { + key.put("hashKey", AttributeValue.builder().n("0").build()); + key.put("rangeKey", AttributeValue.builder().n(String.valueOf(i)).build()); + response = getItems(key, "TableName"); + assertEquals(0, Integer.parseInt(response.get("hashKey").n())); + assertEquals(i, Integer.parseInt(response.get("rangeKey").n())); + + key.put("hashKey", AttributeValue.builder().n("1").build()); + key.put("rangeKey", AttributeValue.builder().n(String.valueOf(i)).build()); + response = getItems(key, "TableName"); + assertEquals(1, Integer.parseInt(response.get("hashKey").n())); + assertEquals(i, Integer.parseInt(response.get("rangeKey").n())); + + key.put("hashKey", AttributeValue.builder().n(String.valueOf(4 + i)).build()); + key.put("rangeKey", AttributeValue.builder().n(String.valueOf(i)).build()); + response = getItems(key, "TableName"); + assertEquals(4 + i, Integer.parseInt(response.get("hashKey").n())); + assertEquals(i, Integer.parseInt(response.get("rangeKey").n())); + } + } + + private void assertVersionCompatibility_2(EncryptionMaterialsProvider provider, String tableName) + throws GeneralSecurityException { + DynamoDbEncryptor encryptor = DynamoDbEncryptor.getInstance(provider); + Map response; + Map decryptedRecord; + EncryptionContext encryptionContext = + EncryptionContext.builder() + .tableName(tableName) + .hashKeyName("hashKey") + .rangeKeyName("rangeKey") + .build(); + + // Set up maps for table items + HashMap untouched = new HashMap<>(); + HashMap signed = new HashMap<>(); + HashMap mixed = new HashMap<>(); + HashMap encrypted = new HashMap<>(); + HashMap hashKey1 = new HashMap<>(); + HashMap hashKey2 = new HashMap<>(); + HashMap hashKey3 = new HashMap<>(); + + untouched.put("hashKey", UNTOUCHED_TEST_VALUE_2.get("hashKey")); + untouched.put("rangeKey", UNTOUCHED_TEST_VALUE_2.get("rangeKey")); + + signed.put("hashKey", SIGNED_TEST_VALUE_2.get("hashKey")); + signed.put("rangeKey", SIGNED_TEST_VALUE_2.get("rangeKey")); + + mixed.put("hashKey", MIXED_TEST_VALUE_2.get("hashKey")); + mixed.put("rangeKey", MIXED_TEST_VALUE_2.get("rangeKey")); + + encrypted.put("hashKey", ENCRYPTED_TEST_VALUE_2.get("hashKey")); + encrypted.put("rangeKey", ENCRYPTED_TEST_VALUE_2.get("rangeKey")); + + hashKey1.put("hashKey", AttributeValue.builder().s("Foo").build()); + hashKey2.put("hashKey", AttributeValue.builder().s("Bar").build()); + hashKey3.put("hashKey", AttributeValue.builder().s("Baz").build()); + + // check untouched attr + assert new DdbRecordMatcher(UNTOUCHED_TEST_VALUE_2, false) + .matches(getItems(untouched, tableName)); + + // check signed attr + // Describe what actions need to be taken for each attribute + Map> actions = new HashMap<>(); + for (final String attr : SIGNED_TEST_VALUE_2.keySet()) { + actions.put(attr, signOnly); + } + response = getItems(signed, tableName); + decryptedRecord = encryptor.decryptRecord(response, actions, encryptionContext); + assertTrue(new DdbRecordMatcher(SIGNED_TEST_VALUE_2, false).matches(decryptedRecord)); + + // check mixed attr + actions = new HashMap<>(); + for (final String attr : MIXED_TEST_VALUE_2.keySet()) { + switch (attr) { + case "rangeKey": + case "hashKey": + case "version": + case "stringValue": + case "doubleValue": + case "doubleSet": + actions.put(attr, signOnly); + break; + case "intValue": + break; + default: + actions.put(attr, encryptAndSign); + break; + } + } + response = getItems(mixed, tableName); + decryptedRecord = encryptor.decryptRecord(response, actions, encryptionContext); + assertTrue(new DdbRecordMatcher(MIXED_TEST_VALUE_2, false).matches(decryptedRecord)); + + // check encrypted attr + actions = new HashMap<>(); + for (final String attr : ENCRYPTED_TEST_VALUE_2.keySet()) { + switch (attr) { + case "hashKey": + case "rangeKey": + case "version": + actions.put(attr, signOnly); + break; + default: + actions.put(attr, encryptAndSign); + break; + } + } + response = getItems(encrypted, tableName); + decryptedRecord = encryptor.decryptRecord(response, actions, encryptionContext); + assertTrue(new DdbRecordMatcher(ENCRYPTED_TEST_VALUE_2, false).matches(decryptedRecord)); + + // check HashKey Table + assertEquals("Foo", getItems(hashKey1, "HashKeyOnly").get("hashKey").s()); + assertEquals("Bar", getItems(hashKey2, "HashKeyOnly").get("hashKey").s()); + assertEquals("Baz", getItems(hashKey3, "HashKeyOnly").get("hashKey").s()); + + // Check Hash and Range Key Values + Map key = new HashMap<>(); + for (int i = 1; i <= 3; ++i) { + key.put("hashKey", AttributeValue.builder().n("0").build()); + key.put("rangeKey", AttributeValue.builder().n(String.valueOf(i)).build()); + response = getItems(key, tableName); + assertEquals(0, Integer.parseInt(response.get("hashKey").n())); + assertEquals(i, Integer.parseInt(response.get("rangeKey").n())); + + key.put("hashKey", AttributeValue.builder().n("1").build()); + key.put("rangeKey", AttributeValue.builder().n(String.valueOf(i)).build()); + response = getItems(key, tableName); + assertEquals(1, Integer.parseInt(response.get("hashKey").n())); + assertEquals(i, Integer.parseInt(response.get("rangeKey").n())); + + key.put("hashKey", AttributeValue.builder().n(String.valueOf(4 + i)).build()); + key.put("rangeKey", AttributeValue.builder().n(String.valueOf(i)).build()); + response = getItems(key, tableName); + assertEquals(4 + i, Integer.parseInt(response.get("hashKey").n())); + assertEquals(i, Integer.parseInt(response.get("rangeKey").n())); + } + } + + private static String stripFilePath(String path) { + return path.replaceFirst("file://", ""); + } + + @JsonDeserialize(using = AttributeValueDeserializer.class) + public abstract static class DeserializedAttributeValue implements AttributeValue.Builder {} +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DelegatedEncryptionTest.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DelegatedEncryptionTest.java new file mode 100644 index 0000000000..fd3bf37ace --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DelegatedEncryptionTest.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; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertNotNull; +import static org.testng.AssertJUnit.assertNull; +import static org.testng.AssertJUnit.assertTrue; + +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.SignatureException; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.crypto.spec.SecretKeySpec; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.EncryptionMaterialsProvider; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.SymmetricStaticProvider; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.internal.Utils; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.testing.AttrMatcher; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.testing.TestDelegatedKey; + +public class DelegatedEncryptionTest { + private static SecretKeySpec rawEncryptionKey; + private static SecretKeySpec rawMacKey; + private static DelegatedKey encryptionKey; + private static DelegatedKey macKey; + + private EncryptionMaterialsProvider prov; + private DynamoDbEncryptor encryptor; + private Map attribs; + private EncryptionContext context; + + @BeforeClass + public static void setupClass() { + rawEncryptionKey = new SecretKeySpec(Utils.getRandom(32), "AES"); + encryptionKey = new TestDelegatedKey(rawEncryptionKey); + + rawMacKey = new SecretKeySpec(Utils.getRandom(32), "HmacSHA256"); + macKey = new TestDelegatedKey(rawMacKey); + } + + @BeforeMethod + public void setUp() { + prov = new SymmetricStaticProvider(encryptionKey, macKey, + Collections.emptyMap()); + encryptor = DynamoDbEncryptor.getInstance(prov, "encryptor-"); + + attribs = new HashMap<>(); + attribs.put("intValue", AttributeValue.builder().n("123").build()); + attribs.put("stringValue", AttributeValue.builder().s("Hello world!").build()); + attribs.put("byteArrayValue", + AttributeValue.builder().b(SdkBytes.fromByteArray(new byte[] {0, 1, 2, 3, 4, 5})).build()); + attribs.put("stringSet", AttributeValue.builder().ss("Goodbye", "Cruel", "World", "?").build()); + attribs.put("intSet", AttributeValue.builder().ns("1", "200", "10", "15", "0").build()); + attribs.put("hashKey", AttributeValue.builder().n("5").build()); + attribs.put("rangeKey", AttributeValue.builder().n("7").build()); + attribs.put("version", AttributeValue.builder().n("0").build()); + + context = EncryptionContext.builder() + .tableName("TableName") + .hashKeyName("hashKey") + .rangeKeyName("rangeKey") + .build(); + } + + @Test + public void testSetSignatureFieldName() { + assertNotNull(encryptor.getSignatureFieldName()); + encryptor.setSignatureFieldName("A different value"); + assertEquals("A different value", encryptor.getSignatureFieldName()); + } + + @Test + public void testSetMaterialDescriptionFieldName() { + assertNotNull(encryptor.getMaterialDescriptionFieldName()); + encryptor.setMaterialDescriptionFieldName("A different value"); + assertEquals("A different value", encryptor.getMaterialDescriptionFieldName()); + } + + @Test + public void fullEncryption() throws GeneralSecurityException { + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept( + Collections.unmodifiableMap(attribs), context, "hashKey", "rangeKey", "version"); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = + encryptor.decryptAllFieldsExcept( + Collections.unmodifiableMap(encryptedAttributes), + context, + "hashKey", + "rangeKey", + "version"); + assertThat(decryptedAttributes, AttrMatcher.match(attribs)); + + // Make sure keys and version are not encrypted + assertAttrEquals(attribs.get("hashKey"), encryptedAttributes.get("hashKey")); + assertAttrEquals(attribs.get("rangeKey"), encryptedAttributes.get("rangeKey")); + assertAttrEquals(attribs.get("version"), encryptedAttributes.get("version")); + + // Make sure String has been encrypted (we'll assume the others are correct as well) + assertTrue(encryptedAttributes.containsKey("stringValue")); + assertNull(encryptedAttributes.get("stringValue").s()); + assertNotNull(encryptedAttributes.get("stringValue").b()); + } + + @Test(expectedExceptions = SignatureException.class) + public void fullEncryptionBadSignature() throws GeneralSecurityException { + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept( + Collections.unmodifiableMap(attribs), context, "hashKey", "rangeKey", "version"); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + encryptedAttributes.put("hashKey", AttributeValue.builder().n("666").build()); + encryptor.decryptAllFieldsExcept( + Collections.unmodifiableMap(encryptedAttributes), + context, + "hashKey", + "rangeKey", + "version"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void badVersionNumber() throws GeneralSecurityException { + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept( + Collections.unmodifiableMap(attribs), context, "hashKey", "rangeKey", "version"); + SdkBytes materialDescription = + encryptedAttributes.get(encryptor.getMaterialDescriptionFieldName()).b(); + byte[] rawArray = materialDescription.asByteArray(); + assertEquals(0, rawArray[0]); // This will need to be kept in sync with the current version. + rawArray[0] = 100; + encryptedAttributes.put( + encryptor.getMaterialDescriptionFieldName(), + AttributeValue.builder().b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(rawArray))).build()); + encryptor.decryptAllFieldsExcept( + Collections.unmodifiableMap(encryptedAttributes), + context, + "hashKey", + "rangeKey", + "version"); + } + + @Test + public void signedOnly() throws GeneralSecurityException { + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept(attribs, context, attribs.keySet().toArray(new String[0])); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = + encryptor.decryptAllFieldsExcept( + encryptedAttributes, context, attribs.keySet().toArray(new String[0])); + assertThat(decryptedAttributes, AttrMatcher.match(attribs)); + + // Make sure keys and version are not encrypted + assertAttrEquals(attribs.get("hashKey"), encryptedAttributes.get("hashKey")); + assertAttrEquals(attribs.get("rangeKey"), encryptedAttributes.get("rangeKey")); + assertAttrEquals(attribs.get("version"), encryptedAttributes.get("version")); + + // Make sure String has not been encrypted (we'll assume the others are correct as well) + assertAttrEquals(attribs.get("stringValue"), encryptedAttributes.get("stringValue")); + } + + @Test + public void signedOnlyNullCryptoKey() throws GeneralSecurityException { + prov = new SymmetricStaticProvider(null, macKey, Collections.emptyMap()); + encryptor = DynamoDbEncryptor.getInstance(prov, "encryptor-"); + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept(attribs, context, attribs.keySet().toArray(new String[0])); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = + encryptor.decryptAllFieldsExcept( + encryptedAttributes, context, attribs.keySet().toArray(new String[0])); + assertThat(decryptedAttributes, AttrMatcher.match(attribs)); + + // Make sure keys and version are not encrypted + assertAttrEquals(attribs.get("hashKey"), encryptedAttributes.get("hashKey")); + assertAttrEquals(attribs.get("rangeKey"), encryptedAttributes.get("rangeKey")); + assertAttrEquals(attribs.get("version"), encryptedAttributes.get("version")); + + // Make sure String has not been encrypted (we'll assume the others are correct as well) + assertAttrEquals(attribs.get("stringValue"), encryptedAttributes.get("stringValue")); + } + + + @Test(expectedExceptions = SignatureException.class) + public void signedOnlyBadSignature() throws GeneralSecurityException { + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept(attribs, context, attribs.keySet().toArray(new String[0])); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + encryptedAttributes.put("hashKey", AttributeValue.builder().n("666").build()); + encryptor.decryptAllFieldsExcept( + encryptedAttributes, context, attribs.keySet().toArray(new String[0])); + } + + @Test(expectedExceptions = SignatureException.class) + public void signedOnlyNoSignature() throws GeneralSecurityException { + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept(attribs, context, attribs.keySet().toArray(new String[0])); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + encryptedAttributes.remove(encryptor.getSignatureFieldName()); + encryptor.decryptAllFieldsExcept( + encryptedAttributes, context, attribs.keySet().toArray(new String[0])); + } + + @Test + public void RsaSignedOnly() throws GeneralSecurityException { + KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA"); + rsaGen.initialize(2048, Utils.getRng()); + KeyPair sigPair = rsaGen.generateKeyPair(); + encryptor = + DynamoDbEncryptor.getInstance( + new SymmetricStaticProvider( + encryptionKey, sigPair, Collections.emptyMap()), + "encryptor-" + ); + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept(attribs, context, attribs.keySet().toArray(new String[0])); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = + encryptor.decryptAllFieldsExcept( + encryptedAttributes, context, attribs.keySet().toArray(new String[0])); + assertThat(decryptedAttributes, AttrMatcher.match(attribs)); + + // Make sure keys and version are not encrypted + assertAttrEquals(attribs.get("hashKey"), encryptedAttributes.get("hashKey")); + assertAttrEquals(attribs.get("rangeKey"), encryptedAttributes.get("rangeKey")); + assertAttrEquals(attribs.get("version"), encryptedAttributes.get("version")); + + // Make sure String has not been encrypted (we'll assume the others are correct as well) + assertAttrEquals(attribs.get("stringValue"), encryptedAttributes.get("stringValue")); + } + + @Test(expectedExceptions = SignatureException.class) + public void RsaSignedOnlyBadSignature() throws GeneralSecurityException { + KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA"); + rsaGen.initialize(2048, Utils.getRng()); + KeyPair sigPair = rsaGen.generateKeyPair(); + encryptor = + DynamoDbEncryptor.getInstance( + new SymmetricStaticProvider( + encryptionKey, sigPair, Collections.emptyMap()), + "encryptor-" + ); + + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept(attribs, context, attribs.keySet().toArray(new String[0])); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + encryptedAttributes.replace("hashKey", AttributeValue.builder().n("666").build()); + encryptor.decryptAllFieldsExcept( + encryptedAttributes, context, attribs.keySet().toArray(new String[0])); + } + + private void assertAttrEquals(AttributeValue o1, AttributeValue o2) { + assertEquals(o1.b(), o2.b()); + assertSetsEqual(o1.bs(), o2.bs()); + assertEquals(o1.n(), o2.n()); + assertSetsEqual(o1.ns(), o2.ns()); + assertEquals(o1.s(), o2.s()); + assertSetsEqual(o1.ss(), o2.ss()); + } + + private void assertSetsEqual(Collection c1, Collection c2) { + assertFalse(c1 == null ^ c2 == null); + if (c1 != null) { + Set s1 = new HashSet<>(c1); + Set s2 = new HashSet<>(c2); + assertEquals(s1, s2); + } + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DelegatedEnvelopeEncryptionTest.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DelegatedEnvelopeEncryptionTest.java new file mode 100644 index 0000000000..ce22c396fa --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DelegatedEnvelopeEncryptionTest.java @@ -0,0 +1,280 @@ +/* + * 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 static org.hamcrest.MatcherAssert.assertThat; +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertNotNull; +import static org.testng.AssertJUnit.assertNull; +import static org.testng.AssertJUnit.assertTrue; + +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.SignatureException; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.crypto.spec.SecretKeySpec; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.EncryptionMaterialsProvider; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.SymmetricStaticProvider; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.WrappedMaterialsProvider; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.internal.Utils; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.testing.AttrMatcher; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.testing.TestDelegatedKey; + +public class DelegatedEnvelopeEncryptionTest { + private static SecretKeySpec rawEncryptionKey; + private static SecretKeySpec rawMacKey; + private static DelegatedKey encryptionKey; + private static DelegatedKey macKey; + + private EncryptionMaterialsProvider prov; + private DynamoDbEncryptor encryptor; + private Map attribs; + private EncryptionContext context; + + @BeforeClass + public static void setupClass() { + rawEncryptionKey = new SecretKeySpec(Utils.getRandom(32), "AES"); + encryptionKey = new TestDelegatedKey(rawEncryptionKey); + + rawMacKey = new SecretKeySpec(Utils.getRandom(32), "HmacSHA256"); + macKey = new TestDelegatedKey(rawMacKey); + } + + @BeforeMethod + public void setUp() throws Exception { + prov = + new WrappedMaterialsProvider( + encryptionKey, encryptionKey, macKey, Collections.emptyMap()); + encryptor = DynamoDbEncryptor.getInstance(prov, "encryptor-"); + + attribs = new HashMap(); + attribs.put("intValue", AttributeValue.builder().n("123").build()); + attribs.put("stringValue", AttributeValue.builder().s("Hello world!").build()); + attribs.put( + "byteArrayValue", + AttributeValue.builder().b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {0, 1, 2, 3, 4, 5}))).build()); + attribs.put("stringSet", AttributeValue.builder().ss("Goodbye", "Cruel", "World", "?").build()); + attribs.put("intSet", AttributeValue.builder().ns("1", "200", "10", "15", "0").build()); + attribs.put("hashKey", AttributeValue.builder().n("5").build()); + attribs.put("rangeKey", AttributeValue.builder().n("7").build()); + attribs.put("version", AttributeValue.builder().n("0").build()); + + context = + new EncryptionContext.Builder() + .tableName("TableName") + .hashKeyName("hashKey") + .rangeKeyName("rangeKey") + .build(); + } + + @Test + public void testSetSignatureFieldName() { + assertNotNull(encryptor.getSignatureFieldName()); + encryptor.setSignatureFieldName("A different value"); + assertEquals("A different value", encryptor.getSignatureFieldName()); + } + + @Test + public void testSetMaterialDescriptionFieldName() { + assertNotNull(encryptor.getMaterialDescriptionFieldName()); + encryptor.setMaterialDescriptionFieldName("A different value"); + assertEquals("A different value", encryptor.getMaterialDescriptionFieldName()); + } + + @Test + public void fullEncryption() throws GeneralSecurityException{ + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept( + Collections.unmodifiableMap(attribs), context, "hashKey", "rangeKey", "version"); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = + encryptor.decryptAllFieldsExcept( + Collections.unmodifiableMap(encryptedAttributes), + context, + "hashKey", + "rangeKey", + "version"); + assertThat(decryptedAttributes, AttrMatcher.match(attribs)); + + // Make sure keys and version are not encrypted + assertAttrEquals(attribs.get("hashKey"), encryptedAttributes.get("hashKey")); + assertAttrEquals(attribs.get("rangeKey"), encryptedAttributes.get("rangeKey")); + assertAttrEquals(attribs.get("version"), encryptedAttributes.get("version")); + + // Make sure String has been encrypted (we'll assume the others are correct as well) + assertTrue(encryptedAttributes.containsKey("stringValue")); + assertNull(encryptedAttributes.get("stringValue").s()); + assertNotNull(encryptedAttributes.get("stringValue").b()); + } + + @Test(expectedExceptions = SignatureException.class) + public void fullEncryptionBadSignature() throws GeneralSecurityException { + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept( + Collections.unmodifiableMap(attribs), context, "hashKey", "rangeKey", "version"); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + encryptedAttributes.put("hashKey", AttributeValue.builder().n("666").build()); + encryptor.decryptAllFieldsExcept( + Collections.unmodifiableMap(encryptedAttributes), + context, + "hashKey", + "rangeKey", + "version"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void badVersionNumber() throws GeneralSecurityException { + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept( + Collections.unmodifiableMap(attribs), context, "hashKey", "rangeKey", "version"); + SdkBytes materialDescription = + encryptedAttributes.get(encryptor.getMaterialDescriptionFieldName()).b(); + byte[] rawArray = materialDescription.asByteArray(); + assertEquals(0, rawArray[0]); // This will need to be kept in sync with the current version. + rawArray[0] = 100; + encryptedAttributes.put( + encryptor.getMaterialDescriptionFieldName(), + AttributeValue.builder().b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(rawArray))).build()); + encryptor.decryptAllFieldsExcept( + Collections.unmodifiableMap(encryptedAttributes), + context, + "hashKey", + "rangeKey", + "version"); + } + + @Test + public void signedOnlyNullCryptoKey() throws GeneralSecurityException { + prov = new SymmetricStaticProvider(null, macKey, Collections.emptyMap()); + encryptor = DynamoDbEncryptor.getInstance(prov, "encryptor-"); + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept(attribs, context, attribs.keySet().toArray(new String[0])); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = + encryptor.decryptAllFieldsExcept( + encryptedAttributes, context, attribs.keySet().toArray(new String[0])); + assertThat(decryptedAttributes, AttrMatcher.match(attribs)); + + // Make sure keys and version are not encrypted + assertAttrEquals(attribs.get("hashKey"), encryptedAttributes.get("hashKey")); + assertAttrEquals(attribs.get("rangeKey"), encryptedAttributes.get("rangeKey")); + assertAttrEquals(attribs.get("version"), encryptedAttributes.get("version")); + + // Make sure String has not been encrypted (we'll assume the others are correct as well) + assertAttrEquals(attribs.get("stringValue"), encryptedAttributes.get("stringValue")); + } + + @Test(expectedExceptions = SignatureException.class) + public void signedOnlyBadSignature() throws GeneralSecurityException { + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept(attribs, context, attribs.keySet().toArray(new String[0])); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + encryptedAttributes.put("hashKey", AttributeValue.builder().n("666").build()); + encryptor.decryptAllFieldsExcept( + encryptedAttributes, context, attribs.keySet().toArray(new String[0])); + } + + @Test(expectedExceptions = SignatureException.class) + public void signedOnlyNoSignature() throws GeneralSecurityException { + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept(attribs, context, attribs.keySet().toArray(new String[0])); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + encryptedAttributes.remove(encryptor.getSignatureFieldName()); + encryptor.decryptAllFieldsExcept( + encryptedAttributes, context, attribs.keySet().toArray(new String[0])); + } + + @Test + public void RsaSignedOnly() throws GeneralSecurityException { + KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA"); + rsaGen.initialize(2048, Utils.getRng()); + KeyPair sigPair = rsaGen.generateKeyPair(); + encryptor = + DynamoDbEncryptor.getInstance( + new SymmetricStaticProvider( + encryptionKey, sigPair, Collections.emptyMap()), + "encryptor-"); + + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept(attribs, context, attribs.keySet().toArray(new String[0])); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = + encryptor.decryptAllFieldsExcept( + encryptedAttributes, context, attribs.keySet().toArray(new String[0])); + assertThat(decryptedAttributes, AttrMatcher.match(attribs)); + + // Make sure keys and version are not encrypted + assertAttrEquals(attribs.get("hashKey"), encryptedAttributes.get("hashKey")); + assertAttrEquals(attribs.get("rangeKey"), encryptedAttributes.get("rangeKey")); + assertAttrEquals(attribs.get("version"), encryptedAttributes.get("version")); + + // Make sure String has not been encrypted (we'll assume the others are correct as well) + assertAttrEquals(attribs.get("stringValue"), encryptedAttributes.get("stringValue")); + } + + @Test(expectedExceptions = SignatureException.class) + public void RsaSignedOnlyBadSignature() throws GeneralSecurityException { + KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA"); + rsaGen.initialize(2048, Utils.getRng()); + KeyPair sigPair = rsaGen.generateKeyPair(); + encryptor = + DynamoDbEncryptor.getInstance( + new SymmetricStaticProvider( + encryptionKey, sigPair, Collections.emptyMap()), + "encryptor-"); + + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept(attribs, context, attribs.keySet().toArray(new String[0])); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + encryptedAttributes.replace("hashKey", AttributeValue.builder().n("666").build()); + encryptor.decryptAllFieldsExcept( + encryptedAttributes, context, attribs.keySet().toArray(new String[0])); + } + + private void assertAttrEquals(AttributeValue o1, AttributeValue o2) { + assertEquals(o1.b(), o2.b()); + assertSetsEqual(o1.bs(), o2.bs()); + assertEquals(o1.n(), o2.n()); + assertSetsEqual(o1.ns(), o2.ns()); + assertEquals(o1.s(), o2.s()); + assertSetsEqual(o1.ss(), o2.ss()); + } + + private void assertSetsEqual(Collection c1, Collection c2) { + assertFalse(c1 == null ^ c2 == null); + if (c1 != null) { + Set s1 = new HashSet<>(c1); + Set s2 = new HashSet<>(c2); + assertEquals(s1, s2); + } + } + +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbEncryptorTest.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbEncryptorTest.java new file mode 100644 index 0000000000..87fb8353bb --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbEncryptorTest.java @@ -0,0 +1,591 @@ +/* + * 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 static java.util.stream.Collectors.toMap; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertNotNull; +import static org.testng.AssertJUnit.assertNull; +import static org.testng.AssertJUnit.assertTrue; +import static org.testng.collections.Sets.newHashSet; +import static software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.utils.EncryptionContextOperators.overrideEncryptionContextTableName; + +import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import java.security.*; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; + +import org.bouncycastle.jce.ECNamedCurveTable; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.mockito.internal.util.collections.Sets; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +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.providers.SymmetricStaticProvider; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.internal.Utils; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.testing.AttrMatcher; + +public class DynamoDbEncryptorTest { + private static SecretKey encryptionKey; + private static SecretKey macKey; + + private InstrumentedEncryptionMaterialsProvider prov; + private DynamoDbEncryptor encryptor; + private Map attribs; + private EncryptionContext context; + private static final String OVERRIDDEN_TABLE_NAME = "TheBestTableName"; + + @BeforeClass + public static void setUpClass() throws Exception { + KeyGenerator aesGen = KeyGenerator.getInstance("AES"); + aesGen.init(128, Utils.getRng()); + encryptionKey = aesGen.generateKey(); + + KeyGenerator macGen = KeyGenerator.getInstance("HmacSHA256"); + macGen.init(256, Utils.getRng()); + macKey = macGen.generateKey(); + } + + @BeforeMethod + public void setUp() { + prov = new InstrumentedEncryptionMaterialsProvider( + new SymmetricStaticProvider(encryptionKey, macKey, + Collections.emptyMap())); + encryptor = DynamoDbEncryptor.getInstance(prov, "enryptor-"); + + attribs = new HashMap<>(); + attribs.put("intValue", AttributeValue.builder().n("123").build()); + attribs.put("stringValue", AttributeValue.builder().s("Hello world!").build()); + attribs.put("byteArrayValue", + AttributeValue.builder().b(SdkBytes.fromByteArray(new byte[] {0, 1, 2, 3, 4, 5})).build()); + attribs.put("stringSet", AttributeValue.builder().ss("Goodbye", "Cruel", "World", "?").build()); + attribs.put("intSet", AttributeValue.builder().ns("1", "200", "10", "15", "0").build()); + attribs.put("hashKey", AttributeValue.builder().n("5").build()); + attribs.put("rangeKey", AttributeValue.builder().n("7").build()); + attribs.put("version", AttributeValue.builder().n("0").build()); + + // New(er) data types + attribs.put("booleanTrue", AttributeValue.builder().bool(true).build()); + attribs.put("booleanFalse", AttributeValue.builder().bool(false).build()); + attribs.put("nullValue", AttributeValue.builder().nul(true).build()); + Map tmpMap = new HashMap<>(attribs); + attribs.put("listValue", AttributeValue.builder().l( + AttributeValue.builder().s("I'm a string").build(), + AttributeValue.builder().n("42").build(), + AttributeValue.builder().s("Another string").build(), + AttributeValue.builder().ns("1", "4", "7").build(), + AttributeValue.builder().m(tmpMap).build(), + AttributeValue.builder().l( + AttributeValue.builder().n("123").build(), + AttributeValue.builder().ns("1", "200", "10", "15", "0").build(), + AttributeValue.builder().ss("Goodbye", "Cruel", "World", "!").build() + ).build()).build()); + tmpMap = new HashMap<>(); + tmpMap.put("another string", AttributeValue.builder().s("All around the cobbler's bench").build()); + tmpMap.put("next line", AttributeValue.builder().ss("the monkey", "chased", "the weasel").build()); + tmpMap.put("more lyrics", AttributeValue.builder().l( + AttributeValue.builder().s("the monkey").build(), + AttributeValue.builder().s("thought twas").build(), + AttributeValue.builder().s("all in fun").build() + ).build()); + tmpMap.put("weasel", AttributeValue.builder().m(Collections.singletonMap("pop", AttributeValue.builder().bool(true).build())).build()); + attribs.put("song", AttributeValue.builder().m(tmpMap).build()); + + context = EncryptionContext.builder() + .tableName("TableName") + .hashKeyName("hashKey") + .rangeKeyName("rangeKey") + .build(); + } + + @Test + public void testSetSignatureFieldName() { + assertNotNull(encryptor.getSignatureFieldName()); + encryptor.setSignatureFieldName("A different value"); + assertEquals("A different value", encryptor.getSignatureFieldName()); + } + + @Test + public void testSetMaterialDescriptionFieldName() { + assertNotNull(encryptor.getMaterialDescriptionFieldName()); + encryptor.setMaterialDescriptionFieldName("A different value"); + assertEquals("A different value", encryptor.getMaterialDescriptionFieldName()); + } + + @Test + public void fullEncryption() throws GeneralSecurityException { + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept( + Collections.unmodifiableMap(attribs), context, "hashKey", "rangeKey", "version"); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + + Map decryptedAttributes = + encryptor.decryptAllFieldsExcept( + Collections.unmodifiableMap(encryptedAttributes), + context, + "hashKey", + "rangeKey", + "version"); + assertThat(decryptedAttributes, AttrMatcher.match(attribs)); + + // Make sure keys and version are not encrypted + assertAttrEquals(attribs.get("hashKey"), encryptedAttributes.get("hashKey")); + assertAttrEquals(attribs.get("rangeKey"), encryptedAttributes.get("rangeKey")); + assertAttrEquals(attribs.get("version"), encryptedAttributes.get("version")); + + // Make sure String has been encrypted (we'll assume the others are correct as well) + assertTrue(encryptedAttributes.containsKey("stringValue")); + assertNull(encryptedAttributes.get("stringValue").s()); + assertNotNull(encryptedAttributes.get("stringValue").b()); + + // Make sure we're calling the proper getEncryptionMaterials method + assertEquals( + "Wrong getEncryptionMaterials() called", + 1, + prov.getCallCount("getEncryptionMaterials(EncryptionContext context)")); + } + + @Test + public void ensureEncryptedAttributesUnmodified() throws GeneralSecurityException { + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept( + Collections.unmodifiableMap(attribs), context, "hashKey", "rangeKey", "version"); + String encryptedString = encryptedAttributes.toString(); + encryptor.decryptAllFieldsExcept( + Collections.unmodifiableMap(encryptedAttributes), + context, + "hashKey", + "rangeKey", + "version"); + + assertEquals(encryptedString, encryptedAttributes.toString()); + } + + @Test(expectedExceptions = SignatureException.class) + public void fullEncryptionBadSignature() throws GeneralSecurityException { + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept( + Collections.unmodifiableMap(attribs), context, "hashKey", "rangeKey", "version"); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + encryptedAttributes.put("hashKey", AttributeValue.builder().n("666").build()); + encryptor.decryptAllFieldsExcept( + Collections.unmodifiableMap(encryptedAttributes), + context, + "hashKey", + "rangeKey", + "version"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void badVersionNumber() throws GeneralSecurityException { + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept( + Collections.unmodifiableMap(attribs), context, "hashKey", "rangeKey", "version"); + SdkBytes materialDescription = + encryptedAttributes.get(encryptor.getMaterialDescriptionFieldName()).b(); + byte[] rawArray = materialDescription.asByteArray(); + assertEquals(0, rawArray[0]); // This will need to be kept in sync with the current version. + rawArray[0] = 100; + encryptedAttributes.put( + encryptor.getMaterialDescriptionFieldName(), + AttributeValue.builder().b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(rawArray))).build()); + encryptor.decryptAllFieldsExcept( + Collections.unmodifiableMap(encryptedAttributes), + context, + "hashKey", + "rangeKey", + "version"); + } + + @Test + public void signedOnly() throws GeneralSecurityException { + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept(attribs, context, attribs.keySet().toArray(new String[0])); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = + encryptor.decryptAllFieldsExcept( + encryptedAttributes, context, attribs.keySet().toArray(new String[0])); + assertThat(decryptedAttributes, AttrMatcher.match(attribs)); + + // Make sure keys and version are not encrypted + assertAttrEquals(attribs.get("hashKey"), encryptedAttributes.get("hashKey")); + assertAttrEquals(attribs.get("rangeKey"), encryptedAttributes.get("rangeKey")); + assertAttrEquals(attribs.get("version"), encryptedAttributes.get("version")); + + // Make sure String has not been encrypted (we'll assume the others are correct as well) + assertAttrEquals(attribs.get("stringValue"), encryptedAttributes.get("stringValue")); + } + + @Test + public void signedOnlyNullCryptoKey() throws GeneralSecurityException { + prov = + new InstrumentedEncryptionMaterialsProvider( + new SymmetricStaticProvider(null, macKey, Collections.emptyMap())); + encryptor = DynamoDbEncryptor.getInstance(prov, "encryptor-"); + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept(attribs, context, attribs.keySet().toArray(new String[0])); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = + encryptor.decryptAllFieldsExcept( + encryptedAttributes, context, attribs.keySet().toArray(new String[0])); + assertThat(decryptedAttributes, AttrMatcher.match(attribs)); + + // Make sure keys and version are not encrypted + assertAttrEquals(attribs.get("hashKey"), encryptedAttributes.get("hashKey")); + assertAttrEquals(attribs.get("rangeKey"), encryptedAttributes.get("rangeKey")); + assertAttrEquals(attribs.get("version"), encryptedAttributes.get("version")); + + // Make sure String has not been encrypted (we'll assume the others are correct as well) + assertAttrEquals(attribs.get("stringValue"), encryptedAttributes.get("stringValue")); + } + + @Test(expectedExceptions = SignatureException.class) + public void signedOnlyBadSignature() throws GeneralSecurityException { + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept(attribs, context, attribs.keySet().toArray(new String[0])); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + encryptedAttributes.put("hashKey", AttributeValue.builder().n("666").build()); + encryptor.decryptAllFieldsExcept( + encryptedAttributes, context, attribs.keySet().toArray(new String[0])); + } + + @Test(expectedExceptions = SignatureException.class) + public void signedOnlyNoSignature() throws GeneralSecurityException { + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept(attribs, context, attribs.keySet().toArray(new String[0])); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + encryptedAttributes.remove(encryptor.getSignatureFieldName()); + encryptor.decryptAllFieldsExcept( + encryptedAttributes, context, attribs.keySet().toArray(new String[0])); + } + + @Test + public void RsaSignedOnly() throws GeneralSecurityException { + KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA"); + rsaGen.initialize(2048, Utils.getRng()); + KeyPair sigPair = rsaGen.generateKeyPair(); + encryptor = + DynamoDbEncryptor.getInstance( + new SymmetricStaticProvider( + encryptionKey, sigPair, Collections.emptyMap()), + "encryptor-"); + + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept(attribs, context, attribs.keySet().toArray(new String[0])); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = + encryptor.decryptAllFieldsExcept( + encryptedAttributes, context, attribs.keySet().toArray(new String[0])); + assertThat(decryptedAttributes, AttrMatcher.match(attribs)); + + // Make sure keys and version are not encrypted + assertAttrEquals(attribs.get("hashKey"), encryptedAttributes.get("hashKey")); + assertAttrEquals(attribs.get("rangeKey"), encryptedAttributes.get("rangeKey")); + assertAttrEquals(attribs.get("version"), encryptedAttributes.get("version")); + + // Make sure String has not been encrypted (we'll assume the others are correct as well) + assertAttrEquals(attribs.get("stringValue"), encryptedAttributes.get("stringValue")); + } + + @Test(expectedExceptions = SignatureException.class) + public void RsaSignedOnlyBadSignature() throws GeneralSecurityException { + KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA"); + rsaGen.initialize(2048, Utils.getRng()); + KeyPair sigPair = rsaGen.generateKeyPair(); + encryptor = + DynamoDbEncryptor.getInstance( + new SymmetricStaticProvider( + encryptionKey, sigPair, Collections.emptyMap()), + "encryptor-"); + + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept(attribs, context, attribs.keySet().toArray(new String[0])); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + encryptedAttributes.put("hashKey", AttributeValue.builder().n("666").build()); + encryptor.decryptAllFieldsExcept( + encryptedAttributes, context, attribs.keySet().toArray(new String[0])); + } + + /** + * Tests that no exception is thrown when the encryption context override operator is null + * + * @throws GeneralSecurityException + */ + @Test + public void testNullEncryptionContextOperator() throws GeneralSecurityException { + DynamoDbEncryptor encryptor = DynamoDbEncryptor.getInstance(prov); + encryptor.setEncryptionContextOverrideOperator(null); + encryptor.encryptAllFieldsExcept(attribs, context, Collections.emptyList()); + } + + /** + * Tests decrypt and encrypt with an encryption context override operator + */ + @Test + public void testTableNameOverriddenEncryptionContextOperator() throws GeneralSecurityException { + // Ensure that the table name is different from what we override the table to. + assertThat(context.getTableName(), not(equalTo(OVERRIDDEN_TABLE_NAME))); + DynamoDbEncryptor encryptor = DynamoDbEncryptor.getInstance(prov); + encryptor.setEncryptionContextOverrideOperator( + overrideEncryptionContextTableName(context.getTableName(), OVERRIDDEN_TABLE_NAME)); + Map encryptedItems = + encryptor.encryptAllFieldsExcept(attribs, context, Collections.emptyList()); + Map decryptedItems = + encryptor.decryptAllFieldsExcept(encryptedItems, context, Collections.emptyList()); + assertThat(decryptedItems, AttrMatcher.match(attribs)); + } + + + /** + * Tests encrypt with an encryption context override operator, and a second encryptor without an override + */ + @Test + public void testTableNameOverriddenEncryptionContextOperatorWithSecondEncryptor() + throws GeneralSecurityException { + // Ensure that the table name is different from what we override the table to. + assertThat(context.getTableName(), not(equalTo(OVERRIDDEN_TABLE_NAME))); + DynamoDbEncryptor encryptor = DynamoDbEncryptor.getInstance(prov); + DynamoDbEncryptor encryptorWithoutOverride = DynamoDbEncryptor.getInstance(prov); + encryptor.setEncryptionContextOverrideOperator( + overrideEncryptionContextTableName(context.getTableName(), OVERRIDDEN_TABLE_NAME)); + Map encryptedItems = + encryptor.encryptAllFieldsExcept(attribs, context, Collections.emptyList()); + + EncryptionContext expectedOverriddenContext = + new EncryptionContext.Builder(context).tableName("TheBestTableName").build(); + Map decryptedItems = + encryptorWithoutOverride.decryptAllFieldsExcept( + encryptedItems, expectedOverriddenContext, Collections.emptyList()); + assertThat(decryptedItems, AttrMatcher.match(attribs)); + } + + /** + * Tests encrypt with an encryption context override operator, and a second encryptor without an override + */ + @Test(expectedExceptions = SignatureException.class) + public void + testTableNameOverriddenEncryptionContextOperatorWithSecondEncryptorButTheOriginalEncryptionContext() + throws GeneralSecurityException { + // Ensure that the table name is different from what we override the table to. + assertThat(context.getTableName(), not(equalTo(OVERRIDDEN_TABLE_NAME))); + DynamoDbEncryptor encryptor = DynamoDbEncryptor.getInstance(prov); + DynamoDbEncryptor encryptorWithoutOverride = DynamoDbEncryptor.getInstance(prov); + encryptor.setEncryptionContextOverrideOperator( + overrideEncryptionContextTableName(context.getTableName(), OVERRIDDEN_TABLE_NAME)); + Map encryptedItems = + encryptor.encryptAllFieldsExcept(attribs, context, Collections.emptyList()); + + // Use the original encryption context, and expect a signature failure + Map decryptedItems = + encryptorWithoutOverride.decryptAllFieldsExcept( + encryptedItems, context, Collections.emptyList()); + } + + @Test + public void EcdsaSignedOnly() throws GeneralSecurityException { + + encryptor = DynamoDbEncryptor.getInstance(getMaterialProviderwithECDSA()); + + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept(attribs, context, attribs.keySet().toArray(new String[0])); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = + encryptor.decryptAllFieldsExcept( + encryptedAttributes, context, attribs.keySet().toArray(new String[0])); + assertThat(decryptedAttributes, AttrMatcher.match(attribs)); + + // Make sure keys and version are not encrypted + assertAttrEquals(attribs.get("hashKey"), encryptedAttributes.get("hashKey")); + assertAttrEquals(attribs.get("rangeKey"), encryptedAttributes.get("rangeKey")); + assertAttrEquals(attribs.get("version"), encryptedAttributes.get("version")); + + // Make sure String has not been encrypted (we'll assume the others are correct as well) + assertAttrEquals(attribs.get("stringValue"), encryptedAttributes.get("stringValue")); + } + + @Test(expectedExceptions = SignatureException.class) + public void EcdsaSignedOnlyBadSignature() throws GeneralSecurityException { + + encryptor = DynamoDbEncryptor.getInstance(getMaterialProviderwithECDSA()); + + Map encryptedAttributes = + encryptor.encryptAllFieldsExcept(attribs, context, attribs.keySet().toArray(new String[0])); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + encryptedAttributes.put("hashKey", AttributeValue.builder().n("666").build()); + encryptor.decryptAllFieldsExcept( + encryptedAttributes, context, attribs.keySet().toArray(new String[0])); + } + + @Test + public void toByteArray() throws ReflectiveOperationException { + final byte[] expected = new byte[] {0, 1, 2, 3, 4, 5}; + assertToByteArray("Wrap", expected, ByteBuffer.wrap(expected)); + assertToByteArray("Wrap-RO", expected, ByteBuffer.wrap(expected).asReadOnlyBuffer()); + + assertToByteArray("Wrap-Truncated-Sliced", expected, ByteBuffer.wrap(new byte[] {0, 1, 2, 3, 4, 5, 6}, 0, 6).slice()); + assertToByteArray("Wrap-Offset-Sliced", expected, ByteBuffer.wrap(new byte[] {6, 0, 1, 2, 3, 4, 5, 6}, 1, 6).slice()); + assertToByteArray("Wrap-Truncated", expected, ByteBuffer.wrap(new byte[] {0, 1, 2, 3, 4, 5, 6}, 0, 6)); + assertToByteArray("Wrap-Offset", expected, ByteBuffer.wrap(new byte[] {6, 0, 1, 2, 3, 4, 5, 6}, 1, 6)); + + ByteBuffer buff = ByteBuffer.allocate(expected.length + 10); + buff.put(expected); + buff.flip(); + assertToByteArray("Normal", expected, buff); + + buff = ByteBuffer.allocateDirect(expected.length + 10); + buff.put(expected); + buff.flip(); + assertToByteArray("Direct", expected, buff); + } + + @Test + public void testDecryptWithPlaintextItem() throws GeneralSecurityException { + Map> attributeWithEmptyEncryptionFlags = + attribs.keySet().stream().collect(toMap(k -> k, k -> newHashSet())); + + Map decryptedAttributes = + encryptor.decryptRecord(attribs, attributeWithEmptyEncryptionFlags, context); + assertThat(decryptedAttributes, AttrMatcher.match(attribs)); + } + + /* + Test decrypt with a map that contains a new key (not included in attribs) with an encryption flag set that contains ENCRYPT and SIGN. + */ + @Test + public void testDecryptWithPlainTextItemAndAdditionNewAttributeHavingEncryptionFlag() + throws GeneralSecurityException { + Map> attributeWithEmptyEncryptionFlags = + attribs.keySet().stream().collect(toMap(k -> k, k -> newHashSet())); + attributeWithEmptyEncryptionFlags.put( + "newAttribute", Sets.newSet(EncryptionFlags.ENCRYPT, EncryptionFlags.SIGN)); + + Map decryptedAttributes = + encryptor.decryptRecord(attribs, attributeWithEmptyEncryptionFlags, context); + assertThat(decryptedAttributes, AttrMatcher.match(attribs)); + } + private void assertToByteArray( + final String msg, final byte[] expected, final ByteBuffer testValue) + throws ReflectiveOperationException { + Method m = DynamoDbEncryptor.class.getDeclaredMethod("toByteArray", ByteBuffer.class); + m.setAccessible(true); + + int oldPosition = testValue.position(); + int oldLimit = testValue.limit(); + + assertThat(m.invoke(null, testValue), is(expected)); + assertEquals(msg + ":Position", oldPosition, testValue.position()); + assertEquals(msg + ":Limit", oldLimit, testValue.limit()); + } + + private void assertAttrEquals(AttributeValue o1, AttributeValue o2) { + assertEquals(o1.b(), o2.b()); + assertSetsEqual(o1.bs(), o2.bs()); + assertEquals(o1.n(), o2.n()); + assertSetsEqual(o1.ns(), o2.ns()); + assertEquals(o1.s(), o2.s()); + assertSetsEqual(o1.ss(), o2.ss()); + } + + private void assertSetsEqual(Collection c1, Collection c2) { + assertFalse(c1 == null ^ c2 == null); + if (c1 != null) { + Set s1 = new HashSet<>(c1); + Set s2 = new HashSet<>(c2); + assertEquals(s1, s2); + } + } + + private EncryptionMaterialsProvider getMaterialProviderwithECDSA() + throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchProviderException { + Security.addProvider(new BouncyCastleProvider()); + ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp384r1"); + KeyPairGenerator g = KeyPairGenerator.getInstance("ECDSA", "BC"); + g.initialize(ecSpec, Utils.getRng()); + KeyPair keypair = g.generateKeyPair(); + Map description = new HashMap<>(); + description.put(DynamoDbEncryptor.DEFAULT_SIGNING_ALGORITHM_HEADER, "SHA384withECDSA"); + return new SymmetricStaticProvider(null, keypair, description); + } + + private static final class InstrumentedEncryptionMaterialsProvider implements EncryptionMaterialsProvider { + private final EncryptionMaterialsProvider delegate; + private final ConcurrentHashMap calls = new ConcurrentHashMap<>(); + + InstrumentedEncryptionMaterialsProvider(EncryptionMaterialsProvider delegate) { + this.delegate = delegate; + } + + @Override + public DecryptionMaterials getDecryptionMaterials(EncryptionContext context) { + incrementMethodCount("getDecryptionMaterials()"); + return delegate.getDecryptionMaterials(context); + } + + @Override + public EncryptionMaterials getEncryptionMaterials(EncryptionContext context) { + incrementMethodCount("getEncryptionMaterials(EncryptionContext context)"); + return delegate.getEncryptionMaterials(context); + } + + @Override + public void refresh() { + incrementMethodCount("refresh()"); + delegate.refresh(); + } + + int getCallCount(String method) { + AtomicInteger count = calls.get(method); + if (count != null) { + return count.intValue(); + } else { + return 0; + } + } + + @SuppressWarnings("unused") + public void resetCallCounts() { + calls.clear(); + } + + private void incrementMethodCount(String method) { + AtomicInteger oldValue = calls.putIfAbsent(method, new AtomicInteger(1)); + if (oldValue != null) { + oldValue.incrementAndGet(); + } + } + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbSignerTest.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbSignerTest.java new file mode 100644 index 0000000000..8320e79526 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbSignerTest.java @@ -0,0 +1,567 @@ +/* + * 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.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.Security; +import java.security.SignatureException; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import javax.crypto.KeyGenerator; + +import org.bouncycastle.jce.ECNamedCurveTable; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import software.amazon.cryptools.dynamodbencryptionclientsdk2.internal.Utils; + +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class DynamoDbSignerTest { + // These use the Key type (rather than PublicKey, PrivateKey, and SecretKey) + // to test the routing logic within the signer. + private static Key pubKeyRsa; + private static Key privKeyRsa; + private static Key macKey; + private DynamoDbSigner signerRsa; + private DynamoDbSigner signerEcdsa; + private static Key pubKeyEcdsa; + private static Key privKeyEcdsa; + + @BeforeClass + public static void setUpClass() throws Exception { + + // RSA key generation + KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA"); + rsaGen.initialize(2048, Utils.getRng()); + KeyPair sigPair = rsaGen.generateKeyPair(); + pubKeyRsa = sigPair.getPublic(); + privKeyRsa = sigPair.getPrivate(); + + KeyGenerator macGen = KeyGenerator.getInstance("HmacSHA256"); + macGen.init(256, Utils.getRng()); + macKey = macGen.generateKey(); + + Security.addProvider(new BouncyCastleProvider()); + ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp384r1"); + KeyPairGenerator g = KeyPairGenerator.getInstance("ECDSA", "BC"); + g.initialize(ecSpec, Utils.getRng()); + KeyPair keypair = g.generateKeyPair(); + pubKeyEcdsa = keypair.getPublic(); + privKeyEcdsa = keypair.getPrivate(); + } + + @BeforeMethod + public void setUp() { + signerRsa = DynamoDbSigner.getInstance("SHA256withRSA", Utils.getRng()); + signerEcdsa = DynamoDbSigner.getInstance("SHA384withECDSA", Utils.getRng()); + } + + @Test + public void mac() throws GeneralSecurityException { + Map itemAttributes = new HashMap(); + Map> attributeFlags = new HashMap>(); + + itemAttributes.put("Key1", AttributeValue.builder().s("Value1").build()); + attributeFlags.put("Key1", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put("Key2", AttributeValue.builder().n("100").build()); + attributeFlags.put("Key2", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put( + "Key3", + AttributeValue.builder() + .b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {0, 1, 2, 3}))) + .build()); + attributeFlags.put("Key3", EnumSet.of(EncryptionFlags.SIGN, EncryptionFlags.ENCRYPT)); + byte[] signature = + signerRsa.calculateSignature(itemAttributes, attributeFlags, new byte[0], macKey); + + signerRsa.verifySignature( + itemAttributes, attributeFlags, new byte[0], macKey, ByteBuffer.wrap(signature)); + } + + @Test + public void macLists() throws GeneralSecurityException { + Map itemAttributes = new HashMap(); + Map> attributeFlags = new HashMap>(); + + itemAttributes.put("Key1", AttributeValue.builder().ss("Value1", "Value2", "Value3").build()); + attributeFlags.put("Key1", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put("Key2", AttributeValue.builder().ns("100", "200", "300").build()); + attributeFlags.put("Key2", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put( + "Key3", + AttributeValue.builder() + .bs( + SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {0, 1, 2, 3})), + SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {3, 2, 1}))) + .build()); + attributeFlags.put("Key3", EnumSet.of(EncryptionFlags.SIGN, EncryptionFlags.ENCRYPT)); + byte[] signature = + signerRsa.calculateSignature(itemAttributes, attributeFlags, new byte[0], macKey); + + signerRsa.verifySignature( + itemAttributes, attributeFlags, new byte[0], macKey, ByteBuffer.wrap(signature)); + } + + @Test + public void macListsUnsorted() throws GeneralSecurityException { + Map itemAttributes = new HashMap(); + Map> attributeFlags = new HashMap>(); + + itemAttributes.put("Key1", AttributeValue.builder().ss("Value3", "Value1", "Value2").build()); + attributeFlags.put("Key1", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put("Key2", AttributeValue.builder().ns("100", "300", "200").build()); + attributeFlags.put("Key2", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put( + "Key3", + AttributeValue.builder() + .bs( + SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {3, 2, 1})), + SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {0, 1, 2, 3}))) + .build()); + attributeFlags.put("Key3", EnumSet.of(EncryptionFlags.SIGN, EncryptionFlags.ENCRYPT)); + byte[] signature = + signerRsa.calculateSignature(itemAttributes, attributeFlags, new byte[0], macKey); + + Map scrambledAttributes = new HashMap(); + scrambledAttributes.put("Key1", AttributeValue.builder().ss("Value1", "Value2", "Value3").build()); + scrambledAttributes.put("Key2", AttributeValue.builder().ns("100", "200", "300").build()); + scrambledAttributes.put( + "Key3", + AttributeValue.builder() + .bs( + SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {0, 1, 2, 3})), + SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {3, 2, 1}))) + .build()); + + signerRsa.verifySignature( + scrambledAttributes, attributeFlags, new byte[0], macKey, ByteBuffer.wrap(signature)); + } + + @Test + public void macNoAdMatchesEmptyAd() throws GeneralSecurityException { + Map itemAttributes = new HashMap(); + Map> attributeFlags = new HashMap>(); + + itemAttributes.put("Key1", AttributeValue.builder().s("Value1").build()); + attributeFlags.put("Key1", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put("Key2", AttributeValue.builder().n("100").build()); + attributeFlags.put("Key2", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put( + "Key3", AttributeValue.builder() + .b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {0, 1, 2, 3}))) + .build()); + attributeFlags.put("Key3", EnumSet.of(EncryptionFlags.SIGN, EncryptionFlags.ENCRYPT)); + byte[] signature = signerRsa.calculateSignature(itemAttributes, attributeFlags, null, macKey); + + signerRsa.verifySignature( + itemAttributes, attributeFlags, new byte[0], macKey, ByteBuffer.wrap(signature)); + } + + @Test + public void macWithIgnoredChange() throws GeneralSecurityException { + Map itemAttributes = new HashMap(); + Map> attributeFlags = new HashMap>(); + + itemAttributes.put("Key1", AttributeValue.builder().s("Value1").build()); + attributeFlags.put("Key1", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put("Key2", AttributeValue.builder().n("100").build()); + attributeFlags.put("Key2", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put( + "Key3", AttributeValue.builder() + .b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {0, 1, 2, 3}))) + .build()); + attributeFlags.put("Key3", EnumSet.of(EncryptionFlags.SIGN, EncryptionFlags.ENCRYPT)); + itemAttributes.put("Key4", AttributeValue.builder().s("Ignored Value").build()); + byte[] signature = + signerRsa.calculateSignature(itemAttributes, attributeFlags, new byte[0], macKey); + + itemAttributes.put("Key4", AttributeValue.builder().s("New Ignored Value").build()); + signerRsa.verifySignature( + itemAttributes, attributeFlags, new byte[0], macKey, ByteBuffer.wrap(signature)); + } + + @Test(expectedExceptions = SignatureException.class) + public void macChangedValue() throws GeneralSecurityException { + Map itemAttributes = new HashMap(); + Map> attributeFlags = new HashMap>(); + + itemAttributes.put("Key1", AttributeValue.builder().s("Value1").build()); + attributeFlags.put("Key1", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put("Key2", AttributeValue.builder().n("100").build()); + attributeFlags.put("Key2", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put( + "Key3", AttributeValue.builder() + .b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {0, 1, 2, 3}))) + .build()); + attributeFlags.put("Key3", EnumSet.of(EncryptionFlags.SIGN, EncryptionFlags.ENCRYPT)); + byte[] signature = + signerRsa.calculateSignature(itemAttributes, attributeFlags, new byte[0], macKey); + + itemAttributes.put("Key2", AttributeValue.builder().n("99").build()); + signerRsa.verifySignature( + itemAttributes, attributeFlags, new byte[0], macKey, ByteBuffer.wrap(signature)); + } + + @Test(expectedExceptions = SignatureException.class) + public void macChangedFlag() throws GeneralSecurityException { + Map itemAttributes = new HashMap(); + Map> attributeFlags = new HashMap>(); + + itemAttributes.put("Key1", AttributeValue.builder().s("Value1").build()); + attributeFlags.put("Key1", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put("Key2", AttributeValue.builder().n("100").build()); + attributeFlags.put("Key2", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put( + "Key3", AttributeValue.builder() + .b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {0, 1, 2, 3}))) + .build()); + attributeFlags.put("Key3", EnumSet.of(EncryptionFlags.SIGN, EncryptionFlags.ENCRYPT)); + byte[] signature = + signerRsa.calculateSignature(itemAttributes, attributeFlags, new byte[0], macKey); + + attributeFlags.put("Key3", EnumSet.of(EncryptionFlags.SIGN)); + signerRsa.verifySignature( + itemAttributes, attributeFlags, new byte[0], macKey, ByteBuffer.wrap(signature)); + } + + @Test(expectedExceptions = SignatureException.class) + public void macChangedAssociatedData() throws GeneralSecurityException { + Map itemAttributes = new HashMap(); + Map> attributeFlags = new HashMap>(); + + itemAttributes.put("Key1", AttributeValue.builder().s("Value1").build()); + attributeFlags.put("Key1", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put("Key2", AttributeValue.builder().n("100").build()); + attributeFlags.put("Key2", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put( + "Key3", AttributeValue.builder() + .b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {0, 1, 2, 3}))) + .build()); + attributeFlags.put("Key3", EnumSet.of(EncryptionFlags.SIGN, EncryptionFlags.ENCRYPT)); + byte[] signature = + signerRsa.calculateSignature(itemAttributes, attributeFlags, new byte[] {3, 2, 1}, macKey); + + signerRsa.verifySignature( + itemAttributes, attributeFlags, new byte[] {1, 2, 3}, macKey, ByteBuffer.wrap(signature)); + } + + @Test + public void sig() throws GeneralSecurityException { + Map itemAttributes = new HashMap(); + Map> attributeFlags = new HashMap>(); + + itemAttributes.put("Key1", AttributeValue.builder().s("Value1").build()); + attributeFlags.put("Key1", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put("Key2", AttributeValue.builder().n("100").build()); + attributeFlags.put("Key2", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put( + "Key3", AttributeValue.builder() + .b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {0, 1, 2, 3}))) + .build()); + attributeFlags.put("Key3", EnumSet.of(EncryptionFlags.SIGN, EncryptionFlags.ENCRYPT)); + byte[] signature = + signerRsa.calculateSignature(itemAttributes, attributeFlags, new byte[0], privKeyRsa); + + signerRsa.verifySignature( + itemAttributes, attributeFlags, new byte[0], pubKeyRsa, ByteBuffer.wrap(signature)); + } + + @Test + public void sigWithReadOnlySignature() throws GeneralSecurityException { + Map itemAttributes = new HashMap(); + Map> attributeFlags = new HashMap>(); + + itemAttributes.put("Key1", AttributeValue.builder().s("Value1").build()); + attributeFlags.put("Key1", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put("Key2", AttributeValue.builder().n("100").build()); + attributeFlags.put("Key2", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put( + "Key3", AttributeValue.builder() + .b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {0, 1, 2, 3}))) + .build()); + attributeFlags.put("Key3", EnumSet.of(EncryptionFlags.SIGN, EncryptionFlags.ENCRYPT)); + byte[] signature = + signerRsa.calculateSignature(itemAttributes, attributeFlags, new byte[0], privKeyRsa); + + signerRsa.verifySignature( + itemAttributes, + attributeFlags, + new byte[0], + pubKeyRsa, + ByteBuffer.wrap(signature).asReadOnlyBuffer()); + } + + @Test + public void sigNoAdMatchesEmptyAd() throws GeneralSecurityException { + Map itemAttributes = new HashMap(); + Map> attributeFlags = new HashMap>(); + + itemAttributes.put("Key1", AttributeValue.builder().s("Value1").build()); + attributeFlags.put("Key1", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put("Key2", AttributeValue.builder().n("100").build()); + attributeFlags.put("Key2", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put( + "Key3", AttributeValue.builder() + .b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {0, 1, 2, 3}))) + .build()); + attributeFlags.put("Key3", EnumSet.of(EncryptionFlags.SIGN, EncryptionFlags.ENCRYPT)); + byte[] signature = + signerRsa.calculateSignature(itemAttributes, attributeFlags, null, privKeyRsa); + + signerRsa.verifySignature( + itemAttributes, attributeFlags, new byte[0], pubKeyRsa, ByteBuffer.wrap(signature)); + } + + @Test + public void sigWithIgnoredChange() throws GeneralSecurityException { + Map itemAttributes = new HashMap(); + Map> attributeFlags = new HashMap>(); + + itemAttributes.put("Key1", AttributeValue.builder().s("Value1").build()); + attributeFlags.put("Key1", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put("Key2", AttributeValue.builder().n("100").build()); + attributeFlags.put("Key2", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put( + "Key3", AttributeValue.builder() + .b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {0, 1, 2, 3}))) + .build()); + attributeFlags.put("Key3", EnumSet.of(EncryptionFlags.SIGN, EncryptionFlags.ENCRYPT)); + itemAttributes.put("Key4", AttributeValue.builder().s("Ignored Value").build()); + byte[] signature = + signerRsa.calculateSignature(itemAttributes, attributeFlags, new byte[0], privKeyRsa); + + itemAttributes.put("Key4", AttributeValue.builder().s("New Ignored Value").build()); + signerRsa.verifySignature( + itemAttributes, attributeFlags, new byte[0], pubKeyRsa, ByteBuffer.wrap(signature)); + } + + @Test(expectedExceptions = SignatureException.class) + public void sigChangedValue() throws GeneralSecurityException { + Map itemAttributes = new HashMap(); + Map> attributeFlags = new HashMap>(); + + itemAttributes.put("Key1", AttributeValue.builder().s("Value1").build()); + attributeFlags.put("Key1", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put("Key2", AttributeValue.builder().n("100").build()); + attributeFlags.put("Key2", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put( + "Key3", AttributeValue.builder() + .b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {0, 1, 2, 3}))) + .build()); + attributeFlags.put("Key3", EnumSet.of(EncryptionFlags.SIGN, EncryptionFlags.ENCRYPT)); + byte[] signature = + signerRsa.calculateSignature(itemAttributes, attributeFlags, new byte[0], privKeyRsa); + + itemAttributes.put("Key2", AttributeValue.builder().n("99").build()); + signerRsa.verifySignature( + itemAttributes, attributeFlags, new byte[0], pubKeyRsa, ByteBuffer.wrap(signature)); + } + + @Test(expectedExceptions = SignatureException.class) + public void sigChangedFlag() throws GeneralSecurityException { + Map itemAttributes = new HashMap(); + Map> attributeFlags = new HashMap>(); + + itemAttributes.put("Key1", AttributeValue.builder().s("Value1").build()); + attributeFlags.put("Key1", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put("Key2", AttributeValue.builder().n("100").build()); + attributeFlags.put("Key2", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put( + "Key3", AttributeValue.builder() + .b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {0, 1, 2, 3}))) + .build()); + attributeFlags.put("Key3", EnumSet.of(EncryptionFlags.SIGN, EncryptionFlags.ENCRYPT)); + byte[] signature = + signerRsa.calculateSignature(itemAttributes, attributeFlags, new byte[0], privKeyRsa); + + attributeFlags.put("Key3", EnumSet.of(EncryptionFlags.SIGN)); + signerRsa.verifySignature( + itemAttributes, attributeFlags, new byte[0], pubKeyRsa, ByteBuffer.wrap(signature)); + } + + @Test(expectedExceptions = SignatureException.class) + public void sigChangedAssociatedData() throws GeneralSecurityException { + Map itemAttributes = new HashMap(); + Map> attributeFlags = new HashMap>(); + + itemAttributes.put("Key1", AttributeValue.builder().s("Value1").build()); + attributeFlags.put("Key1", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put("Key2", AttributeValue.builder().n("100").build()); + attributeFlags.put("Key2", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put( + "Key3", AttributeValue.builder() + .b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {0, 1, 2, 3}))) + .build()); + attributeFlags.put("Key3", EnumSet.of(EncryptionFlags.SIGN, EncryptionFlags.ENCRYPT)); + byte[] signature = + signerRsa.calculateSignature(itemAttributes, attributeFlags, new byte[0], privKeyRsa); + + signerRsa.verifySignature( + itemAttributes, + attributeFlags, + new byte[] {1, 2, 3}, + pubKeyRsa, + ByteBuffer.wrap(signature)); + } + + @Test + public void sigEcdsa() throws GeneralSecurityException { + Map itemAttributes = new HashMap(); + Map> attributeFlags = new HashMap>(); + + itemAttributes.put("Key1", AttributeValue.builder().s("Value1").build()); + attributeFlags.put("Key1", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put("Key2", AttributeValue.builder().n("100").build()); + attributeFlags.put("Key2", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put( + "Key3", AttributeValue.builder() + .b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {0, 1, 2, 3}))) + .build()); + attributeFlags.put("Key3", EnumSet.of(EncryptionFlags.SIGN)); + byte[] signature = + signerEcdsa.calculateSignature(itemAttributes, attributeFlags, new byte[0], privKeyEcdsa); + + signerEcdsa.verifySignature( + itemAttributes, attributeFlags, new byte[0], pubKeyEcdsa, ByteBuffer.wrap(signature)); + } + + @Test + public void sigEcdsaWithReadOnlySignature() throws GeneralSecurityException { + Map itemAttributes = new HashMap(); + Map> attributeFlags = new HashMap>(); + + itemAttributes.put("Key1", AttributeValue.builder().s("Value1").build()); + attributeFlags.put("Key1", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put("Key2", AttributeValue.builder().n("100").build()); + attributeFlags.put("Key2", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put( + "Key3", AttributeValue.builder() + .b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {0, 1, 2, 3}))) + .build()); + attributeFlags.put("Key3", EnumSet.of(EncryptionFlags.SIGN)); + byte[] signature = + signerEcdsa.calculateSignature(itemAttributes, attributeFlags, new byte[0], privKeyEcdsa); + + signerEcdsa.verifySignature( + itemAttributes, + attributeFlags, + new byte[0], + pubKeyEcdsa, + ByteBuffer.wrap(signature).asReadOnlyBuffer()); + } + + @Test + public void sigEcdsaNoAdMatchesEmptyAd() throws GeneralSecurityException { + Map itemAttributes = new HashMap(); + Map> attributeFlags = new HashMap>(); + + itemAttributes.put("Key1", AttributeValue.builder().s("Value1").build()); + attributeFlags.put("Key1", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put("Key2", AttributeValue.builder().n("100").build()); + attributeFlags.put("Key2", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put( + "Key3", AttributeValue.builder() + .b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {0, 1, 2, 3}))) + .build()); + attributeFlags.put("Key3", EnumSet.of(EncryptionFlags.SIGN)); + byte[] signature = + signerEcdsa.calculateSignature(itemAttributes, attributeFlags, null, privKeyEcdsa); + + signerEcdsa.verifySignature( + itemAttributes, attributeFlags, new byte[0], pubKeyEcdsa, ByteBuffer.wrap(signature)); + } + + @Test + public void sigEcdsaWithIgnoredChange() throws GeneralSecurityException { + Map itemAttributes = new HashMap(); + Map> attributeFlags = new HashMap>(); + + itemAttributes.put("Key1", AttributeValue.builder().s("Value1").build()); + attributeFlags.put("Key1", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put("Key2", AttributeValue.builder().n("100").build()); + attributeFlags.put("Key2", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put( + "Key3", AttributeValue.builder() + .b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {0, 1, 2, 3}))) + .build()); + attributeFlags.put("Key3", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put("Key4", AttributeValue.builder().s("Ignored Value").build()); + byte[] signature = + signerEcdsa.calculateSignature(itemAttributes, attributeFlags, new byte[0], privKeyEcdsa); + + itemAttributes.put("Key4", AttributeValue.builder().s("New Ignored Value").build()); + signerEcdsa.verifySignature( + itemAttributes, attributeFlags, new byte[0], pubKeyEcdsa, ByteBuffer.wrap(signature)); + } + + @Test(expectedExceptions = SignatureException.class) + public void sigEcdsaChangedValue() throws GeneralSecurityException { + Map itemAttributes = new HashMap(); + Map> attributeFlags = new HashMap>(); + + itemAttributes.put("Key1", AttributeValue.builder().s("Value1").build()); + attributeFlags.put("Key1", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put("Key2", AttributeValue.builder().n("100").build()); + attributeFlags.put("Key2", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put( + "Key3", AttributeValue.builder() + .b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {0, 1, 2, 3}))) + .build()); + attributeFlags.put("Key3", EnumSet.of(EncryptionFlags.SIGN)); + byte[] signature = + signerEcdsa.calculateSignature(itemAttributes, attributeFlags, new byte[0], privKeyEcdsa); + + itemAttributes.put("Key2", AttributeValue.builder().n("99").build()); + signerEcdsa.verifySignature( + itemAttributes, attributeFlags, new byte[0], pubKeyEcdsa, ByteBuffer.wrap(signature)); + } + + @Test(expectedExceptions = SignatureException.class) + public void sigEcdsaChangedAssociatedData() throws GeneralSecurityException { + Map itemAttributes = new HashMap(); + Map> attributeFlags = new HashMap>(); + + itemAttributes.put("Key1", AttributeValue.builder().s("Value1").build()); + attributeFlags.put("Key1", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put("Key2", AttributeValue.builder().n("100").build()); + attributeFlags.put("Key2", EnumSet.of(EncryptionFlags.SIGN)); + itemAttributes.put( + "Key3", AttributeValue.builder() + .b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(new byte[] {0, 1, 2, 3}))) + .build()); + attributeFlags.put("Key3", EnumSet.of(EncryptionFlags.SIGN)); + byte[] signature = + signerEcdsa.calculateSignature(itemAttributes, attributeFlags, new byte[0], privKeyEcdsa); + + signerEcdsa.verifySignature( + itemAttributes, + attributeFlags, + new byte[] {1, 2, 3}, + pubKeyEcdsa, + ByteBuffer.wrap(signature)); + } +} \ No newline at end of file diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/AsymmetricRawMaterialsTest.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/AsymmetricRawMaterialsTest.java new file mode 100644 index 0000000000..b9258c5f83 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/AsymmetricRawMaterialsTest.java @@ -0,0 +1,138 @@ +/* + * 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 static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.HashMap; +import java.util.Map; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +public class AsymmetricRawMaterialsTest { + private static SecureRandom rnd; + private static KeyPair encryptionPair; + private static SecretKey macKey; + private static KeyPair sigPair; + private Map description; + + @BeforeClass + public static void setUpClass() throws NoSuchAlgorithmException { + rnd = new SecureRandom(); + KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA"); + rsaGen.initialize(2048, rnd); + encryptionPair = rsaGen.generateKeyPair(); + sigPair = rsaGen.generateKeyPair(); + + KeyGenerator macGen = KeyGenerator.getInstance("HmacSHA256"); + macGen.init(256, rnd); + macKey = macGen.generateKey(); + } + + @BeforeMethod + public void setUp() { + description = new HashMap(); + description.put("TestKey", "test value"); + } + + @Test + public void macNoDescription() throws GeneralSecurityException { + AsymmetricRawMaterials matEncryption = new AsymmetricRawMaterials(encryptionPair, macKey); + assertEquals(macKey, matEncryption.getSigningKey()); + assertEquals(macKey, matEncryption.getVerificationKey()); + assertFalse(matEncryption.getMaterialDescription().isEmpty()); + + SecretKey envelopeKey = matEncryption.getEncryptionKey(); + assertEquals(envelopeKey, matEncryption.getDecryptionKey()); + + AsymmetricRawMaterials matDecryption = + new AsymmetricRawMaterials(encryptionPair, macKey, matEncryption.getMaterialDescription()); + assertEquals(macKey, matDecryption.getSigningKey()); + assertEquals(macKey, matDecryption.getVerificationKey()); + assertEquals(envelopeKey, matDecryption.getEncryptionKey()); + assertEquals(envelopeKey, matDecryption.getDecryptionKey()); + } + + @Test + public void macWithDescription() throws GeneralSecurityException { + AsymmetricRawMaterials matEncryption = + new AsymmetricRawMaterials(encryptionPair, macKey, description); + assertEquals(macKey, matEncryption.getSigningKey()); + assertEquals(macKey, matEncryption.getVerificationKey()); + assertFalse(matEncryption.getMaterialDescription().isEmpty()); + assertEquals("test value", matEncryption.getMaterialDescription().get("TestKey")); + + SecretKey envelopeKey = matEncryption.getEncryptionKey(); + assertEquals(envelopeKey, matEncryption.getDecryptionKey()); + + AsymmetricRawMaterials matDecryption = + new AsymmetricRawMaterials(encryptionPair, macKey, matEncryption.getMaterialDescription()); + assertEquals(macKey, matDecryption.getSigningKey()); + assertEquals(macKey, matDecryption.getVerificationKey()); + assertEquals(envelopeKey, matDecryption.getEncryptionKey()); + assertEquals(envelopeKey, matDecryption.getDecryptionKey()); + assertEquals("test value", matDecryption.getMaterialDescription().get("TestKey")); + } + + @Test + public void sigNoDescription() throws GeneralSecurityException { + AsymmetricRawMaterials matEncryption = new AsymmetricRawMaterials(encryptionPair, sigPair); + assertEquals(sigPair.getPrivate(), matEncryption.getSigningKey()); + assertEquals(sigPair.getPublic(), matEncryption.getVerificationKey()); + assertFalse(matEncryption.getMaterialDescription().isEmpty()); + + SecretKey envelopeKey = matEncryption.getEncryptionKey(); + assertEquals(envelopeKey, matEncryption.getDecryptionKey()); + + AsymmetricRawMaterials matDecryption = + new AsymmetricRawMaterials(encryptionPair, sigPair, matEncryption.getMaterialDescription()); + assertEquals(sigPair.getPrivate(), matDecryption.getSigningKey()); + assertEquals(sigPair.getPublic(), matDecryption.getVerificationKey()); + assertEquals(envelopeKey, matDecryption.getEncryptionKey()); + assertEquals(envelopeKey, matDecryption.getDecryptionKey()); + } + + @Test + public void sigWithDescription() throws GeneralSecurityException { + AsymmetricRawMaterials matEncryption = + new AsymmetricRawMaterials(encryptionPair, sigPair, description); + assertEquals(sigPair.getPrivate(), matEncryption.getSigningKey()); + assertEquals(sigPair.getPublic(), matEncryption.getVerificationKey()); + assertFalse(matEncryption.getMaterialDescription().isEmpty()); + assertEquals("test value", matEncryption.getMaterialDescription().get("TestKey")); + + SecretKey envelopeKey = matEncryption.getEncryptionKey(); + assertEquals(envelopeKey, matEncryption.getDecryptionKey()); + + AsymmetricRawMaterials matDecryption = + new AsymmetricRawMaterials(encryptionPair, sigPair, matEncryption.getMaterialDescription()); + assertEquals(sigPair.getPrivate(), matDecryption.getSigningKey()); + assertEquals(sigPair.getPublic(), matDecryption.getVerificationKey()); + assertEquals(envelopeKey, matDecryption.getEncryptionKey()); + assertEquals(envelopeKey, matDecryption.getDecryptionKey()); + assertEquals("test value", matDecryption.getMaterialDescription().get("TestKey")); + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/SymmetricRawMaterialsTest.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/SymmetricRawMaterialsTest.java new file mode 100644 index 0000000000..a6987ce792 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/SymmetricRawMaterialsTest.java @@ -0,0 +1,104 @@ +/* + * 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 static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertTrue; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.HashMap; +import java.util.Map; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +public class SymmetricRawMaterialsTest { + private static SecretKey encryptionKey; + private static SecretKey macKey; + private static KeyPair sigPair; + private static SecureRandom rnd; + private Map description; + + @BeforeClass + public static void setUpClass() throws NoSuchAlgorithmException { + rnd = new SecureRandom(); + KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA"); + rsaGen.initialize(2048, rnd); + sigPair = rsaGen.generateKeyPair(); + + KeyGenerator aesGen = KeyGenerator.getInstance("AES"); + aesGen.init(128, rnd); + encryptionKey = aesGen.generateKey(); + + KeyGenerator macGen = KeyGenerator.getInstance("HmacSHA256"); + macGen.init(256, rnd); + macKey = macGen.generateKey(); + } + + @BeforeMethod + public void setUp() { + description = new HashMap(); + description.put("TestKey", "test value"); + } + + @Test + public void macNoDescription() throws NoSuchAlgorithmException { + SymmetricRawMaterials mat = new SymmetricRawMaterials(encryptionKey, macKey); + assertEquals(encryptionKey, mat.getEncryptionKey()); + assertEquals(encryptionKey, mat.getDecryptionKey()); + assertEquals(macKey, mat.getSigningKey()); + assertEquals(macKey, mat.getVerificationKey()); + assertTrue(mat.getMaterialDescription().isEmpty()); + } + + @Test + public void macWithDescription() throws NoSuchAlgorithmException { + SymmetricRawMaterials mat = new SymmetricRawMaterials(encryptionKey, macKey, description); + assertEquals(encryptionKey, mat.getEncryptionKey()); + assertEquals(encryptionKey, mat.getDecryptionKey()); + assertEquals(macKey, mat.getSigningKey()); + assertEquals(macKey, mat.getVerificationKey()); + assertEquals(description, mat.getMaterialDescription()); + assertEquals("test value", mat.getMaterialDescription().get("TestKey")); + } + + @Test + public void sigNoDescription() throws NoSuchAlgorithmException { + SymmetricRawMaterials mat = new SymmetricRawMaterials(encryptionKey, sigPair); + assertEquals(encryptionKey, mat.getEncryptionKey()); + assertEquals(encryptionKey, mat.getDecryptionKey()); + assertEquals(sigPair.getPrivate(), mat.getSigningKey()); + assertEquals(sigPair.getPublic(), mat.getVerificationKey()); + assertTrue(mat.getMaterialDescription().isEmpty()); + } + + @Test + public void sigWithDescription() throws NoSuchAlgorithmException { + SymmetricRawMaterials mat = new SymmetricRawMaterials(encryptionKey, sigPair, description); + assertEquals(encryptionKey, mat.getEncryptionKey()); + assertEquals(encryptionKey, mat.getDecryptionKey()); + assertEquals(sigPair.getPrivate(), mat.getSigningKey()); + assertEquals(sigPair.getPublic(), mat.getVerificationKey()); + assertEquals(description, mat.getMaterialDescription()); + assertEquals("test value", mat.getMaterialDescription().get("TestKey")); + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/AsymmetricStaticProviderTest.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/AsymmetricStaticProviderTest.java new file mode 100644 index 0000000000..8f71ac7b28 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/AsymmetricStaticProviderTest.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 static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertNotNull; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +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.internal.Utils; + +public class AsymmetricStaticProviderTest { + private static KeyPair encryptionPair; + private static SecretKey macKey; + private static KeyPair sigPair; + private Map description; + private EncryptionContext ctx; + + @BeforeClass + public static void setUpClass() throws Exception { + KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA"); + rsaGen.initialize(2048, Utils.getRng()); + sigPair = rsaGen.generateKeyPair(); + encryptionPair = rsaGen.generateKeyPair(); + + KeyGenerator macGen = KeyGenerator.getInstance("HmacSHA256"); + macGen.init(256, Utils.getRng()); + macKey = macGen.generateKey(); + } + + @BeforeMethod + public void setUp() { + description = new HashMap(); + description.put("TestKey", "test value"); + description = Collections.unmodifiableMap(description); + ctx = new EncryptionContext.Builder().build(); + } + + @Test + public void constructWithMac() throws GeneralSecurityException { + AsymmetricStaticProvider prov = + new AsymmetricStaticProvider( + encryptionPair, macKey, Collections.emptyMap()); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + assertEquals(macKey, eMat.getSigningKey()); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(macKey, dMat.getVerificationKey()); + } + + @Test + public void constructWithSigPair() throws GeneralSecurityException { + AsymmetricStaticProvider prov = + new AsymmetricStaticProvider( + encryptionPair, sigPair, Collections.emptyMap()); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + assertEquals(sigPair.getPrivate(), eMat.getSigningKey()); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(sigPair.getPublic(), dMat.getVerificationKey()); + } + + @Test + public void randomEnvelopeKeys() throws GeneralSecurityException { + AsymmetricStaticProvider prov = + new AsymmetricStaticProvider( + encryptionPair, macKey, Collections.emptyMap()); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + assertEquals(macKey, eMat.getSigningKey()); + + EncryptionMaterials eMat2 = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey2 = eMat2.getEncryptionKey(); + assertEquals(macKey, eMat.getSigningKey()); + + assertFalse("Envelope keys must be different", encryptionKey.equals(encryptionKey2)); + } + + @Test + public void testRefresh() { + // This does nothing, make sure we don't throw and exception. + AsymmetricStaticProvider prov = + new AsymmetricStaticProvider(encryptionPair, macKey, description); + prov.refresh(); + } + + private static EncryptionContext ctx(EncryptionMaterials mat) { + return new EncryptionContext.Builder() + .materialDescription(mat.getMaterialDescription()) + .build(); + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/CachingMostRecentProviderTests.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/CachingMostRecentProviderTests.java new file mode 100644 index 0000000000..f286648332 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/CachingMostRecentProviderTests.java @@ -0,0 +1,610 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertNull; +import static org.testng.AssertJUnit.assertTrue; + +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.DynamoDbEncryptor; +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.MetaStore; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.store.ProviderStore; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import com.amazonaws.services.dynamodbv2.local.embedded.DynamoDBEmbedded; + +public class CachingMostRecentProviderTests { + private static final String TABLE_NAME = "keystoreTable"; + private static final String MATERIAL_NAME = "material"; + private static final String MATERIAL_PARAM = "materialName"; + private static final SecretKey AES_KEY = + new SecretKeySpec(new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, "AES"); + private static final SecretKey HMAC_KEY = + new SecretKeySpec(new byte[] {0, 1, 2, 3, 4, 5, 6, 7}, "HmacSHA256"); + private static final EncryptionMaterialsProvider BASE_PROVIDER = + new SymmetricStaticProvider(AES_KEY, HMAC_KEY); + private static final DynamoDbEncryptor ENCRYPTOR = DynamoDbEncryptor.getInstance(BASE_PROVIDER); + + private DynamoDbClient client; + private Map methodCalls; + private ProvisionedThroughput throughput; + private ProviderStore store; + private EncryptionContext ctx; + + @BeforeMethod + public void setup() { + methodCalls = new HashMap(); + throughput = ProvisionedThroughput.builder().readCapacityUnits(1L).writeCapacityUnits(1L).build(); + + client = instrument(DynamoDBEmbedded.create().dynamoDbClient(), DynamoDbClient.class, methodCalls); + MetaStore.createTable(client, TABLE_NAME, throughput); + store = new MetaStore(client, TABLE_NAME, ENCRYPTOR); + ctx = new EncryptionContext.Builder().build(); + methodCalls.clear(); + } + + @Test + public void testConstructors() { + final CachingMostRecentProvider prov = + new CachingMostRecentProvider(store, MATERIAL_NAME, 100, 1000); + assertEquals(MATERIAL_NAME, prov.getMaterialName()); + assertEquals(100, prov.getTtlInMills()); + assertEquals(-1, prov.getCurrentVersion()); + assertEquals(0, prov.getLastUpdated()); + + final CachingMostRecentProvider prov2 = + new CachingMostRecentProvider(store, MATERIAL_NAME, 500); + assertEquals(MATERIAL_NAME, prov2.getMaterialName()); + assertEquals(500, prov2.getTtlInMills()); + assertEquals(-1, prov2.getCurrentVersion()); + assertEquals(0, prov2.getLastUpdated()); + } + + + @Test + public void testSmallMaxCacheSize() { + final Map attr1 = + Collections.singletonMap(MATERIAL_PARAM, AttributeValue.builder().s("material1").build()); + final EncryptionContext ctx1 = ctx(attr1); + final Map attr2 = + Collections.singletonMap(MATERIAL_PARAM, AttributeValue.builder().s("material2").build()); + final EncryptionContext ctx2 = ctx(attr2); + + final CachingMostRecentProvider prov = new ExtendedProvider(store, 500, 1); + assertNull(methodCalls.get("putItem")); + final EncryptionMaterials eMat1_1 = prov.getEncryptionMaterials(ctx1); + // It's a new provider, so we see a single putItem + assertEquals(1, (int) methodCalls.getOrDefault("putItem", 0)); + methodCalls.clear(); + final EncryptionMaterials eMat1_2 = prov.getEncryptionMaterials(ctx2); + // It's a new provider, so we see a single putItem + assertEquals(1, (int) methodCalls.getOrDefault("putItem", 0)); + methodCalls.clear(); + // Ensure the two materials are, in fact, different + assertFalse(eMat1_1.getSigningKey().equals(eMat1_2.getSigningKey())); + + // Ensure the second set of materials are cached + final EncryptionMaterials eMat2_2 = prov.getEncryptionMaterials(ctx2); + assertTrue("Expected no calls but was " + methodCalls.toString(), methodCalls.isEmpty()); + + // Ensure the first set of materials are no longer cached, due to being the LRU + final EncryptionMaterials eMat2_1 = prov.getEncryptionMaterials(ctx1); + assertEquals(1, (int) methodCalls.getOrDefault("query", 0)); // To find current version + assertEquals(1, (int) methodCalls.getOrDefault("getItem", 0)); + + assertEquals(0, store.getVersionFromMaterialDescription(eMat1_2.getMaterialDescription())); + assertEquals(0, store.getVersionFromMaterialDescription(eMat2_2.getMaterialDescription())); + } + + @Test + public void testSingleVersion() throws InterruptedException { + final CachingMostRecentProvider prov = new CachingMostRecentProvider(store, MATERIAL_NAME, 500); + assertNull(methodCalls.get("putItem")); + final EncryptionMaterials eMat1 = prov.getEncryptionMaterials(ctx); + // It's a new provider, so we see a single putItem + assertEquals(1, (int) methodCalls.getOrDefault("putItem", 0)); + methodCalls.clear(); + // Ensure the cache is working + final EncryptionMaterials eMat2 = prov.getEncryptionMaterials(ctx); + assertTrue("Expected no calls but was " + methodCalls.toString(), methodCalls.isEmpty()); + assertEquals(0, store.getVersionFromMaterialDescription(eMat1.getMaterialDescription())); + assertEquals(0, store.getVersionFromMaterialDescription(eMat2.getMaterialDescription())); + // Let the TTL be exceeded + Thread.sleep(500); + final EncryptionMaterials eMat3 = prov.getEncryptionMaterials(ctx); + assertEquals(2, methodCalls.size()); + assertEquals(1, (int) methodCalls.getOrDefault("query", 0)); // To find current version + assertEquals(1, (int) methodCalls.getOrDefault("getItem", 0)); // To get provider + assertEquals(0, store.getVersionFromMaterialDescription(eMat3.getMaterialDescription())); + + assertEquals(eMat1.getSigningKey(), eMat2.getSigningKey()); + assertEquals(eMat1.getSigningKey(), eMat3.getSigningKey()); + // Check algorithms. Right now we only support AES and HmacSHA256 + assertEquals("AES", eMat1.getEncryptionKey().getAlgorithm()); + assertEquals("HmacSHA256", eMat1.getSigningKey().getAlgorithm()); + + // Ensure we can decrypt all of them without hitting ddb more than the minimum + final CachingMostRecentProvider prov2 = + new CachingMostRecentProvider(store, MATERIAL_NAME, 500); + final DecryptionMaterials dMat1 = prov2.getDecryptionMaterials(ctx(eMat1)); + methodCalls.clear(); + assertEquals(eMat1.getEncryptionKey(), dMat1.getDecryptionKey()); + assertEquals(eMat1.getSigningKey(), dMat1.getVerificationKey()); + final DecryptionMaterials dMat2 = prov2.getDecryptionMaterials(ctx(eMat2)); + assertEquals(eMat2.getEncryptionKey(), dMat2.getDecryptionKey()); + assertEquals(eMat2.getSigningKey(), dMat2.getVerificationKey()); + final DecryptionMaterials dMat3 = prov2.getDecryptionMaterials(ctx(eMat3)); + assertEquals(eMat3.getEncryptionKey(), dMat3.getDecryptionKey()); + assertEquals(eMat3.getSigningKey(), dMat3.getVerificationKey()); + assertTrue("Expected no calls but was " + methodCalls.toString(), methodCalls.isEmpty()); + } + + @Test + public void testSingleVersionWithRefresh() throws InterruptedException { + final CachingMostRecentProvider prov = new CachingMostRecentProvider(store, MATERIAL_NAME, 500); + assertNull(methodCalls.get("putItem")); + final EncryptionMaterials eMat1 = prov.getEncryptionMaterials(ctx); + // It's a new provider, so we see a single putItem + assertEquals(1, (int) methodCalls.getOrDefault("putItem", 0)); + methodCalls.clear(); + // Ensure the cache is working + final EncryptionMaterials eMat2 = prov.getEncryptionMaterials(ctx); + assertTrue("Expected no calls but was " + methodCalls.toString(), methodCalls.isEmpty()); + assertEquals(0, store.getVersionFromMaterialDescription(eMat1.getMaterialDescription())); + assertEquals(0, store.getVersionFromMaterialDescription(eMat2.getMaterialDescription())); + prov.refresh(); + final EncryptionMaterials eMat3 = prov.getEncryptionMaterials(ctx); + assertEquals(1, (int) methodCalls.getOrDefault("query", 0)); // To find current version + assertEquals(1, (int) methodCalls.getOrDefault("getItem", 0)); + assertEquals(0, store.getVersionFromMaterialDescription(eMat3.getMaterialDescription())); + prov.refresh(); + + assertEquals(eMat1.getSigningKey(), eMat2.getSigningKey()); + assertEquals(eMat1.getSigningKey(), eMat3.getSigningKey()); + + // Ensure that after cache refresh we only get one more hit as opposed to multiple + prov.getEncryptionMaterials(ctx); + Thread.sleep(700); + // Force refresh + prov.getEncryptionMaterials(ctx); + methodCalls.clear(); + // Check to ensure no more hits + assertEquals(eMat1.getSigningKey(), prov.getEncryptionMaterials(ctx).getSigningKey()); + assertEquals(eMat1.getSigningKey(), prov.getEncryptionMaterials(ctx).getSigningKey()); + assertEquals(eMat1.getSigningKey(), prov.getEncryptionMaterials(ctx).getSigningKey()); + assertEquals(eMat1.getSigningKey(), prov.getEncryptionMaterials(ctx).getSigningKey()); + assertEquals(eMat1.getSigningKey(), prov.getEncryptionMaterials(ctx).getSigningKey()); + assertTrue("Expected no calls but was " + methodCalls.toString(), methodCalls.isEmpty()); + + // Ensure we can decrypt all of them without hitting ddb more than the minimum + final CachingMostRecentProvider prov2 = + new CachingMostRecentProvider(store, MATERIAL_NAME, 500); + final DecryptionMaterials dMat1 = prov2.getDecryptionMaterials(ctx(eMat1)); + methodCalls.clear(); + assertEquals(eMat1.getEncryptionKey(), dMat1.getDecryptionKey()); + assertEquals(eMat1.getSigningKey(), dMat1.getVerificationKey()); + final DecryptionMaterials dMat2 = prov2.getDecryptionMaterials(ctx(eMat2)); + assertEquals(eMat2.getEncryptionKey(), dMat2.getDecryptionKey()); + assertEquals(eMat2.getSigningKey(), dMat2.getVerificationKey()); + final DecryptionMaterials dMat3 = prov2.getDecryptionMaterials(ctx(eMat3)); + assertEquals(eMat3.getEncryptionKey(), dMat3.getDecryptionKey()); + assertEquals(eMat3.getSigningKey(), dMat3.getVerificationKey()); + assertTrue("Expected no calls but was " + methodCalls.toString(), methodCalls.isEmpty()); + } + + @Test + public void testTwoVersions() throws InterruptedException { + final CachingMostRecentProvider prov = new CachingMostRecentProvider(store, MATERIAL_NAME, 500); + assertNull(methodCalls.get("putItem")); + final EncryptionMaterials eMat1 = prov.getEncryptionMaterials(ctx); + // It's a new provider, so we see a single putItem + assertEquals(1, (int) methodCalls.getOrDefault("putItem", 0)); + methodCalls.clear(); + // Create the new material + store.newProvider(MATERIAL_NAME); + methodCalls.clear(); + + // Ensure the cache is working + final EncryptionMaterials eMat2 = prov.getEncryptionMaterials(ctx); + assertTrue("Expected no calls but was " + methodCalls.toString(), methodCalls.isEmpty()); + assertEquals(0, store.getVersionFromMaterialDescription(eMat1.getMaterialDescription())); + assertEquals(0, store.getVersionFromMaterialDescription(eMat2.getMaterialDescription())); + assertTrue("Expected no calls but was " + methodCalls.toString(), methodCalls.isEmpty()); + // Let the TTL be exceeded + Thread.sleep(500); + final EncryptionMaterials eMat3 = prov.getEncryptionMaterials(ctx); + + assertEquals(1, (int) methodCalls.getOrDefault("query", 0)); // To find current version + assertEquals(1, (int) methodCalls.getOrDefault("getItem", 0)); // To retrieve current version + assertNull(methodCalls.get("putItem")); // No attempt to create a new item + assertEquals(1, store.getVersionFromMaterialDescription(eMat3.getMaterialDescription())); + + assertEquals(eMat1.getSigningKey(), eMat2.getSigningKey()); + assertFalse(eMat1.getSigningKey().equals(eMat3.getSigningKey())); + + // Ensure we can decrypt all of them without hitting ddb more than the minimum + final CachingMostRecentProvider prov2 = + new CachingMostRecentProvider(store, MATERIAL_NAME, 500); + final DecryptionMaterials dMat1 = prov2.getDecryptionMaterials(ctx(eMat1)); + methodCalls.clear(); + assertEquals(eMat1.getEncryptionKey(), dMat1.getDecryptionKey()); + assertEquals(eMat1.getSigningKey(), dMat1.getVerificationKey()); + final DecryptionMaterials dMat2 = prov2.getDecryptionMaterials(ctx(eMat2)); + assertEquals(eMat2.getEncryptionKey(), dMat2.getDecryptionKey()); + assertEquals(eMat2.getSigningKey(), dMat2.getVerificationKey()); + final DecryptionMaterials dMat3 = prov2.getDecryptionMaterials(ctx(eMat3)); + assertEquals(eMat3.getEncryptionKey(), dMat3.getDecryptionKey()); + assertEquals(eMat3.getSigningKey(), dMat3.getVerificationKey()); + // Get item will be hit once for the one old key + assertEquals(1, methodCalls.size()); + assertEquals(1, (int) methodCalls.getOrDefault("getItem", 0)); + } + + @Test + public void testTwoVersionsWithRefresh() throws InterruptedException { + final CachingMostRecentProvider prov = new CachingMostRecentProvider(store, MATERIAL_NAME, 100); + assertNull(methodCalls.get("putItem")); + final EncryptionMaterials eMat1 = prov.getEncryptionMaterials(ctx); + // It's a new provider, so we see a single putItem + assertEquals(1, (int) methodCalls.getOrDefault("putItem", 0)); + methodCalls.clear(); + // Create the new material + store.newProvider(MATERIAL_NAME); + methodCalls.clear(); + // Ensure the cache is working + final EncryptionMaterials eMat2 = prov.getEncryptionMaterials(ctx); + assertTrue("Expected no calls but was " + methodCalls.toString(), methodCalls.isEmpty()); + assertEquals(0, store.getVersionFromMaterialDescription(eMat1.getMaterialDescription())); + assertEquals(0, store.getVersionFromMaterialDescription(eMat2.getMaterialDescription())); + prov.refresh(); + final EncryptionMaterials eMat3 = prov.getEncryptionMaterials(ctx); + assertEquals(1, (int) methodCalls.getOrDefault("query", 0)); // To find current version + assertEquals(1, (int) methodCalls.getOrDefault("getItem", 0)); + assertEquals(1, store.getVersionFromMaterialDescription(eMat3.getMaterialDescription())); + + assertEquals(eMat1.getSigningKey(), eMat2.getSigningKey()); + assertFalse(eMat1.getSigningKey().equals(eMat3.getSigningKey())); + + // Ensure we can decrypt all of them without hitting ddb more than the minimum + final CachingMostRecentProvider prov2 = + new CachingMostRecentProvider(store, MATERIAL_NAME, 500); + final DecryptionMaterials dMat1 = prov2.getDecryptionMaterials(ctx(eMat1)); + methodCalls.clear(); + assertEquals(eMat1.getEncryptionKey(), dMat1.getDecryptionKey()); + assertEquals(eMat1.getSigningKey(), dMat1.getVerificationKey()); + final DecryptionMaterials dMat2 = prov2.getDecryptionMaterials(ctx(eMat2)); + assertEquals(eMat2.getEncryptionKey(), dMat2.getDecryptionKey()); + assertEquals(eMat2.getSigningKey(), dMat2.getVerificationKey()); + final DecryptionMaterials dMat3 = prov2.getDecryptionMaterials(ctx(eMat3)); + assertEquals(eMat3.getEncryptionKey(), dMat3.getDecryptionKey()); + assertEquals(eMat3.getSigningKey(), dMat3.getVerificationKey()); + // Get item will be hit once for the one old key + assertEquals(1, methodCalls.size()); + assertEquals(1, (int) methodCalls.getOrDefault("getItem", 0)); + } + + @Test + public void testSingleVersionTwoMaterials() throws InterruptedException { + final Map attr1 = + Collections.singletonMap(MATERIAL_PARAM, AttributeValue.builder().s("material1").build()); + final EncryptionContext ctx1 = ctx(attr1); + final Map attr2 = + Collections.singletonMap(MATERIAL_PARAM, AttributeValue.builder().s("material2").build()); + final EncryptionContext ctx2 = ctx(attr2); + + final CachingMostRecentProvider prov = new ExtendedProvider(store, 500, 100); + assertNull(methodCalls.get("putItem")); + final EncryptionMaterials eMat1_1 = prov.getEncryptionMaterials(ctx1); + // It's a new provider, so we see a single putItem + assertEquals(1, (int) methodCalls.getOrDefault("putItem", 0)); + methodCalls.clear(); + final EncryptionMaterials eMat1_2 = prov.getEncryptionMaterials(ctx2); + // It's a new provider, so we see a single putItem + assertEquals(1, (int) methodCalls.getOrDefault("putItem", 0)); + methodCalls.clear(); + // Ensure the two materials are, in fact, different + assertFalse(eMat1_1.getSigningKey().equals(eMat1_2.getSigningKey())); + + // Ensure the cache is working + final EncryptionMaterials eMat2_1 = prov.getEncryptionMaterials(ctx1); + assertTrue("Expected no calls but was " + methodCalls.toString(), methodCalls.isEmpty()); + assertEquals(0, store.getVersionFromMaterialDescription(eMat1_1.getMaterialDescription())); + assertEquals(0, store.getVersionFromMaterialDescription(eMat2_1.getMaterialDescription())); + final EncryptionMaterials eMat2_2 = prov.getEncryptionMaterials(ctx2); + assertTrue("Expected no calls but was " + methodCalls.toString(), methodCalls.isEmpty()); + assertEquals(0, store.getVersionFromMaterialDescription(eMat1_2.getMaterialDescription())); + assertEquals(0, store.getVersionFromMaterialDescription(eMat2_2.getMaterialDescription())); + + // Let the TTL be exceeded + Thread.sleep(500); + final EncryptionMaterials eMat3_1 = prov.getEncryptionMaterials(ctx1); + assertEquals(2, methodCalls.size()); + assertEquals(1, (int) methodCalls.get("query")); // To find current version + assertEquals(1, (int) methodCalls.get("getItem")); // To get the provider + assertEquals(0, store.getVersionFromMaterialDescription(eMat3_1.getMaterialDescription())); + methodCalls.clear(); + final EncryptionMaterials eMat3_2 = prov.getEncryptionMaterials(ctx2); + assertEquals(2, methodCalls.size()); + assertEquals(1, (int) methodCalls.get("query")); // To find current version + assertEquals(1, (int) methodCalls.get("getItem")); // To get the provider + assertEquals(0, store.getVersionFromMaterialDescription(eMat3_2.getMaterialDescription())); + + assertEquals(eMat1_1.getSigningKey(), eMat2_1.getSigningKey()); + assertEquals(eMat1_2.getSigningKey(), eMat2_2.getSigningKey()); + assertEquals(eMat1_1.getSigningKey(), eMat3_1.getSigningKey()); + assertEquals(eMat1_2.getSigningKey(), eMat3_2.getSigningKey()); + // Check algorithms. Right now we only support AES and HmacSHA256 + assertEquals("AES", eMat1_1.getEncryptionKey().getAlgorithm()); + assertEquals("AES", eMat1_2.getEncryptionKey().getAlgorithm()); + assertEquals("HmacSHA256", eMat1_1.getSigningKey().getAlgorithm()); + assertEquals("HmacSHA256", eMat1_2.getSigningKey().getAlgorithm()); + + // Ensure we can decrypt all of them without hitting ddb more than the minimum + final CachingMostRecentProvider prov2 = new ExtendedProvider(store, 500, 100); + final DecryptionMaterials dMat1_1 = prov2.getDecryptionMaterials(ctx(eMat1_1, attr1)); + final DecryptionMaterials dMat1_2 = prov2.getDecryptionMaterials(ctx(eMat1_2, attr2)); + methodCalls.clear(); + assertEquals(eMat1_1.getEncryptionKey(), dMat1_1.getDecryptionKey()); + assertEquals(eMat1_2.getEncryptionKey(), dMat1_2.getDecryptionKey()); + assertEquals(eMat1_1.getSigningKey(), dMat1_1.getVerificationKey()); + assertEquals(eMat1_2.getSigningKey(), dMat1_2.getVerificationKey()); + final DecryptionMaterials dMat2_1 = prov2.getDecryptionMaterials(ctx(eMat2_1, attr1)); + final DecryptionMaterials dMat2_2 = prov2.getDecryptionMaterials(ctx(eMat2_2, attr2)); + assertEquals(eMat2_1.getEncryptionKey(), dMat2_1.getDecryptionKey()); + assertEquals(eMat2_2.getEncryptionKey(), dMat2_2.getDecryptionKey()); + assertEquals(eMat2_1.getSigningKey(), dMat2_1.getVerificationKey()); + assertEquals(eMat2_2.getSigningKey(), dMat2_2.getVerificationKey()); + final DecryptionMaterials dMat3_1 = prov2.getDecryptionMaterials(ctx(eMat3_1, attr1)); + final DecryptionMaterials dMat3_2 = prov2.getDecryptionMaterials(ctx(eMat3_2, attr2)); + assertEquals(eMat3_1.getEncryptionKey(), dMat3_1.getDecryptionKey()); + assertEquals(eMat3_2.getEncryptionKey(), dMat3_2.getDecryptionKey()); + assertEquals(eMat3_1.getSigningKey(), dMat3_1.getVerificationKey()); + assertEquals(eMat3_2.getSigningKey(), dMat3_2.getVerificationKey()); + assertTrue("Expected no calls but was " + methodCalls.toString(), methodCalls.isEmpty()); + } + + @Test + public void testSingleVersionWithTwoMaterialsWithRefresh() throws InterruptedException { + final Map attr1 = + Collections.singletonMap(MATERIAL_PARAM, AttributeValue.builder().s("material1").build()); + final EncryptionContext ctx1 = ctx(attr1); + final Map attr2 = + Collections.singletonMap(MATERIAL_PARAM, AttributeValue.builder().s("material2").build()); + final EncryptionContext ctx2 = ctx(attr2); + + final CachingMostRecentProvider prov = new ExtendedProvider(store, 500, 100); + assertNull(methodCalls.get("putItem")); + final EncryptionMaterials eMat1_1 = prov.getEncryptionMaterials(ctx1); + // It's a new provider, so we see a single putItem + assertEquals(1, (int) methodCalls.getOrDefault("putItem", 0)); + methodCalls.clear(); + final EncryptionMaterials eMat1_2 = prov.getEncryptionMaterials(ctx2); + // It's a new provider, so we see a single putItem + assertEquals(1, (int) methodCalls.getOrDefault("putItem", 0)); + methodCalls.clear(); + // Ensure the two materials are, in fact, different + assertFalse(eMat1_1.getSigningKey().equals(eMat1_2.getSigningKey())); + + // Ensure the cache is working + final EncryptionMaterials eMat2_1 = prov.getEncryptionMaterials(ctx1); + assertTrue("Expected no calls but was " + methodCalls.toString(), methodCalls.isEmpty()); + assertEquals(0, store.getVersionFromMaterialDescription(eMat1_1.getMaterialDescription())); + assertEquals(0, store.getVersionFromMaterialDescription(eMat2_1.getMaterialDescription())); + final EncryptionMaterials eMat2_2 = prov.getEncryptionMaterials(ctx2); + assertTrue("Expected no calls but was " + methodCalls.toString(), methodCalls.isEmpty()); + assertEquals(0, store.getVersionFromMaterialDescription(eMat1_2.getMaterialDescription())); + assertEquals(0, store.getVersionFromMaterialDescription(eMat2_2.getMaterialDescription())); + + prov.refresh(); + final EncryptionMaterials eMat3_1 = prov.getEncryptionMaterials(ctx1); + assertEquals(1, (int) methodCalls.getOrDefault("query", 0)); // To find current version + assertEquals(1, (int) methodCalls.getOrDefault("getItem", 0)); + final EncryptionMaterials eMat3_2 = prov.getEncryptionMaterials(ctx2); + assertEquals(2, (int) methodCalls.getOrDefault("query", 0)); // To find current version + assertEquals(2, (int) methodCalls.getOrDefault("getItem", 0)); + assertEquals(0, store.getVersionFromMaterialDescription(eMat3_1.getMaterialDescription())); + assertEquals(0, store.getVersionFromMaterialDescription(eMat3_2.getMaterialDescription())); + prov.refresh(); + + assertEquals(eMat1_1.getSigningKey(), eMat2_1.getSigningKey()); + assertEquals(eMat1_1.getSigningKey(), eMat3_1.getSigningKey()); + assertEquals(eMat1_2.getSigningKey(), eMat2_2.getSigningKey()); + assertEquals(eMat1_2.getSigningKey(), eMat3_2.getSigningKey()); + + // Ensure that after cache refresh we only get one more hit as opposed to multiple + prov.getEncryptionMaterials(ctx1); + prov.getEncryptionMaterials(ctx2); + Thread.sleep(700); + // Force refresh + prov.getEncryptionMaterials(ctx1); + prov.getEncryptionMaterials(ctx2); + methodCalls.clear(); + // Check to ensure no more hits + assertEquals(eMat1_1.getSigningKey(), prov.getEncryptionMaterials(ctx1).getSigningKey()); + assertEquals(eMat1_1.getSigningKey(), prov.getEncryptionMaterials(ctx1).getSigningKey()); + assertEquals(eMat1_1.getSigningKey(), prov.getEncryptionMaterials(ctx1).getSigningKey()); + assertEquals(eMat1_1.getSigningKey(), prov.getEncryptionMaterials(ctx1).getSigningKey()); + assertEquals(eMat1_1.getSigningKey(), prov.getEncryptionMaterials(ctx1).getSigningKey()); + + assertEquals(eMat1_2.getSigningKey(), prov.getEncryptionMaterials(ctx2).getSigningKey()); + assertEquals(eMat1_2.getSigningKey(), prov.getEncryptionMaterials(ctx2).getSigningKey()); + assertEquals(eMat1_2.getSigningKey(), prov.getEncryptionMaterials(ctx2).getSigningKey()); + assertEquals(eMat1_2.getSigningKey(), prov.getEncryptionMaterials(ctx2).getSigningKey()); + assertEquals(eMat1_2.getSigningKey(), prov.getEncryptionMaterials(ctx2).getSigningKey()); + assertTrue("Expected no calls but was " + methodCalls.toString(), methodCalls.isEmpty()); + + // Ensure we can decrypt all of them without hitting ddb more than the minimum + final CachingMostRecentProvider prov2 = new ExtendedProvider(store, 500, 100); + final DecryptionMaterials dMat1_1 = prov2.getDecryptionMaterials(ctx(eMat1_1, attr1)); + final DecryptionMaterials dMat1_2 = prov2.getDecryptionMaterials(ctx(eMat1_2, attr2)); + methodCalls.clear(); + assertEquals(eMat1_1.getEncryptionKey(), dMat1_1.getDecryptionKey()); + assertEquals(eMat1_2.getEncryptionKey(), dMat1_2.getDecryptionKey()); + assertEquals(eMat1_1.getSigningKey(), dMat1_1.getVerificationKey()); + assertEquals(eMat1_2.getSigningKey(), dMat1_2.getVerificationKey()); + final DecryptionMaterials dMat2_1 = prov2.getDecryptionMaterials(ctx(eMat2_1, attr1)); + final DecryptionMaterials dMat2_2 = prov2.getDecryptionMaterials(ctx(eMat2_2, attr2)); + assertEquals(eMat2_1.getEncryptionKey(), dMat2_1.getDecryptionKey()); + assertEquals(eMat2_2.getEncryptionKey(), dMat2_2.getDecryptionKey()); + assertEquals(eMat2_1.getSigningKey(), dMat2_1.getVerificationKey()); + assertEquals(eMat2_2.getSigningKey(), dMat2_2.getVerificationKey()); + final DecryptionMaterials dMat3_1 = prov2.getDecryptionMaterials(ctx(eMat3_1, attr1)); + final DecryptionMaterials dMat3_2 = prov2.getDecryptionMaterials(ctx(eMat3_2, attr2)); + assertEquals(eMat3_1.getEncryptionKey(), dMat3_1.getDecryptionKey()); + assertEquals(eMat3_2.getEncryptionKey(), dMat3_2.getDecryptionKey()); + assertEquals(eMat3_1.getSigningKey(), dMat3_1.getVerificationKey()); + assertEquals(eMat3_2.getSigningKey(), dMat3_2.getVerificationKey()); + assertTrue("Expected no calls but was " + methodCalls.toString(), methodCalls.isEmpty()); + } + + @Test + public void testTwoVersionsWithTwoMaterialsWithRefresh() throws InterruptedException { + final Map attr1 = + Collections.singletonMap(MATERIAL_PARAM, AttributeValue.builder().s("material1").build()); + final EncryptionContext ctx1 = ctx(attr1); + final Map attr2 = + Collections.singletonMap(MATERIAL_PARAM, AttributeValue.builder().s("material2").build()); + final EncryptionContext ctx2 = ctx(attr2); + + final CachingMostRecentProvider prov = new ExtendedProvider(store, 500, 100); + assertNull(methodCalls.get("putItem")); + final EncryptionMaterials eMat1_1 = prov.getEncryptionMaterials(ctx1); + // It's a new provider, so we see a single putItem + assertEquals(1, (int) methodCalls.getOrDefault("putItem", 0)); + methodCalls.clear(); + final EncryptionMaterials eMat1_2 = prov.getEncryptionMaterials(ctx2); + // It's a new provider, so we see a single putItem + assertEquals(1, (int) methodCalls.getOrDefault("putItem", 0)); + methodCalls.clear(); + // Create the new material + store.newProvider("material1"); + store.newProvider("material2"); + methodCalls.clear(); + // Ensure the cache is working + final EncryptionMaterials eMat2_1 = prov.getEncryptionMaterials(ctx1); + final EncryptionMaterials eMat2_2 = prov.getEncryptionMaterials(ctx2); + assertTrue("Expected no calls but was " + methodCalls.toString(), methodCalls.isEmpty()); + assertEquals(0, store.getVersionFromMaterialDescription(eMat1_1.getMaterialDescription())); + assertEquals(0, store.getVersionFromMaterialDescription(eMat2_1.getMaterialDescription())); + assertEquals(0, store.getVersionFromMaterialDescription(eMat1_2.getMaterialDescription())); + assertEquals(0, store.getVersionFromMaterialDescription(eMat2_2.getMaterialDescription())); + prov.refresh(); + final EncryptionMaterials eMat3_1 = prov.getEncryptionMaterials(ctx1); + final EncryptionMaterials eMat3_2 = prov.getEncryptionMaterials(ctx2); + assertEquals(2, (int) methodCalls.getOrDefault("query", 0)); // To find current version + assertEquals(2, (int) methodCalls.getOrDefault("getItem", 0)); + assertEquals(1, store.getVersionFromMaterialDescription(eMat3_1.getMaterialDescription())); + assertEquals(1, store.getVersionFromMaterialDescription(eMat3_2.getMaterialDescription())); + + assertEquals(eMat1_1.getSigningKey(), eMat2_1.getSigningKey()); + assertFalse(eMat1_1.getSigningKey().equals(eMat3_1.getSigningKey())); + assertEquals(eMat1_2.getSigningKey(), eMat2_2.getSigningKey()); + assertFalse(eMat1_2.getSigningKey().equals(eMat3_2.getSigningKey())); + + // Ensure we can decrypt all of them without hitting ddb more than the minimum + final CachingMostRecentProvider prov2 = new ExtendedProvider(store, 500, 100); + final DecryptionMaterials dMat1_1 = prov2.getDecryptionMaterials(ctx(eMat1_1, attr1)); + final DecryptionMaterials dMat1_2 = prov2.getDecryptionMaterials(ctx(eMat1_2, attr2)); + methodCalls.clear(); + assertEquals(eMat1_1.getEncryptionKey(), dMat1_1.getDecryptionKey()); + assertEquals(eMat1_2.getEncryptionKey(), dMat1_2.getDecryptionKey()); + assertEquals(eMat1_1.getSigningKey(), dMat1_1.getVerificationKey()); + assertEquals(eMat1_2.getSigningKey(), dMat1_2.getVerificationKey()); + final DecryptionMaterials dMat2_1 = prov2.getDecryptionMaterials(ctx(eMat2_1, attr1)); + final DecryptionMaterials dMat2_2 = prov2.getDecryptionMaterials(ctx(eMat2_2, attr2)); + assertEquals(eMat2_1.getEncryptionKey(), dMat2_1.getDecryptionKey()); + assertEquals(eMat2_2.getEncryptionKey(), dMat2_2.getDecryptionKey()); + assertEquals(eMat2_1.getSigningKey(), dMat2_1.getVerificationKey()); + assertEquals(eMat2_2.getSigningKey(), dMat2_2.getVerificationKey()); + final DecryptionMaterials dMat3_1 = prov2.getDecryptionMaterials(ctx(eMat3_1, attr1)); + final DecryptionMaterials dMat3_2 = prov2.getDecryptionMaterials(ctx(eMat3_2, attr2)); + assertEquals(eMat3_1.getEncryptionKey(), dMat3_1.getDecryptionKey()); + assertEquals(eMat3_2.getEncryptionKey(), dMat3_2.getDecryptionKey()); + assertEquals(eMat3_1.getSigningKey(), dMat3_1.getVerificationKey()); + assertEquals(eMat3_2.getSigningKey(), dMat3_2.getVerificationKey()); + // Get item will be hit twice, once for each old key + assertEquals(1, methodCalls.size()); + assertEquals(2, (int) methodCalls.getOrDefault("getItem", 0)); + } + + private static EncryptionContext ctx(final Map attr) { + return new EncryptionContext.Builder().attributeValues(attr).build(); + } + + private static EncryptionContext ctx( + final EncryptionMaterials mat, Map attr) { + return new EncryptionContext.Builder() + .attributeValues(attr) + .materialDescription(mat.getMaterialDescription()) + .build(); + } + + private static EncryptionContext ctx(final EncryptionMaterials mat) { + return new EncryptionContext.Builder() + .materialDescription(mat.getMaterialDescription()) + .build(); + } + + private static class ExtendedProvider extends CachingMostRecentProvider { + public ExtendedProvider(ProviderStore keystore, long ttlInMillis, int maxCacheSize) { + super(keystore, null, ttlInMillis, maxCacheSize); + } + + @Override + public long getCurrentVersion() { + throw new UnsupportedOperationException(); + } + + @Override + protected String getMaterialName(final EncryptionContext context) { + return context.getAttributeValues().get(MATERIAL_PARAM).s(); + } + } + + @SuppressWarnings("unchecked") + private static T instrument( + final T obj, final Class clazz, final Map map) { + return (T) + Proxy.newProxyInstance( + clazz.getClassLoader(), + new Class[] {clazz}, + new InvocationHandler() { + private final Object lock = new Object(); + + @Override + public Object invoke(final Object proxy, final Method method, final Object[] args) + throws Throwable { + synchronized (lock) { + try { + final Integer oldCount = map.get(method.getName()); + if (oldCount != null) { + map.put(method.getName(), oldCount + 1); + } else { + map.put(method.getName(), 1); + } + return method.invoke(obj, args); + } catch (final InvocationTargetException ex) { + throw ex.getCause(); + } + } + } + }); + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/DirectKmsMaterialsProviderTest.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/DirectKmsMaterialsProviderTest.java new file mode 100644 index 0000000000..f5832a1e62 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/DirectKmsMaterialsProviderTest.java @@ -0,0 +1,449 @@ +/* + * 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 org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertNotNull; +import static org.testng.AssertJUnit.assertNull; +import static org.testng.AssertJUnit.assertTrue; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.crypto.SecretKey; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DynamoDbException; +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; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.EncryptionContext; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.internal.Base64; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.DecryptionMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.EncryptionMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.WrappedRawMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.testing.FakeKMS; + +public class DirectKmsMaterialsProviderTest { + private FakeKMS kms; + private String keyId; + private Map description; + private EncryptionContext ctx; + + @BeforeMethod + public void setUp() { + description = new HashMap<>(); + description.put("TestKey", "test value"); + description = Collections.unmodifiableMap(description); + ctx = new EncryptionContext.Builder().build(); + kms = new FakeKMS(); + keyId = kms.createKey().keyMetadata().keyId(); + } + + @Test + public void simple() { + DirectKmsMaterialsProvider prov = new DirectKmsMaterialsProvider(kms, keyId); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + Key signingKey = eMat.getSigningKey(); + assertNotNull(signingKey); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(signingKey, dMat.getVerificationKey()); + + String expectedEncAlg = + encryptionKey.getAlgorithm() + "/" + (encryptionKey.getEncoded().length * 8); + String expectedSigAlg = signingKey.getAlgorithm() + "/" + (signingKey.getEncoded().length * 8); + + Map kmsCtx = kms.getSingleEc(); + assertEquals(expectedEncAlg, kmsCtx.get("*" + WrappedRawMaterials.CONTENT_KEY_ALGORITHM + "*")); + assertEquals(expectedSigAlg, kmsCtx.get("*amzn-ddb-sig-alg*")); + } + + @Test + public void simpleWithKmsEc() { + DirectKmsMaterialsProvider prov = new DirectKmsMaterialsProvider(kms, keyId); + + Map attrVals = new HashMap<>(); + attrVals.put("hk", AttributeValue.builder().s("HashKeyValue").build()); + attrVals.put("rk", AttributeValue.builder().s("RangeKeyValue").build()); + + ctx = + new EncryptionContext.Builder() + .hashKeyName("hk") + .rangeKeyName("rk") + .tableName("KmsTableName") + .attributeValues(attrVals) + .build(); + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + Key signingKey = eMat.getSigningKey(); + assertNotNull(signingKey); + Map kmsCtx = kms.getSingleEc(); + assertEquals("HashKeyValue", kmsCtx.get("hk")); + assertEquals("RangeKeyValue", kmsCtx.get("rk")); + assertEquals("KmsTableName", kmsCtx.get("*aws-kms-table*")); + + EncryptionContext dCtx = + new EncryptionContext.Builder(ctx(eMat)) + .hashKeyName("hk") + .rangeKeyName("rk") + .tableName("KmsTableName") + .attributeValues(attrVals) + .build(); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(dCtx); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(signingKey, dMat.getVerificationKey()); + } + + @Test + public void simpleWithKmsEc2() throws GeneralSecurityException { + DirectKmsMaterialsProvider prov = new DirectKmsMaterialsProvider(kms, keyId); + + Map attrVals = new HashMap<>(); + attrVals.put("hk", AttributeValue.builder().n("10").build()); + attrVals.put("rk", AttributeValue.builder().n("20").build()); + + ctx = + new EncryptionContext.Builder() + .hashKeyName("hk") + .rangeKeyName("rk") + .tableName("KmsTableName") + .attributeValues(attrVals) + .build(); + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + Key signingKey = eMat.getSigningKey(); + assertNotNull(signingKey); + Map kmsCtx = kms.getSingleEc(); + assertEquals("10", kmsCtx.get("hk")); + assertEquals("20", kmsCtx.get("rk")); + assertEquals("KmsTableName", kmsCtx.get("*aws-kms-table*")); + + EncryptionContext dCtx = + new EncryptionContext.Builder(ctx(eMat)) + .hashKeyName("hk") + .rangeKeyName("rk") + .tableName("KmsTableName") + .attributeValues(attrVals) + .build(); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(dCtx); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(signingKey, dMat.getVerificationKey()); + } + + @Test + public void simpleWithKmsEc3() throws GeneralSecurityException { + DirectKmsMaterialsProvider prov = new DirectKmsMaterialsProvider(kms, keyId); + + Map attrVals = new HashMap<>(); + attrVals.put( + "hk", AttributeValue.builder() + .b(SdkBytes.fromByteBuffer(ByteBuffer.wrap("Foo".getBytes(StandardCharsets.UTF_8)))) + .build()); + attrVals.put( + "rk", AttributeValue.builder() + .b(SdkBytes.fromByteBuffer(ByteBuffer.wrap("Bar".getBytes(StandardCharsets.UTF_8)))) + .build()); + + ctx = + new EncryptionContext.Builder() + .hashKeyName("hk") + .rangeKeyName("rk") + .tableName("KmsTableName") + .attributeValues(attrVals) + .build(); + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + Key signingKey = eMat.getSigningKey(); + assertNotNull(signingKey); + assertNotNull(signingKey); + Map kmsCtx = kms.getSingleEc(); + assertEquals(Base64.encodeToString("Foo".getBytes(StandardCharsets.UTF_8)), kmsCtx.get("hk")); + assertEquals(Base64.encodeToString("Bar".getBytes(StandardCharsets.UTF_8)), kmsCtx.get("rk")); + assertEquals("KmsTableName", kmsCtx.get("*aws-kms-table*")); + + EncryptionContext dCtx = + new EncryptionContext.Builder(ctx(eMat)) + .hashKeyName("hk") + .rangeKeyName("rk") + .tableName("KmsTableName") + .attributeValues(attrVals) + .build(); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(dCtx); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(signingKey, dMat.getVerificationKey()); + } + + @Test + public void randomEnvelopeKeys() throws GeneralSecurityException { + DirectKmsMaterialsProvider prov = new DirectKmsMaterialsProvider(kms, keyId); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + + EncryptionMaterials eMat2 = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey2 = eMat2.getEncryptionKey(); + + assertFalse("Envelope keys must be different", encryptionKey.equals(encryptionKey2)); + } + + @Test + public void testRefresh() { + // This does nothing, make sure we don't throw and exception. + DirectKmsMaterialsProvider prov = new DirectKmsMaterialsProvider(kms, keyId); + prov.refresh(); + } + + @Test + public void explicitContentKeyAlgorithm() throws GeneralSecurityException { + Map desc = new HashMap<>(); + desc.put(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, "AES"); + + DirectKmsMaterialsProvider prov = new DirectKmsMaterialsProvider(kms, keyId, desc); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals( + "AES", eMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM)); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + } + + @Test + public void explicitContentKeyLength128() throws GeneralSecurityException { + Map desc = new HashMap<>(); + desc.put(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, "AES/128"); + + DirectKmsMaterialsProvider prov = new DirectKmsMaterialsProvider(kms, keyId, desc); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + assertEquals(16, encryptionKey.getEncoded().length); // 128 Bits + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals( + "AES/128", eMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM)); + assertEquals("AES", eMat.getEncryptionKey().getAlgorithm()); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + } + + @Test + public void explicitContentKeyLength256() throws GeneralSecurityException { + Map desc = new HashMap<>(); + desc.put(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, "AES/256"); + + DirectKmsMaterialsProvider prov = new DirectKmsMaterialsProvider(kms, keyId, desc); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + assertEquals(32, encryptionKey.getEncoded().length); // 256 Bits + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals( + "AES/256", eMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM)); + assertEquals("AES", eMat.getEncryptionKey().getAlgorithm()); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + } + + @Test + public void extendedWithDerivedEncryptionKeyId() { + ExtendedKmsMaterialsProvider prov = + new ExtendedKmsMaterialsProvider(kms, keyId, "encryptionKeyId"); + String customKeyId = kms.createKey().keyMetadata().keyId(); + + Map attrVals = new HashMap<>(); + attrVals.put("hk", AttributeValue.builder().n("10").build()); + attrVals.put("rk", AttributeValue.builder().n("20").build()); + attrVals.put("encryptionKeyId", AttributeValue.builder().s(customKeyId).build()); + + ctx = + new EncryptionContext.Builder() + .hashKeyName("hk") + .rangeKeyName("rk") + .tableName("KmsTableName") + .attributeValues(attrVals) + .build(); + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + Key signingKey = eMat.getSigningKey(); + assertNotNull(signingKey); + Map kmsCtx = kms.getSingleEc(); + assertEquals("10", kmsCtx.get("hk")); + assertEquals("20", kmsCtx.get("rk")); + assertEquals("KmsTableName", kmsCtx.get("*aws-kms-table*")); + + EncryptionContext dCtx = + new EncryptionContext.Builder(ctx(eMat)) + .hashKeyName("hk") + .rangeKeyName("rk") + .tableName("KmsTableName") + .attributeValues(attrVals) + .build(); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(dCtx); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(signingKey, dMat.getVerificationKey()); + } + + @Test(expectedExceptions = SdkException.class) + public void encryptionKeyIdMismatch() throws SdkException { + DirectKmsMaterialsProvider directProvider = new DirectKmsMaterialsProvider(kms, keyId); + String customKeyId = kms.createKey().keyMetadata().keyId(); + + Map attrVals = new HashMap<>(); + attrVals.put("hk", AttributeValue.builder().n("10").build()); + attrVals.put("rk", AttributeValue.builder().n("20").build()); + attrVals.put("encryptionKeyId", AttributeValue.builder().s(customKeyId).build()); + + ctx = + new EncryptionContext.Builder() + .hashKeyName("hk") + .rangeKeyName("rk") + .tableName("KmsTableName") + .attributeValues(attrVals) + .build(); + EncryptionMaterials eMat = directProvider.getEncryptionMaterials(ctx); + + EncryptionContext dCtx = + new EncryptionContext.Builder(ctx(eMat)) + .hashKeyName("hk") + .rangeKeyName("rk") + .tableName("KmsTableName") + .attributeValues(attrVals) + .build(); + + ExtendedKmsMaterialsProvider extendedProvider = + new ExtendedKmsMaterialsProvider(kms, keyId, "encryptionKeyId"); + + extendedProvider.getDecryptionMaterials(dCtx); + } + + @Test(expectedExceptions = SdkException.class) + public void missingEncryptionKeyId() throws SdkException { + ExtendedKmsMaterialsProvider prov = + new ExtendedKmsMaterialsProvider(kms, keyId, "encryptionKeyId"); + + Map attrVals = new HashMap<>(); + attrVals.put("hk", AttributeValue.builder().n("10").build()); + attrVals.put("rk", AttributeValue.builder().n("20").build()); + + ctx = + new EncryptionContext.Builder() + .hashKeyName("hk") + .rangeKeyName("rk") + .tableName("KmsTableName") + .attributeValues(attrVals) + .build(); + prov.getEncryptionMaterials(ctx); + } + + @Test + public void generateDataKeyIsCalledWith256NumberOfBits() { + final AtomicBoolean gdkCalled = new AtomicBoolean(false); + KmsClient kmsSpy = + new FakeKMS() { + @Override + public GenerateDataKeyResponse generateDataKey(GenerateDataKeyRequest r) { + gdkCalled.set(true); + assertEquals((Integer) 32, r.numberOfBytes()); + assertNull(r.keySpec()); + return super.generateDataKey(r); + } + }; + assertFalse(gdkCalled.get()); + new DirectKmsMaterialsProvider(kmsSpy, keyId).getEncryptionMaterials(ctx); + assertTrue(gdkCalled.get()); + } + + private static class ExtendedKmsMaterialsProvider extends DirectKmsMaterialsProvider { + private final String encryptionKeyIdAttributeName; + + public ExtendedKmsMaterialsProvider( + KmsClient kms, String encryptionKeyId, String encryptionKeyIdAttributeName) { + super(kms, encryptionKeyId); + + this.encryptionKeyIdAttributeName = encryptionKeyIdAttributeName; + } + + @Override + protected String selectEncryptionKeyId(EncryptionContext context) + throws DynamoDbException { + if (!context.getAttributeValues().containsKey(encryptionKeyIdAttributeName)) { + throw DynamoDbException.create("encryption key attribute is not provided", new Exception()); + } + + return context.getAttributeValues().get(encryptionKeyIdAttributeName).s(); + } + + @Override + protected void validateEncryptionKeyId(String encryptionKeyId, EncryptionContext context) + throws DynamoDbException { + if (!context.getAttributeValues().containsKey(encryptionKeyIdAttributeName)) { + throw DynamoDbException.create("encryption key attribute is not provided", new Exception()); + } + + String customEncryptionKeyId = + context.getAttributeValues().get(encryptionKeyIdAttributeName).s(); + if (!customEncryptionKeyId.equals(encryptionKeyId)) { + throw DynamoDbException.create("encryption key ids do not match.", new Exception()); + } + } + + @Override + protected DecryptResponse decrypt(DecryptRequest request, EncryptionContext context) { + return super.decrypt(request, context); + } + + @Override + protected GenerateDataKeyResponse generateDataKey( + GenerateDataKeyRequest request, EncryptionContext context) { + return super.generateDataKey(request, context); + } + } + + private static EncryptionContext ctx(EncryptionMaterials mat) { + return new EncryptionContext.Builder() + .materialDescription(mat.getMaterialDescription()) + .build(); + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/KeyStoreMaterialsProviderTest.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/KeyStoreMaterialsProviderTest.java new file mode 100644 index 0000000000..406052452e --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/KeyStoreMaterialsProviderTest.java @@ -0,0 +1,315 @@ +/* + * 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 org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertNotNull; +import static org.testng.AssertJUnit.assertNull; +import static org.testng.AssertJUnit.fail; + +import java.io.ByteArrayInputStream; +import java.security.KeyFactory; +import java.security.KeyStore; +import java.security.KeyStore.PasswordProtection; +import java.security.KeyStore.PrivateKeyEntry; +import java.security.KeyStore.SecretKeyEntry; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +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.internal.Utils; + +public class KeyStoreMaterialsProviderTest { + private static final String certPem = + "MIIDbTCCAlWgAwIBAgIJANdRvzVsW1CIMA0GCSqGSIb3DQEBBQUAME0xCzAJBgNV" + + "BAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMQwwCgYDVQQKDANBV1MxGzAZBgNV" + + "BAMMEktleVN0b3JlIFRlc3QgQ2VydDAeFw0xMzA1MDgyMzMyMjBaFw0xMzA2MDcy" + + "MzMyMjBaME0xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMQwwCgYD" + + "VQQKDANBV1MxGzAZBgNVBAMMEktleVN0b3JlIFRlc3QgQ2VydDCCASIwDQYJKoZI" + + "hvcNAQEBBQADggEPADCCAQoCggEBAJ8+umOX8x/Ma4OZishtYpcA676bwK5KScf3" + + "w+YGM37L12KTdnOyieiGtRW8p0fS0YvnhmVTvaky09I33bH+qy9gliuNL2QkyMxp" + + "uu1IwkTKKuB67CaKT6osYJLFxV/OwHcaZnTszzDgbAVg/Z+8IZxhPgxMzMa+7nDn" + + "hEm9Jd+EONq3PnRagnFeLNbMIePprdJzXHyNNiZKRRGQ/Mo9rr7mqMLSKnFNsmzB" + + "OIfeZM8nXeg+cvlmtXl72obwnGGw2ksJfaxTPm4eEhzRoAgkbjPPLHbwiJlc+GwF" + + "i8kh0Y3vQTj/gOFE4nzipkm7ux1lsGHNRVpVDWpjNd8Fl9JFELkCAwEAAaNQME4w" + + "HQYDVR0OBBYEFM0oGUuFAWlLXZaMXoJgGZxWqfOxMB8GA1UdIwQYMBaAFM0oGUuF" + + "AWlLXZaMXoJgGZxWqfOxMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB" + + "AAXCsXeC8ZRxovP0Wc6C5qv3d7dtgJJVzHwoIRt2YR3yScBa1XI40GKT80jP3MYH" + + "8xMu3mBQtcYrgRKZBy4GpHAyxoFTnPcuzq5Fg7dw7fx4E4OKIbWOahdxwtbVxQfZ" + + "UHnGG88Z0bq2twj7dALGyJhUDdiccckJGmJPOFMzjqsvoAu0n/p7eS6y5WZ5ewqw" + + "p7VwYOP3N9wVV7Podmkh1os+eCcp9GoFf0MHBMFXi2Ps2azKx8wHRIA5D1MZv/Va" + + "4L4/oTBKCjORpFlP7EhMksHBYnjqXLDP6awPMAgQNYB5J9zX6GfJsAgly3t4Rjr5" + + "cLuNYBmRuByFGo+SOdrj6D8="; + private static final String keyPem = + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCfPrpjl/MfzGuD" + + "mYrIbWKXAOu+m8CuSknH98PmBjN+y9dik3ZzsonohrUVvKdH0tGL54ZlU72pMtPS" + + "N92x/qsvYJYrjS9kJMjMabrtSMJEyirgeuwmik+qLGCSxcVfzsB3GmZ07M8w4GwF" + + "YP2fvCGcYT4MTMzGvu5w54RJvSXfhDjatz50WoJxXizWzCHj6a3Sc1x8jTYmSkUR" + + "kPzKPa6+5qjC0ipxTbJswTiH3mTPJ13oPnL5ZrV5e9qG8JxhsNpLCX2sUz5uHhIc" + + "0aAIJG4zzyx28IiZXPhsBYvJIdGN70E4/4DhROJ84qZJu7sdZbBhzUVaVQ1qYzXf" + + "BZfSRRC5AgMBAAECggEBAJMwx9eGe5LIwBfDtCPN93LbxwtHq7FtuQS8XrYexTpN" + + "76eN5c7LF+11lauh1HzuwAEw32iJHqVl9aQ5PxFm85O3ExbuSP+ngHJwx/bLacVr" + + "mHYlKGH3Net1WU5Qvz7vO7bbEBjDSj9DMJVIMSWUHv0MZO25jw2lLX/ufrgpvPf7" + + "KXSgXg/8uV7PbnTbBDNlg02u8eOc+IbH4O8XDKAhD+YQ8AE3pxtopJbb912U/cJs" + + "Y0hQ01zbkWYH7wL9BeQmR7+TEjjtr/IInNjnXmaOmSX867/rTSTuozaVrl1Ce7r8" + + "EmUDg9ZLZeKfoNYovMy08wnxWVX2J+WnNDjNiSOm+IECgYEA0v3jtGrOnKbd0d9E" + + "dbyIuhjgnwp+UsgALIiBeJYjhFS9NcWgs+02q/0ztqOK7g088KBBQOmiA+frLIVb" + + "uNCt/3jF6kJvHYkHMZ0eBEstxjVSM2UcxzJ6ceHZ68pmrru74382TewVosxccNy0" + + "glsUWNN0t5KQDcetaycRYg50MmcCgYEAwTb8klpNyQE8AWxVQlbOIEV24iarXxex" + + "7HynIg9lSeTzquZOXjp0m5omQ04psil2gZ08xjiudG+Dm7QKgYQcxQYUtZPQe15K" + + "m+2hQM0jA7tRfM1NAZHoTmUlYhzRNX6GWAqQXOgjOqBocT4ySBXRaSQq9zuZu36s" + + "fI17knap798CgYArDa2yOf0xEAfBdJqmn7MSrlLfgSenwrHuZGhu78wNi7EUUOBq" + + "9qOqUr+DrDmEO+VMgJbwJPxvaZqeehPuUX6/26gfFjFQSI7UO+hNHf4YLPc6D47g" + + "wtcjd9+c8q8jRqGfWWz+V4dOsf7G9PJMi0NKoNN3RgvpE+66J72vUZ26TwKBgEUq" + + "DdfGA7pEetp3kT2iHT9oHlpuRUJRFRv2s015/WQqVR+EOeF5Q2zADZpiTIK+XPGg" + + "+7Rpbem4UYBXPruGM1ZECv3E4AiJhGO0+Nhdln8reswWIc7CEEqf4nXwouNnW2gA" + + "wBTB9Hp0GW8QOKedR80/aTH/X9TCT7R2YRnY6JQ5AoGBAKjgPySgrNDhlJkW7jXR" + + "WiGpjGSAFPT9NMTvEHDo7oLTQ8AcYzcGQ7ISMRdVXR6GJOlFVsH4NLwuHGtcMTPK" + + "zoHbPHJyOn1SgC5tARD/1vm5CsG2hATRpWRQCTJFg5VRJ4R7Pz+HuxY4SoABcPQd" + + "K+MP8GlGqTldC6NaB1s7KuAX"; + + private static SecretKey encryptionKey; + private static SecretKey macKey; + private static KeyStore keyStore; + private static final String password = "Password"; + private static final PasswordProtection passwordProtection = new PasswordProtection(password.toCharArray()); + + private Map description; + private EncryptionContext ctx; + private static PrivateKey privateKey; + private static Certificate certificate; + + + @BeforeClass + public static void setUpBeforeClass() throws Exception { + + KeyGenerator macGen = KeyGenerator.getInstance("HmacSHA256"); + macGen.init(256, Utils.getRng()); + macKey = macGen.generateKey(); + + KeyGenerator aesGen = KeyGenerator.getInstance("AES"); + aesGen.init(128, Utils.getRng()); + encryptionKey = aesGen.generateKey(); + + keyStore = KeyStore.getInstance("jceks"); + keyStore.load(null, password.toCharArray()); + + KeyFactory kf = KeyFactory.getInstance("RSA"); + PKCS8EncodedKeySpec rsaSpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyPem)); + privateKey = kf.generatePrivate(rsaSpec); + CertificateFactory cf = CertificateFactory.getInstance("X509"); + certificate = cf.generateCertificate(new ByteArrayInputStream(Base64.getDecoder().decode(certPem))); + + keyStore.setEntry("enc", new SecretKeyEntry(encryptionKey), passwordProtection); + keyStore.setEntry("sig", new SecretKeyEntry(macKey), passwordProtection); + keyStore.setEntry( + "enc-a", + new PrivateKeyEntry(privateKey, new Certificate[] {certificate}), + passwordProtection); + keyStore.setEntry( + "sig-a", + new PrivateKeyEntry(privateKey, new Certificate[] {certificate}), + passwordProtection); + keyStore.setCertificateEntry("trustedCert", certificate); + } + + @BeforeMethod + public void setUp() { + description = new HashMap<>(); + description.put("TestKey", "test value"); + description = Collections.unmodifiableMap(description); + ctx = EncryptionContext.builder().build(); + } + + + @Test + @SuppressWarnings("unchecked") + public void simpleSymMac() throws Exception { + KeyStoreMaterialsProvider prov = + new KeyStoreMaterialsProvider( + keyStore, "enc", "sig", passwordProtection, passwordProtection, Collections.EMPTY_MAP); + EncryptionMaterials encryptionMaterials = prov.getEncryptionMaterials(ctx); + assertEquals(encryptionKey, encryptionMaterials.getEncryptionKey()); + assertEquals(macKey, encryptionMaterials.getSigningKey()); + + assertEquals(encryptionKey, prov.getDecryptionMaterials(ctx(encryptionMaterials)).getDecryptionKey()); + assertEquals(macKey, prov.getDecryptionMaterials(ctx(encryptionMaterials)).getVerificationKey()); + } + + @Test + @SuppressWarnings("unchecked") + public void simpleSymSig() throws Exception { + KeyStoreMaterialsProvider prov = + new KeyStoreMaterialsProvider( + keyStore, "enc", "sig-a", passwordProtection, passwordProtection, Collections.EMPTY_MAP); + EncryptionMaterials encryptionMaterials = prov.getEncryptionMaterials(ctx); + assertEquals(encryptionKey, encryptionMaterials.getEncryptionKey()); + assertEquals(privateKey, encryptionMaterials.getSigningKey()); + + assertEquals(encryptionKey, prov.getDecryptionMaterials(ctx(encryptionMaterials)).getDecryptionKey()); + assertEquals(certificate.getPublicKey(), prov.getDecryptionMaterials(ctx(encryptionMaterials)).getVerificationKey()); + } + + @Test + public void equalSymDescMac() throws Exception { + KeyStoreMaterialsProvider prov = + new KeyStoreMaterialsProvider( + keyStore, "enc", "sig", passwordProtection, passwordProtection, description); + EncryptionMaterials encryptionMaterials = prov.getEncryptionMaterials(ctx); + assertEquals(encryptionKey, encryptionMaterials.getEncryptionKey()); + assertEquals(macKey, encryptionMaterials.getSigningKey()); + + assertEquals(encryptionKey, prov.getDecryptionMaterials(ctx(encryptionMaterials)).getDecryptionKey()); + assertEquals(macKey, prov.getDecryptionMaterials(ctx(encryptionMaterials)).getVerificationKey()); + } + + @Test + public void superSetSymDescMac() throws Exception { + KeyStoreMaterialsProvider prov = + new KeyStoreMaterialsProvider( + keyStore, "enc", "sig", passwordProtection, passwordProtection, description); + EncryptionMaterials encryptionMaterials = prov.getEncryptionMaterials(ctx); + assertEquals(encryptionKey, encryptionMaterials.getEncryptionKey()); + assertEquals(macKey, encryptionMaterials.getSigningKey()); + Map tmpDesc = + new HashMap<>(encryptionMaterials.getMaterialDescription()); + tmpDesc.put("randomValue", "random"); + + assertEquals(encryptionKey, prov.getDecryptionMaterials(ctx(tmpDesc)).getDecryptionKey()); + assertEquals(macKey, prov.getDecryptionMaterials(ctx(tmpDesc)).getVerificationKey()); + } + + @Test + @SuppressWarnings("unchecked") + public void subSetSymDescMac() throws Exception { + KeyStoreMaterialsProvider prov = + new KeyStoreMaterialsProvider( + keyStore, "enc", "sig", passwordProtection, passwordProtection, description); + EncryptionMaterials encryptionMaterials = prov.getEncryptionMaterials(ctx); + assertEquals(encryptionKey, encryptionMaterials.getEncryptionKey()); + assertEquals(macKey, encryptionMaterials.getSigningKey()); + + assertNull(prov.getDecryptionMaterials(ctx(Collections.EMPTY_MAP))); + } + + + @Test + public void noMatchSymDescMac() throws Exception { + KeyStoreMaterialsProvider prov = new + KeyStoreMaterialsProvider( + keyStore, "enc", "sig", passwordProtection, passwordProtection, description); + EncryptionMaterials encryptionMaterials = prov.getEncryptionMaterials(ctx); + assertEquals(encryptionKey, encryptionMaterials.getEncryptionKey()); + assertEquals(macKey, encryptionMaterials.getSigningKey()); + Map tmpDesc = new HashMap<>(); + tmpDesc.put("randomValue", "random"); + + assertNull(prov.getDecryptionMaterials(ctx(tmpDesc))); + } + + @Test + public void testRefresh() throws Exception { + // Mostly make sure we don't throw an exception + KeyStoreMaterialsProvider prov = + new KeyStoreMaterialsProvider( + keyStore, "enc", "sig", passwordProtection, passwordProtection, description); + prov.refresh(); + } + + @Test + public void asymSimpleMac() throws Exception { + KeyStoreMaterialsProvider prov = + new KeyStoreMaterialsProvider( + keyStore, "enc-a", "sig", passwordProtection, passwordProtection, description); + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + assertEquals(macKey, eMat.getSigningKey()); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(macKey, dMat.getVerificationKey()); + } + + @Test + public void asymSimpleSig() throws Exception { + KeyStoreMaterialsProvider prov = new KeyStoreMaterialsProvider(keyStore, "enc-a", "sig-a", passwordProtection, passwordProtection, description); + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + assertEquals(privateKey, eMat.getSigningKey()); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(certificate.getPublicKey(), dMat.getVerificationKey()); + } + + @Test + public void asymSigVerifyOnly() throws Exception { + KeyStoreMaterialsProvider prov = + new KeyStoreMaterialsProvider( + keyStore, "enc-a", "trustedCert", passwordProtection, null, description); + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + assertNull(eMat.getSigningKey()); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(certificate.getPublicKey(), dMat.getVerificationKey()); + } + + @Test + public void asymSigEncryptOnly() throws Exception { + KeyStoreMaterialsProvider prov = + new KeyStoreMaterialsProvider( + keyStore, "trustedCert", "sig-a", null, passwordProtection, description); + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + assertEquals(privateKey, eMat.getSigningKey()); + + try { + prov.getDecryptionMaterials(ctx(eMat)); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertEquals("No private decryption key provided.", ex.getMessage()); + } + } + + private static EncryptionContext ctx(EncryptionMaterials mat) { + return ctx(mat.getMaterialDescription()); + } + + private static EncryptionContext ctx(Map desc) { + return EncryptionContext.builder() + .materialDescription(desc).build(); + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/SymmetricStaticProviderTest.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/SymmetricStaticProviderTest.java new file mode 100644 index 0000000000..0485d4dff7 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/SymmetricStaticProviderTest.java @@ -0,0 +1,182 @@ +/* + * 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 org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertNull; +import static org.testng.AssertJUnit.assertTrue; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.EncryptionContext; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.EncryptionMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.internal.Utils; + +public class SymmetricStaticProviderTest { + private static SecretKey encryptionKey; + private static SecretKey macKey; + private static KeyPair sigPair; + private Map description; + private EncryptionContext ctx; + + @BeforeClass + public static void setUpClass() throws Exception { + KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA"); + rsaGen.initialize(2048, Utils.getRng()); + sigPair = rsaGen.generateKeyPair(); + + KeyGenerator macGen = KeyGenerator.getInstance("HmacSHA256"); + macGen.init(256, Utils.getRng()); + macKey = macGen.generateKey(); + + KeyGenerator aesGen = KeyGenerator.getInstance("AES"); + aesGen.init(128, Utils.getRng()); + encryptionKey = aesGen.generateKey(); + } + + @BeforeMethod + public void setUp() { + description = new HashMap(); + description.put("TestKey", "test value"); + description = Collections.unmodifiableMap(description); + ctx = new EncryptionContext.Builder().build(); + } + + @Test + public void simpleMac() { + SymmetricStaticProvider prov = + new SymmetricStaticProvider(encryptionKey, macKey, Collections.emptyMap()); + assertEquals(encryptionKey, prov.getEncryptionMaterials(ctx).getEncryptionKey()); + assertEquals(macKey, prov.getEncryptionMaterials(ctx).getSigningKey()); + + assertEquals( + encryptionKey, + prov.getDecryptionMaterials(ctx(Collections.emptyMap())) + .getDecryptionKey()); + assertEquals( + macKey, + prov.getDecryptionMaterials(ctx(Collections.emptyMap())) + .getVerificationKey()); + } + + @Test + public void simpleSig() { + SymmetricStaticProvider prov = + new SymmetricStaticProvider(encryptionKey, sigPair, Collections.emptyMap()); + assertEquals(encryptionKey, prov.getEncryptionMaterials(ctx).getEncryptionKey()); + assertEquals(sigPair.getPrivate(), prov.getEncryptionMaterials(ctx).getSigningKey()); + + assertEquals( + encryptionKey, + prov.getDecryptionMaterials(ctx(Collections.emptyMap())) + .getDecryptionKey()); + assertEquals( + sigPair.getPublic(), + prov.getDecryptionMaterials(ctx(Collections.emptyMap())) + .getVerificationKey()); + } + + @Test + public void equalDescMac() { + SymmetricStaticProvider prov = new SymmetricStaticProvider(encryptionKey, macKey, description); + assertEquals(encryptionKey, prov.getEncryptionMaterials(ctx).getEncryptionKey()); + assertEquals(macKey, prov.getEncryptionMaterials(ctx).getSigningKey()); + assertTrue( + prov.getEncryptionMaterials(ctx) + .getMaterialDescription() + .entrySet() + .containsAll(description.entrySet())); + + assertEquals(encryptionKey, prov.getDecryptionMaterials(ctx(description)).getDecryptionKey()); + assertEquals(macKey, prov.getDecryptionMaterials(ctx(description)).getVerificationKey()); + } + + @Test + public void supersetDescMac() { + SymmetricStaticProvider prov = new SymmetricStaticProvider(encryptionKey, macKey, description); + assertEquals(encryptionKey, prov.getEncryptionMaterials(ctx).getEncryptionKey()); + assertEquals(macKey, prov.getEncryptionMaterials(ctx).getSigningKey()); + assertTrue( + prov.getEncryptionMaterials(ctx) + .getMaterialDescription() + .entrySet() + .containsAll(description.entrySet())); + + Map superSet = new HashMap(description); + superSet.put("NewValue", "super!"); + + assertEquals(encryptionKey, prov.getDecryptionMaterials(ctx(superSet)).getDecryptionKey()); + assertEquals(macKey, prov.getDecryptionMaterials(ctx(superSet)).getVerificationKey()); + } + + @Test + public void subsetDescMac() { + SymmetricStaticProvider prov = new SymmetricStaticProvider(encryptionKey, macKey, description); + assertEquals(encryptionKey, prov.getEncryptionMaterials(ctx).getEncryptionKey()); + assertEquals(macKey, prov.getEncryptionMaterials(ctx).getSigningKey()); + assertTrue( + prov.getEncryptionMaterials(ctx) + .getMaterialDescription() + .entrySet() + .containsAll(description.entrySet())); + + assertNull(prov.getDecryptionMaterials(ctx(Collections.emptyMap()))); + } + + @Test + public void noMatchDescMac() { + SymmetricStaticProvider prov = new SymmetricStaticProvider(encryptionKey, macKey, description); + assertEquals(encryptionKey, prov.getEncryptionMaterials(ctx).getEncryptionKey()); + assertEquals(macKey, prov.getEncryptionMaterials(ctx).getSigningKey()); + assertTrue( + prov.getEncryptionMaterials(ctx) + .getMaterialDescription() + .entrySet() + .containsAll(description.entrySet())); + + Map noMatch = new HashMap(); + noMatch.put("NewValue", "no match!"); + + assertNull(prov.getDecryptionMaterials(ctx(noMatch))); + } + + @Test + public void testRefresh() { + // This does nothing, make sure we don't throw and exception. + SymmetricStaticProvider prov = new SymmetricStaticProvider(encryptionKey, macKey, description); + prov.refresh(); + } + + @SuppressWarnings("unused") + private static EncryptionContext ctx(EncryptionMaterials mat) { + return ctx(mat.getMaterialDescription()); + } + + private static EncryptionContext ctx(Map desc) { + return EncryptionContext.builder() + .materialDescription(desc).build(); + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/WrappedMaterialsProviderTest.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/WrappedMaterialsProviderTest.java new file mode 100644 index 0000000000..5f82b47dd8 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/WrappedMaterialsProviderTest.java @@ -0,0 +1,414 @@ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertNotNull; + +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.materials.WrappedRawMaterials; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +public class WrappedMaterialsProviderTest { + private static SecretKey symEncryptionKey; + private static SecretKey macKey; + private static KeyPair sigPair; + private static KeyPair encryptionPair; + private static SecureRandom rnd; + private Map description; + private EncryptionContext ctx; + + @BeforeClass + public static void setUpClass() throws NoSuchAlgorithmException { + rnd = new SecureRandom(); + KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA"); + rsaGen.initialize(2048, rnd); + sigPair = rsaGen.generateKeyPair(); + encryptionPair = rsaGen.generateKeyPair(); + + KeyGenerator aesGen = KeyGenerator.getInstance("AES"); + aesGen.init(128, rnd); + symEncryptionKey = aesGen.generateKey(); + + KeyGenerator macGen = KeyGenerator.getInstance("HmacSHA256"); + macGen.init(256, rnd); + macKey = macGen.generateKey(); + } + + @BeforeMethod + public void setUp() { + description = new HashMap(); + description.put("TestKey", "test value"); + ctx = new EncryptionContext.Builder().build(); + } + + @Test + public void simpleMac() { + WrappedMaterialsProvider prov = + new WrappedMaterialsProvider( + symEncryptionKey, symEncryptionKey, macKey, Collections.emptyMap()); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey contentEncryptionKey = eMat.getEncryptionKey(); + assertNotNull(contentEncryptionKey); + assertEquals(macKey, eMat.getSigningKey()); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals(contentEncryptionKey, dMat.getDecryptionKey()); + assertEquals(macKey, dMat.getVerificationKey()); + } + + @Test + public void simpleSigPair() { + WrappedMaterialsProvider prov = + new WrappedMaterialsProvider( + symEncryptionKey, symEncryptionKey, sigPair, Collections.emptyMap()); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey contentEncryptionKey = eMat.getEncryptionKey(); + assertNotNull(contentEncryptionKey); + assertEquals(sigPair.getPrivate(), eMat.getSigningKey()); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals(contentEncryptionKey, dMat.getDecryptionKey()); + assertEquals(sigPair.getPublic(), dMat.getVerificationKey()); + } + + @Test + public void randomEnvelopeKeys() { + WrappedMaterialsProvider prov = + new WrappedMaterialsProvider( + symEncryptionKey, symEncryptionKey, macKey, Collections.emptyMap()); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey contentEncryptionKey = eMat.getEncryptionKey(); + assertNotNull(contentEncryptionKey); + assertEquals(macKey, eMat.getSigningKey()); + + EncryptionMaterials eMat2 = prov.getEncryptionMaterials(ctx); + SecretKey contentEncryptionKey2 = eMat2.getEncryptionKey(); + assertEquals(macKey, eMat.getSigningKey()); + + assertFalse( + "Envelope keys must be different", contentEncryptionKey.equals(contentEncryptionKey2)); + } + + @Test + public void testRefresh() { + // This does nothing, make sure we don't throw an exception. + WrappedMaterialsProvider prov = + new WrappedMaterialsProvider( + symEncryptionKey, symEncryptionKey, macKey, Collections.emptyMap()); + prov.refresh(); + } + + @Test + public void wrapUnwrapAsymMatExplicitWrappingAlgorithmPkcs1() { + Map desc = new HashMap(); + desc.put(WrappedRawMaterials.KEY_WRAPPING_ALGORITHM, "RSA/ECB/PKCS1Padding"); + + WrappedMaterialsProvider prov = + new WrappedMaterialsProvider( + encryptionPair.getPublic(), encryptionPair.getPrivate(), sigPair, desc); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey contentEncryptionKey = eMat.getEncryptionKey(); + assertNotNull(contentEncryptionKey); + assertEquals(sigPair.getPrivate(), eMat.getSigningKey()); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals( + "RSA/ECB/PKCS1Padding", + eMat.getMaterialDescription().get(WrappedRawMaterials.KEY_WRAPPING_ALGORITHM)); + assertEquals(contentEncryptionKey, dMat.getDecryptionKey()); + assertEquals(sigPair.getPublic(), dMat.getVerificationKey()); + } + + @Test + public void wrapUnwrapAsymMatExplicitWrappingAlgorithmPkcs2() { + Map desc = new HashMap(); + desc.put(WrappedRawMaterials.KEY_WRAPPING_ALGORITHM, "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); + + WrappedMaterialsProvider prov = + new WrappedMaterialsProvider( + encryptionPair.getPublic(), encryptionPair.getPrivate(), sigPair, desc); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey contentEncryptionKey = eMat.getEncryptionKey(); + assertNotNull(contentEncryptionKey); + assertEquals(sigPair.getPrivate(), eMat.getSigningKey()); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals( + "RSA/ECB/OAEPWithSHA-256AndMGF1Padding", + eMat.getMaterialDescription().get(WrappedRawMaterials.KEY_WRAPPING_ALGORITHM)); + assertEquals(contentEncryptionKey, dMat.getDecryptionKey()); + assertEquals(sigPair.getPublic(), dMat.getVerificationKey()); + } + + @Test + public void wrapUnwrapAsymMatExplicitContentKeyAlgorithm() { + Map desc = new HashMap(); + desc.put(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, "AES"); + + WrappedMaterialsProvider prov = + new WrappedMaterialsProvider( + encryptionPair.getPublic(), + encryptionPair.getPrivate(), + sigPair, + Collections.emptyMap()); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey contentEncryptionKey = eMat.getEncryptionKey(); + assertNotNull(contentEncryptionKey); + assertEquals("AES", contentEncryptionKey.getAlgorithm()); + assertEquals( + "AES", eMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM)); + assertEquals(sigPair.getPrivate(), eMat.getSigningKey()); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals( + "AES", dMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM)); + assertEquals(contentEncryptionKey, dMat.getDecryptionKey()); + assertEquals(sigPair.getPublic(), dMat.getVerificationKey()); + } + + @Test + public void wrapUnwrapAsymMatExplicitContentKeyLength128() { + Map desc = new HashMap(); + desc.put(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, "AES/128"); + + WrappedMaterialsProvider prov = + new WrappedMaterialsProvider( + encryptionPair.getPublic(), encryptionPair.getPrivate(), sigPair, desc); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey contentEncryptionKey = eMat.getEncryptionKey(); + assertNotNull(contentEncryptionKey); + assertEquals("AES", contentEncryptionKey.getAlgorithm()); + assertEquals( + "AES", eMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM)); + assertEquals(16, contentEncryptionKey.getEncoded().length); // 128 Bits + assertEquals(sigPair.getPrivate(), eMat.getSigningKey()); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals( + "AES", dMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM)); + assertEquals(contentEncryptionKey, dMat.getDecryptionKey()); + assertEquals(sigPair.getPublic(), dMat.getVerificationKey()); + } + + @Test + public void wrapUnwrapAsymMatExplicitContentKeyLength256() { + Map desc = new HashMap(); + desc.put(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, "AES/256"); + + WrappedMaterialsProvider prov = + new WrappedMaterialsProvider( + encryptionPair.getPublic(), encryptionPair.getPrivate(), sigPair, desc); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey contentEncryptionKey = eMat.getEncryptionKey(); + assertNotNull(contentEncryptionKey); + assertEquals("AES", contentEncryptionKey.getAlgorithm()); + assertEquals( + "AES", eMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM)); + assertEquals(32, contentEncryptionKey.getEncoded().length); // 256 Bits + assertEquals(sigPair.getPrivate(), eMat.getSigningKey()); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals( + "AES", dMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM)); + assertEquals(contentEncryptionKey, dMat.getDecryptionKey()); + assertEquals(sigPair.getPublic(), dMat.getVerificationKey()); + } + + @Test + public void unwrapAsymMatExplicitEncAlgAes128() { + Map desc = new HashMap(); + desc.put(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, "AES/128"); + + WrappedMaterialsProvider prov = + new WrappedMaterialsProvider( + encryptionPair.getPublic(), encryptionPair.getPrivate(), sigPair, desc); + + // Get materials we can test unwrapping on + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + + // Ensure "AES/128" on the created materials creates the expected key + Map aes128Desc = eMat.getMaterialDescription(); + aes128Desc.put(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, "AES/128"); + EncryptionContext aes128Ctx = + new EncryptionContext.Builder().materialDescription(aes128Desc).build(); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(aes128Ctx); + assertEquals( + "AES/128", dMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM)); + assertEquals("AES", dMat.getDecryptionKey().getAlgorithm()); + assertEquals(eMat.getEncryptionKey(), dMat.getDecryptionKey()); + assertEquals(sigPair.getPublic(), dMat.getVerificationKey()); + } + + @Test + public void unwrapAsymMatExplicitEncAlgAes256() { + Map desc = new HashMap(); + desc.put(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, "AES/256"); + + WrappedMaterialsProvider prov = + new WrappedMaterialsProvider( + encryptionPair.getPublic(), encryptionPair.getPrivate(), sigPair, desc); + + // Get materials we can test unwrapping on + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + + // Ensure "AES/256" on the created materials creates the expected key + Map aes256Desc = eMat.getMaterialDescription(); + aes256Desc.put(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, "AES/256"); + EncryptionContext aes256Ctx = + new EncryptionContext.Builder().materialDescription(aes256Desc).build(); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(aes256Ctx); + assertEquals( + "AES/256", dMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM)); + assertEquals("AES", dMat.getDecryptionKey().getAlgorithm()); + assertEquals(eMat.getEncryptionKey(), dMat.getDecryptionKey()); + assertEquals(sigPair.getPublic(), dMat.getVerificationKey()); + } + + @Test + public void wrapUnwrapSymMatExplicitContentKeyAlgorithm() { + Map desc = new HashMap(); + desc.put(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, "AES"); + + WrappedMaterialsProvider prov = + new WrappedMaterialsProvider(symEncryptionKey, symEncryptionKey, macKey, desc); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey contentEncryptionKey = eMat.getEncryptionKey(); + assertNotNull(contentEncryptionKey); + assertEquals("AES", contentEncryptionKey.getAlgorithm()); + assertEquals( + "AES", eMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM)); + assertEquals(macKey, eMat.getSigningKey()); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals( + "AES", dMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM)); + assertEquals(contentEncryptionKey, dMat.getDecryptionKey()); + assertEquals(macKey, dMat.getVerificationKey()); + } + + @Test + public void wrapUnwrapSymMatExplicitContentKeyLength128() { + Map desc = new HashMap(); + desc.put(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, "AES/128"); + + WrappedMaterialsProvider prov = + new WrappedMaterialsProvider(symEncryptionKey, symEncryptionKey, macKey, desc); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey contentEncryptionKey = eMat.getEncryptionKey(); + assertNotNull(contentEncryptionKey); + assertEquals("AES", contentEncryptionKey.getAlgorithm()); + assertEquals( + "AES", eMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM)); + assertEquals(16, contentEncryptionKey.getEncoded().length); // 128 Bits + assertEquals(macKey, eMat.getSigningKey()); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals( + "AES", dMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM)); + assertEquals(contentEncryptionKey, dMat.getDecryptionKey()); + assertEquals(macKey, dMat.getVerificationKey()); + } + + @Test + public void wrapUnwrapSymMatExplicitContentKeyLength256() { + Map desc = new HashMap(); + desc.put(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, "AES/256"); + + WrappedMaterialsProvider prov = + new WrappedMaterialsProvider(symEncryptionKey, symEncryptionKey, macKey, desc); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey contentEncryptionKey = eMat.getEncryptionKey(); + assertNotNull(contentEncryptionKey); + assertEquals("AES", contentEncryptionKey.getAlgorithm()); + assertEquals( + "AES", eMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM)); + assertEquals(32, contentEncryptionKey.getEncoded().length); // 256 Bits + assertEquals(macKey, eMat.getSigningKey()); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals( + "AES", dMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM)); + assertEquals(contentEncryptionKey, dMat.getDecryptionKey()); + assertEquals(macKey, dMat.getVerificationKey()); + } + + @Test + public void unwrapSymMatExplicitEncAlgAes128() { + Map desc = new HashMap(); + desc.put(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, "AES/128"); + + WrappedMaterialsProvider prov = + new WrappedMaterialsProvider(symEncryptionKey, symEncryptionKey, macKey, desc); + + // Get materials we can test unwrapping on + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + + // Ensure "AES/128" on the created materials creates the expected key + Map aes128Desc = eMat.getMaterialDescription(); + aes128Desc.put(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, "AES/128"); + EncryptionContext aes128Ctx = + new EncryptionContext.Builder().materialDescription(aes128Desc).build(); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(aes128Ctx); + assertEquals( + "AES/128", dMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM)); + assertEquals("AES", dMat.getDecryptionKey().getAlgorithm()); + assertEquals(eMat.getEncryptionKey(), dMat.getDecryptionKey()); + assertEquals(macKey, dMat.getVerificationKey()); + } + + @Test + public void unwrapSymMatExplicitEncAlgAes256() { + Map desc = new HashMap(); + desc.put(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, "AES/256"); + + WrappedMaterialsProvider prov = + new WrappedMaterialsProvider(symEncryptionKey, symEncryptionKey, macKey, desc); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + + Map aes256Desc = eMat.getMaterialDescription(); + aes256Desc.put(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, "AES/256"); + EncryptionContext aes256Ctx = + new EncryptionContext.Builder().materialDescription(aes256Desc).build(); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(aes256Ctx); + assertEquals( + "AES/256", dMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM)); + assertEquals("AES", dMat.getDecryptionKey().getAlgorithm()); + assertEquals(eMat.getEncryptionKey(), dMat.getDecryptionKey()); + assertEquals(macKey, dMat.getVerificationKey()); + } + + private static EncryptionContext ctx(EncryptionMaterials mat) { + return new EncryptionContext.Builder() + .materialDescription(mat.getMaterialDescription()) + .build(); + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/store/MetaStoreTests.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/store/MetaStoreTests.java new file mode 100644 index 0000000000..3449908a6d --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/store/MetaStoreTests.java @@ -0,0 +1,346 @@ +/* + * 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 static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertNotNull; +import static org.testng.AssertJUnit.fail; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.DynamoDbEncryptor; +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.providers.EncryptionMaterialsProvider; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.SymmetricStaticProvider; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.testing.AttributeValueBuilder; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.testing.LocalDynamoDb; + +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; + +public class MetaStoreTests { + private static final String SOURCE_TABLE_NAME = "keystoreTable"; + private static final String DESTINATION_TABLE_NAME = "keystoreDestinationTable"; + private static final String MATERIAL_NAME = "material"; + private static final SecretKey AES_KEY = new SecretKeySpec(new byte[] { 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, "AES"); + private static final SecretKey TARGET_AES_KEY = new SecretKeySpec(new byte[] { 0, + 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30 }, "AES"); + private static final SecretKey HMAC_KEY = new SecretKeySpec(new byte[] { 0, + 1, 2, 3, 4, 5, 6, 7 }, "HmacSHA256"); + private static final SecretKey TARGET_HMAC_KEY = new SecretKeySpec(new byte[] { 0, + 2, 4, 6, 8, 10, 12, 14 }, "HmacSHA256"); + private static final EncryptionMaterialsProvider BASE_PROVIDER = new SymmetricStaticProvider(AES_KEY, HMAC_KEY); + private static final EncryptionMaterialsProvider TARGET_BASE_PROVIDER = new SymmetricStaticProvider(TARGET_AES_KEY, TARGET_HMAC_KEY); + private static final DynamoDbEncryptor ENCRYPTOR = DynamoDbEncryptor.getInstance(BASE_PROVIDER); + private static final DynamoDbEncryptor TARGET_ENCRYPTOR = DynamoDbEncryptor.getInstance(TARGET_BASE_PROVIDER); + + private final LocalDynamoDb localDynamoDb = new LocalDynamoDb(); + private final LocalDynamoDb targetLocalDynamoDb = new LocalDynamoDb(); + private DynamoDbClient client; + private DynamoDbClient targetClient; + private MetaStore store; + private MetaStore targetStore; + private EncryptionContext ctx; + + private static class TestExtraDataSupplier implements MetaStore.ExtraDataSupplier { + + private final Map attributeValueMap; + private final Set signedOnlyFieldNames; + + TestExtraDataSupplier(final Map attributeValueMap, + final Set signedOnlyFieldNames) { + this.attributeValueMap = attributeValueMap; + this.signedOnlyFieldNames = signedOnlyFieldNames; + } + + @Override + public Map getAttributes(String materialName, long version) { + return this.attributeValueMap; + } + + @Override + public Set getSignedOnlyFieldNames() { + return this.signedOnlyFieldNames; + } + } + + @BeforeMethod + public void setup() { + localDynamoDb.start(); + targetLocalDynamoDb.start(); + client = localDynamoDb.createClient(); + targetClient = targetLocalDynamoDb.createClient(); + + MetaStore.createTable(client, SOURCE_TABLE_NAME, ProvisionedThroughput.builder() + .readCapacityUnits(1L) + .writeCapacityUnits(1L) + .build()); + //Creating Targeted DynamoDB Object + MetaStore.createTable(targetClient, DESTINATION_TABLE_NAME, ProvisionedThroughput.builder() + .readCapacityUnits(1L) + .writeCapacityUnits(1L) + .build()); + store = new MetaStore(client, SOURCE_TABLE_NAME, ENCRYPTOR); + targetStore = new MetaStore(targetClient, DESTINATION_TABLE_NAME, TARGET_ENCRYPTOR); + ctx = EncryptionContext.builder().build(); + } + + @AfterMethod + public void stopLocalDynamoDb() { + localDynamoDb.stop(); + targetLocalDynamoDb.stop(); + } + + @Test + public void testNoMaterials() { + assertEquals(-1, store.getMaxVersion(MATERIAL_NAME)); + } + + @Test + public void singleMaterial() { + assertEquals(-1, store.getMaxVersion(MATERIAL_NAME)); + final EncryptionMaterialsProvider prov = store.newProvider(MATERIAL_NAME); + assertEquals(0, store.getMaxVersion(MATERIAL_NAME)); + + final EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + final SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + + final DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals(0, store.getVersionFromMaterialDescription(eMat.getMaterialDescription())); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(eMat.getSigningKey(), dMat.getVerificationKey()); + } + + @Test + public void singleMaterialExplicitAccess() { + assertEquals(-1, store.getMaxVersion(MATERIAL_NAME)); + final EncryptionMaterialsProvider prov1 = store.newProvider(MATERIAL_NAME); + assertEquals(0, store.getMaxVersion(MATERIAL_NAME)); + final EncryptionMaterialsProvider prov2 = store.getProvider(MATERIAL_NAME); + + final EncryptionMaterials eMat = prov1.getEncryptionMaterials(ctx); + final SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + + final DecryptionMaterials dMat = prov2.getDecryptionMaterials(ctx(eMat)); + assertEquals(0, store.getVersionFromMaterialDescription(eMat.getMaterialDescription())); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(eMat.getSigningKey(), dMat.getVerificationKey()); + } + + @Test + public void singleMaterialExplicitAccessWithVersion() { + assertEquals(-1, store.getMaxVersion(MATERIAL_NAME)); + final EncryptionMaterialsProvider prov1 = store.newProvider(MATERIAL_NAME); + assertEquals(0, store.getMaxVersion(MATERIAL_NAME)); + final EncryptionMaterialsProvider prov2 = store.getProvider(MATERIAL_NAME, 0); + + final EncryptionMaterials eMat = prov1.getEncryptionMaterials(ctx); + final SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + + final DecryptionMaterials dMat = prov2.getDecryptionMaterials(ctx(eMat)); + assertEquals(0, store.getVersionFromMaterialDescription(eMat.getMaterialDescription())); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(eMat.getSigningKey(), dMat.getVerificationKey()); + } + + @Test + public void singleMaterialWithImplicitCreation() { + assertEquals(-1, store.getMaxVersion(MATERIAL_NAME)); + final EncryptionMaterialsProvider prov = store.getProvider(MATERIAL_NAME); + assertEquals(0, store.getMaxVersion(MATERIAL_NAME)); + + final EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + final SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + + final DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals(0, store.getVersionFromMaterialDescription(eMat.getMaterialDescription())); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(eMat.getSigningKey(), dMat.getVerificationKey()); + } + + @Test + public void twoDifferentMaterials() { + assertEquals(-1, store.getMaxVersion(MATERIAL_NAME)); + final EncryptionMaterialsProvider prov1 = store.newProvider(MATERIAL_NAME); + assertEquals(0, store.getMaxVersion(MATERIAL_NAME)); + final EncryptionMaterialsProvider prov2 = store.newProvider(MATERIAL_NAME); + assertEquals(1, store.getMaxVersion(MATERIAL_NAME)); + + final EncryptionMaterials eMat = prov1.getEncryptionMaterials(ctx); + assertEquals(0, store.getVersionFromMaterialDescription(eMat.getMaterialDescription())); + final SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + + try { + prov2.getDecryptionMaterials(ctx(eMat)); + fail("Missing expected exception"); + } catch (final DynamoDbEncryptionException ex) { + // Expected Exception + } + final EncryptionMaterials eMat2 = prov2.getEncryptionMaterials(ctx); + assertEquals(1, store.getVersionFromMaterialDescription(eMat2.getMaterialDescription())); + } + + @Test + public void getOrCreateCollision() { + assertEquals(-1, store.getMaxVersion(MATERIAL_NAME)); + final EncryptionMaterialsProvider prov1 = store.getOrCreate(MATERIAL_NAME, 0); + assertEquals(0, store.getMaxVersion(MATERIAL_NAME)); + final EncryptionMaterialsProvider prov2 = store.getOrCreate(MATERIAL_NAME, 0); + + final EncryptionMaterials eMat = prov1.getEncryptionMaterials(ctx); + final SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + + final DecryptionMaterials dMat = prov2.getDecryptionMaterials(ctx(eMat)); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(eMat.getSigningKey(), dMat.getVerificationKey()); + } + + @Test + public void getOrCreateWithContextSupplier() { + final Map attributeValueMap = new HashMap<>(); + attributeValueMap.put("CustomKeyId", AttributeValueBuilder.ofS("testCustomKeyId")); + attributeValueMap.put("KeyToken", AttributeValueBuilder.ofS("testKeyToken")); + + final Set signedOnlyAttributes = new HashSet<>(); + signedOnlyAttributes.add("CustomKeyId"); + + final TestExtraDataSupplier extraDataSupplier = new TestExtraDataSupplier( + attributeValueMap, signedOnlyAttributes); + + final MetaStore metaStore = new MetaStore(client, SOURCE_TABLE_NAME, ENCRYPTOR, extraDataSupplier); + + assertEquals(-1, metaStore.getMaxVersion(MATERIAL_NAME)); + final EncryptionMaterialsProvider prov1 = metaStore.getOrCreate(MATERIAL_NAME, 0); + assertEquals(0, metaStore.getMaxVersion(MATERIAL_NAME)); + final EncryptionMaterialsProvider prov2 = metaStore.getOrCreate(MATERIAL_NAME, 0); + + final EncryptionMaterials eMat = prov1.getEncryptionMaterials(ctx); + final SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + + final DecryptionMaterials dMat = prov2.getDecryptionMaterials(ctx(eMat)); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(eMat.getSigningKey(), dMat.getVerificationKey()); + } + + @Test + public void replicateIntermediateKeysTest() { + assertEquals(-1, store.getMaxVersion(MATERIAL_NAME)); + + final EncryptionMaterialsProvider prov1 = store.getOrCreate(MATERIAL_NAME, 0); + assertEquals(0, store.getMaxVersion(MATERIAL_NAME)); + + store.replicate(MATERIAL_NAME, 0, targetStore); + assertEquals(0, targetStore.getMaxVersion(MATERIAL_NAME)); + + final EncryptionMaterials eMat = prov1.getEncryptionMaterials(ctx); + final DecryptionMaterials dMat = targetStore.getProvider(MATERIAL_NAME, 0).getDecryptionMaterials(ctx(eMat)); + + assertEquals(eMat.getEncryptionKey(), dMat.getDecryptionKey()); + assertEquals(eMat.getSigningKey(), dMat.getVerificationKey()); + } + + @Test(expectedExceptions = IndexOutOfBoundsException.class) + public void replicateIntermediateKeysWhenMaterialNotFoundTest() { + store.replicate(MATERIAL_NAME, 0, targetStore); + } + + @Test + public void newProviderCollision() throws InterruptedException { + final SlowNewProvider slowProv = new SlowNewProvider(); + assertEquals(-1, store.getMaxVersion(MATERIAL_NAME)); + assertEquals(-1, slowProv.slowStore.getMaxVersion(MATERIAL_NAME)); + + slowProv.start(); + Thread.sleep(100); + final EncryptionMaterialsProvider prov1 = store.newProvider(MATERIAL_NAME); + slowProv.join(); + assertEquals(0, store.getMaxVersion(MATERIAL_NAME)); + assertEquals(0, slowProv.slowStore.getMaxVersion(MATERIAL_NAME)); + final EncryptionMaterialsProvider prov2 = slowProv.result; + + final EncryptionMaterials eMat = prov1.getEncryptionMaterials(ctx); + final SecretKey encryptionKey = eMat.getEncryptionKey(); + assertNotNull(encryptionKey); + + final DecryptionMaterials dMat = prov2.getDecryptionMaterials(ctx(eMat)); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(eMat.getSigningKey(), dMat.getVerificationKey()); + } + + @Test(expectedExceptions= IndexOutOfBoundsException.class) + public void invalidVersion() { + store.getProvider(MATERIAL_NAME, 1000); + } + + @Test(expectedExceptions= IllegalArgumentException.class) + public void invalidSignedOnlyField() { + final Map attributeValueMap = new HashMap<>(); + attributeValueMap.put("enc", AttributeValueBuilder.ofS("testEncryptionKey")); + + final Set signedOnlyAttributes = new HashSet<>(); + signedOnlyAttributes.add("enc"); + + final TestExtraDataSupplier extraDataSupplier = new TestExtraDataSupplier( + attributeValueMap, signedOnlyAttributes); + + new MetaStore(client, SOURCE_TABLE_NAME, ENCRYPTOR, extraDataSupplier); + } + + private static EncryptionContext ctx(final EncryptionMaterials mat) { + return EncryptionContext.builder() + .materialDescription(mat.getMaterialDescription()).build(); + } + + private class SlowNewProvider extends Thread { + public volatile EncryptionMaterialsProvider result; + public ProviderStore slowStore = new MetaStore(client, SOURCE_TABLE_NAME, ENCRYPTOR) { + @Override + public EncryptionMaterialsProvider newProvider(final String materialName) { + final long nextId = getMaxVersion(materialName) + 1; + try { + Thread.sleep(1000); + } catch (final InterruptedException e) { + // Ignored + } + return getOrCreate(materialName, nextId); + } + }; + + @Override + public void run() { + result = slowStore.newProvider(MATERIAL_NAME); + } + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/utils/EncryptionContextOperatorsTest.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/utils/EncryptionContextOperatorsTest.java new file mode 100644 index 0000000000..2ed128e9d3 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/utils/EncryptionContextOperatorsTest.java @@ -0,0 +1,164 @@ +/* + * 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 static org.testng.AssertJUnit.assertEquals; +import static software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.utils.EncryptionContextOperators.overrideEncryptionContextTableName; +import static software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.utils.EncryptionContextOperators.overrideEncryptionContextTableNameUsingMap; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import org.testng.annotations.Test; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.EncryptionContext; + +public class EncryptionContextOperatorsTest { + + @Test + public void testCreateEncryptionContextTableNameOverride_expectedOverride() { + Function myNewTableName = overrideEncryptionContextTableName("OriginalTableName", "MyNewTableName"); + + EncryptionContext context = EncryptionContext.builder().tableName("OriginalTableName").build(); + + EncryptionContext newContext = myNewTableName.apply(context); + + assertEquals("OriginalTableName", context.getTableName()); + assertEquals("MyNewTableName", newContext.getTableName()); + } + + /** + * Some pretty clear repetition in null cases. May make sense to replace with data providers or parameterized + * classes for null cases + */ + @Test + public void testNullCasesCreateEncryptionContextTableNameOverride_nullOriginalTableName() { + assertEncryptionContextUnchanged(EncryptionContext.builder().tableName("example").build(), + null, + "MyNewTableName"); + } + + @Test + public void testCreateEncryptionContextTableNameOverride_differentOriginalTableName() { + assertEncryptionContextUnchanged(EncryptionContext.builder().tableName("example").build(), + "DifferentTableName", + "MyNewTableName"); + } + + @Test + public void testNullCasesCreateEncryptionContextTableNameOverride_nullEncryptionContext() { + assertEncryptionContextUnchanged(null, + "DifferentTableName", + "MyNewTableName"); + } + + @Test + public void testCreateEncryptionContextTableNameOverrideMap_expectedOverride() { + Map tableNameOverrides = new HashMap<>(); + tableNameOverrides.put("OriginalTableName", "MyNewTableName"); + + + Function nameOverrideMap = + overrideEncryptionContextTableNameUsingMap(tableNameOverrides); + + EncryptionContext context = EncryptionContext.builder().tableName("OriginalTableName").build(); + + EncryptionContext newContext = nameOverrideMap.apply(context); + + assertEquals("OriginalTableName", context.getTableName()); + assertEquals("MyNewTableName", newContext.getTableName()); + } + + @Test + public void testCreateEncryptionContextTableNameOverrideMap_multipleOverrides() { + Map tableNameOverrides = new HashMap<>(); + tableNameOverrides.put("OriginalTableName1", "MyNewTableName1"); + tableNameOverrides.put("OriginalTableName2", "MyNewTableName2"); + + + Function overrideOperator = + overrideEncryptionContextTableNameUsingMap(tableNameOverrides); + + EncryptionContext context = EncryptionContext.builder().tableName("OriginalTableName1").build(); + + EncryptionContext newContext = overrideOperator.apply(context); + + assertEquals("OriginalTableName1", context.getTableName()); + assertEquals("MyNewTableName1", newContext.getTableName()); + + EncryptionContext context2 = EncryptionContext.builder().tableName("OriginalTableName2").build(); + + EncryptionContext newContext2 = overrideOperator.apply(context2); + + assertEquals("OriginalTableName2", context2.getTableName()); + assertEquals("MyNewTableName2", newContext2.getTableName()); + + } + + + @Test + public void testNullCasesCreateEncryptionContextTableNameOverrideFromMap_nullEncryptionContextTableName() { + Map tableNameOverrides = new HashMap<>(); + tableNameOverrides.put("DifferentTableName", "MyNewTableName"); + assertEncryptionContextUnchangedFromMap(EncryptionContext.builder().build(), + tableNameOverrides); + } + + @Test + public void testNullCasesCreateEncryptionContextTableNameOverrideFromMap_nullEncryptionContext() { + Map tableNameOverrides = new HashMap<>(); + tableNameOverrides.put("DifferentTableName", "MyNewTableName"); + assertEncryptionContextUnchangedFromMap(null, + tableNameOverrides); + } + + + @Test + public void testNullCasesCreateEncryptionContextTableNameOverrideFromMap_nullOriginalTableName() { + Map tableNameOverrides = new HashMap<>(); + tableNameOverrides.put(null, "MyNewTableName"); + assertEncryptionContextUnchangedFromMap(EncryptionContext.builder().tableName("example").build(), + tableNameOverrides); + } + + @Test + public void testNullCasesCreateEncryptionContextTableNameOverrideFromMap_nullNewTableName() { + Map tableNameOverrides = new HashMap<>(); + tableNameOverrides.put("MyOriginalTableName", null); + assertEncryptionContextUnchangedFromMap(EncryptionContext.builder().tableName("MyOriginalTableName").build(), + tableNameOverrides); + } + + + @Test + public void testNullCasesCreateEncryptionContextTableNameOverrideFromMap_nullMap() { + assertEncryptionContextUnchangedFromMap(EncryptionContext.builder().tableName("MyOriginalTableName").build(), + null); + } + + + private void assertEncryptionContextUnchanged(EncryptionContext encryptionContext, String originalTableName, String newTableName) { + Function encryptionContextTableNameOverride = overrideEncryptionContextTableName(originalTableName, newTableName); + EncryptionContext newEncryptionContext = encryptionContextTableNameOverride.apply(encryptionContext); + assertEquals(encryptionContext, newEncryptionContext); + } + + + private void assertEncryptionContextUnchangedFromMap(EncryptionContext encryptionContext, Map overrideMap) { + Function encryptionContextTableNameOverrideFromMap = overrideEncryptionContextTableNameUsingMap(overrideMap); + EncryptionContext newEncryptionContext = encryptionContextTableNameOverrideFromMap.apply(encryptionContext); + assertEquals(encryptionContext, newEncryptionContext); + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/AttributeValueMarshallerTest.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/AttributeValueMarshallerTest.java new file mode 100644 index 0000000000..e098816275 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/AttributeValueMarshallerTest.java @@ -0,0 +1,393 @@ +/* + * 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 static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertNotNull; +import static org.testng.AssertJUnit.fail; +import static software.amazon.cryptools.dynamodbencryptionclientsdk2.internal.AttributeValueMarshaller.marshall; +import static software.amazon.cryptools.dynamodbencryptionclientsdk2.internal.AttributeValueMarshaller.unmarshall; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static java.util.Collections.unmodifiableList; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.testng.annotations.Test; + +import software.amazon.cryptools.dynamodbencryptionclientsdk2.testing.AttributeValueBuilder; + +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class AttributeValueMarshallerTest { + @Test(expectedExceptions = IllegalArgumentException.class) + public void testEmpty() { + AttributeValue av = AttributeValue.builder().build(); + marshall(av); + } + + @Test + public void testNumber() { + AttributeValue av = AttributeValue.builder().n("1337").build(); + assertAttributesAreEqual(av, unmarshall(marshall(av))); + } + + @Test + public void testString() { + AttributeValue av = AttributeValue.builder().s("1337").build(); + assertAttributesAreEqual(av, unmarshall(marshall(av))); + } + + @Test + public void testByteBuffer() { + AttributeValue av = AttributeValue.builder().b(SdkBytes.fromByteArray(new byte[] {0, 1, 2, 3, 4, 5})).build(); + assertAttributesAreEqual(av, unmarshall(marshall(av))); + } + + // We can't use straight .equals for comparison because Attribute Values represents Sets + // as Lists and so incorrectly does an ordered comparison + + @Test + public void testNumberS() { + AttributeValue av = AttributeValue.builder().ns(unmodifiableList(Arrays.asList("1337", "1", "5"))).build(); + assertAttributesAreEqual(av, unmarshall(marshall(av))); + } + + @Test + public void testNumberSOrdering() { + AttributeValue av1 = AttributeValue.builder().ns(unmodifiableList(Arrays.asList("1337", "1", "5"))).build(); + AttributeValue av2 = AttributeValue.builder().ns(unmodifiableList(Arrays.asList("1", "5", "1337"))).build(); + assertAttributesAreEqual(av1, av2); + ByteBuffer buff1 = marshall(av1); + ByteBuffer buff2 = marshall(av2); + assertEquals(buff1, buff2); + } + + @Test + public void testStringS() { + AttributeValue av = AttributeValue.builder().ss(unmodifiableList(Arrays.asList("Bob", "Ann", "5"))).build(); + assertAttributesAreEqual(av, unmarshall(marshall(av))); + } + + @Test + public void testStringSOrdering() { + AttributeValue av1 = AttributeValue.builder().ss(unmodifiableList(Arrays.asList("Bob", "Ann", "5"))).build(); + AttributeValue av2 = AttributeValue.builder().ss(unmodifiableList(Arrays.asList("Ann", "Bob", "5"))).build(); + assertAttributesAreEqual(av1, av2); + ByteBuffer buff1 = marshall(av1); + ByteBuffer buff2 = marshall(av2); + assertEquals(buff1, buff2); + } + + @Test + public void testByteBufferS() { + AttributeValue av = AttributeValue.builder().bs(unmodifiableList( + Arrays.asList(SdkBytes.fromByteArray(new byte[] {0, 1, 2, 3, 4, 5}), + SdkBytes.fromByteArray(new byte[] {5, 4, 3, 2, 1, 0, 0, 0, 5, 6, 7})))).build(); + assertAttributesAreEqual(av, unmarshall(marshall(av))); + } + + @Test + public void testByteBufferSOrdering() { + AttributeValue av1 = AttributeValue.builder().bs(unmodifiableList( + Arrays.asList(SdkBytes.fromByteArray(new byte[] {0, 1, 2, 3, 4, 5}), + SdkBytes.fromByteArray(new byte[] {5, 4, 3, 2, 1, 0, 0, 0, 5, 6, 7})))).build(); + AttributeValue av2 = AttributeValue.builder().bs(unmodifiableList( + Arrays.asList(SdkBytes.fromByteArray(new byte[] {5, 4, 3, 2, 1, 0, 0, 0, 5, 6, 7}), + SdkBytes.fromByteArray(new byte[]{0, 1, 2, 3, 4, 5})))).build(); + + assertAttributesAreEqual(av1, av2); + ByteBuffer buff1 = marshall(av1); + ByteBuffer buff2 = marshall(av2); + assertEquals(buff1, buff2); + } + + @Test + public void testBoolTrue() { + AttributeValue av = AttributeValue.builder().bool(Boolean.TRUE).build(); + assertAttributesAreEqual(av, unmarshall(marshall(av))); + } + + @Test + public void testBoolFalse() { + AttributeValue av = AttributeValue.builder().bool(Boolean.FALSE).build(); + assertAttributesAreEqual(av, unmarshall(marshall(av))); + } + + @Test + public void testNULL() { + AttributeValue av = AttributeValue.builder().nul(Boolean.TRUE).build(); + assertAttributesAreEqual(av, unmarshall(marshall(av))); + } + + @Test(expectedExceptions = NullPointerException.class) + public void testActualNULL() { + unmarshall(marshall(null)); + } + + @Test + public void testEmptyList() { + AttributeValue av = AttributeValue.builder().l(emptyList()).build(); + assertAttributesAreEqual(av, unmarshall(marshall(av))); + } + + @Test + public void testListOfString() { + AttributeValue av = + AttributeValue.builder().l(singletonList(AttributeValue.builder().s("StringValue").build())).build(); + assertAttributesAreEqual(av, unmarshall(marshall(av))); + } + + @Test + public void testList() { + AttributeValue av = AttributeValueBuilder.ofL( + AttributeValueBuilder.ofS("StringValue"), + AttributeValueBuilder.ofN("1000"), + AttributeValueBuilder.ofBool(Boolean.TRUE)); + assertAttributesAreEqual(av, unmarshall(marshall(av))); + } + + @Test + public void testListWithNull() { + final AttributeValue av = AttributeValueBuilder.ofL( + AttributeValueBuilder.ofS("StringValue"), + AttributeValueBuilder.ofN("1000"), + AttributeValueBuilder.ofBool(Boolean.TRUE), + null); + + try { + marshall(av); + } catch (NullPointerException e) { + assertThat(e.getMessage(), + startsWith("Encountered null list entry value while marshalling attribute value")); + } + } + + @Test + public void testListDuplicates() { + AttributeValue av = AttributeValueBuilder.ofL( + AttributeValueBuilder.ofN("1000"), + AttributeValueBuilder.ofN("1000"), + AttributeValueBuilder.ofN("1000"), + AttributeValueBuilder.ofN("1000")); + AttributeValue result = unmarshall(marshall(av)); + assertAttributesAreEqual(av, result); + assertEquals(4, result.l().size()); + } + + @Test + public void testComplexList() { + final List list1 = Arrays.asList( + AttributeValueBuilder.ofS("StringValue"), + AttributeValueBuilder.ofN("1000"), + AttributeValueBuilder.ofBool(Boolean.TRUE)); + final List list22 = Arrays.asList( + AttributeValueBuilder.ofS("AWS"), + AttributeValueBuilder.ofN("-3700"), + AttributeValueBuilder.ofBool(Boolean.FALSE)); + final List list2 = Arrays.asList( + AttributeValueBuilder.ofL(list22), + AttributeValueBuilder.ofNull()); + AttributeValue av = AttributeValueBuilder.ofL( + AttributeValueBuilder.ofS("StringValue1"), + AttributeValueBuilder.ofL(list1), + AttributeValueBuilder.ofN("50"), + AttributeValueBuilder.ofL(list2)); + assertAttributesAreEqual(av, unmarshall(marshall(av))); + } + + @Test + public void testEmptyMap() { + Map map = new HashMap<>(); + AttributeValue av = AttributeValueBuilder.ofM(map); + assertAttributesAreEqual(av, unmarshall(marshall(av))); + } + + @Test + public void testSimpleMap() { + Map map = new HashMap<>(); + map.put("KeyValue", AttributeValueBuilder.ofS("ValueValue")); + AttributeValue av = AttributeValueBuilder.ofM(map); + assertAttributesAreEqual(av, unmarshall(marshall(av))); + } + + @Test + public void testSimpleMapWithNull() { + final Map map = new HashMap<>(); + map.put("KeyValue", AttributeValueBuilder.ofS("ValueValue")); + map.put("NullKeyValue", null); + + final AttributeValue av = AttributeValueBuilder.ofM(map); + + try { + marshall(av); + fail("NullPointerException should have been thrown"); + } catch (NullPointerException e) { + assertThat(e.getMessage(), startsWith("Encountered null map value for key NullKeyValue while marshalling " + + "attribute value")); + } + } + + @Test + public void testMapOrdering() { + LinkedHashMap m1 = new LinkedHashMap<>(); + LinkedHashMap m2 = new LinkedHashMap<>(); + + m1.put("Value1", AttributeValueBuilder.ofN("1")); + m1.put("Value2", AttributeValueBuilder.ofBool(Boolean.TRUE)); + + m2.put("Value2", AttributeValueBuilder.ofBool(Boolean.TRUE)); + m2.put("Value1", AttributeValueBuilder.ofN("1")); + + AttributeValue av1 = AttributeValueBuilder.ofM(m1); + AttributeValue av2 = AttributeValueBuilder.ofM(m2); + + ByteBuffer buff1 = marshall(av1); + ByteBuffer buff2 = marshall(av2); + assertEquals(buff1, buff2); + assertAttributesAreEqual(av1, unmarshall(buff1)); + assertAttributesAreEqual(av1, unmarshall(buff2)); + assertAttributesAreEqual(av2, unmarshall(buff1)); + assertAttributesAreEqual(av2, unmarshall(buff2)); + } + + @Test + public void testComplexMap() { + AttributeValue av = buildComplexAttributeValue(); + assertAttributesAreEqual(av, unmarshall(marshall(av))); + } + + // This test ensures that an AttributeValue marshalled by an older + // version of this library still unmarshalls correctly. It also + // ensures that old and new marshalling is identical. + @Test + public void testVersioningCompatibility() { + AttributeValue newObject = buildComplexAttributeValue(); + byte[] oldBytes = Base64.getDecoder().decode(COMPLEX_ATTRIBUTE_MARSHALLED); + byte[] newBytes = marshall(newObject).array(); + assertThat(oldBytes, is(newBytes)); + + AttributeValue oldObject = unmarshall(ByteBuffer.wrap(oldBytes)); + assertAttributesAreEqual(oldObject, newObject); + } + + private static final String COMPLEX_ATTRIBUTE_MARSHALLED = "AE0AAAADAHM" + + "AAAAJSW5uZXJMaXN0AEwAAAAGAHMAAAALQ29tcGxleExpc3QAbgAAAAE1AGIAA" + + "AAGAAECAwQFAEwAAAAFAD8BAAAAAABMAAAAAQA/AABNAAAAAwBzAAAABFBpbms" + + "AcwAAAAVGbG95ZABzAAAABFRlc3QAPwEAcwAAAAdWZXJzaW9uAG4AAAABMQAAA" + + "E0AAAADAHMAAAAETGlzdABMAAAABQBuAAAAATUAbgAAAAE0AG4AAAABMwBuAAA" + + "AATIAbgAAAAExAHMAAAADTWFwAE0AAAABAHMAAAAGTmVzdGVkAD8BAHMAAAAEV" + + "HJ1ZQA/AQBzAAAACVNpbmdsZU1hcABNAAAAAQBzAAAAA0ZPTwBzAAAAA0JBUgB" + + "zAAAACVN0cmluZ1NldABTAAAAAwAAAANiYXIAAAADYmF6AAAAA2Zvbw=="; + + private static AttributeValue buildComplexAttributeValue() { + Map floydMap = new HashMap<>(); + floydMap.put("Pink", AttributeValueBuilder.ofS("Floyd")); + floydMap.put("Version", AttributeValueBuilder.ofN("1")); + floydMap.put("Test", AttributeValueBuilder.ofBool(Boolean.TRUE)); + List floydList = Arrays.asList( + AttributeValueBuilder.ofBool(Boolean.TRUE), + AttributeValueBuilder.ofNull(), + AttributeValueBuilder.ofNull(), + AttributeValueBuilder.ofL(AttributeValueBuilder.ofBool(Boolean.FALSE)), + AttributeValueBuilder.ofM(floydMap) + ); + + List nestedList = Arrays.asList( + AttributeValueBuilder.ofN("5"), + AttributeValueBuilder.ofN("4"), + AttributeValueBuilder.ofN("3"), + AttributeValueBuilder.ofN("2"), + AttributeValueBuilder.ofN("1") + ); + Map nestedMap = new HashMap<>(); + nestedMap.put("True", AttributeValueBuilder.ofBool(Boolean.TRUE)); + nestedMap.put("List", AttributeValueBuilder.ofL(nestedList)); + nestedMap.put("Map", AttributeValueBuilder.ofM( + Collections.singletonMap("Nested", + AttributeValueBuilder.ofBool(Boolean.TRUE)))); + + List innerList = Arrays.asList( + AttributeValueBuilder.ofS("ComplexList"), + AttributeValueBuilder.ofN("5"), + AttributeValueBuilder.ofB(new byte[] {0, 1, 2, 3, 4, 5}), + AttributeValueBuilder.ofL(floydList), + AttributeValueBuilder.ofNull(), + AttributeValueBuilder.ofM(nestedMap) + ); + + Map result = new HashMap<>(); + result.put("SingleMap", AttributeValueBuilder.ofM( + Collections.singletonMap("FOO", AttributeValueBuilder.ofS("BAR")))); + result.put("InnerList", AttributeValueBuilder.ofL(innerList)); + result.put("StringSet", AttributeValueBuilder.ofSS("foo", "bar", "baz")); + return AttributeValue.builder().m(Collections.unmodifiableMap(result)).build(); + } + + private void assertAttributesAreEqual(AttributeValue o1, AttributeValue o2) { + assertEquals(o1.b(), o2.b()); + assertSetsEqual(o1.bs(), o2.bs()); + assertEquals(o1.n(), o2.n()); + assertSetsEqual(o1.ns(), o2.ns()); + assertEquals(o1.s(), o2.s()); + assertSetsEqual(o1.ss(), o2.ss()); + assertEquals(o1.bool(), o2.bool()); + assertEquals(o1.nul(), o2.nul()); + + if (o1.l() != null) { + assertNotNull(o2.l()); + final List l1 = o1.l(); + final List l2 = o2.l(); + assertEquals(l1.size(), l2.size()); + for (int x = 0; x < l1.size(); ++x) { + assertAttributesAreEqual(l1.get(x), l2.get(x)); + } + } + + if (o1.m() != null) { + assertNotNull(o2.m()); + final Map m1 = o1.m(); + final Map m2 = o2.m(); + assertEquals(m1.size(), m2.size()); + for (Map.Entry entry : m1.entrySet()) { + assertAttributesAreEqual(entry.getValue(), m2.get(entry.getKey())); + } + } + } + + private void assertSetsEqual(Collection c1, Collection c2) { + assertFalse(c1 == null ^ c2 == null); + if (c1 != null) { + Set s1 = new HashSet<>(c1); + Set s2 = new HashSet<>(c2); + assertEquals(s1, s2); + } + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/Base64Tests.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/Base64Tests.java new file mode 100644 index 0000000000..4ec9c03ae4 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/Base64Tests.java @@ -0,0 +1,93 @@ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.internal; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.quicktheories.QuickTheory.qt; +import static org.quicktheories.generators.Generate.byteArrays; +import static org.quicktheories.generators.Generate.bytes; +import static org.quicktheories.generators.SourceDSL.integers; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import org.apache.commons.lang3.StringUtils; +import org.testng.annotations.Test; + +public class Base64Tests { + @Test + public void testBase64EncodeEquivalence() { + qt().forAll( + byteArrays( + integers().between(0, 1000000), bytes(Byte.MIN_VALUE, Byte.MAX_VALUE, (byte) 0))) + .check( + (a) -> { + // Base64 encode using both implementations and check for equality of output + // in case one version produces different output + String sdkV1Base64 = com.amazonaws.util.Base64.encodeAsString(a); + String encryptionClientBase64 = Base64.encodeToString(a); + return StringUtils.equals(sdkV1Base64, encryptionClientBase64); + }); + } + + @Test + public void testBase64DecodeEquivalence() { + qt().forAll( + byteArrays( + integers().between(0, 10000), bytes(Byte.MIN_VALUE, Byte.MAX_VALUE, (byte) 0))) + .as((b) -> java.util.Base64.getMimeEncoder().encodeToString(b)) + .check( + (s) -> { + // Check for equality using the MimeEncoder, which inserts newlines + // The encryptionClient's decoder is expected to ignore them + byte[] sdkV1Bytes = com.amazonaws.util.Base64.decode(s); + byte[] encryptionClientBase64 = Base64.decode(s); + return Arrays.equals(sdkV1Bytes, encryptionClientBase64); + }); + } + + @Test + public void testNullDecodeBehavior() { + byte[] decoded = Base64.decode(null); + assertThat(decoded, equalTo(null)); + } + + @Test + public void testNullDecodeBehaviorSdk1() { + byte[] decoded = com.amazonaws.util.Base64.decode((String) null); + assertThat(decoded, equalTo(null)); + + byte[] decoded2 = com.amazonaws.util.Base64.decode((byte[]) null); + assertThat(decoded2, equalTo(null)); + } + + @Test + public void testBase64PaddingBehavior() { + String testInput = "another one bites the dust"; + String expectedEncoding = "YW5vdGhlciBvbmUgYml0ZXMgdGhlIGR1c3Q="; + assertThat( + Base64.encodeToString(testInput.getBytes(StandardCharsets.UTF_8)), + equalTo(expectedEncoding)); + + String encodingWithoutPadding = "YW5vdGhlciBvbmUgYml0ZXMgdGhlIGR1c3Q"; + assertThat(Base64.decode(encodingWithoutPadding), equalTo(testInput.getBytes())); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testBase64PaddingBehaviorSdk1() { + String testInput = "another one bites the dust"; + String encodingWithoutPadding = "YW5vdGhlciBvbmUgYml0ZXMgdGhlIGR1c3Q"; + com.amazonaws.util.Base64.decode(encodingWithoutPadding); + } + + @Test + public void rfc4648TestVectors() { + assertThat(Base64.encodeToString("".getBytes(StandardCharsets.UTF_8)), equalTo("")); + assertThat(Base64.encodeToString("f".getBytes(StandardCharsets.UTF_8)), equalTo("Zg==")); + assertThat(Base64.encodeToString("fo".getBytes(StandardCharsets.UTF_8)), equalTo("Zm8=")); + assertThat(Base64.encodeToString("foo".getBytes(StandardCharsets.UTF_8)), equalTo("Zm9v")); + assertThat(Base64.encodeToString("foob".getBytes(StandardCharsets.UTF_8)), equalTo("Zm9vYg==")); + assertThat( + Base64.encodeToString("fooba".getBytes(StandardCharsets.UTF_8)), equalTo("Zm9vYmE=")); + assertThat( + Base64.encodeToString("foobar".getBytes(StandardCharsets.UTF_8)), equalTo("Zm9vYmFy")); + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/ByteBufferInputStreamTest.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/ByteBufferInputStreamTest.java new file mode 100644 index 0000000000..71b90c195e --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/ByteBufferInputStreamTest.java @@ -0,0 +1,86 @@ +/* + * 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 static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.testng.annotations.Test; + +public class ByteBufferInputStreamTest { + + @Test + public void testRead() throws IOException { + ByteBufferInputStream bis = new ByteBufferInputStream(ByteBuffer.wrap(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9})); + for (int x = 0; x < 10; ++x) { + assertEquals(10 - x, bis.available()); + assertEquals(x, bis.read()); + } + assertEquals(0, bis.available()); + bis.close(); + } + + @Test + public void testReadByteArray() throws IOException { + ByteBufferInputStream bis = new ByteBufferInputStream(ByteBuffer.wrap(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9})); + assertEquals(10, bis.available()); + + byte[] buff = new byte[4]; + + int len = bis.read(buff); + assertEquals(4, len); + assertEquals(6, bis.available()); + assertThat(buff, is(new byte[] {0, 1, 2, 3})); + + len = bis.read(buff); + assertEquals(4, len); + assertEquals(2, bis.available()); + assertThat(buff, is(new byte[] {4, 5, 6, 7})); + + len = bis.read(buff); + assertEquals(2, len); + assertEquals(0, bis.available()); + assertThat(buff, is(new byte[] {8, 9, 6, 7})); + bis.close(); + } + + @Test + public void testSkip() throws IOException { + ByteBufferInputStream bis = new ByteBufferInputStream(ByteBuffer.wrap(new byte[]{(byte) 0xFA, 15, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9})); + assertEquals(13, bis.available()); + assertEquals(0xFA, bis.read()); + assertEquals(12, bis.available()); + bis.skip(2); + assertEquals(10, bis.available()); + for (int x = 0; x < 10; ++x) { + assertEquals(x, bis.read()); + } + assertEquals(0, bis.available()); + assertEquals(-1, bis.read()); + bis.close(); + } + + @Test + public void testMarkSupported() throws IOException { + try (ByteBufferInputStream bis = new ByteBufferInputStream(ByteBuffer.allocate(0))) { + assertFalse(bis.markSupported()); + } + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/ConcurrentTTLCacheTest.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/ConcurrentTTLCacheTest.java new file mode 100644 index 0000000000..7fcb5b89ab --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/ConcurrentTTLCacheTest.java @@ -0,0 +1,244 @@ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.internal; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import edu.umd.cs.mtc.MultithreadedTestCase; +import edu.umd.cs.mtc.TestFramework; +import java.util.concurrent.TimeUnit; +import org.testng.annotations.Test; + +/* Test specific thread interleavings with behaviors we care about in the + * TTLCache. + */ +public class ConcurrentTTLCacheTest { + + private static final long TTL_GRACE_IN_NANO = TimeUnit.MILLISECONDS.toNanos(500); + private static final long ttlInMillis = 1000; + + @Test + public void testGracePeriodCase() throws Throwable { + TestFramework.runOnce(new GracePeriodCase()); + } + + @Test + public void testExpiredCase() throws Throwable { + TestFramework.runOnce(new ExpiredCase()); + } + + @Test + public void testNewEntryCase() throws Throwable { + TestFramework.runOnce(new NewEntryCase()); + } + + @Test + public void testPutLoadCase() throws Throwable { + TestFramework.runOnce(new PutLoadCase()); + } + + // Ensure the loader is only called once if two threads attempt to load during the grace period + class GracePeriodCase extends MultithreadedTestCase { + TTLCache cache; + TTLCache.EntryLoader loader; + MsClock clock = mock(MsClock.class); + + @Override + public void initialize() { + loader = + spy( + new TTLCache.EntryLoader() { + @Override + public String load(String entryKey) { + // Wait until thread2 finishes to complete load + waitForTick(2); + return "loadedValue"; + } + }); + when(clock.timestampNano()).thenReturn((long) 0); + cache = new TTLCache<>(3, ttlInMillis, loader); + cache.clock = clock; + + // Put an initial value into the cache at time 0 + cache.put("k1", "v1"); + } + + // The thread that first calls load in the grace period and acquires the lock + public void thread1() { + when(clock.timestampNano()).thenReturn(TimeUnit.MILLISECONDS.toNanos(ttlInMillis) + 1); + String loadedValue = cache.load("k1"); + assertTick(2); + // Expect to get back the value calculated from load + assertEquals("loadedValue", loadedValue); + } + + // The thread that calls load in the grace period after the lock has been acquired + public void thread2() { + // Wait until the first thread acquires the lock and starts load + waitForTick(1); + when(clock.timestampNano()).thenReturn(TimeUnit.MILLISECONDS.toNanos(ttlInMillis) + 1); + String loadedValue = cache.load("k1"); + // Expect to get back the original value in the cache + assertEquals("v1", loadedValue); + } + + @Override + public void finish() { + // Ensure the loader was only called once + verify(loader, times(1)).load("k1"); + } + } + + // Ensure the loader is only called once if two threads attempt to load an expired entry. + class ExpiredCase extends MultithreadedTestCase { + TTLCache cache; + TTLCache.EntryLoader loader; + MsClock clock = mock(MsClock.class); + + @Override + public void initialize() { + loader = + spy( + new TTLCache.EntryLoader() { + @Override + public String load(String entryKey) { + // Wait until thread2 is waiting for the lock to complete load + waitForTick(2); + return "loadedValue"; + } + }); + when(clock.timestampNano()).thenReturn((long) 0); + cache = new TTLCache<>(3, ttlInMillis, loader); + cache.clock = clock; + + // Put an initial value into the cache at time 0 + cache.put("k1", "v1"); + } + + // The thread that first calls load after expiration + public void thread1() { + when(clock.timestampNano()) + .thenReturn(TimeUnit.MILLISECONDS.toNanos(ttlInMillis) + TTL_GRACE_IN_NANO + 1); + String loadedValue = cache.load("k1"); + assertTick(2); + // Expect to get back the value calculated from load + assertEquals("loadedValue", loadedValue); + } + + // The thread that calls load after expiration, + // after the first thread calls load, but before + // the new value is put into the cache. + public void thread2() { + // Wait until the first thread acquires the lock and starts load + waitForTick(1); + when(clock.timestampNano()) + .thenReturn(TimeUnit.MILLISECONDS.toNanos(ttlInMillis) + TTL_GRACE_IN_NANO + 1); + String loadedValue = cache.load("k1"); + // Expect to get back the newly loaded value + assertEquals("loadedValue", loadedValue); + // assert that this thread only finishes once the first thread's load does + assertTick(2); + } + + @Override + public void finish() { + // Ensure the loader was only called once + verify(loader, times(1)).load("k1"); + } + } + + // Ensure the loader is only called once if two threads attempt to load the same new entry. + class NewEntryCase extends MultithreadedTestCase { + TTLCache cache; + TTLCache.EntryLoader loader; + MsClock clock = mock(MsClock.class); + + @Override + public void initialize() { + loader = + spy( + new TTLCache.EntryLoader() { + @Override + public String load(String entryKey) { + // Wait until thread2 is blocked to complete load + waitForTick(2); + return "loadedValue"; + } + }); + when(clock.timestampNano()).thenReturn((long) 0); + cache = new TTLCache<>(3, ttlInMillis, loader); + cache.clock = clock; + } + + // The thread that first calls load + public void thread1() { + String loadedValue = cache.load("k1"); + assertTick(2); + // Expect to get back the value calculated from load + assertEquals("loadedValue", loadedValue); + } + + // The thread that calls load after the first thread calls load, + // but before the new value is put into the cache. + public void thread2() { + // Wait until the first thread acquires the lock and starts load + waitForTick(1); + String loadedValue = cache.load("k1"); + // Expect to get back the newly loaded value + assertEquals("loadedValue", loadedValue); + // assert that this thread only finishes once the first thread's load does + assertTick(2); + } + + @Override + public void finish() { + // Ensure the loader was only called once + verify(loader, times(1)).load("k1"); + } + } + + // Ensure the loader blocks put on load/put of the same new entry + class PutLoadCase extends MultithreadedTestCase { + TTLCache cache; + TTLCache.EntryLoader loader; + MsClock clock = mock(MsClock.class); + + @Override + public void initialize() { + loader = + spy( + new TTLCache.EntryLoader() { + @Override + public String load(String entryKey) { + // Wait until the put blocks to complete load + waitForTick(2); + return "loadedValue"; + } + }); + when(clock.timestampNano()).thenReturn((long) 0); + cache = new TTLCache<>(3, ttlInMillis, loader); + cache.clock = clock; + } + + // The thread that first calls load + public void thread1() { + String loadedValue = cache.load("k1"); + // Expect to get back the value calculated from load + assertEquals("loadedValue", loadedValue); + verify(loader, times(1)).load("k1"); + } + + // The thread that calls put during the first thread's load + public void thread2() { + // Wait until the first thread is loading + waitForTick(1); + String previousValue = cache.put("k1", "v1"); + // Expect to get back the value loaded into the cache by thread1 + assertEquals("loadedValue", previousValue); + // assert that this thread was blocked by the first thread + assertTick(2); + } + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/HkdfTests.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/HkdfTests.java new file mode 100644 index 0000000000..b9fdcb1d09 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/HkdfTests.java @@ -0,0 +1,209 @@ +/* + * 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 static org.testng.AssertJUnit.assertArrayEquals; + +import org.testng.annotations.Test; + +public class HkdfTests { + private static final testCase[] testCases = + new testCase[] { + new testCase( + "HmacSHA256", + fromCHex( + "\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b" + + "\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b"), + fromCHex("\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x09\\x0a\\x0b\\x0c"), + fromCHex("\\xf0\\xf1\\xf2\\xf3\\xf4\\xf5\\xf6\\xf7\\xf8\\xf9"), + fromHex( + "3CB25F25FAACD57A90434F64D0362F2A2D2D0A90CF1A5A4C5DB02D56ECC4C5BF34007208D5B887185865")), + new testCase( + "HmacSHA256", + fromCHex( + "\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x09\\x0a\\x0b\\x0c\\x0d" + + "\\x0e\\x0f\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1a\\x1b" + + "\\x1c\\x1d\\x1e\\x1f\\x20\\x21\\x22\\x23\\x24\\x25\\x26\\x27\\x28\\x29" + + "\\x2a\\x2b\\x2c\\x2d\\x2e\\x2f\\x30\\x31\\x32\\x33\\x34\\x35\\x36\\x37" + + "\\x38\\x39\\x3a\\x3b\\x3c\\x3d\\x3e\\x3f\\x40\\x41\\x42\\x43\\x44\\x45" + + "\\x46\\x47\\x48\\x49\\x4a\\x4b\\x4c\\x4d\\x4e\\x4f"), + fromCHex( + "\\x60\\x61\\x62\\x63\\x64\\x65\\x66\\x67\\x68\\x69\\x6a\\x6b\\x6c\\x6d" + + "\\x6e\\x6f\\x70\\x71\\x72\\x73\\x74\\x75\\x76\\x77\\x78\\x79\\x7a\\x7b" + + "\\x7c\\x7d\\x7e\\x7f\\x80\\x81\\x82\\x83\\x84\\x85\\x86\\x87\\x88\\x89" + + "\\x8a\\x8b\\x8c\\x8d\\x8e\\x8f\\x90\\x91\\x92\\x93\\x94\\x95\\x96\\x97" + + "\\x98\\x99\\x9a\\x9b\\x9c\\x9d\\x9e\\x9f\\xa0\\xa1\\xa2\\xa3\\xa4\\xa5" + + "\\xa6\\xa7\\xa8\\xa9\\xaa\\xab\\xac\\xad\\xae\\xaf"), + fromCHex( + "\\xb0\\xb1\\xb2\\xb3\\xb4\\xb5\\xb6\\xb7\\xb8\\xb9\\xba\\xbb\\xbc\\xbd" + + "\\xbe\\xbf\\xc0\\xc1\\xc2\\xc3\\xc4\\xc5\\xc6\\xc7\\xc8\\xc9\\xca\\xcb" + + "\\xcc\\xcd\\xce\\xcf\\xd0\\xd1\\xd2\\xd3\\xd4\\xd5\\xd6\\xd7\\xd8\\xd9" + + "\\xda\\xdb\\xdc\\xdd\\xde\\xdf\\xe0\\xe1\\xe2\\xe3\\xe4\\xe5\\xe6\\xe7" + + "\\xe8\\xe9\\xea\\xeb\\xec\\xed\\xee\\xef\\xf0\\xf1\\xf2\\xf3\\xf4\\xf5" + + "\\xf6\\xf7\\xf8\\xf9\\xfa\\xfb\\xfc\\xfd\\xfe\\xff"), + fromHex( + "B11E398DC80327A1C8E7F78C596A4934" + + "4F012EDA2D4EFAD8A050CC4C19AFA97C" + + "59045A99CAC7827271CB41C65E590E09" + + "DA3275600C2F09B8367793A9ACA3DB71" + + "CC30C58179EC3E87C14C01D5C1F3434F" + + "1D87")), + new testCase( + "HmacSHA256", + fromCHex( + "\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b" + + "\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b"), + new byte[0], + new byte[0], + fromHex( + "8DA4E775A563C18F715F802A063C5A31" + + "B8A11F5C5EE1879EC3454E5F3C738D2D" + + "9D201395FAA4B61A96C8")), + new testCase( + "HmacSHA1", + fromCHex("\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b"), + fromCHex("\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x09\\x0a\\x0b\\x0c"), + fromCHex("\\xf0\\xf1\\xf2\\xf3\\xf4\\xf5\\xf6\\xf7\\xf8\\xf9"), + fromHex( + "085A01EA1B10F36933068B56EFA5AD81" + + "A4F14B822F5B091568A9CDD4F155FDA2" + + "C22E422478D305F3F896")), + new testCase( + "HmacSHA1", + fromCHex( + "\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x09\\x0a\\x0b\\x0c\\x0d" + + "\\x0e\\x0f\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1a\\x1b" + + "\\x1c\\x1d\\x1e\\x1f\\x20\\x21\\x22\\x23\\x24\\x25\\x26\\x27\\x28\\x29" + + "\\x2a\\x2b\\x2c\\x2d\\x2e\\x2f\\x30\\x31\\x32\\x33\\x34\\x35\\x36\\x37" + + "\\x38\\x39\\x3a\\x3b\\x3c\\x3d\\x3e\\x3f\\x40\\x41\\x42\\x43\\x44\\x45" + + "\\x46\\x47\\x48\\x49\\x4a\\x4b\\x4c\\x4d\\x4e\\x4f"), + fromCHex( + "\\x60\\x61\\x62\\x63\\x64\\x65\\x66\\x67\\x68\\x69\\x6A\\x6B\\x6C\\x6D" + + "\\x6E\\x6F\\x70\\x71\\x72\\x73\\x74\\x75\\x76\\x77\\x78\\x79\\x7A\\x7B" + + "\\x7C\\x7D\\x7E\\x7F\\x80\\x81\\x82\\x83\\x84\\x85\\x86\\x87\\x88\\x89" + + "\\x8A\\x8B\\x8C\\x8D\\x8E\\x8F\\x90\\x91\\x92\\x93\\x94\\x95\\x96\\x97" + + "\\x98\\x99\\x9A\\x9B\\x9C\\x9D\\x9E\\x9F\\xA0\\xA1\\xA2\\xA3\\xA4\\xA5" + + "\\xA6\\xA7\\xA8\\xA9\\xAA\\xAB\\xAC\\xAD\\xAE\\xAF"), + fromCHex( + "\\xB0\\xB1\\xB2\\xB3\\xB4\\xB5\\xB6\\xB7\\xB8\\xB9\\xBA\\xBB\\xBC\\xBD" + + "\\xBE\\xBF\\xC0\\xC1\\xC2\\xC3\\xC4\\xC5\\xC6\\xC7\\xC8\\xC9\\xCA\\xCB" + + "\\xCC\\xCD\\xCE\\xCF\\xD0\\xD1\\xD2\\xD3\\xD4\\xD5\\xD6\\xD7\\xD8\\xD9" + + "\\xDA\\xDB\\xDC\\xDD\\xDE\\xDF\\xE0\\xE1\\xE2\\xE3\\xE4\\xE5\\xE6\\xE7" + + "\\xE8\\xE9\\xEA\\xEB\\xEC\\xED\\xEE\\xEF\\xF0\\xF1\\xF2\\xF3\\xF4\\xF5" + + "\\xF6\\xF7\\xF8\\xF9\\xFA\\xFB\\xFC\\xFD\\xFE\\xFF"), + fromHex( + "0BD770A74D1160F7C9F12CD5912A06EB" + + "FF6ADCAE899D92191FE4305673BA2FFE" + + "8FA3F1A4E5AD79F3F334B3B202B2173C" + + "486EA37CE3D397ED034C7F9DFEB15C5E" + + "927336D0441F4C4300E2CFF0D0900B52D3B4")), + new testCase( + "HmacSHA1", + fromCHex( + "\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b" + + "\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b\\x0b"), + new byte[0], + new byte[0], + fromHex("0AC1AF7002B3D761D1E55298DA9D0506" + "B9AE52057220A306E07B6B87E8DF21D0")), + new testCase( + "HmacSHA1", + fromCHex( + "\\x0c\\x0c\\x0c\\x0c\\x0c\\x0c\\x0c\\x0c\\x0c\\x0c\\x0c\\x0c\\x0c\\x0c" + + "\\x0c\\x0c\\x0c\\x0c\\x0c\\x0c\\x0c\\x0c"), + null, + new byte[0], + fromHex( + "2C91117204D745F3500D636A62F64F0A" + + "B3BAE548AA53D423B0D1F27EBBA6F5E5" + + "673A081D70CCE7ACFC48")) + }; + + @Test + public void rfc5869Tests() throws Exception { + for (int x = 0; x < testCases.length; x++) { + testCase trial = testCases[x]; + System.out.println("Test case A." + (x + 1)); + Hkdf kdf = Hkdf.getInstance(trial.algo); + kdf.init(trial.ikm, trial.salt); + byte[] result = kdf.deriveKey(trial.info, trial.expected.length); + assertArrayEquals("Trial A." + x, trial.expected, result); + } + } + + @Test + public void nullTests() throws Exception { + testCase trial = testCases[0]; + Hkdf kdf = Hkdf.getInstance(trial.algo); + kdf.init(trial.ikm, trial.salt); + // Just ensuring no exceptions are thrown + kdf.deriveKey((String) null, 16); + kdf.deriveKey((byte[]) null, 16); + } + + @Test + public void defaultSalt() throws Exception { + // Tests all the different ways to get the default salt + + testCase trial = testCases[0]; + Hkdf kdf1 = Hkdf.getInstance(trial.algo); + kdf1.init(trial.ikm, null); + Hkdf kdf2 = Hkdf.getInstance(trial.algo); + kdf2.init(trial.ikm, new byte[0]); + Hkdf kdf3 = Hkdf.getInstance(trial.algo); + kdf3.init(trial.ikm); + Hkdf kdf4 = Hkdf.getInstance(trial.algo); + kdf4.init(trial.ikm, new byte[32]); + + byte[] key1 = kdf1.deriveKey("Test", 16); + byte[] key2 = kdf2.deriveKey("Test", 16); + byte[] key3 = kdf3.deriveKey("Test", 16); + byte[] key4 = kdf4.deriveKey("Test", 16); + + assertArrayEquals(key1, key2); + assertArrayEquals(key1, key3); + assertArrayEquals(key1, key4); + } + + private static byte[] fromHex(String data) { + byte[] result = new byte[data.length() / 2]; + for (int x = 0; x < result.length; x++) { + result[x] = (byte) Integer.parseInt(data.substring(2 * x, 2 * x + 2), 16); + } + return result; + } + + private static byte[] fromCHex(String data) { + byte[] result = new byte[data.length() / 4]; + for (int x = 0; x < result.length; x++) { + result[x] = (byte) Integer.parseInt(data.substring(4 * x + 2, 4 * x + 4), 16); + } + return result; + } + + private static class testCase { + public final String algo; + public final byte[] ikm; + public final byte[] salt; + public final byte[] info; + public final byte[] expected; + + public testCase(String algo, byte[] ikm, byte[] salt, byte[] info, byte[] expected) { + super(); + this.algo = algo; + this.ikm = ikm; + this.salt = salt; + this.info = info; + this.expected = expected; + } + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/LRUCacheTest.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/LRUCacheTest.java new file mode 100644 index 0000000000..8f56d35b96 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/LRUCacheTest.java @@ -0,0 +1,85 @@ +/* + * 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 static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertNull; +import static org.testng.AssertJUnit.assertTrue; + +import org.testng.annotations.Test; + +public class LRUCacheTest { + @Test + public void test() { + final LRUCache cache = new LRUCache(3); + assertEquals(0, cache.size()); + assertEquals(3, cache.getMaxSize()); + cache.add("k1", "v1"); + assertTrue(cache.size() == 1); + cache.add("k1", "v11"); + assertTrue(cache.size() == 1); + cache.add("k2", "v2"); + assertTrue(cache.size() == 2); + cache.add("k3", "v3"); + assertTrue(cache.size() == 3); + assertEquals("v11", cache.get("k1")); + assertEquals("v2", cache.get("k2")); + assertEquals("v3", cache.get("k3")); + cache.add("k4", "v4"); + assertTrue(cache.size() == 3); + assertNull(cache.get("k1")); + assertEquals("v4", cache.get("k4")); + assertEquals("v2", cache.get("k2")); + assertEquals("v3", cache.get("k3")); + assertTrue(cache.size() == 3); + cache.add("k5", "v5"); + assertNull(cache.get("k4")); + assertEquals("v5", cache.get("k5")); + assertEquals("v2", cache.get("k2")); + assertEquals("v3", cache.get("k3")); + cache.clear(); + assertEquals(0, cache.size()); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testZeroSize() { + new LRUCache(0); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testIllegalArgument() { + new LRUCache(-1); + } + + @Test + public void testSingleEntry() { + final LRUCache cache = new LRUCache(1); + assertTrue(cache.size() == 0); + cache.add("k1", "v1"); + assertTrue(cache.size() == 1); + cache.add("k1", "v11"); + assertTrue(cache.size() == 1); + assertEquals("v11", cache.get("k1")); + + cache.add("k2", "v2"); + assertTrue(cache.size() == 1); + assertEquals("v2", cache.get("k2")); + assertNull(cache.get("k1")); + + cache.add("k3", "v3"); + assertTrue(cache.size() == 1); + assertEquals("v3", cache.get("k3")); + assertNull(cache.get("k2")); + } + +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/TTLCacheTest.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/TTLCacheTest.java new file mode 100644 index 0000000000..55e246f391 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/TTLCacheTest.java @@ -0,0 +1,372 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.cryptools.dynamodbencryptionclientsdk2.internal; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertThrows; +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertNull; +import static org.testng.AssertJUnit.assertTrue; + +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import org.testng.annotations.Test; + +public class TTLCacheTest { + + private static final long TTL_GRACE_IN_NANO = TimeUnit.MILLISECONDS.toNanos(500); + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidSize() { + final TTLCache cache = new TTLCache(0, 1000, mock(TTLCache.EntryLoader.class)); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidTTL() { + final TTLCache cache = new TTLCache(3, 0, mock(TTLCache.EntryLoader.class)); + } + + + @Test(expectedExceptions = NullPointerException.class) + public void testNullLoader() { + final TTLCache cache = new TTLCache(3, 1000, null); + } + + @Test + public void testConstructor() { + final TTLCache cache = + new TTLCache(1000, 1000, mock(TTLCache.EntryLoader.class)); + assertEquals(0, cache.size()); + assertEquals(1000, cache.getMaxSize()); + } + + @Test + public void testLoadPastMaxSize() { + final String loadedValue = "loaded value"; + final long ttlInMillis = 1000; + final int maxSize = 1; + TTLCache.EntryLoader loader = spy(TTLCache.EntryLoader.class); + when(loader.load(any())).thenReturn(loadedValue); + MsClock clock = mock(MsClock.class); + when(clock.timestampNano()).thenReturn((long) 0); + + final TTLCache cache = new TTLCache(maxSize, ttlInMillis, loader); + cache.clock = clock; + + assertEquals(0, cache.size()); + assertEquals(maxSize, cache.getMaxSize()); + + cache.load("k1"); + verify(loader, times(1)).load("k1"); + assertTrue(cache.size() == 1); + + String result = cache.load("k2"); + verify(loader, times(1)).load("k2"); + assertTrue(cache.size() == 1); + assertEquals(loadedValue, result); + + // to verify result is in the cache, load one more time + // and expect the loader to not be called + String cachedValue = cache.load("k2"); + verifyNoMoreInteractions(loader); + assertTrue(cache.size() == 1); + assertEquals(loadedValue, cachedValue); + } + + @Test + public void testLoadNoExistingEntry() { + final String loadedValue = "loaded value"; + final long ttlInMillis = 1000; + final int maxSize = 3; + TTLCache.EntryLoader loader = spy(TTLCache.EntryLoader.class); + when(loader.load(any())).thenReturn(loadedValue); + MsClock clock = mock(MsClock.class); + when(clock.timestampNano()).thenReturn((long) 0); + + final TTLCache cache = new TTLCache(maxSize, ttlInMillis, loader); + cache.clock = clock; + + assertEquals(0, cache.size()); + assertEquals(maxSize, cache.getMaxSize()); + + String result = cache.load("k1"); + verify(loader, times(1)).load("k1"); + assertTrue(cache.size() == 1); + assertEquals(loadedValue, result); + + // to verify result is in the cache, load one more time + // and expect the loader to not be called + String cachedValue = cache.load("k1"); + verifyNoMoreInteractions(loader); + assertTrue(cache.size() == 1); + assertEquals(loadedValue, cachedValue); + } + + @Test + public void testLoadNotExpired() { + final String loadedValue = "loaded value"; + final long ttlInMillis = 1000; + final int maxSize = 3; + TTLCache.EntryLoader loader = spy(TTLCache.EntryLoader.class); + when(loader.load(any())).thenReturn(loadedValue); + MsClock clock = mock(MsClock.class); + + final TTLCache cache = new TTLCache(maxSize, ttlInMillis, loader); + cache.clock = clock; + + assertEquals(0, cache.size()); + assertEquals(maxSize, cache.getMaxSize()); + + // when first creating the entry, time is 0 + when(clock.timestampNano()).thenReturn((long) 0); + cache.load("k1"); + assertTrue(cache.size() == 1); + verify(loader, times(1)).load("k1"); + + // on load, time is within TTL + when(clock.timestampNano()).thenReturn(TimeUnit.MILLISECONDS.toNanos(ttlInMillis)); + String result = cache.load("k1"); + + verifyNoMoreInteractions(loader); + assertTrue(cache.size() == 1); + assertEquals(loadedValue, result); + } + + @Test + public void testLoadInGrace() { + final String loadedValue = "loaded value"; + final long ttlInMillis = 1000; + final int maxSize = 3; + TTLCache.EntryLoader loader = spy(TTLCache.EntryLoader.class); + when(loader.load(any())).thenReturn(loadedValue); + MsClock clock = mock(MsClock.class); + + final TTLCache cache = new TTLCache(maxSize, ttlInMillis, loader); + cache.clock = clock; + + assertEquals(0, cache.size()); + assertEquals(maxSize, cache.getMaxSize()); + + // when first creating the entry, time is zero + when(clock.timestampNano()).thenReturn((long) 0); + cache.load("k1"); + assertTrue(cache.size() == 1); + verify(loader, times(1)).load("k1"); + + // on load, time is past TTL but within the grace period + when(clock.timestampNano()).thenReturn(TimeUnit.MILLISECONDS.toNanos(ttlInMillis) + 1); + String result = cache.load("k1"); + + // Because this is tested in a single thread, + // this is expected to obtain the lock and load the new value + verify(loader, times(2)).load("k1"); + verifyNoMoreInteractions(loader); + assertTrue(cache.size() == 1); + assertEquals(loadedValue, result); + } + + @Test + public void testLoadExpired() { + final String loadedValue = "loaded value"; + final long ttlInMillis = 1000; + final int maxSize = 3; + TTLCache.EntryLoader loader = spy(TTLCache.EntryLoader.class); + when(loader.load(any())).thenReturn(loadedValue); + MsClock clock = mock(MsClock.class); + + final TTLCache cache = new TTLCache(maxSize, ttlInMillis, loader); + cache.clock = clock; + + assertEquals(0, cache.size()); + assertEquals(maxSize, cache.getMaxSize()); + + // when first creating the entry, time is zero + when(clock.timestampNano()).thenReturn((long) 0); + cache.load("k1"); + assertTrue(cache.size() == 1); + verify(loader, times(1)).load("k1"); + + // on load, time is past TTL and grace period + when(clock.timestampNano()) + .thenReturn(TimeUnit.MILLISECONDS.toNanos(ttlInMillis) + TTL_GRACE_IN_NANO + 1); + String result = cache.load("k1"); + + verify(loader, times(2)).load("k1"); + verifyNoMoreInteractions(loader); + assertTrue(cache.size() == 1); + assertEquals(loadedValue, result); + } + + @Test + public void testLoadExpiredEviction() { + final String loadedValue = "loaded value"; + final long ttlInMillis = 1000; + final int maxSize = 3; + TTLCache.EntryLoader loader = spy(TTLCache.EntryLoader.class); + when(loader.load(any())) + .thenReturn(loadedValue) + .thenThrow(new IllegalStateException("This loader is mocked to throw a failure.")); + MsClock clock = mock(MsClock.class); + + final TTLCache cache = new TTLCache(maxSize, ttlInMillis, loader); + cache.clock = clock; + + assertEquals(0, cache.size()); + assertEquals(maxSize, cache.getMaxSize()); + + // when first creating the entry, time is zero + when(clock.timestampNano()).thenReturn((long) 0); + cache.load("k1"); + verify(loader, times(1)).load("k1"); + assertTrue(cache.size() == 1); + + // on load, time is past TTL and grace period + when(clock.timestampNano()) + .thenReturn(TimeUnit.MILLISECONDS.toNanos(ttlInMillis) + TTL_GRACE_IN_NANO + 1); + assertThrows(IllegalStateException.class, () -> cache.load("k1")); + + verify(loader, times(2)).load("k1"); + verifyNoMoreInteractions(loader); + assertTrue(cache.size() == 0); + } + + @Test + public void testLoadWithFunction() { + final String loadedValue = "loaded value"; + final String functionValue = "function value"; + final long ttlInMillis = 1000; + final int maxSize = 3; + final Function function = spy(Function.class); + when(function.apply(any())).thenReturn(functionValue); + TTLCache.EntryLoader loader = spy(TTLCache.EntryLoader.class); + when(loader.load(any())) + .thenReturn(loadedValue) + .thenThrow(new IllegalStateException("This loader is mocked to throw a failure.")); + MsClock clock = mock(MsClock.class); + when(clock.timestampNano()).thenReturn((long) 0); + + final TTLCache cache = new TTLCache(maxSize, ttlInMillis, loader); + cache.clock = clock; + + assertEquals(0, cache.size()); + assertEquals(maxSize, cache.getMaxSize()); + + String result = cache.load("k1", function); + verify(function, times(1)).apply("k1"); + assertTrue(cache.size() == 1); + assertEquals(functionValue, result); + + // to verify result is in the cache, load one more time + // and expect the loader to not be called + String cachedValue = cache.load("k1"); + verifyNoMoreInteractions(function); + verifyNoMoreInteractions(loader); + assertTrue(cache.size() == 1); + assertEquals(functionValue, cachedValue); + } + + @Test + public void testClear() { + final String loadedValue = "loaded value"; + final long ttlInMillis = 1000; + final int maxSize = 3; + TTLCache.EntryLoader loader = spy(TTLCache.EntryLoader.class); + when(loader.load(any())).thenReturn(loadedValue); + + final TTLCache cache = new TTLCache(maxSize, ttlInMillis, loader); + + assertTrue(cache.size() == 0); + cache.load("k1"); + cache.load("k2"); + assertTrue(cache.size() == 2); + + cache.clear(); + assertTrue(cache.size() == 0); + } + + @Test + public void testPut() { + final long ttlInMillis = 1000; + final int maxSize = 3; + TTLCache.EntryLoader loader = spy(TTLCache.EntryLoader.class); + MsClock clock = mock(MsClock.class); + when(clock.timestampNano()).thenReturn((long) 0); + + final TTLCache cache = new TTLCache(maxSize, ttlInMillis, loader); + cache.clock = clock; + + assertEquals(0, cache.size()); + assertEquals(maxSize, cache.getMaxSize()); + + String oldValue = cache.put("k1", "v1"); + assertNull(oldValue); + assertTrue(cache.size() == 1); + + String oldValue2 = cache.put("k1", "v11"); + assertEquals("v1", oldValue2); + assertTrue(cache.size() == 1); + } + + @Test + public void testExpiredPut() { + final long ttlInMillis = 1000; + final int maxSize = 3; + TTLCache.EntryLoader loader = spy(TTLCache.EntryLoader.class); + MsClock clock = mock(MsClock.class); + when(clock.timestampNano()).thenReturn((long) 0); + + final TTLCache cache = new TTLCache(maxSize, ttlInMillis, loader); + cache.clock = clock; + + assertEquals(0, cache.size()); + assertEquals(maxSize, cache.getMaxSize()); + + // First put is at time 0 + String oldValue = cache.put("k1", "v1"); + assertNull(oldValue); + assertTrue(cache.size() == 1); + + // Second put is at time past TTL and grace period + when(clock.timestampNano()) + .thenReturn(TimeUnit.MILLISECONDS.toNanos(ttlInMillis) + TTL_GRACE_IN_NANO + 1); + String oldValue2 = cache.put("k1", "v11"); + assertNull(oldValue2); + assertTrue(cache.size() == 1); + } + + @Test + public void testPutPastMaxSize() { + final String loadedValue = "loaded value"; + final long ttlInMillis = 1000; + final int maxSize = 1; + TTLCache.EntryLoader loader = spy(TTLCache.EntryLoader.class); + when(loader.load(any())).thenReturn(loadedValue); + MsClock clock = mock(MsClock.class); + when(clock.timestampNano()).thenReturn((long) 0); + + final TTLCache cache = new TTLCache(maxSize, ttlInMillis, loader); + cache.clock = clock; + + assertEquals(0, cache.size()); + assertEquals(maxSize, cache.getMaxSize()); + + cache.put("k1", "v1"); + assertTrue(cache.size() == 1); + + cache.put("k2", "v2"); + assertTrue(cache.size() == 1); + + // to verify put value is in the cache, load + // and expect the loader to not be called + String cachedValue = cache.load("k2"); + verifyNoMoreInteractions(loader); + assertTrue(cache.size() == 1); + assertEquals(cachedValue, "v2"); + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/AttrMatcher.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/AttrMatcher.java new file mode 100644 index 0000000000..122364e6db --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/AttrMatcher.java @@ -0,0 +1,125 @@ +/* + * 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.testing; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class AttrMatcher extends BaseMatcher> { + private final Map expected; + private final boolean invert; + + public static AttrMatcher invert(Map expected) { + return new AttrMatcher(expected, true); + } + + public static AttrMatcher match(Map expected) { + return new AttrMatcher(expected, false); + } + + public AttrMatcher(Map expected, boolean invert) { + this.expected = expected; + this.invert = invert; + } + + @Override + public boolean matches(Object item) { + @SuppressWarnings("unchecked") + Map actual = (Map)item; + if (!expected.keySet().equals(actual.keySet())) { + return invert; + } + for (String key: expected.keySet()) { + AttributeValue e = expected.get(key); + AttributeValue a = actual.get(key); + if (!attrEquals(a, e)) { + return invert; + } + } + return !invert; + } + + public static boolean attrEquals(AttributeValue e, AttributeValue a) { + if (!isEqual(e.b(), a.b()) || + !isEqual(e.bool(), a.bool()) || + !isSetEqual(e.bs(), a.bs()) || + !isEqual(e.n(), a.n()) || + !isSetEqual(e.ns(), a.ns()) || + !isEqual(e.nul(), a.nul()) || + !isEqual(e.s(), a.s()) || + !isSetEqual(e.ss(), a.ss())) { + return false; + } + // Recursive types need special handling + if (e.m() == null ^ a.m() == null) { + return false; + } else if (e.m() != null) { + if (!e.m().keySet().equals(a.m().keySet())) { + return false; + } + for (final String key : e.m().keySet()) { + if (!attrEquals(e.m().get(key), a.m().get(key))) { + return false; + } + } + } + if (e.l() == null ^ a.l() == null) { + return false; + } else if (e.l() != null) { + if (e.l().size() != a.l().size()) { + return false; + } + for (int x = 0; x < e.l().size(); x++) { + if (!attrEquals(e.l().get(x), a.l().get(x))) { + return false; + } + } + } + return true; + } + + @Override + public void describeTo(Description description) { } + + private static boolean isEqual(Object o1, Object o2) { + if(o1 == null ^ o2 == null) { + return false; + } + if (o1 == o2) + return true; + return o1.equals(o2); + } + + private static boolean isSetEqual(Collection c1, Collection c2) { + if(c1 == null ^ c2 == null) { + return false; + } + if (c1 != null) { + Set s1 = new HashSet(c1); + Set s2 = new HashSet(c2); + if(!s1.equals(s2)) { + return false; + } + } + return true; + } +} \ No newline at end of file diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/AttributeValueBuilder.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/AttributeValueBuilder.java new file mode 100644 index 0000000000..3ff7e4dff8 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/AttributeValueBuilder.java @@ -0,0 +1,67 @@ +/* + * 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.testing; + +import java.util.List; +import java.util.Map; + +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * Static helper methods to construct standard AttributeValues in a more compact way than specifying the full builder + * chain. + */ +public final class AttributeValueBuilder { + private AttributeValueBuilder() { + // Static helper class + } + + public static AttributeValue ofS(String value) { + return AttributeValue.builder().s(value).build(); + } + + public static AttributeValue ofN(String value) { + return AttributeValue.builder().n(value).build(); + } + + public static AttributeValue ofB(byte [] value) { + return AttributeValue.builder().b(SdkBytes.fromByteArray(value)).build(); + } + + public static AttributeValue ofBool(Boolean value) { + return AttributeValue.builder().bool(value).build(); + } + + public static AttributeValue ofNull() { + return AttributeValue.builder().nul(true).build(); + } + + public static AttributeValue ofL(List values) { + return AttributeValue.builder().l(values).build(); + } + + public static AttributeValue ofL(AttributeValue ...values) { + return AttributeValue.builder().l(values).build(); + } + + public static AttributeValue ofM(Map valueMap) { + return AttributeValue.builder().m(valueMap).build(); + } + + public static AttributeValue ofSS(String ...values) { + return AttributeValue.builder().ss(values).build(); + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/AttributeValueDeserializer.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/AttributeValueDeserializer.java new file mode 100644 index 0000000000..42e479ba70 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/AttributeValueDeserializer.java @@ -0,0 +1,58 @@ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.testing; + +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +public class AttributeValueDeserializer extends JsonDeserializer { + @Override + public AttributeValue deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode attribute = jp.getCodec().readTree(jp); + + for (Iterator> iter = attribute.fields(); iter.hasNext(); ) { + Map.Entry rawAttribute = iter.next(); + // If there is more than one entry in this map, there is an error with our test data + if (iter.hasNext()) { + throw new IllegalStateException("Attribute value JSON has more than one value mapped."); + } + String typeString = rawAttribute.getKey(); + JsonNode value = rawAttribute.getValue(); + switch (typeString) { + case "S": + return AttributeValue.builder().s(value.asText()).build(); + case "B": + SdkBytes b = SdkBytes.fromByteArray(java.util.Base64.getDecoder().decode(value.asText())); + return AttributeValue.builder().b(b).build(); + case "N": + return AttributeValue.builder().n(value.asText()).build(); + case "SS": + final Set stringSet = + objectMapper.readValue( + objectMapper.treeAsTokens(value), new TypeReference>() {}); + return AttributeValue.builder().ss(stringSet).build(); + case "NS": + final Set numSet = + objectMapper.readValue( + objectMapper.treeAsTokens(value), new TypeReference>() {}); + return AttributeValue.builder().ns(numSet).build(); + default: + throw new IllegalStateException( + "DDB JSON type " + + typeString + + " not implemented for test attribute value deserialization."); + } + } + return null; + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/AttributeValueMatcher.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/AttributeValueMatcher.java new file mode 100644 index 0000000000..0dbea38209 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/AttributeValueMatcher.java @@ -0,0 +1,101 @@ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.testing; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public class AttributeValueMatcher extends BaseMatcher { + private final AttributeValue expected; + private final boolean invert; + + public static AttributeValueMatcher invert(AttributeValue expected) { + return new AttributeValueMatcher(expected, true); + } + + public static AttributeValueMatcher match(AttributeValue expected) { + return new AttributeValueMatcher(expected, false); + } + + public AttributeValueMatcher(AttributeValue expected, boolean invert) { + this.expected = expected; + this.invert = invert; + } + + @Override + public boolean matches(Object item) { + AttributeValue other = (AttributeValue) item; + return invert ^ attrEquals(expected, other); + } + + @Override + public void describeTo(Description description) {} + + public static boolean attrEquals(AttributeValue e, AttributeValue a) { + if (!isEqual(e.b(), a.b()) + || !isNumberEqual(e.n(), a.n()) + || !isEqual(e.s(), a.s()) + || !isEqual(e.bs(), a.bs()) + || !isNumberEqual(e.ns(), a.ns()) + || !isEqual(e.ss(), a.ss())) { + return false; + } + return true; + } + + private static boolean isNumberEqual(String o1, String o2) { + if (o1 == null ^ o2 == null) { + return false; + } + if (o1 == o2) return true; + BigDecimal d1 = new BigDecimal(o1); + BigDecimal d2 = new BigDecimal(o2); + return d1.equals(d2); + } + + private static boolean isEqual(Object o1, Object o2) { + if (o1 == null ^ o2 == null) { + return false; + } + if (o1 == o2) return true; + return o1.equals(o2); + } + + private static boolean isNumberEqual(Collection c1, Collection c2) { + if (c1 == null ^ c2 == null) { + return false; + } + if (c1 != null) { + Set s1 = new HashSet(); + Set s2 = new HashSet(); + for (String s : c1) { + s1.add(new BigDecimal(s)); + } + for (String s : c2) { + s2.add(new BigDecimal(s)); + } + if (!s1.equals(s2)) { + return false; + } + } + return true; + } + + private static boolean isEqual(Collection c1, Collection c2) { + if (c1 == null ^ c2 == null) { + return false; + } + if (c1 != null) { + Set s1 = new HashSet(c1); + Set s2 = new HashSet(c2); + if (!s1.equals(s2)) { + return false; + } + } + return true; + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/AttributeValueSerializer.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/AttributeValueSerializer.java new file mode 100644 index 0000000000..95abc5471d --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/AttributeValueSerializer.java @@ -0,0 +1,48 @@ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.testing; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import com.amazonaws.util.Base64; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public class AttributeValueSerializer extends JsonSerializer { + @Override + public void serialize(AttributeValue.Builder value, JsonGenerator jgen, SerializerProvider provider) + throws IOException { + if (value != null) { + jgen.writeStartObject(); + if (value.build().s() != null) { + jgen.writeStringField("S", value.build().s()); + } else if (value.build().b() != null) { + ByteBuffer valueBytes = value.build().b().asByteBuffer(); + byte[] arr = new byte[valueBytes.remaining()]; + valueBytes.get(arr); + jgen.writeStringField("B", Base64.encodeAsString(arr)); + } else if (value.build().n() != null) { + jgen.writeStringField("N", value.build().n()); + } else if (value.build().ss() != null) { + jgen.writeFieldName("SS"); + jgen.writeStartArray(); + for (String s : value.build().ss()) { + jgen.writeString(s); + } + jgen.writeEndArray(); + } else if (value.build().ns() != null) { + jgen.writeFieldName("NS"); + jgen.writeStartArray(); + for (String num : value.build().ns()) { + jgen.writeString(num); + } + jgen.writeEndArray(); + } else { + throw new IllegalStateException( + "AttributeValue has no value or type not implemented for serialization."); + } + jgen.writeEndObject(); + } + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/DdbRecordMatcher.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/DdbRecordMatcher.java new file mode 100644 index 0000000000..75cc574a1c --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/DdbRecordMatcher.java @@ -0,0 +1,47 @@ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.testing; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import java.util.Map; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; + +public class DdbRecordMatcher extends BaseMatcher>{ + private final Map expected; + private final boolean invert; + + public static DdbRecordMatcher invert(Map expected) { + return new DdbRecordMatcher(expected, true); + } + + public static DdbRecordMatcher match(Map expected) { + return new DdbRecordMatcher(expected, false); + } + + public DdbRecordMatcher(Map expected, boolean invert) { + this.expected = expected; + this.invert = invert; + } + + @Override + public boolean matches(Object item) { + @SuppressWarnings("unchecked") + Map actual = (Map) item; + if (!expected.keySet().equals(actual.keySet())) { + return invert; + } + for (String key : expected.keySet()) { + if (key.equals("version")) continue; + AttributeValue e = expected.get(key); + AttributeValue a = actual.get(key); + if (!AttributeValueMatcher.attrEquals(a, e)) { + return invert; + } + } + return !invert; + } + + @Override + public void describeTo(Description description) { + + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/FakeKMS.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/FakeKMS.java new file mode 100644 index 0000000000..d05aff4113 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/FakeKMS.java @@ -0,0 +1,201 @@ +/* + * 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.testing; + +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.kms.KmsClient; +import software.amazon.awssdk.services.kms.model.CreateKeyRequest; +import software.amazon.awssdk.services.kms.model.CreateKeyResponse; +import software.amazon.awssdk.services.kms.model.DecryptRequest; +import software.amazon.awssdk.services.kms.model.DecryptResponse; +import software.amazon.awssdk.services.kms.model.EncryptRequest; +import software.amazon.awssdk.services.kms.model.EncryptResponse; +import software.amazon.awssdk.services.kms.model.GenerateDataKeyRequest; +import software.amazon.awssdk.services.kms.model.GenerateDataKeyResponse; +import software.amazon.awssdk.services.kms.model.GenerateDataKeyWithoutPlaintextRequest; +import software.amazon.awssdk.services.kms.model.GenerateDataKeyWithoutPlaintextResponse; +import software.amazon.awssdk.services.kms.model.InvalidCiphertextException; +import software.amazon.awssdk.services.kms.model.KeyMetadata; +import software.amazon.awssdk.services.kms.model.KeyUsageType; + +public class FakeKMS implements KmsClient { + private static final SecureRandom rnd = new SecureRandom(); + private static final String ACCOUNT_ID = "01234567890"; + private final Map results_ = new HashMap<>(); + + @Override + public CreateKeyResponse createKey(CreateKeyRequest createKeyRequest) { + String keyId = UUID.randomUUID().toString(); + String arn = "arn:aws:testing:kms:" + ACCOUNT_ID + ":key/" + keyId; + return CreateKeyResponse.builder() + .keyMetadata(KeyMetadata.builder().awsAccountId(ACCOUNT_ID) + .creationDate(Instant.now()) + .description(createKeyRequest.description()) + .enabled(true) + .keyId(keyId) + .keyUsage(KeyUsageType.ENCRYPT_DECRYPT) + .arn(arn) + .build()) + .build(); + } + + @Override + public DecryptResponse decrypt(DecryptRequest decryptRequest) { + DecryptResponse result = results_.get(new DecryptMapKey(decryptRequest)); + if (result != null) { + return result; + } else { + throw InvalidCiphertextException.create("Invalid Ciphertext", new RuntimeException()); + } + } + + @Override + public EncryptResponse encrypt(EncryptRequest encryptRequest) { + final byte[] cipherText = new byte[512]; + rnd.nextBytes(cipherText); + DecryptResponse.Builder dec = DecryptResponse.builder(); + dec.keyId(encryptRequest.keyId()) + .plaintext(SdkBytes.fromByteBuffer(encryptRequest.plaintext().asByteBuffer().asReadOnlyBuffer())); + ByteBuffer ctBuff = ByteBuffer.wrap(cipherText); + + results_.put(new DecryptMapKey(ctBuff, encryptRequest.encryptionContext()), dec.build()); + + return EncryptResponse.builder() + .ciphertextBlob(SdkBytes.fromByteBuffer(ctBuff)) + .keyId(encryptRequest.keyId()) + .build(); + } + + @Override + public GenerateDataKeyResponse generateDataKey(GenerateDataKeyRequest generateDataKeyRequest) { + byte[] pt; + if (generateDataKeyRequest.keySpec() != null) { + if (generateDataKeyRequest.keySpec().toString().contains("256")) { + pt = new byte[32]; + } else if (generateDataKeyRequest.keySpec().toString().contains("128")) { + pt = new byte[16]; + } else { + throw new UnsupportedOperationException(); + } + } else { + pt = new byte[generateDataKeyRequest.numberOfBytes()]; + } + rnd.nextBytes(pt); + ByteBuffer ptBuff = ByteBuffer.wrap(pt); + EncryptResponse encryptresponse = encrypt(EncryptRequest.builder() + .keyId(generateDataKeyRequest.keyId()) + .plaintext(SdkBytes.fromByteBuffer(ptBuff)) + .encryptionContext(generateDataKeyRequest.encryptionContext()) + .build()); + return GenerateDataKeyResponse.builder().keyId(generateDataKeyRequest.keyId()) + .ciphertextBlob(encryptresponse.ciphertextBlob()) + .plaintext(SdkBytes.fromByteBuffer(ptBuff)) + .build(); + } + + @Override + public GenerateDataKeyWithoutPlaintextResponse generateDataKeyWithoutPlaintext( + GenerateDataKeyWithoutPlaintextRequest req) { + GenerateDataKeyResponse generateDataKey = generateDataKey(GenerateDataKeyRequest.builder() + .encryptionContext(req.encryptionContext()).numberOfBytes(req.numberOfBytes()).build()); + return GenerateDataKeyWithoutPlaintextResponse.builder().ciphertextBlob( + generateDataKey.ciphertextBlob()).keyId(req.keyId()).build(); + } + + public Map getSingleEc() { + if (results_.size() != 1) { + throw new IllegalStateException("Unexpected number of ciphertexts"); + } + for (final DecryptMapKey k : results_.keySet()) { + return k.ec; + } + throw new IllegalStateException("Unexpected number of ciphertexts"); + } + + @Override + public String serviceName() { + return KmsClient.SERVICE_NAME; + } + + @Override + public void close() { + // do nothing + } + + private static class DecryptMapKey { + private final ByteBuffer cipherText; + private final Map ec; + + public DecryptMapKey(DecryptRequest req) { + cipherText = req.ciphertextBlob().asByteBuffer(); + if (req.encryptionContext() != null) { + ec = Collections.unmodifiableMap(new HashMap<>(req.encryptionContext())); + } else { + ec = Collections.emptyMap(); + } + } + + public DecryptMapKey(ByteBuffer ctBuff, Map ec) { + cipherText = ctBuff.asReadOnlyBuffer(); + if (ec != null) { + this.ec = Collections.unmodifiableMap(new HashMap<>(ec)); + } else { + this.ec = Collections.emptyMap(); + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((cipherText == null) ? 0 : cipherText.hashCode()); + result = prime * result + ((ec == null) ? 0 : ec.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + DecryptMapKey other = (DecryptMapKey) obj; + if (cipherText == null) { + if (other.cipherText != null) + return false; + } else if (!cipherText.equals(other.cipherText)) + return false; + if (ec == null) { + if (other.ec != null) + return false; + } else if (!ec.equals(other.ec)) + return false; + return true; + } + + @Override + public String toString() { + return "DecryptMapKey [cipherText=" + cipherText + ", ec=" + ec + "]"; + } + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/LocalDynamoDb.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/LocalDynamoDb.java new file mode 100644 index 0000000000..fe293a0d02 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/LocalDynamoDb.java @@ -0,0 +1,175 @@ +/* + * 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.testing; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.URI; + +import com.amazonaws.services.dynamodbv2.local.main.ServerRunner; +import com.amazonaws.services.dynamodbv2.local.server.DynamoDBProxyServer; + +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.*; + +/** + * Wrapper for a local DynamoDb server used in testing. Each instance of this class will find a new port to run on, + * so multiple instances can be safely run simultaneously. Each instance of this service uses memory as a storage medium + * and is thus completely ephemeral; no data will be persisted between stops and starts. + * + * LocalDynamoDb localDynamoDb = new LocalDynamoDb(); + * localDynamoDb.start(); // Start the service running locally on host + * DynamoDbClient dynamoDbClient = localDynamoDb.createClient(); + * ... // Do your testing with the client + * localDynamoDb.stop(); // Stop the service and free up resources + * + * If possible it's recommended to keep a single running instance for all your tests, as it can be slow to teardown + * and create new servers for every test, but there have been observed problems when dropping tables between tests for + * this scenario, so it's best to write your tests to be resilient to tables that already have data in them. + */ +public class LocalDynamoDb { + private static DynamoDBProxyServer server; + private static int port; + + /** + * Start the local DynamoDb service and run in background + */ + public void start() { + port = getFreePort(); + String portString = Integer.toString(port); + + try { + server = createServer(portString); + server.start(); + } catch (Exception e) { + throw propagate(e); + } + } + + /** + * Create a standard AWS v2 SDK client pointing to the local DynamoDb instance + * @return A DynamoDbClient pointing to the local DynamoDb instance + */ + public DynamoDbClient createClient() { + start(); + String endpoint = String.format("http://localhost:%d", port); + return DynamoDbClient.builder() + .endpointOverride(URI.create(endpoint)) + // The region is meaningless for local DynamoDb but required for client builder validation + .region(Region.US_EAST_1) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create("dummy-key", "dummy-secret"))) + .build(); + } + + /** + * If you require a client object that can be mocked or spied using standard mocking frameworks, then you must call + * this method to create the client instead. Only some methods are supported by this client, but it is easy to add + * new ones. + * @return A mockable/spyable DynamoDbClient pointing to the Local DynamoDB service. + */ + public DynamoDbClient createLimitedWrappedClient() { + return new WrappedDynamoDbClient(createClient()); + } + + /** + * Stops the local DynamoDb service and frees up resources it is using. + */ + public void stop() { + try { + server.stop(); + } catch (Exception e) { + throw propagate(e); + } + } + + private static DynamoDBProxyServer createServer(String portString) throws Exception { + return ServerRunner.createServerFromCommandLineArgs( + new String[]{ + "-inMemory", + "-port", portString + }); + } + + private static int getFreePort() { + try { + ServerSocket socket = new ServerSocket(0); + int port = socket.getLocalPort(); + socket.close(); + return port; + } catch (IOException ioe) { + throw propagate(ioe); + } + } + + private static RuntimeException propagate(Exception e) { + if (e instanceof RuntimeException) { + throw (RuntimeException)e; + } + throw new RuntimeException(e); + } + + /** + * This class can wrap any other implementation of a DynamoDbClient. The default implementation of the real + * DynamoDbClient is a final class, therefore it cannot be easily spied upon unless you first wrap it in a class + * like this. If there's a method you need it to support, just add it to the wrapper here. + */ + private static class WrappedDynamoDbClient implements DynamoDbClient { + private final DynamoDbClient wrappedClient; + + private WrappedDynamoDbClient(DynamoDbClient wrappedClient) { + this.wrappedClient = wrappedClient; + } + + @Override + public String serviceName() { + return wrappedClient.serviceName(); + } + + @Override + public void close() { + wrappedClient.close(); + } + + @Override + public PutItemResponse putItem(PutItemRequest putItemRequest) { + return wrappedClient.putItem(putItemRequest); + } + + @Override + public GetItemResponse getItem(GetItemRequest getItemRequest) { + return wrappedClient.getItem(getItemRequest); + } + + @Override + public QueryResponse query(QueryRequest queryRequest) { + return wrappedClient.query(queryRequest); + } + + @Override + public ListTablesResponse listTables(ListTablesRequest listTablesRequest) { return wrappedClient.listTables(listTablesRequest); } + + @Override + public ScanResponse scan(ScanRequest scanRequest) { return wrappedClient.scan(scanRequest); } + + @Override + public CreateTableResponse createTable(CreateTableRequest createTableRequest) { + return wrappedClient.createTable(createTableRequest); + } + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/ScenarioManifest.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/ScenarioManifest.java new file mode 100644 index 0000000000..14c84d8605 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/ScenarioManifest.java @@ -0,0 +1,77 @@ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.testing; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class ScenarioManifest { + + public static final String MOST_RECENT_PROVIDER_NAME = "most_recent"; + public static final String WRAPPED_PROVIDER_NAME = "wrapped"; + public static final String STATIC_PROVIDER_NAME = "static"; + public static final String AWS_KMS_PROVIDER_NAME = "awskms"; + public static final String SYMMETRIC_KEY_TYPE = "symmetric"; + + public List scenarios; + + @JsonProperty("keys") + public String keyDataPath; + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Scenario { + @JsonProperty("ciphertext") + public String ciphertextPath; + + @JsonProperty("provider") + public String providerName; + + public String version; + + @JsonProperty("material_name") + public String materialName; + + public Metastore metastore; + public Keys keys; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Metastore { + @JsonProperty("ciphertext") + public String path; + + @JsonProperty("table_name") + public String tableName; + + @JsonProperty("provider") + public String providerName; + + public Keys keys; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Keys { + @JsonProperty("encrypt") + public String encryptName; + + @JsonProperty("sign") + public String signName; + + @JsonProperty("decrypt") + public String decryptName; + + @JsonProperty("verify") + public String verifyName; + } + + public static class KeyData { + public String material; + public String algorithm; + public String encoding; + + @JsonProperty("type") + public String keyType; + + public String keyId; + } +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/TestDelegatedKey.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/TestDelegatedKey.java new file mode 100644 index 0000000000..c19c5565b3 --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/TestDelegatedKey.java @@ -0,0 +1,128 @@ +/* + * 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.testing; + +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.IvParameterSpec; + +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.DelegatedKey; + +public class TestDelegatedKey implements DelegatedKey { + private static final long serialVersionUID = 1L; + + private final Key realKey; + + public TestDelegatedKey(Key key) { + this.realKey = key; + } + + @Override + public String getAlgorithm() { + return "DELEGATED:" + realKey.getAlgorithm(); + } + + @Override + public byte[] getEncoded() { + return realKey.getEncoded(); + } + + @Override + public String getFormat() { + return realKey.getFormat(); + } + + @Override + public byte[] encrypt(byte[] plainText, byte[] additionalAssociatedData, String algorithm) + throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException, + NoSuchPaddingException { + Cipher cipher = Cipher.getInstance(extractAlgorithm(algorithm)); + cipher.init(Cipher.ENCRYPT_MODE, realKey); + byte[] iv = cipher.getIV(); + byte[] result = new byte[cipher.getOutputSize(plainText.length) + iv.length + 1]; + result[0] = (byte) iv.length; + System.arraycopy(iv, 0, result, 1, iv.length); + try { + cipher.doFinal(plainText, 0, plainText.length, result, iv.length + 1); + } catch (ShortBufferException e) { + throw new RuntimeException(e); + } + return result; + } + + @Override + public byte[] decrypt(byte[] cipherText, byte[] additionalAssociatedData, String algorithm) + throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException, + NoSuchPaddingException, InvalidAlgorithmParameterException { + final byte ivLength = cipherText[0]; + IvParameterSpec iv = new IvParameterSpec(cipherText, 1, ivLength); + Cipher cipher = Cipher.getInstance(extractAlgorithm(algorithm)); + cipher.init(Cipher.DECRYPT_MODE, realKey, iv); + return cipher.doFinal(cipherText, ivLength + 1, cipherText.length - ivLength - 1); + } + + @Override + public byte[] wrap(Key key, byte[] additionalAssociatedData, String algorithm) throws InvalidKeyException, + NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException { + Cipher cipher = Cipher.getInstance(extractAlgorithm(algorithm)); + cipher.init(Cipher.WRAP_MODE, realKey); + return cipher.wrap(key); + } + + @Override + public Key unwrap(byte[] wrappedKey, String wrappedKeyAlgorithm, int wrappedKeyType, + byte[] additionalAssociatedData, String algorithm) throws NoSuchAlgorithmException, NoSuchPaddingException, + InvalidKeyException { + Cipher cipher = Cipher.getInstance(extractAlgorithm(algorithm)); + cipher.init(Cipher.UNWRAP_MODE, realKey); + return cipher.unwrap(wrappedKey, wrappedKeyAlgorithm, wrappedKeyType); + } + + @Override + public byte[] sign(byte[] dataToSign, String algorithm) throws NoSuchAlgorithmException, InvalidKeyException { + Mac mac = Mac.getInstance(extractAlgorithm(algorithm)); + mac.init(realKey); + return mac.doFinal(dataToSign); + } + + @Override + public boolean verify(byte[] dataToSign, byte[] signature, String algorithm) { + try { + byte[] expected = sign(dataToSign, extractAlgorithm(algorithm)); + return MessageDigest.isEqual(expected, signature); + } catch (GeneralSecurityException ex) { + return false; + } + } + + private String extractAlgorithm(String alg) { + if (alg.startsWith(getAlgorithm())) { + return alg.substring(10); + } else { + return alg; + } + } +}