Skip to content

Feature merge #47

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 29, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ You can download the [latest snapshot release][download] or pick it up from Mave
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-dynamodb-encryption-java</artifactId>
<version>1.11.2</version>
<version>1.12.0</version>
</dependency>
```

Expand Down Expand Up @@ -164,4 +164,4 @@ For signing, the user specified signing key can be either symmetric or asymmetri
[materialprovider]: src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/providers/EncryptionMaterialsProvider.java
[privatekey]: http://docs.oracle.com/javase/7/docs/api/java/security/PrivateKey.html
[secretkey]: http://docs.oracle.com/javase/7/docs/api/javax/crypto/SecretKey.html
[download]: https://github.com/aws/aws-dynamodb-encryption-java/releases/tag/1.11.2
[download]: https://github.com/aws/aws-dynamodb-encryption-java/releases/tag/1.12.0
12 changes: 6 additions & 6 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<groupId>com.amazonaws</groupId>
<artifactId>aws-dynamodb-encryption-java</artifactId>
<version>1.11.2</version>
<version>1.12.0</version>
<packaging>jar</packaging>

<name>aws-dynamodb-encryption-java</name>
Expand Down Expand Up @@ -43,7 +43,7 @@
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-bom</artifactId>
<version>1.11.315</version>
<version>1.11.380</version>
<type>pom</type>
<scope>import</scope>
</dependency>
Expand All @@ -64,14 +64,14 @@
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.1</version>
<version>4.12</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-ext-jdk15on</artifactId>
<version>1.50</version>
<version>1.60</version>
<scope>test</scope>
</dependency>

Expand Down Expand Up @@ -114,8 +114,8 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
Expand Down Expand Up @@ -58,7 +60,16 @@ public class DynamoDBEncryptor {
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<String, Integer> BLOCK_SIZE_CACHE = new ConcurrentHashMap<>();
private static final Function<String, Integer> BLOCK_SIZE_CALCULATOR = (transformation) -> {
try {
final Cipher c = Cipher.getInstance(transformation);
return c.getBlockSize();
} catch (final GeneralSecurityException ex) {
throw new IllegalArgumentException("Algorithm does not exist", ex);
}
};

private static final int CURRENT_VERSION = 0;

private String signatureFieldName = DEFAULT_SIGNATURE_FIELD;
Expand Down Expand Up @@ -339,7 +350,7 @@ private void actualDecryption(Map<String, AttributeValue> itemAttributes,
final String encryptionMode = encryptionKey != null ? encryptionKey.getAlgorithm() +
materialDescription.get(symmetricEncryptionModeHeader) : null;
Cipher cipher = null;
int ivSize = -1;
int blockSize = -1;

for (Map.Entry<String, AttributeValue> entry: itemAttributes.entrySet()) {
Set<EncryptionFlags> flags = attributeFlags.get(entry.getKey());
Expand All @@ -354,15 +365,13 @@ private void actualDecryption(Map<String, AttributeValue> itemAttributes,
plainText = ByteBuffer.wrap(((DelegatedKey)encryptionKey).decrypt(toByteArray(cipherText), null, encryptionMode));
} else {
if (cipher == null) {
cipher = Cipher.getInstance(
encryptionMode);
ivSize = cipher.getBlockSize();
blockSize = getBlockSize(encryptionMode);
cipher = Cipher.getInstance(encryptionMode);
}
byte[] iv = new byte[ivSize];
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()));
plainText = ByteBuffer.allocate(cipher.getOutputSize(cipherText.remaining()));
cipher.doFinal(cipherText, plainText);
plainText.rewind();
}
Expand All @@ -371,6 +380,10 @@ private void actualDecryption(Map<String, AttributeValue> itemAttributes,
}
}

protected 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
Expand All @@ -388,7 +401,7 @@ private void actualEncryption(Map<String, AttributeValue> itemAttributes,
encryptionMode = encryptionKey.getAlgorithm() + SYMMETRIC_ENCRYPTION_MODE;
}
Cipher cipher = null;
int ivSize = -1;
int blockSize = -1;

for (Map.Entry<String, AttributeValue> entry: itemAttributes.entrySet()) {
Set<EncryptionFlags> flags = attributeFlags.get(entry.getKey());
Expand All @@ -405,16 +418,22 @@ private void actualEncryption(Map<String, AttributeValue> itemAttributes,
dk.encrypt(toByteArray(plainText), null, encryptionMode));
} else {
if (cipher == null) {
blockSize = getBlockSize(encryptionMode);
cipher = Cipher.getInstance(encryptionMode);
ivSize = cipher.getBlockSize();
}
// Encryption format: <iv><ciphertext>
// Note a unique iv is generated per attribute
byte[] iv = Utils.getRandom(ivSize);
cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, new IvParameterSpec(iv), Utils.getRng());
cipherText = ByteBuffer.allocate(ivSize + cipher.getOutputSize(plainText.remaining()));
cipherText.put(iv);
cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, Utils.getRng());
cipherText = ByteBuffer.allocate(blockSize + cipher.getOutputSize(plainText.remaining()));
cipherText.position(blockSize);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Block size may not equal IV size, depending on the mode (e.g. AES-GCM would use a 12 byte IV with a 16 byte block size.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this data-format version of the client they must be the same. It's a limitation of the format which we cannot change until we do a more significant version rev.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please leave a comment to this effect, and a runtime check to confirm that the returned iv array is the correct size.

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
Expand Down Expand Up @@ -539,17 +558,22 @@ protected static Map<String, String> unmarshallDescription(AttributeValue attrib
attributeValue.getB().reset();
}
}

private static byte[] toByteArray(ByteBuffer buffer) {
if (buffer.hasArray()) {
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();
buffer.rewind();
return result;
} else {
byte[] result = new byte[buffer.remaining()];
buffer.get(result);
buffer.rewind();
return result;
if (buffer.remaining() == result.length) {
return result;
}
}

byte[] result = new byte[buffer.remaining()];
buffer.get(result);
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,32 @@ public long getVersionFromMaterialDescription(final Map<String, String> descript
throw new IllegalArgumentException("No meta id found");
}
}

/**
* This API retrieves the intermediate keys from the source region and replicates it in the target region.
* @param materialName
* @param version
* @param targetMetaStore
*/
public void replicate(final String materialName, final long version, final MetaStore targetMetaStore) {
try {
final Map<String, AttributeValue> ddbKey = new HashMap<String, AttributeValue>();
ddbKey.put(DEFAULT_HASH_KEY, new AttributeValue().withS(materialName));
ddbKey.put(DEFAULT_RANGE_KEY, new AttributeValue().withN(Long.toString(version)));
final Map<String, AttributeValue> item = ddbGet(ddbKey);
if (item == null || item.isEmpty()) {
throw new IndexOutOfBoundsException("No material found: " + materialName + "#" + version);
}

final Map<String, AttributeValue> plainText = getPlainText(item);
final Map<String, AttributeValue> encryptedText = targetMetaStore.getEncryptedText(plainText);
final PutItemRequest put = new PutItemRequest().withTableName(targetMetaStore.tableName).withItem(encryptedText)
.withExpected(doesNotExist);
targetMetaStore.ddb.putItem(put);
} catch (ConditionalCheckFailedException e) {
//Item already present.
}
}
/**
* Creates a DynamoDB Table with the correct properties to be used with a ProviderStore.
*/
Expand Down Expand Up @@ -187,36 +213,43 @@ private Map<String, AttributeValue> encryptKeys(final String name, final long ve
plaintext
.put(INTEGRITY_KEY_FIELD, new AttributeValue().withB(ByteBuffer.wrap(integrityKey.getEncoded())));
plaintext.put(INTEGRITY_ALGORITHM_FIELD, new AttributeValue().withS(integrityKey.getAlgorithm()));
return getEncryptedText(plaintext);
}

private EncryptionMaterialsProvider decryptProvider(final Map<String, AttributeValue> item) {
final Map<String, AttributeValue> plaintext = getPlainText(item);

final String type = plaintext.get(MATERIAL_TYPE_VERSION).getS();
final SecretKey encryptionKey;
final SecretKey integrityKey;
// This switch statement is to make future extensibility easier and more obvious
switch (type) {
case "0": // Only currently supported type
encryptionKey = new SecretKeySpec(plaintext.get(ENCRYPTION_KEY_FIELD).getB().array(),
plaintext.get(ENCRYPTION_ALGORITHM_FIELD).getS());
integrityKey = new SecretKeySpec(plaintext.get(INTEGRITY_KEY_FIELD).getB().array(), plaintext
.get(INTEGRITY_ALGORITHM_FIELD).getS());
break;
default:
throw new IllegalStateException("Unsupported material type: " + type);
}
return new WrappedMaterialsProvider(encryptionKey, encryptionKey, integrityKey,
buildDescription(plaintext));
}

private Map<String, AttributeValue> getPlainText(Map<String, AttributeValue> item) {
try {
return encryptor.encryptAllFieldsExcept(plaintext, ddbCtx, DEFAULT_HASH_KEY,
DEFAULT_RANGE_KEY);
return encryptor.decryptAllFieldsExcept(item,
ddbCtx, DEFAULT_HASH_KEY, DEFAULT_RANGE_KEY);
} catch (final GeneralSecurityException e) {
throw new AmazonClientException(e);
}
}

private EncryptionMaterialsProvider decryptProvider(final Map<String, AttributeValue> item) {
private Map<String, AttributeValue> getEncryptedText(Map<String, AttributeValue> plaintext) {
try {
final Map<String, AttributeValue> plaintext = encryptor.decryptAllFieldsExcept(item,
ddbCtx, DEFAULT_HASH_KEY, DEFAULT_RANGE_KEY);

final String type = plaintext.get(MATERIAL_TYPE_VERSION).getS();
final SecretKey encryptionKey;
final SecretKey integrityKey;
// This switch statement is to make future extensibility easier and more obvious
switch (type) {
case "0": // Only currently supported type
encryptionKey = new SecretKeySpec(plaintext.get(ENCRYPTION_KEY_FIELD).getB().array(),
plaintext.get(ENCRYPTION_ALGORITHM_FIELD).getS());
integrityKey = new SecretKeySpec(plaintext.get(INTEGRITY_KEY_FIELD).getB().array(), plaintext
.get(INTEGRITY_ALGORITHM_FIELD).getS());
break;
default:
throw new IllegalStateException("Unsupported material type: " + type);
}
return new WrappedMaterialsProvider(encryptionKey, encryptionKey, integrityKey,
buildDescription(plaintext));
return encryptor.encryptAllFieldsExcept(plaintext, ddbCtx, DEFAULT_HASH_KEY,
DEFAULT_RANGE_KEY);
} catch (final GeneralSecurityException e) {
throw new AmazonClientException(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@
*/
package com.amazonaws.services.dynamodbv2.datamodeling.internal;

import java.util.AbstractMap.SimpleImmutableEntry;
import com.amazonaws.annotation.ThreadSafe;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import com.amazonaws.annotation.ThreadSafe;
import java.util.Map.Entry;

/**
* A bounded cache that has a LRU eviction policy when the cache is full.
Expand Down Expand Up @@ -98,11 +99,16 @@ public void clear() {
// The more complicated logic is to ensure that the listener is
// actually called for all entries.
if (listener != null) {
Set<String> keys = new TreeSet<String>(map.keySet());
for (String key : keys) {
T val = map.get(key);
listener.onRemoval(new SimpleImmutableEntry<String, T>(key, val));
map.remove(key);
List<Entry<String, T>> removedEntries = new ArrayList<Entry<String, T>>();
synchronized (map) {
Iterator<Entry<String, T>> it = map.entrySet().iterator();
while(it.hasNext()) {
removedEntries.add(it.next());
it.remove();
}
}
for (Entry<String, T> entry : removedEntries) {
listener.onRemoval(entry);
}
} else {
map.clear();
Expand All @@ -126,7 +132,7 @@ private LRUHashMap(final int maxSize, final RemovalListener<T> listener) {
}

@Override
protected boolean removeEldestEntry(final Map.Entry<String, T> eldest) {
protected boolean removeEldestEntry(final Entry<String, T> eldest) {
if (size() > maxSize) {
if (listener != null) {
listener.onRemoval(eldest);
Expand All @@ -138,6 +144,6 @@ protected boolean removeEldestEntry(final Map.Entry<String, T> eldest) {
}

public static interface RemovalListener<T> {
public void onRemoval(Map.Entry<String, T> entry);
public void onRemoval(Entry<String, T> entry);
}
}
Loading