diff --git a/ddej-build-tools/src/main/resources/software/amazon/cryptools/ddej-build-tools/checkstyle/checkstyle.xml b/ddej-build-tools/src/main/resources/software/amazon/cryptools/ddej-build-tools/checkstyle/checkstyle.xml index 9b11b80e..3b20f72d 100644 --- a/ddej-build-tools/src/main/resources/software/amazon/cryptools/ddej-build-tools/checkstyle/checkstyle.xml +++ b/ddej-build-tools/src/main/resources/software/amazon/cryptools/ddej-build-tools/checkstyle/checkstyle.xml @@ -8,6 +8,6 @@ + value="^/*\n * Copyright \d{4}([-]\d{4})? Amazon\.com, Inc\. or its affiliates\. All Rights Reserved\.$"/> diff --git a/sdk2/pom.xml b/sdk2/pom.xml index 4f8ef798..d9def4a5 100644 --- a/sdk2/pom.xml +++ b/sdk2/pom.xml @@ -22,7 +22,7 @@ software.amazon.awssdk bom - 2.4.11 + 2.5.47 pom import @@ -44,5 +44,12 @@ dynamodbencryptionclient-common 0.1.0-SNAPSHOT + + + org.mockito + mockito-junit-jupiter + 2.27.0 + test + diff --git a/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/BasicDynamoDbEncryptionConfiguration.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/BasicDynamoDbEncryptionConfiguration.java new file mode 100644 index 00000000..103b9c7a --- /dev/null +++ b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/BasicDynamoDbEncryptionConfiguration.java @@ -0,0 +1,110 @@ +/* + * 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; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.EncryptionContext; + +public class BasicDynamoDbEncryptionConfiguration implements DynamoDbEncryptionConfiguration { + private final EncryptionAction defaultEncryptionAction; + private final Map encryptionActionOverrides; + private final EncryptionContext encryptionContext; + + private BasicDynamoDbEncryptionConfiguration(Builder builder) { + this.defaultEncryptionAction = builder.defaultEncryptionAction; + this.encryptionActionOverrides = Collections.unmodifiableMap(builder.encryptionActionOverrides); + this.encryptionContext = builder.encryptionContext; + } + + @Override + public EncryptionAction getDefaultEncryptionAction() { + return this.defaultEncryptionAction; + } + + @Override + public Map getEncryptionActionOverrides() { + return this.encryptionActionOverrides; + } + + @Override + public EncryptionContext getEncryptionContext() { + return this.encryptionContext; + } + + /** + * Builder for an immutable implementation of {@link DynamoDbEncryptionConfiguration}. + */ + public static class Builder { + private EncryptionAction defaultEncryptionAction; + private Map encryptionActionOverrides = new HashMap<>(); + private EncryptionContext encryptionContext; + + /** + * Set the default {@link EncryptionAction} that should be applied to any attribute that is found in the + * record and does not have a specific override associated with it. + * @param defaultEncryptionAction The default encryption action that should be applied to attributes. + * @return a mutated instance of this builder. + */ + public Builder defaultEncryptionAction(EncryptionAction defaultEncryptionAction) { + this.defaultEncryptionAction = defaultEncryptionAction; + return this; + } + + /** + * Add a map of encryption action overrides for specific attributes. Will be merged into any existing overrides + * the builder already has and will overwrite existing values with the same key. + * @param encryptionActionOverrides A map of encryption action overrides. + * @return a mutated instance of this builder. + */ + public Builder addEncryptionActionOverrides(Map encryptionActionOverrides) { + this.encryptionActionOverrides.putAll(encryptionActionOverrides); + return this; + } + + /** + * Add a single encryption action override for a specific attribute. Will be merged into any existing overrides + * ths builder already has and will overwrite existing values with the same key. + * @param attributeKey The name of the attribute. + * @param encryptionAction The encryption action to apply to that attribute. + * @return a mutated instance of this builder. + */ + public Builder addEncryptionActionOverride(String attributeKey, EncryptionAction encryptionAction) { + this.encryptionActionOverrides.put(attributeKey, encryptionAction); + return this; + } + + /** + * Sets the encryption context to be used by the encryption client when encrypting or decrypting records. At + * a minimum the following fields should be set on the context: tableName, hashKeyName, rangeKeyName. + * @param encryptionContext An {@link EncryptionContext} object to associate with this configuration. + * @return a mutated instance of this builder. + */ + public Builder encryptionContext(EncryptionContext encryptionContext) { + this.encryptionContext = encryptionContext; + return this; + } + + /** + * Construct an immutable {@link DynamoDbEncryptionConfiguration} from the information provided to the builder. + * @return an initialized {@link BasicDynamoDbEncryptionConfiguration} object. + */ + public BasicDynamoDbEncryptionConfiguration build() { + return new BasicDynamoDbEncryptionConfiguration(this); + } + } +} diff --git a/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/DynamoDbEncryptionClient.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/DynamoDbEncryptionClient.java new file mode 100644 index 00000000..9825d18f --- /dev/null +++ b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/DynamoDbEncryptionClient.java @@ -0,0 +1,55 @@ +/* + * 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; + +import java.util.Map; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.DynamoDbEncryptor; + +/** + * General interface for a class that is capable of encrypting and decrypting DynamoDB records as well as signing and + * verifying signatures. + */ +public interface DynamoDbEncryptionClient { + /** + * Encrypt and sign a record. + * @param itemAttributes The map of AttributeValues that make up the record. + * @param configuration A {@link DynamoDbEncryptionConfiguration} object that configures the behavior and scope + * of encryption and signing on the record. + * @return A map of AttributeValues that has been encrypted and signed as directed. + */ + Map encryptRecord(Map itemAttributes, + DynamoDbEncryptionConfiguration configuration); + + /** + * Decrypt and verify signature on a record. + * @param itemAttributes The map of AttributeValues that make up the encrypted/signed record. + * @param configuration A {@link DynamoDbEncryptionConfiguration} object that configures the behavior and scope + * of decryption and signature verification on the record. + * @return A map of AttributeValues that have been decrypted and verified as directed. + */ + Map decryptRecord(Map itemAttributes, + DynamoDbEncryptionConfiguration configuration); + + /** + * Convenience method to return a builder for the default approved implementation of this interface, a + * {@link DynamoDbEncryptor}. + * @return A builder object for the default implementation of this interface. + */ + static DynamoDbEncryptor.Builder builder() { + return DynamoDbEncryptor.builder(); + } +} diff --git a/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/DynamoDbEncryptionConfiguration.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/DynamoDbEncryptionConfiguration.java new file mode 100644 index 00000000..3c2ca113 --- /dev/null +++ b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/DynamoDbEncryptionConfiguration.java @@ -0,0 +1,52 @@ +/* + * 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; + +import java.util.Map; + +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.EncryptionContext; + +/** + * An interface to an object that supplies configuration and context to the {@link DynamoDbEncryptionClient}. + */ +public interface DynamoDbEncryptionConfiguration { + /** + * Get the default {@link EncryptionAction} that should be applied to any attribute that is found in the record and + * does not have a specific override associated with it. + * @return The default {@link EncryptionAction}. + */ + EncryptionAction getDefaultEncryptionAction(); + + /** + * Gets a map of specific attribute {@link EncryptionAction} overrides. + * @return A map of {@link EncryptionAction} overrides, keyed by attribute name. + */ + Map getEncryptionActionOverrides(); + + /** + * Returns an {@link EncryptionContext} to be used by the encryption client. Has information about the table + * name, the names of the primary indices etc. + * @return An {@link EncryptionContext} object. + */ + EncryptionContext getEncryptionContext(); + + /** + * Default builder for an immutable implementation of {@link DynamoDbEncryptionConfiguration}. + * @return A newly initialized {@link BasicDynamoDbEncryptionConfiguration.Builder}. + */ + static BasicDynamoDbEncryptionConfiguration.Builder builder() { + return new BasicDynamoDbEncryptionConfiguration.Builder(); + } +} diff --git a/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/EncryptionAction.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/EncryptionAction.java new file mode 100644 index 00000000..9ee1c798 --- /dev/null +++ b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/EncryptionAction.java @@ -0,0 +1,40 @@ +/* + * 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; + +/** + * When configuring the {@link DynamoDbEncryptionClient} you may specify a default behavior for how attributes should + * be treated when encrypting and decrypting, and also you may include overrides to change the behavior for specific + * attributes. The following enumeration are the different valid behaviors for how a single attribute should be treated. + */ +public enum EncryptionAction { + /** + * DO_NOTHING : This instructs the encryption client to completely ignore the attribute. The attribute will not be + * encrypted and it will not be included in the signature calculation of the record. + */ + DO_NOTHING, + + /** + * SIGN_ONLY : This instructs the encryption client to include the attribute in the signature calculation of the + * record, but not to encrypt its value. + */ + SIGN_ONLY, + + /** + * ENCRYPT_AND_SIGN : This instructs the encryption client to include the attribute in the signature calculation of + * the record and to encrypt its value. + */ + ENCRYPT_AND_SIGN +} diff --git a/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DelegatedKey.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DelegatedKey.java new file mode 100644 index 00000000..52e02f2e --- /dev/null +++ b/sdk2/src/main/java/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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbEncryptor.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbEncryptor.java new file mode 100644 index 00000000..775fd12d --- /dev/null +++ b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbEncryptor.java @@ -0,0 +1,564 @@ +/* + * 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.util.Arrays; +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.function.Function; +import java.util.stream.Collectors; + +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.DynamoDbEncryptionClient; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.DynamoDbEncryptionConfiguration; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.EncryptionAction; +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.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 implements DynamoDbEncryptionClient { + 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; + + // Static map used to convert an EncryptionAction into a corresponding set of EncryptionFlags + private static final Map> ENCRYPTION_ACTION_TO_FLAGS_MAP; + static { + Map> encrytionActionToFlagsMap = new HashMap<>(); + encrytionActionToFlagsMap.put(EncryptionAction.DO_NOTHING, Collections.emptySet()); + encrytionActionToFlagsMap.put(EncryptionAction.SIGN_ONLY, Collections.singleton(EncryptionFlags.SIGN)); + encrytionActionToFlagsMap.put(EncryptionAction.ENCRYPT_AND_SIGN, + Collections.unmodifiableSet(new HashSet<>(Arrays.asList(EncryptionFlags.SIGN, EncryptionFlags.ENCRYPT)))); + ENCRYPTION_ACTION_TO_FLAGS_MAP = Collections.unmodifiableMap(encrytionActionToFlagsMap); + } + + 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"; + } + + protected DynamoDbEncryptor(EncryptionMaterialsProvider provider) { + this(provider, DEFAULT_DESCRIPTION_BASE); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private EncryptionMaterialsProvider encryptionMaterialsProvider; + + public Builder encryptionMaterialsProvider(EncryptionMaterialsProvider encryptionMaterialsProvider) { + this.encryptionMaterialsProvider = encryptionMaterialsProvider; + return this; + } + + public DynamoDbEncryptor build() { + if (encryptionMaterialsProvider == null) { + throw new IllegalArgumentException("A DynamoDbEncryptor cannot be built without an " + + "EncryptionMaterialsProvider"); + } + + return new DynamoDbEncryptor(encryptionMaterialsProvider); + } + } + + @Override + public Map encryptRecord(Map record, + DynamoDbEncryptionConfiguration configuration) { + + validateParameters(record, configuration); + return internalEncryptRecord(record, + getEncryptionFlagsFromConfiguration(record, configuration), + configuration.getEncryptionContext()); + } + + @Override + public Map decryptRecord(Map record, + DynamoDbEncryptionConfiguration configuration) { + + validateParameters(record, configuration); + return internalDecryptRecord(record, + getEncryptionFlagsFromConfiguration(record, configuration), + configuration.getEncryptionContext()); + } + + private void validateParameters(Map record, + DynamoDbEncryptionConfiguration configuration) { + if (record == null) { + throw new IllegalArgumentException("AttributeValues must not be null"); + } + if (configuration == null) { + throw new IllegalArgumentException("DynamoDbEncryptionConfiguration must not be null"); + } + if (configuration.getEncryptionContext() == null) { + throw new IllegalArgumentException("DynamoDbEncryptionConfiguration's EncryptionContext must not be null"); + } + if (configuration.getDefaultEncryptionAction() == null) { + throw new IllegalArgumentException("DynamoDbEncryptionConfiguration's DefaultEncryptionAction must not be" + + " null"); + } + } + + + private Map> getEncryptionFlagsFromConfiguration( + Map record, + DynamoDbEncryptionConfiguration configuration) { + + return record.keySet() + .stream() + // Do not let attributes created by the encryption library participate in encrypting or signing + .filter(key -> !key.equals(getMaterialDescriptionFieldName()) + && !key.equals(getSignatureFieldName())) + .collect(Collectors.toMap(Function.identity(), key -> { + EncryptionAction encryptionAction = configuration.getEncryptionActionOverrides().get(key); + + if (encryptionAction == null) { + encryptionAction = configuration.getDefaultEncryptionAction(); + } + + return getEncryptionFlagsForAction(encryptionAction); + })); + } + + private Map internalDecryptRecord( + Map itemAttributes, + Map> attributeFlags, + EncryptionContext context) { + if (attributeFlags.isEmpty()) { + 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 = context.toBuilder() + .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(); + } + itemAttributes.remove(signatureFieldName); + + String associatedData = "TABLE>" + context.getTableName() + " internalEncryptRecord( + 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 Set getEncryptionFlagsForAction(EncryptionAction encryptionAction) { + Set encryptionFlags = ENCRYPTION_ACTION_TO_FLAGS_MAP.get(encryptionAction); + + if (encryptionFlags == null) { + throw new RuntimeException("Unrecognized EncryptionAction : " + encryptionAction.name()); + } + + return encryptionFlags; + } + + 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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbSigner.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbSigner.java new file mode 100644 index 00000000..e27710c3 --- /dev/null +++ b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbSigner.java @@ -0,0 +1,250 @@ +/* + * 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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/EncryptionContext.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/EncryptionContext.java new file mode 100644 index 00000000..4651a8ea --- /dev/null +++ b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/EncryptionContext.java @@ -0,0 +1,187 @@ +/* + * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.DecryptionMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.materials.EncryptionMaterials; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.providers.EncryptionMaterialsProvider; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * This class serves to provide additional useful data to + * {@link EncryptionMaterialsProvider}s so they can more intelligently select + * the proper {@link EncryptionMaterials} or {@link DecryptionMaterials} for + * use. Any of the methods are permitted to return null. + *

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

    + *
  • TableName
  • + *
  • HashKeyName
  • + *
  • RangeKeyName (if present)
  • + *
+ * + * This class is immutable. + * + * @author Greg Rubin + */ +public final class EncryptionContext { + private final String tableName; + private final Map attributeValues; + private final Object developerContext; + private final String hashKeyName; + private final String rangeKeyName; + private final Map materialDescription; + + /** + * Return a new builder that can be used to construct an {@link EncryptionContext} + * @return A newly initialized {@link EncryptionContext.Builder}. + */ + public static Builder builder() { + return new Builder(); + } + + private EncryptionContext(Builder builder) { + tableName = builder.tableName; + attributeValues = builder.attributeValues; + developerContext = builder.developerContext; + hashKeyName = builder.hashKeyName; + rangeKeyName = builder.rangeKeyName; + materialDescription = builder.materialDescription; + } + + /** + * Returns the name of the DynamoDB Table this record is associated with. + */ + public String getTableName() { + return tableName; + } + + /** + * Returns the DynamoDB record about to be encrypted/decrypted. + */ + public Map getAttributeValues() { + return attributeValues; + } + + /** + * This object has no meaning (and will not be set or examined) by any core libraries. + * It exists to allow custom object mappers and data access layers to pass + * data to {@link EncryptionMaterialsProvider}s through the {@link DynamoDbEncryptor}. + */ + public Object getDeveloperContext() { + return developerContext; + } + + /** + * Returns the name of the HashKey attribute for the record to be encrypted/decrypted. + */ + public String getHashKeyName() { + return hashKeyName; + } + + /** + * Returns the name of the RangeKey attribute for the record to be encrypted/decrypted. + */ + public String getRangeKeyName() { + return rangeKeyName; + } + + public Map getMaterialDescription() { + return materialDescription; + } + + /** + * Converts an existing {@link EncryptionContext} into a builder that can be used to mutate and make a new version. + * @return A new {@link EncryptionContext.Builder} with all the fields filled out to match the current object. + */ + public Builder toBuilder() { + return new Builder(this); + } + + /** + * Builder class for {@link EncryptionContext}. + * Mutable objects (other than developerContext) will undergo + * a defensive copy prior to being stored in the builder. + * + * This class is not thread-safe. + */ + public static final class Builder { + private String tableName = null; + private Map attributeValues = null; + private Object developerContext = null; + private String hashKeyName = null; + private String rangeKeyName = null; + private Map materialDescription = null; + + private Builder() { + } + + private 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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/EncryptionContextOperators.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/EncryptionContextOperators.java new file mode 100644 index 00000000..2c880d39 --- /dev/null +++ b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/EncryptionContextOperators.java @@ -0,0 +1,79 @@ +/* + * 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; + +import java.util.Map; +import java.util.function.UnaryOperator; + +/** + * Implementations of common operators for overriding the EncryptionContext + */ +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 + */ + 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 + */ + 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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/EncryptionFlags.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/EncryptionFlags.java new file mode 100644 index 00000000..ce3031da --- /dev/null +++ b/sdk2/src/main/java/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 + */ +enum EncryptionFlags { + ENCRYPT, + SIGN +} diff --git a/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/exceptions/DynamoDbEncryptionException.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/exceptions/DynamoDbEncryptionException.java new file mode 100644 index 00000000..f245d66e --- /dev/null +++ b/sdk2/src/main/java/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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/AbstractRawMaterials.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/AbstractRawMaterials.java new file mode 100644 index 00000000..5dfbb197 --- /dev/null +++ b/sdk2/src/main/java/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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/AsymmetricRawMaterials.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/AsymmetricRawMaterials.java new file mode 100644 index 00000000..003d0b60 --- /dev/null +++ b/sdk2/src/main/java/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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/CryptographicMaterials.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/CryptographicMaterials.java new file mode 100644 index 00000000..033d331f --- /dev/null +++ b/sdk2/src/main/java/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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/DecryptionMaterials.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/DecryptionMaterials.java new file mode 100644 index 00000000..00f8548b --- /dev/null +++ b/sdk2/src/main/java/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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/EncryptionMaterials.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/EncryptionMaterials.java new file mode 100644 index 00000000..ecef9e9f --- /dev/null +++ b/sdk2/src/main/java/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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/SymmetricRawMaterials.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/SymmetricRawMaterials.java new file mode 100644 index 00000000..b3daab44 --- /dev/null +++ b/sdk2/src/main/java/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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/WrappedRawMaterials.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/WrappedRawMaterials.java new file mode 100644 index 00000000..2941cf68 --- /dev/null +++ b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/WrappedRawMaterials.java @@ -0,0 +1,200 @@ +/* + * 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); + cipher.init(Cipher.UNWRAP_MODE, unwrappingKey, Utils.getRng()); + return (SecretKey) cipher.unwrap(encryptedKey, + description.get(CONTENT_KEY_ALGORITHM), 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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/AsymmetricStaticProvider.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/AsymmetricStaticProvider.java new file mode 100644 index 00000000..b49e2b9a --- /dev/null +++ b/sdk2/src/main/java/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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/DirectKmsMaterialsProvider.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/DirectKmsMaterialsProvider.java new file mode 100644 index 00000000..425a4119 --- /dev/null +++ b/sdk2/src/main/java/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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/EncryptionMaterialsProvider.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/EncryptionMaterialsProvider.java new file mode 100644 index 00000000..b60fee3e --- /dev/null +++ b/sdk2/src/main/java/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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/KeyStoreMaterialsProvider.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/KeyStoreMaterialsProvider.java new file mode 100644 index 00000000..483b81b5 --- /dev/null +++ b/sdk2/src/main/java/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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/MostRecentProvider.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/MostRecentProvider.java new file mode 100644 index 00000000..f0edc768 --- /dev/null +++ b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/MostRecentProvider.java @@ -0,0 +1,219 @@ +/* + * Copyright 2016-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.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; + +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.LRUCache; + +/** + * 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. The cache + * is not currently configurable. + */ +public class MostRecentProvider implements EncryptionMaterialsProvider { + private static final long MILLI_TO_NANO = 1000000L; + private static final long TTL_GRACE_IN_NANO = 500 * MILLI_TO_NANO; + private final ProviderStore keystore; + protected final String defaultMaterialName; + private final long ttlInNanos; + private final LRUCache cache; + private final LRUCache currentVersions; + + /** + * Creates a new {@link MostRecentProvider}. + * + * @param ttlInMillis + * The length of time in milliseconds to cache the most recent provider + */ + public MostRecentProvider(final ProviderStore keystore, final String materialName, final long ttlInMillis) { + this.keystore = checkNotNull(keystore, "keystore must not be null"); + this.defaultMaterialName = materialName; + this.ttlInNanos = ttlInMillis * MILLI_TO_NANO; + this.cache = new LRUCache(1000); + this.currentVersions = new LRUCache<>(1000); + } + + @Override + public EncryptionMaterials getEncryptionMaterials(EncryptionContext context) { + final String materialName = getMaterialName(context); + final LockedState ls = getCurrentVersion(materialName); + + final State s = ls.getState(); + if (s.provider != null && System.nanoTime() - s.lastUpdated <= ttlInNanos) { + return s.provider.getEncryptionMaterials(context); + } + if (s.provider == null || System.nanoTime() - s.lastUpdated > ttlInNanos + TTL_GRACE_IN_NANO) { + // Either we don't have a provider at all, or we're more than 500 milliseconds past + // our update time. Either way, grab the lock and force an update. + ls.lock(); + } else if (!ls.tryLock()) { + // If we can't get the lock immediately, just use the current provider + return s.provider.getEncryptionMaterials(context); + } + + try { + final long newVersion = keystore.getMaxVersion(materialName); + final long currentVersion; + final EncryptionMaterialsProvider currentProvider; + if (newVersion < 0) { + // First version of the material, so we want to allow creation + currentVersion = 0; + currentProvider = keystore.getOrCreate(materialName, currentVersion); + cache.add(buildCacheKey(materialName, currentVersion), currentProvider); + } else if (newVersion != s.currentVersion) { + // We're retrieving an existing version, so we avoid the creation + // flow as it is slower + currentVersion = newVersion; + currentProvider = keystore.getProvider(materialName, currentVersion); + cache.add(buildCacheKey(materialName, currentVersion), currentProvider); + } else { + // Our version hasn't changed, so we'll just re-use the existing + // provider to avoid the overhead of retrieving and building a new one + currentVersion = newVersion; + currentProvider = s.provider; + // There is no need to add this to the cache as it's already there + } + + ls.update(currentProvider, currentVersion); + + return ls.getState().provider.getEncryptionMaterials(context); + } finally { + ls.unlock(); + } + } + + public DecryptionMaterials getDecryptionMaterials(EncryptionContext context) { + final String materialName = getMaterialName(context); + final long version = keystore.getVersionFromMaterialDescription( + context.getMaterialDescription()); + EncryptionMaterialsProvider provider = cache.get(buildCacheKey(materialName, version)); + if (provider == null) { + provider = keystore.getProvider(materialName, version); + cache.add(buildCacheKey(materialName, version), provider); + } + return provider.getDecryptionMaterials(context); + } + + /** + * Completely empties the cache of both the current and old versions. + */ + @Override + public void refresh() { + currentVersions.clear(); + cache.clear(); + } + + public String getMaterialName() { + return defaultMaterialName; + } + + public long getTtlInMills() { + return ttlInNanos / MILLI_TO_NANO; + } + + /** + * 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 getCurrentVersion(getMaterialName()).getState().currentVersion; + } + + /** + * The last time the current version was updated. Returns 0 if we do not currently have a + * current version. + */ + public long getLastUpdated() { + return getCurrentVersion(getMaterialName()).getState().lastUpdated / MILLI_TO_NANO; + } + + protected String getMaterialName(final EncryptionContext context) { + return defaultMaterialName; + } + + private LockedState getCurrentVersion(final String materialName) { + final LockedState result = currentVersions.get(materialName); + if (result == null) { + currentVersions.add(materialName, new LockedState()); + return currentVersions.get(materialName); + } else { + return result; + } + } + + private static String buildCacheKey(final String materialName, final long version) { + StringBuilder result = new StringBuilder(materialName); + result.append('#'); + result.append(version); + return result.toString(); + } + + private static V checkNotNull(final V ref, final String errMsg) { + if (ref == null) { + throw new NullPointerException(errMsg); + } else { + return ref; + } + } + + private static class LockedState { + private final ReentrantLock lock = new ReentrantLock(true); + private volatile AtomicReference state = new AtomicReference<>(new State()); + + public State getState() { + return state.get(); + } + + public void unlock() { + lock.unlock(); + } + + public boolean tryLock() { + return lock.tryLock(); + } + + public void lock() { + lock.lock(); + } + + public void update(EncryptionMaterialsProvider provider, long currentVersion) { + if (!lock.isHeldByCurrentThread()) { + throw new IllegalStateException("Lock not held by current thread"); + } + state.set(new State(provider, currentVersion)); + } + } + + private static class State { + public final EncryptionMaterialsProvider provider; + public final long currentVersion; + public final long lastUpdated; + + public State() { + this(null, -1); + } + + public State(EncryptionMaterialsProvider provider, long currentVersion) { + this.provider = provider; + this.currentVersion = currentVersion; + this.lastUpdated = currentVersion == -1 ? 0 : System.nanoTime(); + } + } +} diff --git a/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/SymmetricStaticProvider.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/SymmetricStaticProvider.java new file mode 100644 index 00000000..8a63a032 --- /dev/null +++ b/sdk2/src/main/java/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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/WrappedMaterialsProvider.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/WrappedMaterialsProvider.java new file mode 100644 index 00000000..1c92fb3f --- /dev/null +++ b/sdk2/src/main/java/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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/store/MetaStore.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/store/MetaStore.java new file mode 100644 index 00000000..9bc50036 --- /dev/null +++ b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/store/MetaStore.java @@ -0,0 +1,432 @@ +/* + * 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.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.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +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.DynamoDbEncryptionConfiguration; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.EncryptionAction; +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 DynamoDbEncryptionConfiguration encryptionConfiguration; + private final String tableName; + private final DynamoDbClient ddb; + private final DynamoDbEncryptor encryptor; + 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"); + + 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.encryptionConfiguration = DynamoDbEncryptionConfiguration.builder() + .encryptionContext(EncryptionContext.builder() + .tableName(this.tableName) + .hashKeyName(DEFAULT_HASH_KEY) + .rangeKeyName(DEFAULT_RANGE_KEY) + .build()) + // All fields default to ENCRYPT_AND_SIGN with 'sign only' fields being explicitly overridden + .defaultEncryptionAction(EncryptionAction.ENCRYPT_AND_SIGN) + .addEncryptionActionOverrides(getSignedOnlyFields(extraDataSupplier).stream() + .collect(Collectors.toMap(Function.identity(), + ignored -> EncryptionAction.SIGN_ONLY))) + .build(); + ; + } + + @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) { + return encryptor.decryptRecord(ciphertext, this.encryptionConfiguration); + } + + /** + * 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) { + return encryptor.encryptRecord(plaintext, this.encryptionConfiguration); + } + + 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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/store/ProviderStore.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/store/ProviderStore.java new file mode 100644 index 00000000..a29fe9b3 --- /dev/null +++ b/sdk2/src/main/java/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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/AttributeValueMarshaller.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/AttributeValueMarshaller.java new file mode 100644 index 00000000..e9348af0 --- /dev/null +++ b/sdk2/src/main/java/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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/Base64.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/Base64.java new file mode 100644 index 00000000..ee94a86a --- /dev/null +++ b/sdk2/src/main/java/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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/ByteBufferInputStream.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/ByteBufferInputStream.java new file mode 100644 index 00000000..ff703068 --- /dev/null +++ b/sdk2/src/main/java/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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/Hkdf.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/Hkdf.java new file mode 100644 index 00000000..15422aaa --- /dev/null +++ b/sdk2/src/main/java/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/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/LRUCache.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/LRUCache.java new file mode 100644 index 00000000..22a4d35e --- /dev/null +++ b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/LRUCache.java @@ -0,0 +1,149 @@ +/* + * 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.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +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; + /** + * Listener for cache entry eviction. + */ + private final RemovalListener listener; + /** + * Maximum size of the cache. + */ + private final int maxSize; + + /** + * @param maxSize + * the maximum number of entries of the cache + * @param listener + * object which is notified immediately prior to the removal of + * any objects from the cache + */ + public LRUCache(final int maxSize, final RemovalListener listener) { + if (maxSize < 1) { + throw new IllegalArgumentException("maxSize " + maxSize + " must be at least 1"); + } + this.maxSize = maxSize; + this.listener = listener; + map = Collections.synchronizedMap(new LRUHashMap(maxSize, listener)); + } + + /** + * @param maxSize + * the maximum number of entries of the cache + */ + public LRUCache(final int maxSize) { + this(maxSize, null); + } + + /** + * 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() { + // The more complicated logic is to ensure that the listener is + // actually called for all entries. + if (listener != null) { + List> removedEntries = new ArrayList>(); + synchronized (map) { + Iterator> it = map.entrySet().iterator(); + while(it.hasNext()) { + removedEntries.add(it.next()); + it.remove(); + } + } + for (Entry entry : removedEntries) { + listener.onRemoval(entry); + } + } else { + map.clear(); + } + } + + @Override + public String toString() { + return map.toString(); + } + + @SuppressWarnings("serial") + private static class LRUHashMap extends LinkedHashMap { + private final int maxSize; + private final RemovalListener listener; + + private LRUHashMap(final int maxSize, final RemovalListener listener) { + super(10, 0.75F, true); + this.maxSize = maxSize; + this.listener = listener; + } + + @Override + protected boolean removeEldestEntry(final Entry eldest) { + if (size() > maxSize) { + if (listener != null) { + listener.onRemoval(eldest); + } + return true; + } + return false; + } + } + + public interface RemovalListener { + void onRemoval(Entry entry); + } +} diff --git a/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/Utils.java b/sdk2/src/main/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/Utils.java new file mode 100644 index 00000000..6d092cc0 --- /dev/null +++ b/sdk2/src/main/java/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/sdk2/src/test/java/log4j.properties b/sdk2/src/test/java/log4j.properties new file mode 100644 index 00000000..ad0e4b0a --- /dev/null +++ b/sdk2/src/test/java/log4j.properties @@ -0,0 +1,13 @@ +log4j.rootLogger=INFO, A1 +log4j.appender.A1=org.apache.log4j.ConsoleAppender +log4j.appender.A1.layout=org.apache.log4j.PatternLayout + +# Print the date in ISO 8601 format +log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c - %m%n + +# Adjust to see more / less logging +#log4j.logger.httpclient.wire=TRACE +log4j.logger.com.amazonaws=DEBUG + +# HttpClient 4 Wire Logging +# log4j.logger.org.apache.http.wire=DEBUG \ No newline at end of file diff --git a/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DelegatedEncryptionTest.java b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DelegatedEncryptionTest.java new file mode 100644 index 00000000..f11de383 --- /dev/null +++ b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DelegatedEncryptionTest.java @@ -0,0 +1,263 @@ +/* + * 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.Arrays.asList; +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.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +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.exceptions.DynamoDbEncryptionException; +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.EncryptionTestHelper; +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 = new DynamoDbEncryptor(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() { + Map encryptedAttributes = + EncryptionTestHelper.encryptAllFieldsExcept(encryptor, Collections.unmodifiableMap(attribs), context, asList("hashKey", "rangeKey", "version")); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, Collections.unmodifiableMap(encryptedAttributes), context, asList("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 = DynamoDbEncryptionException.class) + public void fullEncryptionBadSignature() { + Map encryptedAttributes = + EncryptionTestHelper.encryptAllFieldsExcept(encryptor, Collections.unmodifiableMap(attribs), context, asList("hashKey", "rangeKey", "version")); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map modifiedEncryptedAttributes = new HashMap<>(encryptedAttributes); + modifiedEncryptedAttributes.put("hashKey", AttributeValue.builder().n("666").build()); + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, Collections.unmodifiableMap(modifiedEncryptedAttributes), context, asList("hashKey", "rangeKey", "version")); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void badVersionNumber() { + Map encryptedAttributes = + EncryptionTestHelper.encryptAllFieldsExcept(encryptor, Collections.unmodifiableMap(attribs), context, asList("hashKey", "rangeKey", "version")); + byte[] rawArray = encryptedAttributes.get(encryptor.getMaterialDescriptionFieldName()).b().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.fromByteArray(rawArray)).build()); + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, Collections.unmodifiableMap(encryptedAttributes), context, asList("hashKey", "rangeKey", "version")); + } + + @Test + public void signedOnly() { + Map encryptedAttributes = + EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, attribs.keySet()); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, encryptedAttributes, context, attribs.keySet()); + 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() { + prov = new SymmetricStaticProvider(null, macKey, Collections.emptyMap()); + encryptor = new DynamoDbEncryptor(prov, "encryptor-"); + Map encryptedAttributes = + EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, attribs.keySet()); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = EncryptionTestHelper.decryptAllFieldsExcept(encryptor, encryptedAttributes, context, attribs.keySet()); + 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 = DynamoDbEncryptionException.class) + public void signedOnlyBadSignature() { + Map encryptedAttributes = + EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, attribs.keySet()); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map modifiedEncryptedAttributes = new HashMap<>(encryptedAttributes); + modifiedEncryptedAttributes.put("hashKey", AttributeValue.builder().n("666").build()); + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, modifiedEncryptedAttributes, context, attribs.keySet()); + } + + @Test(expectedExceptions = DynamoDbEncryptionException.class) + public void signedOnlyNoSignature() { + Map encryptedAttributes = + EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, attribs.keySet()); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + encryptedAttributes.remove(encryptor.getSignatureFieldName()); + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, encryptedAttributes, context, attribs.keySet()); + } + + @Test + public void RsaSignedOnly() throws GeneralSecurityException { + KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA"); + rsaGen.initialize(2048, Utils.getRng()); + KeyPair sigPair = rsaGen.generateKeyPair(); + encryptor = new DynamoDbEncryptor( + new SymmetricStaticProvider(encryptionKey, sigPair, Collections.emptyMap()), "encryptor-"); + + Map encryptedAttributes = EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, attribs.keySet()); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, encryptedAttributes, context, attribs.keySet()); + 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 = DynamoDbEncryptionException.class) + public void RsaSignedOnlyBadSignature() throws GeneralSecurityException { + KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA"); + rsaGen.initialize(2048, Utils.getRng()); + KeyPair sigPair = rsaGen.generateKeyPair(); + encryptor = new DynamoDbEncryptor( + new SymmetricStaticProvider(encryptionKey, sigPair, Collections.emptyMap()), "encryptor-"); + + Map encryptedAttributes = EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, attribs.keySet()); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map modifiedEncryptedAttributes = new HashMap<>(encryptedAttributes); + modifiedEncryptedAttributes.put("hashKey", AttributeValue.builder().n("666").build()); + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, modifiedEncryptedAttributes, context, attribs.keySet()); + } + + 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/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DelegatedEnvelopeEncryptionTest.java b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DelegatedEnvelopeEncryptionTest.java new file mode 100644 index 00000000..a403db97 --- /dev/null +++ b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DelegatedEnvelopeEncryptionTest.java @@ -0,0 +1,265 @@ +/* + * 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.Arrays.asList; +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.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +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.exceptions.DynamoDbEncryptionException; +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.EncryptionTestHelper; +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() { + prov = new WrappedMaterialsProvider(encryptionKey, encryptionKey, macKey, Collections.emptyMap()); + encryptor = new DynamoDbEncryptor(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() { + Map encryptedAttributes = + EncryptionTestHelper.encryptAllFieldsExcept(encryptor, Collections.unmodifiableMap(attribs), context, asList("hashKey", "rangeKey", "version")); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, Collections.unmodifiableMap(encryptedAttributes), context, asList("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 = DynamoDbEncryptionException.class) + public void fullEncryptionBadSignature() { + Map encryptedAttributes = + EncryptionTestHelper.encryptAllFieldsExcept(encryptor, Collections.unmodifiableMap(attribs), context, asList("hashKey", "rangeKey", "version")); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map modifiedEncryptedAttributes = new HashMap<>(encryptedAttributes); + modifiedEncryptedAttributes.put("hashKey", AttributeValue.builder().n("666").build()); + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, Collections.unmodifiableMap(modifiedEncryptedAttributes), context, asList("hashKey", "rangeKey", "version")); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void badVersionNumber() { + Map encryptedAttributes = + EncryptionTestHelper.encryptAllFieldsExcept(encryptor, Collections.unmodifiableMap(attribs), context, asList("hashKey", "rangeKey", "version")); + byte[] rawArray = encryptedAttributes.get(encryptor.getMaterialDescriptionFieldName()).b().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.fromByteArray(rawArray)).build()); + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, Collections.unmodifiableMap(encryptedAttributes), context, asList("hashKey", "rangeKey", "version")); + } + + @Test + public void signedOnly() { + Map encryptedAttributes = + EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, attribs.keySet()); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, encryptedAttributes, context, attribs.keySet()); + 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() { + prov = new SymmetricStaticProvider(null, macKey, Collections.emptyMap()); + encryptor = new DynamoDbEncryptor(prov, "encryptor-"); + Map encryptedAttributes = + EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, attribs.keySet()); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = EncryptionTestHelper.decryptAllFieldsExcept(encryptor, encryptedAttributes, context, attribs.keySet()); + 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 = DynamoDbEncryptionException.class) + public void signedOnlyBadSignature() { + Map encryptedAttributes = + EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, attribs.keySet()); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map modifiedEncryptedAttributes = new HashMap<>(encryptedAttributes); + modifiedEncryptedAttributes.put("hashKey", AttributeValue.builder().n("666").build()); + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, modifiedEncryptedAttributes, context, attribs.keySet()); + } + + @Test(expectedExceptions = DynamoDbEncryptionException.class) + public void signedOnlyNoSignature() { + Map encryptedAttributes = + EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, attribs.keySet()); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + encryptedAttributes.remove(encryptor.getSignatureFieldName()); + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, encryptedAttributes, context, attribs.keySet()); + } + + @Test + public void RsaSignedOnly() throws GeneralSecurityException { + KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA"); + rsaGen.initialize(2048, Utils.getRng()); + KeyPair sigPair = rsaGen.generateKeyPair(); + encryptor = new DynamoDbEncryptor( + new SymmetricStaticProvider(encryptionKey, sigPair, + Collections.emptyMap()), "encryptor-"); + + Map encryptedAttributes = EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, attribs.keySet()); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, encryptedAttributes, context, attribs.keySet()); + 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 = DynamoDbEncryptionException.class) + public void RsaSignedOnlyBadSignature() throws GeneralSecurityException { + KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA"); + rsaGen.initialize(2048, Utils.getRng()); + KeyPair sigPair = rsaGen.generateKeyPair(); + encryptor = new DynamoDbEncryptor( + new SymmetricStaticProvider(encryptionKey, sigPair, + Collections.emptyMap()), "encryptor-"); + + Map encryptedAttributes = EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, attribs.keySet()); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map modifiedEncryptedAttributes = new HashMap<>(encryptedAttributes); + modifiedEncryptedAttributes.put("hashKey", AttributeValue.builder().n("666").build()); + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, modifiedEncryptedAttributes, context, attribs.keySet()); + } + + 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/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbEncryptorTest.java b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbEncryptorTest.java new file mode 100644 index 00000000..8acbcabc --- /dev/null +++ b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbEncryptorTest.java @@ -0,0 +1,624 @@ +/* + * 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.Arrays.asList; +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 software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.EncryptionContextOperators.overrideEncryptionContextTableName; + +import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.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.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.DynamoDbEncryptionConfiguration; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.EncryptionAction; +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.internal.Utils; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.testing.AttrMatcher; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.testing.EncryptionTestHelper; + +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 = new DynamoDbEncryptor(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()); + + // 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() { + Map encryptedAttributes = EncryptionTestHelper.encryptAllFieldsExcept(encryptor, + Collections.unmodifiableMap(attribs), context, asList("hashKey", "rangeKey", "version")); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + + Map decryptedAttributes = EncryptionTestHelper.decryptAllFieldsExcept(encryptor, + Collections.unmodifiableMap(encryptedAttributes), context, asList("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(expectedExceptions = IllegalArgumentException.class) + public void encryptWithDefaultEncryptionActionOfNullWithKeyOverridesThrowsIllegalArgumentException() { + DynamoDbEncryptionConfiguration configuration = DynamoDbEncryptionConfiguration.builder() + .addEncryptionActionOverride("hashKey", EncryptionAction.SIGN_ONLY) + .addEncryptionActionOverride("rangeKey", EncryptionAction.SIGN_ONLY) + .addEncryptionActionOverride("version", EncryptionAction.SIGN_ONLY) + .encryptionContext(context) + .build(); + + encryptor.encryptRecord(Collections.unmodifiableMap(attribs), configuration); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void decryptWithDefaultEncryptionActionOfNullWithKeyOverridesThrowsIllegalArgumentException() { + DynamoDbEncryptionConfiguration configuration = DynamoDbEncryptionConfiguration.builder() + .addEncryptionActionOverride("hashKey", EncryptionAction.SIGN_ONLY) + .addEncryptionActionOverride("rangeKey", EncryptionAction.SIGN_ONLY) + .addEncryptionActionOverride("version", EncryptionAction.SIGN_ONLY) + .encryptionContext(context) + .build(); + + encryptor.decryptRecord(Collections.unmodifiableMap(attribs), configuration); + } + + @Test + public void defaultEncryptionActionOfSignAndEncryptWithKeyOverrides() { + DynamoDbEncryptionConfiguration configuration = DynamoDbEncryptionConfiguration.builder() + .defaultEncryptionAction(EncryptionAction.ENCRYPT_AND_SIGN) + .addEncryptionActionOverride("hashKey", EncryptionAction.SIGN_ONLY) + .addEncryptionActionOverride("rangeKey", EncryptionAction.SIGN_ONLY) + .addEncryptionActionOverride("version", EncryptionAction.SIGN_ONLY) + .encryptionContext(context) + .build(); + + Map encryptedAttributes = + encryptor.encryptRecord(Collections.unmodifiableMap(attribs), configuration); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = + encryptor.decryptRecord(Collections.unmodifiableMap(encryptedAttributes), configuration); + 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 defaultEncryptionActionOfSignOnlyWithNoOverrides() { + DynamoDbEncryptionConfiguration configuration = DynamoDbEncryptionConfiguration.builder() + .defaultEncryptionAction(EncryptionAction.SIGN_ONLY) + .encryptionContext(context) + .build(); + + Map encryptedAttributes = + encryptor.encryptRecord(Collections.unmodifiableMap(attribs), configuration); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = + encryptor.decryptRecord(Collections.unmodifiableMap(encryptedAttributes), configuration); + assertThat(decryptedAttributes, AttrMatcher.match(attribs)); + + // Verify that nothing actually got encrypted + Map copyOfEncryptedAttributes = new HashMap<>(encryptedAttributes); + copyOfEncryptedAttributes.remove(encryptor.getMaterialDescriptionFieldName()); + copyOfEncryptedAttributes.remove(encryptor.getSignatureFieldName()); + assertThat(copyOfEncryptedAttributes, AttrMatcher.match(attribs)); + } + + @Test + public void defaultEncryptionActionOfIgnoreWithNoOverrides() { + DynamoDbEncryptionConfiguration configuration = DynamoDbEncryptionConfiguration.builder() + .defaultEncryptionAction(EncryptionAction.DO_NOTHING) + .encryptionContext(context) + .build(); + + Map encryptedAttributes = + new HashMap<>(encryptor.encryptRecord(Collections.unmodifiableMap(attribs), configuration)); + + // Verify that nothing actually got encrypted + Map copyOfEncryptedAttributes = new HashMap<>(encryptedAttributes); + copyOfEncryptedAttributes.remove(encryptor.getMaterialDescriptionFieldName()); + copyOfEncryptedAttributes.remove(encryptor.getSignatureFieldName()); + assertThat(copyOfEncryptedAttributes, AttrMatcher.match(attribs)); + + // Now modify one of the attributes and decrypt to prove that it was not signed + Map copyOfAttributes = new HashMap<>(attribs); + encryptedAttributes.put("stringValue", AttributeValue.builder().s("Goodbye world!").build()); + copyOfAttributes.put("stringValue", AttributeValue.builder().s("Goodbye world!").build()); + Map decryptedAttributes = encryptor.decryptRecord(encryptedAttributes, configuration); + assertThat(decryptedAttributes, AttrMatcher.match(copyOfAttributes)); + } + + @Test + public void ensureEncryptedAttributesUnmodified() { + Map encryptedAttributes = + EncryptionTestHelper.encryptAllFieldsExcept(encryptor, Collections.unmodifiableMap(attribs), context, asList("hashKey", "rangeKey", "version")); + String encryptedString = encryptedAttributes.toString(); + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, Collections.unmodifiableMap(encryptedAttributes), context, asList("hashKey", "rangeKey", "version")); + + assertEquals(encryptedString, encryptedAttributes.toString()); + } + + @Test(expectedExceptions = DynamoDbEncryptionException.class) + public void fullEncryptionBadSignature() { + Map encryptedAttributes = + EncryptionTestHelper.encryptAllFieldsExcept(encryptor, Collections.unmodifiableMap(attribs), context, asList("hashKey", "rangeKey", "version")); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map modifiedEncryptedAttributes = new HashMap<>(encryptedAttributes); + modifiedEncryptedAttributes.put("hashKey", AttributeValue.builder().n("666").build()); + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, Collections.unmodifiableMap(modifiedEncryptedAttributes), context, asList("hashKey", "rangeKey", "version")); + } + + @Test(expectedExceptions =IllegalArgumentException.class) + public void badVersionNumber() { + Map encryptedAttributes = + EncryptionTestHelper.encryptAllFieldsExcept(encryptor, Collections.unmodifiableMap(attribs), context, asList("hashKey", "rangeKey", "version")); + byte[] rawArray = encryptedAttributes.get(encryptor.getMaterialDescriptionFieldName()).b().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.fromByteArray(rawArray)).build()); + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, Collections.unmodifiableMap(encryptedAttributes), context, asList("hashKey", "rangeKey", "version")); + } + + @Test + public void signedOnly() { + Map encryptedAttributes = + EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, attribs.keySet()); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, encryptedAttributes, context, attribs.keySet()); + 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() { + prov = new InstrumentedEncryptionMaterialsProvider( + new SymmetricStaticProvider(null, macKey, Collections.emptyMap())); + encryptor = new DynamoDbEncryptor(prov, "encryptor-"); + Map encryptedAttributes = + EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, attribs.keySet()); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = EncryptionTestHelper.decryptAllFieldsExcept(encryptor, encryptedAttributes, context, attribs.keySet()); + 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 = DynamoDbEncryptionException.class) + public void signedOnlyBadSignature() { + Map encryptedAttributes = + EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, attribs.keySet()); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map modifiedEncryptedAttributes = new HashMap<>(encryptedAttributes); + modifiedEncryptedAttributes.put("hashKey", AttributeValue.builder().n("666").build()); + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, modifiedEncryptedAttributes, context, attribs.keySet()); + } + + @Test(expectedExceptions = DynamoDbEncryptionException.class) + public void signedOnlyNoSignature() { + Map encryptedAttributes = + EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, attribs.keySet()); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + encryptedAttributes.remove(encryptor.getSignatureFieldName()); + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, encryptedAttributes, context, attribs.keySet()); + } + + @Test + public void RsaSignedOnly() throws GeneralSecurityException { + KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA"); + rsaGen.initialize(2048, Utils.getRng()); + KeyPair sigPair = rsaGen.generateKeyPair(); + encryptor = new DynamoDbEncryptor( + new SymmetricStaticProvider(encryptionKey, sigPair, + Collections.emptyMap()), "encryptor-"); + + Map encryptedAttributes = EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, attribs.keySet()); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, encryptedAttributes, context, attribs.keySet()); + 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 = DynamoDbEncryptionException.class) + public void RsaSignedOnlyBadSignature() throws GeneralSecurityException { + KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA"); + rsaGen.initialize(2048, Utils.getRng()); + KeyPair sigPair = rsaGen.generateKeyPair(); + encryptor = new DynamoDbEncryptor( + new SymmetricStaticProvider(encryptionKey, sigPair, + Collections.emptyMap()), "encryptor-"); + + Map encryptedAttributes = EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, attribs.keySet()); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map modifiedEncryptedAttributes = new HashMap<>(encryptedAttributes); + modifiedEncryptedAttributes.put("hashKey", AttributeValue.builder().n("666").build()); + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, modifiedEncryptedAttributes, context, attribs.keySet()); + } + + /** + * Tests that no exception is thrown when the encryption context override operator is null + */ + @Test + public void testNullEncryptionContextOperator() { + DynamoDbEncryptor encryptor = new DynamoDbEncryptor(prov); + encryptor.setEncryptionContextOverrideOperator(null); + EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, Collections.emptyList()); + } + + /** + * Tests decrypt and encrypt with an encryption context override operator + */ + @Test + public void testTableNameOverriddenEncryptionContextOperator() { + // Ensure that the table name is different from what we override the table to. + assertThat(context.getTableName(), not(equalTo(OVERRIDDEN_TABLE_NAME))); + DynamoDbEncryptor encryptor = new DynamoDbEncryptor(prov); + encryptor.setEncryptionContextOverrideOperator(overrideEncryptionContextTableName(context.getTableName(), OVERRIDDEN_TABLE_NAME)); + Map encryptedItems = EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, Collections.emptyList()); + Map decryptedItems = EncryptionTestHelper.decryptAllFieldsExcept(encryptor, 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() { + // Ensure that the table name is different from what we override the table to. + assertThat(context.getTableName(), not(equalTo(OVERRIDDEN_TABLE_NAME))); + DynamoDbEncryptor encryptor = new DynamoDbEncryptor(prov); + DynamoDbEncryptor encryptorWithoutOverride = new DynamoDbEncryptor(prov); + encryptor.setEncryptionContextOverrideOperator(overrideEncryptionContextTableName(context.getTableName(), OVERRIDDEN_TABLE_NAME)); + Map encryptedItems = EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, Collections.emptyList()); + + EncryptionContext expectedOverriddenContext = context.toBuilder().tableName("TheBestTableName").build(); + Map decryptedItems = + EncryptionTestHelper.decryptAllFieldsExcept(encryptorWithoutOverride, 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 = DynamoDbEncryptionException.class) + public void testTableNameOverriddenEncryptionContextOperatorWithSecondEncryptorButTheOriginalEncryptionContext() { + // Ensure that the table name is different from what we override the table to. + assertThat(context.getTableName(), not(equalTo(OVERRIDDEN_TABLE_NAME))); + DynamoDbEncryptor encryptor = new DynamoDbEncryptor(prov); + DynamoDbEncryptor encryptorWithoutOverride = new DynamoDbEncryptor(prov); + encryptor.setEncryptionContextOverrideOperator(overrideEncryptionContextTableName(context.getTableName(), OVERRIDDEN_TABLE_NAME)); + Map encryptedItems = EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, Collections.emptyList()); + + // Use the original encryption context, and expect a signature failure + EncryptionTestHelper.decryptAllFieldsExcept(encryptorWithoutOverride, encryptedItems, context, Collections.emptyList()); + } + + @Test + public void EcdsaSignedOnly() throws GeneralSecurityException { + encryptor = new DynamoDbEncryptor(getMaterialProviderwithECDSA()); + + Map encryptedAttributes = EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, attribs.keySet()); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map decryptedAttributes = + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, encryptedAttributes, context, attribs.keySet()); + 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 = DynamoDbEncryptionException.class) + public void EcdsaSignedOnlyBadSignature() throws GeneralSecurityException { + + encryptor = new DynamoDbEncryptor(getMaterialProviderwithECDSA()); + + Map encryptedAttributes = EncryptionTestHelper.encryptAllFieldsExcept(encryptor, attribs, context, attribs.keySet()); + assertThat(encryptedAttributes, AttrMatcher.invert(attribs)); + Map modifiedEncryptedAttributes = new HashMap<>(encryptedAttributes); + modifiedEncryptedAttributes.put("hashKey", AttributeValue.builder().n("666").build()); + EncryptionTestHelper.decryptAllFieldsExcept(encryptor, modifiedEncryptedAttributes, context, attribs.keySet()); + } + + @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(expectedExceptions = IllegalArgumentException.class) + public void encryptWithNullAttributeValuesThrowsIllegalArgumentException() { + encryptor.encryptRecord(null, DynamoDbEncryptionConfiguration.builder().encryptionContext(context).build()); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void encryptWithNullEncryptionContextThrowsIllegalArgumentException() { + encryptor.encryptRecord(attribs, DynamoDbEncryptionConfiguration.builder().build()); + } + + 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/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbSignerTest.java b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbSignerTest.java new file mode 100644 index 00000000..b51193f2 --- /dev/null +++ b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/DynamoDbSignerTest.java @@ -0,0 +1,438 @@ +/* + * 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.fromByteArray(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.fromByteArray(new byte[] { 0, 1, 2, 3}), + SdkBytes.fromByteArray(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.fromByteArray(new byte[] { 3, 2, 1}), + SdkBytes.fromByteArray(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.fromByteArray(new byte[] { 0, 1, 2, 3}), + SdkBytes.fromByteArray(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.fromByteArray(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.fromByteArray(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.fromByteArray(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.fromByteArray(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.fromByteArray(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.fromByteArray(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.fromByteArray(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.fromByteArray(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.fromByteArray(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.fromByteArray(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.fromByteArray(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.fromByteArray(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.fromByteArray(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.fromByteArray(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.fromByteArray(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.fromByteArray(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.fromByteArray(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.fromByteArray(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)); + } +} diff --git a/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/EncryptionContextOperatorsTest.java b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/EncryptionContextOperatorsTest.java new file mode 100644 index 00000000..9a386c8d --- /dev/null +++ b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/EncryptionContextOperatorsTest.java @@ -0,0 +1,163 @@ +/* + * 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; + +import static org.testng.AssertJUnit.assertEquals; +import static software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.EncryptionContextOperators.overrideEncryptionContextTableName; +import static software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.EncryptionContextOperators.overrideEncryptionContextTableNameUsingMap; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import org.testng.annotations.Test; + +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/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/AsymmetricRawMaterialsTest.java b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/AsymmetricRawMaterialsTest.java new file mode 100644 index 00000000..a6f9b1f4 --- /dev/null +++ b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/AsymmetricRawMaterialsTest.java @@ -0,0 +1,132 @@ +/* + * 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/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/SymmetricRawMaterialsTest.java b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/materials/SymmetricRawMaterialsTest.java new file mode 100644 index 00000000..f4d3568f --- /dev/null +++ b/sdk2/src/test/java/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() { + 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() { + 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() { + 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() { + 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/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/AsymmetricStaticProviderTest.java b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/AsymmetricStaticProviderTest.java new file mode 100644 index 00000000..c42460dd --- /dev/null +++ b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/AsymmetricStaticProviderTest.java @@ -0,0 +1,219 @@ +/* + * 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.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +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.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.encryption.materials.WrappedRawMaterials; +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 = EncryptionContext.builder().build(); + } + + @Test + public void simpleMac() { + AsymmetricStaticProvider prov = new AsymmetricStaticProvider(encryptionPair, macKey, Collections.emptyMap()); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertThat(encryptionKey, is(not(nullValue()))); + assertEquals(macKey, eMat.getSigningKey()); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(macKey, dMat.getVerificationKey()); + } + + @Test + public void simpleSig() { + AsymmetricStaticProvider prov = new AsymmetricStaticProvider(encryptionPair, sigPair, Collections.emptyMap()); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertThat(encryptionKey, is(not(nullValue()))); + assertEquals(sigPair.getPrivate(), eMat.getSigningKey()); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(sigPair.getPublic(), dMat.getVerificationKey()); + } + + @Test + public void randomEnvelopeKeys() { + AsymmetricStaticProvider prov = new AsymmetricStaticProvider(encryptionPair, macKey, Collections.emptyMap()); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertThat(encryptionKey, is(not(nullValue()))); + 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(); + } + + // Following tests should be moved the WrappedRawMaterialsTests when that is created + @Test + public void explicitWrappingAlgorithmPkcs1() throws GeneralSecurityException { + Map desc = new HashMap<>(); + desc.put(WrappedRawMaterials.KEY_WRAPPING_ALGORITHM, "RSA/ECB/PKCS1Padding"); + + AsymmetricStaticProvider prov = new AsymmetricStaticProvider(encryptionPair, sigPair, desc); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertThat(encryptionKey, is(not(nullValue()))); + assertEquals(sigPair.getPrivate(), eMat.getSigningKey()); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals("RSA/ECB/PKCS1Padding", eMat.getMaterialDescription().get(WrappedRawMaterials.KEY_WRAPPING_ALGORITHM)); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(sigPair.getPublic(), dMat.getVerificationKey()); + } + + @Test + public void explicitWrappingAlgorithmPkcs2() throws GeneralSecurityException { + Map desc = new HashMap<>(); + desc.put(WrappedRawMaterials.KEY_WRAPPING_ALGORITHM, "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); + + AsymmetricStaticProvider prov = new AsymmetricStaticProvider(encryptionPair, sigPair, desc); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertThat(encryptionKey, is(not(nullValue()))); + assertEquals(sigPair.getPrivate(), eMat.getSigningKey()); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals("RSA/ECB/OAEPWithSHA-256AndMGF1Padding", eMat.getMaterialDescription().get(WrappedRawMaterials.KEY_WRAPPING_ALGORITHM)); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(sigPair.getPublic(), dMat.getVerificationKey()); + } + + @Test + public void explicitContentKeyAlgorithm() throws GeneralSecurityException { + Map desc = new HashMap<>(); + desc.put(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, "AES"); + + AsymmetricStaticProvider prov = new AsymmetricStaticProvider(encryptionPair, sigPair, desc); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertThat(encryptionKey, is(not(nullValue()))); + assertEquals(sigPair.getPrivate(), eMat.getSigningKey()); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals("AES", eMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM)); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(sigPair.getPublic(), dMat.getVerificationKey()); + } + + @Test + public void explicitContentKeyLength128() throws GeneralSecurityException { + Map desc = new HashMap<>(); + desc.put(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, "AES/128"); + + AsymmetricStaticProvider prov = new AsymmetricStaticProvider(encryptionPair, sigPair, desc); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertThat(encryptionKey, is(not(nullValue()))); + assertEquals(16, encryptionKey.getEncoded().length); // 128 Bits + assertEquals(sigPair.getPrivate(), eMat.getSigningKey()); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals("AES", eMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM)); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(sigPair.getPublic(), dMat.getVerificationKey()); + } + + @Test + public void explicitContentKeyLength256() throws GeneralSecurityException { + Map desc = new HashMap<>(); + desc.put(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, "AES/256"); + + AsymmetricStaticProvider prov = new AsymmetricStaticProvider(encryptionPair, sigPair, desc); + + EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx); + SecretKey encryptionKey = eMat.getEncryptionKey(); + assertThat(encryptionKey, is(not(nullValue()))); + assertEquals(32, encryptionKey.getEncoded().length); // 256 Bits + assertEquals(sigPair.getPrivate(), eMat.getSigningKey()); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat)); + assertEquals("AES", eMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM)); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(sigPair.getPublic(), dMat.getVerificationKey()); + } + + private static EncryptionContext ctx(EncryptionMaterials mat) { + return EncryptionContext.builder() + .materialDescription(mat.getMaterialDescription()).build(); + } +} diff --git a/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/DirectKmsMaterialsProviderTest.java b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/DirectKmsMaterialsProviderTest.java new file mode 100644 index 00000000..29aa48f5 --- /dev/null +++ b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/DirectKmsMaterialsProviderTest.java @@ -0,0 +1,402 @@ +/* + * 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.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.util.Base64; +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.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; +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.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 = 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 = 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 = ctx(eMat).toBuilder() + .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 = 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 = ctx(eMat).toBuilder() + .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() { + DirectKmsMaterialsProvider prov = new DirectKmsMaterialsProvider(kms, keyId); + + Map attrVals = new HashMap<>(); + attrVals.put("hk", + AttributeValue.builder().b(SdkBytes.fromByteArray("Foo".getBytes(StandardCharsets.UTF_8))).build()); + attrVals.put("rk", + AttributeValue.builder().b(SdkBytes.fromByteArray("Bar".getBytes(StandardCharsets.UTF_8))).build()); + + ctx = 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.getEncoder().encodeToString("Foo".getBytes(StandardCharsets.UTF_8)), + kmsCtx.get("hk")); + assertEquals(Base64.getEncoder().encodeToString("Bar".getBytes(StandardCharsets.UTF_8)), + kmsCtx.get("rk")); + assertEquals("KmsTableName", kmsCtx.get("*aws-kms-table*")); + + EncryptionContext dCtx = ctx(eMat).toBuilder() + .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() { + 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() { + 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() { + 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() { + 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() { + ExtendedKmsMaterialProvider prov = new ExtendedKmsMaterialProvider(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 = 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 = ctx(eMat).toBuilder() + .hashKeyName("hk") + .rangeKeyName("rk") + .tableName("KmsTableName") + .attributeValues(attrVals) + .build(); + + DecryptionMaterials dMat = prov.getDecryptionMaterials(dCtx); + assertEquals(encryptionKey, dMat.getDecryptionKey()); + assertEquals(signingKey, dMat.getVerificationKey()); + } + + @Test(expectedExceptions = DynamoDbEncryptionException.class) + public void encryptionKeyIdMismatch() { + 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 = EncryptionContext.builder().hashKeyName("hk").rangeKeyName("rk") + .tableName("KmsTableName").attributeValues(attrVals).build(); + EncryptionMaterials eMat = directProvider.getEncryptionMaterials(ctx); + + EncryptionContext dCtx = ctx(eMat).toBuilder() + .hashKeyName("hk") + .rangeKeyName("rk") + .tableName("KmsTableName") + .attributeValues(attrVals) + .build(); + + ExtendedKmsMaterialProvider extendedProvider = new ExtendedKmsMaterialProvider(kms, keyId, "encryptionKeyId"); + + extendedProvider.getDecryptionMaterials(dCtx); + } + + @Test(expectedExceptions = DynamoDbEncryptionException.class) + public void missingEncryptionKeyId() { + ExtendedKmsMaterialProvider prov = new ExtendedKmsMaterialProvider(kms, keyId, "encryptionKeyId"); + + Map attrVals = new HashMap<>(); + attrVals.put("hk", AttributeValue.builder().n("10").build()); + attrVals.put("rk", AttributeValue.builder().n("20").build()); + + ctx = 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 ExtendedKmsMaterialProvider extends DirectKmsMaterialsProvider { + private final String encryptionKeyIdAttributeName; + + public ExtendedKmsMaterialProvider(KmsClient kms, String encryptionKeyId, String encryptionKeyIdAttributeName) { + super(kms, encryptionKeyId); + + this.encryptionKeyIdAttributeName = encryptionKeyIdAttributeName; + } + + @Override + protected String selectEncryptionKeyId(EncryptionContext context) throws DynamoDbEncryptionException { + if (!context.getAttributeValues().containsKey(encryptionKeyIdAttributeName)) { + throw new DynamoDbEncryptionException("encryption key attribute is not provided"); + } + + return context.getAttributeValues().get(encryptionKeyIdAttributeName).s(); + } + + @Override + protected void validateEncryptionKeyId(String encryptionKeyId, EncryptionContext context) + throws DynamoDbEncryptionException { + if (!context.getAttributeValues().containsKey(encryptionKeyIdAttributeName)) { + throw new DynamoDbEncryptionException("encryption key attribute is not provided"); + } + + String customEncryptionKeyId = context.getAttributeValues().get(encryptionKeyIdAttributeName).s(); + if (!customEncryptionKeyId.equals(encryptionKeyId)) { + throw new DynamoDbEncryptionException("encryption key ids do not match."); + } + } + + @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 EncryptionContext.builder() + .materialDescription(mat.getMaterialDescription()).build(); + } +} diff --git a/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/KeyStoreMaterialsProviderTest.java b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/KeyStoreMaterialsProviderTest.java new file mode 100644 index 00000000..3bc77615 --- /dev/null +++ b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/KeyStoreMaterialsProviderTest.java @@ -0,0 +1,288 @@ +/* + * 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/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/MostRecentProviderTests.java b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/MostRecentProviderTests.java new file mode 100644 index 00000000..58236929 --- /dev/null +++ b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/MostRecentProviderTests.java @@ -0,0 +1,545 @@ +/* + * 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; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; + +import java.util.Collections; +import java.util.Map; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.mockito.Mockito; +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.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.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.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.QueryRequest; + +public class MostRecentProviderTests { + 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.builder().encryptionMaterialsProvider(BASE_PROVIDER).build(); + + private LocalDynamoDb localDynamoDb = new LocalDynamoDb(); + private DynamoDbClient client; + private ProviderStore store; + private EncryptionContext ctx; + + @BeforeMethod + public void setup() { + localDynamoDb.start(); + client = Mockito.spy(localDynamoDb.createLimitedWrappedClient()); + MetaStore.createTable(client, TABLE_NAME, ProvisionedThroughput.builder() + .writeCapacityUnits(1L) + .readCapacityUnits(1L) + .build()); + store = new MetaStore(client, TABLE_NAME, ENCRYPTOR); + ctx = EncryptionContext.builder().build(); + reset(client); + } + + @AfterMethod + public void stopLocalDynamoDb() { + localDynamoDb.stop(); + } + + @Test + public void constructor() { + final MostRecentProvider prov = new MostRecentProvider(store, MATERIAL_NAME, 100); + assertEquals(MATERIAL_NAME, prov.getMaterialName()); + assertEquals(100, prov.getTtlInMills()); + assertEquals(-1, prov.getCurrentVersion()); + assertEquals(0, prov.getLastUpdated()); + } + + @Test + public void singleVersion() throws InterruptedException { + final MostRecentProvider prov = new MostRecentProvider(store, MATERIAL_NAME, 500); + verify(client, never()).putItem(any(PutItemRequest.class)); + final EncryptionMaterials eMat1 = prov.getEncryptionMaterials(ctx); + // It's a new provider, so we see a single putItem + verify(client).putItem(any(PutItemRequest.class)); + reset(client); + // Ensure the cache is working + final EncryptionMaterials eMat2 = prov.getEncryptionMaterials(ctx); + verifyNoMoreInteractions(client); + 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); + verify(client).query(any(QueryRequest.class)); + verifyNoMoreInteractions(client); + 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 MostRecentProvider prov2 = new MostRecentProvider(store, MATERIAL_NAME, 500); + final DecryptionMaterials dMat1 = prov2.getDecryptionMaterials(ctx(eMat1)); + reset(client); + 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()); + verifyNoMoreInteractions(client); + } + + @Test + public void singleVersionWithRefresh() throws InterruptedException { + final MostRecentProvider prov = new MostRecentProvider(store, MATERIAL_NAME, 500); + verify(client, never()).putItem(any(PutItemRequest.class)); + final EncryptionMaterials eMat1 = prov.getEncryptionMaterials(ctx); + // It's a new provider, so we see a single putItem + verify(client).putItem(any(PutItemRequest.class)); + reset(client); + // Ensure the cache is working + final EncryptionMaterials eMat2 = prov.getEncryptionMaterials(ctx); + verifyNoMoreInteractions(client); + assertEquals(0, store.getVersionFromMaterialDescription(eMat1.getMaterialDescription())); + assertEquals(0, store.getVersionFromMaterialDescription(eMat2.getMaterialDescription())); + prov.refresh(); + final EncryptionMaterials eMat3 = prov.getEncryptionMaterials(ctx); + verify(client).query(any(QueryRequest.class)); // To find current version + verify(client).getItem(any(GetItemRequest.class)); + 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); + reset(client); + // 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()); + verifyNoMoreInteractions(client); + + // Ensure we can decrypt all of them without hitting ddb more than the minimum + final MostRecentProvider prov2 = new MostRecentProvider(store, MATERIAL_NAME, 500); + final DecryptionMaterials dMat1 = prov2.getDecryptionMaterials(ctx(eMat1)); + reset(client); + 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()); + verifyNoMoreInteractions(client); + } + + + @Test + public void twoVersions() throws InterruptedException { + final MostRecentProvider prov = new MostRecentProvider(store, MATERIAL_NAME, 500); + verify(client, never()).putItem(any(PutItemRequest.class)); + final EncryptionMaterials eMat1 = prov.getEncryptionMaterials(ctx); + // It's a new provider, so we see a single putItem + verify(client).putItem(any(PutItemRequest.class)); + reset(client); + // Create the new material + store.newProvider(MATERIAL_NAME); + reset(client); + + // Ensure the cache is working + final EncryptionMaterials eMat2 = prov.getEncryptionMaterials(ctx); + verifyNoMoreInteractions(client); + assertEquals(0, store.getVersionFromMaterialDescription(eMat1.getMaterialDescription())); + assertEquals(0, store.getVersionFromMaterialDescription(eMat2.getMaterialDescription())); + verifyNoMoreInteractions(client); + // Let the TTL be exceeded + Thread.sleep(500); + final EncryptionMaterials eMat3 = prov.getEncryptionMaterials(ctx); + + verify(client).query(any(QueryRequest.class)); // To find current version + verify(client).getItem(any(GetItemRequest.class)); // To retrieve current version + verify(client, never()).putItem(any(PutItemRequest.class)); // 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 MostRecentProvider prov2 = new MostRecentProvider(store, MATERIAL_NAME, 500); + final DecryptionMaterials dMat1 = prov2.getDecryptionMaterials(ctx(eMat1)); + reset(client); + 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 + verify(client).getItem(any(GetItemRequest.class)); + verifyNoMoreInteractions(client); + } + + @Test + public void twoVersionsWithRefresh() throws InterruptedException { + final MostRecentProvider prov = new MostRecentProvider(store, MATERIAL_NAME, 100); + verify(client, never()).putItem(any(PutItemRequest.class)); + final EncryptionMaterials eMat1 = prov.getEncryptionMaterials(ctx); + // It's a new provider, so we see a single putItem + verify(client).putItem(any(PutItemRequest.class)); + reset(client); + // Create the new material + store.newProvider(MATERIAL_NAME); + reset(client); + // Ensure the cache is working + final EncryptionMaterials eMat2 = prov.getEncryptionMaterials(ctx); + verifyNoMoreInteractions(client); + assertEquals(0, store.getVersionFromMaterialDescription(eMat1.getMaterialDescription())); + assertEquals(0, store.getVersionFromMaterialDescription(eMat2.getMaterialDescription())); + prov.refresh(); + final EncryptionMaterials eMat3 = prov.getEncryptionMaterials(ctx); + verify(client).query(any(QueryRequest.class)); // To find current version + verify(client).getItem(any(GetItemRequest.class)); + 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 MostRecentProvider prov2 = new MostRecentProvider(store, MATERIAL_NAME, 500); + final DecryptionMaterials dMat1 = prov2.getDecryptionMaterials(ctx(eMat1)); + reset(client); + 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 + verify(client).getItem(any(GetItemRequest.class)); + verifyNoMoreInteractions(client); + } + + @Test + public void singleVersionTwoMaterials() throws InterruptedException { + final Map attr1 = Collections.singletonMap(MATERIAL_PARAM, AttributeValueBuilder.ofS("material1")); + final EncryptionContext ctx1 = ctx(attr1); + final Map attr2 = Collections.singletonMap(MATERIAL_PARAM, AttributeValueBuilder.ofS("material2")); + final EncryptionContext ctx2 = ctx(attr2); + + final MostRecentProvider prov = new ExtendedProvider(store, 500); + verify(client, never()).putItem(any(PutItemRequest.class)); + final EncryptionMaterials eMat1_1 = prov.getEncryptionMaterials(ctx1); + // It's a new provider, so we see a single putItem + verify(client).putItem(any(PutItemRequest.class)); + reset(client); + final EncryptionMaterials eMat1_2 = prov.getEncryptionMaterials(ctx2); + // It's a new provider, so we see a single putItem + verify(client).putItem(any(PutItemRequest.class)); + reset(client); + // 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); + verifyNoMoreInteractions(client); + assertEquals(0, store.getVersionFromMaterialDescription(eMat1_1.getMaterialDescription())); + assertEquals(0, store.getVersionFromMaterialDescription(eMat2_1.getMaterialDescription())); + final EncryptionMaterials eMat2_2 = prov.getEncryptionMaterials(ctx2); + verifyNoMoreInteractions(client); + 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); + verify(client).query(any(QueryRequest.class)); // To find current version + verifyNoMoreInteractions(client); + assertEquals(0, store.getVersionFromMaterialDescription(eMat3_1.getMaterialDescription())); + reset(client); + final EncryptionMaterials eMat3_2 = prov.getEncryptionMaterials(ctx2); + verify(client).query(any(QueryRequest.class)); // To find current version + verifyNoMoreInteractions(client); + 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 MostRecentProvider prov2 = new ExtendedProvider(store, 500); + final DecryptionMaterials dMat1_1 = prov2.getDecryptionMaterials(ctx(eMat1_1, attr1)); + final DecryptionMaterials dMat1_2 = prov2.getDecryptionMaterials(ctx(eMat1_2, attr2)); + reset(client); + 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()); + verifyNoMoreInteractions(client); + } + + @Test + public void singleVersionWithTwoMaterialsWithRefresh() throws InterruptedException { + final Map attr1 = Collections.singletonMap(MATERIAL_PARAM, AttributeValueBuilder.ofS("material1")); + final EncryptionContext ctx1 = ctx(attr1); + final Map attr2 = Collections.singletonMap(MATERIAL_PARAM, AttributeValueBuilder.ofS("material2")); + final EncryptionContext ctx2 = ctx(attr2); + + final MostRecentProvider prov = new ExtendedProvider(store, 500); + verify(client, never()).putItem(any(PutItemRequest.class)); + final EncryptionMaterials eMat1_1 = prov.getEncryptionMaterials(ctx1); + // It's a new provider, so we see a single putItem + verify(client).putItem(any(PutItemRequest.class)); + reset(client); + final EncryptionMaterials eMat1_2 = prov.getEncryptionMaterials(ctx2); + // It's a new provider, so we see a single putItem + verify(client).putItem(any(PutItemRequest.class)); + reset(client); + // 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); + verifyNoMoreInteractions(client); + assertEquals(0, store.getVersionFromMaterialDescription(eMat1_1.getMaterialDescription())); + assertEquals(0, store.getVersionFromMaterialDescription(eMat2_1.getMaterialDescription())); + final EncryptionMaterials eMat2_2 = prov.getEncryptionMaterials(ctx2); + verifyNoMoreInteractions(client); + assertEquals(0, store.getVersionFromMaterialDescription(eMat1_2.getMaterialDescription())); + assertEquals(0, store.getVersionFromMaterialDescription(eMat2_2.getMaterialDescription())); + + prov.refresh(); + final EncryptionMaterials eMat3_1 = prov.getEncryptionMaterials(ctx1); + verify(client).query(any(QueryRequest.class)); // To find current version + verify(client).getItem(any(GetItemRequest.class)); + final EncryptionMaterials eMat3_2 = prov.getEncryptionMaterials(ctx2); + verify(client, times(2)).query(any(QueryRequest.class)); // To find current version + verify(client, times(2)).getItem(any(GetItemRequest.class)); + 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); + reset(client); + // 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()); + verifyNoMoreInteractions(client); + + // Ensure we can decrypt all of them without hitting ddb more than the minimum + final MostRecentProvider prov2 = new ExtendedProvider(store, 500); + final DecryptionMaterials dMat1_1 = prov2.getDecryptionMaterials(ctx(eMat1_1, attr1)); + final DecryptionMaterials dMat1_2 = prov2.getDecryptionMaterials(ctx(eMat1_2, attr2)); + reset(client); + 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()); + verifyNoMoreInteractions(client); + } + + @Test + public void twoVersionsWithTwoMaterialsWithRefresh() { + final Map attr1 = Collections.singletonMap(MATERIAL_PARAM, AttributeValueBuilder.ofS("material1")); + final EncryptionContext ctx1 = ctx(attr1); + final Map attr2 = Collections.singletonMap(MATERIAL_PARAM, AttributeValueBuilder.ofS("material2")); + final EncryptionContext ctx2 = ctx(attr2); + + final MostRecentProvider prov = new ExtendedProvider(store, 500); + verify(client, never()).putItem(any(PutItemRequest.class)); + final EncryptionMaterials eMat1_1 = prov.getEncryptionMaterials(ctx1); + // It's a new provider, so we see a single putItem + verify(client).putItem(any(PutItemRequest.class)); + reset(client); + final EncryptionMaterials eMat1_2 = prov.getEncryptionMaterials(ctx2); + // It's a new provider, so we see a single putItem + verify(client).putItem(any(PutItemRequest.class)); + reset(client); + // Create the new material + store.newProvider("material1"); + store.newProvider("material2"); + reset(client); + // Ensure the cache is working + final EncryptionMaterials eMat2_1 = prov.getEncryptionMaterials(ctx1); + final EncryptionMaterials eMat2_2 = prov.getEncryptionMaterials(ctx2); + verifyNoMoreInteractions(client); + 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); + verify(client, times(2)).query(any(QueryRequest.class)); // To find current version + verify(client, times(2)).getItem(any(GetItemRequest.class)); + 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 MostRecentProvider prov2 = new ExtendedProvider(store, 500); + final DecryptionMaterials dMat1_1 = prov2.getDecryptionMaterials(ctx(eMat1_1, attr1)); + final DecryptionMaterials dMat1_2 = prov2.getDecryptionMaterials(ctx(eMat1_2, attr2)); + reset(client); + 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 once for the one old key + verify(client, times(2)).getItem(any(GetItemRequest.class)); + verifyNoMoreInteractions(client); + } + + private static EncryptionContext ctx(final Map attr) { + return EncryptionContext.builder() + .attributeValues(attr).build(); + } + + private static EncryptionContext ctx(final EncryptionMaterials mat, Map attr) { + return EncryptionContext.builder() + .attributeValues(attr) + .materialDescription(mat.getMaterialDescription()).build(); + } + + private static EncryptionContext ctx(final EncryptionMaterials mat) { + return EncryptionContext.builder() + .materialDescription(mat.getMaterialDescription()).build(); + } + + private static class ExtendedProvider extends MostRecentProvider { + public ExtendedProvider(ProviderStore keystore, long ttlInMillis) { + super(keystore, null, ttlInMillis); + } + + @Override + public long getCurrentVersion() { + throw new UnsupportedOperationException(); + } + + @Override + protected String getMaterialName(final EncryptionContext context) { + return context.getAttributeValues().get(MATERIAL_PARAM).s(); + } + } +} diff --git a/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/SymmetricStaticProviderTest.java b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/SymmetricStaticProviderTest.java new file mode 100644 index 00000000..807804bd --- /dev/null +++ b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/SymmetricStaticProviderTest.java @@ -0,0 +1,164 @@ +/* + * 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 = 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/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/store/MetaStoreTests.java b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/store/MetaStoreTests.java new file mode 100644 index 00000000..40ae26ee --- /dev/null +++ b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/encryption/providers/store/MetaStoreTests.java @@ -0,0 +1,348 @@ +/* + * 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.builder().encryptionMaterialsProvider(BASE_PROVIDER).build(); + private static final DynamoDbEncryptor TARGET_ENCRYPTOR = + DynamoDbEncryptor.builder().encryptionMaterialsProvider(TARGET_BASE_PROVIDER).build(); + + 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/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/AttributeValueMarshallerTest.java b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/AttributeValueMarshallerTest.java new file mode 100644 index 00000000..e0988162 --- /dev/null +++ b/sdk2/src/test/java/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/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/ByteBufferInputStreamTest.java b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/ByteBufferInputStreamTest.java new file mode 100644 index 00000000..71b90c19 --- /dev/null +++ b/sdk2/src/test/java/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/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/HkdfTests.java b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/HkdfTests.java new file mode 100644 index 00000000..b73e2ca2 --- /dev/null +++ b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/HkdfTests.java @@ -0,0 +1,192 @@ +/* + * 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.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +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); + assertThat(result, is(trial.expected)); + } + } + + @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); + + assertThat(key1, is(key2)); + assertThat(key1, is(key3)); + assertThat(key1, is(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/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/LRUCacheTest.java b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/LRUCacheTest.java new file mode 100644 index 00000000..3062a831 --- /dev/null +++ b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/internal/LRUCacheTest.java @@ -0,0 +1,138 @@ +/* + * 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 java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +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 + public void testListener() { + final Map removed = new HashMap(); + final LRUCache cache = new LRUCache(3, + new LRUCache.RemovalListener() { + @Override + public void onRemoval(final Entry entry) { + removed.put(entry.getKey(), entry.getValue()); + } + }); + assertTrue(cache.size() == 0); + 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")); + assertTrue(removed.isEmpty()); + cache.add("k4", "v4"); + assertTrue(cache.size() == 3); + assertNull(cache.get("k1")); + assertEquals(1, removed.size()); + assertEquals("v11", removed.get("k1")); + removed.clear(); + assertEquals("v4", cache.get("k4")); + assertEquals("v2", cache.get("k2")); + assertEquals("v3", cache.get("k3")); + assertTrue(cache.size() == 3); + cache.add("k5", "v5"); + assertEquals(1, removed.size()); + assertEquals("v4", removed.get("k4")); + removed.clear(); + 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()); + assertEquals(3, removed.size()); + assertEquals("v5", removed.get("k5")); + assertEquals("v2", removed.get("k2")); + assertEquals("v3", removed.get("k3")); + } + + @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/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/AttrMatcher.java b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/AttrMatcher.java new file mode 100644 index 00000000..122364e6 --- /dev/null +++ b/sdk2/src/test/java/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/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/AttributeValueBuilder.java b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/AttributeValueBuilder.java new file mode 100644 index 00000000..3ff7e4df --- /dev/null +++ b/sdk2/src/test/java/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/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/EncryptionTestHelper.java b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/EncryptionTestHelper.java new file mode 100644 index 00000000..2460ad88 --- /dev/null +++ b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/EncryptionTestHelper.java @@ -0,0 +1,65 @@ +/* + * 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 static java.util.stream.Collectors.toMap; +import static software.amazon.cryptools.dynamodbencryptionclientsdk2.EncryptionAction.ENCRYPT_AND_SIGN; +import static software.amazon.cryptools.dynamodbencryptionclientsdk2.EncryptionAction.SIGN_ONLY; + +import java.util.Collection; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.DynamoDbEncryptionClient; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.DynamoDbEncryptionConfiguration; +import software.amazon.cryptools.dynamodbencryptionclientsdk2.encryption.EncryptionContext; + +public final class EncryptionTestHelper { + private EncryptionTestHelper() { + // Static helper class + } + + public static Map encryptAllFieldsExcept(DynamoDbEncryptionClient encryptionClient, + Map record, + EncryptionContext encryptionContext, + Collection overriddenFields) { + return doSomethingExcept(encryptionClient::encryptRecord, record, encryptionContext, overriddenFields); + } + + public static Map decryptAllFieldsExcept(DynamoDbEncryptionClient encryptionClient, + Map record, + EncryptionContext encryptionContext, + Collection overriddenFields) { + return doSomethingExcept(encryptionClient::decryptRecord, record, encryptionContext, overriddenFields); + } + + private static Map doSomethingExcept( + BiFunction, DynamoDbEncryptionConfiguration, Map> operation, + Map record, + EncryptionContext encryptionContext, + Collection overriddenFields) { + + DynamoDbEncryptionConfiguration encryptionConfiguration = DynamoDbEncryptionConfiguration.builder() + .defaultEncryptionAction(ENCRYPT_AND_SIGN) + .encryptionContext(encryptionContext) + .addEncryptionActionOverrides(overriddenFields.stream().collect(toMap(Function.identity(), + ignored -> SIGN_ONLY))) + .build(); + + return operation.apply(record, encryptionConfiguration); + } +} diff --git a/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/FakeKMS.java b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/FakeKMS.java new file mode 100644 index 00000000..d05aff41 --- /dev/null +++ b/sdk2/src/test/java/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/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/LocalDynamoDb.java b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/LocalDynamoDb.java new file mode 100644 index 00000000..7eaee680 --- /dev/null +++ b/sdk2/src/test/java/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.CreateTableRequest; +import software.amazon.awssdk.services.dynamodb.model.CreateTableResponse; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.PutItemResponse; +import software.amazon.awssdk.services.dynamodb.model.QueryRequest; +import software.amazon.awssdk.services.dynamodb.model.QueryResponse; + +/** + * 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 DynamoDBProxyServer server; + private 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() { + 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 DynamoDBProxyServer createServer(String portString) throws Exception { + return ServerRunner.createServerFromCommandLineArgs( + new String[]{ + "-inMemory", + "-port", portString + }); + } + + private 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 CreateTableResponse createTable(CreateTableRequest createTableRequest) { + return wrappedClient.createTable(createTableRequest); + } + } +} diff --git a/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/TestDelegatedKey.java b/sdk2/src/test/java/software/amazon/cryptools/dynamodbencryptionclientsdk2/testing/TestDelegatedKey.java new file mode 100644 index 00000000..c19c5565 --- /dev/null +++ b/sdk2/src/test/java/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; + } + } +}