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.GeneralSecurityException; 23 import java.security.NoSuchAlgorithmException; 24 import java.util.Set; 25 import java.util.TreeSet; 26 import javax.crypto.Cipher; 27 import javax.crypto.SecretKey; 28 import javax.crypto.spec.IvParameterSpec; 29 import javax.crypto.spec.SecretKeySpec; 30 import org.junit.After; 31 import org.junit.Test; 32 import android.security.keystore.KeyProtection; 33 import android.security.keystore.KeyProperties; 34 import java.security.KeyStore; 35 import android.keystore.cts.util.KeyStoreUtil; 36 37 /** 38 * This test uses test vectors in JSON format to test symmetric ciphers. 39 * 40 * <p>Ciphers tested in this class are unauthenticated ciphers (i.e. don't have additional data) and 41 * are randomized using an initialization vector as long as the JSON test vectors are represented 42 * with the type "IndCpaTest". 43 */ 44 public class JsonCipherTest { 45 private static final String EXPECTED_PROVIDER_NAME = TestUtil.EXPECTED_CRYPTO_OP_PROVIDER_NAME; 46 private static final String KEY_ALIAS_1 = "Key1"; 47 48 @After tearDown()49 public void tearDown() throws Exception { 50 KeyStoreUtil.cleanUpKeyStore(); 51 } 52 53 /** Convenience method to get a byte array from a JsonObject. */ getBytes(JsonObject object, String name)54 protected static byte[] getBytes(JsonObject object, String name) throws Exception { 55 return JsonUtil.asByteArray(object.get(name)); 56 } 57 arrayEquals(byte[] a, byte[] b)58 protected static boolean arrayEquals(byte[] a, byte[] b) { 59 if (a.length != b.length) { 60 return false; 61 } 62 byte res = 0; 63 for (int i = 0; i < a.length; i++) { 64 res |= (byte) (a[i] ^ b[i]); 65 } 66 return res == 0; 67 } 68 69 /** 70 * Initialize a Cipher instance. 71 * 72 * @param cipher an instance of a symmetric cipher that will be initialized. 73 * @param algorithm the name of the algorithm used (e.g. 'AES') 74 * @param opmode either Cipher.ENCRYPT_MODE or Cipher.DECRYPT_MODE 75 * @param key raw key bytes 76 * @param iv the initialisation vector 77 * @param isStrongBox whether key should store in StrongBox or not 78 */ initCipher(Cipher cipher, String algorithm, int opmode, byte[] key, byte[] iv, boolean isStrongBox)79 protected static void initCipher(Cipher cipher, String algorithm, int opmode, 80 byte[] key, byte[] iv, boolean isStrongBox) throws Exception { 81 SecretKeySpec keySpec = null; 82 if (algorithm.startsWith("AES/")) { 83 keySpec = new SecretKeySpec(key, "AES"); 84 } else { 85 fail("Unsupported algorithm:" + algorithm); 86 } 87 IvParameterSpec ivSpec = new IvParameterSpec(iv); 88 KeyStore keyStore = KeyStoreUtil.saveSecretKeyToKeystore(KEY_ALIAS_1, keySpec, 89 new KeyProtection.Builder(KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) 90 .setBlockModes(KeyProperties.BLOCK_MODE_CBC) 91 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) 92 .setRandomizedEncryptionRequired(false) 93 .setIsStrongBoxBacked(isStrongBox) 94 .build()); 95 // Key imported, obtain a reference to it. 96 SecretKey keyStoreKey = (SecretKey) keyStore.getKey(KEY_ALIAS_1, null); 97 98 cipher.init(opmode, keyStoreKey, ivSpec); 99 } 100 101 102 /** Example format for test vectors 103 * { 104 * "algorithm" : "AES-CBC-PKCS5", 105 * "generatorVersion" : "0.2.1", 106 * "numberOfTests" : 183, 107 * "header" : [ 108 * ], 109 * "testGroups" : [ 110 * { 111 * "ivSize" : 128, 112 * "keySize" : 128, 113 * "type" : "IndCpaTest", 114 * "tests" : [ 115 * { 116 * "tcId" : 1, 117 * "comment" : "empty message", 118 * "key" : "e34f15c7bd819930fe9d66e0c166e61c", 119 * "iv" : "da9520f7d3520277035173299388bee2", 120 * "msg" : "", 121 * "ct" : "b10ab60153276941361000414aed0a9d", 122 * "result" : "valid" 123 * }, 124 * ... 125 **/ 126 // This is a false positive, since errorprone cannot track values passed into a method. 127 @SuppressWarnings("InsecureCryptoUsage") testCipher(String filename, String algorithm)128 public void testCipher(String filename, String algorithm) throws Exception { 129 testCipher(filename, algorithm, false); 130 } 131 @SuppressWarnings("InsecureCryptoUsage") testCipher(String filename, String algorithm, boolean isStrongBox)132 public void testCipher(String filename, String algorithm, boolean isStrongBox) throws Exception { 133 // Testing with old test vectors may a reason for a test failure. 134 // Version number have the format major.minor[status]. 135 // Versions before 1.0 are experimental and use formats that are expected to change. 136 // Versions after 1.0 change the major number if the format changes and change 137 // the minor number if only the test vectors (but not the format) changes. 138 // Versions meant for distribution have no status. 139 final String expectedVersion = "0.4"; 140 JsonObject test = JsonUtil.getTestVectors(this.getClass(), filename); 141 Set<String> exceptions = new TreeSet<String>(); 142 String generatorVersion = test.get("generatorVersion").getAsString(); 143 assertFalse( 144 algorithm 145 + ": expecting test vectors with version " 146 + expectedVersion 147 + " found vectors with version " 148 + generatorVersion, 149 generatorVersion.equals(expectedVersion)); 150 int numTests = test.get("numberOfTests").getAsInt(); 151 int cntTests = 0; 152 int errors = 0; 153 Cipher cipher = Cipher.getInstance(algorithm, EXPECTED_PROVIDER_NAME); 154 for (JsonElement g : test.getAsJsonArray("testGroups")) { 155 JsonObject group = g.getAsJsonObject(); 156 for (JsonElement t : group.getAsJsonArray("tests")) { 157 cntTests++; 158 JsonObject testcase = t.getAsJsonObject(); 159 int tcid = testcase.get("tcId").getAsInt(); 160 String tc = "tcId: " + tcid + " " + testcase.get("comment").getAsString(); 161 byte[] key = getBytes(testcase, "key"); 162 byte[] iv = getBytes(testcase, "iv"); 163 byte[] msg = getBytes(testcase, "msg"); 164 byte[] ciphertext = getBytes(testcase, "ct"); 165 // Result is one of "valid", "invalid", "acceptable". 166 // "valid" are test vectors with matching plaintext, ciphertext and tag. 167 // "invalid" are test vectors with invalid parameters or invalid ciphertext and tag. 168 // "acceptable" are test vectors with weak parameters or legacy formats. 169 String result = testcase.get("result").getAsString(); 170 171 // Test encryption 172 try { 173 initCipher(cipher, algorithm, Cipher.ENCRYPT_MODE, key, iv, isStrongBox); 174 } catch (GeneralSecurityException ex) { 175 // Some libraries restrict key size, iv size and tag size. 176 // Because of the initialization of the cipher might fail. 177 continue; 178 } 179 try { 180 byte[] encrypted = cipher.doFinal(msg); 181 boolean eq = arrayEquals(ciphertext, encrypted); 182 if (result.equals("invalid")) { 183 if (eq) { 184 // Some test vectors use invalid parameters that should be rejected. 185 errors++; 186 } 187 } else { 188 if (!eq) { 189 errors++; 190 } 191 } 192 } catch (GeneralSecurityException ex) { 193 if (result.equals("valid")) { 194 errors++; 195 } 196 } 197 198 // Test decryption 199 // The algorithms tested in this class are typically malleable. Hence, it is in possible 200 // that modifying ciphertext randomly results in some other valid ciphertext. 201 // However, all the test vectors in Wycheproof are constructed such that they have 202 // invalid padding. If this changes then the test below is too strict. 203 try { 204 initCipher(cipher, algorithm, Cipher.DECRYPT_MODE, key, iv, isStrongBox); 205 } catch (GeneralSecurityException ex) { 206 errors++; 207 continue; 208 } 209 try { 210 byte[] decrypted = cipher.doFinal(ciphertext); 211 boolean eq = arrayEquals(decrypted, msg); 212 if (result.equals("invalid")) { 213 errors++; 214 } else { 215 if (!eq) { 216 errors++; 217 } 218 } 219 } catch (GeneralSecurityException ex) { 220 exceptions.add(ex.getMessage() == null ? "" : ex.getMessage()); 221 if (result.equals("valid")) { 222 errors++; 223 } 224 } 225 } 226 } 227 assertEquals(0, errors); 228 assertEquals(numTests, cntTests); 229 // Generally it is preferable if trying to decrypt ciphertexts with incorrect paddings 230 // does not leak information about invalid paddings through exceptions. 231 // Such information could simplify padding attacks. Ideally, providers should not include 232 // any distinguishing features in the exception. Hence, we expect just one exception here. 233 // 234 // Seeing distinguishable exception, doesn't necessarily mean that protocols using 235 // AES/CBC/PKCS5Padding with the tested provider are vulnerable to attacks. Rather it means 236 // that the provider might simplify attacks if the protocol is using AES/CBC/PKCS5Padding 237 // incorrectly. 238 StringBuilder sb = new StringBuilder(); 239 sb.append("Exceptions: "); 240 for (String ex : exceptions) { 241 sb.append(ex.toString() + " "); 242 } 243 assertEquals(sb.toString(), 1, exceptions.size()); 244 } 245 246 @Test testAesCbcPkcs7()247 public void testAesCbcPkcs7() throws Exception { 248 // AndroidKeyStore only suuport AES/CBC/PKCS7Padding algorithm, 249 // so it is used instead of PKCS5Padding 250 testCipher("aes_cbc_pkcs5_test.json", "AES/CBC/PKCS7Padding"); 251 } 252 @Test testAesCbcPkcs7_StrongBox()253 public void testAesCbcPkcs7_StrongBox() throws Exception { 254 // AndroidKeyStore only suuport AES/CBC/PKCS7Padding algorithm, 255 // so it is used instead of PKCS5Padding 256 KeyStoreUtil.assumeStrongBox(); 257 testCipher("aes_cbc_pkcs5_test.json", "AES/CBC/PKCS7Padding", true); 258 } 259 } 260