• 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 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 org.junit.Test;
32 import org.junit.runner.RunWith;
33 import org.junit.runners.JUnit4;
34 
35 /**
36  * CipherInputStream tests
37  *
38  * <p>CipherInputStream is a class that is basically unsuitable for authenticated encryption
39  * and hence should be avoided whenever possible. The class is unsuitable, because the interface
40  * does not provide a method to tell the caller when decryption failed. I.e. the specification
41  * now explicitly claims that it catches exceptions thrown by the Cipher class such as
42  * BadPaddingException and that it does not rethrow them.
43  * http://www.oracle.com/technetwork/java/javase/8u171-relnotes-4308888.html
44  *
45  * <p>The Jdk implementation still has the property that no unauthenticated plaintext is released.
46  * In the case of an authentication failure the implementation simply returns an empty plaintext.
47  * This allows a trivial attack where the attacker substitutes any message with an empty message.
48  *
49  * <p>The tests in this class have been adapted to this unfortunate situation. testEmptyPlaintext
50  * checks whether corrupting the tag of an empty message is detected. This test currently fails.
51  * All other tests run under the assumption that returning an empty plaintext is acceptable
52  * behaviour, so that the tests are able to catch additional problems.
53  */
54 @RunWith(JUnit4.class)
55 public class CipherInputStreamTest {
56   static final SecureRandom rand = new SecureRandom();
57 
randomBytes(int size)58   static byte[] randomBytes(int size) {
59     byte[] bytes = new byte[size];
60     rand.nextBytes(bytes);
61     return bytes;
62   }
63 
randomKey(String algorithm, int keySizeInBytes)64   static SecretKeySpec randomKey(String algorithm, int keySizeInBytes) {
65     return new SecretKeySpec(randomBytes(keySizeInBytes), "AES");
66   }
67 
randomParameters( String algorithm, int ivSizeInBytes, int tagSizeInBytes)68   static AlgorithmParameterSpec randomParameters(
69       String algorithm, int ivSizeInBytes, int tagSizeInBytes) {
70     if ("AES/GCM/NoPadding".equals(algorithm) || "AES/EAX/NoPadding".equals(algorithm)) {
71       return new GCMParameterSpec(8 * tagSizeInBytes, randomBytes(ivSizeInBytes));
72     }
73     return null;
74   }
75 
76   /** Test vectors */
77   public static class TestVector {
78     public String algorithm;
79     public SecretKeySpec key;
80     public AlgorithmParameterSpec params;
81     public byte[] pt;
82     public byte[] aad;
83     public byte[] ct;
84 
85     @SuppressWarnings("InsecureCryptoUsage")
TestVector( String algorithm, int keySize, int ivSize, int tagSize, int ptSize, int aadSize)86     public TestVector(
87         String algorithm, int keySize, int ivSize, int tagSize, int ptSize, int aadSize)
88         throws Exception {
89       this.algorithm = algorithm;
90       this.key = randomKey(algorithm, keySize);
91       this.params = randomParameters(algorithm, ivSize, tagSize);
92       this.pt = randomBytes(ptSize);
93       this.aad = randomBytes(aadSize);
94       Cipher cipher = Cipher.getInstance(algorithm);
95       cipher.init(Cipher.ENCRYPT_MODE, this.key, this.params);
96       cipher.updateAAD(aad);
97       this.ct = cipher.doFinal(pt);
98     }
99   }
100 
getTestVectors( String algorithm, int[] keySizes, int[] ivSizes, int[] tagSizes, int[] ptSizes, int[] aadSizes)101   Iterable<TestVector> getTestVectors(
102       String algorithm,
103       int[] keySizes,
104       int[] ivSizes,
105       int[] tagSizes,
106       int[] ptSizes,
107       int[] aadSizes)
108       throws Exception {
109     ArrayList<TestVector> result = new ArrayList<TestVector>();
110     for (int keySize : keySizes) {
111       for (int ivSize : ivSizes) {
112         for (int tagSize : tagSizes) {
113           for (int ptSize : ptSizes) {
114             for (int aadSize : aadSizes) {
115               result.add(new TestVector(algorithm, keySize, ivSize, tagSize, ptSize, aadSize));
116             }
117           }
118         }
119       }
120     }
121     return result;
122   }
123 
124   @SuppressWarnings("InsecureCryptoUsage")
testEncrypt(Iterable<TestVector> tests)125   public void testEncrypt(Iterable<TestVector> tests) throws Exception {
126     for (TestVector t : tests) {
127       Cipher cipher = Cipher.getInstance(t.algorithm);
128       cipher.init(Cipher.ENCRYPT_MODE, t.key, t.params);
129       cipher.updateAAD(t.aad);
130       InputStream is = new ByteArrayInputStream(t.pt);
131       CipherInputStream cis = new CipherInputStream(is, cipher);
132       byte[] result = new byte[t.ct.length];
133       int totalLength = 0;
134       int length = 0;
135       do {
136         length = cis.read(result, totalLength, result.length - totalLength);
137         if (length > 0) {
138           totalLength += length;
139         }
140       } while (length >= 0 && totalLength != result.length);
141       assertEquals(-1, cis.read());
142       assertEquals(TestUtil.bytesToHex(t.ct), TestUtil.bytesToHex(result));
143       cis.close();
144     }
145   }
146 
147   /** JDK-8016249: CipherInputStream in decrypt mode fails on close with AEAD ciphers */
148   @SuppressWarnings("InsecureCryptoUsage")
testDecrypt(Iterable<TestVector> tests)149   public void testDecrypt(Iterable<TestVector> tests) throws Exception {
150     for (TestVector t : tests) {
151       Cipher cipher = Cipher.getInstance(t.algorithm);
152       cipher.init(Cipher.DECRYPT_MODE, t.key, t.params);
153       cipher.updateAAD(t.aad);
154       InputStream is = new ByteArrayInputStream(t.ct);
155       CipherInputStream cis = new CipherInputStream(is, cipher);
156       byte[] result = new byte[t.pt.length];
157       int totalLength = 0;
158       int length = 0;
159       do {
160         length = cis.read(result, totalLength, result.length - totalLength);
161         if (length > 0) {
162           totalLength += length;
163         }
164       } while (length >= 0 && totalLength != result.length);
165       assertEquals(-1, cis.read());
166       cis.close();
167       assertEquals(TestUtil.bytesToHex(t.pt), TestUtil.bytesToHex(result));
168     }
169   }
170 
171   /**
172    * JDK-8016171 : CipherInputStream masks ciphertext tampering with AEAD ciphers in decrypt mode
173    * Further description of the bug is here:
174    * https://blog.heckel.xyz/2014/03/01/cipherinputstream-for-aead-modes-is-broken-in-jdk7-gcm/
175    * BouncyCastle claims that this bug is fixed in version 1.51. However, the test below still fails
176    * with BouncyCastle v 1.52. A possible explanation is that BouncyCastle has its own
177    * implemenatation of CipherInputStream (org.bouncycastle.crypto.io.CipherInputStream).
178    *
179    * @param tests an iterable with valid test vectors, that will be corrupted for the test
180    * @param acceptEmptyPlaintext determines whether an empty plaintext instead of an exception
181    *     is acceptable.
182    */
183   @SuppressWarnings("InsecureCryptoUsage")
testCorruptDecrypt(Iterable<TestVector> tests, boolean acceptEmptyPlaintext)184   public void testCorruptDecrypt(Iterable<TestVector> tests, boolean acceptEmptyPlaintext)
185       throws Exception {
186     for (TestVector t : tests) {
187       Cipher cipher = Cipher.getInstance(t.algorithm);
188       cipher.init(Cipher.DECRYPT_MODE, t.key, t.params);
189       cipher.updateAAD(t.aad);
190       byte[] ct = Arrays.copyOf(t.ct, t.ct.length);
191       ct[ct.length - 1] ^= (byte) 1;
192       InputStream is = new ByteArrayInputStream(ct);
193       CipherInputStream cis = new CipherInputStream(is, cipher);
194       try {
195         byte[] result = new byte[t.pt.length];
196         int totalLength = 0;
197         int length = 0;
198         do {
199           length = cis.read(result, totalLength, result.length - totalLength);
200           if (length > 0) {
201             totalLength += length;
202           }
203         } while (length >= 0 && totalLength != result.length);
204         cis.close();
205         if (result.length > 0) {
206           fail(
207               "this should fail; decrypted:"
208                   + TestUtil.bytesToHex(result)
209                   + " pt: "
210                   + TestUtil.bytesToHex(t.pt));
211         } else if (result.length == 0 && !acceptEmptyPlaintext) {
212           fail("Corrupted ciphertext returns empty plaintext");
213         }
214       } catch (IOException ex) {
215         // expected
216       }
217     }
218   }
219 
220   @Test
testAesGcm()221   public void testAesGcm() throws Exception {
222     final int[] keySizes = {16, 32};
223     final int[] ivSizes = {12};
224     final int[] tagSizes = {12, 16};
225     final int[] ptSizes = {0, 8, 16, 65, 8100};
226     final int[] aadSizes = {0, 8, 24};
227     Iterable<TestVector> v =
228         getTestVectors("AES/GCM/NoPadding", keySizes, ivSizes, tagSizes, ptSizes, aadSizes);
229     testEncrypt(v);
230     testDecrypt(v);
231   }
232 
233   @Test
testCorruptAesGcm()234   public void testCorruptAesGcm() throws Exception {
235     final int[] keySizes = {16, 32};
236     final int[] ivSizes = {12};
237     final int[] tagSizes = {12, 16};
238     final int[] ptSizes = {8, 16, 65, 8100};
239     final int[] aadSizes = {0, 8, 24};
240     Iterable<TestVector> v =
241         getTestVectors("AES/GCM/NoPadding", keySizes, ivSizes, tagSizes, ptSizes, aadSizes);
242     boolean acceptEmptyPlaintext = true;
243     testCorruptDecrypt(v, acceptEmptyPlaintext);
244   }
245 
246   /**
247    * Tests the behaviour for corrupt plaintext more strictly than in the tests above.
248    * This test does not accept that an implementation returns an empty plaintext when the
249    * ciphertext has been corrupted.
250    */
251   @Test
testEmptyPlaintext()252   public void testEmptyPlaintext() throws Exception {
253     final int[] keySizes = {16, 32};
254     final int[] ivSizes = {12};
255     final int[] tagSizes = {12, 16};
256     final int[] ptSizes = {0};
257     final int[] aadSizes = {0, 8, 24};
258     Iterable<TestVector> v =
259         getTestVectors("AES/GCM/NoPadding", keySizes, ivSizes, tagSizes, ptSizes, aadSizes);
260     boolean acceptEmptyPlaintext = false;
261     testCorruptDecrypt(v, acceptEmptyPlaintext);
262   }
263 
264   /** Tests CipherOutputStream with AES-EAX if this algorithm is supported by the provider. */
265   @Test
testAesEax()266   public void testAesEax() throws Exception {
267     final String algorithm = "AES/EAX/NoPadding";
268     final int[] keySizes = {16, 32};
269     final int[] ivSizes = {12, 16};
270     final int[] tagSizes = {12, 16};
271     final int[] ptSizes = {0, 8, 16, 65, 8100};
272     final int[] aadSizes = {0, 8, 24};
273     try {
274       Cipher.getInstance(algorithm);
275     } catch (NoSuchAlgorithmException ex) {
276       System.out.println("Skipping testAesEax");
277       return;
278     }
279     Iterable<TestVector> v =
280         getTestVectors(algorithm, keySizes, ivSizes, tagSizes, ptSizes, aadSizes);
281     testEncrypt(v);
282     testDecrypt(v);
283     boolean acceptEmptyPlaintext = true;
284     testCorruptDecrypt(v, acceptEmptyPlaintext);
285   }
286 }
287