• 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.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