Skip to content

Commit 84bd212

Browse files
authored
Merge pull request #76 from johnwalker/metastoredirectkms
Add ExtraDataSupplier to Metastore, and support for overriding KMS requests in DirectKMSMaterialProvider
2 parents 2173141 + 8d84081 commit 84bd212

File tree

4 files changed

+291
-50
lines changed

4 files changed

+291
-50
lines changed

src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/providers/DirectKmsMaterialProvider.java

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ public DecryptionMaterials getDecryptionMaterials(EncryptionContext context) {
126126
DecryptRequest request = appendUserAgent(new DecryptRequest());
127127
request.setCiphertextBlob(ByteBuffer.wrap(Base64.decode(materialDescription.get(ENVELOPE_KEY))));
128128
request.setEncryptionContext(ec);
129-
final DecryptResult decryptResult = kms.decrypt(request);
129+
final DecryptResult decryptResult = decrypt(request, context);
130130
validateEncryptionKeyId(decryptResult.getKeyId(), context);
131131

132132
final Hkdf kdf;
@@ -167,7 +167,7 @@ public EncryptionMaterials getEncryptionMaterials(EncryptionContext context) {
167167
req.setNumberOfBytes(256 / 8);
168168
req.setEncryptionContext(ec);
169169

170-
final GenerateDataKeyResult dataKeyResult = kms.generateDataKey(req);
170+
final GenerateDataKeyResult dataKeyResult = generateDataKey(req, context);
171171

172172
final Map<String, String> materialDescription = new HashMap<>();
173173
materialDescription.putAll(description);
@@ -192,7 +192,8 @@ public EncryptionMaterials getEncryptionMaterials(EncryptionContext context) {
192192
}
193193

194194
/**
195-
* Get encryption key id.
195+
* Get encryption key id that is used to create the {@link EncryptionMaterials}.
196+
*
196197
* @return encryption key id.
197198
*/
198199
protected String getEncryptionKeyId() {
@@ -202,6 +203,7 @@ protected String getEncryptionKeyId() {
202203
/**
203204
* Select encryption key id to be used to generate data key. The default implementation of this method returns
204205
* {@link DirectKmsMaterialProvider#encryptionKeyId}.
206+
*
205207
* @param context encryption context.
206208
* @return the encryptionKeyId.
207209
* @throws DynamoDBMappingException when we fails to select a valid encryption key id.
@@ -211,7 +213,9 @@ protected String selectEncryptionKeyId(EncryptionContext context) throws DynamoD
211213
}
212214

213215
/**
214-
* Validate the encryption key id. The default implementation of this method does nothing.
216+
* Validate the encryption key id. The default implementation of this method does not validate
217+
* encryption key id.
218+
*
215219
* @param encryptionKeyId encryption key id from {@link DecryptResult}.
216220
* @param context encryption context.
217221
* @throws DynamoDBMappingException when encryptionKeyId is invalid.
@@ -221,6 +225,34 @@ protected void validateEncryptionKeyId(String encryptionKeyId, EncryptionContext
221225
// No action taken.
222226
}
223227

228+
/**
229+
* Decrypts ciphertext. The default implementation calls KMS to decrypt the ciphertext using the parameters
230+
* provided in the {@link DecryptRequest}. Subclass can override the default implementation to provide
231+
* additional request parameters using attributes within the {@link EncryptionContext}.
232+
*
233+
* @param request request parameters to decrypt the given ciphertext.
234+
* @param context additional useful data to decrypt the ciphertext.
235+
* @return the decrypted plaintext for the given ciphertext.
236+
*/
237+
protected DecryptResult decrypt(final DecryptRequest request, final EncryptionContext context) {
238+
return kms.decrypt(request);
239+
}
240+
241+
/**
242+
* Returns a data encryption key that you can use in your application to encrypt data locally. The default
243+
* implementation calls KMS to generate the data key using the parameters provided in the
244+
* {@link GenerateDataKeyRequest}. Subclass can override the default implementation to provide additional
245+
* request parameters using attributes within the {@link EncryptionContext}.
246+
*
247+
* @param request request parameters to generate the data key.
248+
* @param context additional useful data to generate the data key.
249+
* @return the newly generated data key which includes both the plaintext and ciphertext.
250+
*/
251+
protected GenerateDataKeyResult generateDataKey(final GenerateDataKeyRequest request,
252+
final EncryptionContext context) {
253+
return kms.generateDataKey(request);
254+
}
255+
224256
/**
225257
* Extracts relevant information from {@code context} and uses it to populate fields in
226258
* {@code kmsEc}. Currently, these fields are:

src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/providers/store/MetaStore.java

Lines changed: 166 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
import java.util.Arrays;
1818
import java.util.Collections;
1919
import java.util.HashMap;
20+
import java.util.HashSet;
2021
import java.util.List;
2122
import java.util.Map;
23+
import java.util.Set;
2224
import java.util.regex.Matcher;
2325
import java.util.regex.Pattern;
2426

@@ -70,45 +72,100 @@ public class MetaStore extends ProviderStore {
7072
private static final String DEFAULT_HASH_KEY = "N";
7173
private static final String DEFAULT_RANGE_KEY = "V";
7274

75+
/** Default no-op implementation of {@link ExtraDataSupplier}. */
76+
private static final EmptyExtraDataSupplier EMPTY_EXTRA_DATA_SUPPLIER
77+
= new EmptyExtraDataSupplier();
78+
79+
/** DDB fields that must be encrypted. */
80+
private static final Set<String> ENCRYPTED_FIELDS;
81+
static {
82+
final Set<String> tempEncryptedFields = new HashSet<>();
83+
tempEncryptedFields.add(MATERIAL_TYPE_VERSION);
84+
tempEncryptedFields.add(ENCRYPTION_KEY_FIELD);
85+
tempEncryptedFields.add(ENCRYPTION_ALGORITHM_FIELD);
86+
tempEncryptedFields.add(INTEGRITY_KEY_FIELD);
87+
tempEncryptedFields.add(INTEGRITY_ALGORITHM_FIELD);
88+
ENCRYPTED_FIELDS = tempEncryptedFields;
89+
}
90+
7391
private final Map<String, ExpectedAttributeValue> doesNotExist;
92+
private final Set<String> doNotEncrypt;
7493
private final String tableName;
7594
private final AmazonDynamoDB ddb;
7695
private final DynamoDBEncryptor encryptor;
7796
private final EncryptionContext ddbCtx;
97+
private final ExtraDataSupplier extraDataSupplier;
98+
99+
/**
100+
* Provides extra data that should be persisted along with the standard material data.
101+
*/
102+
public interface ExtraDataSupplier {
78103

104+
/**
105+
* Gets the extra data attributes for the specified material name.
106+
*
107+
* @param materialName material name.
108+
* @param version version number.
109+
* @return plain text of the extra data.
110+
*/
111+
Map<String, AttributeValue> getAttributes(final String materialName, final long version);
112+
113+
/**
114+
* Gets the extra data field names that should be signed only but not encrypted.
115+
*
116+
* @return signed only fields.
117+
*/
118+
Set<String> getSignedOnlyFieldNames();
119+
}
120+
121+
/**
122+
* Create a new MetaStore with specified table name.
123+
*
124+
* @param ddb Interface for accessing DynamoDB.
125+
* @param tableName DynamoDB table name for this {@link MetaStore}.
126+
* @param encryptor used to perform crypto operations on the record attributes.
127+
*/
79128
public MetaStore(final AmazonDynamoDB ddb, final String tableName,
80129
final DynamoDBEncryptor encryptor) {
130+
this(ddb, tableName, encryptor, EMPTY_EXTRA_DATA_SUPPLIER);
131+
}
132+
133+
/**
134+
* Create a new MetaStore with specified table name and extra data supplier.
135+
*
136+
* @param ddb Interface for accessing DynamoDB.
137+
* @param tableName DynamoDB table name for this {@link MetaStore}.
138+
* @param encryptor used to perform crypto operations on the record attributes
139+
* @param extraDataSupplier provides extra data that should be stored along with the material.
140+
*/
141+
public MetaStore(final AmazonDynamoDB ddb, final String tableName,
142+
final DynamoDBEncryptor encryptor, final ExtraDataSupplier extraDataSupplier) {
81143
this.ddb = checkNotNull(ddb, "ddb must not be null");
82144
this.tableName = checkNotNull(tableName, "tableName must not be null");
83145
this.encryptor = checkNotNull(encryptor, "encryptor must not be null");
146+
this.extraDataSupplier = checkNotNull(extraDataSupplier, "extraDataSupplier must not be null");
84147

85-
ddbCtx = new EncryptionContext.Builder().withTableName(this.tableName)
148+
this.ddbCtx = new EncryptionContext.Builder().withTableName(this.tableName)
86149
.withHashKeyName(DEFAULT_HASH_KEY).withRangeKeyName(DEFAULT_RANGE_KEY).build();
87150

88-
final Map<String, ExpectedAttributeValue> tmpExpected = new HashMap<String, ExpectedAttributeValue>();
151+
final Map<String, ExpectedAttributeValue> tmpExpected = new HashMap<>();
89152
tmpExpected.put(DEFAULT_HASH_KEY, new ExpectedAttributeValue().withExists(false));
90153
tmpExpected.put(DEFAULT_RANGE_KEY, new ExpectedAttributeValue().withExists(false));
91154
doesNotExist = Collections.unmodifiableMap(tmpExpected);
155+
156+
this.doNotEncrypt = getSignedOnlyFields(extraDataSupplier);
92157
}
93158

94159
@Override
95160
public EncryptionMaterialsProvider getProvider(final String materialName, final long version) {
96-
final Map<String, AttributeValue> ddbKey = new HashMap<String, AttributeValue>();
97-
ddbKey.put(DEFAULT_HASH_KEY, new AttributeValue().withS(materialName));
98-
ddbKey.put(DEFAULT_RANGE_KEY, new AttributeValue().withN(Long.toString(version)));
99-
final Map<String, AttributeValue> item = ddbGet(ddbKey);
100-
if (item == null || item.isEmpty()) {
101-
throw new IndexOutOfBoundsException("No material found: " + materialName + "#" + version);
102-
}
161+
Map<String, AttributeValue> item = getMaterialItem(materialName, version);
103162
return decryptProvider(item);
104163
}
105164

106165
@Override
107166
public EncryptionMaterialsProvider getOrCreate(final String materialName, final long nextId) {
108-
final SecretKeySpec encryptionKey = new SecretKeySpec(Utils.getRandom(32), DEFAULT_ENCRYPTION);
109-
final SecretKeySpec integrityKey = new SecretKeySpec(Utils.getRandom(32), DEFAULT_INTEGRITY);
110-
final Map<String, AttributeValue> ciphertext = conditionalPut(encryptKeys(materialName,
111-
nextId, encryptionKey, integrityKey));
167+
final Map<String, AttributeValue> plaintext = createMaterialItem(materialName, nextId);
168+
final Map<String, AttributeValue> ciphertext = conditionalPut(getEncryptedText(plaintext));
112169
return decryptProvider(ciphertext);
113170
}
114171

@@ -145,20 +202,14 @@ public long getVersionFromMaterialDescription(final Map<String, String> descript
145202

146203
/**
147204
* This API retrieves the intermediate keys from the source region and replicates it in the target region.
148-
* @param materialName
149-
* @param version
150-
* @param targetMetaStore
205+
*
206+
* @param materialName material name of the encryption material.
207+
* @param version version of the encryption material.
208+
* @param targetMetaStore target MetaStore where the encryption material to be stored.
151209
*/
152210
public void replicate(final String materialName, final long version, final MetaStore targetMetaStore) {
153211
try {
154-
final Map<String, AttributeValue> ddbKey = new HashMap<String, AttributeValue>();
155-
ddbKey.put(DEFAULT_HASH_KEY, new AttributeValue().withS(materialName));
156-
ddbKey.put(DEFAULT_RANGE_KEY, new AttributeValue().withN(Long.toString(version)));
157-
final Map<String, AttributeValue> item = ddbGet(ddbKey);
158-
if (item == null || item.isEmpty()) {
159-
throw new IndexOutOfBoundsException("No material found: " + materialName + "#" + version);
160-
}
161-
212+
Map<String, AttributeValue> item = getMaterialItem(materialName, version);
162213
final Map<String, AttributeValue> plainText = getPlainText(item);
163214
final Map<String, AttributeValue> encryptedText = targetMetaStore.getEncryptedText(plainText);
164215
final PutItemRequest put = new PutItemRequest().withTableName(targetMetaStore.tableName).withItem(encryptedText)
@@ -168,8 +219,25 @@ public void replicate(final String materialName, final long version, final MetaS
168219
//Item already present.
169220
}
170221
}
222+
223+
private Map<String, AttributeValue> getMaterialItem(final String materialName, final long version) {
224+
final Map<String, AttributeValue> ddbKey = new HashMap<>();
225+
ddbKey.put(DEFAULT_HASH_KEY, new AttributeValue().withS(materialName));
226+
ddbKey.put(DEFAULT_RANGE_KEY, new AttributeValue().withN(Long.toString(version)));
227+
final Map<String, AttributeValue> item = ddbGet(ddbKey);
228+
if (item == null || item.isEmpty()) {
229+
throw new IndexOutOfBoundsException("No material found: " + materialName + "#" + version);
230+
}
231+
return item;
232+
}
233+
171234
/**
172235
* Creates a DynamoDB Table with the correct properties to be used with a ProviderStore.
236+
*
237+
* @param ddb interface for accessing DynamoDB
238+
* @param tableName name of table that stores the meta data of the material.
239+
* @param provisionedThroughput required provisioned throughput of the this table.
240+
* @return result of create table request.
173241
*/
174242
public static CreateTableResult createTable(final AmazonDynamoDB ddb, final String tableName,
175243
final ProvisionedThroughput provisionedThroughput) {
@@ -181,14 +249,52 @@ public static CreateTableResult createTable(final AmazonDynamoDB ddb, final Stri
181249

182250
}
183251

252+
/**
253+
* Empty extra data supplier. This default class is intended to simplify the default
254+
* implementation of {@link MetaStore}.
255+
*/
256+
private static class EmptyExtraDataSupplier implements ExtraDataSupplier {
257+
@Override
258+
public Map<String, AttributeValue> getAttributes(String materialName, long version) {
259+
return Collections.emptyMap();
260+
}
261+
262+
@Override
263+
public Set<String> getSignedOnlyFieldNames() {
264+
return Collections.emptySet();
265+
}
266+
}
267+
268+
/**
269+
* Get a set of fields that must be signed but not encrypted.
270+
*
271+
* @param extraDataSupplier extra data supplier that is used to return sign only field names.
272+
* @return fields that must be signed.
273+
*/
274+
private static Set<String> getSignedOnlyFields(final ExtraDataSupplier extraDataSupplier) {
275+
final Set<String> signedOnlyFields = extraDataSupplier.getSignedOnlyFieldNames();
276+
for (final String signedOnlyField : signedOnlyFields) {
277+
if (ENCRYPTED_FIELDS.contains(signedOnlyField)) {
278+
throw new IllegalArgumentException(signedOnlyField + " must be encrypted");
279+
}
280+
}
281+
282+
// fields that should not be encrypted
283+
final Set<String> doNotEncryptFields = new HashSet<>();
284+
doNotEncryptFields.add(DEFAULT_HASH_KEY);
285+
doNotEncryptFields.add(DEFAULT_RANGE_KEY);
286+
doNotEncryptFields.addAll(signedOnlyFields);
287+
return Collections.unmodifiableSet(doNotEncryptFields);
288+
}
289+
184290
private Map<String, AttributeValue> conditionalPut(final Map<String, AttributeValue> item) {
185291
try {
186292
final PutItemRequest put = new PutItemRequest().withTableName(tableName).withItem(item)
187293
.withExpected(doesNotExist);
188294
ddb.putItem(put);
189295
return item;
190296
} catch (final ConditionalCheckFailedException ex) {
191-
final Map<String, AttributeValue> ddbKey = new HashMap<String, AttributeValue>();
297+
final Map<String, AttributeValue> ddbKey = new HashMap<>();
192298
ddbKey.put(DEFAULT_HASH_KEY, item.get(DEFAULT_HASH_KEY));
193299
ddbKey.put(DEFAULT_RANGE_KEY, item.get(DEFAULT_RANGE_KEY));
194300
return ddbGet(ddbKey);
@@ -201,19 +307,29 @@ private Map<String, AttributeValue> ddbGet(final Map<String, AttributeValue> ddb
201307
.withKey(ddbKey)).getItem();
202308
}
203309

204-
private Map<String, AttributeValue> encryptKeys(final String name, final long version,
205-
final SecretKeySpec encryptionKey, final SecretKeySpec integrityKey) {
310+
/**
311+
* Build an material item for a given material name and version with newly generated
312+
* encryption and integrity keys.
313+
*
314+
* @param materialName material name.
315+
* @param version version of the material.
316+
* @return newly generated plaintext material item.
317+
*/
318+
private Map<String, AttributeValue> createMaterialItem(final String materialName, final long version) {
319+
final SecretKeySpec encryptionKey = new SecretKeySpec(Utils.getRandom(32), DEFAULT_ENCRYPTION);
320+
final SecretKeySpec integrityKey = new SecretKeySpec(Utils.getRandom(32), DEFAULT_INTEGRITY);
321+
206322
final Map<String, AttributeValue> plaintext = new HashMap<String, AttributeValue>();
207-
plaintext.put(DEFAULT_HASH_KEY, new AttributeValue().withS(name));
323+
plaintext.put(DEFAULT_HASH_KEY, new AttributeValue().withS(materialName));
208324
plaintext.put(DEFAULT_RANGE_KEY, new AttributeValue().withN(Long.toString(version)));
209325
plaintext.put(MATERIAL_TYPE_VERSION, new AttributeValue().withS("0"));
210-
plaintext.put(ENCRYPTION_KEY_FIELD,
211-
new AttributeValue().withB(ByteBuffer.wrap(encryptionKey.getEncoded())));
326+
plaintext.put(ENCRYPTION_KEY_FIELD, new AttributeValue().withB(ByteBuffer.wrap(encryptionKey.getEncoded())));
212327
plaintext.put(ENCRYPTION_ALGORITHM_FIELD, new AttributeValue().withS(encryptionKey.getAlgorithm()));
213-
plaintext
214-
.put(INTEGRITY_KEY_FIELD, new AttributeValue().withB(ByteBuffer.wrap(integrityKey.getEncoded())));
328+
plaintext.put(INTEGRITY_KEY_FIELD, new AttributeValue().withB(ByteBuffer.wrap(integrityKey.getEncoded())));
215329
plaintext.put(INTEGRITY_ALGORITHM_FIELD, new AttributeValue().withS(integrityKey.getAlgorithm()));
216-
return getEncryptedText(plaintext);
330+
plaintext.putAll(extraDataSupplier.getAttributes(materialName, version));
331+
332+
return plaintext;
217333
}
218334

219335
private EncryptionMaterialsProvider decryptProvider(final Map<String, AttributeValue> item) {
@@ -237,19 +353,31 @@ private EncryptionMaterialsProvider decryptProvider(final Map<String, AttributeV
237353
buildDescription(plaintext));
238354
}
239355

240-
private Map<String, AttributeValue> getPlainText(Map<String, AttributeValue> item) {
356+
/**
357+
* Decrypts attributes in the ciphertext item using {@link DynamoDBEncryptor}.
358+
* except the attribute names specified in doNotEncrypt.
359+
* @param ciphertext the ciphertext to be decrypted.
360+
* @throws AmazonClientException when failed to decrypt material item.
361+
* @return decrypted item.
362+
*/
363+
private Map<String, AttributeValue> getPlainText(final Map<String, AttributeValue> ciphertext) {
241364
try {
242-
return encryptor.decryptAllFieldsExcept(item,
243-
ddbCtx, DEFAULT_HASH_KEY, DEFAULT_RANGE_KEY);
365+
return encryptor.decryptAllFieldsExcept(ciphertext, ddbCtx, doNotEncrypt);
244366
} catch (final GeneralSecurityException e) {
245367
throw new AmazonClientException(e);
246368
}
247369
}
248370

371+
/**
372+
* Encrypts attributes in the plaintext item using {@link DynamoDBEncryptor}.
373+
* except the attribute names specified in doNotEncrypt.
374+
*
375+
* @throws AmazonClientException when failed to encrypt material item.
376+
* @param plaintext plaintext to be encrypted.
377+
*/
249378
private Map<String, AttributeValue> getEncryptedText(Map<String, AttributeValue> plaintext) {
250379
try {
251-
return encryptor.encryptAllFieldsExcept(plaintext, ddbCtx, DEFAULT_HASH_KEY,
252-
DEFAULT_RANGE_KEY);
380+
return encryptor.encryptAllFieldsExcept(plaintext, ddbCtx, doNotEncrypt);
253381
} catch (final GeneralSecurityException e) {
254382
throw new AmazonClientException(e);
255383
}

0 commit comments

Comments
 (0)