From aad31c551343b36ab45d0a68c48697b12eae6f09 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Mon, 10 Jun 2024 11:59:35 -0700 Subject: [PATCH 01/19] fix: DecryptionMaterials defaults to empty encryption context --- .../amazonaws/encryptionsdk/model/DecryptionMaterials.java | 7 ++++++- submodules/MaterialProviders | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/amazonaws/encryptionsdk/model/DecryptionMaterials.java b/src/main/java/com/amazonaws/encryptionsdk/model/DecryptionMaterials.java index 1394b0c9..253414e2 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/model/DecryptionMaterials.java +++ b/src/main/java/com/amazonaws/encryptionsdk/model/DecryptionMaterials.java @@ -2,6 +2,7 @@ import com.amazonaws.encryptionsdk.DataKey; import java.security.PublicKey; +import java.util.Collections; import java.util.Map; public final class DecryptionMaterials { @@ -12,7 +13,11 @@ public final class DecryptionMaterials { private DecryptionMaterials(Builder b) { dataKey = b.getDataKey(); trailingSignatureKey = b.getTrailingSignatureKey(); - encryptionContext = b.getEncryptionContext(); + if (b.getEncryptionContext() != null) { + encryptionContext = b.getEncryptionContext(); + } else { + encryptionContext = Collections.emptyMap(); + } } public DataKey getDataKey() { diff --git a/submodules/MaterialProviders b/submodules/MaterialProviders index 9c0efc52..cbcadd90 160000 --- a/submodules/MaterialProviders +++ b/submodules/MaterialProviders @@ -1 +1 @@ -Subproject commit 9c0efc528a0fe5d58d8e0fe0985a29f40259bc00 +Subproject commit cbcadd90db34615ff7b259e9a1975d76e458651a From ae1902d5152f73fcb9baef5ce183f8a441401805 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Mon, 10 Jun 2024 12:01:17 -0700 Subject: [PATCH 02/19] reset submodule --- submodules/MaterialProviders | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/MaterialProviders b/submodules/MaterialProviders index cbcadd90..9c0efc52 160000 --- a/submodules/MaterialProviders +++ b/submodules/MaterialProviders @@ -1 +1 @@ -Subproject commit cbcadd90db34615ff7b259e9a1975d76e458651a +Subproject commit 9c0efc528a0fe5d58d8e0fe0985a29f40259bc00 From 35fe0604a925266aa54dc49897942ef984a348ad Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Mon, 10 Jun 2024 12:14:27 -0700 Subject: [PATCH 03/19] unit tests --- .../encryptionsdk/AllTestsSuite.java | 8 +--- .../model/DecryptionMaterialsTest.java | 41 +++++++++++++++++++ 2 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 src/test/java/com/amazonaws/encryptionsdk/model/DecryptionMaterialsTest.java diff --git a/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java b/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java index 9367233c..730d2da1 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java +++ b/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java @@ -46,12 +46,7 @@ import com.amazonaws.encryptionsdk.kms.KMSProviderBuilderMockTests; import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProviderTest; import com.amazonaws.encryptionsdk.kms.KmsMasterKeyTest; -import com.amazonaws.encryptionsdk.model.CipherBlockHeadersTest; -import com.amazonaws.encryptionsdk.model.CipherFrameHeadersTest; -import com.amazonaws.encryptionsdk.model.CiphertextHeadersTest; -import com.amazonaws.encryptionsdk.model.DecryptionMaterialsRequestTest; -import com.amazonaws.encryptionsdk.model.EncryptionMaterialsRequestTest; -import com.amazonaws.encryptionsdk.model.KeyBlobTest; +import com.amazonaws.encryptionsdk.model.*; import com.amazonaws.encryptionsdk.multi.MultipleMasterKeyTest; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -73,6 +68,7 @@ CipherBlockHeadersTest.class, CipherFrameHeadersTest.class, KeyBlobTest.class, + DecryptionMaterialsTest.class, DecryptionMaterialsRequestTest.class, MultipleMasterKeyTest.class, AwsCryptoTest.class, diff --git a/src/test/java/com/amazonaws/encryptionsdk/model/DecryptionMaterialsTest.java b/src/test/java/com/amazonaws/encryptionsdk/model/DecryptionMaterialsTest.java new file mode 100644 index 00000000..9e76c59f --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/model/DecryptionMaterialsTest.java @@ -0,0 +1,41 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.encryptionsdk.model; + +import org.junit.Test; + +import java.util.Collections; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +public class DecryptionMaterialsTest { + + @Test + public void GIVEN_builder_with_null_EC_WHEN_constructor_THEN_object_EC_is_empty_map() { + // Given: null encryption context + DecryptionMaterials.Builder builder = DecryptionMaterials.newBuilder(); + + // When: constructor + DecryptionMaterials decryptionMaterials = builder.build(); + + // Then: constructor assigns an empty map to DecryptionMaterials objects + assertEquals(Collections.emptyMap(), decryptionMaterials.getEncryptionContext()); + } + + @Test + public void GIVEN_builder_with_EC_WHEN_constructor_THEN_object_EC_is_builder_EC() { + // Given: any non-null encryption context map + Map mockEncryptionContext = mock(Map.class); + DecryptionMaterials.Builder builder = DecryptionMaterials.newBuilder(); + builder.setEncryptionContext(mockEncryptionContext); + + // When: constructor + DecryptionMaterials decryptionMaterials = builder.build(); + + // Then: constructor assigns that encryption context map to DecryptionMaterials objects + assertEquals(mockEncryptionContext, decryptionMaterials.getEncryptionContext()); + } +} From bd2b2e2bdd1e1472832c4b679070caf0bcb70bbc Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Mon, 10 Jun 2024 12:15:17 -0700 Subject: [PATCH 04/19] cleanup --- .../encryptionsdk/AllTestsSuite.java | 8 +- .../encryptionsdk/TestVectorGenerator.java | 570 -------------- .../encryptionsdk/TestVectorRunner.java | 743 ------------------ 3 files changed, 7 insertions(+), 1314 deletions(-) delete mode 100644 src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java delete mode 100644 src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java diff --git a/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java b/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java index 730d2da1..445f77d2 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java +++ b/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java @@ -46,7 +46,13 @@ import com.amazonaws.encryptionsdk.kms.KMSProviderBuilderMockTests; import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProviderTest; import com.amazonaws.encryptionsdk.kms.KmsMasterKeyTest; -import com.amazonaws.encryptionsdk.model.*; +import com.amazonaws.encryptionsdk.model.CipherBlockHeadersTest; +import com.amazonaws.encryptionsdk.model.CipherFrameHeadersTest; +import com.amazonaws.encryptionsdk.model.CiphertextHeadersTest; +import com.amazonaws.encryptionsdk.model.DecryptionMaterialsTest; +import com.amazonaws.encryptionsdk.model.DecryptionMaterialsRequestTest; +import com.amazonaws.encryptionsdk.model.EncryptionMaterialsRequestTest; +import com.amazonaws.encryptionsdk.model.KeyBlobTest; import com.amazonaws.encryptionsdk.multi.MultipleMasterKeyTest; import org.junit.runner.RunWith; import org.junit.runners.Suite; diff --git a/src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java b/src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java deleted file mode 100644 index eb2b1764..00000000 --- a/src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java +++ /dev/null @@ -1,570 +0,0 @@ -// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.amazonaws.encryptionsdk; - -import static java.lang.String.format; - -import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; -import com.amazonaws.encryptionsdk.jce.JceMasterKey; -import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; -import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; -import java.net.URL; -import java.nio.ByteBuffer; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.nio.file.attribute.FileAttribute; -import java.security.GeneralSecurityException; -import java.security.Key; -import java.security.KeyFactory; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.concurrent.Callable; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import org.apache.commons.io.FileUtils; -import org.bouncycastle.util.encoders.Base64; -import org.junit.AfterClass; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import software.amazon.awssdk.utils.ImmutableMap; -import software.amazon.cryptography.materialproviders.IKeyring; -import software.amazon.cryptography.materialproviders.MaterialProviders; -import software.amazon.cryptography.materialproviders.model.CreateMultiKeyringInput; -import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig; -import software.amazon.cryptography.materialproviderstestvectorkeys.KeyVectors; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionInput; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionOutput; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.KeyVectorsConfig; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.TestVectorKeyringInput; - -@RunWith(Parameterized.class) -public class TestVectorGenerator { - - private static final String encryptManifestList = - "https://raw.githubusercontent.com/awslabs/aws-crypto-tools-test-vector-framework/master/features/CANONICAL-GENERATED-MANIFESTS/0003-awses-message-encryption.v2.json"; - // We save the files in memory to avoid repeatedly retrieving them. This won't work if the - // plaintexts are too - // large or numerous - private static final Map cachedData = new HashMap<>(); - private static final ObjectMapper mapper = new ObjectMapper(); - private static EncryptionInterface encryption; - private static boolean isMasterKey; - private final String testName; - private final TestCase testCase; - - // Temp Test Vectors Directory - private static String tempTestVectorPath; - // Zip File Path - private static String zipFilePath; - - public TestVectorGenerator(final String testName, TestCase testCase) { - this.testName = testName; - this.testCase = testCase; - } - - // Zip Temp Folder and delete temp files - @AfterClass - public static void zip() throws IOException { - Path zipFile = Files.createFile(Paths.get(zipFilePath)); - - Path sourceDirPath = Paths.get(tempTestVectorPath); - try (ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(zipFile)); - Stream paths = Files.walk(sourceDirPath)) { - paths - .filter(path -> !Files.isDirectory(path)) - .forEach( - path -> { - ZipEntry zipEntry = new ZipEntry(sourceDirPath.relativize(path).toString()); - try { - zipOutputStream.putNextEntry(zipEntry); - Files.copy(path, zipOutputStream); - zipOutputStream.closeEntry(); - } catch (IOException e) { - throw new UncheckedIOException("Unable to Zip File", e); - } - }); - } - FileUtils.deleteQuietly(sourceDirPath.toFile()); - - // Teardown - cachedData.clear(); - } - - @Test - @SuppressWarnings("unchecked") - public void encrypt() throws Exception { - CryptoAlgorithm cryptoAlgorithm = getCryptoAlgorithm(testCase.algorithmId); - CommitmentPolicy commitmentPolicy = - cryptoAlgorithm.isCommitting() - ? CommitmentPolicy.RequireEncryptRequireDecrypt - : CommitmentPolicy.ForbidEncryptAllowDecrypt; - - AwsCrypto crypto = - AwsCrypto.builder() - .withCommitmentPolicy(commitmentPolicy) - .withEncryptionAlgorithm(cryptoAlgorithm) - .withEncryptionFrameSize(testCase.frameSize) - .build(); - - Callable ciphertext; - if (isMasterKey) { - ciphertext = - () -> - crypto - .encryptData( - testCase.masterKey, - cachedData.get(testCase.plaintext), - testCase.encryptionContext) - .getResult(); - } else { - ciphertext = - () -> - crypto - .encryptData( - testCase.keyring, - cachedData.get(testCase.plaintext), - testCase.encryptionContext) - .getResult(); - } - Files.write(Paths.get(tempTestVectorPath + "ciphertexts/" + testName), ciphertext.call()); - } - - private static CryptoAlgorithm getCryptoAlgorithm(String algorithmId) { - Integer algId = Integer.parseInt(algorithmId, 16); - for (CryptoAlgorithm cryptoAlgorithm : CryptoAlgorithm.values()) { - if (cryptoAlgorithm.getValue() == algId) { - return cryptoAlgorithm; - } - } - throw new IllegalArgumentException("Invalid AlgorithmId: " + algorithmId); - } - - @Parameterized.Parameters(name = "Compatibility Test: {0} - {1}") - @SuppressWarnings("unchecked") - public static Collection data() throws Exception { - final String interfaceOption = System.getProperty("masterkey"); - - if (interfaceOption != null && interfaceOption.equals("true")) { - isMasterKey = true; - encryption = EncryptionInterface.EncryptWithMasterKey; - } else { - encryption = EncryptionInterface.EncryptWithKeyring; - } - - final String encryptKeyManifest = System.getProperty("keysManifest"); - if (encryptKeyManifest == null) { - return Collections.emptyList(); - } - - zipFilePath = System.getProperty("zipFilePath"); - if (zipFilePath == null) { - return Collections.emptyList(); - } - - tempTestVectorPath = Files.createTempDirectory("java", new FileAttribute[0]).toString() + "/"; - createDirectories(tempTestVectorPath + "ciphertexts/"); - createDirectories(tempTestVectorPath + "plaintexts/"); - - File decryptManifest = new File(tempTestVectorPath + "manifest.json"); - File keyManifest = new File(tempTestVectorPath + "keys.json"); - - final Map manifest = mapper.readValue(new URL(encryptManifestList), Map.class); - mapper - .writerWithDefaultPrettyPrinter() - .writeValue(decryptManifest, createDecryptManifest(manifest)); - - try (InputStream in = new FileInputStream(encryptKeyManifest)) { - Files.copy(in, keyManifest.toPath(), StandardCopyOption.REPLACE_EXISTING); - } - - final Map keysManifest = - mapper.readValue(new File(encryptKeyManifest), Map.class); - - cachePlaintext((Map) manifest.get("plaintexts")); - - MaterialProvidersConfig config = MaterialProvidersConfig.builder().build(); - MaterialProviders materialProviders = - MaterialProviders.builder().MaterialProvidersConfig(config).build(); - KeyVectors keyVectors = - KeyVectors.builder() - .KeyVectorsConfig( - KeyVectorsConfig.builder().keyManifiestPath(keyManifest.toString()).build()) - .build(); - - final Map keys = parseKeyManifest(keysManifest); - final KmsMasterKeyProvider kmsProv = - KmsMasterKeyProvider.builder() - .withCredentials(new DefaultAWSCredentialsProviderChain()) - .buildDiscovery(); - - return ((Map>) manifest.get("tests")) - .entrySet().stream() - .map( - entry -> { - String testName = entry.getKey(); - TestCase testCase = - encryption.parseTest(entry, keys, kmsProv, materialProviders, keyVectors); - return new Object[] {testName, testCase}; - }) - .collect(Collectors.toList()); - } - - private static void createDirectories(String path) { - File directory = new File(path); - directory.mkdirs(); - } - - private enum EncryptionInterface { - EncryptWithMasterKey { - @Override - public TestCase parseTest( - Map.Entry> testEntry, - Map keys, - KmsMasterKeyProvider kmsProv, - MaterialProviders materialProviders, - KeyVectors keyVectors) { - return parseTestWithMasterkeys(testEntry, keys, kmsProv); - } - }, - EncryptWithKeyring { - @Override - public TestCase parseTest( - Map.Entry> testEntry, - Map keys, - KmsMasterKeyProvider kmsProv, - MaterialProviders materialProviders, - KeyVectors keyVectors) { - return parseTestWithKeyrings(testEntry, materialProviders, keyVectors); - } - }; - - public abstract TestCase parseTest( - Map.Entry> testEntry, - Map keys, - KmsMasterKeyProvider kmsProv, - MaterialProviders materialProviders, - KeyVectors keyVectors); - } - - private static void cachePlaintext(Map plaintexts) { - Random rd = new Random(); - plaintexts.forEach( - (key, value) -> { - byte[] plaintext = new byte[value]; - rd.nextBytes(plaintext); - try { - Files.write(new File(tempTestVectorPath + "plaintexts/" + key).toPath(), plaintext); - cachedData.put(key, plaintext); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); - } - - private static Map createDecryptManifest(Map encryptManifest) { - Map decryptManifest = new LinkedHashMap<>(); - - decryptManifest.put("manifest", ImmutableMap.of("type", "awses-decrypt", "version", 2)); - - decryptManifest.put( - "client", ImmutableMap.of("name", "aws/aws-encryption-sdk-java", "version", "2.2.0")); - - decryptManifest.put("keys", "file://keys.json"); - - Map> testScenarios = - ((LinkedHashMap>) encryptManifest.get("tests")) - .entrySet().stream() - .collect( - Collectors.toMap( - Map.Entry::getKey, - entry -> { - Map scenario = entry.getValue(); - return new LinkedHashMap() { - { - put("ciphertext", "file://ciphertexts/" + entry.getKey()); - put("master-keys", scenario.get("master-keys")); - put( - "result", - Collections.singletonMap( - "output", - Collections.singletonMap( - "plaintext", - "file://plaintexts/" + scenario.get("plaintext")))); - } - }; - })); - - decryptManifest.put("tests", testScenarios); - return decryptManifest; - } - - private static TestCase parseTestWithMasterkeys( - Map.Entry> testEntry, - Map keys, - KmsMasterKeyProvider kmsProv) { - - String testName = testEntry.getKey(); - Map data = testEntry.getValue(); - - String plaintext = (String) data.get("plaintext"); - String algorithmId = (String) data.get("algorithm"); - int frameSize = (int) data.get("frame-size"); - Map encryptionContext = (Map) data.get("encryption-context"); - - final List> mks = new ArrayList<>(); - - for (Map mkEntry : (List>) data.get("master-keys")) { - if (mkEntry.get("key").equals("rsa-4096-private")) { - mkEntry.replace("key", "rsa-4096-public"); - } - - final String type = mkEntry.get("type"); - final String keyName = mkEntry.get("key"); - final KeyEntry key = keys.get(keyName); - - if ("aws-kms".equals(type)) { - mks.add(kmsProv.getMasterKey(key.keyId)); - } else if ("raw".equals(type)) { - final String provId = mkEntry.get("provider-id"); - final String algorithm = mkEntry.get("encryption-algorithm"); - if ("aes".equals(algorithm)) { - mks.add( - JceMasterKey.getInstance( - (SecretKey) key.key, provId, key.keyId, "AES/GCM/NoPadding")); - } else if ("rsa".equals(algorithm)) { - String transformation = "RSA/ECB/"; - final String padding = mkEntry.get("padding-algorithm"); - if ("pkcs1".equals(padding)) { - transformation += "PKCS1Padding"; - } else if ("oaep-mgf1".equals(padding)) { - final String hashName = - mkEntry.get("padding-hash").replace("sha", "sha-").toUpperCase(); - transformation += "OAEPWith" + hashName + "AndMGF1Padding"; - } else { - throw new IllegalArgumentException("Unsupported padding:" + padding); - } - final PublicKey wrappingKey; - final PrivateKey unwrappingKey; - if (key.key instanceof PublicKey) { - wrappingKey = (PublicKey) key.key; - unwrappingKey = null; - } else { - wrappingKey = null; - unwrappingKey = (PrivateKey) key.key; - } - mks.add( - JceMasterKey.getInstance( - wrappingKey, unwrappingKey, provId, key.keyId, transformation)); - } else { - throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); - } - } else { - throw new IllegalArgumentException("Unsupported Key Type: " + type); - } - } - - MasterKeyProvider multiProvider = MultipleProviderFactory.buildMultiProvider(mks); - - return new TestCase( - testName, null, multiProvider, plaintext, algorithmId, frameSize, encryptionContext); - } - - private static TestCase parseTestWithKeyrings( - Map.Entry> testEntry, - MaterialProviders materialProviders, - KeyVectors keyVectors) { - String testName = testEntry.getKey(); - Map data = testEntry.getValue(); - - String plaintext = (String) data.get("plaintext"); - String algorithmId = (String) data.get("algorithm"); - int frameSize = (int) data.get("frame-size"); - Map encryptionContext = (Map) data.get("encryption-context"); - - List keyrings = new ArrayList<>(); - - ((List>) data.get("master-keys")) - .forEach( - mkEntry -> { - if (mkEntry.get("type").equals("raw") - && mkEntry.get("encryption-algorithm").equals("rsa")) { - if (mkEntry.get("key").equals("rsa-4096-private")) { - mkEntry.replace("key", "rsa-4096-public"); - } - mkEntry.putIfAbsent("padding-hash", "sha1"); - } - - try { - byte[] json = new ObjectMapper().writeValueAsBytes(mkEntry); - GetKeyDescriptionOutput output = - keyVectors.GetKeyDescription( - GetKeyDescriptionInput.builder().json(ByteBuffer.wrap(json)).build()); - - IKeyring testVectorKeyring = - keyVectors.CreateTestVectorKeyring( - TestVectorKeyringInput.builder() - .keyDescription(output.keyDescription()) - .build()); - - keyrings.add(testVectorKeyring); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - }); - - IKeyring primary = keyrings.remove(0); - IKeyring multiKeyring = - materialProviders.CreateMultiKeyring( - CreateMultiKeyringInput.builder().generator(primary).childKeyrings(keyrings).build()); - - return new TestCase( - testName, multiKeyring, null, plaintext, algorithmId, frameSize, encryptionContext); - } - - @SuppressWarnings("unchecked") - private static Map parseKeyManifest(final Map keysManifest) - throws GeneralSecurityException { - // check our type - final Map metaData = (Map) keysManifest.get("manifest"); - if (!"keys".equals(metaData.get("type"))) { - throw new IllegalArgumentException("Invalid manifest type: " + metaData.get("type")); - } - if (!Integer.valueOf(3).equals(metaData.get("version"))) { - throw new IllegalArgumentException("Invalid manifest version: " + metaData.get("version")); - } - - final Map result = new HashMap<>(); - - Map keys = (Map) keysManifest.get("keys"); - for (Map.Entry entry : keys.entrySet()) { - final String name = entry.getKey(); - final Map data = (Map) entry.getValue(); - - final String keyType = (String) data.get("type"); - final String encoding = (String) data.get("encoding"); - final String keyId = (String) data.get("key-id"); - final String material = (String) data.get("material"); // May be null - final String algorithm = (String) data.get("algorithm"); // May be null - - final KeyEntry keyEntry; - - final KeyFactory kf; - switch (keyType) { - case "symmetric": - if (!"base64".equals(encoding)) { - throw new IllegalArgumentException( - format("Key %s is symmetric but has encoding %s", keyId, encoding)); - } - keyEntry = - new KeyEntry( - name, - keyId, - keyType, - new SecretKeySpec(Base64.decode(material), algorithm.toUpperCase())); - break; - case "private": - kf = KeyFactory.getInstance(algorithm); - if (!"pem".equals(encoding)) { - throw new IllegalArgumentException( - format("Key %s is private but has encoding %s", keyId, encoding)); - } - byte[] pkcs8Key = parsePem(material); - keyEntry = - new KeyEntry( - name, keyId, keyType, kf.generatePrivate(new PKCS8EncodedKeySpec(pkcs8Key))); - break; - case "public": - kf = KeyFactory.getInstance(algorithm); - if (!"pem".equals(encoding)) { - throw new IllegalArgumentException( - format("Key %s is private but has encoding %s", keyId, encoding)); - } - byte[] x509Key = parsePem(material); - keyEntry = - new KeyEntry( - name, keyId, keyType, kf.generatePublic(new X509EncodedKeySpec(x509Key))); - break; - case "aws-kms": - keyEntry = new KeyEntry(name, keyId, keyType, null); - break; - default: - throw new IllegalArgumentException("Unsupported key type: " + keyType); - } - - result.put(name, keyEntry); - } - - return result; - } - - private static byte[] parsePem(String pem) { - final String stripped = pem.replaceAll("-+[A-Z ]+-+", ""); - return Base64.decode(stripped); - } - - private static class KeyEntry { - final String name; - final String keyId; - final String type; - final Key key; - - private KeyEntry(String name, String keyId, String type, Key key) { - this.name = name; - this.keyId = keyId; - this.type = type; - this.key = key; - } - } - - private static class TestCase { - private final String name; - private final IKeyring keyring; - private final MasterKeyProvider masterKey; - private final String plaintext; - private final String algorithmId; - private final int frameSize; - private final Map encryptionContext; - - public TestCase( - String name, - IKeyring keyring, - MasterKeyProvider multiProvider, - String plaintext, - String algorithmId, - int frameSize, - Map encryptionContext) { - this.name = name; - this.keyring = keyring; - this.masterKey = multiProvider; - this.plaintext = plaintext; - this.algorithmId = algorithmId; - this.frameSize = frameSize; - this.encryptionContext = encryptionContext; - } - } -} diff --git a/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java b/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java deleted file mode 100644 index 8c0be82f..00000000 --- a/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java +++ /dev/null @@ -1,743 +0,0 @@ -// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.amazonaws.encryptionsdk; - -import static java.lang.String.format; - -import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; -import com.amazonaws.encryptionsdk.internal.SignaturePolicy; -import com.amazonaws.encryptionsdk.jce.JceMasterKey; -import com.amazonaws.encryptionsdk.kms.AwsKmsMrkAwareMasterKeyProvider; -import com.amazonaws.encryptionsdk.kms.DiscoveryFilter; -import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; -import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory; -import com.amazonaws.util.IOUtils; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.JarURLConnection; -import java.net.URL; -import java.nio.ByteBuffer; -import java.security.GeneralSecurityException; -import java.security.Key; -import java.security.KeyFactory; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.Callable; -import java.util.function.Supplier; -import java.util.jar.JarFile; -import java.util.zip.ZipEntry; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import junit.framework.TestCase; -import org.bouncycastle.util.encoders.Base64; -import org.junit.AfterClass; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import software.amazon.awssdk.regions.Region; -import software.amazon.cryptography.materialproviders.ICryptographicMaterialsManager; -import software.amazon.cryptography.materialproviders.IKeyring; -import software.amazon.cryptography.materialproviders.MaterialProviders; -import software.amazon.cryptography.materialproviders.model.CreateDefaultCryptographicMaterialsManagerInput; -import software.amazon.cryptography.materialproviders.model.CreateMultiKeyringInput; -import software.amazon.cryptography.materialproviders.model.CreateRequiredEncryptionContextCMMInput; -import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig; -import software.amazon.cryptography.materialproviderstestvectorkeys.KeyVectors; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionInput; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionOutput; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.KeyVectorsConfig; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.TestVectorKeyringInput; - -@RunWith(Parameterized.class) -public class TestVectorRunner { - - // TODO: Standardize Manifest Version - private static final List MANIFEST_VERSIONS = Arrays.asList(2, 4); - - // We save the files in memory to avoid repeatedly retrieving them. This won't work if the - // plaintexts are too - // large or numerous - private static final Map cachedData = new HashMap<>(); - - private final String testName; - private final TestCase testCase; - private final DecryptionMethod decryptionMethod; - - public TestVectorRunner( - final String testName, TestCase testCase, DecryptionMethod decryptionMethod) { - this.testName = testName; - this.testCase = testCase; - this.decryptionMethod = decryptionMethod; - } - - @Test - public void decrypt() throws Exception { - AwsCrypto crypto = - AwsCrypto.builder() - .withCommitmentPolicy(CommitmentPolicy.ForbidEncryptAllowDecrypt) - .build(); - Callable decryptor; - if (testCase.isKeyring) { - decryptor = - () -> - decryptionMethod.decryptMessage( - crypto, - testCase.cmmSupplier.get(), - cachedData.get(testCase.ciphertextPath), - testCase.encryptionContext); - } else { - decryptor = - () -> - decryptionMethod.decryptMessage( - crypto, testCase.mkpSupplier.get(), cachedData.get(testCase.ciphertextPath)); - } - testCase.matcher.Match(decryptor); - } - - @Parameterized.Parameters(name = "Compatibility Test: {0} - {3}") - @SuppressWarnings("unchecked") - public static Collection data() throws Exception { - final String zipPath = System.getProperty("testVectorZip"); - final String interfaceOption = System.getProperty("masterkey"); - if (zipPath == null) { - return Collections.emptyList(); - } - - final JarURLConnection jarConnection = - (JarURLConnection) new URL("jar:" + zipPath + "!/").openConnection(); - - try (JarFile jar = jarConnection.getJarFile()) { - - final Map manifest = readJsonMapFromJar(jar, "manifest.json"); - final Map keysManifest = readJsonMapFromJar(jar, "keys.json"); - - ObjectMapper objectMapper = new ObjectMapper(); - - // Create a temporary file and write the JSON string to it - File tempFile = File.createTempFile("keys", ".json"); - objectMapper.writeValue(tempFile, keysManifest); - - final Map metaData = (Map) manifest.get("manifest"); - - // We only support "awses-decrypt" type manifests right now - if (!"awses-decrypt".equals(metaData.get("type"))) { - throw new IllegalArgumentException("Unsupported manifest type: " + metaData.get("type")); - } - Integer readVersion = (Integer) metaData.get("version"); - if (!MANIFEST_VERSIONS.contains(readVersion)) { - throw new IllegalArgumentException( - "Unsupported manifest version: " + metaData.get("version")); - } - - final Map keys = - parseKeyManifest(readJsonMapFromJar(jar, (String) manifest.get("keys"))); - - KeyVectors keyVectors = - KeyVectors.builder() - .KeyVectorsConfig( - KeyVectorsConfig.builder().keyManifiestPath(tempFile.getPath()).build()) - .build(); - - MaterialProvidersConfig config = MaterialProvidersConfig.builder().build(); - MaterialProviders materialProviders = - MaterialProviders.builder().MaterialProvidersConfig(config).build(); - - final KmsMasterKeyProvider kmsProvV1 = - KmsMasterKeyProvider.builder() - .withCredentials(new DefaultAWSCredentialsProviderChain()) - .buildDiscovery(); - - final com.amazonaws.encryptionsdk.kmssdkv2.KmsMasterKeyProvider kmsProvV2 = - com.amazonaws.encryptionsdk.kmssdkv2.KmsMasterKeyProvider.builder().buildDiscovery(); - - List testCases = new ArrayList<>(); - for (Map.Entry> testEntry : - ((Map>) manifest.get("tests")).entrySet()) { - if (interfaceOption != null && interfaceOption.equals("true")) { - String testName = testEntry.getKey(); - - TestCase testCaseV1 = - parseTest(testEntry.getKey(), testEntry.getValue(), keys, jar, kmsProvV1); - TestCase testCaseV2 = - parseTest(testEntry.getKey(), testEntry.getValue(), keys, jar, kmsProvV2); - - for (DecryptionMethod decryptionMethod : DecryptionMethod.values()) { - if (testCaseV1.signaturePolicy.equals(decryptionMethod.signaturePolicy())) { - testCases.add(new Object[] {testName, testCaseV1, decryptionMethod}); - testCases.add(new Object[] {testName + "-V2", testCaseV2, decryptionMethod}); - } - } - } else { - String testName = testEntry.getKey(); - TestCase testCaseKeyring = - parseTest( - testEntry.getKey(), - testEntry.getValue(), - keys, - jar, - materialProviders, - keyVectors); - - for (DecryptionMethod decryptionMethod : DecryptionMethod.values()) { - if (testCaseKeyring.signaturePolicy.equals(decryptionMethod.signaturePolicy())) { - testCases.add( - new Object[] {testName + "-Keyrings", testCaseKeyring, decryptionMethod}); - } - } - } - } - return testCases; - } - } - - @AfterClass - public static void teardown() { - cachedData.clear(); - } - - private static byte[] readBytesFromJar(JarFile jar, String fileName) throws IOException { - try (InputStream is = readFromJar(jar, fileName)) { - return IOUtils.toByteArray(is); - } - } - - private static Map readJsonMapFromJar(JarFile jar, String fileName) - throws IOException { - try (InputStream is = readFromJar(jar, fileName)) { - final ObjectMapper mapper = new ObjectMapper(); - return mapper.readValue(is, new TypeReference>() {}); - } - } - - private static InputStream readFromJar(JarFile jar, String name) throws IOException { - // Our manifest URIs incorrectly start with file:// rather than just file: so we need to strip - // this - ZipEntry entry = jar.getEntry(name.replaceFirst("^file://(?!/)", "")); - return jar.getInputStream(entry); - } - - private static void cacheData(JarFile jar, String url) throws IOException { - if (!cachedData.containsKey(url)) { - cachedData.put(url, readBytesFromJar(jar, url)); - } - } - - /** Parse Test to Keyring */ - @SuppressWarnings("unchecked") - private static TestCase parseTest( - String testName, - Map data, - Map keys, - JarFile jar, - MaterialProviders materialProviders, - KeyVectors keyVectors) - throws IOException { - final String ciphertextURL = (String) data.get("ciphertext"); - cacheData(jar, ciphertextURL); - - Supplier cmmSupplier = - () -> { - final List keyrings = new ArrayList<>(); - for (Map mkEntry : (List>) data.get("master-keys")) { - if (mkEntry.get("type").equals("raw") - && mkEntry.get("encryption-algorithm").equals("rsa")) { - mkEntry.putIfAbsent("padding-hash", "sha1"); - } - - try { - byte[] json = new ObjectMapper().writeValueAsBytes(mkEntry); - GetKeyDescriptionOutput output = - keyVectors.GetKeyDescription( - GetKeyDescriptionInput.builder().json(ByteBuffer.wrap(json)).build()); - - IKeyring testVectorKeyring = - keyVectors.CreateTestVectorKeyring( - TestVectorKeyringInput.builder() - .keyDescription(output.keyDescription()) - .build()); - - keyrings.add(testVectorKeyring); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - - IKeyring multiKeyring = - materialProviders.CreateMultiKeyring( - CreateMultiKeyringInput.builder() - .generator(keyrings.get(0)) - .childKeyrings(keyrings) - .build()); - - CreateDefaultCryptographicMaterialsManagerInput defaultInput = - CreateDefaultCryptographicMaterialsManagerInput.builder() - .keyring(multiKeyring) - .build(); - ICryptographicMaterialsManager cmm = - materialProviders.CreateDefaultCryptographicMaterialsManager(defaultInput); - if (data.containsKey("cmm") && data.get("cmm").equals("RequiredEncryptionContext")) { - List requiredKeys = new ArrayList(2); - requiredKeys.add("key1"); - requiredKeys.add("key2"); - CreateRequiredEncryptionContextCMMInput requiredCMMInput = - CreateRequiredEncryptionContextCMMInput.builder() - .underlyingCMM( - materialProviders.CreateDefaultCryptographicMaterialsManager(defaultInput)) - .requiredEncryptionContextKeys(requiredKeys) - .build(); - cmm = materialProviders.CreateRequiredEncryptionContextCMM(requiredCMMInput); - } - return cmm; - }; - @SuppressWarnings("unchecked") - final Map resultSpec = (Map) data.get("result"); - final ResultMatcher matcher = parseResultMatcher(jar, resultSpec); - - Map ec = Collections.emptyMap(); - - if (data.get("encryption-context") != null) { - ec = (Map) data.get("encryption-context"); - } - - String decryptionMethodSpec = (String) data.get("decryption-method"); - SignaturePolicy signaturePolicy = SignaturePolicy.AllowEncryptAllowDecrypt; - if (decryptionMethodSpec != null) { - if ("streaming-unsigned-only".equals(decryptionMethodSpec)) { - signaturePolicy = SignaturePolicy.AllowEncryptForbidDecrypt; - } else { - throw new IllegalArgumentException( - "Unsupported Decryption Method: " + decryptionMethodSpec); - } - } - - return new TestCase( - testName, ciphertextURL, true, null, cmmSupplier, ec, matcher, signaturePolicy); - } - - /** Parse Test to MasterKey for AWS SDK v1 */ - @SuppressWarnings("unchecked") - private static TestCase parseTest( - String testName, - Map data, - Map keys, - JarFile jar, - KmsMasterKeyProvider kmsProv) - throws IOException { - final String ciphertextURL = (String) data.get("ciphertext"); - cacheData(jar, ciphertextURL); - - Supplier> mkpSupplier = - () -> { - @SuppressWarnings("generic") - final List> mks = new ArrayList<>(); - - for (Map mkEntry : (List>) data.get("master-keys")) { - final String type = (String) mkEntry.get("type"); - final String keyName = (String) mkEntry.get("key"); - final KeyEntry key = keys.get(keyName); - - if ("aws-kms".equals(type)) { - mks.add(kmsProv.getMasterKey(key.keyId)); - } else if ("aws-kms-mrk-aware".equals(type)) { - AwsKmsMrkAwareMasterKeyProvider provider = - AwsKmsMrkAwareMasterKeyProvider.builder().buildStrict(key.keyId); - mks.add(provider.getMasterKey(key.keyId)); - } else if ("aws-kms-mrk-aware-discovery".equals(type)) { - final String defaultMrkRegion = (String) mkEntry.get("default-mrk-region"); - final Map discoveryFilterSpec = - (Map) mkEntry.get("aws-kms-discovery-filter"); - final DiscoveryFilter discoveryFilter; - if (discoveryFilterSpec != null) { - discoveryFilter = - new DiscoveryFilter( - (String) discoveryFilterSpec.get("partition"), - (List) discoveryFilterSpec.get("account-ids")); - } else { - discoveryFilter = null; - } - return AwsKmsMrkAwareMasterKeyProvider.builder() - .withDiscoveryMrkRegion(defaultMrkRegion) - .buildDiscovery(discoveryFilter); - } else if ("raw".equals(type)) { - final String provId = (String) mkEntry.get("provider-id"); - final String algorithm = (String) mkEntry.get("encryption-algorithm"); - if ("aes".equals(algorithm)) { - mks.add( - JceMasterKey.getInstance( - (SecretKey) key.key, provId, key.keyId, "AES/GCM/NoPadding")); - } else if ("rsa".equals(algorithm)) { - String transformation = "RSA/ECB/"; - final String padding = (String) mkEntry.get("padding-algorithm"); - if ("pkcs1".equals(padding)) { - transformation += "PKCS1Padding"; - } else if ("oaep-mgf1".equals(padding)) { - final String hashName = - ((String) mkEntry.get("padding-hash")) - .replace("sha", "sha-") - .toUpperCase(Locale.ROOT); - transformation += "OAEPWith" + hashName + "AndMGF1Padding"; - } else { - throw new IllegalArgumentException("Unsupported padding:" + padding); - } - final PublicKey wrappingKey; - final PrivateKey unwrappingKey; - if (key.key instanceof PublicKey) { - wrappingKey = (PublicKey) key.key; - unwrappingKey = null; - } else { - wrappingKey = null; - unwrappingKey = (PrivateKey) key.key; - } - mks.add( - JceMasterKey.getInstance( - wrappingKey, unwrappingKey, provId, key.keyId, transformation)); - } else { - throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); - } - } else { - throw new IllegalArgumentException("Unsupported Key Type: " + type); - } - } - - return MultipleProviderFactory.buildMultiProvider(mks); - }; - - @SuppressWarnings("unchecked") - final Map resultSpec = (Map) data.get("result"); - final ResultMatcher matcher = parseResultMatcher(jar, resultSpec); - - String decryptionMethodSpec = (String) data.get("decryption-method"); - SignaturePolicy signaturePolicy = SignaturePolicy.AllowEncryptAllowDecrypt; - if (decryptionMethodSpec != null) { - if ("streaming-unsigned-only".equals(decryptionMethodSpec)) { - signaturePolicy = SignaturePolicy.AllowEncryptForbidDecrypt; - } else { - throw new IllegalArgumentException( - "Unsupported Decryption Method: " + decryptionMethodSpec); - } - } - - return new TestCase( - testName, - ciphertextURL, - false, - mkpSupplier, - null, - Collections.emptyMap(), - matcher, - signaturePolicy); - } - - /** Parse Test to MasterKey for AWS SDK v2 */ - @SuppressWarnings("unchecked") - private static TestCase parseTest( - String testName, - Map data, - Map keys, - JarFile jar, - com.amazonaws.encryptionsdk.kmssdkv2.KmsMasterKeyProvider kmsProv) - throws IOException { - final String ciphertextURL = (String) data.get("ciphertext"); - cacheData(jar, ciphertextURL); - - Supplier> mkpSupplier = - () -> { - @SuppressWarnings("generic") - final List> mks = new ArrayList<>(); - - for (Map mkEntry : (List>) data.get("master-keys")) { - final String type = (String) mkEntry.get("type"); - final String keyName = (String) mkEntry.get("key"); - final KeyEntry key = keys.get(keyName); - - if ("aws-kms".equals(type)) { - mks.add(kmsProv.getMasterKey(key.keyId)); - } else if ("aws-kms-mrk-aware".equals(type)) { - com.amazonaws.encryptionsdk.kmssdkv2.AwsKmsMrkAwareMasterKeyProvider provider = - com.amazonaws.encryptionsdk.kmssdkv2.AwsKmsMrkAwareMasterKeyProvider.builder() - .buildStrict(key.keyId); - mks.add(provider.getMasterKey(key.keyId)); - } else if ("aws-kms-mrk-aware-discovery".equals(type)) { - final String defaultMrkRegion = (String) mkEntry.get("default-mrk-region"); - final Map discoveryFilterSpec = - (Map) mkEntry.get("aws-kms-discovery-filter"); - final DiscoveryFilter discoveryFilter; - if (discoveryFilterSpec != null) { - discoveryFilter = - new DiscoveryFilter( - (String) discoveryFilterSpec.get("partition"), - (List) discoveryFilterSpec.get("account-ids")); - } else { - discoveryFilter = null; - } - return com.amazonaws.encryptionsdk.kmssdkv2.AwsKmsMrkAwareMasterKeyProvider.builder() - .discoveryMrkRegion(Region.of(defaultMrkRegion)) - .buildDiscovery(discoveryFilter); - } else if ("raw".equals(type)) { - final String provId = (String) mkEntry.get("provider-id"); - final String algorithm = (String) mkEntry.get("encryption-algorithm"); - if ("aes".equals(algorithm)) { - mks.add( - JceMasterKey.getInstance( - (SecretKey) key.key, provId, key.keyId, "AES/GCM/NoPadding")); - } else if ("rsa".equals(algorithm)) { - String transformation = "RSA/ECB/"; - final String padding = (String) mkEntry.get("padding-algorithm"); - if ("pkcs1".equals(padding)) { - transformation += "PKCS1Padding"; - } else if ("oaep-mgf1".equals(padding)) { - final String hashName = - ((String) mkEntry.get("padding-hash")) - .replace("sha", "sha-") - .toUpperCase(Locale.ROOT); - transformation += "OAEPWith" + hashName + "AndMGF1Padding"; - } else { - throw new IllegalArgumentException("Unsupported padding:" + padding); - } - final PublicKey wrappingKey; - final PrivateKey unwrappingKey; - if (key.key instanceof PublicKey) { - wrappingKey = (PublicKey) key.key; - unwrappingKey = null; - } else { - wrappingKey = null; - unwrappingKey = (PrivateKey) key.key; - } - mks.add( - JceMasterKey.getInstance( - wrappingKey, unwrappingKey, provId, key.keyId, transformation)); - } else { - throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); - } - } else { - throw new IllegalArgumentException("Unsupported Key Type: " + type); - } - } - - return MultipleProviderFactory.buildMultiProvider(mks); - }; - - @SuppressWarnings("unchecked") - final Map resultSpec = (Map) data.get("result"); - final ResultMatcher matcher = parseResultMatcher(jar, resultSpec); - - String decryptionMethodSpec = (String) data.get("decryption-method"); - SignaturePolicy signaturePolicy = SignaturePolicy.AllowEncryptAllowDecrypt; - if (decryptionMethodSpec != null) { - if ("streaming-unsigned-only".equals(decryptionMethodSpec)) { - signaturePolicy = SignaturePolicy.AllowEncryptForbidDecrypt; - } else { - throw new IllegalArgumentException( - "Unsupported Decryption Method: " + decryptionMethodSpec); - } - } - - return new TestCase( - testName, - ciphertextURL, - false, - mkpSupplier, - null, - Collections.emptyMap(), - matcher, - signaturePolicy); - } - - private static ResultMatcher parseResultMatcher( - final JarFile jar, final Map result) throws IOException { - if (result.size() != 1) { - throw new IllegalArgumentException("Unsupported result specification: " + result); - } - Map.Entry pair = result.entrySet().iterator().next(); - if (pair.getKey().equals("output")) { - Map outputSpec = (Map) pair.getValue(); - String plaintextUrl = outputSpec.get("plaintext"); - cacheData(jar, plaintextUrl); - return new OutputResultMatcher(plaintextUrl); - } else if (pair.getKey().equals("error")) { - Map errorSpec = (Map) pair.getValue(); - String errorDescription = errorSpec.get("error-description"); - return new ErrorResultMatcher(errorDescription); - } else { - throw new IllegalArgumentException("Unsupported result specification: " + result); - } - } - - @SuppressWarnings("unchecked") - private static Map parseKeyManifest(final Map keysManifest) - throws GeneralSecurityException { - // check our type - final Map metaData = (Map) keysManifest.get("manifest"); - if (!"keys".equals(metaData.get("type"))) { - throw new IllegalArgumentException("Invalid manifest type: " + metaData.get("type")); - } - if (!Integer.valueOf(3).equals(metaData.get("version"))) { - throw new IllegalArgumentException("Invalid manifest version: " + metaData.get("version")); - } - - final Map result = new HashMap<>(); - - Map keys = (Map) keysManifest.get("keys"); - for (Map.Entry entry : keys.entrySet()) { - final String name = entry.getKey(); - final Map data = (Map) entry.getValue(); - - final String keyType = (String) data.get("type"); - final String encoding = (String) data.get("encoding"); - final String keyId = (String) data.get("key-id"); - final String material = (String) data.get("material"); // May be null - final String algorithm = (String) data.get("algorithm"); // May be null - - final KeyEntry keyEntry; - - final KeyFactory kf; - switch (keyType) { - case "symmetric": - if (!"base64".equals(encoding)) { - throw new IllegalArgumentException( - format("Key %s is symmetric but has encoding %s", keyId, encoding)); - } - keyEntry = - new KeyEntry( - name, - keyId, - keyType, - new SecretKeySpec(Base64.decode(material), algorithm.toUpperCase(Locale.ROOT))); - break; - case "private": - kf = KeyFactory.getInstance(algorithm); - if (!"pem".equals(encoding)) { - throw new IllegalArgumentException( - format("Key %s is private but has encoding %s", keyId, encoding)); - } - byte[] pkcs8Key = parsePem(material); - keyEntry = - new KeyEntry( - name, keyId, keyType, kf.generatePrivate(new PKCS8EncodedKeySpec(pkcs8Key))); - break; - case "public": - kf = KeyFactory.getInstance(algorithm); - if (!"pem".equals(encoding)) { - throw new IllegalArgumentException( - format("Key %s is private but has encoding %s", keyId, encoding)); - } - byte[] x509Key = parsePem(material); - keyEntry = - new KeyEntry( - name, keyId, keyType, kf.generatePublic(new X509EncodedKeySpec(x509Key))); - break; - case "aws-kms": - keyEntry = new KeyEntry(name, keyId, keyType, null); - break; - default: - throw new IllegalArgumentException("Unsupported key type: " + keyType); - } - - result.put(name, keyEntry); - } - - return result; - } - - private static byte[] parsePem(String pem) { - final String stripped = pem.replaceAll("-+[A-Z ]+-+", ""); - return Base64.decode(stripped); - } - - private static class KeyEntry { - final String name; - final String keyId; - final String type; - final Key key; - - private KeyEntry(String name, String keyId, String type, Key key) { - this.name = name; - this.keyId = keyId; - this.type = type; - this.key = key; - } - } - - private static class TestCase { - private final String name; - private final String ciphertextPath; - private final ResultMatcher matcher; - private final boolean isKeyring; - private final Supplier> mkpSupplier; - private final Supplier cmmSupplier; - private final Map encryptionContext; - private final SignaturePolicy signaturePolicy; - - private TestCase( - String name, - String ciphertextPath, - boolean isKeyring, - Supplier> mkpSupplier, - Supplier cmmSupplier, - Map encryptionContext, - ResultMatcher matcher, - SignaturePolicy signaturePolicy) { - this.name = name; - this.ciphertextPath = ciphertextPath; - this.matcher = matcher; - this.isKeyring = isKeyring; - this.mkpSupplier = mkpSupplier; - this.cmmSupplier = cmmSupplier; - this.encryptionContext = encryptionContext; - this.signaturePolicy = signaturePolicy; - } - } - - private interface ResultMatcher { - void Match(Callable decryptor) throws Exception; - } - - private static class OutputResultMatcher implements ResultMatcher { - - private final String plaintextPath; - - private OutputResultMatcher(String plaintextPath) { - this.plaintextPath = plaintextPath; - } - - @Override - public void Match(Callable decryptor) throws Exception { - final byte[] plaintext = decryptor.call(); - final byte[] expectedPlaintext = cachedData.get(plaintextPath); - Assert.assertArrayEquals(expectedPlaintext, plaintext); - } - } - - private static class ErrorResultMatcher implements ResultMatcher { - - private final String errorDescription; - - private ErrorResultMatcher(String errorDescription) { - this.errorDescription = errorDescription; - } - - @Override - public void Match(Callable decryptor) { - Assert.assertThrows( - "Decryption expected to fail (" + errorDescription + ") but succeeded", - Exception.class, - decryptor::call); - } - } -} From 16aa79b72360e384c45858c03dee37fd5dafb32d Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Mon, 10 Jun 2024 12:16:21 -0700 Subject: [PATCH 05/19] cleanup --- .../encryptionsdk/TestVectorGenerator.java | 570 ++++++++++++++ .../encryptionsdk/TestVectorRunner.java | 743 ++++++++++++++++++ 2 files changed, 1313 insertions(+) create mode 100644 src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java create mode 100644 src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java diff --git a/src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java b/src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java new file mode 100644 index 00000000..eb2b1764 --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java @@ -0,0 +1,570 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.encryptionsdk; + +import static java.lang.String.format; + +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.amazonaws.encryptionsdk.jce.JceMasterKey; +import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; +import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileAttribute; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import org.apache.commons.io.FileUtils; +import org.bouncycastle.util.encoders.Base64; +import org.junit.AfterClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import software.amazon.awssdk.utils.ImmutableMap; +import software.amazon.cryptography.materialproviders.IKeyring; +import software.amazon.cryptography.materialproviders.MaterialProviders; +import software.amazon.cryptography.materialproviders.model.CreateMultiKeyringInput; +import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig; +import software.amazon.cryptography.materialproviderstestvectorkeys.KeyVectors; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionInput; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionOutput; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.KeyVectorsConfig; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.TestVectorKeyringInput; + +@RunWith(Parameterized.class) +public class TestVectorGenerator { + + private static final String encryptManifestList = + "https://raw.githubusercontent.com/awslabs/aws-crypto-tools-test-vector-framework/master/features/CANONICAL-GENERATED-MANIFESTS/0003-awses-message-encryption.v2.json"; + // We save the files in memory to avoid repeatedly retrieving them. This won't work if the + // plaintexts are too + // large or numerous + private static final Map cachedData = new HashMap<>(); + private static final ObjectMapper mapper = new ObjectMapper(); + private static EncryptionInterface encryption; + private static boolean isMasterKey; + private final String testName; + private final TestCase testCase; + + // Temp Test Vectors Directory + private static String tempTestVectorPath; + // Zip File Path + private static String zipFilePath; + + public TestVectorGenerator(final String testName, TestCase testCase) { + this.testName = testName; + this.testCase = testCase; + } + + // Zip Temp Folder and delete temp files + @AfterClass + public static void zip() throws IOException { + Path zipFile = Files.createFile(Paths.get(zipFilePath)); + + Path sourceDirPath = Paths.get(tempTestVectorPath); + try (ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(zipFile)); + Stream paths = Files.walk(sourceDirPath)) { + paths + .filter(path -> !Files.isDirectory(path)) + .forEach( + path -> { + ZipEntry zipEntry = new ZipEntry(sourceDirPath.relativize(path).toString()); + try { + zipOutputStream.putNextEntry(zipEntry); + Files.copy(path, zipOutputStream); + zipOutputStream.closeEntry(); + } catch (IOException e) { + throw new UncheckedIOException("Unable to Zip File", e); + } + }); + } + FileUtils.deleteQuietly(sourceDirPath.toFile()); + + // Teardown + cachedData.clear(); + } + + @Test + @SuppressWarnings("unchecked") + public void encrypt() throws Exception { + CryptoAlgorithm cryptoAlgorithm = getCryptoAlgorithm(testCase.algorithmId); + CommitmentPolicy commitmentPolicy = + cryptoAlgorithm.isCommitting() + ? CommitmentPolicy.RequireEncryptRequireDecrypt + : CommitmentPolicy.ForbidEncryptAllowDecrypt; + + AwsCrypto crypto = + AwsCrypto.builder() + .withCommitmentPolicy(commitmentPolicy) + .withEncryptionAlgorithm(cryptoAlgorithm) + .withEncryptionFrameSize(testCase.frameSize) + .build(); + + Callable ciphertext; + if (isMasterKey) { + ciphertext = + () -> + crypto + .encryptData( + testCase.masterKey, + cachedData.get(testCase.plaintext), + testCase.encryptionContext) + .getResult(); + } else { + ciphertext = + () -> + crypto + .encryptData( + testCase.keyring, + cachedData.get(testCase.plaintext), + testCase.encryptionContext) + .getResult(); + } + Files.write(Paths.get(tempTestVectorPath + "ciphertexts/" + testName), ciphertext.call()); + } + + private static CryptoAlgorithm getCryptoAlgorithm(String algorithmId) { + Integer algId = Integer.parseInt(algorithmId, 16); + for (CryptoAlgorithm cryptoAlgorithm : CryptoAlgorithm.values()) { + if (cryptoAlgorithm.getValue() == algId) { + return cryptoAlgorithm; + } + } + throw new IllegalArgumentException("Invalid AlgorithmId: " + algorithmId); + } + + @Parameterized.Parameters(name = "Compatibility Test: {0} - {1}") + @SuppressWarnings("unchecked") + public static Collection data() throws Exception { + final String interfaceOption = System.getProperty("masterkey"); + + if (interfaceOption != null && interfaceOption.equals("true")) { + isMasterKey = true; + encryption = EncryptionInterface.EncryptWithMasterKey; + } else { + encryption = EncryptionInterface.EncryptWithKeyring; + } + + final String encryptKeyManifest = System.getProperty("keysManifest"); + if (encryptKeyManifest == null) { + return Collections.emptyList(); + } + + zipFilePath = System.getProperty("zipFilePath"); + if (zipFilePath == null) { + return Collections.emptyList(); + } + + tempTestVectorPath = Files.createTempDirectory("java", new FileAttribute[0]).toString() + "/"; + createDirectories(tempTestVectorPath + "ciphertexts/"); + createDirectories(tempTestVectorPath + "plaintexts/"); + + File decryptManifest = new File(tempTestVectorPath + "manifest.json"); + File keyManifest = new File(tempTestVectorPath + "keys.json"); + + final Map manifest = mapper.readValue(new URL(encryptManifestList), Map.class); + mapper + .writerWithDefaultPrettyPrinter() + .writeValue(decryptManifest, createDecryptManifest(manifest)); + + try (InputStream in = new FileInputStream(encryptKeyManifest)) { + Files.copy(in, keyManifest.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + + final Map keysManifest = + mapper.readValue(new File(encryptKeyManifest), Map.class); + + cachePlaintext((Map) manifest.get("plaintexts")); + + MaterialProvidersConfig config = MaterialProvidersConfig.builder().build(); + MaterialProviders materialProviders = + MaterialProviders.builder().MaterialProvidersConfig(config).build(); + KeyVectors keyVectors = + KeyVectors.builder() + .KeyVectorsConfig( + KeyVectorsConfig.builder().keyManifiestPath(keyManifest.toString()).build()) + .build(); + + final Map keys = parseKeyManifest(keysManifest); + final KmsMasterKeyProvider kmsProv = + KmsMasterKeyProvider.builder() + .withCredentials(new DefaultAWSCredentialsProviderChain()) + .buildDiscovery(); + + return ((Map>) manifest.get("tests")) + .entrySet().stream() + .map( + entry -> { + String testName = entry.getKey(); + TestCase testCase = + encryption.parseTest(entry, keys, kmsProv, materialProviders, keyVectors); + return new Object[] {testName, testCase}; + }) + .collect(Collectors.toList()); + } + + private static void createDirectories(String path) { + File directory = new File(path); + directory.mkdirs(); + } + + private enum EncryptionInterface { + EncryptWithMasterKey { + @Override + public TestCase parseTest( + Map.Entry> testEntry, + Map keys, + KmsMasterKeyProvider kmsProv, + MaterialProviders materialProviders, + KeyVectors keyVectors) { + return parseTestWithMasterkeys(testEntry, keys, kmsProv); + } + }, + EncryptWithKeyring { + @Override + public TestCase parseTest( + Map.Entry> testEntry, + Map keys, + KmsMasterKeyProvider kmsProv, + MaterialProviders materialProviders, + KeyVectors keyVectors) { + return parseTestWithKeyrings(testEntry, materialProviders, keyVectors); + } + }; + + public abstract TestCase parseTest( + Map.Entry> testEntry, + Map keys, + KmsMasterKeyProvider kmsProv, + MaterialProviders materialProviders, + KeyVectors keyVectors); + } + + private static void cachePlaintext(Map plaintexts) { + Random rd = new Random(); + plaintexts.forEach( + (key, value) -> { + byte[] plaintext = new byte[value]; + rd.nextBytes(plaintext); + try { + Files.write(new File(tempTestVectorPath + "plaintexts/" + key).toPath(), plaintext); + cachedData.put(key, plaintext); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } + + private static Map createDecryptManifest(Map encryptManifest) { + Map decryptManifest = new LinkedHashMap<>(); + + decryptManifest.put("manifest", ImmutableMap.of("type", "awses-decrypt", "version", 2)); + + decryptManifest.put( + "client", ImmutableMap.of("name", "aws/aws-encryption-sdk-java", "version", "2.2.0")); + + decryptManifest.put("keys", "file://keys.json"); + + Map> testScenarios = + ((LinkedHashMap>) encryptManifest.get("tests")) + .entrySet().stream() + .collect( + Collectors.toMap( + Map.Entry::getKey, + entry -> { + Map scenario = entry.getValue(); + return new LinkedHashMap() { + { + put("ciphertext", "file://ciphertexts/" + entry.getKey()); + put("master-keys", scenario.get("master-keys")); + put( + "result", + Collections.singletonMap( + "output", + Collections.singletonMap( + "plaintext", + "file://plaintexts/" + scenario.get("plaintext")))); + } + }; + })); + + decryptManifest.put("tests", testScenarios); + return decryptManifest; + } + + private static TestCase parseTestWithMasterkeys( + Map.Entry> testEntry, + Map keys, + KmsMasterKeyProvider kmsProv) { + + String testName = testEntry.getKey(); + Map data = testEntry.getValue(); + + String plaintext = (String) data.get("plaintext"); + String algorithmId = (String) data.get("algorithm"); + int frameSize = (int) data.get("frame-size"); + Map encryptionContext = (Map) data.get("encryption-context"); + + final List> mks = new ArrayList<>(); + + for (Map mkEntry : (List>) data.get("master-keys")) { + if (mkEntry.get("key").equals("rsa-4096-private")) { + mkEntry.replace("key", "rsa-4096-public"); + } + + final String type = mkEntry.get("type"); + final String keyName = mkEntry.get("key"); + final KeyEntry key = keys.get(keyName); + + if ("aws-kms".equals(type)) { + mks.add(kmsProv.getMasterKey(key.keyId)); + } else if ("raw".equals(type)) { + final String provId = mkEntry.get("provider-id"); + final String algorithm = mkEntry.get("encryption-algorithm"); + if ("aes".equals(algorithm)) { + mks.add( + JceMasterKey.getInstance( + (SecretKey) key.key, provId, key.keyId, "AES/GCM/NoPadding")); + } else if ("rsa".equals(algorithm)) { + String transformation = "RSA/ECB/"; + final String padding = mkEntry.get("padding-algorithm"); + if ("pkcs1".equals(padding)) { + transformation += "PKCS1Padding"; + } else if ("oaep-mgf1".equals(padding)) { + final String hashName = + mkEntry.get("padding-hash").replace("sha", "sha-").toUpperCase(); + transformation += "OAEPWith" + hashName + "AndMGF1Padding"; + } else { + throw new IllegalArgumentException("Unsupported padding:" + padding); + } + final PublicKey wrappingKey; + final PrivateKey unwrappingKey; + if (key.key instanceof PublicKey) { + wrappingKey = (PublicKey) key.key; + unwrappingKey = null; + } else { + wrappingKey = null; + unwrappingKey = (PrivateKey) key.key; + } + mks.add( + JceMasterKey.getInstance( + wrappingKey, unwrappingKey, provId, key.keyId, transformation)); + } else { + throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); + } + } else { + throw new IllegalArgumentException("Unsupported Key Type: " + type); + } + } + + MasterKeyProvider multiProvider = MultipleProviderFactory.buildMultiProvider(mks); + + return new TestCase( + testName, null, multiProvider, plaintext, algorithmId, frameSize, encryptionContext); + } + + private static TestCase parseTestWithKeyrings( + Map.Entry> testEntry, + MaterialProviders materialProviders, + KeyVectors keyVectors) { + String testName = testEntry.getKey(); + Map data = testEntry.getValue(); + + String plaintext = (String) data.get("plaintext"); + String algorithmId = (String) data.get("algorithm"); + int frameSize = (int) data.get("frame-size"); + Map encryptionContext = (Map) data.get("encryption-context"); + + List keyrings = new ArrayList<>(); + + ((List>) data.get("master-keys")) + .forEach( + mkEntry -> { + if (mkEntry.get("type").equals("raw") + && mkEntry.get("encryption-algorithm").equals("rsa")) { + if (mkEntry.get("key").equals("rsa-4096-private")) { + mkEntry.replace("key", "rsa-4096-public"); + } + mkEntry.putIfAbsent("padding-hash", "sha1"); + } + + try { + byte[] json = new ObjectMapper().writeValueAsBytes(mkEntry); + GetKeyDescriptionOutput output = + keyVectors.GetKeyDescription( + GetKeyDescriptionInput.builder().json(ByteBuffer.wrap(json)).build()); + + IKeyring testVectorKeyring = + keyVectors.CreateTestVectorKeyring( + TestVectorKeyringInput.builder() + .keyDescription(output.keyDescription()) + .build()); + + keyrings.add(testVectorKeyring); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }); + + IKeyring primary = keyrings.remove(0); + IKeyring multiKeyring = + materialProviders.CreateMultiKeyring( + CreateMultiKeyringInput.builder().generator(primary).childKeyrings(keyrings).build()); + + return new TestCase( + testName, multiKeyring, null, plaintext, algorithmId, frameSize, encryptionContext); + } + + @SuppressWarnings("unchecked") + private static Map parseKeyManifest(final Map keysManifest) + throws GeneralSecurityException { + // check our type + final Map metaData = (Map) keysManifest.get("manifest"); + if (!"keys".equals(metaData.get("type"))) { + throw new IllegalArgumentException("Invalid manifest type: " + metaData.get("type")); + } + if (!Integer.valueOf(3).equals(metaData.get("version"))) { + throw new IllegalArgumentException("Invalid manifest version: " + metaData.get("version")); + } + + final Map result = new HashMap<>(); + + Map keys = (Map) keysManifest.get("keys"); + for (Map.Entry entry : keys.entrySet()) { + final String name = entry.getKey(); + final Map data = (Map) entry.getValue(); + + final String keyType = (String) data.get("type"); + final String encoding = (String) data.get("encoding"); + final String keyId = (String) data.get("key-id"); + final String material = (String) data.get("material"); // May be null + final String algorithm = (String) data.get("algorithm"); // May be null + + final KeyEntry keyEntry; + + final KeyFactory kf; + switch (keyType) { + case "symmetric": + if (!"base64".equals(encoding)) { + throw new IllegalArgumentException( + format("Key %s is symmetric but has encoding %s", keyId, encoding)); + } + keyEntry = + new KeyEntry( + name, + keyId, + keyType, + new SecretKeySpec(Base64.decode(material), algorithm.toUpperCase())); + break; + case "private": + kf = KeyFactory.getInstance(algorithm); + if (!"pem".equals(encoding)) { + throw new IllegalArgumentException( + format("Key %s is private but has encoding %s", keyId, encoding)); + } + byte[] pkcs8Key = parsePem(material); + keyEntry = + new KeyEntry( + name, keyId, keyType, kf.generatePrivate(new PKCS8EncodedKeySpec(pkcs8Key))); + break; + case "public": + kf = KeyFactory.getInstance(algorithm); + if (!"pem".equals(encoding)) { + throw new IllegalArgumentException( + format("Key %s is private but has encoding %s", keyId, encoding)); + } + byte[] x509Key = parsePem(material); + keyEntry = + new KeyEntry( + name, keyId, keyType, kf.generatePublic(new X509EncodedKeySpec(x509Key))); + break; + case "aws-kms": + keyEntry = new KeyEntry(name, keyId, keyType, null); + break; + default: + throw new IllegalArgumentException("Unsupported key type: " + keyType); + } + + result.put(name, keyEntry); + } + + return result; + } + + private static byte[] parsePem(String pem) { + final String stripped = pem.replaceAll("-+[A-Z ]+-+", ""); + return Base64.decode(stripped); + } + + private static class KeyEntry { + final String name; + final String keyId; + final String type; + final Key key; + + private KeyEntry(String name, String keyId, String type, Key key) { + this.name = name; + this.keyId = keyId; + this.type = type; + this.key = key; + } + } + + private static class TestCase { + private final String name; + private final IKeyring keyring; + private final MasterKeyProvider masterKey; + private final String plaintext; + private final String algorithmId; + private final int frameSize; + private final Map encryptionContext; + + public TestCase( + String name, + IKeyring keyring, + MasterKeyProvider multiProvider, + String plaintext, + String algorithmId, + int frameSize, + Map encryptionContext) { + this.name = name; + this.keyring = keyring; + this.masterKey = multiProvider; + this.plaintext = plaintext; + this.algorithmId = algorithmId; + this.frameSize = frameSize; + this.encryptionContext = encryptionContext; + } + } +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java b/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java new file mode 100644 index 00000000..8c0be82f --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java @@ -0,0 +1,743 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.encryptionsdk; + +import static java.lang.String.format; + +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.amazonaws.encryptionsdk.internal.SignaturePolicy; +import com.amazonaws.encryptionsdk.jce.JceMasterKey; +import com.amazonaws.encryptionsdk.kms.AwsKmsMrkAwareMasterKeyProvider; +import com.amazonaws.encryptionsdk.kms.DiscoveryFilter; +import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; +import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory; +import com.amazonaws.util.IOUtils; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.URL; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.function.Supplier; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import junit.framework.TestCase; +import org.bouncycastle.util.encoders.Base64; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import software.amazon.awssdk.regions.Region; +import software.amazon.cryptography.materialproviders.ICryptographicMaterialsManager; +import software.amazon.cryptography.materialproviders.IKeyring; +import software.amazon.cryptography.materialproviders.MaterialProviders; +import software.amazon.cryptography.materialproviders.model.CreateDefaultCryptographicMaterialsManagerInput; +import software.amazon.cryptography.materialproviders.model.CreateMultiKeyringInput; +import software.amazon.cryptography.materialproviders.model.CreateRequiredEncryptionContextCMMInput; +import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig; +import software.amazon.cryptography.materialproviderstestvectorkeys.KeyVectors; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionInput; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionOutput; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.KeyVectorsConfig; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.TestVectorKeyringInput; + +@RunWith(Parameterized.class) +public class TestVectorRunner { + + // TODO: Standardize Manifest Version + private static final List MANIFEST_VERSIONS = Arrays.asList(2, 4); + + // We save the files in memory to avoid repeatedly retrieving them. This won't work if the + // plaintexts are too + // large or numerous + private static final Map cachedData = new HashMap<>(); + + private final String testName; + private final TestCase testCase; + private final DecryptionMethod decryptionMethod; + + public TestVectorRunner( + final String testName, TestCase testCase, DecryptionMethod decryptionMethod) { + this.testName = testName; + this.testCase = testCase; + this.decryptionMethod = decryptionMethod; + } + + @Test + public void decrypt() throws Exception { + AwsCrypto crypto = + AwsCrypto.builder() + .withCommitmentPolicy(CommitmentPolicy.ForbidEncryptAllowDecrypt) + .build(); + Callable decryptor; + if (testCase.isKeyring) { + decryptor = + () -> + decryptionMethod.decryptMessage( + crypto, + testCase.cmmSupplier.get(), + cachedData.get(testCase.ciphertextPath), + testCase.encryptionContext); + } else { + decryptor = + () -> + decryptionMethod.decryptMessage( + crypto, testCase.mkpSupplier.get(), cachedData.get(testCase.ciphertextPath)); + } + testCase.matcher.Match(decryptor); + } + + @Parameterized.Parameters(name = "Compatibility Test: {0} - {3}") + @SuppressWarnings("unchecked") + public static Collection data() throws Exception { + final String zipPath = System.getProperty("testVectorZip"); + final String interfaceOption = System.getProperty("masterkey"); + if (zipPath == null) { + return Collections.emptyList(); + } + + final JarURLConnection jarConnection = + (JarURLConnection) new URL("jar:" + zipPath + "!/").openConnection(); + + try (JarFile jar = jarConnection.getJarFile()) { + + final Map manifest = readJsonMapFromJar(jar, "manifest.json"); + final Map keysManifest = readJsonMapFromJar(jar, "keys.json"); + + ObjectMapper objectMapper = new ObjectMapper(); + + // Create a temporary file and write the JSON string to it + File tempFile = File.createTempFile("keys", ".json"); + objectMapper.writeValue(tempFile, keysManifest); + + final Map metaData = (Map) manifest.get("manifest"); + + // We only support "awses-decrypt" type manifests right now + if (!"awses-decrypt".equals(metaData.get("type"))) { + throw new IllegalArgumentException("Unsupported manifest type: " + metaData.get("type")); + } + Integer readVersion = (Integer) metaData.get("version"); + if (!MANIFEST_VERSIONS.contains(readVersion)) { + throw new IllegalArgumentException( + "Unsupported manifest version: " + metaData.get("version")); + } + + final Map keys = + parseKeyManifest(readJsonMapFromJar(jar, (String) manifest.get("keys"))); + + KeyVectors keyVectors = + KeyVectors.builder() + .KeyVectorsConfig( + KeyVectorsConfig.builder().keyManifiestPath(tempFile.getPath()).build()) + .build(); + + MaterialProvidersConfig config = MaterialProvidersConfig.builder().build(); + MaterialProviders materialProviders = + MaterialProviders.builder().MaterialProvidersConfig(config).build(); + + final KmsMasterKeyProvider kmsProvV1 = + KmsMasterKeyProvider.builder() + .withCredentials(new DefaultAWSCredentialsProviderChain()) + .buildDiscovery(); + + final com.amazonaws.encryptionsdk.kmssdkv2.KmsMasterKeyProvider kmsProvV2 = + com.amazonaws.encryptionsdk.kmssdkv2.KmsMasterKeyProvider.builder().buildDiscovery(); + + List testCases = new ArrayList<>(); + for (Map.Entry> testEntry : + ((Map>) manifest.get("tests")).entrySet()) { + if (interfaceOption != null && interfaceOption.equals("true")) { + String testName = testEntry.getKey(); + + TestCase testCaseV1 = + parseTest(testEntry.getKey(), testEntry.getValue(), keys, jar, kmsProvV1); + TestCase testCaseV2 = + parseTest(testEntry.getKey(), testEntry.getValue(), keys, jar, kmsProvV2); + + for (DecryptionMethod decryptionMethod : DecryptionMethod.values()) { + if (testCaseV1.signaturePolicy.equals(decryptionMethod.signaturePolicy())) { + testCases.add(new Object[] {testName, testCaseV1, decryptionMethod}); + testCases.add(new Object[] {testName + "-V2", testCaseV2, decryptionMethod}); + } + } + } else { + String testName = testEntry.getKey(); + TestCase testCaseKeyring = + parseTest( + testEntry.getKey(), + testEntry.getValue(), + keys, + jar, + materialProviders, + keyVectors); + + for (DecryptionMethod decryptionMethod : DecryptionMethod.values()) { + if (testCaseKeyring.signaturePolicy.equals(decryptionMethod.signaturePolicy())) { + testCases.add( + new Object[] {testName + "-Keyrings", testCaseKeyring, decryptionMethod}); + } + } + } + } + return testCases; + } + } + + @AfterClass + public static void teardown() { + cachedData.clear(); + } + + private static byte[] readBytesFromJar(JarFile jar, String fileName) throws IOException { + try (InputStream is = readFromJar(jar, fileName)) { + return IOUtils.toByteArray(is); + } + } + + private static Map readJsonMapFromJar(JarFile jar, String fileName) + throws IOException { + try (InputStream is = readFromJar(jar, fileName)) { + final ObjectMapper mapper = new ObjectMapper(); + return mapper.readValue(is, new TypeReference>() {}); + } + } + + private static InputStream readFromJar(JarFile jar, String name) throws IOException { + // Our manifest URIs incorrectly start with file:// rather than just file: so we need to strip + // this + ZipEntry entry = jar.getEntry(name.replaceFirst("^file://(?!/)", "")); + return jar.getInputStream(entry); + } + + private static void cacheData(JarFile jar, String url) throws IOException { + if (!cachedData.containsKey(url)) { + cachedData.put(url, readBytesFromJar(jar, url)); + } + } + + /** Parse Test to Keyring */ + @SuppressWarnings("unchecked") + private static TestCase parseTest( + String testName, + Map data, + Map keys, + JarFile jar, + MaterialProviders materialProviders, + KeyVectors keyVectors) + throws IOException { + final String ciphertextURL = (String) data.get("ciphertext"); + cacheData(jar, ciphertextURL); + + Supplier cmmSupplier = + () -> { + final List keyrings = new ArrayList<>(); + for (Map mkEntry : (List>) data.get("master-keys")) { + if (mkEntry.get("type").equals("raw") + && mkEntry.get("encryption-algorithm").equals("rsa")) { + mkEntry.putIfAbsent("padding-hash", "sha1"); + } + + try { + byte[] json = new ObjectMapper().writeValueAsBytes(mkEntry); + GetKeyDescriptionOutput output = + keyVectors.GetKeyDescription( + GetKeyDescriptionInput.builder().json(ByteBuffer.wrap(json)).build()); + + IKeyring testVectorKeyring = + keyVectors.CreateTestVectorKeyring( + TestVectorKeyringInput.builder() + .keyDescription(output.keyDescription()) + .build()); + + keyrings.add(testVectorKeyring); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + IKeyring multiKeyring = + materialProviders.CreateMultiKeyring( + CreateMultiKeyringInput.builder() + .generator(keyrings.get(0)) + .childKeyrings(keyrings) + .build()); + + CreateDefaultCryptographicMaterialsManagerInput defaultInput = + CreateDefaultCryptographicMaterialsManagerInput.builder() + .keyring(multiKeyring) + .build(); + ICryptographicMaterialsManager cmm = + materialProviders.CreateDefaultCryptographicMaterialsManager(defaultInput); + if (data.containsKey("cmm") && data.get("cmm").equals("RequiredEncryptionContext")) { + List requiredKeys = new ArrayList(2); + requiredKeys.add("key1"); + requiredKeys.add("key2"); + CreateRequiredEncryptionContextCMMInput requiredCMMInput = + CreateRequiredEncryptionContextCMMInput.builder() + .underlyingCMM( + materialProviders.CreateDefaultCryptographicMaterialsManager(defaultInput)) + .requiredEncryptionContextKeys(requiredKeys) + .build(); + cmm = materialProviders.CreateRequiredEncryptionContextCMM(requiredCMMInput); + } + return cmm; + }; + @SuppressWarnings("unchecked") + final Map resultSpec = (Map) data.get("result"); + final ResultMatcher matcher = parseResultMatcher(jar, resultSpec); + + Map ec = Collections.emptyMap(); + + if (data.get("encryption-context") != null) { + ec = (Map) data.get("encryption-context"); + } + + String decryptionMethodSpec = (String) data.get("decryption-method"); + SignaturePolicy signaturePolicy = SignaturePolicy.AllowEncryptAllowDecrypt; + if (decryptionMethodSpec != null) { + if ("streaming-unsigned-only".equals(decryptionMethodSpec)) { + signaturePolicy = SignaturePolicy.AllowEncryptForbidDecrypt; + } else { + throw new IllegalArgumentException( + "Unsupported Decryption Method: " + decryptionMethodSpec); + } + } + + return new TestCase( + testName, ciphertextURL, true, null, cmmSupplier, ec, matcher, signaturePolicy); + } + + /** Parse Test to MasterKey for AWS SDK v1 */ + @SuppressWarnings("unchecked") + private static TestCase parseTest( + String testName, + Map data, + Map keys, + JarFile jar, + KmsMasterKeyProvider kmsProv) + throws IOException { + final String ciphertextURL = (String) data.get("ciphertext"); + cacheData(jar, ciphertextURL); + + Supplier> mkpSupplier = + () -> { + @SuppressWarnings("generic") + final List> mks = new ArrayList<>(); + + for (Map mkEntry : (List>) data.get("master-keys")) { + final String type = (String) mkEntry.get("type"); + final String keyName = (String) mkEntry.get("key"); + final KeyEntry key = keys.get(keyName); + + if ("aws-kms".equals(type)) { + mks.add(kmsProv.getMasterKey(key.keyId)); + } else if ("aws-kms-mrk-aware".equals(type)) { + AwsKmsMrkAwareMasterKeyProvider provider = + AwsKmsMrkAwareMasterKeyProvider.builder().buildStrict(key.keyId); + mks.add(provider.getMasterKey(key.keyId)); + } else if ("aws-kms-mrk-aware-discovery".equals(type)) { + final String defaultMrkRegion = (String) mkEntry.get("default-mrk-region"); + final Map discoveryFilterSpec = + (Map) mkEntry.get("aws-kms-discovery-filter"); + final DiscoveryFilter discoveryFilter; + if (discoveryFilterSpec != null) { + discoveryFilter = + new DiscoveryFilter( + (String) discoveryFilterSpec.get("partition"), + (List) discoveryFilterSpec.get("account-ids")); + } else { + discoveryFilter = null; + } + return AwsKmsMrkAwareMasterKeyProvider.builder() + .withDiscoveryMrkRegion(defaultMrkRegion) + .buildDiscovery(discoveryFilter); + } else if ("raw".equals(type)) { + final String provId = (String) mkEntry.get("provider-id"); + final String algorithm = (String) mkEntry.get("encryption-algorithm"); + if ("aes".equals(algorithm)) { + mks.add( + JceMasterKey.getInstance( + (SecretKey) key.key, provId, key.keyId, "AES/GCM/NoPadding")); + } else if ("rsa".equals(algorithm)) { + String transformation = "RSA/ECB/"; + final String padding = (String) mkEntry.get("padding-algorithm"); + if ("pkcs1".equals(padding)) { + transformation += "PKCS1Padding"; + } else if ("oaep-mgf1".equals(padding)) { + final String hashName = + ((String) mkEntry.get("padding-hash")) + .replace("sha", "sha-") + .toUpperCase(Locale.ROOT); + transformation += "OAEPWith" + hashName + "AndMGF1Padding"; + } else { + throw new IllegalArgumentException("Unsupported padding:" + padding); + } + final PublicKey wrappingKey; + final PrivateKey unwrappingKey; + if (key.key instanceof PublicKey) { + wrappingKey = (PublicKey) key.key; + unwrappingKey = null; + } else { + wrappingKey = null; + unwrappingKey = (PrivateKey) key.key; + } + mks.add( + JceMasterKey.getInstance( + wrappingKey, unwrappingKey, provId, key.keyId, transformation)); + } else { + throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); + } + } else { + throw new IllegalArgumentException("Unsupported Key Type: " + type); + } + } + + return MultipleProviderFactory.buildMultiProvider(mks); + }; + + @SuppressWarnings("unchecked") + final Map resultSpec = (Map) data.get("result"); + final ResultMatcher matcher = parseResultMatcher(jar, resultSpec); + + String decryptionMethodSpec = (String) data.get("decryption-method"); + SignaturePolicy signaturePolicy = SignaturePolicy.AllowEncryptAllowDecrypt; + if (decryptionMethodSpec != null) { + if ("streaming-unsigned-only".equals(decryptionMethodSpec)) { + signaturePolicy = SignaturePolicy.AllowEncryptForbidDecrypt; + } else { + throw new IllegalArgumentException( + "Unsupported Decryption Method: " + decryptionMethodSpec); + } + } + + return new TestCase( + testName, + ciphertextURL, + false, + mkpSupplier, + null, + Collections.emptyMap(), + matcher, + signaturePolicy); + } + + /** Parse Test to MasterKey for AWS SDK v2 */ + @SuppressWarnings("unchecked") + private static TestCase parseTest( + String testName, + Map data, + Map keys, + JarFile jar, + com.amazonaws.encryptionsdk.kmssdkv2.KmsMasterKeyProvider kmsProv) + throws IOException { + final String ciphertextURL = (String) data.get("ciphertext"); + cacheData(jar, ciphertextURL); + + Supplier> mkpSupplier = + () -> { + @SuppressWarnings("generic") + final List> mks = new ArrayList<>(); + + for (Map mkEntry : (List>) data.get("master-keys")) { + final String type = (String) mkEntry.get("type"); + final String keyName = (String) mkEntry.get("key"); + final KeyEntry key = keys.get(keyName); + + if ("aws-kms".equals(type)) { + mks.add(kmsProv.getMasterKey(key.keyId)); + } else if ("aws-kms-mrk-aware".equals(type)) { + com.amazonaws.encryptionsdk.kmssdkv2.AwsKmsMrkAwareMasterKeyProvider provider = + com.amazonaws.encryptionsdk.kmssdkv2.AwsKmsMrkAwareMasterKeyProvider.builder() + .buildStrict(key.keyId); + mks.add(provider.getMasterKey(key.keyId)); + } else if ("aws-kms-mrk-aware-discovery".equals(type)) { + final String defaultMrkRegion = (String) mkEntry.get("default-mrk-region"); + final Map discoveryFilterSpec = + (Map) mkEntry.get("aws-kms-discovery-filter"); + final DiscoveryFilter discoveryFilter; + if (discoveryFilterSpec != null) { + discoveryFilter = + new DiscoveryFilter( + (String) discoveryFilterSpec.get("partition"), + (List) discoveryFilterSpec.get("account-ids")); + } else { + discoveryFilter = null; + } + return com.amazonaws.encryptionsdk.kmssdkv2.AwsKmsMrkAwareMasterKeyProvider.builder() + .discoveryMrkRegion(Region.of(defaultMrkRegion)) + .buildDiscovery(discoveryFilter); + } else if ("raw".equals(type)) { + final String provId = (String) mkEntry.get("provider-id"); + final String algorithm = (String) mkEntry.get("encryption-algorithm"); + if ("aes".equals(algorithm)) { + mks.add( + JceMasterKey.getInstance( + (SecretKey) key.key, provId, key.keyId, "AES/GCM/NoPadding")); + } else if ("rsa".equals(algorithm)) { + String transformation = "RSA/ECB/"; + final String padding = (String) mkEntry.get("padding-algorithm"); + if ("pkcs1".equals(padding)) { + transformation += "PKCS1Padding"; + } else if ("oaep-mgf1".equals(padding)) { + final String hashName = + ((String) mkEntry.get("padding-hash")) + .replace("sha", "sha-") + .toUpperCase(Locale.ROOT); + transformation += "OAEPWith" + hashName + "AndMGF1Padding"; + } else { + throw new IllegalArgumentException("Unsupported padding:" + padding); + } + final PublicKey wrappingKey; + final PrivateKey unwrappingKey; + if (key.key instanceof PublicKey) { + wrappingKey = (PublicKey) key.key; + unwrappingKey = null; + } else { + wrappingKey = null; + unwrappingKey = (PrivateKey) key.key; + } + mks.add( + JceMasterKey.getInstance( + wrappingKey, unwrappingKey, provId, key.keyId, transformation)); + } else { + throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); + } + } else { + throw new IllegalArgumentException("Unsupported Key Type: " + type); + } + } + + return MultipleProviderFactory.buildMultiProvider(mks); + }; + + @SuppressWarnings("unchecked") + final Map resultSpec = (Map) data.get("result"); + final ResultMatcher matcher = parseResultMatcher(jar, resultSpec); + + String decryptionMethodSpec = (String) data.get("decryption-method"); + SignaturePolicy signaturePolicy = SignaturePolicy.AllowEncryptAllowDecrypt; + if (decryptionMethodSpec != null) { + if ("streaming-unsigned-only".equals(decryptionMethodSpec)) { + signaturePolicy = SignaturePolicy.AllowEncryptForbidDecrypt; + } else { + throw new IllegalArgumentException( + "Unsupported Decryption Method: " + decryptionMethodSpec); + } + } + + return new TestCase( + testName, + ciphertextURL, + false, + mkpSupplier, + null, + Collections.emptyMap(), + matcher, + signaturePolicy); + } + + private static ResultMatcher parseResultMatcher( + final JarFile jar, final Map result) throws IOException { + if (result.size() != 1) { + throw new IllegalArgumentException("Unsupported result specification: " + result); + } + Map.Entry pair = result.entrySet().iterator().next(); + if (pair.getKey().equals("output")) { + Map outputSpec = (Map) pair.getValue(); + String plaintextUrl = outputSpec.get("plaintext"); + cacheData(jar, plaintextUrl); + return new OutputResultMatcher(plaintextUrl); + } else if (pair.getKey().equals("error")) { + Map errorSpec = (Map) pair.getValue(); + String errorDescription = errorSpec.get("error-description"); + return new ErrorResultMatcher(errorDescription); + } else { + throw new IllegalArgumentException("Unsupported result specification: " + result); + } + } + + @SuppressWarnings("unchecked") + private static Map parseKeyManifest(final Map keysManifest) + throws GeneralSecurityException { + // check our type + final Map metaData = (Map) keysManifest.get("manifest"); + if (!"keys".equals(metaData.get("type"))) { + throw new IllegalArgumentException("Invalid manifest type: " + metaData.get("type")); + } + if (!Integer.valueOf(3).equals(metaData.get("version"))) { + throw new IllegalArgumentException("Invalid manifest version: " + metaData.get("version")); + } + + final Map result = new HashMap<>(); + + Map keys = (Map) keysManifest.get("keys"); + for (Map.Entry entry : keys.entrySet()) { + final String name = entry.getKey(); + final Map data = (Map) entry.getValue(); + + final String keyType = (String) data.get("type"); + final String encoding = (String) data.get("encoding"); + final String keyId = (String) data.get("key-id"); + final String material = (String) data.get("material"); // May be null + final String algorithm = (String) data.get("algorithm"); // May be null + + final KeyEntry keyEntry; + + final KeyFactory kf; + switch (keyType) { + case "symmetric": + if (!"base64".equals(encoding)) { + throw new IllegalArgumentException( + format("Key %s is symmetric but has encoding %s", keyId, encoding)); + } + keyEntry = + new KeyEntry( + name, + keyId, + keyType, + new SecretKeySpec(Base64.decode(material), algorithm.toUpperCase(Locale.ROOT))); + break; + case "private": + kf = KeyFactory.getInstance(algorithm); + if (!"pem".equals(encoding)) { + throw new IllegalArgumentException( + format("Key %s is private but has encoding %s", keyId, encoding)); + } + byte[] pkcs8Key = parsePem(material); + keyEntry = + new KeyEntry( + name, keyId, keyType, kf.generatePrivate(new PKCS8EncodedKeySpec(pkcs8Key))); + break; + case "public": + kf = KeyFactory.getInstance(algorithm); + if (!"pem".equals(encoding)) { + throw new IllegalArgumentException( + format("Key %s is private but has encoding %s", keyId, encoding)); + } + byte[] x509Key = parsePem(material); + keyEntry = + new KeyEntry( + name, keyId, keyType, kf.generatePublic(new X509EncodedKeySpec(x509Key))); + break; + case "aws-kms": + keyEntry = new KeyEntry(name, keyId, keyType, null); + break; + default: + throw new IllegalArgumentException("Unsupported key type: " + keyType); + } + + result.put(name, keyEntry); + } + + return result; + } + + private static byte[] parsePem(String pem) { + final String stripped = pem.replaceAll("-+[A-Z ]+-+", ""); + return Base64.decode(stripped); + } + + private static class KeyEntry { + final String name; + final String keyId; + final String type; + final Key key; + + private KeyEntry(String name, String keyId, String type, Key key) { + this.name = name; + this.keyId = keyId; + this.type = type; + this.key = key; + } + } + + private static class TestCase { + private final String name; + private final String ciphertextPath; + private final ResultMatcher matcher; + private final boolean isKeyring; + private final Supplier> mkpSupplier; + private final Supplier cmmSupplier; + private final Map encryptionContext; + private final SignaturePolicy signaturePolicy; + + private TestCase( + String name, + String ciphertextPath, + boolean isKeyring, + Supplier> mkpSupplier, + Supplier cmmSupplier, + Map encryptionContext, + ResultMatcher matcher, + SignaturePolicy signaturePolicy) { + this.name = name; + this.ciphertextPath = ciphertextPath; + this.matcher = matcher; + this.isKeyring = isKeyring; + this.mkpSupplier = mkpSupplier; + this.cmmSupplier = cmmSupplier; + this.encryptionContext = encryptionContext; + this.signaturePolicy = signaturePolicy; + } + } + + private interface ResultMatcher { + void Match(Callable decryptor) throws Exception; + } + + private static class OutputResultMatcher implements ResultMatcher { + + private final String plaintextPath; + + private OutputResultMatcher(String plaintextPath) { + this.plaintextPath = plaintextPath; + } + + @Override + public void Match(Callable decryptor) throws Exception { + final byte[] plaintext = decryptor.call(); + final byte[] expectedPlaintext = cachedData.get(plaintextPath); + Assert.assertArrayEquals(expectedPlaintext, plaintext); + } + } + + private static class ErrorResultMatcher implements ResultMatcher { + + private final String errorDescription; + + private ErrorResultMatcher(String errorDescription) { + this.errorDescription = errorDescription; + } + + @Override + public void Match(Callable decryptor) { + Assert.assertThrows( + "Decryption expected to fail (" + errorDescription + ") but succeeded", + Exception.class, + decryptor::call); + } + } +} From 23db7f9c69261f09ee4371b7e14b10ef89b76693 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Mon, 10 Jun 2024 14:49:14 -0700 Subject: [PATCH 06/19] changes --- .../crypto/examples/v2/CustomCMMExample.java | 111 +++++++++++++ .../amazonaws/encryptionsdk/CMMHandler.java | 32 +++- .../examples/v2/CustomCMMExampleTest.java | 43 +++++ .../v2/V2DefaultCryptoMaterialsManager.java | 147 ++++++++++++++++++ 4 files changed, 327 insertions(+), 6 deletions(-) create mode 100644 src/examples/java/com/amazonaws/crypto/examples/v2/CustomCMMExample.java create mode 100644 src/test/java/com/amazonaws/crypto/examples/v2/CustomCMMExampleTest.java create mode 100644 src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java diff --git a/src/examples/java/com/amazonaws/crypto/examples/v2/CustomCMMExample.java b/src/examples/java/com/amazonaws/crypto/examples/v2/CustomCMMExample.java new file mode 100644 index 00000000..138d1465 --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/v2/CustomCMMExample.java @@ -0,0 +1,111 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.v2; + +import com.amazonaws.encryptionsdk.*; +import com.amazonaws.encryptionsdk.kmssdkv2.KmsMasterKeyProvider; +import com.amazonaws.encryptionsdk.model.DecryptionMaterials; +import com.amazonaws.encryptionsdk.model.DecryptionMaterialsRequest; +import com.amazonaws.encryptionsdk.model.EncryptionMaterials; +import com.amazonaws.encryptionsdk.model.EncryptionMaterialsRequest; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; + +public class CustomCMMExample { + + private static final byte[] EXAMPLE_DATA = "Hello World".getBytes(StandardCharsets.UTF_8); + + public static void main(final String[] args) { + final String keyArn = args[0]; + + CryptoMaterialsManager cmm = new SigningSuiteOnlyCMM( + KmsMasterKeyProvider.builder().buildStrict(keyArn) + ); + + encryptAndDecryptWithCMM(cmm); + } + + static void encryptAndDecryptWithCMM(final CryptoMaterialsManager cmm) { + // 1. Instantiate the SDK + // This builds the AwsCrypto client with the RequireEncryptRequireDecrypt commitment policy, + // which enforces that this client only encrypts using committing algorithm suites and enforces + // that this client will only decrypt encrypted messages that were created with a committing algorithm suite. + // This is the default commitment policy if you build the client with `AwsCrypto.builder().build()` + // or `AwsCrypto.standard()`. + final AwsCrypto crypto = AwsCrypto.builder() + .withCommitmentPolicy(CommitmentPolicy.RequireEncryptRequireDecrypt) + .build(); + + // 2. Create an encryption context + // Most encrypted data should have an associated encryption context + // to protect integrity. This sample uses placeholder values. + // For more information see: + // blogs.aws.amazon.com/security/post/Tx2LZ6WBJJANTNW/How-to-Protect-the-Integrity-of-Your-Encrypted-Data-by-Using-AWS-Key-Management + final Map encryptionContext = Collections.singletonMap("ExampleContextKey", "ExampleContextValue"); + + // 3. Encrypt the data with the provided CMM + final CryptoResult encryptResult = crypto.encryptData(cmm, EXAMPLE_DATA, encryptionContext); + final byte[] ciphertext = encryptResult.getResult(); + + // 4. Decrypt the data + final CryptoResult decryptResult = crypto.decryptData(cmm, ciphertext); + + // 5. Verify that the encryption context in the result contains the + // encryption context supplied to the encryptData method. Because the + // SDK can add values to the encryption context, don't require that + // the entire context matches. + if (!encryptionContext.entrySet().stream() + .allMatch(e -> e.getValue().equals(decryptResult.getEncryptionContext().get(e.getKey())))) { + throw new IllegalStateException("Wrong Encryption Context!"); + } + + // 6. Verify that the decrypted plaintext matches the original plaintext + assert Arrays.equals(decryptResult.getResult(), EXAMPLE_DATA); + } + + public static class SigningSuiteOnlyCMM implements CryptoMaterialsManager { + + private final HashSet approvedAlgorithms = new HashSet<>(Arrays.asList( + CryptoAlgorithm.ALG_AES_128_GCM_IV12_TAG16_HKDF_SHA256_ECDSA_P256, + CryptoAlgorithm.ALG_AES_192_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + CryptoAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY + )); + + // The underlying CMM. + // The SigningSuiteOnlyCMM will ensure that any operations TODO + private CryptoMaterialsManager underlyingCMM; + + // If only a MasterKeyProvider is constructed, the underlying CMM is the default CMM. + public SigningSuiteOnlyCMM(MasterKeyProvider mkp) { + this.underlyingCMM = new DefaultCryptoMaterialsManager(mkp); + } + + public SigningSuiteOnlyCMM(CryptoMaterialsManager underlyingCMM) { + this.underlyingCMM = underlyingCMM; + } + + @Override + public EncryptionMaterials getMaterialsForEncrypt(EncryptionMaterialsRequest request) { + EncryptionMaterials materials = underlyingCMM.getMaterialsForEncrypt(request); + if (materials.getAlgorithm().getTrailingSignatureAlgo() == null) { + throw new IllegalArgumentException("Algorithm provided to SigningSuiteOnlyCMM is not a supported signing algorithm: " + materials.getAlgorithm()); + } + return materials; + } + + @Override + public DecryptionMaterials decryptMaterials(DecryptionMaterialsRequest request) { + if (request.getAlgorithm().getTrailingSignatureAlgo() == null) { + throw new IllegalArgumentException("Algorithm provided to SigningSuiteOnlyCMM is not a supported signing algorithm: " + request.getAlgorithm()); + } + return underlyingCMM.decryptMaterials(request); + } + } + +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/CMMHandler.java b/src/main/java/com/amazonaws/encryptionsdk/CMMHandler.java index 3b6e64b0..3cc4b2a5 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/CMMHandler.java +++ b/src/main/java/com/amazonaws/encryptionsdk/CMMHandler.java @@ -4,11 +4,8 @@ package com.amazonaws.encryptionsdk; import com.amazonaws.encryptionsdk.internal.Utils; -import com.amazonaws.encryptionsdk.model.DecryptionMaterialsHandler; -import com.amazonaws.encryptionsdk.model.DecryptionMaterialsRequest; -import com.amazonaws.encryptionsdk.model.EncryptionMaterialsHandler; -import com.amazonaws.encryptionsdk.model.EncryptionMaterialsRequest; -import com.amazonaws.encryptionsdk.model.KeyBlob; +import com.amazonaws.encryptionsdk.model.*; + import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; @@ -63,7 +60,30 @@ private GetEncryptionMaterialsInput getEncryptionMaterialsRequestInput( public DecryptionMaterialsHandler decryptMaterials( DecryptionMaterialsRequest request, CommitmentPolicy commitmentPolicy) { if (cmm != null && mplCMM == null) { - return new DecryptionMaterialsHandler(cmm.decryptMaterials(request)); + // This is an implementation of the legacy native CryptoMaterialsManager interface from ESDK-Java. + DecryptionMaterials materials = cmm.decryptMaterials(request); + if (materials.getEncryptionContext().isEmpty() + && !request.getEncryptionContext().isEmpty()) { + // If the request specified an encryption context, + // and we are using the legacy native CMM, + // add the encryptionContext to the materials. + // + // ESDK-Java 3.0 changed the expected behavior for CMMs. + // After 3.0, the ESDK assumes that CMMs' implementations of decryptMaterials + // will return DecryptionMaterials with a set encryptionContext attribute. + // The default CMM's behavior was changed in 3.0 to set the encryptionContext attribute + // with the value from the ciphertext's headers. + // However, this assumption is not true for custom CMMs from earlier ESDK versions. + // + // After 3.0, CMMs MUST set the encryptionContext on the returned decryptMaterials. + // If the materials' encryptionContext is empty but the request's is not, + // we assume the CMM never set the decryptMaterials' encryptionContext attribute + // and set its attribute from the request. + materials = materials.toBuilder() + .setEncryptionContext(request.getEncryptionContext()) + .build(); + } + return new DecryptionMaterialsHandler(materials); } else { DecryptMaterialsInput input = getDecryptMaterialsInput(request, commitmentPolicy); DecryptMaterialsOutput output = mplCMM.DecryptMaterials(input); diff --git a/src/test/java/com/amazonaws/crypto/examples/v2/CustomCMMExampleTest.java b/src/test/java/com/amazonaws/crypto/examples/v2/CustomCMMExampleTest.java new file mode 100644 index 00000000..d80b4768 --- /dev/null +++ b/src/test/java/com/amazonaws/crypto/examples/v2/CustomCMMExampleTest.java @@ -0,0 +1,43 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.v2; + +import com.amazonaws.encryptionsdk.*; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.exception.CannotUnwrapDataKeyException; +import com.amazonaws.encryptionsdk.internal.Constants; +import com.amazonaws.encryptionsdk.internal.TrailingSignatureAlgorithm; +import com.amazonaws.encryptionsdk.internal.Utils; +import com.amazonaws.encryptionsdk.kms.KMSTestFixtures; +import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; +import com.amazonaws.encryptionsdk.model.*; +import org.junit.Test; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.PublicKey; +import java.util.*; + +import static com.amazonaws.encryptionsdk.internal.Utils.assertNonNull; + +public class CustomCMMExampleTest { + + @Test + public void testCustomCMMExample() { + CryptoMaterialsManager cmm = new CustomCMMExample.SigningSuiteOnlyCMM( + KmsMasterKeyProvider.builder().buildStrict(KMSTestFixtures.US_WEST_2_KEY_ID) + ); + CustomCMMExample.encryptAndDecryptWithCMM(cmm); + } + + @Test + public void testV2Cmm() { + V2DefaultCryptoMaterialsManager cmm = new V2DefaultCryptoMaterialsManager( + KmsMasterKeyProvider.builder().buildStrict(KMSTestFixtures.US_WEST_2_KEY_ID) + ); + CustomCMMExample.encryptAndDecryptWithCMM(cmm); + } +} + + diff --git a/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java b/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java new file mode 100644 index 00000000..c1154625 --- /dev/null +++ b/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java @@ -0,0 +1,147 @@ +package com.amazonaws.crypto.examples.v2; + +import com.amazonaws.encryptionsdk.*; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.exception.CannotUnwrapDataKeyException; +import com.amazonaws.encryptionsdk.internal.Constants; +import com.amazonaws.encryptionsdk.internal.TrailingSignatureAlgorithm; +import com.amazonaws.encryptionsdk.model.*; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.amazonaws.encryptionsdk.internal.Utils.assertNonNull; + +public class V2DefaultCryptoMaterialsManager implements CryptoMaterialsManager { + private final MasterKeyProvider mkp; + + private final CryptoAlgorithm DEFAULT_CRYPTO_ALGORITHM = + CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384; + + /** @param mkp The master key provider to delegate to */ + public V2DefaultCryptoMaterialsManager(MasterKeyProvider mkp) { + assertNonNull(mkp, "mkp"); + this.mkp = mkp; + } + + @Override + public EncryptionMaterials getMaterialsForEncrypt(EncryptionMaterialsRequest request) { + Map context = request.getContext(); + + CryptoAlgorithm algo = request.getRequestedAlgorithm(); + CommitmentPolicy commitmentPolicy = request.getCommitmentPolicy(); + // Set default according to commitment policy + if (algo == null && commitmentPolicy == CommitmentPolicy.ForbidEncryptAllowDecrypt) { + algo = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384; + } else if (algo == null) { + algo = CryptoAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384; + } + + KeyPair trailingKeys = null; + if (algo.getTrailingSignatureLength() > 0) { + try { + trailingKeys = generateTrailingSigKeyPair(algo); + if (context.containsKey(Constants.EC_PUBLIC_KEY_FIELD)) { + throw new IllegalArgumentException( + "EncryptionContext contains reserved field " + Constants.EC_PUBLIC_KEY_FIELD); + } + // make mutable + context = new HashMap<>(context); + context.put(Constants.EC_PUBLIC_KEY_FIELD, serializeTrailingKeyForEc(algo, trailingKeys)); + } catch (final GeneralSecurityException ex) { + throw new AwsCryptoException(ex); + } + } + + final MasterKeyRequest.Builder mkRequestBuilder = MasterKeyRequest.newBuilder(); + mkRequestBuilder.setEncryptionContext(context); + + mkRequestBuilder.setStreaming(request.getPlaintextSize() == -1); + if (request.getPlaintext() != null) { + mkRequestBuilder.setPlaintext(request.getPlaintext()); + } else { + mkRequestBuilder.setSize(request.getPlaintextSize()); + } + + @SuppressWarnings("unchecked") + final List mks = + (List) + assertNonNull(mkp, "provider").getMasterKeysForEncryption(mkRequestBuilder.build()); + + if (mks.isEmpty()) { + throw new IllegalArgumentException("No master keys provided"); + } + + DataKey dataKey = mks.get(0).generateDataKey(algo, context); + + List keyBlobs = new ArrayList<>(mks.size()); + keyBlobs.add(new KeyBlob(dataKey)); + + for (int i = 1; i < mks.size(); i++) { + //noinspection unchecked + keyBlobs.add(new KeyBlob(mks.get(i).encryptDataKey(algo, context, dataKey))); + } + + //noinspection unchecked + return EncryptionMaterials.newBuilder() + .setAlgorithm(algo) + .setCleartextDataKey(dataKey.getKey()) + .setEncryptedDataKeys(keyBlobs) + .setEncryptionContext(context) + .setTrailingSignatureKey(trailingKeys == null ? null : trailingKeys.getPrivate()) + .setMasterKeys(mks) + .build(); + } + + @Override + public DecryptionMaterials decryptMaterials(DecryptionMaterialsRequest request) { + DataKey dataKey = + mkp.decryptDataKey( + request.getAlgorithm(), request.getEncryptedDataKeys(), request.getEncryptionContext()); + + if (dataKey == null) { + throw new CannotUnwrapDataKeyException("Could not decrypt any data keys"); + } + + PublicKey pubKey = null; + if (request.getAlgorithm().getTrailingSignatureLength() > 0) { + try { + String serializedPubKey = request.getEncryptionContext().get(Constants.EC_PUBLIC_KEY_FIELD); + + if (serializedPubKey == null) { + throw new AwsCryptoException("Missing trailing signature public key"); + } + + pubKey = deserializeTrailingKeyFromEc(request.getAlgorithm(), serializedPubKey); + } catch (final IllegalStateException ex) { + throw new AwsCryptoException(ex); + } + } else if (request.getEncryptionContext().containsKey(Constants.EC_PUBLIC_KEY_FIELD)) { + throw new AwsCryptoException("Trailing signature public key found for non-signed algorithm"); + } + + return DecryptionMaterials.newBuilder() + .setDataKey(dataKey) + .setTrailingSignatureKey(pubKey) + .build(); + } + + private PublicKey deserializeTrailingKeyFromEc(CryptoAlgorithm algo, String pubKey) { + return TrailingSignatureAlgorithm.forCryptoAlgorithm(algo).deserializePublicKey(pubKey); + } + + private static String serializeTrailingKeyForEc(CryptoAlgorithm algo, KeyPair trailingKeys) { + return TrailingSignatureAlgorithm.forCryptoAlgorithm(algo) + .serializePublicKey(trailingKeys.getPublic()); + } + + private static KeyPair generateTrailingSigKeyPair(CryptoAlgorithm algo) + throws GeneralSecurityException { + return TrailingSignatureAlgorithm.forCryptoAlgorithm(algo).generateKey(); + } +} From 5f491f51361d0880612fe18bd6a90657760ace77 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Mon, 10 Jun 2024 15:52:39 -0700 Subject: [PATCH 07/19] wip --- pom.xml | 4 +- .../crypto/examples/v2/CustomCMMExample.java | 33 +- .../amazonaws/encryptionsdk/CMMHandler.java | 34 +- .../v2/V2DefaultCryptoMaterialsManager.java | 23 +- .../encryptionsdk/AllTestsSuite.java | 2 - .../encryptionsdk/TestVectorGenerator.java | 570 -------------- .../encryptionsdk/TestVectorRunner.java | 743 ------------------ 7 files changed, 70 insertions(+), 1339 deletions(-) delete mode 100644 src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java delete mode 100644 src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java diff --git a/pom.xml b/pom.xml index fa3835da..060ee735 100644 --- a/pom.xml +++ b/pom.xml @@ -149,8 +149,8 @@ maven-compiler-plugin 3.10.1 - 1.8 - 1.8 + 8 + 8 diff --git a/src/examples/java/com/amazonaws/crypto/examples/v2/CustomCMMExample.java b/src/examples/java/com/amazonaws/crypto/examples/v2/CustomCMMExample.java index 138d1465..fad8e44b 100644 --- a/src/examples/java/com/amazonaws/crypto/examples/v2/CustomCMMExample.java +++ b/src/examples/java/com/amazonaws/crypto/examples/v2/CustomCMMExample.java @@ -3,7 +3,12 @@ package com.amazonaws.crypto.examples.v2; -import com.amazonaws.encryptionsdk.*; +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.CommitmentPolicy; +import com.amazonaws.encryptionsdk.CryptoMaterialsManager; +import com.amazonaws.encryptionsdk.CryptoResult; +import com.amazonaws.encryptionsdk.DefaultCryptoMaterialsManager; +import com.amazonaws.encryptionsdk.MasterKeyProvider; import com.amazonaws.encryptionsdk.kmssdkv2.KmsMasterKeyProvider; import com.amazonaws.encryptionsdk.model.DecryptionMaterials; import com.amazonaws.encryptionsdk.model.DecryptionMaterialsRequest; @@ -13,9 +18,20 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.Map; +/** + *

+ * Creates a custom implementation of the CryptoMaterialsManager interface, + * then uses that implementation to encrypt and decrypt a file using an AWS KMS CMK. + * + *

+ * Arguments: + *

    + *
  1. Key ARN: For help finding the Amazon Resource Name (ARN) of your AWS KMS customer master + * key (CMK), see 'Viewing Keys' at http://docs.aws.amazon.com/kms/latest/developerguide/viewing-keys.html + *
+ */ public class CustomCMMExample { private static final byte[] EXAMPLE_DATA = "Hello World".getBytes(StandardCharsets.UTF_8); @@ -68,17 +84,13 @@ static void encryptAndDecryptWithCMM(final CryptoMaterialsManager cmm) { assert Arrays.equals(decryptResult.getResult(), EXAMPLE_DATA); } + // Custom CMM implementation. + // This CMM only allows encryption/decryption using signing algorithms. + // It wraps an underlying CMM implementation and checks its materials + // to ensure that it is only using signed encryption algorithms. public static class SigningSuiteOnlyCMM implements CryptoMaterialsManager { - private final HashSet approvedAlgorithms = new HashSet<>(Arrays.asList( - CryptoAlgorithm.ALG_AES_128_GCM_IV12_TAG16_HKDF_SHA256_ECDSA_P256, - CryptoAlgorithm.ALG_AES_192_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, - CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, - CryptoAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY - )); - // The underlying CMM. - // The SigningSuiteOnlyCMM will ensure that any operations TODO private CryptoMaterialsManager underlyingCMM; // If only a MasterKeyProvider is constructed, the underlying CMM is the default CMM. @@ -86,6 +98,7 @@ public SigningSuiteOnlyCMM(MasterKeyProvider mkp) { this.underlyingCMM = new DefaultCryptoMaterialsManager(mkp); } + // This CMM can wrap any other CMM implementation. public SigningSuiteOnlyCMM(CryptoMaterialsManager underlyingCMM) { this.underlyingCMM = underlyingCMM; } diff --git a/src/main/java/com/amazonaws/encryptionsdk/CMMHandler.java b/src/main/java/com/amazonaws/encryptionsdk/CMMHandler.java index 3cc4b2a5..740be509 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/CMMHandler.java +++ b/src/main/java/com/amazonaws/encryptionsdk/CMMHandler.java @@ -4,7 +4,12 @@ package com.amazonaws.encryptionsdk; import com.amazonaws.encryptionsdk.internal.Utils; -import com.amazonaws.encryptionsdk.model.*; +import com.amazonaws.encryptionsdk.model.DecryptionMaterials; +import com.amazonaws.encryptionsdk.model.DecryptionMaterialsHandler; +import com.amazonaws.encryptionsdk.model.DecryptionMaterialsRequest; +import com.amazonaws.encryptionsdk.model.EncryptionMaterialsHandler; +import com.amazonaws.encryptionsdk.model.EncryptionMaterialsRequest; +import com.amazonaws.encryptionsdk.model.KeyBlob; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -68,17 +73,24 @@ public DecryptionMaterialsHandler decryptMaterials( // and we are using the legacy native CMM, // add the encryptionContext to the materials. // - // ESDK-Java 3.0 changed the expected behavior for CMMs. - // After 3.0, the ESDK assumes that CMMs' implementations of decryptMaterials - // will return DecryptionMaterials with a set encryptionContext attribute. - // The default CMM's behavior was changed in 3.0 to set the encryptionContext attribute - // with the value from the ciphertext's headers. - // However, this assumption is not true for custom CMMs from earlier ESDK versions. + // ESDK-Java 3.0 changed internals of decrypt behavior, + // This code makes earlier CMM implementations compatible with post-3.0 behavior. // - // After 3.0, CMMs MUST set the encryptionContext on the returned decryptMaterials. - // If the materials' encryptionContext is empty but the request's is not, - // we assume the CMM never set the decryptMaterials' encryptionContext attribute - // and set its attribute from the request. + // Version 3.0 assumes that CMMs' implementations of decryptMaterials + // will set an encryptionContext attribute on returned DecryptionMaterials. + // The DefaultCryptoMaterialsManager's behavior was changed in 3.0. + // It now sets the encryptionContext attribute with the value from the ciphertext's headers. + // + // But custom CMMs' behavior was not updated. + // However, there is no custom CMM before version 3.0 that could set an encryptionContext attribute. + // The encryptionContext attribute was only introduced to decryptMaterials objects + // in ESDK 3.0, so no CMM could have set this attribute before 3.0. + // As a result, the ESDK assumes that any legacy native CMM + // that does not add encryptionContext to its decryptMaterials + // SHOULD add encryptionContext to its decryptMaterials. + // + // If a custom CMM implementation conflicts with this assumption. + // that CMM implementation MUST move to the MPL. materials = materials.toBuilder() .setEncryptionContext(request.getEncryptionContext()) .build(); diff --git a/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java b/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java index c1154625..b0303a73 100644 --- a/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java +++ b/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java @@ -1,6 +1,12 @@ package com.amazonaws.crypto.examples.v2; -import com.amazonaws.encryptionsdk.*; +import com.amazonaws.encryptionsdk.CommitmentPolicy; +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.CryptoMaterialsManager; +import com.amazonaws.encryptionsdk.DataKey; +import com.amazonaws.encryptionsdk.MasterKey; +import com.amazonaws.encryptionsdk.MasterKeyProvider; +import com.amazonaws.encryptionsdk.MasterKeyRequest; import com.amazonaws.encryptionsdk.exception.AwsCryptoException; import com.amazonaws.encryptionsdk.exception.CannotUnwrapDataKeyException; import com.amazonaws.encryptionsdk.internal.Constants; @@ -17,6 +23,21 @@ import static com.amazonaws.encryptionsdk.internal.Utils.assertNonNull; +/* + This is a copy-paste of the DefaultCryptoMaterialsManager implementation + from the final commit of the V2 ESDK: 1870a082358d59e32c60d74116d6f43c0efa466b + ESDK V3 implicitly changed the contract between CMMs and the ESDK. + After V3, DecryptMaterials has an `encryptionContext` attribute, + and CMMs are expected to set this attribute. + The V3 commit modified this DefaultCMM's `decryptMaterials` implementation + to set encryptionContext on returned DecryptionMaterials objects. + However, there are custom implementations of the legacy native CMM + that do not set encryptionContext. + This CMM is used to explicitly assert that the V2 implementation of + the DefaultCMM is compatible with V3 logic, + which implicitly asserts that custom implementations of V2-compatible CMMs + are also compatible with V3 logic. + */ public class V2DefaultCryptoMaterialsManager implements CryptoMaterialsManager { private final MasterKeyProvider mkp; diff --git a/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java b/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java index 445f77d2..ce7325b4 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java +++ b/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java @@ -80,8 +80,6 @@ AwsCryptoTest.class, CryptoInputStreamTest.class, CryptoOutputStreamTest.class, - TestVectorRunner.class, - TestVectorGenerator.class, XCompatDecryptTest.class, DefaultCryptoMaterialsManagerTest.class, NullCryptoMaterialsCacheTest.class, diff --git a/src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java b/src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java deleted file mode 100644 index eb2b1764..00000000 --- a/src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java +++ /dev/null @@ -1,570 +0,0 @@ -// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.amazonaws.encryptionsdk; - -import static java.lang.String.format; - -import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; -import com.amazonaws.encryptionsdk.jce.JceMasterKey; -import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; -import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; -import java.net.URL; -import java.nio.ByteBuffer; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.nio.file.attribute.FileAttribute; -import java.security.GeneralSecurityException; -import java.security.Key; -import java.security.KeyFactory; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.concurrent.Callable; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import org.apache.commons.io.FileUtils; -import org.bouncycastle.util.encoders.Base64; -import org.junit.AfterClass; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import software.amazon.awssdk.utils.ImmutableMap; -import software.amazon.cryptography.materialproviders.IKeyring; -import software.amazon.cryptography.materialproviders.MaterialProviders; -import software.amazon.cryptography.materialproviders.model.CreateMultiKeyringInput; -import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig; -import software.amazon.cryptography.materialproviderstestvectorkeys.KeyVectors; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionInput; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionOutput; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.KeyVectorsConfig; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.TestVectorKeyringInput; - -@RunWith(Parameterized.class) -public class TestVectorGenerator { - - private static final String encryptManifestList = - "https://raw.githubusercontent.com/awslabs/aws-crypto-tools-test-vector-framework/master/features/CANONICAL-GENERATED-MANIFESTS/0003-awses-message-encryption.v2.json"; - // We save the files in memory to avoid repeatedly retrieving them. This won't work if the - // plaintexts are too - // large or numerous - private static final Map cachedData = new HashMap<>(); - private static final ObjectMapper mapper = new ObjectMapper(); - private static EncryptionInterface encryption; - private static boolean isMasterKey; - private final String testName; - private final TestCase testCase; - - // Temp Test Vectors Directory - private static String tempTestVectorPath; - // Zip File Path - private static String zipFilePath; - - public TestVectorGenerator(final String testName, TestCase testCase) { - this.testName = testName; - this.testCase = testCase; - } - - // Zip Temp Folder and delete temp files - @AfterClass - public static void zip() throws IOException { - Path zipFile = Files.createFile(Paths.get(zipFilePath)); - - Path sourceDirPath = Paths.get(tempTestVectorPath); - try (ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(zipFile)); - Stream paths = Files.walk(sourceDirPath)) { - paths - .filter(path -> !Files.isDirectory(path)) - .forEach( - path -> { - ZipEntry zipEntry = new ZipEntry(sourceDirPath.relativize(path).toString()); - try { - zipOutputStream.putNextEntry(zipEntry); - Files.copy(path, zipOutputStream); - zipOutputStream.closeEntry(); - } catch (IOException e) { - throw new UncheckedIOException("Unable to Zip File", e); - } - }); - } - FileUtils.deleteQuietly(sourceDirPath.toFile()); - - // Teardown - cachedData.clear(); - } - - @Test - @SuppressWarnings("unchecked") - public void encrypt() throws Exception { - CryptoAlgorithm cryptoAlgorithm = getCryptoAlgorithm(testCase.algorithmId); - CommitmentPolicy commitmentPolicy = - cryptoAlgorithm.isCommitting() - ? CommitmentPolicy.RequireEncryptRequireDecrypt - : CommitmentPolicy.ForbidEncryptAllowDecrypt; - - AwsCrypto crypto = - AwsCrypto.builder() - .withCommitmentPolicy(commitmentPolicy) - .withEncryptionAlgorithm(cryptoAlgorithm) - .withEncryptionFrameSize(testCase.frameSize) - .build(); - - Callable ciphertext; - if (isMasterKey) { - ciphertext = - () -> - crypto - .encryptData( - testCase.masterKey, - cachedData.get(testCase.plaintext), - testCase.encryptionContext) - .getResult(); - } else { - ciphertext = - () -> - crypto - .encryptData( - testCase.keyring, - cachedData.get(testCase.plaintext), - testCase.encryptionContext) - .getResult(); - } - Files.write(Paths.get(tempTestVectorPath + "ciphertexts/" + testName), ciphertext.call()); - } - - private static CryptoAlgorithm getCryptoAlgorithm(String algorithmId) { - Integer algId = Integer.parseInt(algorithmId, 16); - for (CryptoAlgorithm cryptoAlgorithm : CryptoAlgorithm.values()) { - if (cryptoAlgorithm.getValue() == algId) { - return cryptoAlgorithm; - } - } - throw new IllegalArgumentException("Invalid AlgorithmId: " + algorithmId); - } - - @Parameterized.Parameters(name = "Compatibility Test: {0} - {1}") - @SuppressWarnings("unchecked") - public static Collection data() throws Exception { - final String interfaceOption = System.getProperty("masterkey"); - - if (interfaceOption != null && interfaceOption.equals("true")) { - isMasterKey = true; - encryption = EncryptionInterface.EncryptWithMasterKey; - } else { - encryption = EncryptionInterface.EncryptWithKeyring; - } - - final String encryptKeyManifest = System.getProperty("keysManifest"); - if (encryptKeyManifest == null) { - return Collections.emptyList(); - } - - zipFilePath = System.getProperty("zipFilePath"); - if (zipFilePath == null) { - return Collections.emptyList(); - } - - tempTestVectorPath = Files.createTempDirectory("java", new FileAttribute[0]).toString() + "/"; - createDirectories(tempTestVectorPath + "ciphertexts/"); - createDirectories(tempTestVectorPath + "plaintexts/"); - - File decryptManifest = new File(tempTestVectorPath + "manifest.json"); - File keyManifest = new File(tempTestVectorPath + "keys.json"); - - final Map manifest = mapper.readValue(new URL(encryptManifestList), Map.class); - mapper - .writerWithDefaultPrettyPrinter() - .writeValue(decryptManifest, createDecryptManifest(manifest)); - - try (InputStream in = new FileInputStream(encryptKeyManifest)) { - Files.copy(in, keyManifest.toPath(), StandardCopyOption.REPLACE_EXISTING); - } - - final Map keysManifest = - mapper.readValue(new File(encryptKeyManifest), Map.class); - - cachePlaintext((Map) manifest.get("plaintexts")); - - MaterialProvidersConfig config = MaterialProvidersConfig.builder().build(); - MaterialProviders materialProviders = - MaterialProviders.builder().MaterialProvidersConfig(config).build(); - KeyVectors keyVectors = - KeyVectors.builder() - .KeyVectorsConfig( - KeyVectorsConfig.builder().keyManifiestPath(keyManifest.toString()).build()) - .build(); - - final Map keys = parseKeyManifest(keysManifest); - final KmsMasterKeyProvider kmsProv = - KmsMasterKeyProvider.builder() - .withCredentials(new DefaultAWSCredentialsProviderChain()) - .buildDiscovery(); - - return ((Map>) manifest.get("tests")) - .entrySet().stream() - .map( - entry -> { - String testName = entry.getKey(); - TestCase testCase = - encryption.parseTest(entry, keys, kmsProv, materialProviders, keyVectors); - return new Object[] {testName, testCase}; - }) - .collect(Collectors.toList()); - } - - private static void createDirectories(String path) { - File directory = new File(path); - directory.mkdirs(); - } - - private enum EncryptionInterface { - EncryptWithMasterKey { - @Override - public TestCase parseTest( - Map.Entry> testEntry, - Map keys, - KmsMasterKeyProvider kmsProv, - MaterialProviders materialProviders, - KeyVectors keyVectors) { - return parseTestWithMasterkeys(testEntry, keys, kmsProv); - } - }, - EncryptWithKeyring { - @Override - public TestCase parseTest( - Map.Entry> testEntry, - Map keys, - KmsMasterKeyProvider kmsProv, - MaterialProviders materialProviders, - KeyVectors keyVectors) { - return parseTestWithKeyrings(testEntry, materialProviders, keyVectors); - } - }; - - public abstract TestCase parseTest( - Map.Entry> testEntry, - Map keys, - KmsMasterKeyProvider kmsProv, - MaterialProviders materialProviders, - KeyVectors keyVectors); - } - - private static void cachePlaintext(Map plaintexts) { - Random rd = new Random(); - plaintexts.forEach( - (key, value) -> { - byte[] plaintext = new byte[value]; - rd.nextBytes(plaintext); - try { - Files.write(new File(tempTestVectorPath + "plaintexts/" + key).toPath(), plaintext); - cachedData.put(key, plaintext); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); - } - - private static Map createDecryptManifest(Map encryptManifest) { - Map decryptManifest = new LinkedHashMap<>(); - - decryptManifest.put("manifest", ImmutableMap.of("type", "awses-decrypt", "version", 2)); - - decryptManifest.put( - "client", ImmutableMap.of("name", "aws/aws-encryption-sdk-java", "version", "2.2.0")); - - decryptManifest.put("keys", "file://keys.json"); - - Map> testScenarios = - ((LinkedHashMap>) encryptManifest.get("tests")) - .entrySet().stream() - .collect( - Collectors.toMap( - Map.Entry::getKey, - entry -> { - Map scenario = entry.getValue(); - return new LinkedHashMap() { - { - put("ciphertext", "file://ciphertexts/" + entry.getKey()); - put("master-keys", scenario.get("master-keys")); - put( - "result", - Collections.singletonMap( - "output", - Collections.singletonMap( - "plaintext", - "file://plaintexts/" + scenario.get("plaintext")))); - } - }; - })); - - decryptManifest.put("tests", testScenarios); - return decryptManifest; - } - - private static TestCase parseTestWithMasterkeys( - Map.Entry> testEntry, - Map keys, - KmsMasterKeyProvider kmsProv) { - - String testName = testEntry.getKey(); - Map data = testEntry.getValue(); - - String plaintext = (String) data.get("plaintext"); - String algorithmId = (String) data.get("algorithm"); - int frameSize = (int) data.get("frame-size"); - Map encryptionContext = (Map) data.get("encryption-context"); - - final List> mks = new ArrayList<>(); - - for (Map mkEntry : (List>) data.get("master-keys")) { - if (mkEntry.get("key").equals("rsa-4096-private")) { - mkEntry.replace("key", "rsa-4096-public"); - } - - final String type = mkEntry.get("type"); - final String keyName = mkEntry.get("key"); - final KeyEntry key = keys.get(keyName); - - if ("aws-kms".equals(type)) { - mks.add(kmsProv.getMasterKey(key.keyId)); - } else if ("raw".equals(type)) { - final String provId = mkEntry.get("provider-id"); - final String algorithm = mkEntry.get("encryption-algorithm"); - if ("aes".equals(algorithm)) { - mks.add( - JceMasterKey.getInstance( - (SecretKey) key.key, provId, key.keyId, "AES/GCM/NoPadding")); - } else if ("rsa".equals(algorithm)) { - String transformation = "RSA/ECB/"; - final String padding = mkEntry.get("padding-algorithm"); - if ("pkcs1".equals(padding)) { - transformation += "PKCS1Padding"; - } else if ("oaep-mgf1".equals(padding)) { - final String hashName = - mkEntry.get("padding-hash").replace("sha", "sha-").toUpperCase(); - transformation += "OAEPWith" + hashName + "AndMGF1Padding"; - } else { - throw new IllegalArgumentException("Unsupported padding:" + padding); - } - final PublicKey wrappingKey; - final PrivateKey unwrappingKey; - if (key.key instanceof PublicKey) { - wrappingKey = (PublicKey) key.key; - unwrappingKey = null; - } else { - wrappingKey = null; - unwrappingKey = (PrivateKey) key.key; - } - mks.add( - JceMasterKey.getInstance( - wrappingKey, unwrappingKey, provId, key.keyId, transformation)); - } else { - throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); - } - } else { - throw new IllegalArgumentException("Unsupported Key Type: " + type); - } - } - - MasterKeyProvider multiProvider = MultipleProviderFactory.buildMultiProvider(mks); - - return new TestCase( - testName, null, multiProvider, plaintext, algorithmId, frameSize, encryptionContext); - } - - private static TestCase parseTestWithKeyrings( - Map.Entry> testEntry, - MaterialProviders materialProviders, - KeyVectors keyVectors) { - String testName = testEntry.getKey(); - Map data = testEntry.getValue(); - - String plaintext = (String) data.get("plaintext"); - String algorithmId = (String) data.get("algorithm"); - int frameSize = (int) data.get("frame-size"); - Map encryptionContext = (Map) data.get("encryption-context"); - - List keyrings = new ArrayList<>(); - - ((List>) data.get("master-keys")) - .forEach( - mkEntry -> { - if (mkEntry.get("type").equals("raw") - && mkEntry.get("encryption-algorithm").equals("rsa")) { - if (mkEntry.get("key").equals("rsa-4096-private")) { - mkEntry.replace("key", "rsa-4096-public"); - } - mkEntry.putIfAbsent("padding-hash", "sha1"); - } - - try { - byte[] json = new ObjectMapper().writeValueAsBytes(mkEntry); - GetKeyDescriptionOutput output = - keyVectors.GetKeyDescription( - GetKeyDescriptionInput.builder().json(ByteBuffer.wrap(json)).build()); - - IKeyring testVectorKeyring = - keyVectors.CreateTestVectorKeyring( - TestVectorKeyringInput.builder() - .keyDescription(output.keyDescription()) - .build()); - - keyrings.add(testVectorKeyring); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - }); - - IKeyring primary = keyrings.remove(0); - IKeyring multiKeyring = - materialProviders.CreateMultiKeyring( - CreateMultiKeyringInput.builder().generator(primary).childKeyrings(keyrings).build()); - - return new TestCase( - testName, multiKeyring, null, plaintext, algorithmId, frameSize, encryptionContext); - } - - @SuppressWarnings("unchecked") - private static Map parseKeyManifest(final Map keysManifest) - throws GeneralSecurityException { - // check our type - final Map metaData = (Map) keysManifest.get("manifest"); - if (!"keys".equals(metaData.get("type"))) { - throw new IllegalArgumentException("Invalid manifest type: " + metaData.get("type")); - } - if (!Integer.valueOf(3).equals(metaData.get("version"))) { - throw new IllegalArgumentException("Invalid manifest version: " + metaData.get("version")); - } - - final Map result = new HashMap<>(); - - Map keys = (Map) keysManifest.get("keys"); - for (Map.Entry entry : keys.entrySet()) { - final String name = entry.getKey(); - final Map data = (Map) entry.getValue(); - - final String keyType = (String) data.get("type"); - final String encoding = (String) data.get("encoding"); - final String keyId = (String) data.get("key-id"); - final String material = (String) data.get("material"); // May be null - final String algorithm = (String) data.get("algorithm"); // May be null - - final KeyEntry keyEntry; - - final KeyFactory kf; - switch (keyType) { - case "symmetric": - if (!"base64".equals(encoding)) { - throw new IllegalArgumentException( - format("Key %s is symmetric but has encoding %s", keyId, encoding)); - } - keyEntry = - new KeyEntry( - name, - keyId, - keyType, - new SecretKeySpec(Base64.decode(material), algorithm.toUpperCase())); - break; - case "private": - kf = KeyFactory.getInstance(algorithm); - if (!"pem".equals(encoding)) { - throw new IllegalArgumentException( - format("Key %s is private but has encoding %s", keyId, encoding)); - } - byte[] pkcs8Key = parsePem(material); - keyEntry = - new KeyEntry( - name, keyId, keyType, kf.generatePrivate(new PKCS8EncodedKeySpec(pkcs8Key))); - break; - case "public": - kf = KeyFactory.getInstance(algorithm); - if (!"pem".equals(encoding)) { - throw new IllegalArgumentException( - format("Key %s is private but has encoding %s", keyId, encoding)); - } - byte[] x509Key = parsePem(material); - keyEntry = - new KeyEntry( - name, keyId, keyType, kf.generatePublic(new X509EncodedKeySpec(x509Key))); - break; - case "aws-kms": - keyEntry = new KeyEntry(name, keyId, keyType, null); - break; - default: - throw new IllegalArgumentException("Unsupported key type: " + keyType); - } - - result.put(name, keyEntry); - } - - return result; - } - - private static byte[] parsePem(String pem) { - final String stripped = pem.replaceAll("-+[A-Z ]+-+", ""); - return Base64.decode(stripped); - } - - private static class KeyEntry { - final String name; - final String keyId; - final String type; - final Key key; - - private KeyEntry(String name, String keyId, String type, Key key) { - this.name = name; - this.keyId = keyId; - this.type = type; - this.key = key; - } - } - - private static class TestCase { - private final String name; - private final IKeyring keyring; - private final MasterKeyProvider masterKey; - private final String plaintext; - private final String algorithmId; - private final int frameSize; - private final Map encryptionContext; - - public TestCase( - String name, - IKeyring keyring, - MasterKeyProvider multiProvider, - String plaintext, - String algorithmId, - int frameSize, - Map encryptionContext) { - this.name = name; - this.keyring = keyring; - this.masterKey = multiProvider; - this.plaintext = plaintext; - this.algorithmId = algorithmId; - this.frameSize = frameSize; - this.encryptionContext = encryptionContext; - } - } -} diff --git a/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java b/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java deleted file mode 100644 index 8c0be82f..00000000 --- a/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java +++ /dev/null @@ -1,743 +0,0 @@ -// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.amazonaws.encryptionsdk; - -import static java.lang.String.format; - -import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; -import com.amazonaws.encryptionsdk.internal.SignaturePolicy; -import com.amazonaws.encryptionsdk.jce.JceMasterKey; -import com.amazonaws.encryptionsdk.kms.AwsKmsMrkAwareMasterKeyProvider; -import com.amazonaws.encryptionsdk.kms.DiscoveryFilter; -import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; -import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory; -import com.amazonaws.util.IOUtils; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.JarURLConnection; -import java.net.URL; -import java.nio.ByteBuffer; -import java.security.GeneralSecurityException; -import java.security.Key; -import java.security.KeyFactory; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.Callable; -import java.util.function.Supplier; -import java.util.jar.JarFile; -import java.util.zip.ZipEntry; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import junit.framework.TestCase; -import org.bouncycastle.util.encoders.Base64; -import org.junit.AfterClass; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import software.amazon.awssdk.regions.Region; -import software.amazon.cryptography.materialproviders.ICryptographicMaterialsManager; -import software.amazon.cryptography.materialproviders.IKeyring; -import software.amazon.cryptography.materialproviders.MaterialProviders; -import software.amazon.cryptography.materialproviders.model.CreateDefaultCryptographicMaterialsManagerInput; -import software.amazon.cryptography.materialproviders.model.CreateMultiKeyringInput; -import software.amazon.cryptography.materialproviders.model.CreateRequiredEncryptionContextCMMInput; -import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig; -import software.amazon.cryptography.materialproviderstestvectorkeys.KeyVectors; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionInput; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionOutput; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.KeyVectorsConfig; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.TestVectorKeyringInput; - -@RunWith(Parameterized.class) -public class TestVectorRunner { - - // TODO: Standardize Manifest Version - private static final List MANIFEST_VERSIONS = Arrays.asList(2, 4); - - // We save the files in memory to avoid repeatedly retrieving them. This won't work if the - // plaintexts are too - // large or numerous - private static final Map cachedData = new HashMap<>(); - - private final String testName; - private final TestCase testCase; - private final DecryptionMethod decryptionMethod; - - public TestVectorRunner( - final String testName, TestCase testCase, DecryptionMethod decryptionMethod) { - this.testName = testName; - this.testCase = testCase; - this.decryptionMethod = decryptionMethod; - } - - @Test - public void decrypt() throws Exception { - AwsCrypto crypto = - AwsCrypto.builder() - .withCommitmentPolicy(CommitmentPolicy.ForbidEncryptAllowDecrypt) - .build(); - Callable decryptor; - if (testCase.isKeyring) { - decryptor = - () -> - decryptionMethod.decryptMessage( - crypto, - testCase.cmmSupplier.get(), - cachedData.get(testCase.ciphertextPath), - testCase.encryptionContext); - } else { - decryptor = - () -> - decryptionMethod.decryptMessage( - crypto, testCase.mkpSupplier.get(), cachedData.get(testCase.ciphertextPath)); - } - testCase.matcher.Match(decryptor); - } - - @Parameterized.Parameters(name = "Compatibility Test: {0} - {3}") - @SuppressWarnings("unchecked") - public static Collection data() throws Exception { - final String zipPath = System.getProperty("testVectorZip"); - final String interfaceOption = System.getProperty("masterkey"); - if (zipPath == null) { - return Collections.emptyList(); - } - - final JarURLConnection jarConnection = - (JarURLConnection) new URL("jar:" + zipPath + "!/").openConnection(); - - try (JarFile jar = jarConnection.getJarFile()) { - - final Map manifest = readJsonMapFromJar(jar, "manifest.json"); - final Map keysManifest = readJsonMapFromJar(jar, "keys.json"); - - ObjectMapper objectMapper = new ObjectMapper(); - - // Create a temporary file and write the JSON string to it - File tempFile = File.createTempFile("keys", ".json"); - objectMapper.writeValue(tempFile, keysManifest); - - final Map metaData = (Map) manifest.get("manifest"); - - // We only support "awses-decrypt" type manifests right now - if (!"awses-decrypt".equals(metaData.get("type"))) { - throw new IllegalArgumentException("Unsupported manifest type: " + metaData.get("type")); - } - Integer readVersion = (Integer) metaData.get("version"); - if (!MANIFEST_VERSIONS.contains(readVersion)) { - throw new IllegalArgumentException( - "Unsupported manifest version: " + metaData.get("version")); - } - - final Map keys = - parseKeyManifest(readJsonMapFromJar(jar, (String) manifest.get("keys"))); - - KeyVectors keyVectors = - KeyVectors.builder() - .KeyVectorsConfig( - KeyVectorsConfig.builder().keyManifiestPath(tempFile.getPath()).build()) - .build(); - - MaterialProvidersConfig config = MaterialProvidersConfig.builder().build(); - MaterialProviders materialProviders = - MaterialProviders.builder().MaterialProvidersConfig(config).build(); - - final KmsMasterKeyProvider kmsProvV1 = - KmsMasterKeyProvider.builder() - .withCredentials(new DefaultAWSCredentialsProviderChain()) - .buildDiscovery(); - - final com.amazonaws.encryptionsdk.kmssdkv2.KmsMasterKeyProvider kmsProvV2 = - com.amazonaws.encryptionsdk.kmssdkv2.KmsMasterKeyProvider.builder().buildDiscovery(); - - List testCases = new ArrayList<>(); - for (Map.Entry> testEntry : - ((Map>) manifest.get("tests")).entrySet()) { - if (interfaceOption != null && interfaceOption.equals("true")) { - String testName = testEntry.getKey(); - - TestCase testCaseV1 = - parseTest(testEntry.getKey(), testEntry.getValue(), keys, jar, kmsProvV1); - TestCase testCaseV2 = - parseTest(testEntry.getKey(), testEntry.getValue(), keys, jar, kmsProvV2); - - for (DecryptionMethod decryptionMethod : DecryptionMethod.values()) { - if (testCaseV1.signaturePolicy.equals(decryptionMethod.signaturePolicy())) { - testCases.add(new Object[] {testName, testCaseV1, decryptionMethod}); - testCases.add(new Object[] {testName + "-V2", testCaseV2, decryptionMethod}); - } - } - } else { - String testName = testEntry.getKey(); - TestCase testCaseKeyring = - parseTest( - testEntry.getKey(), - testEntry.getValue(), - keys, - jar, - materialProviders, - keyVectors); - - for (DecryptionMethod decryptionMethod : DecryptionMethod.values()) { - if (testCaseKeyring.signaturePolicy.equals(decryptionMethod.signaturePolicy())) { - testCases.add( - new Object[] {testName + "-Keyrings", testCaseKeyring, decryptionMethod}); - } - } - } - } - return testCases; - } - } - - @AfterClass - public static void teardown() { - cachedData.clear(); - } - - private static byte[] readBytesFromJar(JarFile jar, String fileName) throws IOException { - try (InputStream is = readFromJar(jar, fileName)) { - return IOUtils.toByteArray(is); - } - } - - private static Map readJsonMapFromJar(JarFile jar, String fileName) - throws IOException { - try (InputStream is = readFromJar(jar, fileName)) { - final ObjectMapper mapper = new ObjectMapper(); - return mapper.readValue(is, new TypeReference>() {}); - } - } - - private static InputStream readFromJar(JarFile jar, String name) throws IOException { - // Our manifest URIs incorrectly start with file:// rather than just file: so we need to strip - // this - ZipEntry entry = jar.getEntry(name.replaceFirst("^file://(?!/)", "")); - return jar.getInputStream(entry); - } - - private static void cacheData(JarFile jar, String url) throws IOException { - if (!cachedData.containsKey(url)) { - cachedData.put(url, readBytesFromJar(jar, url)); - } - } - - /** Parse Test to Keyring */ - @SuppressWarnings("unchecked") - private static TestCase parseTest( - String testName, - Map data, - Map keys, - JarFile jar, - MaterialProviders materialProviders, - KeyVectors keyVectors) - throws IOException { - final String ciphertextURL = (String) data.get("ciphertext"); - cacheData(jar, ciphertextURL); - - Supplier cmmSupplier = - () -> { - final List keyrings = new ArrayList<>(); - for (Map mkEntry : (List>) data.get("master-keys")) { - if (mkEntry.get("type").equals("raw") - && mkEntry.get("encryption-algorithm").equals("rsa")) { - mkEntry.putIfAbsent("padding-hash", "sha1"); - } - - try { - byte[] json = new ObjectMapper().writeValueAsBytes(mkEntry); - GetKeyDescriptionOutput output = - keyVectors.GetKeyDescription( - GetKeyDescriptionInput.builder().json(ByteBuffer.wrap(json)).build()); - - IKeyring testVectorKeyring = - keyVectors.CreateTestVectorKeyring( - TestVectorKeyringInput.builder() - .keyDescription(output.keyDescription()) - .build()); - - keyrings.add(testVectorKeyring); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - - IKeyring multiKeyring = - materialProviders.CreateMultiKeyring( - CreateMultiKeyringInput.builder() - .generator(keyrings.get(0)) - .childKeyrings(keyrings) - .build()); - - CreateDefaultCryptographicMaterialsManagerInput defaultInput = - CreateDefaultCryptographicMaterialsManagerInput.builder() - .keyring(multiKeyring) - .build(); - ICryptographicMaterialsManager cmm = - materialProviders.CreateDefaultCryptographicMaterialsManager(defaultInput); - if (data.containsKey("cmm") && data.get("cmm").equals("RequiredEncryptionContext")) { - List requiredKeys = new ArrayList(2); - requiredKeys.add("key1"); - requiredKeys.add("key2"); - CreateRequiredEncryptionContextCMMInput requiredCMMInput = - CreateRequiredEncryptionContextCMMInput.builder() - .underlyingCMM( - materialProviders.CreateDefaultCryptographicMaterialsManager(defaultInput)) - .requiredEncryptionContextKeys(requiredKeys) - .build(); - cmm = materialProviders.CreateRequiredEncryptionContextCMM(requiredCMMInput); - } - return cmm; - }; - @SuppressWarnings("unchecked") - final Map resultSpec = (Map) data.get("result"); - final ResultMatcher matcher = parseResultMatcher(jar, resultSpec); - - Map ec = Collections.emptyMap(); - - if (data.get("encryption-context") != null) { - ec = (Map) data.get("encryption-context"); - } - - String decryptionMethodSpec = (String) data.get("decryption-method"); - SignaturePolicy signaturePolicy = SignaturePolicy.AllowEncryptAllowDecrypt; - if (decryptionMethodSpec != null) { - if ("streaming-unsigned-only".equals(decryptionMethodSpec)) { - signaturePolicy = SignaturePolicy.AllowEncryptForbidDecrypt; - } else { - throw new IllegalArgumentException( - "Unsupported Decryption Method: " + decryptionMethodSpec); - } - } - - return new TestCase( - testName, ciphertextURL, true, null, cmmSupplier, ec, matcher, signaturePolicy); - } - - /** Parse Test to MasterKey for AWS SDK v1 */ - @SuppressWarnings("unchecked") - private static TestCase parseTest( - String testName, - Map data, - Map keys, - JarFile jar, - KmsMasterKeyProvider kmsProv) - throws IOException { - final String ciphertextURL = (String) data.get("ciphertext"); - cacheData(jar, ciphertextURL); - - Supplier> mkpSupplier = - () -> { - @SuppressWarnings("generic") - final List> mks = new ArrayList<>(); - - for (Map mkEntry : (List>) data.get("master-keys")) { - final String type = (String) mkEntry.get("type"); - final String keyName = (String) mkEntry.get("key"); - final KeyEntry key = keys.get(keyName); - - if ("aws-kms".equals(type)) { - mks.add(kmsProv.getMasterKey(key.keyId)); - } else if ("aws-kms-mrk-aware".equals(type)) { - AwsKmsMrkAwareMasterKeyProvider provider = - AwsKmsMrkAwareMasterKeyProvider.builder().buildStrict(key.keyId); - mks.add(provider.getMasterKey(key.keyId)); - } else if ("aws-kms-mrk-aware-discovery".equals(type)) { - final String defaultMrkRegion = (String) mkEntry.get("default-mrk-region"); - final Map discoveryFilterSpec = - (Map) mkEntry.get("aws-kms-discovery-filter"); - final DiscoveryFilter discoveryFilter; - if (discoveryFilterSpec != null) { - discoveryFilter = - new DiscoveryFilter( - (String) discoveryFilterSpec.get("partition"), - (List) discoveryFilterSpec.get("account-ids")); - } else { - discoveryFilter = null; - } - return AwsKmsMrkAwareMasterKeyProvider.builder() - .withDiscoveryMrkRegion(defaultMrkRegion) - .buildDiscovery(discoveryFilter); - } else if ("raw".equals(type)) { - final String provId = (String) mkEntry.get("provider-id"); - final String algorithm = (String) mkEntry.get("encryption-algorithm"); - if ("aes".equals(algorithm)) { - mks.add( - JceMasterKey.getInstance( - (SecretKey) key.key, provId, key.keyId, "AES/GCM/NoPadding")); - } else if ("rsa".equals(algorithm)) { - String transformation = "RSA/ECB/"; - final String padding = (String) mkEntry.get("padding-algorithm"); - if ("pkcs1".equals(padding)) { - transformation += "PKCS1Padding"; - } else if ("oaep-mgf1".equals(padding)) { - final String hashName = - ((String) mkEntry.get("padding-hash")) - .replace("sha", "sha-") - .toUpperCase(Locale.ROOT); - transformation += "OAEPWith" + hashName + "AndMGF1Padding"; - } else { - throw new IllegalArgumentException("Unsupported padding:" + padding); - } - final PublicKey wrappingKey; - final PrivateKey unwrappingKey; - if (key.key instanceof PublicKey) { - wrappingKey = (PublicKey) key.key; - unwrappingKey = null; - } else { - wrappingKey = null; - unwrappingKey = (PrivateKey) key.key; - } - mks.add( - JceMasterKey.getInstance( - wrappingKey, unwrappingKey, provId, key.keyId, transformation)); - } else { - throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); - } - } else { - throw new IllegalArgumentException("Unsupported Key Type: " + type); - } - } - - return MultipleProviderFactory.buildMultiProvider(mks); - }; - - @SuppressWarnings("unchecked") - final Map resultSpec = (Map) data.get("result"); - final ResultMatcher matcher = parseResultMatcher(jar, resultSpec); - - String decryptionMethodSpec = (String) data.get("decryption-method"); - SignaturePolicy signaturePolicy = SignaturePolicy.AllowEncryptAllowDecrypt; - if (decryptionMethodSpec != null) { - if ("streaming-unsigned-only".equals(decryptionMethodSpec)) { - signaturePolicy = SignaturePolicy.AllowEncryptForbidDecrypt; - } else { - throw new IllegalArgumentException( - "Unsupported Decryption Method: " + decryptionMethodSpec); - } - } - - return new TestCase( - testName, - ciphertextURL, - false, - mkpSupplier, - null, - Collections.emptyMap(), - matcher, - signaturePolicy); - } - - /** Parse Test to MasterKey for AWS SDK v2 */ - @SuppressWarnings("unchecked") - private static TestCase parseTest( - String testName, - Map data, - Map keys, - JarFile jar, - com.amazonaws.encryptionsdk.kmssdkv2.KmsMasterKeyProvider kmsProv) - throws IOException { - final String ciphertextURL = (String) data.get("ciphertext"); - cacheData(jar, ciphertextURL); - - Supplier> mkpSupplier = - () -> { - @SuppressWarnings("generic") - final List> mks = new ArrayList<>(); - - for (Map mkEntry : (List>) data.get("master-keys")) { - final String type = (String) mkEntry.get("type"); - final String keyName = (String) mkEntry.get("key"); - final KeyEntry key = keys.get(keyName); - - if ("aws-kms".equals(type)) { - mks.add(kmsProv.getMasterKey(key.keyId)); - } else if ("aws-kms-mrk-aware".equals(type)) { - com.amazonaws.encryptionsdk.kmssdkv2.AwsKmsMrkAwareMasterKeyProvider provider = - com.amazonaws.encryptionsdk.kmssdkv2.AwsKmsMrkAwareMasterKeyProvider.builder() - .buildStrict(key.keyId); - mks.add(provider.getMasterKey(key.keyId)); - } else if ("aws-kms-mrk-aware-discovery".equals(type)) { - final String defaultMrkRegion = (String) mkEntry.get("default-mrk-region"); - final Map discoveryFilterSpec = - (Map) mkEntry.get("aws-kms-discovery-filter"); - final DiscoveryFilter discoveryFilter; - if (discoveryFilterSpec != null) { - discoveryFilter = - new DiscoveryFilter( - (String) discoveryFilterSpec.get("partition"), - (List) discoveryFilterSpec.get("account-ids")); - } else { - discoveryFilter = null; - } - return com.amazonaws.encryptionsdk.kmssdkv2.AwsKmsMrkAwareMasterKeyProvider.builder() - .discoveryMrkRegion(Region.of(defaultMrkRegion)) - .buildDiscovery(discoveryFilter); - } else if ("raw".equals(type)) { - final String provId = (String) mkEntry.get("provider-id"); - final String algorithm = (String) mkEntry.get("encryption-algorithm"); - if ("aes".equals(algorithm)) { - mks.add( - JceMasterKey.getInstance( - (SecretKey) key.key, provId, key.keyId, "AES/GCM/NoPadding")); - } else if ("rsa".equals(algorithm)) { - String transformation = "RSA/ECB/"; - final String padding = (String) mkEntry.get("padding-algorithm"); - if ("pkcs1".equals(padding)) { - transformation += "PKCS1Padding"; - } else if ("oaep-mgf1".equals(padding)) { - final String hashName = - ((String) mkEntry.get("padding-hash")) - .replace("sha", "sha-") - .toUpperCase(Locale.ROOT); - transformation += "OAEPWith" + hashName + "AndMGF1Padding"; - } else { - throw new IllegalArgumentException("Unsupported padding:" + padding); - } - final PublicKey wrappingKey; - final PrivateKey unwrappingKey; - if (key.key instanceof PublicKey) { - wrappingKey = (PublicKey) key.key; - unwrappingKey = null; - } else { - wrappingKey = null; - unwrappingKey = (PrivateKey) key.key; - } - mks.add( - JceMasterKey.getInstance( - wrappingKey, unwrappingKey, provId, key.keyId, transformation)); - } else { - throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); - } - } else { - throw new IllegalArgumentException("Unsupported Key Type: " + type); - } - } - - return MultipleProviderFactory.buildMultiProvider(mks); - }; - - @SuppressWarnings("unchecked") - final Map resultSpec = (Map) data.get("result"); - final ResultMatcher matcher = parseResultMatcher(jar, resultSpec); - - String decryptionMethodSpec = (String) data.get("decryption-method"); - SignaturePolicy signaturePolicy = SignaturePolicy.AllowEncryptAllowDecrypt; - if (decryptionMethodSpec != null) { - if ("streaming-unsigned-only".equals(decryptionMethodSpec)) { - signaturePolicy = SignaturePolicy.AllowEncryptForbidDecrypt; - } else { - throw new IllegalArgumentException( - "Unsupported Decryption Method: " + decryptionMethodSpec); - } - } - - return new TestCase( - testName, - ciphertextURL, - false, - mkpSupplier, - null, - Collections.emptyMap(), - matcher, - signaturePolicy); - } - - private static ResultMatcher parseResultMatcher( - final JarFile jar, final Map result) throws IOException { - if (result.size() != 1) { - throw new IllegalArgumentException("Unsupported result specification: " + result); - } - Map.Entry pair = result.entrySet().iterator().next(); - if (pair.getKey().equals("output")) { - Map outputSpec = (Map) pair.getValue(); - String plaintextUrl = outputSpec.get("plaintext"); - cacheData(jar, plaintextUrl); - return new OutputResultMatcher(plaintextUrl); - } else if (pair.getKey().equals("error")) { - Map errorSpec = (Map) pair.getValue(); - String errorDescription = errorSpec.get("error-description"); - return new ErrorResultMatcher(errorDescription); - } else { - throw new IllegalArgumentException("Unsupported result specification: " + result); - } - } - - @SuppressWarnings("unchecked") - private static Map parseKeyManifest(final Map keysManifest) - throws GeneralSecurityException { - // check our type - final Map metaData = (Map) keysManifest.get("manifest"); - if (!"keys".equals(metaData.get("type"))) { - throw new IllegalArgumentException("Invalid manifest type: " + metaData.get("type")); - } - if (!Integer.valueOf(3).equals(metaData.get("version"))) { - throw new IllegalArgumentException("Invalid manifest version: " + metaData.get("version")); - } - - final Map result = new HashMap<>(); - - Map keys = (Map) keysManifest.get("keys"); - for (Map.Entry entry : keys.entrySet()) { - final String name = entry.getKey(); - final Map data = (Map) entry.getValue(); - - final String keyType = (String) data.get("type"); - final String encoding = (String) data.get("encoding"); - final String keyId = (String) data.get("key-id"); - final String material = (String) data.get("material"); // May be null - final String algorithm = (String) data.get("algorithm"); // May be null - - final KeyEntry keyEntry; - - final KeyFactory kf; - switch (keyType) { - case "symmetric": - if (!"base64".equals(encoding)) { - throw new IllegalArgumentException( - format("Key %s is symmetric but has encoding %s", keyId, encoding)); - } - keyEntry = - new KeyEntry( - name, - keyId, - keyType, - new SecretKeySpec(Base64.decode(material), algorithm.toUpperCase(Locale.ROOT))); - break; - case "private": - kf = KeyFactory.getInstance(algorithm); - if (!"pem".equals(encoding)) { - throw new IllegalArgumentException( - format("Key %s is private but has encoding %s", keyId, encoding)); - } - byte[] pkcs8Key = parsePem(material); - keyEntry = - new KeyEntry( - name, keyId, keyType, kf.generatePrivate(new PKCS8EncodedKeySpec(pkcs8Key))); - break; - case "public": - kf = KeyFactory.getInstance(algorithm); - if (!"pem".equals(encoding)) { - throw new IllegalArgumentException( - format("Key %s is private but has encoding %s", keyId, encoding)); - } - byte[] x509Key = parsePem(material); - keyEntry = - new KeyEntry( - name, keyId, keyType, kf.generatePublic(new X509EncodedKeySpec(x509Key))); - break; - case "aws-kms": - keyEntry = new KeyEntry(name, keyId, keyType, null); - break; - default: - throw new IllegalArgumentException("Unsupported key type: " + keyType); - } - - result.put(name, keyEntry); - } - - return result; - } - - private static byte[] parsePem(String pem) { - final String stripped = pem.replaceAll("-+[A-Z ]+-+", ""); - return Base64.decode(stripped); - } - - private static class KeyEntry { - final String name; - final String keyId; - final String type; - final Key key; - - private KeyEntry(String name, String keyId, String type, Key key) { - this.name = name; - this.keyId = keyId; - this.type = type; - this.key = key; - } - } - - private static class TestCase { - private final String name; - private final String ciphertextPath; - private final ResultMatcher matcher; - private final boolean isKeyring; - private final Supplier> mkpSupplier; - private final Supplier cmmSupplier; - private final Map encryptionContext; - private final SignaturePolicy signaturePolicy; - - private TestCase( - String name, - String ciphertextPath, - boolean isKeyring, - Supplier> mkpSupplier, - Supplier cmmSupplier, - Map encryptionContext, - ResultMatcher matcher, - SignaturePolicy signaturePolicy) { - this.name = name; - this.ciphertextPath = ciphertextPath; - this.matcher = matcher; - this.isKeyring = isKeyring; - this.mkpSupplier = mkpSupplier; - this.cmmSupplier = cmmSupplier; - this.encryptionContext = encryptionContext; - this.signaturePolicy = signaturePolicy; - } - } - - private interface ResultMatcher { - void Match(Callable decryptor) throws Exception; - } - - private static class OutputResultMatcher implements ResultMatcher { - - private final String plaintextPath; - - private OutputResultMatcher(String plaintextPath) { - this.plaintextPath = plaintextPath; - } - - @Override - public void Match(Callable decryptor) throws Exception { - final byte[] plaintext = decryptor.call(); - final byte[] expectedPlaintext = cachedData.get(plaintextPath); - Assert.assertArrayEquals(expectedPlaintext, plaintext); - } - } - - private static class ErrorResultMatcher implements ResultMatcher { - - private final String errorDescription; - - private ErrorResultMatcher(String errorDescription) { - this.errorDescription = errorDescription; - } - - @Override - public void Match(Callable decryptor) { - Assert.assertThrows( - "Decryption expected to fail (" + errorDescription + ") but succeeded", - Exception.class, - decryptor::call); - } - } -} From 68e954db18c14a136e4aebe16a83569cdd559892 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Mon, 10 Jun 2024 15:53:15 -0700 Subject: [PATCH 08/19] undo --- .../encryptionsdk/AllTestsSuite.java | 2 + .../encryptionsdk/TestVectorGenerator.java | 570 ++++++++++++++ .../encryptionsdk/TestVectorRunner.java | 743 ++++++++++++++++++ 3 files changed, 1315 insertions(+) create mode 100644 src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java create mode 100644 src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java diff --git a/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java b/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java index ce7325b4..445f77d2 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java +++ b/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java @@ -80,6 +80,8 @@ AwsCryptoTest.class, CryptoInputStreamTest.class, CryptoOutputStreamTest.class, + TestVectorRunner.class, + TestVectorGenerator.class, XCompatDecryptTest.class, DefaultCryptoMaterialsManagerTest.class, NullCryptoMaterialsCacheTest.class, diff --git a/src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java b/src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java new file mode 100644 index 00000000..eb2b1764 --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java @@ -0,0 +1,570 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.encryptionsdk; + +import static java.lang.String.format; + +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.amazonaws.encryptionsdk.jce.JceMasterKey; +import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; +import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileAttribute; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import org.apache.commons.io.FileUtils; +import org.bouncycastle.util.encoders.Base64; +import org.junit.AfterClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import software.amazon.awssdk.utils.ImmutableMap; +import software.amazon.cryptography.materialproviders.IKeyring; +import software.amazon.cryptography.materialproviders.MaterialProviders; +import software.amazon.cryptography.materialproviders.model.CreateMultiKeyringInput; +import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig; +import software.amazon.cryptography.materialproviderstestvectorkeys.KeyVectors; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionInput; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionOutput; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.KeyVectorsConfig; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.TestVectorKeyringInput; + +@RunWith(Parameterized.class) +public class TestVectorGenerator { + + private static final String encryptManifestList = + "https://raw.githubusercontent.com/awslabs/aws-crypto-tools-test-vector-framework/master/features/CANONICAL-GENERATED-MANIFESTS/0003-awses-message-encryption.v2.json"; + // We save the files in memory to avoid repeatedly retrieving them. This won't work if the + // plaintexts are too + // large or numerous + private static final Map cachedData = new HashMap<>(); + private static final ObjectMapper mapper = new ObjectMapper(); + private static EncryptionInterface encryption; + private static boolean isMasterKey; + private final String testName; + private final TestCase testCase; + + // Temp Test Vectors Directory + private static String tempTestVectorPath; + // Zip File Path + private static String zipFilePath; + + public TestVectorGenerator(final String testName, TestCase testCase) { + this.testName = testName; + this.testCase = testCase; + } + + // Zip Temp Folder and delete temp files + @AfterClass + public static void zip() throws IOException { + Path zipFile = Files.createFile(Paths.get(zipFilePath)); + + Path sourceDirPath = Paths.get(tempTestVectorPath); + try (ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(zipFile)); + Stream paths = Files.walk(sourceDirPath)) { + paths + .filter(path -> !Files.isDirectory(path)) + .forEach( + path -> { + ZipEntry zipEntry = new ZipEntry(sourceDirPath.relativize(path).toString()); + try { + zipOutputStream.putNextEntry(zipEntry); + Files.copy(path, zipOutputStream); + zipOutputStream.closeEntry(); + } catch (IOException e) { + throw new UncheckedIOException("Unable to Zip File", e); + } + }); + } + FileUtils.deleteQuietly(sourceDirPath.toFile()); + + // Teardown + cachedData.clear(); + } + + @Test + @SuppressWarnings("unchecked") + public void encrypt() throws Exception { + CryptoAlgorithm cryptoAlgorithm = getCryptoAlgorithm(testCase.algorithmId); + CommitmentPolicy commitmentPolicy = + cryptoAlgorithm.isCommitting() + ? CommitmentPolicy.RequireEncryptRequireDecrypt + : CommitmentPolicy.ForbidEncryptAllowDecrypt; + + AwsCrypto crypto = + AwsCrypto.builder() + .withCommitmentPolicy(commitmentPolicy) + .withEncryptionAlgorithm(cryptoAlgorithm) + .withEncryptionFrameSize(testCase.frameSize) + .build(); + + Callable ciphertext; + if (isMasterKey) { + ciphertext = + () -> + crypto + .encryptData( + testCase.masterKey, + cachedData.get(testCase.plaintext), + testCase.encryptionContext) + .getResult(); + } else { + ciphertext = + () -> + crypto + .encryptData( + testCase.keyring, + cachedData.get(testCase.plaintext), + testCase.encryptionContext) + .getResult(); + } + Files.write(Paths.get(tempTestVectorPath + "ciphertexts/" + testName), ciphertext.call()); + } + + private static CryptoAlgorithm getCryptoAlgorithm(String algorithmId) { + Integer algId = Integer.parseInt(algorithmId, 16); + for (CryptoAlgorithm cryptoAlgorithm : CryptoAlgorithm.values()) { + if (cryptoAlgorithm.getValue() == algId) { + return cryptoAlgorithm; + } + } + throw new IllegalArgumentException("Invalid AlgorithmId: " + algorithmId); + } + + @Parameterized.Parameters(name = "Compatibility Test: {0} - {1}") + @SuppressWarnings("unchecked") + public static Collection data() throws Exception { + final String interfaceOption = System.getProperty("masterkey"); + + if (interfaceOption != null && interfaceOption.equals("true")) { + isMasterKey = true; + encryption = EncryptionInterface.EncryptWithMasterKey; + } else { + encryption = EncryptionInterface.EncryptWithKeyring; + } + + final String encryptKeyManifest = System.getProperty("keysManifest"); + if (encryptKeyManifest == null) { + return Collections.emptyList(); + } + + zipFilePath = System.getProperty("zipFilePath"); + if (zipFilePath == null) { + return Collections.emptyList(); + } + + tempTestVectorPath = Files.createTempDirectory("java", new FileAttribute[0]).toString() + "/"; + createDirectories(tempTestVectorPath + "ciphertexts/"); + createDirectories(tempTestVectorPath + "plaintexts/"); + + File decryptManifest = new File(tempTestVectorPath + "manifest.json"); + File keyManifest = new File(tempTestVectorPath + "keys.json"); + + final Map manifest = mapper.readValue(new URL(encryptManifestList), Map.class); + mapper + .writerWithDefaultPrettyPrinter() + .writeValue(decryptManifest, createDecryptManifest(manifest)); + + try (InputStream in = new FileInputStream(encryptKeyManifest)) { + Files.copy(in, keyManifest.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + + final Map keysManifest = + mapper.readValue(new File(encryptKeyManifest), Map.class); + + cachePlaintext((Map) manifest.get("plaintexts")); + + MaterialProvidersConfig config = MaterialProvidersConfig.builder().build(); + MaterialProviders materialProviders = + MaterialProviders.builder().MaterialProvidersConfig(config).build(); + KeyVectors keyVectors = + KeyVectors.builder() + .KeyVectorsConfig( + KeyVectorsConfig.builder().keyManifiestPath(keyManifest.toString()).build()) + .build(); + + final Map keys = parseKeyManifest(keysManifest); + final KmsMasterKeyProvider kmsProv = + KmsMasterKeyProvider.builder() + .withCredentials(new DefaultAWSCredentialsProviderChain()) + .buildDiscovery(); + + return ((Map>) manifest.get("tests")) + .entrySet().stream() + .map( + entry -> { + String testName = entry.getKey(); + TestCase testCase = + encryption.parseTest(entry, keys, kmsProv, materialProviders, keyVectors); + return new Object[] {testName, testCase}; + }) + .collect(Collectors.toList()); + } + + private static void createDirectories(String path) { + File directory = new File(path); + directory.mkdirs(); + } + + private enum EncryptionInterface { + EncryptWithMasterKey { + @Override + public TestCase parseTest( + Map.Entry> testEntry, + Map keys, + KmsMasterKeyProvider kmsProv, + MaterialProviders materialProviders, + KeyVectors keyVectors) { + return parseTestWithMasterkeys(testEntry, keys, kmsProv); + } + }, + EncryptWithKeyring { + @Override + public TestCase parseTest( + Map.Entry> testEntry, + Map keys, + KmsMasterKeyProvider kmsProv, + MaterialProviders materialProviders, + KeyVectors keyVectors) { + return parseTestWithKeyrings(testEntry, materialProviders, keyVectors); + } + }; + + public abstract TestCase parseTest( + Map.Entry> testEntry, + Map keys, + KmsMasterKeyProvider kmsProv, + MaterialProviders materialProviders, + KeyVectors keyVectors); + } + + private static void cachePlaintext(Map plaintexts) { + Random rd = new Random(); + plaintexts.forEach( + (key, value) -> { + byte[] plaintext = new byte[value]; + rd.nextBytes(plaintext); + try { + Files.write(new File(tempTestVectorPath + "plaintexts/" + key).toPath(), plaintext); + cachedData.put(key, plaintext); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } + + private static Map createDecryptManifest(Map encryptManifest) { + Map decryptManifest = new LinkedHashMap<>(); + + decryptManifest.put("manifest", ImmutableMap.of("type", "awses-decrypt", "version", 2)); + + decryptManifest.put( + "client", ImmutableMap.of("name", "aws/aws-encryption-sdk-java", "version", "2.2.0")); + + decryptManifest.put("keys", "file://keys.json"); + + Map> testScenarios = + ((LinkedHashMap>) encryptManifest.get("tests")) + .entrySet().stream() + .collect( + Collectors.toMap( + Map.Entry::getKey, + entry -> { + Map scenario = entry.getValue(); + return new LinkedHashMap() { + { + put("ciphertext", "file://ciphertexts/" + entry.getKey()); + put("master-keys", scenario.get("master-keys")); + put( + "result", + Collections.singletonMap( + "output", + Collections.singletonMap( + "plaintext", + "file://plaintexts/" + scenario.get("plaintext")))); + } + }; + })); + + decryptManifest.put("tests", testScenarios); + return decryptManifest; + } + + private static TestCase parseTestWithMasterkeys( + Map.Entry> testEntry, + Map keys, + KmsMasterKeyProvider kmsProv) { + + String testName = testEntry.getKey(); + Map data = testEntry.getValue(); + + String plaintext = (String) data.get("plaintext"); + String algorithmId = (String) data.get("algorithm"); + int frameSize = (int) data.get("frame-size"); + Map encryptionContext = (Map) data.get("encryption-context"); + + final List> mks = new ArrayList<>(); + + for (Map mkEntry : (List>) data.get("master-keys")) { + if (mkEntry.get("key").equals("rsa-4096-private")) { + mkEntry.replace("key", "rsa-4096-public"); + } + + final String type = mkEntry.get("type"); + final String keyName = mkEntry.get("key"); + final KeyEntry key = keys.get(keyName); + + if ("aws-kms".equals(type)) { + mks.add(kmsProv.getMasterKey(key.keyId)); + } else if ("raw".equals(type)) { + final String provId = mkEntry.get("provider-id"); + final String algorithm = mkEntry.get("encryption-algorithm"); + if ("aes".equals(algorithm)) { + mks.add( + JceMasterKey.getInstance( + (SecretKey) key.key, provId, key.keyId, "AES/GCM/NoPadding")); + } else if ("rsa".equals(algorithm)) { + String transformation = "RSA/ECB/"; + final String padding = mkEntry.get("padding-algorithm"); + if ("pkcs1".equals(padding)) { + transformation += "PKCS1Padding"; + } else if ("oaep-mgf1".equals(padding)) { + final String hashName = + mkEntry.get("padding-hash").replace("sha", "sha-").toUpperCase(); + transformation += "OAEPWith" + hashName + "AndMGF1Padding"; + } else { + throw new IllegalArgumentException("Unsupported padding:" + padding); + } + final PublicKey wrappingKey; + final PrivateKey unwrappingKey; + if (key.key instanceof PublicKey) { + wrappingKey = (PublicKey) key.key; + unwrappingKey = null; + } else { + wrappingKey = null; + unwrappingKey = (PrivateKey) key.key; + } + mks.add( + JceMasterKey.getInstance( + wrappingKey, unwrappingKey, provId, key.keyId, transformation)); + } else { + throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); + } + } else { + throw new IllegalArgumentException("Unsupported Key Type: " + type); + } + } + + MasterKeyProvider multiProvider = MultipleProviderFactory.buildMultiProvider(mks); + + return new TestCase( + testName, null, multiProvider, plaintext, algorithmId, frameSize, encryptionContext); + } + + private static TestCase parseTestWithKeyrings( + Map.Entry> testEntry, + MaterialProviders materialProviders, + KeyVectors keyVectors) { + String testName = testEntry.getKey(); + Map data = testEntry.getValue(); + + String plaintext = (String) data.get("plaintext"); + String algorithmId = (String) data.get("algorithm"); + int frameSize = (int) data.get("frame-size"); + Map encryptionContext = (Map) data.get("encryption-context"); + + List keyrings = new ArrayList<>(); + + ((List>) data.get("master-keys")) + .forEach( + mkEntry -> { + if (mkEntry.get("type").equals("raw") + && mkEntry.get("encryption-algorithm").equals("rsa")) { + if (mkEntry.get("key").equals("rsa-4096-private")) { + mkEntry.replace("key", "rsa-4096-public"); + } + mkEntry.putIfAbsent("padding-hash", "sha1"); + } + + try { + byte[] json = new ObjectMapper().writeValueAsBytes(mkEntry); + GetKeyDescriptionOutput output = + keyVectors.GetKeyDescription( + GetKeyDescriptionInput.builder().json(ByteBuffer.wrap(json)).build()); + + IKeyring testVectorKeyring = + keyVectors.CreateTestVectorKeyring( + TestVectorKeyringInput.builder() + .keyDescription(output.keyDescription()) + .build()); + + keyrings.add(testVectorKeyring); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }); + + IKeyring primary = keyrings.remove(0); + IKeyring multiKeyring = + materialProviders.CreateMultiKeyring( + CreateMultiKeyringInput.builder().generator(primary).childKeyrings(keyrings).build()); + + return new TestCase( + testName, multiKeyring, null, plaintext, algorithmId, frameSize, encryptionContext); + } + + @SuppressWarnings("unchecked") + private static Map parseKeyManifest(final Map keysManifest) + throws GeneralSecurityException { + // check our type + final Map metaData = (Map) keysManifest.get("manifest"); + if (!"keys".equals(metaData.get("type"))) { + throw new IllegalArgumentException("Invalid manifest type: " + metaData.get("type")); + } + if (!Integer.valueOf(3).equals(metaData.get("version"))) { + throw new IllegalArgumentException("Invalid manifest version: " + metaData.get("version")); + } + + final Map result = new HashMap<>(); + + Map keys = (Map) keysManifest.get("keys"); + for (Map.Entry entry : keys.entrySet()) { + final String name = entry.getKey(); + final Map data = (Map) entry.getValue(); + + final String keyType = (String) data.get("type"); + final String encoding = (String) data.get("encoding"); + final String keyId = (String) data.get("key-id"); + final String material = (String) data.get("material"); // May be null + final String algorithm = (String) data.get("algorithm"); // May be null + + final KeyEntry keyEntry; + + final KeyFactory kf; + switch (keyType) { + case "symmetric": + if (!"base64".equals(encoding)) { + throw new IllegalArgumentException( + format("Key %s is symmetric but has encoding %s", keyId, encoding)); + } + keyEntry = + new KeyEntry( + name, + keyId, + keyType, + new SecretKeySpec(Base64.decode(material), algorithm.toUpperCase())); + break; + case "private": + kf = KeyFactory.getInstance(algorithm); + if (!"pem".equals(encoding)) { + throw new IllegalArgumentException( + format("Key %s is private but has encoding %s", keyId, encoding)); + } + byte[] pkcs8Key = parsePem(material); + keyEntry = + new KeyEntry( + name, keyId, keyType, kf.generatePrivate(new PKCS8EncodedKeySpec(pkcs8Key))); + break; + case "public": + kf = KeyFactory.getInstance(algorithm); + if (!"pem".equals(encoding)) { + throw new IllegalArgumentException( + format("Key %s is private but has encoding %s", keyId, encoding)); + } + byte[] x509Key = parsePem(material); + keyEntry = + new KeyEntry( + name, keyId, keyType, kf.generatePublic(new X509EncodedKeySpec(x509Key))); + break; + case "aws-kms": + keyEntry = new KeyEntry(name, keyId, keyType, null); + break; + default: + throw new IllegalArgumentException("Unsupported key type: " + keyType); + } + + result.put(name, keyEntry); + } + + return result; + } + + private static byte[] parsePem(String pem) { + final String stripped = pem.replaceAll("-+[A-Z ]+-+", ""); + return Base64.decode(stripped); + } + + private static class KeyEntry { + final String name; + final String keyId; + final String type; + final Key key; + + private KeyEntry(String name, String keyId, String type, Key key) { + this.name = name; + this.keyId = keyId; + this.type = type; + this.key = key; + } + } + + private static class TestCase { + private final String name; + private final IKeyring keyring; + private final MasterKeyProvider masterKey; + private final String plaintext; + private final String algorithmId; + private final int frameSize; + private final Map encryptionContext; + + public TestCase( + String name, + IKeyring keyring, + MasterKeyProvider multiProvider, + String plaintext, + String algorithmId, + int frameSize, + Map encryptionContext) { + this.name = name; + this.keyring = keyring; + this.masterKey = multiProvider; + this.plaintext = plaintext; + this.algorithmId = algorithmId; + this.frameSize = frameSize; + this.encryptionContext = encryptionContext; + } + } +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java b/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java new file mode 100644 index 00000000..8c0be82f --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java @@ -0,0 +1,743 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.encryptionsdk; + +import static java.lang.String.format; + +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.amazonaws.encryptionsdk.internal.SignaturePolicy; +import com.amazonaws.encryptionsdk.jce.JceMasterKey; +import com.amazonaws.encryptionsdk.kms.AwsKmsMrkAwareMasterKeyProvider; +import com.amazonaws.encryptionsdk.kms.DiscoveryFilter; +import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; +import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory; +import com.amazonaws.util.IOUtils; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.URL; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.function.Supplier; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import junit.framework.TestCase; +import org.bouncycastle.util.encoders.Base64; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import software.amazon.awssdk.regions.Region; +import software.amazon.cryptography.materialproviders.ICryptographicMaterialsManager; +import software.amazon.cryptography.materialproviders.IKeyring; +import software.amazon.cryptography.materialproviders.MaterialProviders; +import software.amazon.cryptography.materialproviders.model.CreateDefaultCryptographicMaterialsManagerInput; +import software.amazon.cryptography.materialproviders.model.CreateMultiKeyringInput; +import software.amazon.cryptography.materialproviders.model.CreateRequiredEncryptionContextCMMInput; +import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig; +import software.amazon.cryptography.materialproviderstestvectorkeys.KeyVectors; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionInput; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionOutput; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.KeyVectorsConfig; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.TestVectorKeyringInput; + +@RunWith(Parameterized.class) +public class TestVectorRunner { + + // TODO: Standardize Manifest Version + private static final List MANIFEST_VERSIONS = Arrays.asList(2, 4); + + // We save the files in memory to avoid repeatedly retrieving them. This won't work if the + // plaintexts are too + // large or numerous + private static final Map cachedData = new HashMap<>(); + + private final String testName; + private final TestCase testCase; + private final DecryptionMethod decryptionMethod; + + public TestVectorRunner( + final String testName, TestCase testCase, DecryptionMethod decryptionMethod) { + this.testName = testName; + this.testCase = testCase; + this.decryptionMethod = decryptionMethod; + } + + @Test + public void decrypt() throws Exception { + AwsCrypto crypto = + AwsCrypto.builder() + .withCommitmentPolicy(CommitmentPolicy.ForbidEncryptAllowDecrypt) + .build(); + Callable decryptor; + if (testCase.isKeyring) { + decryptor = + () -> + decryptionMethod.decryptMessage( + crypto, + testCase.cmmSupplier.get(), + cachedData.get(testCase.ciphertextPath), + testCase.encryptionContext); + } else { + decryptor = + () -> + decryptionMethod.decryptMessage( + crypto, testCase.mkpSupplier.get(), cachedData.get(testCase.ciphertextPath)); + } + testCase.matcher.Match(decryptor); + } + + @Parameterized.Parameters(name = "Compatibility Test: {0} - {3}") + @SuppressWarnings("unchecked") + public static Collection data() throws Exception { + final String zipPath = System.getProperty("testVectorZip"); + final String interfaceOption = System.getProperty("masterkey"); + if (zipPath == null) { + return Collections.emptyList(); + } + + final JarURLConnection jarConnection = + (JarURLConnection) new URL("jar:" + zipPath + "!/").openConnection(); + + try (JarFile jar = jarConnection.getJarFile()) { + + final Map manifest = readJsonMapFromJar(jar, "manifest.json"); + final Map keysManifest = readJsonMapFromJar(jar, "keys.json"); + + ObjectMapper objectMapper = new ObjectMapper(); + + // Create a temporary file and write the JSON string to it + File tempFile = File.createTempFile("keys", ".json"); + objectMapper.writeValue(tempFile, keysManifest); + + final Map metaData = (Map) manifest.get("manifest"); + + // We only support "awses-decrypt" type manifests right now + if (!"awses-decrypt".equals(metaData.get("type"))) { + throw new IllegalArgumentException("Unsupported manifest type: " + metaData.get("type")); + } + Integer readVersion = (Integer) metaData.get("version"); + if (!MANIFEST_VERSIONS.contains(readVersion)) { + throw new IllegalArgumentException( + "Unsupported manifest version: " + metaData.get("version")); + } + + final Map keys = + parseKeyManifest(readJsonMapFromJar(jar, (String) manifest.get("keys"))); + + KeyVectors keyVectors = + KeyVectors.builder() + .KeyVectorsConfig( + KeyVectorsConfig.builder().keyManifiestPath(tempFile.getPath()).build()) + .build(); + + MaterialProvidersConfig config = MaterialProvidersConfig.builder().build(); + MaterialProviders materialProviders = + MaterialProviders.builder().MaterialProvidersConfig(config).build(); + + final KmsMasterKeyProvider kmsProvV1 = + KmsMasterKeyProvider.builder() + .withCredentials(new DefaultAWSCredentialsProviderChain()) + .buildDiscovery(); + + final com.amazonaws.encryptionsdk.kmssdkv2.KmsMasterKeyProvider kmsProvV2 = + com.amazonaws.encryptionsdk.kmssdkv2.KmsMasterKeyProvider.builder().buildDiscovery(); + + List testCases = new ArrayList<>(); + for (Map.Entry> testEntry : + ((Map>) manifest.get("tests")).entrySet()) { + if (interfaceOption != null && interfaceOption.equals("true")) { + String testName = testEntry.getKey(); + + TestCase testCaseV1 = + parseTest(testEntry.getKey(), testEntry.getValue(), keys, jar, kmsProvV1); + TestCase testCaseV2 = + parseTest(testEntry.getKey(), testEntry.getValue(), keys, jar, kmsProvV2); + + for (DecryptionMethod decryptionMethod : DecryptionMethod.values()) { + if (testCaseV1.signaturePolicy.equals(decryptionMethod.signaturePolicy())) { + testCases.add(new Object[] {testName, testCaseV1, decryptionMethod}); + testCases.add(new Object[] {testName + "-V2", testCaseV2, decryptionMethod}); + } + } + } else { + String testName = testEntry.getKey(); + TestCase testCaseKeyring = + parseTest( + testEntry.getKey(), + testEntry.getValue(), + keys, + jar, + materialProviders, + keyVectors); + + for (DecryptionMethod decryptionMethod : DecryptionMethod.values()) { + if (testCaseKeyring.signaturePolicy.equals(decryptionMethod.signaturePolicy())) { + testCases.add( + new Object[] {testName + "-Keyrings", testCaseKeyring, decryptionMethod}); + } + } + } + } + return testCases; + } + } + + @AfterClass + public static void teardown() { + cachedData.clear(); + } + + private static byte[] readBytesFromJar(JarFile jar, String fileName) throws IOException { + try (InputStream is = readFromJar(jar, fileName)) { + return IOUtils.toByteArray(is); + } + } + + private static Map readJsonMapFromJar(JarFile jar, String fileName) + throws IOException { + try (InputStream is = readFromJar(jar, fileName)) { + final ObjectMapper mapper = new ObjectMapper(); + return mapper.readValue(is, new TypeReference>() {}); + } + } + + private static InputStream readFromJar(JarFile jar, String name) throws IOException { + // Our manifest URIs incorrectly start with file:// rather than just file: so we need to strip + // this + ZipEntry entry = jar.getEntry(name.replaceFirst("^file://(?!/)", "")); + return jar.getInputStream(entry); + } + + private static void cacheData(JarFile jar, String url) throws IOException { + if (!cachedData.containsKey(url)) { + cachedData.put(url, readBytesFromJar(jar, url)); + } + } + + /** Parse Test to Keyring */ + @SuppressWarnings("unchecked") + private static TestCase parseTest( + String testName, + Map data, + Map keys, + JarFile jar, + MaterialProviders materialProviders, + KeyVectors keyVectors) + throws IOException { + final String ciphertextURL = (String) data.get("ciphertext"); + cacheData(jar, ciphertextURL); + + Supplier cmmSupplier = + () -> { + final List keyrings = new ArrayList<>(); + for (Map mkEntry : (List>) data.get("master-keys")) { + if (mkEntry.get("type").equals("raw") + && mkEntry.get("encryption-algorithm").equals("rsa")) { + mkEntry.putIfAbsent("padding-hash", "sha1"); + } + + try { + byte[] json = new ObjectMapper().writeValueAsBytes(mkEntry); + GetKeyDescriptionOutput output = + keyVectors.GetKeyDescription( + GetKeyDescriptionInput.builder().json(ByteBuffer.wrap(json)).build()); + + IKeyring testVectorKeyring = + keyVectors.CreateTestVectorKeyring( + TestVectorKeyringInput.builder() + .keyDescription(output.keyDescription()) + .build()); + + keyrings.add(testVectorKeyring); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + IKeyring multiKeyring = + materialProviders.CreateMultiKeyring( + CreateMultiKeyringInput.builder() + .generator(keyrings.get(0)) + .childKeyrings(keyrings) + .build()); + + CreateDefaultCryptographicMaterialsManagerInput defaultInput = + CreateDefaultCryptographicMaterialsManagerInput.builder() + .keyring(multiKeyring) + .build(); + ICryptographicMaterialsManager cmm = + materialProviders.CreateDefaultCryptographicMaterialsManager(defaultInput); + if (data.containsKey("cmm") && data.get("cmm").equals("RequiredEncryptionContext")) { + List requiredKeys = new ArrayList(2); + requiredKeys.add("key1"); + requiredKeys.add("key2"); + CreateRequiredEncryptionContextCMMInput requiredCMMInput = + CreateRequiredEncryptionContextCMMInput.builder() + .underlyingCMM( + materialProviders.CreateDefaultCryptographicMaterialsManager(defaultInput)) + .requiredEncryptionContextKeys(requiredKeys) + .build(); + cmm = materialProviders.CreateRequiredEncryptionContextCMM(requiredCMMInput); + } + return cmm; + }; + @SuppressWarnings("unchecked") + final Map resultSpec = (Map) data.get("result"); + final ResultMatcher matcher = parseResultMatcher(jar, resultSpec); + + Map ec = Collections.emptyMap(); + + if (data.get("encryption-context") != null) { + ec = (Map) data.get("encryption-context"); + } + + String decryptionMethodSpec = (String) data.get("decryption-method"); + SignaturePolicy signaturePolicy = SignaturePolicy.AllowEncryptAllowDecrypt; + if (decryptionMethodSpec != null) { + if ("streaming-unsigned-only".equals(decryptionMethodSpec)) { + signaturePolicy = SignaturePolicy.AllowEncryptForbidDecrypt; + } else { + throw new IllegalArgumentException( + "Unsupported Decryption Method: " + decryptionMethodSpec); + } + } + + return new TestCase( + testName, ciphertextURL, true, null, cmmSupplier, ec, matcher, signaturePolicy); + } + + /** Parse Test to MasterKey for AWS SDK v1 */ + @SuppressWarnings("unchecked") + private static TestCase parseTest( + String testName, + Map data, + Map keys, + JarFile jar, + KmsMasterKeyProvider kmsProv) + throws IOException { + final String ciphertextURL = (String) data.get("ciphertext"); + cacheData(jar, ciphertextURL); + + Supplier> mkpSupplier = + () -> { + @SuppressWarnings("generic") + final List> mks = new ArrayList<>(); + + for (Map mkEntry : (List>) data.get("master-keys")) { + final String type = (String) mkEntry.get("type"); + final String keyName = (String) mkEntry.get("key"); + final KeyEntry key = keys.get(keyName); + + if ("aws-kms".equals(type)) { + mks.add(kmsProv.getMasterKey(key.keyId)); + } else if ("aws-kms-mrk-aware".equals(type)) { + AwsKmsMrkAwareMasterKeyProvider provider = + AwsKmsMrkAwareMasterKeyProvider.builder().buildStrict(key.keyId); + mks.add(provider.getMasterKey(key.keyId)); + } else if ("aws-kms-mrk-aware-discovery".equals(type)) { + final String defaultMrkRegion = (String) mkEntry.get("default-mrk-region"); + final Map discoveryFilterSpec = + (Map) mkEntry.get("aws-kms-discovery-filter"); + final DiscoveryFilter discoveryFilter; + if (discoveryFilterSpec != null) { + discoveryFilter = + new DiscoveryFilter( + (String) discoveryFilterSpec.get("partition"), + (List) discoveryFilterSpec.get("account-ids")); + } else { + discoveryFilter = null; + } + return AwsKmsMrkAwareMasterKeyProvider.builder() + .withDiscoveryMrkRegion(defaultMrkRegion) + .buildDiscovery(discoveryFilter); + } else if ("raw".equals(type)) { + final String provId = (String) mkEntry.get("provider-id"); + final String algorithm = (String) mkEntry.get("encryption-algorithm"); + if ("aes".equals(algorithm)) { + mks.add( + JceMasterKey.getInstance( + (SecretKey) key.key, provId, key.keyId, "AES/GCM/NoPadding")); + } else if ("rsa".equals(algorithm)) { + String transformation = "RSA/ECB/"; + final String padding = (String) mkEntry.get("padding-algorithm"); + if ("pkcs1".equals(padding)) { + transformation += "PKCS1Padding"; + } else if ("oaep-mgf1".equals(padding)) { + final String hashName = + ((String) mkEntry.get("padding-hash")) + .replace("sha", "sha-") + .toUpperCase(Locale.ROOT); + transformation += "OAEPWith" + hashName + "AndMGF1Padding"; + } else { + throw new IllegalArgumentException("Unsupported padding:" + padding); + } + final PublicKey wrappingKey; + final PrivateKey unwrappingKey; + if (key.key instanceof PublicKey) { + wrappingKey = (PublicKey) key.key; + unwrappingKey = null; + } else { + wrappingKey = null; + unwrappingKey = (PrivateKey) key.key; + } + mks.add( + JceMasterKey.getInstance( + wrappingKey, unwrappingKey, provId, key.keyId, transformation)); + } else { + throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); + } + } else { + throw new IllegalArgumentException("Unsupported Key Type: " + type); + } + } + + return MultipleProviderFactory.buildMultiProvider(mks); + }; + + @SuppressWarnings("unchecked") + final Map resultSpec = (Map) data.get("result"); + final ResultMatcher matcher = parseResultMatcher(jar, resultSpec); + + String decryptionMethodSpec = (String) data.get("decryption-method"); + SignaturePolicy signaturePolicy = SignaturePolicy.AllowEncryptAllowDecrypt; + if (decryptionMethodSpec != null) { + if ("streaming-unsigned-only".equals(decryptionMethodSpec)) { + signaturePolicy = SignaturePolicy.AllowEncryptForbidDecrypt; + } else { + throw new IllegalArgumentException( + "Unsupported Decryption Method: " + decryptionMethodSpec); + } + } + + return new TestCase( + testName, + ciphertextURL, + false, + mkpSupplier, + null, + Collections.emptyMap(), + matcher, + signaturePolicy); + } + + /** Parse Test to MasterKey for AWS SDK v2 */ + @SuppressWarnings("unchecked") + private static TestCase parseTest( + String testName, + Map data, + Map keys, + JarFile jar, + com.amazonaws.encryptionsdk.kmssdkv2.KmsMasterKeyProvider kmsProv) + throws IOException { + final String ciphertextURL = (String) data.get("ciphertext"); + cacheData(jar, ciphertextURL); + + Supplier> mkpSupplier = + () -> { + @SuppressWarnings("generic") + final List> mks = new ArrayList<>(); + + for (Map mkEntry : (List>) data.get("master-keys")) { + final String type = (String) mkEntry.get("type"); + final String keyName = (String) mkEntry.get("key"); + final KeyEntry key = keys.get(keyName); + + if ("aws-kms".equals(type)) { + mks.add(kmsProv.getMasterKey(key.keyId)); + } else if ("aws-kms-mrk-aware".equals(type)) { + com.amazonaws.encryptionsdk.kmssdkv2.AwsKmsMrkAwareMasterKeyProvider provider = + com.amazonaws.encryptionsdk.kmssdkv2.AwsKmsMrkAwareMasterKeyProvider.builder() + .buildStrict(key.keyId); + mks.add(provider.getMasterKey(key.keyId)); + } else if ("aws-kms-mrk-aware-discovery".equals(type)) { + final String defaultMrkRegion = (String) mkEntry.get("default-mrk-region"); + final Map discoveryFilterSpec = + (Map) mkEntry.get("aws-kms-discovery-filter"); + final DiscoveryFilter discoveryFilter; + if (discoveryFilterSpec != null) { + discoveryFilter = + new DiscoveryFilter( + (String) discoveryFilterSpec.get("partition"), + (List) discoveryFilterSpec.get("account-ids")); + } else { + discoveryFilter = null; + } + return com.amazonaws.encryptionsdk.kmssdkv2.AwsKmsMrkAwareMasterKeyProvider.builder() + .discoveryMrkRegion(Region.of(defaultMrkRegion)) + .buildDiscovery(discoveryFilter); + } else if ("raw".equals(type)) { + final String provId = (String) mkEntry.get("provider-id"); + final String algorithm = (String) mkEntry.get("encryption-algorithm"); + if ("aes".equals(algorithm)) { + mks.add( + JceMasterKey.getInstance( + (SecretKey) key.key, provId, key.keyId, "AES/GCM/NoPadding")); + } else if ("rsa".equals(algorithm)) { + String transformation = "RSA/ECB/"; + final String padding = (String) mkEntry.get("padding-algorithm"); + if ("pkcs1".equals(padding)) { + transformation += "PKCS1Padding"; + } else if ("oaep-mgf1".equals(padding)) { + final String hashName = + ((String) mkEntry.get("padding-hash")) + .replace("sha", "sha-") + .toUpperCase(Locale.ROOT); + transformation += "OAEPWith" + hashName + "AndMGF1Padding"; + } else { + throw new IllegalArgumentException("Unsupported padding:" + padding); + } + final PublicKey wrappingKey; + final PrivateKey unwrappingKey; + if (key.key instanceof PublicKey) { + wrappingKey = (PublicKey) key.key; + unwrappingKey = null; + } else { + wrappingKey = null; + unwrappingKey = (PrivateKey) key.key; + } + mks.add( + JceMasterKey.getInstance( + wrappingKey, unwrappingKey, provId, key.keyId, transformation)); + } else { + throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); + } + } else { + throw new IllegalArgumentException("Unsupported Key Type: " + type); + } + } + + return MultipleProviderFactory.buildMultiProvider(mks); + }; + + @SuppressWarnings("unchecked") + final Map resultSpec = (Map) data.get("result"); + final ResultMatcher matcher = parseResultMatcher(jar, resultSpec); + + String decryptionMethodSpec = (String) data.get("decryption-method"); + SignaturePolicy signaturePolicy = SignaturePolicy.AllowEncryptAllowDecrypt; + if (decryptionMethodSpec != null) { + if ("streaming-unsigned-only".equals(decryptionMethodSpec)) { + signaturePolicy = SignaturePolicy.AllowEncryptForbidDecrypt; + } else { + throw new IllegalArgumentException( + "Unsupported Decryption Method: " + decryptionMethodSpec); + } + } + + return new TestCase( + testName, + ciphertextURL, + false, + mkpSupplier, + null, + Collections.emptyMap(), + matcher, + signaturePolicy); + } + + private static ResultMatcher parseResultMatcher( + final JarFile jar, final Map result) throws IOException { + if (result.size() != 1) { + throw new IllegalArgumentException("Unsupported result specification: " + result); + } + Map.Entry pair = result.entrySet().iterator().next(); + if (pair.getKey().equals("output")) { + Map outputSpec = (Map) pair.getValue(); + String plaintextUrl = outputSpec.get("plaintext"); + cacheData(jar, plaintextUrl); + return new OutputResultMatcher(plaintextUrl); + } else if (pair.getKey().equals("error")) { + Map errorSpec = (Map) pair.getValue(); + String errorDescription = errorSpec.get("error-description"); + return new ErrorResultMatcher(errorDescription); + } else { + throw new IllegalArgumentException("Unsupported result specification: " + result); + } + } + + @SuppressWarnings("unchecked") + private static Map parseKeyManifest(final Map keysManifest) + throws GeneralSecurityException { + // check our type + final Map metaData = (Map) keysManifest.get("manifest"); + if (!"keys".equals(metaData.get("type"))) { + throw new IllegalArgumentException("Invalid manifest type: " + metaData.get("type")); + } + if (!Integer.valueOf(3).equals(metaData.get("version"))) { + throw new IllegalArgumentException("Invalid manifest version: " + metaData.get("version")); + } + + final Map result = new HashMap<>(); + + Map keys = (Map) keysManifest.get("keys"); + for (Map.Entry entry : keys.entrySet()) { + final String name = entry.getKey(); + final Map data = (Map) entry.getValue(); + + final String keyType = (String) data.get("type"); + final String encoding = (String) data.get("encoding"); + final String keyId = (String) data.get("key-id"); + final String material = (String) data.get("material"); // May be null + final String algorithm = (String) data.get("algorithm"); // May be null + + final KeyEntry keyEntry; + + final KeyFactory kf; + switch (keyType) { + case "symmetric": + if (!"base64".equals(encoding)) { + throw new IllegalArgumentException( + format("Key %s is symmetric but has encoding %s", keyId, encoding)); + } + keyEntry = + new KeyEntry( + name, + keyId, + keyType, + new SecretKeySpec(Base64.decode(material), algorithm.toUpperCase(Locale.ROOT))); + break; + case "private": + kf = KeyFactory.getInstance(algorithm); + if (!"pem".equals(encoding)) { + throw new IllegalArgumentException( + format("Key %s is private but has encoding %s", keyId, encoding)); + } + byte[] pkcs8Key = parsePem(material); + keyEntry = + new KeyEntry( + name, keyId, keyType, kf.generatePrivate(new PKCS8EncodedKeySpec(pkcs8Key))); + break; + case "public": + kf = KeyFactory.getInstance(algorithm); + if (!"pem".equals(encoding)) { + throw new IllegalArgumentException( + format("Key %s is private but has encoding %s", keyId, encoding)); + } + byte[] x509Key = parsePem(material); + keyEntry = + new KeyEntry( + name, keyId, keyType, kf.generatePublic(new X509EncodedKeySpec(x509Key))); + break; + case "aws-kms": + keyEntry = new KeyEntry(name, keyId, keyType, null); + break; + default: + throw new IllegalArgumentException("Unsupported key type: " + keyType); + } + + result.put(name, keyEntry); + } + + return result; + } + + private static byte[] parsePem(String pem) { + final String stripped = pem.replaceAll("-+[A-Z ]+-+", ""); + return Base64.decode(stripped); + } + + private static class KeyEntry { + final String name; + final String keyId; + final String type; + final Key key; + + private KeyEntry(String name, String keyId, String type, Key key) { + this.name = name; + this.keyId = keyId; + this.type = type; + this.key = key; + } + } + + private static class TestCase { + private final String name; + private final String ciphertextPath; + private final ResultMatcher matcher; + private final boolean isKeyring; + private final Supplier> mkpSupplier; + private final Supplier cmmSupplier; + private final Map encryptionContext; + private final SignaturePolicy signaturePolicy; + + private TestCase( + String name, + String ciphertextPath, + boolean isKeyring, + Supplier> mkpSupplier, + Supplier cmmSupplier, + Map encryptionContext, + ResultMatcher matcher, + SignaturePolicy signaturePolicy) { + this.name = name; + this.ciphertextPath = ciphertextPath; + this.matcher = matcher; + this.isKeyring = isKeyring; + this.mkpSupplier = mkpSupplier; + this.cmmSupplier = cmmSupplier; + this.encryptionContext = encryptionContext; + this.signaturePolicy = signaturePolicy; + } + } + + private interface ResultMatcher { + void Match(Callable decryptor) throws Exception; + } + + private static class OutputResultMatcher implements ResultMatcher { + + private final String plaintextPath; + + private OutputResultMatcher(String plaintextPath) { + this.plaintextPath = plaintextPath; + } + + @Override + public void Match(Callable decryptor) throws Exception { + final byte[] plaintext = decryptor.call(); + final byte[] expectedPlaintext = cachedData.get(plaintextPath); + Assert.assertArrayEquals(expectedPlaintext, plaintext); + } + } + + private static class ErrorResultMatcher implements ResultMatcher { + + private final String errorDescription; + + private ErrorResultMatcher(String errorDescription) { + this.errorDescription = errorDescription; + } + + @Override + public void Match(Callable decryptor) { + Assert.assertThrows( + "Decryption expected to fail (" + errorDescription + ") but succeeded", + Exception.class, + decryptor::call); + } + } +} From bdcd2933f9d5d16d293d7c72ad8f02dc43f359a2 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Mon, 10 Jun 2024 16:38:23 -0700 Subject: [PATCH 09/19] tests, cleanup --- pom.xml | 4 +- .../amazonaws/encryptionsdk/CMMHandler.java | 6 +- .../encryptionsdk/AllTestsSuite.java | 4 +- .../encryptionsdk/CMMHandlerTest.java | 138 ++++ .../encryptionsdk/TestVectorGenerator.java | 570 -------------- .../encryptionsdk/TestVectorRunner.java | 743 ------------------ 6 files changed, 145 insertions(+), 1320 deletions(-) create mode 100644 src/test/java/com/amazonaws/encryptionsdk/CMMHandlerTest.java delete mode 100644 src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java delete mode 100644 src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java diff --git a/pom.xml b/pom.xml index 060ee735..fa3835da 100644 --- a/pom.xml +++ b/pom.xml @@ -149,8 +149,8 @@ maven-compiler-plugin 3.10.1 - 8 - 8 + 1.8 + 1.8 diff --git a/src/main/java/com/amazonaws/encryptionsdk/CMMHandler.java b/src/main/java/com/amazonaws/encryptionsdk/CMMHandler.java index 740be509..c63ca535 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/CMMHandler.java +++ b/src/main/java/com/amazonaws/encryptionsdk/CMMHandler.java @@ -84,10 +84,10 @@ public DecryptionMaterialsHandler decryptMaterials( // But custom CMMs' behavior was not updated. // However, there is no custom CMM before version 3.0 that could set an encryptionContext attribute. // The encryptionContext attribute was only introduced to decryptMaterials objects - // in ESDK 3.0, so no CMM could have set this attribute before 3.0. - // As a result, the ESDK assumes that any legacy native CMM + // in ESDK 3.0, so no CMM could have configured this attribute before 3.0. + // As a result, the ESDK assumes that any native CMM // that does not add encryptionContext to its decryptMaterials - // SHOULD add encryptionContext to its decryptMaterials. + // SHOULD add encryptionContext to its decryptMaterials, // // If a custom CMM implementation conflicts with this assumption. // that CMM implementation MUST move to the MPL. diff --git a/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java b/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java index 445f77d2..a1a9378b 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java +++ b/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java @@ -13,6 +13,7 @@ import com.amazonaws.crypto.examples.keyrings.SetEncryptionAlgorithmKeyringExampleTest; import com.amazonaws.crypto.examples.v2.BasicEncryptionExampleTest; import com.amazonaws.crypto.examples.v2.BasicMultiRegionKeyEncryptionExampleTest; +import com.amazonaws.crypto.examples.v2.CustomCMMExampleTest; import com.amazonaws.crypto.examples.v2.DiscoveryDecryptionExampleTest; import com.amazonaws.crypto.examples.v2.DiscoveryMultiRegionDecryptionExampleTest; import com.amazonaws.crypto.examples.v2.MultipleCmkEncryptExampleTest; @@ -80,8 +81,6 @@ AwsCryptoTest.class, CryptoInputStreamTest.class, CryptoOutputStreamTest.class, - TestVectorRunner.class, - TestVectorGenerator.class, XCompatDecryptTest.class, DefaultCryptoMaterialsManagerTest.class, NullCryptoMaterialsCacheTest.class, @@ -102,6 +101,7 @@ CommitmentKATRunner.class, BasicEncryptionExampleTest.class, BasicMultiRegionKeyEncryptionExampleTest.class, + CustomCMMExampleTest.class, DiscoveryDecryptionExampleTest.class, DiscoveryMultiRegionDecryptionExampleTest.class, MultipleCmkEncryptExampleTest.class, diff --git a/src/test/java/com/amazonaws/encryptionsdk/CMMHandlerTest.java b/src/test/java/com/amazonaws/encryptionsdk/CMMHandlerTest.java new file mode 100644 index 00000000..48d197fb --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/CMMHandlerTest.java @@ -0,0 +1,138 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.encryptionsdk; + +import com.amazonaws.encryptionsdk.model.DecryptionMaterials; +import com.amazonaws.encryptionsdk.model.DecryptionMaterialsHandler; +import com.amazonaws.encryptionsdk.model.DecryptionMaterialsRequest; +import com.amazonaws.encryptionsdk.model.KeyBlob; +import org.junit.Test; + +import java.security.PublicKey; +import java.util.*; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class CMMHandlerTest { + + // + private static final CryptoAlgorithm SOME_CRYPTO_ALGORITHM = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384; + private static final List SOME_EDK_LIST = new ArrayList<>(Collections.singletonList(new KeyBlob())); + private static final CommitmentPolicy SOME_COMMITMENT_POLICY = CommitmentPolicy.RequireEncryptRequireDecrypt; + private static final Map SOME_NON_EMPTY_ENCRYPTION_CONTEXT = new HashMap<>(); + + static {{ + SOME_NON_EMPTY_ENCRYPTION_CONTEXT.put("SomeKey", "SomeValue"); + }} + + private static final DecryptionMaterialsRequest SOME_DECRYPTION_MATERIALS_REQUEST_NON_EMPTY_EC = + DecryptionMaterialsRequest.newBuilder() + .setAlgorithm(SOME_CRYPTO_ALGORITHM) + // Given: Request has some non-empty encryption context + .setEncryptionContext(SOME_NON_EMPTY_ENCRYPTION_CONTEXT) + .setReproducedEncryptionContext(new HashMap<>()) + .setEncryptedDataKeys(SOME_EDK_LIST) + .build(); + + private static final DecryptionMaterialsRequest SOME_DECRYPTION_MATERIALS_REQUEST_EMPTY_EC = + DecryptionMaterialsRequest.newBuilder() + .setAlgorithm(SOME_CRYPTO_ALGORITHM) + // Given: Request has empty encryption context + .setEncryptionContext(new HashMap<>()) + .setReproducedEncryptionContext(new HashMap<>()) + .setEncryptedDataKeys(SOME_EDK_LIST) + .build(); + + @Test + public void GIVEN_CMM_does_not_add_encryption_context_AND_request_has_nonempty_encryption_context_WHEN_decryptMaterials_THEN_output_has_nonempty_encryption_context() { + CryptoMaterialsManager anyNativeCMM = mock(CryptoMaterialsManager.class); + + // Given: native CMM does not set an encryptionContext on returned DecryptionMaterials objects + DecryptionMaterials someDecryptionMaterialsWithoutEC = DecryptionMaterials.newBuilder() + .setDataKey(mock(DataKey.class)) + .setTrailingSignatureKey(mock(PublicKey.class)) + .setEncryptionContext(new HashMap<>()).build(); + // Given: request with nonempty encryption context + when(anyNativeCMM.decryptMaterials(SOME_DECRYPTION_MATERIALS_REQUEST_NON_EMPTY_EC)) + .thenReturn(someDecryptionMaterialsWithoutEC); + + // When: decryptMaterials + CMMHandler handlerUnderTest = new CMMHandler(anyNativeCMM); + DecryptionMaterialsHandler output = handlerUnderTest.decryptMaterials(SOME_DECRYPTION_MATERIALS_REQUEST_NON_EMPTY_EC, + SOME_COMMITMENT_POLICY); + + // Then: output DecryptionMaterialsHandler has encryption context + assertEquals(SOME_NON_EMPTY_ENCRYPTION_CONTEXT, output.getEncryptionContext()); + } + + @Test + public void GIVEN_CMM_does_not_add_encryption_context_AND_request_has_empty_encryption_context_WHEN_decryptMaterials_THEN_output_has_empty_encryption_context() { + CryptoMaterialsManager anyNativeCMM = mock(CryptoMaterialsManager.class); + + // Given: native CMM does not set an encryptionContext on returned DecryptionMaterials objects + DecryptionMaterials someDecryptionMaterialsWithoutEC = DecryptionMaterials.newBuilder() + .setDataKey(mock(DataKey.class)) + .setTrailingSignatureKey(mock(PublicKey.class)) + .setEncryptionContext(new HashMap<>()).build(); + // Given: request with empty encryption context + when(anyNativeCMM.decryptMaterials(SOME_DECRYPTION_MATERIALS_REQUEST_EMPTY_EC)) + .thenReturn(someDecryptionMaterialsWithoutEC); + + // When: decryptMaterials + CMMHandler handlerUnderTest = new CMMHandler(anyNativeCMM); + DecryptionMaterialsHandler output = handlerUnderTest.decryptMaterials(SOME_DECRYPTION_MATERIALS_REQUEST_EMPTY_EC, + SOME_COMMITMENT_POLICY); + + // Then: output DecryptionMaterialsHandler has empty encryption context + assertTrue(output.getEncryptionContext().isEmpty()); + } + + @Test + public void GIVEN_CMM_adds_encryption_context_AND_request_has_nonempty_encryption_context_WHEN_decryptMaterials_THEN_output_has_nonempty_encryption_context() { + CryptoMaterialsManager anyNativeCMM = mock(CryptoMaterialsManager.class); + + // Given: native CMM sets encryptionContext on returned DecryptionMaterials objects + DecryptionMaterials someDecryptionMaterialsWithoutEC = DecryptionMaterials.newBuilder() + .setDataKey(mock(DataKey.class)) + .setTrailingSignatureKey(mock(PublicKey.class)) + .setEncryptionContext(SOME_NON_EMPTY_ENCRYPTION_CONTEXT).build(); + // Given: request with nonempty encryption context + when(anyNativeCMM.decryptMaterials(SOME_DECRYPTION_MATERIALS_REQUEST_NON_EMPTY_EC)) + .thenReturn(someDecryptionMaterialsWithoutEC); + + // When: decryptMaterials + CMMHandler handlerUnderTest = new CMMHandler(anyNativeCMM); + DecryptionMaterialsHandler output = handlerUnderTest.decryptMaterials(SOME_DECRYPTION_MATERIALS_REQUEST_NON_EMPTY_EC, + SOME_COMMITMENT_POLICY); + + // Then: output DecryptionMaterialsHandler has nonempty encryption context + assertEquals(SOME_NON_EMPTY_ENCRYPTION_CONTEXT, output.getEncryptionContext()); + } + + @Test + public void GIVEN_CMM_adds_encryption_context_AND_request_has_empty_encryption_context_WHEN_decryptMaterials_THEN_output_has_empty_encryption_context() { + CryptoMaterialsManager anyNativeCMM = mock(CryptoMaterialsManager.class); + + // Given: native CMM sets encryptionContext on returned DecryptionMaterials objects + DecryptionMaterials someDecryptionMaterialsWithoutEC = DecryptionMaterials.newBuilder() + .setDataKey(mock(DataKey.class)) + .setTrailingSignatureKey(mock(PublicKey.class)) + .setEncryptionContext(new HashMap<>()).build(); + // Given: request with empty encryption context + when(anyNativeCMM.decryptMaterials(SOME_DECRYPTION_MATERIALS_REQUEST_EMPTY_EC)) + .thenReturn(someDecryptionMaterialsWithoutEC); + + // When: decryptMaterials + CMMHandler handlerUnderTest = new CMMHandler(anyNativeCMM); + DecryptionMaterialsHandler output = handlerUnderTest.decryptMaterials(SOME_DECRYPTION_MATERIALS_REQUEST_EMPTY_EC, + SOME_COMMITMENT_POLICY); + + // Then: output DecryptionMaterialsHandler has empty encryption context + assertTrue(output.getEncryptionContext().isEmpty()); + } + +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java b/src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java deleted file mode 100644 index eb2b1764..00000000 --- a/src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java +++ /dev/null @@ -1,570 +0,0 @@ -// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.amazonaws.encryptionsdk; - -import static java.lang.String.format; - -import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; -import com.amazonaws.encryptionsdk.jce.JceMasterKey; -import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; -import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; -import java.net.URL; -import java.nio.ByteBuffer; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.nio.file.attribute.FileAttribute; -import java.security.GeneralSecurityException; -import java.security.Key; -import java.security.KeyFactory; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.concurrent.Callable; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import org.apache.commons.io.FileUtils; -import org.bouncycastle.util.encoders.Base64; -import org.junit.AfterClass; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import software.amazon.awssdk.utils.ImmutableMap; -import software.amazon.cryptography.materialproviders.IKeyring; -import software.amazon.cryptography.materialproviders.MaterialProviders; -import software.amazon.cryptography.materialproviders.model.CreateMultiKeyringInput; -import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig; -import software.amazon.cryptography.materialproviderstestvectorkeys.KeyVectors; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionInput; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionOutput; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.KeyVectorsConfig; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.TestVectorKeyringInput; - -@RunWith(Parameterized.class) -public class TestVectorGenerator { - - private static final String encryptManifestList = - "https://raw.githubusercontent.com/awslabs/aws-crypto-tools-test-vector-framework/master/features/CANONICAL-GENERATED-MANIFESTS/0003-awses-message-encryption.v2.json"; - // We save the files in memory to avoid repeatedly retrieving them. This won't work if the - // plaintexts are too - // large or numerous - private static final Map cachedData = new HashMap<>(); - private static final ObjectMapper mapper = new ObjectMapper(); - private static EncryptionInterface encryption; - private static boolean isMasterKey; - private final String testName; - private final TestCase testCase; - - // Temp Test Vectors Directory - private static String tempTestVectorPath; - // Zip File Path - private static String zipFilePath; - - public TestVectorGenerator(final String testName, TestCase testCase) { - this.testName = testName; - this.testCase = testCase; - } - - // Zip Temp Folder and delete temp files - @AfterClass - public static void zip() throws IOException { - Path zipFile = Files.createFile(Paths.get(zipFilePath)); - - Path sourceDirPath = Paths.get(tempTestVectorPath); - try (ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(zipFile)); - Stream paths = Files.walk(sourceDirPath)) { - paths - .filter(path -> !Files.isDirectory(path)) - .forEach( - path -> { - ZipEntry zipEntry = new ZipEntry(sourceDirPath.relativize(path).toString()); - try { - zipOutputStream.putNextEntry(zipEntry); - Files.copy(path, zipOutputStream); - zipOutputStream.closeEntry(); - } catch (IOException e) { - throw new UncheckedIOException("Unable to Zip File", e); - } - }); - } - FileUtils.deleteQuietly(sourceDirPath.toFile()); - - // Teardown - cachedData.clear(); - } - - @Test - @SuppressWarnings("unchecked") - public void encrypt() throws Exception { - CryptoAlgorithm cryptoAlgorithm = getCryptoAlgorithm(testCase.algorithmId); - CommitmentPolicy commitmentPolicy = - cryptoAlgorithm.isCommitting() - ? CommitmentPolicy.RequireEncryptRequireDecrypt - : CommitmentPolicy.ForbidEncryptAllowDecrypt; - - AwsCrypto crypto = - AwsCrypto.builder() - .withCommitmentPolicy(commitmentPolicy) - .withEncryptionAlgorithm(cryptoAlgorithm) - .withEncryptionFrameSize(testCase.frameSize) - .build(); - - Callable ciphertext; - if (isMasterKey) { - ciphertext = - () -> - crypto - .encryptData( - testCase.masterKey, - cachedData.get(testCase.plaintext), - testCase.encryptionContext) - .getResult(); - } else { - ciphertext = - () -> - crypto - .encryptData( - testCase.keyring, - cachedData.get(testCase.plaintext), - testCase.encryptionContext) - .getResult(); - } - Files.write(Paths.get(tempTestVectorPath + "ciphertexts/" + testName), ciphertext.call()); - } - - private static CryptoAlgorithm getCryptoAlgorithm(String algorithmId) { - Integer algId = Integer.parseInt(algorithmId, 16); - for (CryptoAlgorithm cryptoAlgorithm : CryptoAlgorithm.values()) { - if (cryptoAlgorithm.getValue() == algId) { - return cryptoAlgorithm; - } - } - throw new IllegalArgumentException("Invalid AlgorithmId: " + algorithmId); - } - - @Parameterized.Parameters(name = "Compatibility Test: {0} - {1}") - @SuppressWarnings("unchecked") - public static Collection data() throws Exception { - final String interfaceOption = System.getProperty("masterkey"); - - if (interfaceOption != null && interfaceOption.equals("true")) { - isMasterKey = true; - encryption = EncryptionInterface.EncryptWithMasterKey; - } else { - encryption = EncryptionInterface.EncryptWithKeyring; - } - - final String encryptKeyManifest = System.getProperty("keysManifest"); - if (encryptKeyManifest == null) { - return Collections.emptyList(); - } - - zipFilePath = System.getProperty("zipFilePath"); - if (zipFilePath == null) { - return Collections.emptyList(); - } - - tempTestVectorPath = Files.createTempDirectory("java", new FileAttribute[0]).toString() + "/"; - createDirectories(tempTestVectorPath + "ciphertexts/"); - createDirectories(tempTestVectorPath + "plaintexts/"); - - File decryptManifest = new File(tempTestVectorPath + "manifest.json"); - File keyManifest = new File(tempTestVectorPath + "keys.json"); - - final Map manifest = mapper.readValue(new URL(encryptManifestList), Map.class); - mapper - .writerWithDefaultPrettyPrinter() - .writeValue(decryptManifest, createDecryptManifest(manifest)); - - try (InputStream in = new FileInputStream(encryptKeyManifest)) { - Files.copy(in, keyManifest.toPath(), StandardCopyOption.REPLACE_EXISTING); - } - - final Map keysManifest = - mapper.readValue(new File(encryptKeyManifest), Map.class); - - cachePlaintext((Map) manifest.get("plaintexts")); - - MaterialProvidersConfig config = MaterialProvidersConfig.builder().build(); - MaterialProviders materialProviders = - MaterialProviders.builder().MaterialProvidersConfig(config).build(); - KeyVectors keyVectors = - KeyVectors.builder() - .KeyVectorsConfig( - KeyVectorsConfig.builder().keyManifiestPath(keyManifest.toString()).build()) - .build(); - - final Map keys = parseKeyManifest(keysManifest); - final KmsMasterKeyProvider kmsProv = - KmsMasterKeyProvider.builder() - .withCredentials(new DefaultAWSCredentialsProviderChain()) - .buildDiscovery(); - - return ((Map>) manifest.get("tests")) - .entrySet().stream() - .map( - entry -> { - String testName = entry.getKey(); - TestCase testCase = - encryption.parseTest(entry, keys, kmsProv, materialProviders, keyVectors); - return new Object[] {testName, testCase}; - }) - .collect(Collectors.toList()); - } - - private static void createDirectories(String path) { - File directory = new File(path); - directory.mkdirs(); - } - - private enum EncryptionInterface { - EncryptWithMasterKey { - @Override - public TestCase parseTest( - Map.Entry> testEntry, - Map keys, - KmsMasterKeyProvider kmsProv, - MaterialProviders materialProviders, - KeyVectors keyVectors) { - return parseTestWithMasterkeys(testEntry, keys, kmsProv); - } - }, - EncryptWithKeyring { - @Override - public TestCase parseTest( - Map.Entry> testEntry, - Map keys, - KmsMasterKeyProvider kmsProv, - MaterialProviders materialProviders, - KeyVectors keyVectors) { - return parseTestWithKeyrings(testEntry, materialProviders, keyVectors); - } - }; - - public abstract TestCase parseTest( - Map.Entry> testEntry, - Map keys, - KmsMasterKeyProvider kmsProv, - MaterialProviders materialProviders, - KeyVectors keyVectors); - } - - private static void cachePlaintext(Map plaintexts) { - Random rd = new Random(); - plaintexts.forEach( - (key, value) -> { - byte[] plaintext = new byte[value]; - rd.nextBytes(plaintext); - try { - Files.write(new File(tempTestVectorPath + "plaintexts/" + key).toPath(), plaintext); - cachedData.put(key, plaintext); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); - } - - private static Map createDecryptManifest(Map encryptManifest) { - Map decryptManifest = new LinkedHashMap<>(); - - decryptManifest.put("manifest", ImmutableMap.of("type", "awses-decrypt", "version", 2)); - - decryptManifest.put( - "client", ImmutableMap.of("name", "aws/aws-encryption-sdk-java", "version", "2.2.0")); - - decryptManifest.put("keys", "file://keys.json"); - - Map> testScenarios = - ((LinkedHashMap>) encryptManifest.get("tests")) - .entrySet().stream() - .collect( - Collectors.toMap( - Map.Entry::getKey, - entry -> { - Map scenario = entry.getValue(); - return new LinkedHashMap() { - { - put("ciphertext", "file://ciphertexts/" + entry.getKey()); - put("master-keys", scenario.get("master-keys")); - put( - "result", - Collections.singletonMap( - "output", - Collections.singletonMap( - "plaintext", - "file://plaintexts/" + scenario.get("plaintext")))); - } - }; - })); - - decryptManifest.put("tests", testScenarios); - return decryptManifest; - } - - private static TestCase parseTestWithMasterkeys( - Map.Entry> testEntry, - Map keys, - KmsMasterKeyProvider kmsProv) { - - String testName = testEntry.getKey(); - Map data = testEntry.getValue(); - - String plaintext = (String) data.get("plaintext"); - String algorithmId = (String) data.get("algorithm"); - int frameSize = (int) data.get("frame-size"); - Map encryptionContext = (Map) data.get("encryption-context"); - - final List> mks = new ArrayList<>(); - - for (Map mkEntry : (List>) data.get("master-keys")) { - if (mkEntry.get("key").equals("rsa-4096-private")) { - mkEntry.replace("key", "rsa-4096-public"); - } - - final String type = mkEntry.get("type"); - final String keyName = mkEntry.get("key"); - final KeyEntry key = keys.get(keyName); - - if ("aws-kms".equals(type)) { - mks.add(kmsProv.getMasterKey(key.keyId)); - } else if ("raw".equals(type)) { - final String provId = mkEntry.get("provider-id"); - final String algorithm = mkEntry.get("encryption-algorithm"); - if ("aes".equals(algorithm)) { - mks.add( - JceMasterKey.getInstance( - (SecretKey) key.key, provId, key.keyId, "AES/GCM/NoPadding")); - } else if ("rsa".equals(algorithm)) { - String transformation = "RSA/ECB/"; - final String padding = mkEntry.get("padding-algorithm"); - if ("pkcs1".equals(padding)) { - transformation += "PKCS1Padding"; - } else if ("oaep-mgf1".equals(padding)) { - final String hashName = - mkEntry.get("padding-hash").replace("sha", "sha-").toUpperCase(); - transformation += "OAEPWith" + hashName + "AndMGF1Padding"; - } else { - throw new IllegalArgumentException("Unsupported padding:" + padding); - } - final PublicKey wrappingKey; - final PrivateKey unwrappingKey; - if (key.key instanceof PublicKey) { - wrappingKey = (PublicKey) key.key; - unwrappingKey = null; - } else { - wrappingKey = null; - unwrappingKey = (PrivateKey) key.key; - } - mks.add( - JceMasterKey.getInstance( - wrappingKey, unwrappingKey, provId, key.keyId, transformation)); - } else { - throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); - } - } else { - throw new IllegalArgumentException("Unsupported Key Type: " + type); - } - } - - MasterKeyProvider multiProvider = MultipleProviderFactory.buildMultiProvider(mks); - - return new TestCase( - testName, null, multiProvider, plaintext, algorithmId, frameSize, encryptionContext); - } - - private static TestCase parseTestWithKeyrings( - Map.Entry> testEntry, - MaterialProviders materialProviders, - KeyVectors keyVectors) { - String testName = testEntry.getKey(); - Map data = testEntry.getValue(); - - String plaintext = (String) data.get("plaintext"); - String algorithmId = (String) data.get("algorithm"); - int frameSize = (int) data.get("frame-size"); - Map encryptionContext = (Map) data.get("encryption-context"); - - List keyrings = new ArrayList<>(); - - ((List>) data.get("master-keys")) - .forEach( - mkEntry -> { - if (mkEntry.get("type").equals("raw") - && mkEntry.get("encryption-algorithm").equals("rsa")) { - if (mkEntry.get("key").equals("rsa-4096-private")) { - mkEntry.replace("key", "rsa-4096-public"); - } - mkEntry.putIfAbsent("padding-hash", "sha1"); - } - - try { - byte[] json = new ObjectMapper().writeValueAsBytes(mkEntry); - GetKeyDescriptionOutput output = - keyVectors.GetKeyDescription( - GetKeyDescriptionInput.builder().json(ByteBuffer.wrap(json)).build()); - - IKeyring testVectorKeyring = - keyVectors.CreateTestVectorKeyring( - TestVectorKeyringInput.builder() - .keyDescription(output.keyDescription()) - .build()); - - keyrings.add(testVectorKeyring); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - }); - - IKeyring primary = keyrings.remove(0); - IKeyring multiKeyring = - materialProviders.CreateMultiKeyring( - CreateMultiKeyringInput.builder().generator(primary).childKeyrings(keyrings).build()); - - return new TestCase( - testName, multiKeyring, null, plaintext, algorithmId, frameSize, encryptionContext); - } - - @SuppressWarnings("unchecked") - private static Map parseKeyManifest(final Map keysManifest) - throws GeneralSecurityException { - // check our type - final Map metaData = (Map) keysManifest.get("manifest"); - if (!"keys".equals(metaData.get("type"))) { - throw new IllegalArgumentException("Invalid manifest type: " + metaData.get("type")); - } - if (!Integer.valueOf(3).equals(metaData.get("version"))) { - throw new IllegalArgumentException("Invalid manifest version: " + metaData.get("version")); - } - - final Map result = new HashMap<>(); - - Map keys = (Map) keysManifest.get("keys"); - for (Map.Entry entry : keys.entrySet()) { - final String name = entry.getKey(); - final Map data = (Map) entry.getValue(); - - final String keyType = (String) data.get("type"); - final String encoding = (String) data.get("encoding"); - final String keyId = (String) data.get("key-id"); - final String material = (String) data.get("material"); // May be null - final String algorithm = (String) data.get("algorithm"); // May be null - - final KeyEntry keyEntry; - - final KeyFactory kf; - switch (keyType) { - case "symmetric": - if (!"base64".equals(encoding)) { - throw new IllegalArgumentException( - format("Key %s is symmetric but has encoding %s", keyId, encoding)); - } - keyEntry = - new KeyEntry( - name, - keyId, - keyType, - new SecretKeySpec(Base64.decode(material), algorithm.toUpperCase())); - break; - case "private": - kf = KeyFactory.getInstance(algorithm); - if (!"pem".equals(encoding)) { - throw new IllegalArgumentException( - format("Key %s is private but has encoding %s", keyId, encoding)); - } - byte[] pkcs8Key = parsePem(material); - keyEntry = - new KeyEntry( - name, keyId, keyType, kf.generatePrivate(new PKCS8EncodedKeySpec(pkcs8Key))); - break; - case "public": - kf = KeyFactory.getInstance(algorithm); - if (!"pem".equals(encoding)) { - throw new IllegalArgumentException( - format("Key %s is private but has encoding %s", keyId, encoding)); - } - byte[] x509Key = parsePem(material); - keyEntry = - new KeyEntry( - name, keyId, keyType, kf.generatePublic(new X509EncodedKeySpec(x509Key))); - break; - case "aws-kms": - keyEntry = new KeyEntry(name, keyId, keyType, null); - break; - default: - throw new IllegalArgumentException("Unsupported key type: " + keyType); - } - - result.put(name, keyEntry); - } - - return result; - } - - private static byte[] parsePem(String pem) { - final String stripped = pem.replaceAll("-+[A-Z ]+-+", ""); - return Base64.decode(stripped); - } - - private static class KeyEntry { - final String name; - final String keyId; - final String type; - final Key key; - - private KeyEntry(String name, String keyId, String type, Key key) { - this.name = name; - this.keyId = keyId; - this.type = type; - this.key = key; - } - } - - private static class TestCase { - private final String name; - private final IKeyring keyring; - private final MasterKeyProvider masterKey; - private final String plaintext; - private final String algorithmId; - private final int frameSize; - private final Map encryptionContext; - - public TestCase( - String name, - IKeyring keyring, - MasterKeyProvider multiProvider, - String plaintext, - String algorithmId, - int frameSize, - Map encryptionContext) { - this.name = name; - this.keyring = keyring; - this.masterKey = multiProvider; - this.plaintext = plaintext; - this.algorithmId = algorithmId; - this.frameSize = frameSize; - this.encryptionContext = encryptionContext; - } - } -} diff --git a/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java b/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java deleted file mode 100644 index 8c0be82f..00000000 --- a/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java +++ /dev/null @@ -1,743 +0,0 @@ -// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.amazonaws.encryptionsdk; - -import static java.lang.String.format; - -import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; -import com.amazonaws.encryptionsdk.internal.SignaturePolicy; -import com.amazonaws.encryptionsdk.jce.JceMasterKey; -import com.amazonaws.encryptionsdk.kms.AwsKmsMrkAwareMasterKeyProvider; -import com.amazonaws.encryptionsdk.kms.DiscoveryFilter; -import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; -import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory; -import com.amazonaws.util.IOUtils; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.JarURLConnection; -import java.net.URL; -import java.nio.ByteBuffer; -import java.security.GeneralSecurityException; -import java.security.Key; -import java.security.KeyFactory; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.Callable; -import java.util.function.Supplier; -import java.util.jar.JarFile; -import java.util.zip.ZipEntry; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import junit.framework.TestCase; -import org.bouncycastle.util.encoders.Base64; -import org.junit.AfterClass; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import software.amazon.awssdk.regions.Region; -import software.amazon.cryptography.materialproviders.ICryptographicMaterialsManager; -import software.amazon.cryptography.materialproviders.IKeyring; -import software.amazon.cryptography.materialproviders.MaterialProviders; -import software.amazon.cryptography.materialproviders.model.CreateDefaultCryptographicMaterialsManagerInput; -import software.amazon.cryptography.materialproviders.model.CreateMultiKeyringInput; -import software.amazon.cryptography.materialproviders.model.CreateRequiredEncryptionContextCMMInput; -import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig; -import software.amazon.cryptography.materialproviderstestvectorkeys.KeyVectors; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionInput; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionOutput; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.KeyVectorsConfig; -import software.amazon.cryptography.materialproviderstestvectorkeys.model.TestVectorKeyringInput; - -@RunWith(Parameterized.class) -public class TestVectorRunner { - - // TODO: Standardize Manifest Version - private static final List MANIFEST_VERSIONS = Arrays.asList(2, 4); - - // We save the files in memory to avoid repeatedly retrieving them. This won't work if the - // plaintexts are too - // large or numerous - private static final Map cachedData = new HashMap<>(); - - private final String testName; - private final TestCase testCase; - private final DecryptionMethod decryptionMethod; - - public TestVectorRunner( - final String testName, TestCase testCase, DecryptionMethod decryptionMethod) { - this.testName = testName; - this.testCase = testCase; - this.decryptionMethod = decryptionMethod; - } - - @Test - public void decrypt() throws Exception { - AwsCrypto crypto = - AwsCrypto.builder() - .withCommitmentPolicy(CommitmentPolicy.ForbidEncryptAllowDecrypt) - .build(); - Callable decryptor; - if (testCase.isKeyring) { - decryptor = - () -> - decryptionMethod.decryptMessage( - crypto, - testCase.cmmSupplier.get(), - cachedData.get(testCase.ciphertextPath), - testCase.encryptionContext); - } else { - decryptor = - () -> - decryptionMethod.decryptMessage( - crypto, testCase.mkpSupplier.get(), cachedData.get(testCase.ciphertextPath)); - } - testCase.matcher.Match(decryptor); - } - - @Parameterized.Parameters(name = "Compatibility Test: {0} - {3}") - @SuppressWarnings("unchecked") - public static Collection data() throws Exception { - final String zipPath = System.getProperty("testVectorZip"); - final String interfaceOption = System.getProperty("masterkey"); - if (zipPath == null) { - return Collections.emptyList(); - } - - final JarURLConnection jarConnection = - (JarURLConnection) new URL("jar:" + zipPath + "!/").openConnection(); - - try (JarFile jar = jarConnection.getJarFile()) { - - final Map manifest = readJsonMapFromJar(jar, "manifest.json"); - final Map keysManifest = readJsonMapFromJar(jar, "keys.json"); - - ObjectMapper objectMapper = new ObjectMapper(); - - // Create a temporary file and write the JSON string to it - File tempFile = File.createTempFile("keys", ".json"); - objectMapper.writeValue(tempFile, keysManifest); - - final Map metaData = (Map) manifest.get("manifest"); - - // We only support "awses-decrypt" type manifests right now - if (!"awses-decrypt".equals(metaData.get("type"))) { - throw new IllegalArgumentException("Unsupported manifest type: " + metaData.get("type")); - } - Integer readVersion = (Integer) metaData.get("version"); - if (!MANIFEST_VERSIONS.contains(readVersion)) { - throw new IllegalArgumentException( - "Unsupported manifest version: " + metaData.get("version")); - } - - final Map keys = - parseKeyManifest(readJsonMapFromJar(jar, (String) manifest.get("keys"))); - - KeyVectors keyVectors = - KeyVectors.builder() - .KeyVectorsConfig( - KeyVectorsConfig.builder().keyManifiestPath(tempFile.getPath()).build()) - .build(); - - MaterialProvidersConfig config = MaterialProvidersConfig.builder().build(); - MaterialProviders materialProviders = - MaterialProviders.builder().MaterialProvidersConfig(config).build(); - - final KmsMasterKeyProvider kmsProvV1 = - KmsMasterKeyProvider.builder() - .withCredentials(new DefaultAWSCredentialsProviderChain()) - .buildDiscovery(); - - final com.amazonaws.encryptionsdk.kmssdkv2.KmsMasterKeyProvider kmsProvV2 = - com.amazonaws.encryptionsdk.kmssdkv2.KmsMasterKeyProvider.builder().buildDiscovery(); - - List testCases = new ArrayList<>(); - for (Map.Entry> testEntry : - ((Map>) manifest.get("tests")).entrySet()) { - if (interfaceOption != null && interfaceOption.equals("true")) { - String testName = testEntry.getKey(); - - TestCase testCaseV1 = - parseTest(testEntry.getKey(), testEntry.getValue(), keys, jar, kmsProvV1); - TestCase testCaseV2 = - parseTest(testEntry.getKey(), testEntry.getValue(), keys, jar, kmsProvV2); - - for (DecryptionMethod decryptionMethod : DecryptionMethod.values()) { - if (testCaseV1.signaturePolicy.equals(decryptionMethod.signaturePolicy())) { - testCases.add(new Object[] {testName, testCaseV1, decryptionMethod}); - testCases.add(new Object[] {testName + "-V2", testCaseV2, decryptionMethod}); - } - } - } else { - String testName = testEntry.getKey(); - TestCase testCaseKeyring = - parseTest( - testEntry.getKey(), - testEntry.getValue(), - keys, - jar, - materialProviders, - keyVectors); - - for (DecryptionMethod decryptionMethod : DecryptionMethod.values()) { - if (testCaseKeyring.signaturePolicy.equals(decryptionMethod.signaturePolicy())) { - testCases.add( - new Object[] {testName + "-Keyrings", testCaseKeyring, decryptionMethod}); - } - } - } - } - return testCases; - } - } - - @AfterClass - public static void teardown() { - cachedData.clear(); - } - - private static byte[] readBytesFromJar(JarFile jar, String fileName) throws IOException { - try (InputStream is = readFromJar(jar, fileName)) { - return IOUtils.toByteArray(is); - } - } - - private static Map readJsonMapFromJar(JarFile jar, String fileName) - throws IOException { - try (InputStream is = readFromJar(jar, fileName)) { - final ObjectMapper mapper = new ObjectMapper(); - return mapper.readValue(is, new TypeReference>() {}); - } - } - - private static InputStream readFromJar(JarFile jar, String name) throws IOException { - // Our manifest URIs incorrectly start with file:// rather than just file: so we need to strip - // this - ZipEntry entry = jar.getEntry(name.replaceFirst("^file://(?!/)", "")); - return jar.getInputStream(entry); - } - - private static void cacheData(JarFile jar, String url) throws IOException { - if (!cachedData.containsKey(url)) { - cachedData.put(url, readBytesFromJar(jar, url)); - } - } - - /** Parse Test to Keyring */ - @SuppressWarnings("unchecked") - private static TestCase parseTest( - String testName, - Map data, - Map keys, - JarFile jar, - MaterialProviders materialProviders, - KeyVectors keyVectors) - throws IOException { - final String ciphertextURL = (String) data.get("ciphertext"); - cacheData(jar, ciphertextURL); - - Supplier cmmSupplier = - () -> { - final List keyrings = new ArrayList<>(); - for (Map mkEntry : (List>) data.get("master-keys")) { - if (mkEntry.get("type").equals("raw") - && mkEntry.get("encryption-algorithm").equals("rsa")) { - mkEntry.putIfAbsent("padding-hash", "sha1"); - } - - try { - byte[] json = new ObjectMapper().writeValueAsBytes(mkEntry); - GetKeyDescriptionOutput output = - keyVectors.GetKeyDescription( - GetKeyDescriptionInput.builder().json(ByteBuffer.wrap(json)).build()); - - IKeyring testVectorKeyring = - keyVectors.CreateTestVectorKeyring( - TestVectorKeyringInput.builder() - .keyDescription(output.keyDescription()) - .build()); - - keyrings.add(testVectorKeyring); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - - IKeyring multiKeyring = - materialProviders.CreateMultiKeyring( - CreateMultiKeyringInput.builder() - .generator(keyrings.get(0)) - .childKeyrings(keyrings) - .build()); - - CreateDefaultCryptographicMaterialsManagerInput defaultInput = - CreateDefaultCryptographicMaterialsManagerInput.builder() - .keyring(multiKeyring) - .build(); - ICryptographicMaterialsManager cmm = - materialProviders.CreateDefaultCryptographicMaterialsManager(defaultInput); - if (data.containsKey("cmm") && data.get("cmm").equals("RequiredEncryptionContext")) { - List requiredKeys = new ArrayList(2); - requiredKeys.add("key1"); - requiredKeys.add("key2"); - CreateRequiredEncryptionContextCMMInput requiredCMMInput = - CreateRequiredEncryptionContextCMMInput.builder() - .underlyingCMM( - materialProviders.CreateDefaultCryptographicMaterialsManager(defaultInput)) - .requiredEncryptionContextKeys(requiredKeys) - .build(); - cmm = materialProviders.CreateRequiredEncryptionContextCMM(requiredCMMInput); - } - return cmm; - }; - @SuppressWarnings("unchecked") - final Map resultSpec = (Map) data.get("result"); - final ResultMatcher matcher = parseResultMatcher(jar, resultSpec); - - Map ec = Collections.emptyMap(); - - if (data.get("encryption-context") != null) { - ec = (Map) data.get("encryption-context"); - } - - String decryptionMethodSpec = (String) data.get("decryption-method"); - SignaturePolicy signaturePolicy = SignaturePolicy.AllowEncryptAllowDecrypt; - if (decryptionMethodSpec != null) { - if ("streaming-unsigned-only".equals(decryptionMethodSpec)) { - signaturePolicy = SignaturePolicy.AllowEncryptForbidDecrypt; - } else { - throw new IllegalArgumentException( - "Unsupported Decryption Method: " + decryptionMethodSpec); - } - } - - return new TestCase( - testName, ciphertextURL, true, null, cmmSupplier, ec, matcher, signaturePolicy); - } - - /** Parse Test to MasterKey for AWS SDK v1 */ - @SuppressWarnings("unchecked") - private static TestCase parseTest( - String testName, - Map data, - Map keys, - JarFile jar, - KmsMasterKeyProvider kmsProv) - throws IOException { - final String ciphertextURL = (String) data.get("ciphertext"); - cacheData(jar, ciphertextURL); - - Supplier> mkpSupplier = - () -> { - @SuppressWarnings("generic") - final List> mks = new ArrayList<>(); - - for (Map mkEntry : (List>) data.get("master-keys")) { - final String type = (String) mkEntry.get("type"); - final String keyName = (String) mkEntry.get("key"); - final KeyEntry key = keys.get(keyName); - - if ("aws-kms".equals(type)) { - mks.add(kmsProv.getMasterKey(key.keyId)); - } else if ("aws-kms-mrk-aware".equals(type)) { - AwsKmsMrkAwareMasterKeyProvider provider = - AwsKmsMrkAwareMasterKeyProvider.builder().buildStrict(key.keyId); - mks.add(provider.getMasterKey(key.keyId)); - } else if ("aws-kms-mrk-aware-discovery".equals(type)) { - final String defaultMrkRegion = (String) mkEntry.get("default-mrk-region"); - final Map discoveryFilterSpec = - (Map) mkEntry.get("aws-kms-discovery-filter"); - final DiscoveryFilter discoveryFilter; - if (discoveryFilterSpec != null) { - discoveryFilter = - new DiscoveryFilter( - (String) discoveryFilterSpec.get("partition"), - (List) discoveryFilterSpec.get("account-ids")); - } else { - discoveryFilter = null; - } - return AwsKmsMrkAwareMasterKeyProvider.builder() - .withDiscoveryMrkRegion(defaultMrkRegion) - .buildDiscovery(discoveryFilter); - } else if ("raw".equals(type)) { - final String provId = (String) mkEntry.get("provider-id"); - final String algorithm = (String) mkEntry.get("encryption-algorithm"); - if ("aes".equals(algorithm)) { - mks.add( - JceMasterKey.getInstance( - (SecretKey) key.key, provId, key.keyId, "AES/GCM/NoPadding")); - } else if ("rsa".equals(algorithm)) { - String transformation = "RSA/ECB/"; - final String padding = (String) mkEntry.get("padding-algorithm"); - if ("pkcs1".equals(padding)) { - transformation += "PKCS1Padding"; - } else if ("oaep-mgf1".equals(padding)) { - final String hashName = - ((String) mkEntry.get("padding-hash")) - .replace("sha", "sha-") - .toUpperCase(Locale.ROOT); - transformation += "OAEPWith" + hashName + "AndMGF1Padding"; - } else { - throw new IllegalArgumentException("Unsupported padding:" + padding); - } - final PublicKey wrappingKey; - final PrivateKey unwrappingKey; - if (key.key instanceof PublicKey) { - wrappingKey = (PublicKey) key.key; - unwrappingKey = null; - } else { - wrappingKey = null; - unwrappingKey = (PrivateKey) key.key; - } - mks.add( - JceMasterKey.getInstance( - wrappingKey, unwrappingKey, provId, key.keyId, transformation)); - } else { - throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); - } - } else { - throw new IllegalArgumentException("Unsupported Key Type: " + type); - } - } - - return MultipleProviderFactory.buildMultiProvider(mks); - }; - - @SuppressWarnings("unchecked") - final Map resultSpec = (Map) data.get("result"); - final ResultMatcher matcher = parseResultMatcher(jar, resultSpec); - - String decryptionMethodSpec = (String) data.get("decryption-method"); - SignaturePolicy signaturePolicy = SignaturePolicy.AllowEncryptAllowDecrypt; - if (decryptionMethodSpec != null) { - if ("streaming-unsigned-only".equals(decryptionMethodSpec)) { - signaturePolicy = SignaturePolicy.AllowEncryptForbidDecrypt; - } else { - throw new IllegalArgumentException( - "Unsupported Decryption Method: " + decryptionMethodSpec); - } - } - - return new TestCase( - testName, - ciphertextURL, - false, - mkpSupplier, - null, - Collections.emptyMap(), - matcher, - signaturePolicy); - } - - /** Parse Test to MasterKey for AWS SDK v2 */ - @SuppressWarnings("unchecked") - private static TestCase parseTest( - String testName, - Map data, - Map keys, - JarFile jar, - com.amazonaws.encryptionsdk.kmssdkv2.KmsMasterKeyProvider kmsProv) - throws IOException { - final String ciphertextURL = (String) data.get("ciphertext"); - cacheData(jar, ciphertextURL); - - Supplier> mkpSupplier = - () -> { - @SuppressWarnings("generic") - final List> mks = new ArrayList<>(); - - for (Map mkEntry : (List>) data.get("master-keys")) { - final String type = (String) mkEntry.get("type"); - final String keyName = (String) mkEntry.get("key"); - final KeyEntry key = keys.get(keyName); - - if ("aws-kms".equals(type)) { - mks.add(kmsProv.getMasterKey(key.keyId)); - } else if ("aws-kms-mrk-aware".equals(type)) { - com.amazonaws.encryptionsdk.kmssdkv2.AwsKmsMrkAwareMasterKeyProvider provider = - com.amazonaws.encryptionsdk.kmssdkv2.AwsKmsMrkAwareMasterKeyProvider.builder() - .buildStrict(key.keyId); - mks.add(provider.getMasterKey(key.keyId)); - } else if ("aws-kms-mrk-aware-discovery".equals(type)) { - final String defaultMrkRegion = (String) mkEntry.get("default-mrk-region"); - final Map discoveryFilterSpec = - (Map) mkEntry.get("aws-kms-discovery-filter"); - final DiscoveryFilter discoveryFilter; - if (discoveryFilterSpec != null) { - discoveryFilter = - new DiscoveryFilter( - (String) discoveryFilterSpec.get("partition"), - (List) discoveryFilterSpec.get("account-ids")); - } else { - discoveryFilter = null; - } - return com.amazonaws.encryptionsdk.kmssdkv2.AwsKmsMrkAwareMasterKeyProvider.builder() - .discoveryMrkRegion(Region.of(defaultMrkRegion)) - .buildDiscovery(discoveryFilter); - } else if ("raw".equals(type)) { - final String provId = (String) mkEntry.get("provider-id"); - final String algorithm = (String) mkEntry.get("encryption-algorithm"); - if ("aes".equals(algorithm)) { - mks.add( - JceMasterKey.getInstance( - (SecretKey) key.key, provId, key.keyId, "AES/GCM/NoPadding")); - } else if ("rsa".equals(algorithm)) { - String transformation = "RSA/ECB/"; - final String padding = (String) mkEntry.get("padding-algorithm"); - if ("pkcs1".equals(padding)) { - transformation += "PKCS1Padding"; - } else if ("oaep-mgf1".equals(padding)) { - final String hashName = - ((String) mkEntry.get("padding-hash")) - .replace("sha", "sha-") - .toUpperCase(Locale.ROOT); - transformation += "OAEPWith" + hashName + "AndMGF1Padding"; - } else { - throw new IllegalArgumentException("Unsupported padding:" + padding); - } - final PublicKey wrappingKey; - final PrivateKey unwrappingKey; - if (key.key instanceof PublicKey) { - wrappingKey = (PublicKey) key.key; - unwrappingKey = null; - } else { - wrappingKey = null; - unwrappingKey = (PrivateKey) key.key; - } - mks.add( - JceMasterKey.getInstance( - wrappingKey, unwrappingKey, provId, key.keyId, transformation)); - } else { - throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); - } - } else { - throw new IllegalArgumentException("Unsupported Key Type: " + type); - } - } - - return MultipleProviderFactory.buildMultiProvider(mks); - }; - - @SuppressWarnings("unchecked") - final Map resultSpec = (Map) data.get("result"); - final ResultMatcher matcher = parseResultMatcher(jar, resultSpec); - - String decryptionMethodSpec = (String) data.get("decryption-method"); - SignaturePolicy signaturePolicy = SignaturePolicy.AllowEncryptAllowDecrypt; - if (decryptionMethodSpec != null) { - if ("streaming-unsigned-only".equals(decryptionMethodSpec)) { - signaturePolicy = SignaturePolicy.AllowEncryptForbidDecrypt; - } else { - throw new IllegalArgumentException( - "Unsupported Decryption Method: " + decryptionMethodSpec); - } - } - - return new TestCase( - testName, - ciphertextURL, - false, - mkpSupplier, - null, - Collections.emptyMap(), - matcher, - signaturePolicy); - } - - private static ResultMatcher parseResultMatcher( - final JarFile jar, final Map result) throws IOException { - if (result.size() != 1) { - throw new IllegalArgumentException("Unsupported result specification: " + result); - } - Map.Entry pair = result.entrySet().iterator().next(); - if (pair.getKey().equals("output")) { - Map outputSpec = (Map) pair.getValue(); - String plaintextUrl = outputSpec.get("plaintext"); - cacheData(jar, plaintextUrl); - return new OutputResultMatcher(plaintextUrl); - } else if (pair.getKey().equals("error")) { - Map errorSpec = (Map) pair.getValue(); - String errorDescription = errorSpec.get("error-description"); - return new ErrorResultMatcher(errorDescription); - } else { - throw new IllegalArgumentException("Unsupported result specification: " + result); - } - } - - @SuppressWarnings("unchecked") - private static Map parseKeyManifest(final Map keysManifest) - throws GeneralSecurityException { - // check our type - final Map metaData = (Map) keysManifest.get("manifest"); - if (!"keys".equals(metaData.get("type"))) { - throw new IllegalArgumentException("Invalid manifest type: " + metaData.get("type")); - } - if (!Integer.valueOf(3).equals(metaData.get("version"))) { - throw new IllegalArgumentException("Invalid manifest version: " + metaData.get("version")); - } - - final Map result = new HashMap<>(); - - Map keys = (Map) keysManifest.get("keys"); - for (Map.Entry entry : keys.entrySet()) { - final String name = entry.getKey(); - final Map data = (Map) entry.getValue(); - - final String keyType = (String) data.get("type"); - final String encoding = (String) data.get("encoding"); - final String keyId = (String) data.get("key-id"); - final String material = (String) data.get("material"); // May be null - final String algorithm = (String) data.get("algorithm"); // May be null - - final KeyEntry keyEntry; - - final KeyFactory kf; - switch (keyType) { - case "symmetric": - if (!"base64".equals(encoding)) { - throw new IllegalArgumentException( - format("Key %s is symmetric but has encoding %s", keyId, encoding)); - } - keyEntry = - new KeyEntry( - name, - keyId, - keyType, - new SecretKeySpec(Base64.decode(material), algorithm.toUpperCase(Locale.ROOT))); - break; - case "private": - kf = KeyFactory.getInstance(algorithm); - if (!"pem".equals(encoding)) { - throw new IllegalArgumentException( - format("Key %s is private but has encoding %s", keyId, encoding)); - } - byte[] pkcs8Key = parsePem(material); - keyEntry = - new KeyEntry( - name, keyId, keyType, kf.generatePrivate(new PKCS8EncodedKeySpec(pkcs8Key))); - break; - case "public": - kf = KeyFactory.getInstance(algorithm); - if (!"pem".equals(encoding)) { - throw new IllegalArgumentException( - format("Key %s is private but has encoding %s", keyId, encoding)); - } - byte[] x509Key = parsePem(material); - keyEntry = - new KeyEntry( - name, keyId, keyType, kf.generatePublic(new X509EncodedKeySpec(x509Key))); - break; - case "aws-kms": - keyEntry = new KeyEntry(name, keyId, keyType, null); - break; - default: - throw new IllegalArgumentException("Unsupported key type: " + keyType); - } - - result.put(name, keyEntry); - } - - return result; - } - - private static byte[] parsePem(String pem) { - final String stripped = pem.replaceAll("-+[A-Z ]+-+", ""); - return Base64.decode(stripped); - } - - private static class KeyEntry { - final String name; - final String keyId; - final String type; - final Key key; - - private KeyEntry(String name, String keyId, String type, Key key) { - this.name = name; - this.keyId = keyId; - this.type = type; - this.key = key; - } - } - - private static class TestCase { - private final String name; - private final String ciphertextPath; - private final ResultMatcher matcher; - private final boolean isKeyring; - private final Supplier> mkpSupplier; - private final Supplier cmmSupplier; - private final Map encryptionContext; - private final SignaturePolicy signaturePolicy; - - private TestCase( - String name, - String ciphertextPath, - boolean isKeyring, - Supplier> mkpSupplier, - Supplier cmmSupplier, - Map encryptionContext, - ResultMatcher matcher, - SignaturePolicy signaturePolicy) { - this.name = name; - this.ciphertextPath = ciphertextPath; - this.matcher = matcher; - this.isKeyring = isKeyring; - this.mkpSupplier = mkpSupplier; - this.cmmSupplier = cmmSupplier; - this.encryptionContext = encryptionContext; - this.signaturePolicy = signaturePolicy; - } - } - - private interface ResultMatcher { - void Match(Callable decryptor) throws Exception; - } - - private static class OutputResultMatcher implements ResultMatcher { - - private final String plaintextPath; - - private OutputResultMatcher(String plaintextPath) { - this.plaintextPath = plaintextPath; - } - - @Override - public void Match(Callable decryptor) throws Exception { - final byte[] plaintext = decryptor.call(); - final byte[] expectedPlaintext = cachedData.get(plaintextPath); - Assert.assertArrayEquals(expectedPlaintext, plaintext); - } - } - - private static class ErrorResultMatcher implements ResultMatcher { - - private final String errorDescription; - - private ErrorResultMatcher(String errorDescription) { - this.errorDescription = errorDescription; - } - - @Override - public void Match(Callable decryptor) { - Assert.assertThrows( - "Decryption expected to fail (" + errorDescription + ") but succeeded", - Exception.class, - decryptor::call); - } - } -} From 1082dc0ddc9aa947b5f23464727ad6d1525fce8b Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Mon, 10 Jun 2024 16:39:40 -0700 Subject: [PATCH 10/19] undo --- .../encryptionsdk/AllTestsSuite.java | 2 + .../encryptionsdk/TestVectorGenerator.java | 570 ++++++++++++++ .../encryptionsdk/TestVectorRunner.java | 743 ++++++++++++++++++ 3 files changed, 1315 insertions(+) create mode 100644 src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java create mode 100644 src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java diff --git a/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java b/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java index a1a9378b..1deaf067 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java +++ b/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java @@ -81,6 +81,8 @@ AwsCryptoTest.class, CryptoInputStreamTest.class, CryptoOutputStreamTest.class, + TestVectorRunner.class, + TestVectorGenerator.class, XCompatDecryptTest.class, DefaultCryptoMaterialsManagerTest.class, NullCryptoMaterialsCacheTest.class, diff --git a/src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java b/src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java new file mode 100644 index 00000000..eb2b1764 --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/TestVectorGenerator.java @@ -0,0 +1,570 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.encryptionsdk; + +import static java.lang.String.format; + +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.amazonaws.encryptionsdk.jce.JceMasterKey; +import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; +import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileAttribute; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import org.apache.commons.io.FileUtils; +import org.bouncycastle.util.encoders.Base64; +import org.junit.AfterClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import software.amazon.awssdk.utils.ImmutableMap; +import software.amazon.cryptography.materialproviders.IKeyring; +import software.amazon.cryptography.materialproviders.MaterialProviders; +import software.amazon.cryptography.materialproviders.model.CreateMultiKeyringInput; +import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig; +import software.amazon.cryptography.materialproviderstestvectorkeys.KeyVectors; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionInput; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionOutput; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.KeyVectorsConfig; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.TestVectorKeyringInput; + +@RunWith(Parameterized.class) +public class TestVectorGenerator { + + private static final String encryptManifestList = + "https://raw.githubusercontent.com/awslabs/aws-crypto-tools-test-vector-framework/master/features/CANONICAL-GENERATED-MANIFESTS/0003-awses-message-encryption.v2.json"; + // We save the files in memory to avoid repeatedly retrieving them. This won't work if the + // plaintexts are too + // large or numerous + private static final Map cachedData = new HashMap<>(); + private static final ObjectMapper mapper = new ObjectMapper(); + private static EncryptionInterface encryption; + private static boolean isMasterKey; + private final String testName; + private final TestCase testCase; + + // Temp Test Vectors Directory + private static String tempTestVectorPath; + // Zip File Path + private static String zipFilePath; + + public TestVectorGenerator(final String testName, TestCase testCase) { + this.testName = testName; + this.testCase = testCase; + } + + // Zip Temp Folder and delete temp files + @AfterClass + public static void zip() throws IOException { + Path zipFile = Files.createFile(Paths.get(zipFilePath)); + + Path sourceDirPath = Paths.get(tempTestVectorPath); + try (ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(zipFile)); + Stream paths = Files.walk(sourceDirPath)) { + paths + .filter(path -> !Files.isDirectory(path)) + .forEach( + path -> { + ZipEntry zipEntry = new ZipEntry(sourceDirPath.relativize(path).toString()); + try { + zipOutputStream.putNextEntry(zipEntry); + Files.copy(path, zipOutputStream); + zipOutputStream.closeEntry(); + } catch (IOException e) { + throw new UncheckedIOException("Unable to Zip File", e); + } + }); + } + FileUtils.deleteQuietly(sourceDirPath.toFile()); + + // Teardown + cachedData.clear(); + } + + @Test + @SuppressWarnings("unchecked") + public void encrypt() throws Exception { + CryptoAlgorithm cryptoAlgorithm = getCryptoAlgorithm(testCase.algorithmId); + CommitmentPolicy commitmentPolicy = + cryptoAlgorithm.isCommitting() + ? CommitmentPolicy.RequireEncryptRequireDecrypt + : CommitmentPolicy.ForbidEncryptAllowDecrypt; + + AwsCrypto crypto = + AwsCrypto.builder() + .withCommitmentPolicy(commitmentPolicy) + .withEncryptionAlgorithm(cryptoAlgorithm) + .withEncryptionFrameSize(testCase.frameSize) + .build(); + + Callable ciphertext; + if (isMasterKey) { + ciphertext = + () -> + crypto + .encryptData( + testCase.masterKey, + cachedData.get(testCase.plaintext), + testCase.encryptionContext) + .getResult(); + } else { + ciphertext = + () -> + crypto + .encryptData( + testCase.keyring, + cachedData.get(testCase.plaintext), + testCase.encryptionContext) + .getResult(); + } + Files.write(Paths.get(tempTestVectorPath + "ciphertexts/" + testName), ciphertext.call()); + } + + private static CryptoAlgorithm getCryptoAlgorithm(String algorithmId) { + Integer algId = Integer.parseInt(algorithmId, 16); + for (CryptoAlgorithm cryptoAlgorithm : CryptoAlgorithm.values()) { + if (cryptoAlgorithm.getValue() == algId) { + return cryptoAlgorithm; + } + } + throw new IllegalArgumentException("Invalid AlgorithmId: " + algorithmId); + } + + @Parameterized.Parameters(name = "Compatibility Test: {0} - {1}") + @SuppressWarnings("unchecked") + public static Collection data() throws Exception { + final String interfaceOption = System.getProperty("masterkey"); + + if (interfaceOption != null && interfaceOption.equals("true")) { + isMasterKey = true; + encryption = EncryptionInterface.EncryptWithMasterKey; + } else { + encryption = EncryptionInterface.EncryptWithKeyring; + } + + final String encryptKeyManifest = System.getProperty("keysManifest"); + if (encryptKeyManifest == null) { + return Collections.emptyList(); + } + + zipFilePath = System.getProperty("zipFilePath"); + if (zipFilePath == null) { + return Collections.emptyList(); + } + + tempTestVectorPath = Files.createTempDirectory("java", new FileAttribute[0]).toString() + "/"; + createDirectories(tempTestVectorPath + "ciphertexts/"); + createDirectories(tempTestVectorPath + "plaintexts/"); + + File decryptManifest = new File(tempTestVectorPath + "manifest.json"); + File keyManifest = new File(tempTestVectorPath + "keys.json"); + + final Map manifest = mapper.readValue(new URL(encryptManifestList), Map.class); + mapper + .writerWithDefaultPrettyPrinter() + .writeValue(decryptManifest, createDecryptManifest(manifest)); + + try (InputStream in = new FileInputStream(encryptKeyManifest)) { + Files.copy(in, keyManifest.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + + final Map keysManifest = + mapper.readValue(new File(encryptKeyManifest), Map.class); + + cachePlaintext((Map) manifest.get("plaintexts")); + + MaterialProvidersConfig config = MaterialProvidersConfig.builder().build(); + MaterialProviders materialProviders = + MaterialProviders.builder().MaterialProvidersConfig(config).build(); + KeyVectors keyVectors = + KeyVectors.builder() + .KeyVectorsConfig( + KeyVectorsConfig.builder().keyManifiestPath(keyManifest.toString()).build()) + .build(); + + final Map keys = parseKeyManifest(keysManifest); + final KmsMasterKeyProvider kmsProv = + KmsMasterKeyProvider.builder() + .withCredentials(new DefaultAWSCredentialsProviderChain()) + .buildDiscovery(); + + return ((Map>) manifest.get("tests")) + .entrySet().stream() + .map( + entry -> { + String testName = entry.getKey(); + TestCase testCase = + encryption.parseTest(entry, keys, kmsProv, materialProviders, keyVectors); + return new Object[] {testName, testCase}; + }) + .collect(Collectors.toList()); + } + + private static void createDirectories(String path) { + File directory = new File(path); + directory.mkdirs(); + } + + private enum EncryptionInterface { + EncryptWithMasterKey { + @Override + public TestCase parseTest( + Map.Entry> testEntry, + Map keys, + KmsMasterKeyProvider kmsProv, + MaterialProviders materialProviders, + KeyVectors keyVectors) { + return parseTestWithMasterkeys(testEntry, keys, kmsProv); + } + }, + EncryptWithKeyring { + @Override + public TestCase parseTest( + Map.Entry> testEntry, + Map keys, + KmsMasterKeyProvider kmsProv, + MaterialProviders materialProviders, + KeyVectors keyVectors) { + return parseTestWithKeyrings(testEntry, materialProviders, keyVectors); + } + }; + + public abstract TestCase parseTest( + Map.Entry> testEntry, + Map keys, + KmsMasterKeyProvider kmsProv, + MaterialProviders materialProviders, + KeyVectors keyVectors); + } + + private static void cachePlaintext(Map plaintexts) { + Random rd = new Random(); + plaintexts.forEach( + (key, value) -> { + byte[] plaintext = new byte[value]; + rd.nextBytes(plaintext); + try { + Files.write(new File(tempTestVectorPath + "plaintexts/" + key).toPath(), plaintext); + cachedData.put(key, plaintext); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } + + private static Map createDecryptManifest(Map encryptManifest) { + Map decryptManifest = new LinkedHashMap<>(); + + decryptManifest.put("manifest", ImmutableMap.of("type", "awses-decrypt", "version", 2)); + + decryptManifest.put( + "client", ImmutableMap.of("name", "aws/aws-encryption-sdk-java", "version", "2.2.0")); + + decryptManifest.put("keys", "file://keys.json"); + + Map> testScenarios = + ((LinkedHashMap>) encryptManifest.get("tests")) + .entrySet().stream() + .collect( + Collectors.toMap( + Map.Entry::getKey, + entry -> { + Map scenario = entry.getValue(); + return new LinkedHashMap() { + { + put("ciphertext", "file://ciphertexts/" + entry.getKey()); + put("master-keys", scenario.get("master-keys")); + put( + "result", + Collections.singletonMap( + "output", + Collections.singletonMap( + "plaintext", + "file://plaintexts/" + scenario.get("plaintext")))); + } + }; + })); + + decryptManifest.put("tests", testScenarios); + return decryptManifest; + } + + private static TestCase parseTestWithMasterkeys( + Map.Entry> testEntry, + Map keys, + KmsMasterKeyProvider kmsProv) { + + String testName = testEntry.getKey(); + Map data = testEntry.getValue(); + + String plaintext = (String) data.get("plaintext"); + String algorithmId = (String) data.get("algorithm"); + int frameSize = (int) data.get("frame-size"); + Map encryptionContext = (Map) data.get("encryption-context"); + + final List> mks = new ArrayList<>(); + + for (Map mkEntry : (List>) data.get("master-keys")) { + if (mkEntry.get("key").equals("rsa-4096-private")) { + mkEntry.replace("key", "rsa-4096-public"); + } + + final String type = mkEntry.get("type"); + final String keyName = mkEntry.get("key"); + final KeyEntry key = keys.get(keyName); + + if ("aws-kms".equals(type)) { + mks.add(kmsProv.getMasterKey(key.keyId)); + } else if ("raw".equals(type)) { + final String provId = mkEntry.get("provider-id"); + final String algorithm = mkEntry.get("encryption-algorithm"); + if ("aes".equals(algorithm)) { + mks.add( + JceMasterKey.getInstance( + (SecretKey) key.key, provId, key.keyId, "AES/GCM/NoPadding")); + } else if ("rsa".equals(algorithm)) { + String transformation = "RSA/ECB/"; + final String padding = mkEntry.get("padding-algorithm"); + if ("pkcs1".equals(padding)) { + transformation += "PKCS1Padding"; + } else if ("oaep-mgf1".equals(padding)) { + final String hashName = + mkEntry.get("padding-hash").replace("sha", "sha-").toUpperCase(); + transformation += "OAEPWith" + hashName + "AndMGF1Padding"; + } else { + throw new IllegalArgumentException("Unsupported padding:" + padding); + } + final PublicKey wrappingKey; + final PrivateKey unwrappingKey; + if (key.key instanceof PublicKey) { + wrappingKey = (PublicKey) key.key; + unwrappingKey = null; + } else { + wrappingKey = null; + unwrappingKey = (PrivateKey) key.key; + } + mks.add( + JceMasterKey.getInstance( + wrappingKey, unwrappingKey, provId, key.keyId, transformation)); + } else { + throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); + } + } else { + throw new IllegalArgumentException("Unsupported Key Type: " + type); + } + } + + MasterKeyProvider multiProvider = MultipleProviderFactory.buildMultiProvider(mks); + + return new TestCase( + testName, null, multiProvider, plaintext, algorithmId, frameSize, encryptionContext); + } + + private static TestCase parseTestWithKeyrings( + Map.Entry> testEntry, + MaterialProviders materialProviders, + KeyVectors keyVectors) { + String testName = testEntry.getKey(); + Map data = testEntry.getValue(); + + String plaintext = (String) data.get("plaintext"); + String algorithmId = (String) data.get("algorithm"); + int frameSize = (int) data.get("frame-size"); + Map encryptionContext = (Map) data.get("encryption-context"); + + List keyrings = new ArrayList<>(); + + ((List>) data.get("master-keys")) + .forEach( + mkEntry -> { + if (mkEntry.get("type").equals("raw") + && mkEntry.get("encryption-algorithm").equals("rsa")) { + if (mkEntry.get("key").equals("rsa-4096-private")) { + mkEntry.replace("key", "rsa-4096-public"); + } + mkEntry.putIfAbsent("padding-hash", "sha1"); + } + + try { + byte[] json = new ObjectMapper().writeValueAsBytes(mkEntry); + GetKeyDescriptionOutput output = + keyVectors.GetKeyDescription( + GetKeyDescriptionInput.builder().json(ByteBuffer.wrap(json)).build()); + + IKeyring testVectorKeyring = + keyVectors.CreateTestVectorKeyring( + TestVectorKeyringInput.builder() + .keyDescription(output.keyDescription()) + .build()); + + keyrings.add(testVectorKeyring); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }); + + IKeyring primary = keyrings.remove(0); + IKeyring multiKeyring = + materialProviders.CreateMultiKeyring( + CreateMultiKeyringInput.builder().generator(primary).childKeyrings(keyrings).build()); + + return new TestCase( + testName, multiKeyring, null, plaintext, algorithmId, frameSize, encryptionContext); + } + + @SuppressWarnings("unchecked") + private static Map parseKeyManifest(final Map keysManifest) + throws GeneralSecurityException { + // check our type + final Map metaData = (Map) keysManifest.get("manifest"); + if (!"keys".equals(metaData.get("type"))) { + throw new IllegalArgumentException("Invalid manifest type: " + metaData.get("type")); + } + if (!Integer.valueOf(3).equals(metaData.get("version"))) { + throw new IllegalArgumentException("Invalid manifest version: " + metaData.get("version")); + } + + final Map result = new HashMap<>(); + + Map keys = (Map) keysManifest.get("keys"); + for (Map.Entry entry : keys.entrySet()) { + final String name = entry.getKey(); + final Map data = (Map) entry.getValue(); + + final String keyType = (String) data.get("type"); + final String encoding = (String) data.get("encoding"); + final String keyId = (String) data.get("key-id"); + final String material = (String) data.get("material"); // May be null + final String algorithm = (String) data.get("algorithm"); // May be null + + final KeyEntry keyEntry; + + final KeyFactory kf; + switch (keyType) { + case "symmetric": + if (!"base64".equals(encoding)) { + throw new IllegalArgumentException( + format("Key %s is symmetric but has encoding %s", keyId, encoding)); + } + keyEntry = + new KeyEntry( + name, + keyId, + keyType, + new SecretKeySpec(Base64.decode(material), algorithm.toUpperCase())); + break; + case "private": + kf = KeyFactory.getInstance(algorithm); + if (!"pem".equals(encoding)) { + throw new IllegalArgumentException( + format("Key %s is private but has encoding %s", keyId, encoding)); + } + byte[] pkcs8Key = parsePem(material); + keyEntry = + new KeyEntry( + name, keyId, keyType, kf.generatePrivate(new PKCS8EncodedKeySpec(pkcs8Key))); + break; + case "public": + kf = KeyFactory.getInstance(algorithm); + if (!"pem".equals(encoding)) { + throw new IllegalArgumentException( + format("Key %s is private but has encoding %s", keyId, encoding)); + } + byte[] x509Key = parsePem(material); + keyEntry = + new KeyEntry( + name, keyId, keyType, kf.generatePublic(new X509EncodedKeySpec(x509Key))); + break; + case "aws-kms": + keyEntry = new KeyEntry(name, keyId, keyType, null); + break; + default: + throw new IllegalArgumentException("Unsupported key type: " + keyType); + } + + result.put(name, keyEntry); + } + + return result; + } + + private static byte[] parsePem(String pem) { + final String stripped = pem.replaceAll("-+[A-Z ]+-+", ""); + return Base64.decode(stripped); + } + + private static class KeyEntry { + final String name; + final String keyId; + final String type; + final Key key; + + private KeyEntry(String name, String keyId, String type, Key key) { + this.name = name; + this.keyId = keyId; + this.type = type; + this.key = key; + } + } + + private static class TestCase { + private final String name; + private final IKeyring keyring; + private final MasterKeyProvider masterKey; + private final String plaintext; + private final String algorithmId; + private final int frameSize; + private final Map encryptionContext; + + public TestCase( + String name, + IKeyring keyring, + MasterKeyProvider multiProvider, + String plaintext, + String algorithmId, + int frameSize, + Map encryptionContext) { + this.name = name; + this.keyring = keyring; + this.masterKey = multiProvider; + this.plaintext = plaintext; + this.algorithmId = algorithmId; + this.frameSize = frameSize; + this.encryptionContext = encryptionContext; + } + } +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java b/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java new file mode 100644 index 00000000..8c0be82f --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java @@ -0,0 +1,743 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.encryptionsdk; + +import static java.lang.String.format; + +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.amazonaws.encryptionsdk.internal.SignaturePolicy; +import com.amazonaws.encryptionsdk.jce.JceMasterKey; +import com.amazonaws.encryptionsdk.kms.AwsKmsMrkAwareMasterKeyProvider; +import com.amazonaws.encryptionsdk.kms.DiscoveryFilter; +import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; +import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory; +import com.amazonaws.util.IOUtils; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.URL; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.function.Supplier; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import junit.framework.TestCase; +import org.bouncycastle.util.encoders.Base64; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import software.amazon.awssdk.regions.Region; +import software.amazon.cryptography.materialproviders.ICryptographicMaterialsManager; +import software.amazon.cryptography.materialproviders.IKeyring; +import software.amazon.cryptography.materialproviders.MaterialProviders; +import software.amazon.cryptography.materialproviders.model.CreateDefaultCryptographicMaterialsManagerInput; +import software.amazon.cryptography.materialproviders.model.CreateMultiKeyringInput; +import software.amazon.cryptography.materialproviders.model.CreateRequiredEncryptionContextCMMInput; +import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig; +import software.amazon.cryptography.materialproviderstestvectorkeys.KeyVectors; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionInput; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.GetKeyDescriptionOutput; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.KeyVectorsConfig; +import software.amazon.cryptography.materialproviderstestvectorkeys.model.TestVectorKeyringInput; + +@RunWith(Parameterized.class) +public class TestVectorRunner { + + // TODO: Standardize Manifest Version + private static final List MANIFEST_VERSIONS = Arrays.asList(2, 4); + + // We save the files in memory to avoid repeatedly retrieving them. This won't work if the + // plaintexts are too + // large or numerous + private static final Map cachedData = new HashMap<>(); + + private final String testName; + private final TestCase testCase; + private final DecryptionMethod decryptionMethod; + + public TestVectorRunner( + final String testName, TestCase testCase, DecryptionMethod decryptionMethod) { + this.testName = testName; + this.testCase = testCase; + this.decryptionMethod = decryptionMethod; + } + + @Test + public void decrypt() throws Exception { + AwsCrypto crypto = + AwsCrypto.builder() + .withCommitmentPolicy(CommitmentPolicy.ForbidEncryptAllowDecrypt) + .build(); + Callable decryptor; + if (testCase.isKeyring) { + decryptor = + () -> + decryptionMethod.decryptMessage( + crypto, + testCase.cmmSupplier.get(), + cachedData.get(testCase.ciphertextPath), + testCase.encryptionContext); + } else { + decryptor = + () -> + decryptionMethod.decryptMessage( + crypto, testCase.mkpSupplier.get(), cachedData.get(testCase.ciphertextPath)); + } + testCase.matcher.Match(decryptor); + } + + @Parameterized.Parameters(name = "Compatibility Test: {0} - {3}") + @SuppressWarnings("unchecked") + public static Collection data() throws Exception { + final String zipPath = System.getProperty("testVectorZip"); + final String interfaceOption = System.getProperty("masterkey"); + if (zipPath == null) { + return Collections.emptyList(); + } + + final JarURLConnection jarConnection = + (JarURLConnection) new URL("jar:" + zipPath + "!/").openConnection(); + + try (JarFile jar = jarConnection.getJarFile()) { + + final Map manifest = readJsonMapFromJar(jar, "manifest.json"); + final Map keysManifest = readJsonMapFromJar(jar, "keys.json"); + + ObjectMapper objectMapper = new ObjectMapper(); + + // Create a temporary file and write the JSON string to it + File tempFile = File.createTempFile("keys", ".json"); + objectMapper.writeValue(tempFile, keysManifest); + + final Map metaData = (Map) manifest.get("manifest"); + + // We only support "awses-decrypt" type manifests right now + if (!"awses-decrypt".equals(metaData.get("type"))) { + throw new IllegalArgumentException("Unsupported manifest type: " + metaData.get("type")); + } + Integer readVersion = (Integer) metaData.get("version"); + if (!MANIFEST_VERSIONS.contains(readVersion)) { + throw new IllegalArgumentException( + "Unsupported manifest version: " + metaData.get("version")); + } + + final Map keys = + parseKeyManifest(readJsonMapFromJar(jar, (String) manifest.get("keys"))); + + KeyVectors keyVectors = + KeyVectors.builder() + .KeyVectorsConfig( + KeyVectorsConfig.builder().keyManifiestPath(tempFile.getPath()).build()) + .build(); + + MaterialProvidersConfig config = MaterialProvidersConfig.builder().build(); + MaterialProviders materialProviders = + MaterialProviders.builder().MaterialProvidersConfig(config).build(); + + final KmsMasterKeyProvider kmsProvV1 = + KmsMasterKeyProvider.builder() + .withCredentials(new DefaultAWSCredentialsProviderChain()) + .buildDiscovery(); + + final com.amazonaws.encryptionsdk.kmssdkv2.KmsMasterKeyProvider kmsProvV2 = + com.amazonaws.encryptionsdk.kmssdkv2.KmsMasterKeyProvider.builder().buildDiscovery(); + + List testCases = new ArrayList<>(); + for (Map.Entry> testEntry : + ((Map>) manifest.get("tests")).entrySet()) { + if (interfaceOption != null && interfaceOption.equals("true")) { + String testName = testEntry.getKey(); + + TestCase testCaseV1 = + parseTest(testEntry.getKey(), testEntry.getValue(), keys, jar, kmsProvV1); + TestCase testCaseV2 = + parseTest(testEntry.getKey(), testEntry.getValue(), keys, jar, kmsProvV2); + + for (DecryptionMethod decryptionMethod : DecryptionMethod.values()) { + if (testCaseV1.signaturePolicy.equals(decryptionMethod.signaturePolicy())) { + testCases.add(new Object[] {testName, testCaseV1, decryptionMethod}); + testCases.add(new Object[] {testName + "-V2", testCaseV2, decryptionMethod}); + } + } + } else { + String testName = testEntry.getKey(); + TestCase testCaseKeyring = + parseTest( + testEntry.getKey(), + testEntry.getValue(), + keys, + jar, + materialProviders, + keyVectors); + + for (DecryptionMethod decryptionMethod : DecryptionMethod.values()) { + if (testCaseKeyring.signaturePolicy.equals(decryptionMethod.signaturePolicy())) { + testCases.add( + new Object[] {testName + "-Keyrings", testCaseKeyring, decryptionMethod}); + } + } + } + } + return testCases; + } + } + + @AfterClass + public static void teardown() { + cachedData.clear(); + } + + private static byte[] readBytesFromJar(JarFile jar, String fileName) throws IOException { + try (InputStream is = readFromJar(jar, fileName)) { + return IOUtils.toByteArray(is); + } + } + + private static Map readJsonMapFromJar(JarFile jar, String fileName) + throws IOException { + try (InputStream is = readFromJar(jar, fileName)) { + final ObjectMapper mapper = new ObjectMapper(); + return mapper.readValue(is, new TypeReference>() {}); + } + } + + private static InputStream readFromJar(JarFile jar, String name) throws IOException { + // Our manifest URIs incorrectly start with file:// rather than just file: so we need to strip + // this + ZipEntry entry = jar.getEntry(name.replaceFirst("^file://(?!/)", "")); + return jar.getInputStream(entry); + } + + private static void cacheData(JarFile jar, String url) throws IOException { + if (!cachedData.containsKey(url)) { + cachedData.put(url, readBytesFromJar(jar, url)); + } + } + + /** Parse Test to Keyring */ + @SuppressWarnings("unchecked") + private static TestCase parseTest( + String testName, + Map data, + Map keys, + JarFile jar, + MaterialProviders materialProviders, + KeyVectors keyVectors) + throws IOException { + final String ciphertextURL = (String) data.get("ciphertext"); + cacheData(jar, ciphertextURL); + + Supplier cmmSupplier = + () -> { + final List keyrings = new ArrayList<>(); + for (Map mkEntry : (List>) data.get("master-keys")) { + if (mkEntry.get("type").equals("raw") + && mkEntry.get("encryption-algorithm").equals("rsa")) { + mkEntry.putIfAbsent("padding-hash", "sha1"); + } + + try { + byte[] json = new ObjectMapper().writeValueAsBytes(mkEntry); + GetKeyDescriptionOutput output = + keyVectors.GetKeyDescription( + GetKeyDescriptionInput.builder().json(ByteBuffer.wrap(json)).build()); + + IKeyring testVectorKeyring = + keyVectors.CreateTestVectorKeyring( + TestVectorKeyringInput.builder() + .keyDescription(output.keyDescription()) + .build()); + + keyrings.add(testVectorKeyring); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + IKeyring multiKeyring = + materialProviders.CreateMultiKeyring( + CreateMultiKeyringInput.builder() + .generator(keyrings.get(0)) + .childKeyrings(keyrings) + .build()); + + CreateDefaultCryptographicMaterialsManagerInput defaultInput = + CreateDefaultCryptographicMaterialsManagerInput.builder() + .keyring(multiKeyring) + .build(); + ICryptographicMaterialsManager cmm = + materialProviders.CreateDefaultCryptographicMaterialsManager(defaultInput); + if (data.containsKey("cmm") && data.get("cmm").equals("RequiredEncryptionContext")) { + List requiredKeys = new ArrayList(2); + requiredKeys.add("key1"); + requiredKeys.add("key2"); + CreateRequiredEncryptionContextCMMInput requiredCMMInput = + CreateRequiredEncryptionContextCMMInput.builder() + .underlyingCMM( + materialProviders.CreateDefaultCryptographicMaterialsManager(defaultInput)) + .requiredEncryptionContextKeys(requiredKeys) + .build(); + cmm = materialProviders.CreateRequiredEncryptionContextCMM(requiredCMMInput); + } + return cmm; + }; + @SuppressWarnings("unchecked") + final Map resultSpec = (Map) data.get("result"); + final ResultMatcher matcher = parseResultMatcher(jar, resultSpec); + + Map ec = Collections.emptyMap(); + + if (data.get("encryption-context") != null) { + ec = (Map) data.get("encryption-context"); + } + + String decryptionMethodSpec = (String) data.get("decryption-method"); + SignaturePolicy signaturePolicy = SignaturePolicy.AllowEncryptAllowDecrypt; + if (decryptionMethodSpec != null) { + if ("streaming-unsigned-only".equals(decryptionMethodSpec)) { + signaturePolicy = SignaturePolicy.AllowEncryptForbidDecrypt; + } else { + throw new IllegalArgumentException( + "Unsupported Decryption Method: " + decryptionMethodSpec); + } + } + + return new TestCase( + testName, ciphertextURL, true, null, cmmSupplier, ec, matcher, signaturePolicy); + } + + /** Parse Test to MasterKey for AWS SDK v1 */ + @SuppressWarnings("unchecked") + private static TestCase parseTest( + String testName, + Map data, + Map keys, + JarFile jar, + KmsMasterKeyProvider kmsProv) + throws IOException { + final String ciphertextURL = (String) data.get("ciphertext"); + cacheData(jar, ciphertextURL); + + Supplier> mkpSupplier = + () -> { + @SuppressWarnings("generic") + final List> mks = new ArrayList<>(); + + for (Map mkEntry : (List>) data.get("master-keys")) { + final String type = (String) mkEntry.get("type"); + final String keyName = (String) mkEntry.get("key"); + final KeyEntry key = keys.get(keyName); + + if ("aws-kms".equals(type)) { + mks.add(kmsProv.getMasterKey(key.keyId)); + } else if ("aws-kms-mrk-aware".equals(type)) { + AwsKmsMrkAwareMasterKeyProvider provider = + AwsKmsMrkAwareMasterKeyProvider.builder().buildStrict(key.keyId); + mks.add(provider.getMasterKey(key.keyId)); + } else if ("aws-kms-mrk-aware-discovery".equals(type)) { + final String defaultMrkRegion = (String) mkEntry.get("default-mrk-region"); + final Map discoveryFilterSpec = + (Map) mkEntry.get("aws-kms-discovery-filter"); + final DiscoveryFilter discoveryFilter; + if (discoveryFilterSpec != null) { + discoveryFilter = + new DiscoveryFilter( + (String) discoveryFilterSpec.get("partition"), + (List) discoveryFilterSpec.get("account-ids")); + } else { + discoveryFilter = null; + } + return AwsKmsMrkAwareMasterKeyProvider.builder() + .withDiscoveryMrkRegion(defaultMrkRegion) + .buildDiscovery(discoveryFilter); + } else if ("raw".equals(type)) { + final String provId = (String) mkEntry.get("provider-id"); + final String algorithm = (String) mkEntry.get("encryption-algorithm"); + if ("aes".equals(algorithm)) { + mks.add( + JceMasterKey.getInstance( + (SecretKey) key.key, provId, key.keyId, "AES/GCM/NoPadding")); + } else if ("rsa".equals(algorithm)) { + String transformation = "RSA/ECB/"; + final String padding = (String) mkEntry.get("padding-algorithm"); + if ("pkcs1".equals(padding)) { + transformation += "PKCS1Padding"; + } else if ("oaep-mgf1".equals(padding)) { + final String hashName = + ((String) mkEntry.get("padding-hash")) + .replace("sha", "sha-") + .toUpperCase(Locale.ROOT); + transformation += "OAEPWith" + hashName + "AndMGF1Padding"; + } else { + throw new IllegalArgumentException("Unsupported padding:" + padding); + } + final PublicKey wrappingKey; + final PrivateKey unwrappingKey; + if (key.key instanceof PublicKey) { + wrappingKey = (PublicKey) key.key; + unwrappingKey = null; + } else { + wrappingKey = null; + unwrappingKey = (PrivateKey) key.key; + } + mks.add( + JceMasterKey.getInstance( + wrappingKey, unwrappingKey, provId, key.keyId, transformation)); + } else { + throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); + } + } else { + throw new IllegalArgumentException("Unsupported Key Type: " + type); + } + } + + return MultipleProviderFactory.buildMultiProvider(mks); + }; + + @SuppressWarnings("unchecked") + final Map resultSpec = (Map) data.get("result"); + final ResultMatcher matcher = parseResultMatcher(jar, resultSpec); + + String decryptionMethodSpec = (String) data.get("decryption-method"); + SignaturePolicy signaturePolicy = SignaturePolicy.AllowEncryptAllowDecrypt; + if (decryptionMethodSpec != null) { + if ("streaming-unsigned-only".equals(decryptionMethodSpec)) { + signaturePolicy = SignaturePolicy.AllowEncryptForbidDecrypt; + } else { + throw new IllegalArgumentException( + "Unsupported Decryption Method: " + decryptionMethodSpec); + } + } + + return new TestCase( + testName, + ciphertextURL, + false, + mkpSupplier, + null, + Collections.emptyMap(), + matcher, + signaturePolicy); + } + + /** Parse Test to MasterKey for AWS SDK v2 */ + @SuppressWarnings("unchecked") + private static TestCase parseTest( + String testName, + Map data, + Map keys, + JarFile jar, + com.amazonaws.encryptionsdk.kmssdkv2.KmsMasterKeyProvider kmsProv) + throws IOException { + final String ciphertextURL = (String) data.get("ciphertext"); + cacheData(jar, ciphertextURL); + + Supplier> mkpSupplier = + () -> { + @SuppressWarnings("generic") + final List> mks = new ArrayList<>(); + + for (Map mkEntry : (List>) data.get("master-keys")) { + final String type = (String) mkEntry.get("type"); + final String keyName = (String) mkEntry.get("key"); + final KeyEntry key = keys.get(keyName); + + if ("aws-kms".equals(type)) { + mks.add(kmsProv.getMasterKey(key.keyId)); + } else if ("aws-kms-mrk-aware".equals(type)) { + com.amazonaws.encryptionsdk.kmssdkv2.AwsKmsMrkAwareMasterKeyProvider provider = + com.amazonaws.encryptionsdk.kmssdkv2.AwsKmsMrkAwareMasterKeyProvider.builder() + .buildStrict(key.keyId); + mks.add(provider.getMasterKey(key.keyId)); + } else if ("aws-kms-mrk-aware-discovery".equals(type)) { + final String defaultMrkRegion = (String) mkEntry.get("default-mrk-region"); + final Map discoveryFilterSpec = + (Map) mkEntry.get("aws-kms-discovery-filter"); + final DiscoveryFilter discoveryFilter; + if (discoveryFilterSpec != null) { + discoveryFilter = + new DiscoveryFilter( + (String) discoveryFilterSpec.get("partition"), + (List) discoveryFilterSpec.get("account-ids")); + } else { + discoveryFilter = null; + } + return com.amazonaws.encryptionsdk.kmssdkv2.AwsKmsMrkAwareMasterKeyProvider.builder() + .discoveryMrkRegion(Region.of(defaultMrkRegion)) + .buildDiscovery(discoveryFilter); + } else if ("raw".equals(type)) { + final String provId = (String) mkEntry.get("provider-id"); + final String algorithm = (String) mkEntry.get("encryption-algorithm"); + if ("aes".equals(algorithm)) { + mks.add( + JceMasterKey.getInstance( + (SecretKey) key.key, provId, key.keyId, "AES/GCM/NoPadding")); + } else if ("rsa".equals(algorithm)) { + String transformation = "RSA/ECB/"; + final String padding = (String) mkEntry.get("padding-algorithm"); + if ("pkcs1".equals(padding)) { + transformation += "PKCS1Padding"; + } else if ("oaep-mgf1".equals(padding)) { + final String hashName = + ((String) mkEntry.get("padding-hash")) + .replace("sha", "sha-") + .toUpperCase(Locale.ROOT); + transformation += "OAEPWith" + hashName + "AndMGF1Padding"; + } else { + throw new IllegalArgumentException("Unsupported padding:" + padding); + } + final PublicKey wrappingKey; + final PrivateKey unwrappingKey; + if (key.key instanceof PublicKey) { + wrappingKey = (PublicKey) key.key; + unwrappingKey = null; + } else { + wrappingKey = null; + unwrappingKey = (PrivateKey) key.key; + } + mks.add( + JceMasterKey.getInstance( + wrappingKey, unwrappingKey, provId, key.keyId, transformation)); + } else { + throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); + } + } else { + throw new IllegalArgumentException("Unsupported Key Type: " + type); + } + } + + return MultipleProviderFactory.buildMultiProvider(mks); + }; + + @SuppressWarnings("unchecked") + final Map resultSpec = (Map) data.get("result"); + final ResultMatcher matcher = parseResultMatcher(jar, resultSpec); + + String decryptionMethodSpec = (String) data.get("decryption-method"); + SignaturePolicy signaturePolicy = SignaturePolicy.AllowEncryptAllowDecrypt; + if (decryptionMethodSpec != null) { + if ("streaming-unsigned-only".equals(decryptionMethodSpec)) { + signaturePolicy = SignaturePolicy.AllowEncryptForbidDecrypt; + } else { + throw new IllegalArgumentException( + "Unsupported Decryption Method: " + decryptionMethodSpec); + } + } + + return new TestCase( + testName, + ciphertextURL, + false, + mkpSupplier, + null, + Collections.emptyMap(), + matcher, + signaturePolicy); + } + + private static ResultMatcher parseResultMatcher( + final JarFile jar, final Map result) throws IOException { + if (result.size() != 1) { + throw new IllegalArgumentException("Unsupported result specification: " + result); + } + Map.Entry pair = result.entrySet().iterator().next(); + if (pair.getKey().equals("output")) { + Map outputSpec = (Map) pair.getValue(); + String plaintextUrl = outputSpec.get("plaintext"); + cacheData(jar, plaintextUrl); + return new OutputResultMatcher(plaintextUrl); + } else if (pair.getKey().equals("error")) { + Map errorSpec = (Map) pair.getValue(); + String errorDescription = errorSpec.get("error-description"); + return new ErrorResultMatcher(errorDescription); + } else { + throw new IllegalArgumentException("Unsupported result specification: " + result); + } + } + + @SuppressWarnings("unchecked") + private static Map parseKeyManifest(final Map keysManifest) + throws GeneralSecurityException { + // check our type + final Map metaData = (Map) keysManifest.get("manifest"); + if (!"keys".equals(metaData.get("type"))) { + throw new IllegalArgumentException("Invalid manifest type: " + metaData.get("type")); + } + if (!Integer.valueOf(3).equals(metaData.get("version"))) { + throw new IllegalArgumentException("Invalid manifest version: " + metaData.get("version")); + } + + final Map result = new HashMap<>(); + + Map keys = (Map) keysManifest.get("keys"); + for (Map.Entry entry : keys.entrySet()) { + final String name = entry.getKey(); + final Map data = (Map) entry.getValue(); + + final String keyType = (String) data.get("type"); + final String encoding = (String) data.get("encoding"); + final String keyId = (String) data.get("key-id"); + final String material = (String) data.get("material"); // May be null + final String algorithm = (String) data.get("algorithm"); // May be null + + final KeyEntry keyEntry; + + final KeyFactory kf; + switch (keyType) { + case "symmetric": + if (!"base64".equals(encoding)) { + throw new IllegalArgumentException( + format("Key %s is symmetric but has encoding %s", keyId, encoding)); + } + keyEntry = + new KeyEntry( + name, + keyId, + keyType, + new SecretKeySpec(Base64.decode(material), algorithm.toUpperCase(Locale.ROOT))); + break; + case "private": + kf = KeyFactory.getInstance(algorithm); + if (!"pem".equals(encoding)) { + throw new IllegalArgumentException( + format("Key %s is private but has encoding %s", keyId, encoding)); + } + byte[] pkcs8Key = parsePem(material); + keyEntry = + new KeyEntry( + name, keyId, keyType, kf.generatePrivate(new PKCS8EncodedKeySpec(pkcs8Key))); + break; + case "public": + kf = KeyFactory.getInstance(algorithm); + if (!"pem".equals(encoding)) { + throw new IllegalArgumentException( + format("Key %s is private but has encoding %s", keyId, encoding)); + } + byte[] x509Key = parsePem(material); + keyEntry = + new KeyEntry( + name, keyId, keyType, kf.generatePublic(new X509EncodedKeySpec(x509Key))); + break; + case "aws-kms": + keyEntry = new KeyEntry(name, keyId, keyType, null); + break; + default: + throw new IllegalArgumentException("Unsupported key type: " + keyType); + } + + result.put(name, keyEntry); + } + + return result; + } + + private static byte[] parsePem(String pem) { + final String stripped = pem.replaceAll("-+[A-Z ]+-+", ""); + return Base64.decode(stripped); + } + + private static class KeyEntry { + final String name; + final String keyId; + final String type; + final Key key; + + private KeyEntry(String name, String keyId, String type, Key key) { + this.name = name; + this.keyId = keyId; + this.type = type; + this.key = key; + } + } + + private static class TestCase { + private final String name; + private final String ciphertextPath; + private final ResultMatcher matcher; + private final boolean isKeyring; + private final Supplier> mkpSupplier; + private final Supplier cmmSupplier; + private final Map encryptionContext; + private final SignaturePolicy signaturePolicy; + + private TestCase( + String name, + String ciphertextPath, + boolean isKeyring, + Supplier> mkpSupplier, + Supplier cmmSupplier, + Map encryptionContext, + ResultMatcher matcher, + SignaturePolicy signaturePolicy) { + this.name = name; + this.ciphertextPath = ciphertextPath; + this.matcher = matcher; + this.isKeyring = isKeyring; + this.mkpSupplier = mkpSupplier; + this.cmmSupplier = cmmSupplier; + this.encryptionContext = encryptionContext; + this.signaturePolicy = signaturePolicy; + } + } + + private interface ResultMatcher { + void Match(Callable decryptor) throws Exception; + } + + private static class OutputResultMatcher implements ResultMatcher { + + private final String plaintextPath; + + private OutputResultMatcher(String plaintextPath) { + this.plaintextPath = plaintextPath; + } + + @Override + public void Match(Callable decryptor) throws Exception { + final byte[] plaintext = decryptor.call(); + final byte[] expectedPlaintext = cachedData.get(plaintextPath); + Assert.assertArrayEquals(expectedPlaintext, plaintext); + } + } + + private static class ErrorResultMatcher implements ResultMatcher { + + private final String errorDescription; + + private ErrorResultMatcher(String errorDescription) { + this.errorDescription = errorDescription; + } + + @Override + public void Match(Callable decryptor) { + Assert.assertThrows( + "Decryption expected to fail (" + errorDescription + ") but succeeded", + Exception.class, + decryptor::call); + } + } +} From 43f92fbbc7045e63ad3fdc9f76123c9a8c9a3dc4 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Mon, 10 Jun 2024 16:44:02 -0700 Subject: [PATCH 11/19] cleanup --- .../crypto/examples/v2/CustomCMMExampleTest.java | 14 +------------- .../v2/V2DefaultCryptoMaterialsManager.java | 6 +++++- .../amazonaws/encryptionsdk/CMMHandlerTest.java | 6 +++++- .../model/DecryptionMaterialsTest.java | 12 ++++++------ 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/test/java/com/amazonaws/crypto/examples/v2/CustomCMMExampleTest.java b/src/test/java/com/amazonaws/crypto/examples/v2/CustomCMMExampleTest.java index d80b4768..acecd8f9 100644 --- a/src/test/java/com/amazonaws/crypto/examples/v2/CustomCMMExampleTest.java +++ b/src/test/java/com/amazonaws/crypto/examples/v2/CustomCMMExampleTest.java @@ -3,23 +3,11 @@ package com.amazonaws.crypto.examples.v2; -import com.amazonaws.encryptionsdk.*; -import com.amazonaws.encryptionsdk.exception.AwsCryptoException; -import com.amazonaws.encryptionsdk.exception.CannotUnwrapDataKeyException; -import com.amazonaws.encryptionsdk.internal.Constants; -import com.amazonaws.encryptionsdk.internal.TrailingSignatureAlgorithm; -import com.amazonaws.encryptionsdk.internal.Utils; +import com.amazonaws.encryptionsdk.CryptoMaterialsManager; import com.amazonaws.encryptionsdk.kms.KMSTestFixtures; import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; -import com.amazonaws.encryptionsdk.model.*; import org.junit.Test; -import java.security.GeneralSecurityException; -import java.security.KeyPair; -import java.security.PublicKey; -import java.util.*; - -import static com.amazonaws.encryptionsdk.internal.Utils.assertNonNull; public class CustomCMMExampleTest { diff --git a/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java b/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java index b0303a73..5cb1255b 100644 --- a/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java +++ b/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java @@ -11,7 +11,11 @@ import com.amazonaws.encryptionsdk.exception.CannotUnwrapDataKeyException; import com.amazonaws.encryptionsdk.internal.Constants; import com.amazonaws.encryptionsdk.internal.TrailingSignatureAlgorithm; -import com.amazonaws.encryptionsdk.model.*; +import com.amazonaws.encryptionsdk.model.DecryptionMaterials; +import com.amazonaws.encryptionsdk.model.DecryptionMaterialsRequest; +import com.amazonaws.encryptionsdk.model.EncryptionMaterials; +import com.amazonaws.encryptionsdk.model.EncryptionMaterialsRequest; +import com.amazonaws.encryptionsdk.model.KeyBlob; import java.security.GeneralSecurityException; import java.security.KeyPair; diff --git a/src/test/java/com/amazonaws/encryptionsdk/CMMHandlerTest.java b/src/test/java/com/amazonaws/encryptionsdk/CMMHandlerTest.java index 48d197fb..de86fa6c 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/CMMHandlerTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/CMMHandlerTest.java @@ -10,7 +10,11 @@ import org.junit.Test; import java.security.PublicKey; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; diff --git a/src/test/java/com/amazonaws/encryptionsdk/model/DecryptionMaterialsTest.java b/src/test/java/com/amazonaws/encryptionsdk/model/DecryptionMaterialsTest.java index 9e76c59f..3466b852 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/model/DecryptionMaterialsTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/model/DecryptionMaterialsTest.java @@ -14,8 +14,8 @@ public class DecryptionMaterialsTest { @Test - public void GIVEN_builder_with_null_EC_WHEN_constructor_THEN_object_EC_is_empty_map() { - // Given: null encryption context + public void GIVEN_builder_with_unset_EC_WHEN_constructor_THEN_object_EC_is_empty_map() { + // Given: DecryptionMaterials.Builder with unset encryption context DecryptionMaterials.Builder builder = DecryptionMaterials.newBuilder(); // When: constructor @@ -27,15 +27,15 @@ public void GIVEN_builder_with_null_EC_WHEN_constructor_THEN_object_EC_is_empty_ @Test public void GIVEN_builder_with_EC_WHEN_constructor_THEN_object_EC_is_builder_EC() { - // Given: any non-null encryption context map - Map mockEncryptionContext = mock(Map.class); + // Given: DecryptionMaterials.Builder with any encryption context map set + Map anyEncryptionContext = mock(Map.class); DecryptionMaterials.Builder builder = DecryptionMaterials.newBuilder(); - builder.setEncryptionContext(mockEncryptionContext); + builder.setEncryptionContext(anyEncryptionContext); // When: constructor DecryptionMaterials decryptionMaterials = builder.build(); // Then: constructor assigns that encryption context map to DecryptionMaterials objects - assertEquals(mockEncryptionContext, decryptionMaterials.getEncryptionContext()); + assertEquals(anyEncryptionContext, decryptionMaterials.getEncryptionContext()); } } From 24ee4f4c062dd52cee6418873baa7d8c360470f7 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Mon, 10 Jun 2024 16:44:25 -0700 Subject: [PATCH 12/19] cleanup --- src/test/java/com/amazonaws/encryptionsdk/CMMHandlerTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/com/amazonaws/encryptionsdk/CMMHandlerTest.java b/src/test/java/com/amazonaws/encryptionsdk/CMMHandlerTest.java index de86fa6c..50035f5c 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/CMMHandlerTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/CMMHandlerTest.java @@ -23,7 +23,6 @@ public class CMMHandlerTest { - // private static final CryptoAlgorithm SOME_CRYPTO_ALGORITHM = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384; private static final List SOME_EDK_LIST = new ArrayList<>(Collections.singletonList(new KeyBlob())); private static final CommitmentPolicy SOME_COMMITMENT_POLICY = CommitmentPolicy.RequireEncryptRequireDecrypt; From abfc8d945636dfb97b019ff94e7b9143f0b870c6 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Tue, 11 Jun 2024 09:10:20 -0700 Subject: [PATCH 13/19] checkstyle? --- .../amazonaws/encryptionsdk/CMMHandler.java | 15 ++-- .../examples/v2/CustomCMMExampleTest.java | 15 ++-- .../v2/V2DefaultCryptoMaterialsManager.java | 68 ++++++++++--------- .../encryptionsdk/AllTestsSuite.java | 2 +- .../encryptionsdk/CMMHandlerTest.java | 13 ++-- .../model/DecryptionMaterialsTest.java | 7 +- 6 files changed, 58 insertions(+), 62 deletions(-) diff --git a/src/main/java/com/amazonaws/encryptionsdk/CMMHandler.java b/src/main/java/com/amazonaws/encryptionsdk/CMMHandler.java index c63ca535..3e3eff50 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/CMMHandler.java +++ b/src/main/java/com/amazonaws/encryptionsdk/CMMHandler.java @@ -10,7 +10,6 @@ import com.amazonaws.encryptionsdk.model.EncryptionMaterialsHandler; import com.amazonaws.encryptionsdk.model.EncryptionMaterialsRequest; import com.amazonaws.encryptionsdk.model.KeyBlob; - import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; @@ -65,10 +64,10 @@ private GetEncryptionMaterialsInput getEncryptionMaterialsRequestInput( public DecryptionMaterialsHandler decryptMaterials( DecryptionMaterialsRequest request, CommitmentPolicy commitmentPolicy) { if (cmm != null && mplCMM == null) { - // This is an implementation of the legacy native CryptoMaterialsManager interface from ESDK-Java. + // This is an implementation of the legacy native CryptoMaterialsManager interface from + // ESDK-Java. DecryptionMaterials materials = cmm.decryptMaterials(request); - if (materials.getEncryptionContext().isEmpty() - && !request.getEncryptionContext().isEmpty()) { + if (materials.getEncryptionContext().isEmpty() && !request.getEncryptionContext().isEmpty()) { // If the request specified an encryption context, // and we are using the legacy native CMM, // add the encryptionContext to the materials. @@ -82,7 +81,8 @@ public DecryptionMaterialsHandler decryptMaterials( // It now sets the encryptionContext attribute with the value from the ciphertext's headers. // // But custom CMMs' behavior was not updated. - // However, there is no custom CMM before version 3.0 that could set an encryptionContext attribute. + // However, there is no custom CMM before version 3.0 that could set an encryptionContext + // attribute. // The encryptionContext attribute was only introduced to decryptMaterials objects // in ESDK 3.0, so no CMM could have configured this attribute before 3.0. // As a result, the ESDK assumes that any native CMM @@ -91,9 +91,8 @@ public DecryptionMaterialsHandler decryptMaterials( // // If a custom CMM implementation conflicts with this assumption. // that CMM implementation MUST move to the MPL. - materials = materials.toBuilder() - .setEncryptionContext(request.getEncryptionContext()) - .build(); + materials = + materials.toBuilder().setEncryptionContext(request.getEncryptionContext()).build(); } return new DecryptionMaterialsHandler(materials); } else { diff --git a/src/test/java/com/amazonaws/crypto/examples/v2/CustomCMMExampleTest.java b/src/test/java/com/amazonaws/crypto/examples/v2/CustomCMMExampleTest.java index acecd8f9..55115dfb 100644 --- a/src/test/java/com/amazonaws/crypto/examples/v2/CustomCMMExampleTest.java +++ b/src/test/java/com/amazonaws/crypto/examples/v2/CustomCMMExampleTest.java @@ -8,24 +8,21 @@ import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; import org.junit.Test; - public class CustomCMMExampleTest { @Test public void testCustomCMMExample() { - CryptoMaterialsManager cmm = new CustomCMMExample.SigningSuiteOnlyCMM( - KmsMasterKeyProvider.builder().buildStrict(KMSTestFixtures.US_WEST_2_KEY_ID) - ); + CryptoMaterialsManager cmm = + new CustomCMMExample.SigningSuiteOnlyCMM( + KmsMasterKeyProvider.builder().buildStrict(KMSTestFixtures.US_WEST_2_KEY_ID)); CustomCMMExample.encryptAndDecryptWithCMM(cmm); } @Test public void testV2Cmm() { - V2DefaultCryptoMaterialsManager cmm = new V2DefaultCryptoMaterialsManager( - KmsMasterKeyProvider.builder().buildStrict(KMSTestFixtures.US_WEST_2_KEY_ID) - ); + V2DefaultCryptoMaterialsManager cmm = + new V2DefaultCryptoMaterialsManager( + KmsMasterKeyProvider.builder().buildStrict(KMSTestFixtures.US_WEST_2_KEY_ID)); CustomCMMExample.encryptAndDecryptWithCMM(cmm); } } - - diff --git a/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java b/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java index 5cb1255b..cd8c8eeb 100644 --- a/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java +++ b/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java @@ -28,27 +28,29 @@ import static com.amazonaws.encryptionsdk.internal.Utils.assertNonNull; /* - This is a copy-paste of the DefaultCryptoMaterialsManager implementation - from the final commit of the V2 ESDK: 1870a082358d59e32c60d74116d6f43c0efa466b - ESDK V3 implicitly changed the contract between CMMs and the ESDK. - After V3, DecryptMaterials has an `encryptionContext` attribute, - and CMMs are expected to set this attribute. - The V3 commit modified this DefaultCMM's `decryptMaterials` implementation - to set encryptionContext on returned DecryptionMaterials objects. - However, there are custom implementations of the legacy native CMM - that do not set encryptionContext. - This CMM is used to explicitly assert that the V2 implementation of - the DefaultCMM is compatible with V3 logic, - which implicitly asserts that custom implementations of V2-compatible CMMs - are also compatible with V3 logic. - */ + This is a copy-paste of the DefaultCryptoMaterialsManager implementation + from the final commit of the V2 ESDK: 1870a082358d59e32c60d74116d6f43c0efa466b + ESDK V3 implicitly changed the contract between CMMs and the ESDK. + After V3, DecryptMaterials has an `encryptionContext` attribute, + and CMMs are expected to set this attribute. + The V3 commit modified this DefaultCMM's `decryptMaterials` implementation + to set encryptionContext on returned DecryptionMaterials objects. + However, there are custom implementations of the legacy native CMM + that do not set encryptionContext. + This CMM is used to explicitly assert that the V2 implementation of + the DefaultCMM is compatible with V3 logic, + which implicitly asserts that custom implementations of V2-compatible CMMs + are also compatible with V3 logic. +*/ public class V2DefaultCryptoMaterialsManager implements CryptoMaterialsManager { private final MasterKeyProvider mkp; private final CryptoAlgorithm DEFAULT_CRYPTO_ALGORITHM = - CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384; + CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384; - /** @param mkp The master key provider to delegate to */ + /** + * @param mkp The master key provider to delegate to + */ public V2DefaultCryptoMaterialsManager(MasterKeyProvider mkp) { assertNonNull(mkp, "mkp"); this.mkp = mkp; @@ -73,7 +75,7 @@ public EncryptionMaterials getMaterialsForEncrypt(EncryptionMaterialsRequest req trailingKeys = generateTrailingSigKeyPair(algo); if (context.containsKey(Constants.EC_PUBLIC_KEY_FIELD)) { throw new IllegalArgumentException( - "EncryptionContext contains reserved field " + Constants.EC_PUBLIC_KEY_FIELD); + "EncryptionContext contains reserved field " + Constants.EC_PUBLIC_KEY_FIELD); } // make mutable context = new HashMap<>(context); @@ -95,8 +97,8 @@ public EncryptionMaterials getMaterialsForEncrypt(EncryptionMaterialsRequest req @SuppressWarnings("unchecked") final List mks = - (List) - assertNonNull(mkp, "provider").getMasterKeysForEncryption(mkRequestBuilder.build()); + (List) + assertNonNull(mkp, "provider").getMasterKeysForEncryption(mkRequestBuilder.build()); if (mks.isEmpty()) { throw new IllegalArgumentException("No master keys provided"); @@ -114,20 +116,20 @@ public EncryptionMaterials getMaterialsForEncrypt(EncryptionMaterialsRequest req //noinspection unchecked return EncryptionMaterials.newBuilder() - .setAlgorithm(algo) - .setCleartextDataKey(dataKey.getKey()) - .setEncryptedDataKeys(keyBlobs) - .setEncryptionContext(context) - .setTrailingSignatureKey(trailingKeys == null ? null : trailingKeys.getPrivate()) - .setMasterKeys(mks) - .build(); + .setAlgorithm(algo) + .setCleartextDataKey(dataKey.getKey()) + .setEncryptedDataKeys(keyBlobs) + .setEncryptionContext(context) + .setTrailingSignatureKey(trailingKeys == null ? null : trailingKeys.getPrivate()) + .setMasterKeys(mks) + .build(); } @Override public DecryptionMaterials decryptMaterials(DecryptionMaterialsRequest request) { DataKey dataKey = - mkp.decryptDataKey( - request.getAlgorithm(), request.getEncryptedDataKeys(), request.getEncryptionContext()); + mkp.decryptDataKey( + request.getAlgorithm(), request.getEncryptedDataKeys(), request.getEncryptionContext()); if (dataKey == null) { throw new CannotUnwrapDataKeyException("Could not decrypt any data keys"); @@ -151,9 +153,9 @@ public DecryptionMaterials decryptMaterials(DecryptionMaterialsRequest request) } return DecryptionMaterials.newBuilder() - .setDataKey(dataKey) - .setTrailingSignatureKey(pubKey) - .build(); + .setDataKey(dataKey) + .setTrailingSignatureKey(pubKey) + .build(); } private PublicKey deserializeTrailingKeyFromEc(CryptoAlgorithm algo, String pubKey) { @@ -162,11 +164,11 @@ private PublicKey deserializeTrailingKeyFromEc(CryptoAlgorithm algo, String pubK private static String serializeTrailingKeyForEc(CryptoAlgorithm algo, KeyPair trailingKeys) { return TrailingSignatureAlgorithm.forCryptoAlgorithm(algo) - .serializePublicKey(trailingKeys.getPublic()); + .serializePublicKey(trailingKeys.getPublic()); } private static KeyPair generateTrailingSigKeyPair(CryptoAlgorithm algo) - throws GeneralSecurityException { + throws GeneralSecurityException { return TrailingSignatureAlgorithm.forCryptoAlgorithm(algo).generateKey(); } } diff --git a/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java b/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java index 1deaf067..6f337f6c 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java +++ b/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java @@ -50,8 +50,8 @@ import com.amazonaws.encryptionsdk.model.CipherBlockHeadersTest; import com.amazonaws.encryptionsdk.model.CipherFrameHeadersTest; import com.amazonaws.encryptionsdk.model.CiphertextHeadersTest; -import com.amazonaws.encryptionsdk.model.DecryptionMaterialsTest; import com.amazonaws.encryptionsdk.model.DecryptionMaterialsRequestTest; +import com.amazonaws.encryptionsdk.model.DecryptionMaterialsTest; import com.amazonaws.encryptionsdk.model.EncryptionMaterialsRequestTest; import com.amazonaws.encryptionsdk.model.KeyBlobTest; import com.amazonaws.encryptionsdk.multi.MultipleMasterKeyTest; diff --git a/src/test/java/com/amazonaws/encryptionsdk/CMMHandlerTest.java b/src/test/java/com/amazonaws/encryptionsdk/CMMHandlerTest.java index 50035f5c..f0b138f1 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/CMMHandlerTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/CMMHandlerTest.java @@ -3,23 +3,22 @@ package com.amazonaws.encryptionsdk; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import com.amazonaws.encryptionsdk.model.DecryptionMaterials; import com.amazonaws.encryptionsdk.model.DecryptionMaterialsHandler; import com.amazonaws.encryptionsdk.model.DecryptionMaterialsRequest; import com.amazonaws.encryptionsdk.model.KeyBlob; -import org.junit.Test; - import java.security.PublicKey; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import org.junit.Test; public class CMMHandlerTest { diff --git a/src/test/java/com/amazonaws/encryptionsdk/model/DecryptionMaterialsTest.java b/src/test/java/com/amazonaws/encryptionsdk/model/DecryptionMaterialsTest.java index 3466b852..af1972b9 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/model/DecryptionMaterialsTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/model/DecryptionMaterialsTest.java @@ -3,13 +3,12 @@ package com.amazonaws.encryptionsdk.model; -import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; import java.util.Collections; import java.util.Map; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; +import org.junit.Test; public class DecryptionMaterialsTest { From 98fbe3996afbee7945464f49f926a1dedd8716a5 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Tue, 11 Jun 2024 09:12:29 -0700 Subject: [PATCH 14/19] ? --- .../v2/V2DefaultCryptoMaterialsManager.java | 5 +- .../encryptionsdk/CMMHandlerTest.java | 114 ++++++++++-------- 2 files changed, 69 insertions(+), 50 deletions(-) diff --git a/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java b/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java index cd8c8eeb..cac9e707 100644 --- a/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java +++ b/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java @@ -1,5 +1,7 @@ package com.amazonaws.crypto.examples.v2; +import static com.amazonaws.encryptionsdk.internal.Utils.assertNonNull; + import com.amazonaws.encryptionsdk.CommitmentPolicy; import com.amazonaws.encryptionsdk.CryptoAlgorithm; import com.amazonaws.encryptionsdk.CryptoMaterialsManager; @@ -16,7 +18,6 @@ import com.amazonaws.encryptionsdk.model.EncryptionMaterials; import com.amazonaws.encryptionsdk.model.EncryptionMaterialsRequest; import com.amazonaws.encryptionsdk.model.KeyBlob; - import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.PublicKey; @@ -25,8 +26,6 @@ import java.util.List; import java.util.Map; -import static com.amazonaws.encryptionsdk.internal.Utils.assertNonNull; - /* This is a copy-paste of the DefaultCryptoMaterialsManager implementation from the final commit of the V2 ESDK: 1870a082358d59e32c60d74116d6f43c0efa466b diff --git a/src/test/java/com/amazonaws/encryptionsdk/CMMHandlerTest.java b/src/test/java/com/amazonaws/encryptionsdk/CMMHandlerTest.java index f0b138f1..84073879 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/CMMHandlerTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/CMMHandlerTest.java @@ -22,119 +22,139 @@ public class CMMHandlerTest { - private static final CryptoAlgorithm SOME_CRYPTO_ALGORITHM = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384; - private static final List SOME_EDK_LIST = new ArrayList<>(Collections.singletonList(new KeyBlob())); - private static final CommitmentPolicy SOME_COMMITMENT_POLICY = CommitmentPolicy.RequireEncryptRequireDecrypt; + private static final CryptoAlgorithm SOME_CRYPTO_ALGORITHM = + CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384; + private static final List SOME_EDK_LIST = + new ArrayList<>(Collections.singletonList(new KeyBlob())); + private static final CommitmentPolicy SOME_COMMITMENT_POLICY = + CommitmentPolicy.RequireEncryptRequireDecrypt; private static final Map SOME_NON_EMPTY_ENCRYPTION_CONTEXT = new HashMap<>(); - static {{ - SOME_NON_EMPTY_ENCRYPTION_CONTEXT.put("SomeKey", "SomeValue"); - }} + static { + { + SOME_NON_EMPTY_ENCRYPTION_CONTEXT.put("SomeKey", "SomeValue"); + } + } private static final DecryptionMaterialsRequest SOME_DECRYPTION_MATERIALS_REQUEST_NON_EMPTY_EC = - DecryptionMaterialsRequest.newBuilder() - .setAlgorithm(SOME_CRYPTO_ALGORITHM) - // Given: Request has some non-empty encryption context - .setEncryptionContext(SOME_NON_EMPTY_ENCRYPTION_CONTEXT) - .setReproducedEncryptionContext(new HashMap<>()) - .setEncryptedDataKeys(SOME_EDK_LIST) - .build(); + DecryptionMaterialsRequest.newBuilder() + .setAlgorithm(SOME_CRYPTO_ALGORITHM) + // Given: Request has some non-empty encryption context + .setEncryptionContext(SOME_NON_EMPTY_ENCRYPTION_CONTEXT) + .setReproducedEncryptionContext(new HashMap<>()) + .setEncryptedDataKeys(SOME_EDK_LIST) + .build(); private static final DecryptionMaterialsRequest SOME_DECRYPTION_MATERIALS_REQUEST_EMPTY_EC = - DecryptionMaterialsRequest.newBuilder() - .setAlgorithm(SOME_CRYPTO_ALGORITHM) - // Given: Request has empty encryption context - .setEncryptionContext(new HashMap<>()) - .setReproducedEncryptionContext(new HashMap<>()) - .setEncryptedDataKeys(SOME_EDK_LIST) - .build(); + DecryptionMaterialsRequest.newBuilder() + .setAlgorithm(SOME_CRYPTO_ALGORITHM) + // Given: Request has empty encryption context + .setEncryptionContext(new HashMap<>()) + .setReproducedEncryptionContext(new HashMap<>()) + .setEncryptedDataKeys(SOME_EDK_LIST) + .build(); @Test - public void GIVEN_CMM_does_not_add_encryption_context_AND_request_has_nonempty_encryption_context_WHEN_decryptMaterials_THEN_output_has_nonempty_encryption_context() { + public void + GIVEN_CMM_does_not_add_encryption_context_AND_request_has_nonempty_encryption_context_WHEN_decryptMaterials_THEN_output_has_nonempty_encryption_context() { CryptoMaterialsManager anyNativeCMM = mock(CryptoMaterialsManager.class); // Given: native CMM does not set an encryptionContext on returned DecryptionMaterials objects - DecryptionMaterials someDecryptionMaterialsWithoutEC = DecryptionMaterials.newBuilder() - .setDataKey(mock(DataKey.class)) - .setTrailingSignatureKey(mock(PublicKey.class)) - .setEncryptionContext(new HashMap<>()).build(); + DecryptionMaterials someDecryptionMaterialsWithoutEC = + DecryptionMaterials.newBuilder() + .setDataKey(mock(DataKey.class)) + .setTrailingSignatureKey(mock(PublicKey.class)) + .setEncryptionContext(new HashMap<>()) + .build(); // Given: request with nonempty encryption context when(anyNativeCMM.decryptMaterials(SOME_DECRYPTION_MATERIALS_REQUEST_NON_EMPTY_EC)) - .thenReturn(someDecryptionMaterialsWithoutEC); + .thenReturn(someDecryptionMaterialsWithoutEC); // When: decryptMaterials CMMHandler handlerUnderTest = new CMMHandler(anyNativeCMM); - DecryptionMaterialsHandler output = handlerUnderTest.decryptMaterials(SOME_DECRYPTION_MATERIALS_REQUEST_NON_EMPTY_EC, - SOME_COMMITMENT_POLICY); + DecryptionMaterialsHandler output = + handlerUnderTest.decryptMaterials( + SOME_DECRYPTION_MATERIALS_REQUEST_NON_EMPTY_EC, SOME_COMMITMENT_POLICY); // Then: output DecryptionMaterialsHandler has encryption context assertEquals(SOME_NON_EMPTY_ENCRYPTION_CONTEXT, output.getEncryptionContext()); } @Test - public void GIVEN_CMM_does_not_add_encryption_context_AND_request_has_empty_encryption_context_WHEN_decryptMaterials_THEN_output_has_empty_encryption_context() { + public void + GIVEN_CMM_does_not_add_encryption_context_AND_request_has_empty_encryption_context_WHEN_decryptMaterials_THEN_output_has_empty_encryption_context() { CryptoMaterialsManager anyNativeCMM = mock(CryptoMaterialsManager.class); // Given: native CMM does not set an encryptionContext on returned DecryptionMaterials objects - DecryptionMaterials someDecryptionMaterialsWithoutEC = DecryptionMaterials.newBuilder() + DecryptionMaterials someDecryptionMaterialsWithoutEC = + DecryptionMaterials.newBuilder() .setDataKey(mock(DataKey.class)) .setTrailingSignatureKey(mock(PublicKey.class)) - .setEncryptionContext(new HashMap<>()).build(); + .setEncryptionContext(new HashMap<>()) + .build(); // Given: request with empty encryption context when(anyNativeCMM.decryptMaterials(SOME_DECRYPTION_MATERIALS_REQUEST_EMPTY_EC)) - .thenReturn(someDecryptionMaterialsWithoutEC); + .thenReturn(someDecryptionMaterialsWithoutEC); // When: decryptMaterials CMMHandler handlerUnderTest = new CMMHandler(anyNativeCMM); - DecryptionMaterialsHandler output = handlerUnderTest.decryptMaterials(SOME_DECRYPTION_MATERIALS_REQUEST_EMPTY_EC, - SOME_COMMITMENT_POLICY); + DecryptionMaterialsHandler output = + handlerUnderTest.decryptMaterials( + SOME_DECRYPTION_MATERIALS_REQUEST_EMPTY_EC, SOME_COMMITMENT_POLICY); // Then: output DecryptionMaterialsHandler has empty encryption context assertTrue(output.getEncryptionContext().isEmpty()); } @Test - public void GIVEN_CMM_adds_encryption_context_AND_request_has_nonempty_encryption_context_WHEN_decryptMaterials_THEN_output_has_nonempty_encryption_context() { + public void + GIVEN_CMM_adds_encryption_context_AND_request_has_nonempty_encryption_context_WHEN_decryptMaterials_THEN_output_has_nonempty_encryption_context() { CryptoMaterialsManager anyNativeCMM = mock(CryptoMaterialsManager.class); // Given: native CMM sets encryptionContext on returned DecryptionMaterials objects - DecryptionMaterials someDecryptionMaterialsWithoutEC = DecryptionMaterials.newBuilder() + DecryptionMaterials someDecryptionMaterialsWithoutEC = + DecryptionMaterials.newBuilder() .setDataKey(mock(DataKey.class)) .setTrailingSignatureKey(mock(PublicKey.class)) - .setEncryptionContext(SOME_NON_EMPTY_ENCRYPTION_CONTEXT).build(); + .setEncryptionContext(SOME_NON_EMPTY_ENCRYPTION_CONTEXT) + .build(); // Given: request with nonempty encryption context when(anyNativeCMM.decryptMaterials(SOME_DECRYPTION_MATERIALS_REQUEST_NON_EMPTY_EC)) - .thenReturn(someDecryptionMaterialsWithoutEC); + .thenReturn(someDecryptionMaterialsWithoutEC); // When: decryptMaterials CMMHandler handlerUnderTest = new CMMHandler(anyNativeCMM); - DecryptionMaterialsHandler output = handlerUnderTest.decryptMaterials(SOME_DECRYPTION_MATERIALS_REQUEST_NON_EMPTY_EC, - SOME_COMMITMENT_POLICY); + DecryptionMaterialsHandler output = + handlerUnderTest.decryptMaterials( + SOME_DECRYPTION_MATERIALS_REQUEST_NON_EMPTY_EC, SOME_COMMITMENT_POLICY); // Then: output DecryptionMaterialsHandler has nonempty encryption context assertEquals(SOME_NON_EMPTY_ENCRYPTION_CONTEXT, output.getEncryptionContext()); } @Test - public void GIVEN_CMM_adds_encryption_context_AND_request_has_empty_encryption_context_WHEN_decryptMaterials_THEN_output_has_empty_encryption_context() { + public void + GIVEN_CMM_adds_encryption_context_AND_request_has_empty_encryption_context_WHEN_decryptMaterials_THEN_output_has_empty_encryption_context() { CryptoMaterialsManager anyNativeCMM = mock(CryptoMaterialsManager.class); // Given: native CMM sets encryptionContext on returned DecryptionMaterials objects - DecryptionMaterials someDecryptionMaterialsWithoutEC = DecryptionMaterials.newBuilder() + DecryptionMaterials someDecryptionMaterialsWithoutEC = + DecryptionMaterials.newBuilder() .setDataKey(mock(DataKey.class)) .setTrailingSignatureKey(mock(PublicKey.class)) - .setEncryptionContext(new HashMap<>()).build(); + .setEncryptionContext(new HashMap<>()) + .build(); // Given: request with empty encryption context when(anyNativeCMM.decryptMaterials(SOME_DECRYPTION_MATERIALS_REQUEST_EMPTY_EC)) - .thenReturn(someDecryptionMaterialsWithoutEC); + .thenReturn(someDecryptionMaterialsWithoutEC); // When: decryptMaterials CMMHandler handlerUnderTest = new CMMHandler(anyNativeCMM); - DecryptionMaterialsHandler output = handlerUnderTest.decryptMaterials(SOME_DECRYPTION_MATERIALS_REQUEST_EMPTY_EC, - SOME_COMMITMENT_POLICY); + DecryptionMaterialsHandler output = + handlerUnderTest.decryptMaterials( + SOME_DECRYPTION_MATERIALS_REQUEST_EMPTY_EC, SOME_COMMITMENT_POLICY); // Then: output DecryptionMaterialsHandler has empty encryption context assertTrue(output.getEncryptionContext().isEmpty()); } - } From 6c96166c30e938b68699ae524f842e51f7be86da Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Tue, 11 Jun 2024 09:13:09 -0700 Subject: [PATCH 15/19] Update src/main/java/com/amazonaws/encryptionsdk/model/DecryptionMaterials.java Co-authored-by: seebees --- .../amazonaws/encryptionsdk/model/DecryptionMaterials.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/amazonaws/encryptionsdk/model/DecryptionMaterials.java b/src/main/java/com/amazonaws/encryptionsdk/model/DecryptionMaterials.java index 253414e2..5f50793c 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/model/DecryptionMaterials.java +++ b/src/main/java/com/amazonaws/encryptionsdk/model/DecryptionMaterials.java @@ -13,9 +13,8 @@ public final class DecryptionMaterials { private DecryptionMaterials(Builder b) { dataKey = b.getDataKey(); trailingSignatureKey = b.getTrailingSignatureKey(); - if (b.getEncryptionContext() != null) { - encryptionContext = b.getEncryptionContext(); - } else { + encryptionContext = b.getEncryptionContext(); + if (encryptionContext == null) { encryptionContext = Collections.emptyMap(); } } From 51db28e944bb97583c3fe83d4c13438ea3f9363b Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Tue, 11 Jun 2024 09:18:08 -0700 Subject: [PATCH 16/19] ? --- codebuild/ci/static-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codebuild/ci/static-analysis.yml b/codebuild/ci/static-analysis.yml index db66feaa..484d40d9 100644 --- a/codebuild/ci/static-analysis.yml +++ b/codebuild/ci/static-analysis.yml @@ -7,5 +7,5 @@ phases: java: corretto11 build: commands: - - mvn -T 4 -ntp com.coveo:fmt-maven-plugin:check + - mvn -T 4 -ntp com.coveo:fmt-maven-plugin:check -Dfmt.verbose=true - ./util/test-conditions.sh From 5008c0d823d5c83f9668509b233ea97e320eb6c1 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Tue, 11 Jun 2024 09:20:24 -0700 Subject: [PATCH 17/19] fix --- .../amazonaws/encryptionsdk/model/DecryptionMaterials.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/amazonaws/encryptionsdk/model/DecryptionMaterials.java b/src/main/java/com/amazonaws/encryptionsdk/model/DecryptionMaterials.java index 5f50793c..253414e2 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/model/DecryptionMaterials.java +++ b/src/main/java/com/amazonaws/encryptionsdk/model/DecryptionMaterials.java @@ -13,8 +13,9 @@ public final class DecryptionMaterials { private DecryptionMaterials(Builder b) { dataKey = b.getDataKey(); trailingSignatureKey = b.getTrailingSignatureKey(); - encryptionContext = b.getEncryptionContext(); - if (encryptionContext == null) { + if (b.getEncryptionContext() != null) { + encryptionContext = b.getEncryptionContext(); + } else { encryptionContext = Collections.emptyMap(); } } From dbbccfbf2dc36cb11688b4d42f5058765c7a88f5 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Tue, 11 Jun 2024 09:29:06 -0700 Subject: [PATCH 18/19] ?? --- .../crypto/examples/v2/V2DefaultCryptoMaterialsManager.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java b/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java index cac9e707..9dc109e5 100644 --- a/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java +++ b/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java @@ -1,3 +1,5 @@ +// @formatter:off +// This is copy-paste and has formatting issues. package com.amazonaws.crypto.examples.v2; import static com.amazonaws.encryptionsdk.internal.Utils.assertNonNull; @@ -171,3 +173,4 @@ private static KeyPair generateTrailingSigKeyPair(CryptoAlgorithm algo) return TrailingSignatureAlgorithm.forCryptoAlgorithm(algo).generateKey(); } } +// @formatter:on \ No newline at end of file From ff5bf3bf7d2df7e015b4e7bb323e9c11a6d95b70 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Tue, 11 Jun 2024 09:41:22 -0700 Subject: [PATCH 19/19] fix --- codebuild/ci/static-analysis.yml | 2 +- .../examples/v2/V2DefaultCryptoMaterialsManager.java | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/codebuild/ci/static-analysis.yml b/codebuild/ci/static-analysis.yml index 484d40d9..db66feaa 100644 --- a/codebuild/ci/static-analysis.yml +++ b/codebuild/ci/static-analysis.yml @@ -7,5 +7,5 @@ phases: java: corretto11 build: commands: - - mvn -T 4 -ntp com.coveo:fmt-maven-plugin:check -Dfmt.verbose=true + - mvn -T 4 -ntp com.coveo:fmt-maven-plugin:check - ./util/test-conditions.sh diff --git a/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java b/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java index 9dc109e5..650a8ef5 100644 --- a/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java +++ b/src/test/java/com/amazonaws/crypto/examples/v2/V2DefaultCryptoMaterialsManager.java @@ -1,5 +1,3 @@ -// @formatter:off -// This is copy-paste and has formatting issues. package com.amazonaws.crypto.examples.v2; import static com.amazonaws.encryptionsdk.internal.Utils.assertNonNull; @@ -49,9 +47,7 @@ public class V2DefaultCryptoMaterialsManager implements CryptoMaterialsManager { private final CryptoAlgorithm DEFAULT_CRYPTO_ALGORITHM = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384; - /** - * @param mkp The master key provider to delegate to - */ + /** @param mkp The master key provider to delegate to */ public V2DefaultCryptoMaterialsManager(MasterKeyProvider mkp) { assertNonNull(mkp, "mkp"); this.mkp = mkp; @@ -173,4 +169,3 @@ private static KeyPair generateTrailingSigKeyPair(CryptoAlgorithm algo) return TrailingSignatureAlgorithm.forCryptoAlgorithm(algo).generateKey(); } } -// @formatter:on \ No newline at end of file