Skip to content

Commit 32580c5

Browse files
Add support for standard test vectors. (#127)
* Add support for standard test vectors. Now handles standard test vectors when the ZIP containing them is pointed to by the testVectorZip system property. Example: `mvn install -Dgpg.skip=true '-DtestVectorZip=https://github.com/awslabs/aws-encryption-sdk-test-vectors/raw/master/vectors/awses-decrypt/python-1.3.8.zip'` This also adds them to our standard travis build. * Address minor feedback
1 parent 58b88c9 commit 32580c5

File tree

4 files changed

+300
-1
lines changed

4 files changed

+300
-1
lines changed

.travis.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ jdk:
66
- openjdk11
77
- oraclejdk8
88
- oraclejdk9
9-
script: mvn install -Dgpg.skip=true
9+
script: mvn install -Dgpg.skip=true '-DtestVectorZip=https://github.com/awslabs/aws-encryption-sdk-test-vectors/raw/master/vectors/awses-decrypt/python-1.3.8.zip'
1010
sudo: false
1111
dist: trusty

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 1.6.1 -- Unreleased
4+
### Maintenance
5+
* Add support for standard test vectors via `testVectorZip` system property.
6+
37
## 1.6.0 -- 2019-05-31
48

59
### Minor Changes

src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
AwsCryptoTest.class,
4747
CryptoInputStreamTest.class,
4848
CryptoOutputStreamTest.class,
49+
TestVectorRunner.class,
4950
XCompatDecryptTest.class,
5051
DefaultCryptoMaterialsManagerTest.class,
5152
NullCryptoMaterialsCacheTest.class,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
package com.amazonaws.encryptionsdk;
2+
3+
import static java.lang.String.format;
4+
5+
import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
6+
import com.amazonaws.encryptionsdk.jce.JceMasterKey;
7+
import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider;
8+
import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory;
9+
import com.amazonaws.util.IOUtils;
10+
import com.fasterxml.jackson.core.type.TypeReference;
11+
import com.fasterxml.jackson.databind.ObjectMapper;
12+
import org.bouncycastle.util.encoders.Base64;
13+
import org.junit.AfterClass;
14+
import org.junit.Assert;
15+
import org.junit.Test;
16+
import org.junit.runner.RunWith;
17+
import org.junit.runners.Parameterized;
18+
19+
import javax.crypto.SecretKey;
20+
import javax.crypto.spec.SecretKeySpec;
21+
import java.io.IOException;
22+
import java.io.InputStream;
23+
import java.net.JarURLConnection;
24+
import java.net.URL;
25+
import java.security.GeneralSecurityException;
26+
import java.security.Key;
27+
import java.security.KeyFactory;
28+
import java.security.PrivateKey;
29+
import java.security.PublicKey;
30+
import java.security.spec.PKCS8EncodedKeySpec;
31+
import java.security.spec.X509EncodedKeySpec;
32+
import java.util.ArrayList;
33+
import java.util.Collection;
34+
import java.util.Collections;
35+
import java.util.HashMap;
36+
import java.util.List;
37+
import java.util.Map;
38+
import java.util.jar.JarFile;
39+
import java.util.zip.ZipEntry;
40+
41+
@RunWith(Parameterized.class)
42+
public class TestVectorRunner {
43+
// We save the files in memory to avoid repeatedly retrieving them. This won't work if the plaintexts are too
44+
// large or numerous
45+
private static final Map<String, byte[]> cachedData = new HashMap<>();
46+
47+
private final String testName;
48+
private final TestCase testCase;
49+
50+
public TestVectorRunner(final String testName, TestCase testCase) {
51+
this.testName = testName;
52+
this.testCase = testCase;
53+
}
54+
55+
@Test
56+
public void decrypt() {
57+
AwsCrypto crypto = new AwsCrypto();
58+
byte[] plaintext = crypto.decryptData(testCase.mkp, cachedData.get(testCase.ciphertextPath)).getResult();
59+
final byte[] expectedPlaintext = cachedData.get(testCase.plaintextPath);
60+
61+
Assert.assertArrayEquals(expectedPlaintext, plaintext);
62+
}
63+
64+
@Parameterized.Parameters(name="Compatibility Test: {0}")
65+
@SuppressWarnings("unchecked")
66+
public static Collection<Object[]> data() throws Exception {
67+
final String zipPath = System.getProperty("testVectorZip");
68+
if (zipPath == null) {
69+
return Collections.emptyList();
70+
}
71+
72+
final JarURLConnection jarConnection = (JarURLConnection) new URL("jar:" + zipPath + "!/").openConnection();
73+
74+
try (JarFile jar = jarConnection.getJarFile()) {
75+
final Map<String, Object> manifest = readJsonMapFromJar(jar, "manifest.json");
76+
77+
final Map<String, Object> metaData = (Map<String, Object>) manifest.get("manifest");
78+
79+
// We only support "awses-decrypt" type manifests right now
80+
if (!"awses-decrypt".equals(metaData.get("type"))) {
81+
throw new IllegalArgumentException("Unsupported manifest type: " + metaData.get("type"));
82+
}
83+
84+
if (!Integer.valueOf(1).equals(metaData.get("version"))) {
85+
throw new IllegalArgumentException("Unsupported manifest version: " + metaData.get("version"));
86+
}
87+
88+
final Map<String, KeyEntry> keys = parseKeyManifest(readJsonMapFromJar(jar, (String) manifest.get("keys")));
89+
90+
final KmsMasterKeyProvider kmsProv = KmsMasterKeyProvider
91+
.builder()
92+
.withCredentials(new DefaultAWSCredentialsProviderChain())
93+
.build();
94+
95+
List<Object[]> testCases = new ArrayList<>();
96+
for (Map.Entry<String, Map<String, Object>> testEntry :
97+
((Map<String, Map<String, Object>>) manifest.get("tests")).entrySet()) {
98+
testCases.add(new Object[]{testEntry.getKey(),
99+
parseTest(testEntry.getKey(), testEntry.getValue(), keys, jar, kmsProv)});
100+
}
101+
return testCases;
102+
}
103+
}
104+
105+
@AfterClass
106+
public static void teardown() {
107+
cachedData.clear();
108+
}
109+
110+
private static byte[] readBytesFromJar(JarFile jar, String fileName) throws IOException {
111+
try (InputStream is = readFromJar(jar, fileName)) {
112+
return IOUtils.toByteArray(is);
113+
}
114+
}
115+
116+
private static Map<String, Object> readJsonMapFromJar(JarFile jar, String fileName) throws IOException {
117+
try (InputStream is = readFromJar(jar, fileName)) {
118+
final ObjectMapper mapper = new ObjectMapper();
119+
return mapper.readValue(is, new TypeReference<Map<String, Object>>() {});
120+
}
121+
}
122+
123+
private static InputStream readFromJar(JarFile jar, String name) throws IOException {
124+
// Our manifest URIs incorrectly start with file:// rather than just file: so we need to strip this
125+
ZipEntry entry = jar.getEntry(name.replaceFirst("^file://(?!/)", ""));
126+
return jar.getInputStream(entry);
127+
}
128+
129+
private static void cacheData(JarFile jar, String url) throws IOException {
130+
if (!cachedData.containsKey(url)) {
131+
cachedData.put(url, readBytesFromJar(jar, url));
132+
}
133+
}
134+
135+
@SuppressWarnings("unchecked")
136+
private static TestCase parseTest(String testName, Map<String, Object> data, Map<String, KeyEntry> keys,
137+
JarFile jar, KmsMasterKeyProvider kmsProv) throws IOException {
138+
final String plaintextUrl = (String) data.get("plaintext");
139+
cacheData(jar, plaintextUrl);
140+
final String ciphertextURL = (String) data.get("ciphertext");
141+
cacheData(jar, ciphertextURL);
142+
143+
@SuppressWarnings("generic")
144+
final List<MasterKey<?>> mks = new ArrayList<>();
145+
146+
for (Map<String, String> mkEntry : (List<Map<String, String>>) data.get("master-keys")) {
147+
final String type = mkEntry.get("type");
148+
final String keyName = mkEntry.get("key");
149+
final KeyEntry key = keys.get(keyName);
150+
151+
if ("aws-kms".equals(type)) {
152+
mks.add(kmsProv.getMasterKey(key.keyId));
153+
} else if ("raw".equals(type)) {
154+
final String provId = mkEntry.get("provider-id");
155+
final String algorithm = mkEntry.get("encryption-algorithm");
156+
if ("aes".equals(algorithm)) {
157+
mks.add(JceMasterKey.getInstance((SecretKey) key.key, provId, key.keyId, "AES/GCM/NoPadding"));
158+
} else if ("rsa".equals(algorithm)) {
159+
String transformation = "RSA/ECB/";
160+
final String padding = mkEntry.get("padding-algorithm");
161+
if ("pkcs1".equals(padding)) {
162+
transformation += "PKCS1Padding";
163+
} else if ("oaep-mgf1".equals(padding)) {
164+
final String hashName = mkEntry.get("padding-hash")
165+
.replace("sha", "sha-")
166+
.toUpperCase();
167+
transformation += "OAEPWith" + hashName + "AndMGF1Padding";
168+
} else {
169+
throw new IllegalArgumentException("Unsupported padding:" + padding);
170+
}
171+
final PublicKey wrappingKey;
172+
final PrivateKey unwrappingKey;
173+
if (key.key instanceof PublicKey) {
174+
wrappingKey = (PublicKey) key.key;
175+
unwrappingKey = null;
176+
} else {
177+
wrappingKey = null;
178+
unwrappingKey = (PrivateKey) key.key;
179+
}
180+
mks.add(JceMasterKey.getInstance(wrappingKey, unwrappingKey, provId, key.keyId, transformation));
181+
} else {
182+
throw new IllegalArgumentException("Unsupported algorithm: " + algorithm);
183+
}
184+
} else {
185+
throw new IllegalArgumentException("Unsupported Key Type: " + type);
186+
}
187+
}
188+
189+
return new TestCase(testName, ciphertextURL, plaintextUrl, mks);
190+
}
191+
192+
@SuppressWarnings("unchecked")
193+
private static Map<String, KeyEntry> parseKeyManifest(final Map<String, Object> keysManifest) throws GeneralSecurityException {
194+
// check our type
195+
final Map<String, Object> metaData = (Map<String, Object>) keysManifest.get("manifest");
196+
if (!"keys".equals(metaData.get("type"))) {
197+
throw new IllegalArgumentException("Invalid manifest type: " + metaData.get("type"));
198+
}
199+
if (!Integer.valueOf(3).equals(metaData.get("version"))) {
200+
throw new IllegalArgumentException("Invalid manifest version: " + metaData.get("version"));
201+
}
202+
203+
final Map<String, KeyEntry> result = new HashMap<>();
204+
205+
Map<String, Object> keys = (Map<String, Object>) keysManifest.get("keys");
206+
for (Map.Entry<String, Object> entry : keys.entrySet()) {
207+
final String name = entry.getKey();
208+
final Map<String, Object> data = (Map<String, Object>) entry.getValue();
209+
210+
final String keyType = (String) data.get("type");
211+
final String encoding = (String) data.get("encoding");
212+
final String keyId = (String) data.get("key-id");
213+
final String material = (String) data.get("material"); // May be null
214+
final String algorithm = (String) data.get("algorithm"); // May be null
215+
216+
final KeyEntry keyEntry;
217+
218+
final KeyFactory kf;
219+
switch (keyType) {
220+
case "symmetric":
221+
if (!"base64".equals(encoding)) {
222+
throw new IllegalArgumentException(format("Key %s is symmetric but has encoding %s", keyId, encoding));
223+
}
224+
keyEntry = new KeyEntry(name, keyId, keyType,
225+
new SecretKeySpec(Base64.decode(material), algorithm.toUpperCase()));
226+
break;
227+
case "private":
228+
kf = KeyFactory.getInstance(algorithm);
229+
if (!"pem".equals(encoding)) {
230+
throw new IllegalArgumentException(format("Key %s is private but has encoding %s", keyId, encoding));
231+
}
232+
byte[] pkcs8Key = parsePem(material);
233+
keyEntry = new KeyEntry(name, keyId, keyType,
234+
kf.generatePrivate(new PKCS8EncodedKeySpec(pkcs8Key)));
235+
break;
236+
case "public":
237+
kf = KeyFactory.getInstance(algorithm);
238+
if (!"pem".equals(encoding)) {
239+
throw new IllegalArgumentException(format("Key %s is private but has encoding %s", keyId, encoding));
240+
}
241+
byte[] x509Key = parsePem(material);
242+
keyEntry = new KeyEntry(name, keyId, keyType,
243+
kf.generatePublic(new X509EncodedKeySpec(x509Key)));
244+
break;
245+
case "aws-kms":
246+
keyEntry = new KeyEntry(name, keyId, keyType, null);
247+
break;
248+
default:
249+
throw new IllegalArgumentException("Unsupported key type: " + keyType);
250+
}
251+
252+
result.put(name, keyEntry);
253+
}
254+
255+
return result;
256+
}
257+
258+
private static byte[] parsePem(String pem) {
259+
final String stripped = pem.replaceAll("-+[A-Z ]+-+", "");
260+
return Base64.decode(stripped);
261+
}
262+
263+
private static class KeyEntry {
264+
final String name;
265+
final String keyId;
266+
final String type;
267+
final Key key;
268+
269+
private KeyEntry(String name, String keyId, String type, Key key) {
270+
this.name = name;
271+
this.keyId = keyId;
272+
this.type = type;
273+
this.key = key;
274+
}
275+
}
276+
277+
private static class TestCase {
278+
private final String name;
279+
private final String ciphertextPath;
280+
private final String plaintextPath;
281+
private final MasterKeyProvider<?> mkp;
282+
283+
private TestCase(String name, String ciphertextPath, String plaintextPath, List<MasterKey<?>> mks) {
284+
this(name, ciphertextPath, plaintextPath, MultipleProviderFactory.buildMultiProvider(mks));
285+
}
286+
287+
private TestCase(String name, String ciphertextPath, String plaintextPath, MasterKeyProvider<?> mkp) {
288+
this.name = name;
289+
this.ciphertextPath = ciphertextPath;
290+
this.plaintextPath = plaintextPath;
291+
this.mkp = mkp;
292+
}
293+
}
294+
}

0 commit comments

Comments
 (0)