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