17
17
import java .util .Arrays ;
18
18
import java .util .Collections ;
19
19
import java .util .HashMap ;
20
+ import java .util .HashSet ;
20
21
import java .util .List ;
21
22
import java .util .Map ;
23
+ import java .util .Set ;
22
24
import java .util .regex .Matcher ;
23
25
import java .util .regex .Pattern ;
24
26
@@ -70,45 +72,100 @@ public class MetaStore extends ProviderStore {
70
72
private static final String DEFAULT_HASH_KEY = "N" ;
71
73
private static final String DEFAULT_RANGE_KEY = "V" ;
72
74
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
+
73
91
private final Map <String , ExpectedAttributeValue > doesNotExist ;
92
+ private final Set <String > doNotEncrypt ;
74
93
private final String tableName ;
75
94
private final AmazonDynamoDB ddb ;
76
95
private final DynamoDBEncryptor encryptor ;
77
96
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 {
78
103
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
+ */
79
128
public MetaStore (final AmazonDynamoDB ddb , final String tableName ,
80
129
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 ) {
81
143
this .ddb = checkNotNull (ddb , "ddb must not be null" );
82
144
this .tableName = checkNotNull (tableName , "tableName must not be null" );
83
145
this .encryptor = checkNotNull (encryptor , "encryptor must not be null" );
146
+ this .extraDataSupplier = checkNotNull (extraDataSupplier , "extraDataSupplier must not be null" );
84
147
85
- ddbCtx = new EncryptionContext .Builder ().withTableName (this .tableName )
148
+ this . ddbCtx = new EncryptionContext .Builder ().withTableName (this .tableName )
86
149
.withHashKeyName (DEFAULT_HASH_KEY ).withRangeKeyName (DEFAULT_RANGE_KEY ).build ();
87
150
88
- final Map <String , ExpectedAttributeValue > tmpExpected = new HashMap <String , ExpectedAttributeValue >();
151
+ final Map <String , ExpectedAttributeValue > tmpExpected = new HashMap <>();
89
152
tmpExpected .put (DEFAULT_HASH_KEY , new ExpectedAttributeValue ().withExists (false ));
90
153
tmpExpected .put (DEFAULT_RANGE_KEY , new ExpectedAttributeValue ().withExists (false ));
91
154
doesNotExist = Collections .unmodifiableMap (tmpExpected );
155
+
156
+ this .doNotEncrypt = getSignedOnlyFields (extraDataSupplier );
92
157
}
93
158
94
159
@ Override
95
160
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 );
103
162
return decryptProvider (item );
104
163
}
105
164
106
165
@ Override
107
166
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 ));
112
169
return decryptProvider (ciphertext );
113
170
}
114
171
@@ -145,20 +202,14 @@ public long getVersionFromMaterialDescription(final Map<String, String> descript
145
202
146
203
/**
147
204
* 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.
151
209
*/
152
210
public void replicate (final String materialName , final long version , final MetaStore targetMetaStore ) {
153
211
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 );
162
213
final Map <String , AttributeValue > plainText = getPlainText (item );
163
214
final Map <String , AttributeValue > encryptedText = targetMetaStore .getEncryptedText (plainText );
164
215
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
168
219
//Item already present.
169
220
}
170
221
}
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
+
171
234
/**
172
235
* 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.
173
241
*/
174
242
public static CreateTableResult createTable (final AmazonDynamoDB ddb , final String tableName ,
175
243
final ProvisionedThroughput provisionedThroughput ) {
@@ -181,14 +249,52 @@ public static CreateTableResult createTable(final AmazonDynamoDB ddb, final Stri
181
249
182
250
}
183
251
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
+
184
290
private Map <String , AttributeValue > conditionalPut (final Map <String , AttributeValue > item ) {
185
291
try {
186
292
final PutItemRequest put = new PutItemRequest ().withTableName (tableName ).withItem (item )
187
293
.withExpected (doesNotExist );
188
294
ddb .putItem (put );
189
295
return item ;
190
296
} catch (final ConditionalCheckFailedException ex ) {
191
- final Map <String , AttributeValue > ddbKey = new HashMap <String , AttributeValue >();
297
+ final Map <String , AttributeValue > ddbKey = new HashMap <>();
192
298
ddbKey .put (DEFAULT_HASH_KEY , item .get (DEFAULT_HASH_KEY ));
193
299
ddbKey .put (DEFAULT_RANGE_KEY , item .get (DEFAULT_RANGE_KEY ));
194
300
return ddbGet (ddbKey );
@@ -201,19 +307,29 @@ private Map<String, AttributeValue> ddbGet(final Map<String, AttributeValue> ddb
201
307
.withKey (ddbKey )).getItem ();
202
308
}
203
309
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
+
206
322
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 ));
208
324
plaintext .put (DEFAULT_RANGE_KEY , new AttributeValue ().withN (Long .toString (version )));
209
325
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 ())));
212
327
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 ())));
215
329
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 ;
217
333
}
218
334
219
335
private EncryptionMaterialsProvider decryptProvider (final Map <String , AttributeValue > item ) {
@@ -237,19 +353,31 @@ private EncryptionMaterialsProvider decryptProvider(final Map<String, AttributeV
237
353
buildDescription (plaintext ));
238
354
}
239
355
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 ) {
241
364
try {
242
- return encryptor .decryptAllFieldsExcept (item ,
243
- ddbCtx , DEFAULT_HASH_KEY , DEFAULT_RANGE_KEY );
365
+ return encryptor .decryptAllFieldsExcept (ciphertext , ddbCtx , doNotEncrypt );
244
366
} catch (final GeneralSecurityException e ) {
245
367
throw new AmazonClientException (e );
246
368
}
247
369
}
248
370
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
+ */
249
378
private Map <String , AttributeValue > getEncryptedText (Map <String , AttributeValue > plaintext ) {
250
379
try {
251
- return encryptor .encryptAllFieldsExcept (plaintext , ddbCtx , DEFAULT_HASH_KEY ,
252
- DEFAULT_RANGE_KEY );
380
+ return encryptor .encryptAllFieldsExcept (plaintext , ddbCtx , doNotEncrypt );
253
381
} catch (final GeneralSecurityException e ) {
254
382
throw new AmazonClientException (e );
255
383
}
0 commit comments