/*
* Copyright 2015 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 org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.junit.Before;
import org.junit.Test;
import com.amazonaws.services.dynamodbv2.local.embedded.DynamoDBEmbedded;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.DynamoDBEncryptor;
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.providers.EncryptionMaterialsProvider;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.MostRecentProvider;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.SymmetricStaticProvider;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.store.MetaStore;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.store.ProviderStore;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput;
public class MostRecentProviderTests {
private static final String TABLE_NAME = "keystoreTable";
private static final String MATERIAL_NAME = "material";
private static final String MATERIAL_PARAM = "materialName";
private static final SecretKey AES_KEY = new SecretKeySpec(new byte[] { 0,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, "AES");
private static final SecretKey HMAC_KEY = new SecretKeySpec(new byte[] { 0,
1, 2, 3, 4, 5, 6, 7 }, "HmacSHA256");
private static final EncryptionMaterialsProvider BASE_PROVIDER = new SymmetricStaticProvider(AES_KEY, HMAC_KEY);
private static final DynamoDBEncryptor ENCRYPTOR = DynamoDBEncryptor.getInstance(BASE_PROVIDER);
private AmazonDynamoDB client;
private Map<String, Integer> methodCalls;
private ProviderStore store;
private EncryptionContext ctx;
@Before
public void setup() {
methodCalls = new HashMap<String, Integer>();
client = instrument(DynamoDBEmbedded.create(), AmazonDynamoDB.class, methodCalls);
MetaStore.createTable(client, TABLE_NAME, new ProvisionedThroughput(1L, 1L));
store = new MetaStore(client, TABLE_NAME, ENCRYPTOR);
ctx = new EncryptionContext.Builder().build();
methodCalls.clear();
}
@Test
public void constructor() {
final MostRecentProvider prov = new MostRecentProvider(store, MATERIAL_NAME, 100);
assertEquals(MATERIAL_NAME, prov.getMaterialName());
assertEquals(100, prov.getTtlInMills());
assertEquals(-1, prov.getCurrentVersion());
assertEquals(0, prov.getLastUpdated());
}
@Test
public void singleVersion() throws InterruptedException {
final MostRecentProvider prov = new MostRecentProvider(store, MATERIAL_NAME, 500);
assertNull(methodCalls.get("putItem"));
final EncryptionMaterials eMat1 = prov.getEncryptionMaterials(ctx);
// It's a new provider, so we see a single putItem
assertEquals(1, (int) methodCalls.getOrDefault("putItem", 0));
methodCalls.clear();
// Ensure the cache is working
final EncryptionMaterials eMat2 = prov.getEncryptionMaterials(ctx);
assertTrue(methodCalls.isEmpty());
assertEquals(0, store.getVersionFromMaterialDescription(eMat1.getMaterialDescription()));
assertEquals(0, store.getVersionFromMaterialDescription(eMat2.getMaterialDescription()));
// Let the TTL be exceeded
Thread.sleep(500);
final EncryptionMaterials eMat3 = prov.getEncryptionMaterials(ctx);
assertEquals(1, methodCalls.size());
assertEquals(1, (int) methodCalls.getOrDefault("query", 0)); // To find current version
assertEquals(0, store.getVersionFromMaterialDescription(eMat3.getMaterialDescription()));
assertEquals(eMat1.getSigningKey(), eMat2.getSigningKey());
assertEquals(eMat1.getSigningKey(), eMat3.getSigningKey());
// Check algorithms. Right now we only support AES and HmacSHA256
assertEquals("AES", eMat1.getEncryptionKey().getAlgorithm());
assertEquals("HmacSHA256", eMat1.getSigningKey().getAlgorithm());
// Ensure we can decrypt all of them without hitting ddb more than the minimum
final MostRecentProvider prov2 = new MostRecentProvider(store, MATERIAL_NAME, 500);
final DecryptionMaterials dMat1 = prov2.getDecryptionMaterials(ctx(eMat1));
methodCalls.clear();
assertEquals(eMat1.getEncryptionKey(), dMat1.getDecryptionKey());
assertEquals(eMat1.getSigningKey(), dMat1.getVerificationKey());
final DecryptionMaterials dMat2 = prov2.getDecryptionMaterials(ctx(eMat2));
assertEquals(eMat2.getEncryptionKey(), dMat2.getDecryptionKey());
assertEquals(eMat2.getSigningKey(), dMat2.getVerificationKey());
final DecryptionMaterials dMat3 = prov2.getDecryptionMaterials(ctx(eMat3));
assertEquals(eMat3.getEncryptionKey(), dMat3.getDecryptionKey());
assertEquals(eMat3.getSigningKey(), dMat3.getVerificationKey());
assertTrue("Expected no calls but was " + methodCalls.toString(), methodCalls.isEmpty());
}
@Test
public void singleVersionWithRefresh() throws InterruptedException {
final MostRecentProvider prov = new MostRecentProvider(store, MATERIAL_NAME, 500);
assertNull(methodCalls.get("putItem"));
final EncryptionMaterials eMat1 = prov.getEncryptionMaterials(ctx);
// It's a new provider, so we see a single putItem
assertEquals(1, (int) methodCalls.getOrDefault("putItem", 0));
methodCalls.clear();
// Ensure the cache is working
final EncryptionMaterials eMat2 = prov.getEncryptionMaterials(ctx);
assertTrue(methodCalls.isEmpty());
assertEquals(0, store.getVersionFromMaterialDescription(eMat1.getMaterialDescription()));
assertEquals(0, store.getVersionFromMaterialDescription(eMat2.getMaterialDescription()));
prov.refresh();
final EncryptionMaterials eMat3 = prov.getEncryptionMaterials(ctx);
assertEquals(1, (int) methodCalls.getOrDefault("query", 0)); // To find current version
assertEquals(1, (int) methodCalls.getOrDefault("getItem", 0));
assertEquals(0, store.getVersionFromMaterialDescription(eMat3.getMaterialDescription()));
prov.refresh();
assertEquals(eMat1.getSigningKey(), eMat2.getSigningKey());
assertEquals(eMat1.getSigningKey(), eMat3.getSigningKey());
// Ensure that after cache refresh we only get one more hit as opposed to multiple
prov.getEncryptionMaterials(ctx);
Thread.sleep(700);
// Force refresh
prov.getEncryptionMaterials(ctx);
methodCalls.clear();
// Check to ensure no more hits
assertEquals(eMat1.getSigningKey(), prov.getEncryptionMaterials(ctx).getSigningKey());
assertEquals(eMat1.getSigningKey(), prov.getEncryptionMaterials(ctx).getSigningKey());
assertEquals(eMat1.getSigningKey(), prov.getEncryptionMaterials(ctx).getSigningKey());
assertEquals(eMat1.getSigningKey(), prov.getEncryptionMaterials(ctx).getSigningKey());
assertEquals(eMat1.getSigningKey(), prov.getEncryptionMaterials(ctx).getSigningKey());
assertTrue(methodCalls.isEmpty());
// Ensure we can decrypt all of them without hitting ddb more than the minimum
final MostRecentProvider prov2 = new MostRecentProvider(store, MATERIAL_NAME, 500);
final DecryptionMaterials dMat1 = prov2.getDecryptionMaterials(ctx(eMat1));
methodCalls.clear();
assertEquals(eMat1.getEncryptionKey(), dMat1.getDecryptionKey());
assertEquals(eMat1.getSigningKey(), dMat1.getVerificationKey());
final DecryptionMaterials dMat2 = prov2.getDecryptionMaterials(ctx(eMat2));
assertEquals(eMat2.getEncryptionKey(), dMat2.getDecryptionKey());
assertEquals(eMat2.getSigningKey(), dMat2.getVerificationKey());
final DecryptionMaterials dMat3 = prov2.getDecryptionMaterials(ctx(eMat3));
assertEquals(eMat3.getEncryptionKey(), dMat3.getDecryptionKey());
assertEquals(eMat3.getSigningKey(), dMat3.getVerificationKey());
assertTrue(methodCalls.isEmpty());
}
@Test
public void twoVersions() throws InterruptedException {
final MostRecentProvider prov = new MostRecentProvider(store, MATERIAL_NAME, 500);
assertNull(methodCalls.get("putItem"));
final EncryptionMaterials eMat1 = prov.getEncryptionMaterials(ctx);
// It's a new provider, so we see a single putItem
assertEquals(1, (int) methodCalls.getOrDefault("putItem", 0));
methodCalls.clear();
// Create the new material
store.newProvider(MATERIAL_NAME);
methodCalls.clear();
// Ensure the cache is working
final EncryptionMaterials eMat2 = prov.getEncryptionMaterials(ctx);
assertTrue(methodCalls.isEmpty());
assertEquals(0, store.getVersionFromMaterialDescription(eMat1.getMaterialDescription()));
assertEquals(0, store.getVersionFromMaterialDescription(eMat2.getMaterialDescription()));
assertTrue(methodCalls.isEmpty());
// Let the TTL be exceeded
Thread.sleep(500);
final EncryptionMaterials eMat3 = prov.getEncryptionMaterials(ctx);
assertEquals(1, (int) methodCalls.getOrDefault("query", 0)); // To find current version
assertEquals(1, (int) methodCalls.getOrDefault("getItem", 0)); // To retrieve current version
assertNull(methodCalls.get("putItem")); // No attempt to create a new item
assertEquals(1, store.getVersionFromMaterialDescription(eMat3.getMaterialDescription()));
assertEquals(eMat1.getSigningKey(), eMat2.getSigningKey());
assertFalse(eMat1.getSigningKey().equals(eMat3.getSigningKey()));
// Ensure we can decrypt all of them without hitting ddb more than the minimum
final MostRecentProvider prov2 = new MostRecentProvider(store, MATERIAL_NAME, 500);
final DecryptionMaterials dMat1 = prov2.getDecryptionMaterials(ctx(eMat1));
methodCalls.clear();
assertEquals(eMat1.getEncryptionKey(), dMat1.getDecryptionKey());
assertEquals(eMat1.getSigningKey(), dMat1.getVerificationKey());
final DecryptionMaterials dMat2 = prov2.getDecryptionMaterials(ctx(eMat2));
assertEquals(eMat2.getEncryptionKey(), dMat2.getDecryptionKey());
assertEquals(eMat2.getSigningKey(), dMat2.getVerificationKey());
final DecryptionMaterials dMat3 = prov2.getDecryptionMaterials(ctx(eMat3));
assertEquals(eMat3.getEncryptionKey(), dMat3.getDecryptionKey());
assertEquals(eMat3.getSigningKey(), dMat3.getVerificationKey());
// Get item will be hit once for the one old key
assertEquals(1, methodCalls.size());
assertEquals(1, (int) methodCalls.getOrDefault("getItem", 0));
}
@Test
public void twoVersionsWithRefresh() throws InterruptedException {
final MostRecentProvider prov = new MostRecentProvider(store, MATERIAL_NAME, 100);
assertNull(methodCalls.get("putItem"));
final EncryptionMaterials eMat1 = prov.getEncryptionMaterials(ctx);
// It's a new provider, so we see a single putItem
assertEquals(1, (int) methodCalls.getOrDefault("putItem", 0));
methodCalls.clear();
// Create the new material
store.newProvider(MATERIAL_NAME);
methodCalls.clear();
// Ensure the cache is working
final EncryptionMaterials eMat2 = prov.getEncryptionMaterials(ctx);
assertTrue(methodCalls.isEmpty());
assertEquals(0, store.getVersionFromMaterialDescription(eMat1.getMaterialDescription()));
assertEquals(0, store.getVersionFromMaterialDescription(eMat2.getMaterialDescription()));
prov.refresh();
final EncryptionMaterials eMat3 = prov.getEncryptionMaterials(ctx);
assertEquals(1, (int) methodCalls.getOrDefault("query", 0)); // To find current version
assertEquals(1, (int) methodCalls.getOrDefault("getItem", 0));
assertEquals(1, store.getVersionFromMaterialDescription(eMat3.getMaterialDescription()));
assertEquals(eMat1.getSigningKey(), eMat2.getSigningKey());
assertFalse(eMat1.getSigningKey().equals(eMat3.getSigningKey()));
// Ensure we can decrypt all of them without hitting ddb more than the minimum
final MostRecentProvider prov2 = new MostRecentProvider(store, MATERIAL_NAME, 500);
final DecryptionMaterials dMat1 = prov2.getDecryptionMaterials(ctx(eMat1));
methodCalls.clear();
assertEquals(eMat1.getEncryptionKey(), dMat1.getDecryptionKey());
assertEquals(eMat1.getSigningKey(), dMat1.getVerificationKey());
final DecryptionMaterials dMat2 = prov2.getDecryptionMaterials(ctx(eMat2));
assertEquals(eMat2.getEncryptionKey(), dMat2.getDecryptionKey());
assertEquals(eMat2.getSigningKey(), dMat2.getVerificationKey());
final DecryptionMaterials dMat3 = prov2.getDecryptionMaterials(ctx(eMat3));
assertEquals(eMat3.getEncryptionKey(), dMat3.getDecryptionKey());
assertEquals(eMat3.getSigningKey(), dMat3.getVerificationKey());
// Get item will be hit once for the one old key
assertEquals(1, methodCalls.size());
assertEquals(1, (int) methodCalls.getOrDefault("getItem", 0));
}
@Test
public void singleVersionTwoMaterials() throws InterruptedException {
final Map<String, AttributeValue> attr1 = Collections.singletonMap(MATERIAL_PARAM, new AttributeValue("material1"));
final EncryptionContext ctx1 = ctx(attr1);
final Map<String, AttributeValue> attr2 = Collections.singletonMap(MATERIAL_PARAM, new AttributeValue("material2"));
final EncryptionContext ctx2 = ctx(attr2);
final MostRecentProvider prov = new ExtendedProvider(store, 500);
assertNull(methodCalls.get("putItem"));
final EncryptionMaterials eMat1_1 = prov.getEncryptionMaterials(ctx1);
// It's a new provider, so we see a single putItem
assertEquals(1, (int) methodCalls.getOrDefault("putItem", 0));
methodCalls.clear();
final EncryptionMaterials eMat1_2 = prov.getEncryptionMaterials(ctx2);
// It's a new provider, so we see a single putItem
assertEquals(1, (int) methodCalls.getOrDefault("putItem", 0));
methodCalls.clear();
// Ensure the two materials are, in fact, different
assertFalse(eMat1_1.getSigningKey().equals(eMat1_2.getSigningKey()));
// Ensure the cache is working
final EncryptionMaterials eMat2_1 = prov.getEncryptionMaterials(ctx1);
assertTrue(methodCalls.isEmpty());
assertEquals(0, store.getVersionFromMaterialDescription(eMat1_1.getMaterialDescription()));
assertEquals(0, store.getVersionFromMaterialDescription(eMat2_1.getMaterialDescription()));
final EncryptionMaterials eMat2_2 = prov.getEncryptionMaterials(ctx2);
assertTrue(methodCalls.isEmpty());
assertEquals(0, store.getVersionFromMaterialDescription(eMat1_2.getMaterialDescription()));
assertEquals(0, store.getVersionFromMaterialDescription(eMat2_2.getMaterialDescription()));
// Let the TTL be exceeded
Thread.sleep(500);
final EncryptionMaterials eMat3_1 = prov.getEncryptionMaterials(ctx1);
assertEquals(1, methodCalls.size());
assertEquals(1, (int) methodCalls.get("query")); // To find current version
assertEquals(0, store.getVersionFromMaterialDescription(eMat3_1.getMaterialDescription()));
methodCalls.clear();
final EncryptionMaterials eMat3_2 = prov.getEncryptionMaterials(ctx2);
assertEquals(1, methodCalls.size());
assertEquals(1, (int) methodCalls.get("query")); // To find current version
assertEquals(0, store.getVersionFromMaterialDescription(eMat3_2.getMaterialDescription()));
assertEquals(eMat1_1.getSigningKey(), eMat2_1.getSigningKey());
assertEquals(eMat1_2.getSigningKey(), eMat2_2.getSigningKey());
assertEquals(eMat1_1.getSigningKey(), eMat3_1.getSigningKey());
assertEquals(eMat1_2.getSigningKey(), eMat3_2.getSigningKey());
// Check algorithms. Right now we only support AES and HmacSHA256
assertEquals("AES", eMat1_1.getEncryptionKey().getAlgorithm());
assertEquals("AES", eMat1_2.getEncryptionKey().getAlgorithm());
assertEquals("HmacSHA256", eMat1_1.getSigningKey().getAlgorithm());
assertEquals("HmacSHA256", eMat1_2.getSigningKey().getAlgorithm());
// Ensure we can decrypt all of them without hitting ddb more than the minimum
final MostRecentProvider prov2 = new ExtendedProvider(store, 500);
final DecryptionMaterials dMat1_1 = prov2.getDecryptionMaterials(ctx(eMat1_1, attr1));
final DecryptionMaterials dMat1_2 = prov2.getDecryptionMaterials(ctx(eMat1_2, attr2));
methodCalls.clear();
assertEquals(eMat1_1.getEncryptionKey(), dMat1_1.getDecryptionKey());
assertEquals(eMat1_2.getEncryptionKey(), dMat1_2.getDecryptionKey());
assertEquals(eMat1_1.getSigningKey(), dMat1_1.getVerificationKey());
assertEquals(eMat1_2.getSigningKey(), dMat1_2.getVerificationKey());
final DecryptionMaterials dMat2_1 = prov2.getDecryptionMaterials(ctx(eMat2_1, attr1));
final DecryptionMaterials dMat2_2 = prov2.getDecryptionMaterials(ctx(eMat2_2, attr2));
assertEquals(eMat2_1.getEncryptionKey(), dMat2_1.getDecryptionKey());
assertEquals(eMat2_2.getEncryptionKey(), dMat2_2.getDecryptionKey());
assertEquals(eMat2_1.getSigningKey(), dMat2_1.getVerificationKey());
assertEquals(eMat2_2.getSigningKey(), dMat2_2.getVerificationKey());
final DecryptionMaterials dMat3_1 = prov2.getDecryptionMaterials(ctx(eMat3_1, attr1));
final DecryptionMaterials dMat3_2 = prov2.getDecryptionMaterials(ctx(eMat3_2, attr2));
assertEquals(eMat3_1.getEncryptionKey(), dMat3_1.getDecryptionKey());
assertEquals(eMat3_2.getEncryptionKey(), dMat3_2.getDecryptionKey());
assertEquals(eMat3_1.getSigningKey(), dMat3_1.getVerificationKey());
assertEquals(eMat3_2.getSigningKey(), dMat3_2.getVerificationKey());
assertTrue("Expected no calls but was " + methodCalls.toString(), methodCalls.isEmpty());
}
@Test
public void singleVersionWithTwoMaterialsWithRefresh() throws InterruptedException {
final Map<String, AttributeValue> attr1 = Collections.singletonMap(MATERIAL_PARAM, new AttributeValue("material1"));
final EncryptionContext ctx1 = ctx(attr1);
final Map<String, AttributeValue> attr2 = Collections.singletonMap(MATERIAL_PARAM, new AttributeValue("material2"));
final EncryptionContext ctx2 = ctx(attr2);
final MostRecentProvider prov = new ExtendedProvider(store, 500);
assertNull(methodCalls.get("putItem"));
final EncryptionMaterials eMat1_1 = prov.getEncryptionMaterials(ctx1);
// It's a new provider, so we see a single putItem
assertEquals(1, (int) methodCalls.getOrDefault("putItem", 0));
methodCalls.clear();
final EncryptionMaterials eMat1_2 = prov.getEncryptionMaterials(ctx2);
// It's a new provider, so we see a single putItem
assertEquals(1, (int) methodCalls.getOrDefault("putItem", 0));
methodCalls.clear();
// Ensure the two materials are, in fact, different
assertFalse(eMat1_1.getSigningKey().equals(eMat1_2.getSigningKey()));
// Ensure the cache is working
final EncryptionMaterials eMat2_1 = prov.getEncryptionMaterials(ctx1);
assertTrue(methodCalls.isEmpty());
assertEquals(0, store.getVersionFromMaterialDescription(eMat1_1.getMaterialDescription()));
assertEquals(0, store.getVersionFromMaterialDescription(eMat2_1.getMaterialDescription()));
final EncryptionMaterials eMat2_2 = prov.getEncryptionMaterials(ctx2);
assertTrue(methodCalls.isEmpty());
assertEquals(0, store.getVersionFromMaterialDescription(eMat1_2.getMaterialDescription()));
assertEquals(0, store.getVersionFromMaterialDescription(eMat2_2.getMaterialDescription()));
prov.refresh();
final EncryptionMaterials eMat3_1 = prov.getEncryptionMaterials(ctx1);
assertEquals(1, (int) methodCalls.getOrDefault("query", 0)); // To find current version
assertEquals(1, (int) methodCalls.getOrDefault("getItem", 0));
final EncryptionMaterials eMat3_2 = prov.getEncryptionMaterials(ctx2);
assertEquals(2, (int) methodCalls.getOrDefault("query", 0)); // To find current version
assertEquals(2, (int) methodCalls.getOrDefault("getItem", 0));
assertEquals(0, store.getVersionFromMaterialDescription(eMat3_1.getMaterialDescription()));
assertEquals(0, store.getVersionFromMaterialDescription(eMat3_2.getMaterialDescription()));
prov.refresh();
assertEquals(eMat1_1.getSigningKey(), eMat2_1.getSigningKey());
assertEquals(eMat1_1.getSigningKey(), eMat3_1.getSigningKey());
assertEquals(eMat1_2.getSigningKey(), eMat2_2.getSigningKey());
assertEquals(eMat1_2.getSigningKey(), eMat3_2.getSigningKey());
// Ensure that after cache refresh we only get one more hit as opposed to multiple
prov.getEncryptionMaterials(ctx1);
prov.getEncryptionMaterials(ctx2);
Thread.sleep(700);
// Force refresh
prov.getEncryptionMaterials(ctx1);
prov.getEncryptionMaterials(ctx2);
methodCalls.clear();
// Check to ensure no more hits
assertEquals(eMat1_1.getSigningKey(), prov.getEncryptionMaterials(ctx1).getSigningKey());
assertEquals(eMat1_1.getSigningKey(), prov.getEncryptionMaterials(ctx1).getSigningKey());
assertEquals(eMat1_1.getSigningKey(), prov.getEncryptionMaterials(ctx1).getSigningKey());
assertEquals(eMat1_1.getSigningKey(), prov.getEncryptionMaterials(ctx1).getSigningKey());
assertEquals(eMat1_1.getSigningKey(), prov.getEncryptionMaterials(ctx1).getSigningKey());
assertEquals(eMat1_2.getSigningKey(), prov.getEncryptionMaterials(ctx2).getSigningKey());
assertEquals(eMat1_2.getSigningKey(), prov.getEncryptionMaterials(ctx2).getSigningKey());
assertEquals(eMat1_2.getSigningKey(), prov.getEncryptionMaterials(ctx2).getSigningKey());
assertEquals(eMat1_2.getSigningKey(), prov.getEncryptionMaterials(ctx2).getSigningKey());
assertEquals(eMat1_2.getSigningKey(), prov.getEncryptionMaterials(ctx2).getSigningKey());
assertTrue(methodCalls.isEmpty());
// Ensure we can decrypt all of them without hitting ddb more than the minimum
final MostRecentProvider prov2 = new ExtendedProvider(store, 500);
final DecryptionMaterials dMat1_1 = prov2.getDecryptionMaterials(ctx(eMat1_1, attr1));
final DecryptionMaterials dMat1_2 = prov2.getDecryptionMaterials(ctx(eMat1_2, attr2));
methodCalls.clear();
assertEquals(eMat1_1.getEncryptionKey(), dMat1_1.getDecryptionKey());
assertEquals(eMat1_2.getEncryptionKey(), dMat1_2.getDecryptionKey());
assertEquals(eMat1_1.getSigningKey(), dMat1_1.getVerificationKey());
assertEquals(eMat1_2.getSigningKey(), dMat1_2.getVerificationKey());
final DecryptionMaterials dMat2_1 = prov2.getDecryptionMaterials(ctx(eMat2_1, attr1));
final DecryptionMaterials dMat2_2 = prov2.getDecryptionMaterials(ctx(eMat2_2, attr2));
assertEquals(eMat2_1.getEncryptionKey(), dMat2_1.getDecryptionKey());
assertEquals(eMat2_2.getEncryptionKey(), dMat2_2.getDecryptionKey());
assertEquals(eMat2_1.getSigningKey(), dMat2_1.getVerificationKey());
assertEquals(eMat2_2.getSigningKey(), dMat2_2.getVerificationKey());
final DecryptionMaterials dMat3_1 = prov2.getDecryptionMaterials(ctx(eMat3_1, attr1));
final DecryptionMaterials dMat3_2 = prov2.getDecryptionMaterials(ctx(eMat3_2, attr2));
assertEquals(eMat3_1.getEncryptionKey(), dMat3_1.getDecryptionKey());
assertEquals(eMat3_2.getEncryptionKey(), dMat3_2.getDecryptionKey());
assertEquals(eMat3_1.getSigningKey(), dMat3_1.getVerificationKey());
assertEquals(eMat3_2.getSigningKey(), dMat3_2.getVerificationKey());
assertTrue(methodCalls.isEmpty());
}
@Test
public void twoVersionsWithTwoMaterialsWithRefresh() throws InterruptedException {
final Map<String, AttributeValue> attr1 = Collections.singletonMap(MATERIAL_PARAM, new AttributeValue("material1"));
final EncryptionContext ctx1 = ctx(attr1);
final Map<String, AttributeValue> attr2 = Collections.singletonMap(MATERIAL_PARAM, new AttributeValue("material2"));
final EncryptionContext ctx2 = ctx(attr2);
final MostRecentProvider prov = new ExtendedProvider(store, 500);
assertNull(methodCalls.get("putItem"));
final EncryptionMaterials eMat1_1 = prov.getEncryptionMaterials(ctx1);
// It's a new provider, so we see a single putItem
assertEquals(1, (int) methodCalls.getOrDefault("putItem", 0));
methodCalls.clear();
final EncryptionMaterials eMat1_2 = prov.getEncryptionMaterials(ctx2);
// It's a new provider, so we see a single putItem
assertEquals(1, (int) methodCalls.getOrDefault("putItem", 0));
methodCalls.clear();
// Create the new material
store.newProvider("material1");
store.newProvider("material2");
methodCalls.clear();
// Ensure the cache is working
final EncryptionMaterials eMat2_1 = prov.getEncryptionMaterials(ctx1);
final EncryptionMaterials eMat2_2 = prov.getEncryptionMaterials(ctx2);
assertTrue(methodCalls.isEmpty());
assertEquals(0, store.getVersionFromMaterialDescription(eMat1_1.getMaterialDescription()));
assertEquals(0, store.getVersionFromMaterialDescription(eMat2_1.getMaterialDescription()));
assertEquals(0, store.getVersionFromMaterialDescription(eMat1_2.getMaterialDescription()));
assertEquals(0, store.getVersionFromMaterialDescription(eMat2_2.getMaterialDescription()));
prov.refresh();
final EncryptionMaterials eMat3_1 = prov.getEncryptionMaterials(ctx1);
final EncryptionMaterials eMat3_2 = prov.getEncryptionMaterials(ctx2);
assertEquals(2, (int) methodCalls.getOrDefault("query", 0)); // To find current version
assertEquals(2, (int) methodCalls.getOrDefault("getItem", 0));
assertEquals(1, store.getVersionFromMaterialDescription(eMat3_1.getMaterialDescription()));
assertEquals(1, store.getVersionFromMaterialDescription(eMat3_2.getMaterialDescription()));
assertEquals(eMat1_1.getSigningKey(), eMat2_1.getSigningKey());
assertFalse(eMat1_1.getSigningKey().equals(eMat3_1.getSigningKey()));
assertEquals(eMat1_2.getSigningKey(), eMat2_2.getSigningKey());
assertFalse(eMat1_2.getSigningKey().equals(eMat3_2.getSigningKey()));
// Ensure we can decrypt all of them without hitting ddb more than the minimum
final MostRecentProvider prov2 = new ExtendedProvider(store, 500);
final DecryptionMaterials dMat1_1 = prov2.getDecryptionMaterials(ctx(eMat1_1, attr1));
final DecryptionMaterials dMat1_2 = prov2.getDecryptionMaterials(ctx(eMat1_2, attr2));
methodCalls.clear();
assertEquals(eMat1_1.getEncryptionKey(), dMat1_1.getDecryptionKey());
assertEquals(eMat1_2.getEncryptionKey(), dMat1_2.getDecryptionKey());
assertEquals(eMat1_1.getSigningKey(), dMat1_1.getVerificationKey());
assertEquals(eMat1_2.getSigningKey(), dMat1_2.getVerificationKey());
final DecryptionMaterials dMat2_1 = prov2.getDecryptionMaterials(ctx(eMat2_1, attr1));
final DecryptionMaterials dMat2_2 = prov2.getDecryptionMaterials(ctx(eMat2_2, attr2));
assertEquals(eMat2_1.getEncryptionKey(), dMat2_1.getDecryptionKey());
assertEquals(eMat2_2.getEncryptionKey(), dMat2_2.getDecryptionKey());
assertEquals(eMat2_1.getSigningKey(), dMat2_1.getVerificationKey());
assertEquals(eMat2_2.getSigningKey(), dMat2_2.getVerificationKey());
final DecryptionMaterials dMat3_1 = prov2.getDecryptionMaterials(ctx(eMat3_1, attr1));
final DecryptionMaterials dMat3_2 = prov2.getDecryptionMaterials(ctx(eMat3_2, attr2));
assertEquals(eMat3_1.getEncryptionKey(), dMat3_1.getDecryptionKey());
assertEquals(eMat3_2.getEncryptionKey(), dMat3_2.getDecryptionKey());
assertEquals(eMat3_1.getSigningKey(), dMat3_1.getVerificationKey());
assertEquals(eMat3_2.getSigningKey(), dMat3_2.getVerificationKey());
// Get item will be hit once for the one old key
assertEquals(1, methodCalls.size());
assertEquals(2, (int) methodCalls.getOrDefault("getItem", 0));
}
private static EncryptionContext ctx(final Map<String, AttributeValue> attr) {
return new EncryptionContext.Builder()
.withAttributeValues(attr).build();
}
private static EncryptionContext ctx(final EncryptionMaterials mat, Map<String, AttributeValue> attr) {
return new EncryptionContext.Builder()
.withAttributeValues(attr)
.withMaterialDescription(mat.getMaterialDescription()).build();
}
private static EncryptionContext ctx(final EncryptionMaterials mat) {
return new EncryptionContext.Builder()
.withMaterialDescription(mat.getMaterialDescription()).build();
}
private static class ExtendedProvider extends MostRecentProvider {
public ExtendedProvider(ProviderStore keystore, long ttlInMillis) {
super(keystore, null, ttlInMillis);
}
@Override
public long getCurrentVersion() {
throw new UnsupportedOperationException();
}
@Override
protected String getMaterialName(final EncryptionContext context) {
return context.getAttributeValues().get(MATERIAL_PARAM).getS();
}
}
@SuppressWarnings("unchecked")
private static <T> T instrument(final T obj, final Class<T> clazz, final Map<String, Integer> map) {
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz },
new InvocationHandler() {
private final Object lock = new Object();
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
synchronized (lock) {
try {
final Integer oldCount = map.get(method.getName());
if (oldCount != null) {
map.put(method.getName(), oldCount + 1);
} else {
map.put(method.getName(), 1);
}
return method.invoke(obj, args);
} catch (final InvocationTargetException ex) {
throw ex.getCause();
}
}
}
}
);
}
}