Skip to content

Commit c216e75

Browse files
committed
Add KMS support to the DDB Encryption Client
1 parent 74b9ecc commit c216e75

File tree

4 files changed

+562
-1
lines changed

4 files changed

+562
-1
lines changed

src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/EncryptionContext.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,4 +214,12 @@ public Map<String, String> getMaterialDescription() {
214214
return materialDescription;
215215
}
216216
}
217+
218+
@Override
219+
public String toString() {
220+
return "EncryptionContext [tableName=" + tableName + ", attributeValues=" + attributeValues
221+
+ ", modeledClass=" + modeledClass + ", developerContext=" + developerContext
222+
+ ", hashKeyName=" + hashKeyName + ", rangeKeyName=" + rangeKeyName
223+
+ ", materialDescription=" + materialDescription + "]";
224+
}
217225
}

src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/materials/WrappedRawMaterials.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public class WrappedRawMaterials extends AbstractRawMaterials {
6161
* The key-name in the Description which which contains the wrapped content
6262
* key.
6363
*/
64-
protected static final String ENVELOPE_KEY = "amzn-ddb-env-key";
64+
public static final String ENVELOPE_KEY = "amzn-ddb-env-key";
6565
private static final String DEFAULT_ALGORITHM = "AES/256";
6666
private static final SecureRandom rand = new SecureRandom();
6767

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/*
2+
* Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
package com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers;
16+
17+
import static com.amazonaws.services.dynamodbv2.datamodeling.encryption.materials.WrappedRawMaterials.CONTENT_KEY_ALGORITHM;
18+
import static com.amazonaws.services.dynamodbv2.datamodeling.encryption.materials.WrappedRawMaterials.ENVELOPE_KEY;
19+
import static com.amazonaws.services.dynamodbv2.datamodeling.encryption.materials.WrappedRawMaterials.KEY_WRAPPING_ALGORITHM;
20+
21+
import java.nio.ByteBuffer;
22+
import java.security.NoSuchAlgorithmException;
23+
import java.util.Collections;
24+
import java.util.HashMap;
25+
import java.util.Map;
26+
27+
import javax.crypto.SecretKey;
28+
import javax.crypto.spec.SecretKeySpec;
29+
30+
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMappingException;
31+
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.EncryptionContext;
32+
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.materials.DecryptionMaterials;
33+
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.materials.EncryptionMaterials;
34+
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.materials.SymmetricRawMaterials;
35+
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.materials.WrappedRawMaterials;
36+
import com.amazonaws.services.dynamodbv2.datamodeling.internal.Hkdf;
37+
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
38+
import com.amazonaws.services.kms.AWSKMS;
39+
import com.amazonaws.services.kms.model.DecryptRequest;
40+
import com.amazonaws.services.kms.model.DecryptResult;
41+
import com.amazonaws.services.kms.model.GenerateDataKeyRequest;
42+
import com.amazonaws.services.kms.model.GenerateDataKeyResult;
43+
import com.amazonaws.util.Base64;
44+
45+
/**
46+
* Generates a unique data key for each record in DynamoDB and protects that key
47+
* using {@link AWSKMS}. Currently, the HashKey, RangeKey, and TableName will be
48+
* included in the KMS EncryptionContext for wrapping/unwrapping the key. This
49+
* means that records cannot be copied/moved between tables without re-encryption.
50+
*
51+
* @see <a href="http://docs.aws.amazon.com/kms/latest/developerguide/encrypt-context.html">KMS Encryption Context</a>
52+
*/
53+
public class DirectKmsMaterialProvider implements EncryptionMaterialsProvider {
54+
private static final String COVERED_ATTR_CTX_KEY = "aws-kms-ec-attr";
55+
private static final String SIGNING_KEY_ALGORITHM = "amzn-ddb-sig-alg";
56+
private static final String TABLE_NAME_EC_KEY = "*aws-kms-table*";
57+
58+
private static final String DEFAULT_ENC_ALG = "AES/256";
59+
private static final String DEFAULT_SIG_ALG = "HmacSHA256/256";
60+
private static final String KEY_COVERAGE = "*keys*";
61+
private static final String KDF_ALG = "HmacSHA256";
62+
private static final String KDF_SIG_INFO = "Signing";
63+
private static final String KDF_ENC_INFO = "Encryption";
64+
65+
private final AWSKMS kms;
66+
private final String encryptionKeyId;
67+
private final Map<String, String> description;
68+
private final String dataKeyAlg;
69+
private final int dataKeyLength;
70+
private final String dataKeyDesc;
71+
private final String sigKeyAlg;
72+
private final int sigKeyLength;
73+
private final String sigKeyDesc;
74+
75+
public DirectKmsMaterialProvider(AWSKMS kms) {
76+
this(kms, null);
77+
}
78+
79+
public DirectKmsMaterialProvider(AWSKMS kms, String encryptionKeyId, Map<String, String> materialDescription) {
80+
this.kms = kms;
81+
this.encryptionKeyId = encryptionKeyId;
82+
this.description = materialDescription != null ?
83+
Collections.unmodifiableMap(new HashMap<>(materialDescription)) :
84+
Collections.<String, String> emptyMap();
85+
86+
dataKeyDesc = description
87+
.containsKey(WrappedRawMaterials.CONTENT_KEY_ALGORITHM) ? description
88+
.get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM) : DEFAULT_ENC_ALG;
89+
90+
String[] parts = dataKeyDesc.split("/", 2);
91+
this.dataKeyAlg = parts[0];
92+
this.dataKeyLength = parts.length == 2 ? Integer.parseInt(parts[1]) : 256;
93+
94+
sigKeyDesc = description
95+
.containsKey(SIGNING_KEY_ALGORITHM) ? description
96+
.get(SIGNING_KEY_ALGORITHM) : DEFAULT_SIG_ALG;
97+
98+
parts = sigKeyDesc.split("/", 2);
99+
this.sigKeyAlg = parts[0];
100+
this.sigKeyLength = parts.length == 2 ? Integer.parseInt(parts[1]) : 256;
101+
}
102+
103+
public DirectKmsMaterialProvider(AWSKMS kms, String encryptionKeyId) {
104+
this(kms, encryptionKeyId, Collections.<String, String> emptyMap());
105+
}
106+
107+
@Override
108+
public DecryptionMaterials getDecryptionMaterials(EncryptionContext context) {
109+
final Map<String, String> materialDescription = context.getMaterialDescription();
110+
111+
final Map<String, String> ec = new HashMap<>();
112+
final String providedEncAlg = materialDescription.get(CONTENT_KEY_ALGORITHM);
113+
final String providedSigAlg = materialDescription.get(SIGNING_KEY_ALGORITHM);
114+
115+
ec.put("*" + CONTENT_KEY_ALGORITHM + "*", providedEncAlg);
116+
ec.put("*" + SIGNING_KEY_ALGORITHM + "*", providedSigAlg);
117+
118+
populateKmsEcFromEc(context, ec);
119+
120+
DecryptRequest request = new DecryptRequest();
121+
request.setCiphertextBlob(ByteBuffer.wrap(Base64.decode(materialDescription.get(ENVELOPE_KEY))));
122+
request.setEncryptionContext(ec);
123+
final DecryptResult decryptResult = kms.decrypt(request);
124+
125+
final Hkdf kdf;
126+
try {
127+
kdf = Hkdf.getInstance(KDF_ALG);
128+
} catch (NoSuchAlgorithmException e) {
129+
throw new DynamoDBMappingException(e);
130+
}
131+
kdf.init(toArray(decryptResult.getPlaintext()));
132+
133+
final String[] encAlgParts = providedEncAlg.split("/", 2);
134+
int encLength = encAlgParts.length == 2 ? Integer.parseInt(encAlgParts[1]) : 256;
135+
final String[] sigAlgParts = providedSigAlg.split("/", 2);
136+
int sigLength = sigAlgParts.length == 2 ? Integer.parseInt(sigAlgParts[1]) : 256;
137+
138+
final SecretKey encryptionKey = new SecretKeySpec(kdf.deriveKey(KDF_ENC_INFO, encLength / 8), encAlgParts[0]);
139+
final SecretKey macKey = new SecretKeySpec(kdf.deriveKey(KDF_SIG_INFO, sigLength / 8), sigAlgParts[0]);
140+
141+
return new SymmetricRawMaterials(encryptionKey, macKey, materialDescription);
142+
}
143+
144+
@Override
145+
public EncryptionMaterials getEncryptionMaterials(EncryptionContext context) {
146+
final Map<String, String> ec = new HashMap<>();
147+
ec.put("*" + CONTENT_KEY_ALGORITHM + "*", dataKeyDesc);
148+
ec.put("*" + SIGNING_KEY_ALGORITHM + "*", sigKeyDesc);
149+
populateKmsEcFromEc(context, ec);
150+
151+
final GenerateDataKeyRequest req = new GenerateDataKeyRequest();
152+
req.setKeyId(encryptionKeyId);
153+
req.setNumberOfBytes(256);
154+
req.setEncryptionContext(ec);
155+
156+
final GenerateDataKeyResult dataKeyResult = kms.generateDataKey(req);
157+
158+
final Map<String, String> materialDescription = new HashMap<>();
159+
materialDescription.putAll(description);
160+
materialDescription.put(COVERED_ATTR_CTX_KEY, KEY_COVERAGE);
161+
materialDescription.put(KEY_WRAPPING_ALGORITHM, "kms");
162+
materialDescription.put(CONTENT_KEY_ALGORITHM, dataKeyDesc);
163+
materialDescription.put(SIGNING_KEY_ALGORITHM, sigKeyDesc);
164+
materialDescription.put(ENVELOPE_KEY, Base64.encodeAsString(toArray(dataKeyResult.getCiphertextBlob())));
165+
166+
final Hkdf kdf;
167+
try {
168+
kdf = Hkdf.getInstance(KDF_ALG);
169+
} catch (NoSuchAlgorithmException e) {
170+
throw new DynamoDBMappingException(e);
171+
}
172+
173+
kdf.init(toArray(dataKeyResult.getPlaintext()));
174+
175+
final SecretKey encryptionKey = new SecretKeySpec(kdf.deriveKey(KDF_ENC_INFO, dataKeyLength / 8), dataKeyAlg);
176+
final SecretKey signatureKey = new SecretKeySpec(kdf.deriveKey(KDF_SIG_INFO, sigKeyLength / 8), sigKeyAlg);
177+
return new SymmetricRawMaterials(encryptionKey, signatureKey, materialDescription);
178+
}
179+
180+
/**
181+
* Extracts relevant information from {@code context} and uses it to populate fields in
182+
* {@code kmsEc}. Currently, these fields are:
183+
* <dl>
184+
* <dt>{@code HashKeyName}</dt>
185+
* <dd>{@code HashKeyValue}</dd>
186+
* <dt>{@code RangeKeyName}</dt>
187+
* <dd>{@code RangeKeyValue}</dd>
188+
* <dt>{@link #TABLE_NAME_EC_KEY}</dt>
189+
* <dd>{@code TableName}</dd>
190+
*/
191+
private static void populateKmsEcFromEc(EncryptionContext context, Map<String, String> kmsEc) {
192+
final String hashKeyName = context.getHashKeyName();
193+
if (hashKeyName != null) {
194+
final AttributeValue hashKey = context.getAttributeValues().get(hashKeyName);
195+
if (hashKey.getN() != null) {
196+
kmsEc.put(hashKeyName, hashKey.getN());
197+
} else if (hashKey.getS() != null) {
198+
kmsEc.put(hashKeyName, hashKey.getS());
199+
} else if (hashKey.getB() != null) {
200+
kmsEc.put(hashKeyName, Base64.encodeAsString(toArray(hashKey.getB())));
201+
} else {
202+
throw new UnsupportedOperationException("DirectKmsMaterialProvider only supports String, Number, and Binary HashKeys");
203+
}
204+
}
205+
final String rangeKeyName = context.getRangeKeyName();
206+
if (rangeKeyName != null) {
207+
final AttributeValue rangeKey = context.getAttributeValues().get(rangeKeyName);
208+
if (rangeKey.getN() != null) {
209+
kmsEc.put(rangeKeyName, rangeKey.getN());
210+
} else if (rangeKey.getS() != null) {
211+
kmsEc.put(rangeKeyName, rangeKey.getS());
212+
} else if (rangeKey.getB() != null) {
213+
kmsEc.put(rangeKeyName, Base64.encodeAsString(toArray(rangeKey.getB())));
214+
} else {
215+
throw new UnsupportedOperationException("DirectKmsMaterialProvider only supports String, Number, and Binary RangeKeys");
216+
}
217+
}
218+
219+
final String tableName = context.getTableName();
220+
if (tableName != null) {
221+
kmsEc.put(TABLE_NAME_EC_KEY, tableName);
222+
}
223+
}
224+
225+
private static byte[] toArray(final ByteBuffer buff) {
226+
final ByteBuffer dup = buff.asReadOnlyBuffer();
227+
byte[] result = new byte[dup.remaining()];
228+
dup.get(result);
229+
return result;
230+
}
231+
232+
@Override
233+
public void refresh() {
234+
// No action needed
235+
}
236+
}

0 commit comments

Comments
 (0)