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