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