Skip to content

Commit 7752c75

Browse files
Support for meta-providers.
This introduces the new concept of a ProviderStore which can be used to simplify managing multiple providers on a named or versioned basis. The MetaStore is specificaly backed by a DynamoDB Table and instance of DynamoDBEncryptor. This allows developers to build key-hierarchies and caching layes.
1 parent 4e82e07 commit 7752c75

File tree

7 files changed

+1249
-0
lines changed

7 files changed

+1249
-0
lines changed
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except
5+
* in compliance with the License. A copy of the License is located at
6+
*
7+
* http://aws.amazon.com/apache2.0
8+
*
9+
* or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*/
13+
package com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers;
14+
15+
import com.amazonaws.services.dynamodbv2.datamodeling.internal.LRUCache;
16+
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.EncryptionContext;
17+
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.materials.DecryptionMaterials;
18+
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.materials.EncryptionMaterials;
19+
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.store.ProviderStore;
20+
21+
/**
22+
* This meta-Provider encrypts data with the most recent version of keying materials from a
23+
* {@link ProviderStore} and decrypts using whichever version is appropriate. It also caches the
24+
* results from the {@link ProviderStore} to avoid excessive load on the backing systems. The cache
25+
* is not currently configurable.
26+
*/
27+
public class MostRecentProvider implements EncryptionMaterialsProvider {
28+
private final Object lock;
29+
private final ProviderStore keystore;
30+
private final String materialName;
31+
private final long ttlInMillis;
32+
private final LRUCache<EncryptionMaterialsProvider> cache;
33+
private EncryptionMaterialsProvider currentProvider;
34+
private long currentVersion;
35+
private long lastUpdated;
36+
37+
/**
38+
* Creates a new {@link MostRecentProvider}.
39+
*
40+
* @param ttlInMillis
41+
* The length of time in milliseconds to cache the most recent provider
42+
*/
43+
public MostRecentProvider(final ProviderStore keystore, final String materialName, final long ttlInMillis) {
44+
this.keystore = checkNotNull(keystore, "keystore must not be null");
45+
this.materialName = checkNotNull(materialName, "materialName must not be null");
46+
this.ttlInMillis = ttlInMillis;
47+
this.cache = new LRUCache<EncryptionMaterialsProvider>(1000);
48+
this.lock = new Object();
49+
currentProvider = null;
50+
currentVersion = -1;
51+
lastUpdated = 0;
52+
}
53+
54+
@Override
55+
public EncryptionMaterials getEncryptionMaterials(EncryptionContext context) {
56+
synchronized (lock) {
57+
if ((System.currentTimeMillis() - lastUpdated) > ttlInMillis) {
58+
long newVersion = keystore.getMaxVersion(materialName);
59+
if (newVersion < 0) {
60+
currentVersion = 0;
61+
currentProvider = keystore.getOrCreate(materialName, currentVersion);
62+
} else if (newVersion != currentVersion) {
63+
currentVersion = newVersion;
64+
currentProvider = keystore.getProvider(materialName, currentVersion);
65+
cache.add(Long.toString(currentVersion), currentProvider);
66+
}
67+
lastUpdated = System.currentTimeMillis();
68+
}
69+
return currentProvider.getEncryptionMaterials(context);
70+
}
71+
}
72+
73+
public DecryptionMaterials getDecryptionMaterials(EncryptionContext context) {
74+
final long version = keystore.getVersionFromMaterialDescription(
75+
context.getMaterialDescription());
76+
EncryptionMaterialsProvider provider = cache.get(Long.toString(version));
77+
if (provider == null) {
78+
provider = keystore.getProvider(materialName, version);
79+
cache.add(Long.toString(version), provider);
80+
}
81+
return provider.getDecryptionMaterials(context);
82+
}
83+
84+
/**
85+
* Completely empties the cache of both the current and old versions.
86+
*/
87+
@Override
88+
public void refresh() {
89+
synchronized (lock) {
90+
lastUpdated = 0;
91+
currentVersion = -1;
92+
currentProvider = null;
93+
}
94+
cache.clear();
95+
}
96+
97+
public String getMaterialName() {
98+
return materialName;
99+
}
100+
101+
public long getTtlInMills() {
102+
return ttlInMillis;
103+
}
104+
105+
/**
106+
* The current version of the materials being used for encryption. Returns -1 if we do not
107+
* currently have a current version.
108+
*/
109+
public long getCurrentVersion() {
110+
synchronized (lock) {
111+
return currentVersion;
112+
}
113+
}
114+
115+
/**
116+
* The last time the current version was updated. Returns 0 if we do not currently have a
117+
* current version.
118+
*/
119+
public long getLastUpdated() {
120+
synchronized (lock) {
121+
return lastUpdated;
122+
}
123+
}
124+
125+
private static <V> V checkNotNull(final V ref, final String errMsg) {
126+
if (ref == null) {
127+
throw new NullPointerException(errMsg);
128+
} else {
129+
return ref;
130+
}
131+
}
132+
}
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
/*
2+
* Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except
5+
* in compliance with the License. A copy of the License is located at
6+
*
7+
* http://aws.amazon.com/apache2.0
8+
*
9+
* or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*/
13+
package com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.store;
14+
15+
import java.nio.ByteBuffer;
16+
import java.security.GeneralSecurityException;
17+
import java.security.SecureRandom;
18+
import java.util.Arrays;
19+
import java.util.Collections;
20+
import java.util.HashMap;
21+
import java.util.List;
22+
import java.util.Map;
23+
import java.util.regex.Matcher;
24+
import java.util.regex.Pattern;
25+
26+
import javax.crypto.SecretKey;
27+
import javax.crypto.spec.SecretKeySpec;
28+
29+
import com.amazonaws.AmazonClientException;
30+
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
31+
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.DynamoDBEncryptor;
32+
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.EncryptionContext;
33+
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.EncryptionMaterialsProvider;
34+
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.WrappedMaterialsProvider;
35+
import com.amazonaws.services.dynamodbv2.model.AttributeDefinition;
36+
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
37+
import com.amazonaws.services.dynamodbv2.model.ComparisonOperator;
38+
import com.amazonaws.services.dynamodbv2.model.Condition;
39+
import com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException;
40+
import com.amazonaws.services.dynamodbv2.model.CreateTableResult;
41+
import com.amazonaws.services.dynamodbv2.model.ExpectedAttributeValue;
42+
import com.amazonaws.services.dynamodbv2.model.GetItemRequest;
43+
import com.amazonaws.services.dynamodbv2.model.KeySchemaElement;
44+
import com.amazonaws.services.dynamodbv2.model.KeyType;
45+
import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput;
46+
import com.amazonaws.services.dynamodbv2.model.PutItemRequest;
47+
import com.amazonaws.services.dynamodbv2.model.QueryRequest;
48+
import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType;
49+
50+
/**
51+
* Provides a simple collection of EncryptionMaterialProviders backed by an encrypted DynamoDB
52+
* table. This can be used to build key hierarchies or meta providers.
53+
*
54+
* Currently, this only supports AES-256 in AESWrap mode and HmacSHA256 for the providers persisted
55+
* in the table.
56+
*
57+
* @author rubin
58+
*/
59+
public class MetaStore extends ProviderStore {
60+
private static final String INTEGRITY_ALGORITHM_FIELD = "intAlg";
61+
private static final String INTEGRITY_KEY_FIELD = "int";
62+
private static final String ENCRYPTION_ALGORITHM_FIELD = "encAlg";
63+
private static final String ENCRYPTION_KEY_FIELD = "enc";
64+
private static final Pattern COMBINED_PATTERN = Pattern.compile("([^#]+)#(\\d*)");
65+
private static final String DEFAULT_INTEGRITY = "HmacSHA256";
66+
private static final String DEFAULT_ENCRYPTION = "AES";
67+
private static final String MATERIAL_TYPE_VERSION = "t";
68+
private static final String META_ID = "amzn-ddb-meta-id";
69+
70+
private static final String DEFAULT_HASH_KEY = "N";
71+
private static final String DEFAULT_RANGE_KEY = "V";
72+
73+
private final SecureRandom rng = new SecureRandom();
74+
private final Map<String, ExpectedAttributeValue> doesNotExist;
75+
private final String tableName;
76+
private final AmazonDynamoDB ddb;
77+
private final DynamoDBEncryptor encryptor;
78+
private final EncryptionContext ddbCtx;
79+
80+
public MetaStore(final AmazonDynamoDB ddb, final String tableName,
81+
final DynamoDBEncryptor encryptor) {
82+
this.ddb = checkNotNull(ddb, "ddb must not be null");
83+
this.tableName = checkNotNull(tableName, "tableName must not be null");
84+
this.encryptor = checkNotNull(encryptor, "encryptor must not be null");
85+
86+
ddbCtx = new EncryptionContext.Builder().withTableName(this.tableName)
87+
.withHashKeyName(DEFAULT_HASH_KEY).withRangeKeyName(DEFAULT_RANGE_KEY).build();
88+
89+
final Map<String, ExpectedAttributeValue> tmpExpected = new HashMap<String, ExpectedAttributeValue>();
90+
tmpExpected.put(DEFAULT_HASH_KEY, new ExpectedAttributeValue().withExists(false));
91+
tmpExpected.put(DEFAULT_RANGE_KEY, new ExpectedAttributeValue().withExists(false));
92+
doesNotExist = Collections.unmodifiableMap(tmpExpected);
93+
}
94+
95+
@Override
96+
public EncryptionMaterialsProvider getProvider(final String materialName, final long version) {
97+
final Map<String, AttributeValue> ddbKey = new HashMap<String, AttributeValue>();
98+
ddbKey.put(DEFAULT_HASH_KEY, new AttributeValue().withS(materialName));
99+
ddbKey.put(DEFAULT_RANGE_KEY, new AttributeValue().withN(Long.toString(version)));
100+
final Map<String, AttributeValue> item = ddbGet(ddbKey);
101+
if (item == null || item.isEmpty()) {
102+
throw new IndexOutOfBoundsException("No material found: " + materialName + "#" + version);
103+
}
104+
return decryptProvider(item);
105+
}
106+
107+
@Override
108+
public EncryptionMaterialsProvider getOrCreate(final String materialName, final long nextId) {
109+
final byte[] rawEncKey = new byte[32];
110+
rng.nextBytes(rawEncKey);
111+
final byte[] rawIntKey = new byte[32];
112+
rng.nextBytes(rawIntKey);
113+
final SecretKeySpec encryptionKey = new SecretKeySpec(rawEncKey, DEFAULT_ENCRYPTION);
114+
final SecretKeySpec integrityKey = new SecretKeySpec(rawIntKey, DEFAULT_INTEGRITY);
115+
final Map<String, AttributeValue> ciphertext = conditionalPut(encryptKeys(materialName,
116+
nextId, encryptionKey, integrityKey));
117+
return decryptProvider(ciphertext);
118+
}
119+
120+
@Override
121+
public long getMaxVersion(final String materialName) {
122+
final List<Map<String, AttributeValue>> items = ddb.query(
123+
new QueryRequest()
124+
.withTableName(tableName)
125+
.withConsistentRead(Boolean.TRUE)
126+
.withKeyConditions(
127+
Collections.singletonMap(
128+
DEFAULT_HASH_KEY,
129+
new Condition().withComparisonOperator(
130+
ComparisonOperator.EQ).withAttributeValueList(
131+
new AttributeValue().withS(materialName))))
132+
.withLimit(1).withScanIndexForward(false)
133+
.withAttributesToGet(DEFAULT_RANGE_KEY)).getItems();
134+
if (items.isEmpty()) {
135+
return -1L;
136+
} else {
137+
return Long.valueOf(items.get(0).get(DEFAULT_RANGE_KEY).getN());
138+
}
139+
}
140+
141+
@Override
142+
public long getVersionFromMaterialDescription(final Map<String, String> description) {
143+
final Matcher m = COMBINED_PATTERN.matcher(description.get(META_ID));
144+
if (m.matches()) {
145+
return Long.valueOf(m.group(2));
146+
} else {
147+
throw new IllegalArgumentException("No meta id found");
148+
}
149+
}
150+
/**
151+
* Creates a DynamoDB Table with the correct properties to be used with a ProviderStore.
152+
*/
153+
public static CreateTableResult createTable(final AmazonDynamoDB ddb, final String tableName,
154+
final ProvisionedThroughput provisionedThroughput) {
155+
return ddb.createTable(Arrays.asList(new AttributeDefinition(DEFAULT_HASH_KEY,
156+
ScalarAttributeType.S), new AttributeDefinition(DEFAULT_RANGE_KEY,
157+
ScalarAttributeType.N)), tableName, Arrays.asList(new KeySchemaElement(
158+
DEFAULT_HASH_KEY, KeyType.HASH), new KeySchemaElement(DEFAULT_RANGE_KEY,
159+
KeyType.RANGE)), provisionedThroughput);
160+
161+
}
162+
163+
private Map<String, AttributeValue> conditionalPut(final Map<String, AttributeValue> item) {
164+
try {
165+
final PutItemRequest put = new PutItemRequest().withTableName(tableName).withItem(item)
166+
.withExpected(doesNotExist);
167+
ddb.putItem(put);
168+
return item;
169+
} catch (final ConditionalCheckFailedException ex) {
170+
final Map<String, AttributeValue> ddbKey = new HashMap<String, AttributeValue>();
171+
ddbKey.put(DEFAULT_HASH_KEY, item.get(DEFAULT_HASH_KEY));
172+
ddbKey.put(DEFAULT_RANGE_KEY, item.get(DEFAULT_RANGE_KEY));
173+
return ddbGet(ddbKey);
174+
}
175+
}
176+
177+
private Map<String, AttributeValue> ddbGet(final Map<String, AttributeValue> ddbKey) {
178+
return ddb.getItem(
179+
new GetItemRequest().withTableName(tableName).withConsistentRead(true)
180+
.withKey(ddbKey)).getItem();
181+
}
182+
183+
private Map<String, AttributeValue> encryptKeys(final String name, final long version,
184+
final SecretKeySpec encryptionKey, final SecretKeySpec integrityKey) {
185+
final Map<String, AttributeValue> plaintext = new HashMap<String, AttributeValue>();
186+
plaintext.put(DEFAULT_HASH_KEY, new AttributeValue().withS(name));
187+
plaintext.put(DEFAULT_RANGE_KEY, new AttributeValue().withN(Long.toString(version)));
188+
plaintext.put(MATERIAL_TYPE_VERSION, new AttributeValue().withS("0"));
189+
plaintext.put(ENCRYPTION_KEY_FIELD,
190+
new AttributeValue().withB(ByteBuffer.wrap(encryptionKey.getEncoded())));
191+
plaintext.put(ENCRYPTION_ALGORITHM_FIELD, new AttributeValue().withS(encryptionKey.getAlgorithm()));
192+
plaintext
193+
.put(INTEGRITY_KEY_FIELD, new AttributeValue().withB(ByteBuffer.wrap(integrityKey.getEncoded())));
194+
plaintext.put(INTEGRITY_ALGORITHM_FIELD, new AttributeValue().withS(encryptionKey.getAlgorithm()));
195+
196+
try {
197+
return encryptor.encryptAllFieldsExcept(plaintext, ddbCtx, DEFAULT_HASH_KEY,
198+
DEFAULT_RANGE_KEY);
199+
} catch (final GeneralSecurityException e) {
200+
throw new AmazonClientException(e);
201+
}
202+
}
203+
204+
private EncryptionMaterialsProvider decryptProvider(final Map<String, AttributeValue> item) {
205+
try {
206+
final Map<String, AttributeValue> plaintext = encryptor.decryptAllFieldsExcept(item,
207+
ddbCtx, DEFAULT_HASH_KEY, DEFAULT_RANGE_KEY);
208+
209+
final String type = plaintext.get(MATERIAL_TYPE_VERSION).getS();
210+
final SecretKey encryptionKey;
211+
final SecretKey integrityKey;
212+
// This switch statement is to make future extensibility easier and more obvious
213+
switch (type) {
214+
case "0": // Only currently supported type
215+
encryptionKey = new SecretKeySpec(plaintext.get(ENCRYPTION_KEY_FIELD).getB().array(),
216+
plaintext.get(ENCRYPTION_ALGORITHM_FIELD).getS());
217+
integrityKey = new SecretKeySpec(plaintext.get(INTEGRITY_KEY_FIELD).getB().array(), plaintext
218+
.get(INTEGRITY_ALGORITHM_FIELD).getS());
219+
break;
220+
default:
221+
throw new IllegalStateException("Unsupported material type: " + type);
222+
}
223+
return new WrappedMaterialsProvider(encryptionKey, encryptionKey, integrityKey,
224+
buildDescription(plaintext));
225+
} catch (final GeneralSecurityException e) {
226+
throw new AmazonClientException(e);
227+
}
228+
}
229+
230+
private Map<String, String> buildDescription(final Map<String, AttributeValue> plaintext) {
231+
return Collections.singletonMap(META_ID, plaintext.get(DEFAULT_HASH_KEY).getS() + "#"
232+
+ plaintext.get(DEFAULT_RANGE_KEY).getN());
233+
}
234+
235+
private static <V> V checkNotNull(final V ref, final String errMsg) {
236+
if (ref == null) {
237+
throw new NullPointerException(errMsg);
238+
} else {
239+
return ref;
240+
}
241+
}
242+
}

0 commit comments

Comments
 (0)