• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /**
2  * Licensed under the Apache License, Version 2.0 (the "License");
3  * you may not use this file except in compliance with the License.
4  * You may obtain a copy of the License at
5  *
6  *   http://www.apache.org/licenses/LICENSE-2.0
7  *
8  * Unless required by applicable law or agreed to in writing, software
9  * distributed under the License is distributed on an "AS IS" BASIS,
10  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11  * See the License for the specific language governing permissions and
12  * limitations under the License.
13  */
14 package com.google.security.wycheproof;
15 
16 import static org.junit.Assert.assertEquals;
17 import static org.junit.Assert.assertFalse;
18 import static org.junit.Assert.fail;
19 
20 import com.google.gson.JsonElement;
21 import com.google.gson.JsonObject;
22 import java.security.AlgorithmParameters;
23 import java.security.GeneralSecurityException;
24 import java.security.KeyStore;
25 import java.security.NoSuchAlgorithmException;
26 import javax.crypto.Cipher;
27 import javax.crypto.SecretKey;
28 import javax.crypto.spec.GCMParameterSpec;
29 import javax.crypto.spec.IvParameterSpec;
30 import javax.crypto.spec.SecretKeySpec;
31 import org.junit.Test;
32 import org.junit.Ignore;
33 import org.junit.After;
34 import android.security.keystore.KeyProtection;
35 import android.security.keystore.KeyProperties;
36 import android.keystore.cts.util.KeyStoreUtil;
37 
38 /** This test uses test vectors in JSON format to test AEAD schemes. */
39 public class JsonAeadTest {
40 
41   private static final String EXPECTED_PROVIDER_NAME = TestUtil.EXPECTED_PROVIDER_NAME;
42   private static final String EXPECTED_CRYPTO_PROVIDER_NAME =
43                                                         TestUtil.EXPECTED_CRYPTO_OP_PROVIDER_NAME;
44   private static final String KEY_ALIAS_1 = "Key1";
45 
46   @After
tearDown()47   public void tearDown() throws Exception {
48     KeyStoreUtil.cleanUpKeyStore();
49   }
50 
51   /** Joins two bytearrays. */
join(byte[] head, byte[] tail)52   protected static byte[] join(byte[] head, byte[] tail) {
53     byte[] res = new byte[head.length + tail.length];
54     System.arraycopy(head, 0, res, 0, head.length);
55     System.arraycopy(tail, 0, res, head.length, tail.length);
56     return res;
57   }
58 
59   /** Convenience method to get a byte array from an JsonObject */
getBytes(JsonObject obj, String name)60   protected static byte[] getBytes(JsonObject obj, String name) throws Exception {
61     return JsonUtil.asByteArray(obj.get(name));
62   }
63 
arrayEquals(byte[] a, byte[] b)64   protected static boolean arrayEquals(byte[] a, byte[] b) {
65     if (a.length != b.length) {
66       return false;
67     }
68     byte res = 0;
69     for (int i = 0; i < a.length; i++) {
70       res |= (byte) (a[i] ^ b[i]);
71     }
72     return res == 0;
73   }
74 
75   /**
76    * Returns an initialized instance of Cipher. Typically it is somewhat
77    * time consuming to generate a new instance of Cipher for each encryption.
78    * However, some implementations of ciphers (e.g. AES-GCM in jdk) check that
79    * the same key and nonce are not reused twice in a row to catch simple
80    * programming errors. This precaution can interfere with the tests, since
81    * the test vectors do sometimes repeat nonces. To avoid such problems cipher
82    * instances are not reused.
83    * @param algorithm the cipher algorithm including encryption mode and padding.
84    * @param opmode one of Cipher.ENCRYPT_MODE or Cipher.DECRYPT_MODE
85    * @param key the key bytes
86    * @param iv the bytes of the initialization vector
87    * @param tagSize the expected size of the tag
88    * @return an initialized instance of Cipher
89    * @throws Exception if the initialization failed.
90    */
getInitializedCipher( String algorithm, int opmode, byte[] key, byte[] iv, int tagSize, boolean isStrongBox)91   protected static Cipher getInitializedCipher(
92       String algorithm, int opmode, byte[] key, byte[] iv, int tagSize, boolean isStrongBox)
93       throws Exception {
94     Cipher cipher = Cipher.getInstance(algorithm, EXPECTED_CRYPTO_PROVIDER_NAME);
95     if (algorithm.equalsIgnoreCase("AES/GCM/NoPadding")) {
96       SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
97       KeyStore keyStore = KeyStoreUtil.saveSecretKeyToKeystore(KEY_ALIAS_1, keySpec,
98           new KeyProtection.Builder(KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
99                   .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
100                   .setRandomizedEncryptionRequired(false)
101                   .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
102                   .setIsStrongBoxBacked(isStrongBox)
103                   .build());
104       // Key imported, obtain a reference to it.
105       SecretKey keyStoreKey = (SecretKey) keyStore.getKey(KEY_ALIAS_1, null);
106 
107       AlgorithmParameters params = AlgorithmParameters.getInstance("GCM");
108       params.init(new GCMParameterSpec(tagSize, iv));
109       cipher.init(opmode, keyStoreKey, params);
110     } else if (algorithm.equalsIgnoreCase("AES/EAX/NoPadding")
111                || algorithm.equalsIgnoreCase("AES/CCM/NoPadding")) {
112       SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
113       // TODO(bleichen): This works for BouncyCastle but looks non-standard.
114       //   org.bouncycastle.crypto.params.AEADParameters works too, but would add a dependency that
115       //   we want to avoid.
116       GCMParameterSpec parameters = new GCMParameterSpec(tagSize, iv);
117       cipher.init(opmode, keySpec, parameters);
118     } else if (algorithm.toUpperCase().startsWith("CHACHA")) {
119       SecretKeySpec keySpec = new SecretKeySpec(key, "ChaCha20");
120       IvParameterSpec parameters = new IvParameterSpec(iv);
121       cipher.init(opmode, keySpec, parameters);
122     } else {
123       fail("Algorithm not supported:" + algorithm);
124     }
125     return cipher;
126   }
127 
128   /** Example format for test vectors
129    * {
130    *   "algorithm" : "AES-EAX",
131    *   "generatorVersion" : "0.0a14",
132    *   "numberOfTests" : 143,
133    *   "testGroups" : [
134    *     {
135    *       "ivSize" : 128,
136    *       "keySize" : 128,
137    *       "tagSize" : 128,
138    *       "type" : "AES-EAX",
139    *       "tests" : [
140    *         {
141    *           "aad" : "6bfb914fd07eae6b",
142    *           "comment" : "eprint.iacr.org/2003/069",
143    *           "ct" : "",
144    *           "iv" : "62ec67f9c3a4a407fcb2a8c49031a8b3",
145    *           "key" : "233952dee4d5ed5f9b9c6d6ff80ff478",
146    *           "msg" : "",
147    *           "result" : "valid",
148    *           "tag" : "e037830e8389f27b025a2d6527e79d01",
149    *           "tcId" : 1
150    *         },
151    *        ...
152    **/
153   // This is a false positive, since errorprone cannot track values passed into a method.
154   @SuppressWarnings("InsecureCryptoUsage")
testAead(String filename, String algorithm)155   public void testAead(String filename, String algorithm) throws Exception {
156     testAead(filename, algorithm, false);
157   }
testAead(String filename, String algorithm, boolean isStrongBox)158   public void testAead(String filename, String algorithm, boolean isStrongBox) throws Exception {
159     // Version number have the format major.minor[.subversion].
160     // Versions before 1.0 are experimental and  use formats that are expected to change.
161     // Versions after 1.0 change the major number if the format changes and change
162     // the minor number if only the test vectors (but not the format) changes.
163     // Subversions are release candidate for the next version.
164     //
165     // Relevant version changes:
166     // <ul>
167     // <li> Version 0.5 adds test vectors for CCM.
168     // <li> Version 0.6 adds test vectors for Chacha20-Poly1305.
169     //      Chacha20-Poly1305 is a new cipher added in jdk11.
170     // </ul>
171     final String expectedVersion = "0.6";
172 
173     // Checking preconditions.
174     Cipher.getInstance(algorithm, EXPECTED_CRYPTO_PROVIDER_NAME);
175 
176     JsonObject test = JsonUtil.getTestVectors(this.getClass(), filename);
177     String generatorVersion = test.get("generatorVersion").getAsString();
178     assertFalse(
179           algorithm
180               + ": expecting test vectors with version "
181               + expectedVersion
182               + " found vectors with version "
183               + generatorVersion,
184           generatorVersion.equals(expectedVersion));
185     int numTests = test.get("numberOfTests").getAsInt();
186     int cntTests = 0;
187     int errors = 0;
188     for (JsonElement g : test.getAsJsonArray("testGroups")) {
189       JsonObject group = g.getAsJsonObject();
190       int tagSize = group.get("tagSize").getAsInt();
191       for (JsonElement t : group.getAsJsonArray("tests")) {
192         cntTests++;
193         JsonObject testcase = t.getAsJsonObject();
194         int tcid = testcase.get("tcId").getAsInt();
195         String tc = "tcId: " + tcid + " " + testcase.get("comment").getAsString();
196         byte[] key = getBytes(testcase, "key");
197         byte[] iv = getBytes(testcase, "iv");
198         byte[] msg = getBytes(testcase, "msg");
199         byte[] aad = getBytes(testcase, "aad");
200         byte[] ciphertext = join(getBytes(testcase, "ct"), getBytes(testcase, "tag"));
201         // Result is one of "valid", "invalid", "acceptable".
202         // "valid" are test vectors with matching plaintext, ciphertext and tag.
203         // "invalid" are test vectors with invalid parameters or invalid ciphertext and tag.
204         // "acceptable" are test vectors with weak parameters or legacy formats.
205         String result = testcase.get("result").getAsString();
206 
207         // Test encryption
208         Cipher cipher;
209         try {
210           cipher = getInitializedCipher(algorithm, Cipher.ENCRYPT_MODE, key, iv, tagSize,
211                     isStrongBox);
212         } catch (GeneralSecurityException ex) {
213           // Some libraries restrict key size, iv size and tag size.
214           // Because of the initialization of the cipher might fail.
215           continue;
216         }
217         try {
218           cipher.updateAAD(aad);
219           byte[] encrypted = cipher.doFinal(msg);
220           boolean eq = arrayEquals(ciphertext, encrypted);
221           if (result.equals("invalid")) {
222             if (eq) {
223               // Some test vectors use invalid parameters that should be rejected.
224               // E.g. an implementation must never encrypt using AES-GCM with an IV of length 0,
225               // since this leaks the authentication key.
226               errors++;
227             }
228           } else {
229             if (!eq) {
230               errors++;
231             }
232           }
233         } catch (GeneralSecurityException ex) {
234           if (result.equals("valid")) {
235             errors++;
236           }
237         }
238 
239         // Test decryption
240         Cipher decCipher;
241         try {
242           decCipher = getInitializedCipher(algorithm, Cipher.DECRYPT_MODE, key, iv, tagSize,
243                                           isStrongBox);
244         } catch (GeneralSecurityException ex) {
245           errors++;
246           continue;
247         }
248         try {
249           decCipher.updateAAD(aad);
250           byte[] decrypted = decCipher.doFinal(ciphertext);
251           boolean eq = arrayEquals(decrypted, msg);
252           if (result.equals("invalid")) {
253             errors++;
254           } else {
255             if (!eq) {
256               errors++;
257             }
258           }
259         } catch (GeneralSecurityException ex) {
260           if (result.equals("valid")) {
261             errors++;
262           }
263         }
264       }
265     }
266     assertEquals(0, errors);
267     assertEquals(numTests, cntTests);
268   }
269 
270   @Test
testAesGcm()271   public void testAesGcm() throws Exception {
272     testAead("aes_gcm_test.json", "AES/GCM/NoPadding");
273   }
274   @Test
testAesGcm_StrongBox()275   public void testAesGcm_StrongBox() throws Exception {
276     KeyStoreUtil.assumeStrongBox();
277     testAead("aes_gcm_test.json", "AES/GCM/NoPadding", true);
278   }
279 
280   @Test
281   @Ignore // Ignored due to AES/EAX algorithm not supported in AndroidKeyStore.
testAesEax()282   public void testAesEax() throws Exception {
283     testAead("aes_eax_test.json", "AES/EAX/NoPadding");
284   }
285 
286   @Test
287   @Ignore // Ignored due to AES/CCM algorithm not supported in AndroidKeyStore.
testAesCcm()288   public void testAesCcm() throws Exception {
289     testAead("aes_ccm_test.json", "AES/CCM/NoPadding");
290   }
291 
292   /**
293    * Tests ChaCha20-Poly1305 defined in RFC 7539.
294    *
295    * <p>The algorithm name for ChaCha20-Poly1305 is not well defined:
296    * jdk11 uses "ChaCha20-Poly1305".
297    * ConsCrypt uses "ChaCha20/Poly1305/NoPadding".
298    * These two implementations implement RFC 7539.
299    *
300    * <p>BouncyCastle has a cipher "ChaCha7539". This implementation
301    * only implements ChaCha20 with a 12 byte IV. An implementation
302    * of RFC 7539 is the class JceChaCha20Poly1305. It is unclear
303    * whether this class can be accessed through the JCA interface.
304    */
305   @Test
306   @Ignore // Ignored due to ChaCha20 algorithm not supported in AndroidKeyStore.
testChaCha20Poly1305()307   public void testChaCha20Poly1305() throws Exception {
308     // A list of potential algorithm names for ChaCha20-Poly1305.
309     String[] algorithms =
310         new String[]{"ChaCha20-Poly1305",
311                      "ChaCha20/Poly1305/NoPadding"};
312     for (String name : algorithms) {
313       try {
314         Cipher.getInstance(name, EXPECTED_CRYPTO_PROVIDER_NAME);
315       } catch (NoSuchAlgorithmException ex) {
316         continue;
317       }
318       testAead("chacha20_poly1305_test.json", name);
319       return;
320     }
321   }
322 }
323