/*
* Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except
* in compliance with the License. A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers;
import static com.amazonaws.services.dynamodbv2.datamodeling.encryption.materials.WrappedRawMaterials.CONTENT_KEY_ALGORITHM;
import static com.amazonaws.services.dynamodbv2.datamodeling.encryption.materials.WrappedRawMaterials.ENVELOPE_KEY;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.crypto.SecretKey;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMappingException;
import com.amazonaws.services.kms.AWSKMS;
import com.amazonaws.services.kms.AbstractAWSKMS;
import com.amazonaws.services.kms.model.DecryptRequest;
import com.amazonaws.services.kms.model.DecryptResult;
import com.amazonaws.services.kms.model.GenerateDataKeyRequest;
import com.amazonaws.services.kms.model.GenerateDataKeyResult;
import org.junit.Before;
import org.junit.Test;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.EncryptionContext;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.materials.DecryptionMaterials;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.materials.EncryptionMaterials;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.materials.WrappedRawMaterials;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.dynamodbv2.testing.FakeKMS;
import com.amazonaws.util.Base64;
public class DirectKmsMaterialProviderTest {
private FakeKMS kms;
private String keyId;
private Map<String, String> description;
private EncryptionContext ctx;
@Before
public void setUp() {
description = new HashMap<String, String>();
description.put("TestKey", "test value");
description = Collections.unmodifiableMap(description);
ctx = new EncryptionContext.Builder().build();
kms = new FakeKMS();
keyId = kms.createKey().getKeyMetadata().getKeyId();
}
@Test
public void simple() throws GeneralSecurityException {
DirectKmsMaterialProvider prov = new DirectKmsMaterialProvider(kms, keyId);
EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx);
SecretKey encryptionKey = eMat.getEncryptionKey();
assertNotNull(encryptionKey);
Key signingKey = eMat.getSigningKey();
assertNotNull(signingKey);
DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat));
assertEquals(encryptionKey, dMat.getDecryptionKey());
assertEquals(signingKey, dMat.getVerificationKey());
String expectedEncAlg = encryptionKey.getAlgorithm() + "/"
+ (encryptionKey.getEncoded().length * 8);
String expectedSigAlg = signingKey.getAlgorithm() + "/"
+ (signingKey.getEncoded().length * 8);
Map<String, String> kmsCtx = kms.getSingleEc();
assertEquals(expectedEncAlg,
kmsCtx.get("*" + WrappedRawMaterials.CONTENT_KEY_ALGORITHM + "*"));
assertEquals(expectedSigAlg, kmsCtx.get("*amzn-ddb-sig-alg*"));
}
@Test
public void simpleWithKmsEc() throws GeneralSecurityException {
DirectKmsMaterialProvider prov = new DirectKmsMaterialProvider(kms, keyId);
Map<String, AttributeValue> attrVals = new HashMap<String, AttributeValue>();
attrVals.put("hk", new AttributeValue("HashKeyValue"));
attrVals.put("rk", new AttributeValue("RangeKeyValue"));
ctx = new EncryptionContext.Builder().withHashKeyName("hk").withRangeKeyName("rk")
.withTableName("KmsTableName").withAttributeValues(attrVals).build();
EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx);
SecretKey encryptionKey = eMat.getEncryptionKey();
assertNotNull(encryptionKey);
Key signingKey = eMat.getSigningKey();
assertNotNull(signingKey);
Map<String, String> kmsCtx = kms.getSingleEc();
assertEquals("HashKeyValue", kmsCtx.get("hk"));
assertEquals("RangeKeyValue", kmsCtx.get("rk"));
assertEquals("KmsTableName", kmsCtx.get("*aws-kms-table*"));
EncryptionContext dCtx = new EncryptionContext.Builder(ctx(eMat)).withHashKeyName("hk")
.withRangeKeyName("rk").withTableName("KmsTableName").withAttributeValues(attrVals)
.build();
DecryptionMaterials dMat = prov.getDecryptionMaterials(dCtx);
assertEquals(encryptionKey, dMat.getDecryptionKey());
assertEquals(signingKey, dMat.getVerificationKey());
}
@Test
public void simpleWithKmsEc2() throws GeneralSecurityException {
DirectKmsMaterialProvider prov = new DirectKmsMaterialProvider(kms, keyId);
Map<String, AttributeValue> attrVals = new HashMap<String, AttributeValue>();
attrVals.put("hk", new AttributeValue().withN("10"));
attrVals.put("rk", new AttributeValue().withN("20"));
ctx = new EncryptionContext.Builder().withHashKeyName("hk").withRangeKeyName("rk")
.withTableName("KmsTableName").withAttributeValues(attrVals).build();
EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx);
SecretKey encryptionKey = eMat.getEncryptionKey();
assertNotNull(encryptionKey);
Key signingKey = eMat.getSigningKey();
assertNotNull(signingKey);
Map<String, String> kmsCtx = kms.getSingleEc();
assertEquals("10", kmsCtx.get("hk"));
assertEquals("20", kmsCtx.get("rk"));
assertEquals("KmsTableName", kmsCtx.get("*aws-kms-table*"));
EncryptionContext dCtx = new EncryptionContext.Builder(ctx(eMat)).withHashKeyName("hk")
.withRangeKeyName("rk").withTableName("KmsTableName").withAttributeValues(attrVals)
.build();
DecryptionMaterials dMat = prov.getDecryptionMaterials(dCtx);
assertEquals(encryptionKey, dMat.getDecryptionKey());
assertEquals(signingKey, dMat.getVerificationKey());
}
@Test
public void simpleWithKmsEc3() throws GeneralSecurityException {
DirectKmsMaterialProvider prov = new DirectKmsMaterialProvider(kms, keyId);
Map<String, AttributeValue> attrVals = new HashMap<String, AttributeValue>();
attrVals.put("hk",
new AttributeValue().withB(ByteBuffer.wrap("Foo".getBytes(StandardCharsets.UTF_8))));
attrVals.put("rk",
new AttributeValue().withB(ByteBuffer.wrap("Bar".getBytes(StandardCharsets.UTF_8))));
ctx = new EncryptionContext.Builder().withHashKeyName("hk").withRangeKeyName("rk")
.withTableName("KmsTableName").withAttributeValues(attrVals).build();
EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx);
SecretKey encryptionKey = eMat.getEncryptionKey();
assertNotNull(encryptionKey);
Key signingKey = eMat.getSigningKey();
assertNotNull(signingKey);
assertNotNull(signingKey);
Map<String, String> kmsCtx = kms.getSingleEc();
assertEquals(Base64.encodeAsString("Foo".getBytes(StandardCharsets.UTF_8)),
kmsCtx.get("hk"));
assertEquals(Base64.encodeAsString("Bar".getBytes(StandardCharsets.UTF_8)),
kmsCtx.get("rk"));
assertEquals("KmsTableName", kmsCtx.get("*aws-kms-table*"));
EncryptionContext dCtx = new EncryptionContext.Builder(ctx(eMat)).withHashKeyName("hk")
.withRangeKeyName("rk").withTableName("KmsTableName").withAttributeValues(attrVals)
.build();
DecryptionMaterials dMat = prov.getDecryptionMaterials(dCtx);
assertEquals(encryptionKey, dMat.getDecryptionKey());
assertEquals(signingKey, dMat.getVerificationKey());
}
@Test
public void randomEnvelopeKeys() throws GeneralSecurityException {
DirectKmsMaterialProvider prov = new DirectKmsMaterialProvider(kms, keyId);
EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx);
SecretKey encryptionKey = eMat.getEncryptionKey();
assertNotNull(encryptionKey);
EncryptionMaterials eMat2 = prov.getEncryptionMaterials(ctx);
SecretKey encryptionKey2 = eMat2.getEncryptionKey();
assertFalse("Envelope keys must be different", encryptionKey.equals(encryptionKey2));
}
@Test
public void testRefresh() {
// This does nothing, make sure we don't throw and exception.
DirectKmsMaterialProvider prov = new DirectKmsMaterialProvider(kms, keyId);
prov.refresh();
}
@Test
public void explicitContentKeyAlgorithm() throws GeneralSecurityException {
Map<String, String> desc = new HashMap<String, String>();
desc.put(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, "AES");
DirectKmsMaterialProvider prov = new DirectKmsMaterialProvider(kms, keyId, desc);
EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx);
SecretKey encryptionKey = eMat.getEncryptionKey();
assertNotNull(encryptionKey);
DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat));
assertEquals("AES",
eMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM));
assertEquals(encryptionKey, dMat.getDecryptionKey());
}
@Test
public void explicitContentKeyLength128() throws GeneralSecurityException {
Map<String, String> desc = new HashMap<String, String>();
desc.put(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, "AES/128");
DirectKmsMaterialProvider prov = new DirectKmsMaterialProvider(kms, keyId, desc);
EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx);
SecretKey encryptionKey = eMat.getEncryptionKey();
assertNotNull(encryptionKey);
assertEquals(16, encryptionKey.getEncoded().length); // 128 Bits
DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat));
assertEquals("AES/128",
eMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM));
assertEquals("AES", eMat.getEncryptionKey().getAlgorithm());
assertEquals(encryptionKey, dMat.getDecryptionKey());
}
@Test
public void explicitContentKeyLength256() throws GeneralSecurityException {
Map<String, String> desc = new HashMap<String, String>();
desc.put(WrappedRawMaterials.CONTENT_KEY_ALGORITHM, "AES/256");
DirectKmsMaterialProvider prov = new DirectKmsMaterialProvider(kms, keyId, desc);
EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx);
SecretKey encryptionKey = eMat.getEncryptionKey();
assertNotNull(encryptionKey);
assertEquals(32, encryptionKey.getEncoded().length); // 256 Bits
DecryptionMaterials dMat = prov.getDecryptionMaterials(ctx(eMat));
assertEquals("AES/256",
eMat.getMaterialDescription().get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM));
assertEquals("AES", eMat.getEncryptionKey().getAlgorithm());
assertEquals(encryptionKey, dMat.getDecryptionKey());
}
@Test
public void extendedWithDerivedEncryptionKeyId() throws GeneralSecurityException {
ExtendedKmsMaterialProvider prov = new ExtendedKmsMaterialProvider(kms, keyId, "encryptionKeyId");
String customKeyId = kms.createKey().getKeyMetadata().getKeyId();
Map<String, AttributeValue> attrVals = new HashMap<>();
attrVals.put("hk", new AttributeValue().withN("10"));
attrVals.put("rk", new AttributeValue().withN("20"));
attrVals.put("encryptionKeyId", new AttributeValue().withS(customKeyId));
ctx = new EncryptionContext.Builder().withHashKeyName("hk").withRangeKeyName("rk")
.withTableName("KmsTableName").withAttributeValues(attrVals).build();
EncryptionMaterials eMat = prov.getEncryptionMaterials(ctx);
SecretKey encryptionKey = eMat.getEncryptionKey();
assertNotNull(encryptionKey);
Key signingKey = eMat.getSigningKey();
assertNotNull(signingKey);
Map<String, String> kmsCtx = kms.getSingleEc();
assertEquals("10", kmsCtx.get("hk"));
assertEquals("20", kmsCtx.get("rk"));
assertEquals("KmsTableName", kmsCtx.get("*aws-kms-table*"));
EncryptionContext dCtx = new EncryptionContext.Builder(ctx(eMat)).withHashKeyName("hk")
.withRangeKeyName("rk").withTableName("KmsTableName").withAttributeValues(attrVals)
.build();
DecryptionMaterials dMat = prov.getDecryptionMaterials(dCtx);
assertEquals(encryptionKey, dMat.getDecryptionKey());
assertEquals(signingKey, dMat.getVerificationKey());
}
@Test(expected = DynamoDBMappingException.class)
public void encryptionKeyIdMismatch() throws GeneralSecurityException {
DirectKmsMaterialProvider directProvider = new DirectKmsMaterialProvider(kms, keyId);
String customKeyId = kms.createKey().getKeyMetadata().getKeyId();
Map<String, AttributeValue> attrVals = new HashMap<>();
attrVals.put("hk", new AttributeValue().withN("10"));
attrVals.put("rk", new AttributeValue().withN("20"));
attrVals.put("encryptionKeyId", new AttributeValue().withS(customKeyId));
ctx = new EncryptionContext.Builder().withHashKeyName("hk").withRangeKeyName("rk")
.withTableName("KmsTableName").withAttributeValues(attrVals).build();
EncryptionMaterials eMat = directProvider.getEncryptionMaterials(ctx);
EncryptionContext dCtx = new EncryptionContext.Builder(ctx(eMat)).withHashKeyName("hk")
.withRangeKeyName("rk").withTableName("KmsTableName").withAttributeValues(attrVals)
.build();
ExtendedKmsMaterialProvider extendedProvider = new ExtendedKmsMaterialProvider(kms, keyId, "encryptionKeyId");
extendedProvider.getDecryptionMaterials(dCtx);
}
@Test(expected = DynamoDBMappingException.class)
public void missingEncryptionKeyId() throws GeneralSecurityException {
ExtendedKmsMaterialProvider prov = new ExtendedKmsMaterialProvider(kms, keyId, "encryptionKeyId");
Map<String, AttributeValue> attrVals = new HashMap<>();
attrVals.put("hk", new AttributeValue().withN("10"));
attrVals.put("rk", new AttributeValue().withN("20"));
ctx = new EncryptionContext.Builder().withHashKeyName("hk").withRangeKeyName("rk")
.withTableName("KmsTableName").withAttributeValues(attrVals).build();
prov.getEncryptionMaterials(ctx);
}
@Test
public void generateDataKeyIsCalledWith256NumberOfBits() {
final AtomicBoolean gdkCalled = new AtomicBoolean(false);
AWSKMS kmsSpy = new FakeKMS() {
@Override public GenerateDataKeyResult generateDataKey(GenerateDataKeyRequest r) {
gdkCalled.set(true);
assertEquals((Integer) 32, r.getNumberOfBytes());
assertNull(r.getKeySpec());
return super.generateDataKey(r);
}
};
assertFalse(gdkCalled.get());
new DirectKmsMaterialProvider(kmsSpy, keyId).getEncryptionMaterials(ctx);
assertTrue(gdkCalled.get());
}
private static class ExtendedKmsMaterialProvider extends DirectKmsMaterialProvider {
protected final String encryptionKeyIdAttributeName;
public ExtendedKmsMaterialProvider(AWSKMS kms, String encryptionKeyId, String encryptionKeyIdAttributeName) {
super(kms, encryptionKeyId);
this.encryptionKeyIdAttributeName = encryptionKeyIdAttributeName;
}
@Override
protected String selectEncryptionKeyId(EncryptionContext context) throws DynamoDBMappingException {
if (!context.getAttributeValues().containsKey(encryptionKeyIdAttributeName)) {
throw new DynamoDBMappingException("encryption key attribute is not provided");
}
return context.getAttributeValues().get(encryptionKeyIdAttributeName).getS();
}
@Override
protected void validateEncryptionKeyId(String encryptionKeyId, EncryptionContext context)
throws DynamoDBMappingException {
if (!context.getAttributeValues().containsKey(encryptionKeyIdAttributeName)) {
throw new DynamoDBMappingException("encryption key attribute is not provided");
}
String customEncryptionKeyId = context.getAttributeValues().get(encryptionKeyIdAttributeName).getS();
if (!customEncryptionKeyId.equals(encryptionKeyId)) {
throw new DynamoDBMappingException("encryption key ids do not match.");
}
}
}
private static EncryptionContext ctx(EncryptionMaterials mat) {
return new EncryptionContext.Builder()
.withMaterialDescription(mat.getMaterialDescription()).build();
}
}