1 /**
2  * @license
3  * Copyright 2016 Google Inc. All rights reserved.
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *   http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.security.wycheproof;
18 
19 import java.io.ByteArrayInputStream;
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.security.NoSuchAlgorithmException;
23 import java.security.SecureRandom;
24 import java.security.spec.AlgorithmParameterSpec;
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import javax.crypto.Cipher;
28 import javax.crypto.CipherInputStream;
29 import javax.crypto.spec.GCMParameterSpec;
30 import javax.crypto.spec.SecretKeySpec;
31 import junit.framework.TestCase;
32 
33 /** CipherInputStream tests */
34 public class CipherInputStreamTest extends TestCase {
35   static final SecureRandom rand = new SecureRandom();
36 
randomBytes(int size)37   static byte[] randomBytes(int size) {
38     byte[] bytes = new byte[size];
39     rand.nextBytes(bytes);
40     return bytes;
41   }
42 
randomKey(String algorithm, int keySizeInBytes)43   static SecretKeySpec randomKey(String algorithm, int keySizeInBytes) {
44     return new SecretKeySpec(randomBytes(keySizeInBytes), "AES");
45   }
46 
randomParameters( String algorithm, int ivSizeInBytes, int tagSizeInBytes)47   static AlgorithmParameterSpec randomParameters(
48       String algorithm, int ivSizeInBytes, int tagSizeInBytes) {
49     if ("AES/GCM/NoPadding".equals(algorithm) || "AES/EAX/NoPadding".equals(algorithm)) {
50       return new GCMParameterSpec(8 * tagSizeInBytes, randomBytes(ivSizeInBytes));
51     }
52     return null;
53   }
54 
55   /** Test vectors */
56   public static class TestVector {
57     public String algorithm;
58     public SecretKeySpec key;
59     public AlgorithmParameterSpec params;
60     public byte[] pt;
61     public byte[] aad;
62     public byte[] ct;
63 
64     @SuppressWarnings("InsecureCryptoUsage")
TestVector( String algorithm, int keySize, int ivSize, int tagSize, int ptSize, int aadSize)65     public TestVector(
66         String algorithm, int keySize, int ivSize, int tagSize, int ptSize, int aadSize)
67         throws Exception {
68       this.algorithm = algorithm;
69       this.key = randomKey(algorithm, keySize);
70       this.params = randomParameters(algorithm, ivSize, tagSize);
71       this.pt = randomBytes(ptSize);
72       this.aad = randomBytes(aadSize);
73       Cipher cipher = Cipher.getInstance(algorithm);
74       cipher.init(Cipher.ENCRYPT_MODE, this.key, this.params);
75       cipher.updateAAD(aad);
76       this.ct = cipher.doFinal(pt);
77     }
78   }
79 
getTestVectors( String algorithm, int[] keySizes, int[] ivSizes, int[] tagSizes, int[] ptSizes, int[] aadSizes)80   Iterable<TestVector> getTestVectors(
81       String algorithm,
82       int[] keySizes,
83       int[] ivSizes,
84       int[] tagSizes,
85       int[] ptSizes,
86       int[] aadSizes)
87       throws Exception {
88     ArrayList<TestVector> result = new ArrayList<TestVector>();
89     for (int keySize : keySizes) {
90       for (int ivSize : ivSizes) {
91         for (int tagSize : tagSizes) {
92           for (int ptSize : ptSizes) {
93             for (int aadSize : aadSizes) {
94               result.add(new TestVector(algorithm, keySize, ivSize, tagSize, ptSize, aadSize));
95             }
96           }
97         }
98       }
99     }
100     return result;
101   }
102 
103   @SuppressWarnings("InsecureCryptoUsage")
testEncrypt(Iterable<TestVector> tests)104   public void testEncrypt(Iterable<TestVector> tests) throws Exception {
105     for (TestVector t : tests) {
106       Cipher cipher = Cipher.getInstance(t.algorithm);
107       cipher.init(Cipher.ENCRYPT_MODE, t.key, t.params);
108       cipher.updateAAD(t.aad);
109       InputStream is = new ByteArrayInputStream(t.pt);
110       CipherInputStream cis = new CipherInputStream(is, cipher);
111       byte[] result = new byte[t.ct.length];
112       int totalLength = 0;
113       int length = 0;
114       do {
115         length = cis.read(result, totalLength, result.length - totalLength);
116         if (length > 0) {
117           totalLength += length;
118         }
119       } while (length >= 0 && totalLength != result.length);
120       assertEquals(-1, cis.read());
121       assertEquals(TestUtil.bytesToHex(t.ct), TestUtil.bytesToHex(result));
122       cis.close();
123     }
124   }
125 
126   /** JDK-8016249: CipherInputStream in decrypt mode fails on close with AEAD ciphers */
127   @SuppressWarnings("InsecureCryptoUsage")
testDecrypt(Iterable<TestVector> tests)128   public void testDecrypt(Iterable<TestVector> tests) throws Exception {
129     for (TestVector t : tests) {
130       Cipher cipher = Cipher.getInstance(t.algorithm);
131       cipher.init(Cipher.DECRYPT_MODE, t.key, t.params);
132       cipher.updateAAD(t.aad);
133       InputStream is = new ByteArrayInputStream(t.ct);
134       CipherInputStream cis = new CipherInputStream(is, cipher);
135       byte[] result = new byte[t.pt.length];
136       int totalLength = 0;
137       int length = 0;
138       do {
139         length = cis.read(result, totalLength, result.length - totalLength);
140         if (length > 0) {
141           totalLength += length;
142         }
143       } while (length >= 0 && totalLength != result.length);
144       assertEquals(-1, cis.read());
145       cis.close();
146       assertEquals(TestUtil.bytesToHex(t.pt), TestUtil.bytesToHex(result));
147     }
148   }
149 
150   /**
151    * JDK-8016171 : CipherInputStream masks ciphertext tampering with AEAD ciphers in decrypt mode
152    * Further description of the bug is here:
153    * https://blog.heckel.xyz/2014/03/01/cipherinputstream-for-aead-modes-is-broken-in-jdk7-gcm/
154    * BouncyCastle claims that this bug is fixed in version 1.51. However, the test below still fails
155    * with BouncyCastle v 1.52. A possible explanation is that BouncyCastle has its own
156    * implemenatation of CipherInputStream (org.bouncycastle.crypto.io.CipherInputStream).
157    */
158   @SuppressWarnings("InsecureCryptoUsage")
testCorruptDecrypt(Iterable<TestVector> tests)159   public void testCorruptDecrypt(Iterable<TestVector> tests) throws Exception {
160     for (TestVector t : tests) {
161       Cipher cipher = Cipher.getInstance(t.algorithm);
162       cipher.init(Cipher.DECRYPT_MODE, t.key, t.params);
163       cipher.updateAAD(t.aad);
164       byte[] ct = Arrays.copyOf(t.ct, t.ct.length);
165       ct[ct.length - 1] ^= (byte) 1;
166       InputStream is = new ByteArrayInputStream(ct);
167       CipherInputStream cis = new CipherInputStream(is, cipher);
168       try {
169         byte[] result = new byte[t.pt.length];
170         int totalLength = 0;
171         int length = 0;
172         do {
173           length = cis.read(result, totalLength, result.length - totalLength);
174           if (length > 0) {
175             totalLength += length;
176           }
177         } while (length >= 0 && totalLength != result.length);
178         cis.close();
179         if (result.length > 0) {
180           fail(
181               "this should fail; decrypted:"
182                   + TestUtil.bytesToHex(result)
183                   + " pt: "
184                   + TestUtil.bytesToHex(t.pt));
185         }
186       } catch (IOException ex) {
187         // expected
188       }
189     }
190   }
191 
192   @SuppressWarnings("InsecureCryptoUsage")
testCorruptDecryptEmpty(Iterable<TestVector> tests)193   public void testCorruptDecryptEmpty(Iterable<TestVector> tests) throws Exception {
194     for (TestVector t : tests) {
195       Cipher cipher = Cipher.getInstance(t.algorithm);
196       cipher.init(Cipher.DECRYPT_MODE, t.key, t.params);
197       cipher.updateAAD(t.aad);
198       byte[] ct = Arrays.copyOf(t.ct, t.ct.length);
199       ct[ct.length - 1] ^= (byte) 1;
200       InputStream is = new ByteArrayInputStream(ct);
201       CipherInputStream cis = new CipherInputStream(is, cipher);
202       try {
203         byte[] result = new byte[t.pt.length];
204         int totalLength = 0;
205         int length = 0;
206         do {
207           length = cis.read(result, totalLength, result.length - totalLength);
208           if (length > 0) {
209             totalLength += length;
210           }
211         } while (length >= 0 && totalLength != result.length);
212         cis.close();
213         fail("this should fail");
214       } catch (IOException ex) {
215         // expected
216       }
217     }
218   }
219 
testAesGcm()220   public void testAesGcm() throws Exception {
221     final int[] keySizes = {16, 32};
222     final int[] ivSizes = {12};
223     final int[] tagSizes = {12, 16};
224     final int[] ptSizes = {0, 8, 16, 65, 8100};
225     final int[] aadSizes = {0, 8, 24};
226     Iterable<TestVector> v =
227         getTestVectors("AES/GCM/NoPadding", keySizes, ivSizes, tagSizes, ptSizes, aadSizes);
228     testEncrypt(v);
229     testDecrypt(v);
230   }
231 
testCorruptAesGcm()232   public void testCorruptAesGcm() throws Exception {
233     final int[] keySizes = {16, 32};
234     final int[] ivSizes = {12};
235     final int[] tagSizes = {12, 16};
236     final int[] ptSizes = {8, 16, 65, 8100};
237     final int[] aadSizes = {0, 8, 24};
238     Iterable<TestVector> v =
239         getTestVectors("AES/GCM/NoPadding", keySizes, ivSizes, tagSizes, ptSizes, aadSizes);
240     testCorruptDecrypt(v);
241   }
242 
243   /**
244    * Unfortunately Oracle thinks that returning an empty array is valid behaviour for corrupt
245    * ciphertexts. Because of this we test empty plaintext separately to distinguish behaviour
246    * considered acceptable by Oracle from other behaviour.
247    */
testEmptyPlaintext()248   public void testEmptyPlaintext() throws Exception {
249     final int[] keySizes = {16, 32};
250     final int[] ivSizes = {12};
251     final int[] tagSizes = {12, 16};
252     final int[] ptSizes = {0};
253     final int[] aadSizes = {0, 8, 24};
254     Iterable<TestVector> v =
255         getTestVectors("AES/GCM/NoPadding", keySizes, ivSizes, tagSizes, ptSizes, aadSizes);
256     testCorruptDecryptEmpty(v);
257   }
258 
259   /** Tests CipherOutputStream with AES-EAX if this algorithm is supported by the provider. */
testAesEax()260   public void testAesEax() throws Exception {
261     final String algorithm = "AES/EAX/NoPadding";
262     final int[] keySizes = {16, 32};
263     final int[] ivSizes = {12, 16};
264     final int[] tagSizes = {12, 16};
265     final int[] ptSizes = {0, 8, 16, 65, 8100};
266     final int[] aadSizes = {0, 8, 24};
267     try {
268       Cipher.getInstance(algorithm);
269     } catch (NoSuchAlgorithmException ex) {
270       System.out.println("Skipping testAesEax");
271       return;
272     }
273     Iterable<TestVector> v =
274         getTestVectors(algorithm, keySizes, ivSizes, tagSizes, ptSizes, aadSizes);
275     testEncrypt(v);
276     testDecrypt(v);
277     testCorruptDecrypt(v);
278   }
279 }
280