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