• 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.ByteArrayOutputStream;
20 import java.io.IOException;
21 import java.security.NoSuchAlgorithmException;
22 import java.security.SecureRandom;
23 import java.security.spec.AlgorithmParameterSpec;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import javax.crypto.Cipher;
27 import javax.crypto.CipherOutputStream;
28 import javax.crypto.spec.GCMParameterSpec;
29 import javax.crypto.spec.SecretKeySpec;
30 import org.junit.Test;
31 import org.junit.runner.RunWith;
32 import org.junit.runners.JUnit4;
33 
34 /**
35  * CipherOutputStream tests
36  *
37  * <p>CipherOutputStream is a class that is basically unsuitable for authenticated encryption
38  * and hence should be avoided whenever possible. The class is unsuitable, because the interface
39  * does not provide a method to tell the caller when decryption failed. I.e. the specification
40  * now explicitly claims that it catches exceptions thrown by the Cipher class such as
41  * BadPaddingException and that it does not rethrow them.
42  * http://www.oracle.com/technetwork/java/javase/8u171-relnotes-4308888.html
43  *
44  * <p>The Jdk implementation has the property that no unauthenticated plaintext is released.
45  * In the case of an authentication failure the implementation simply returns an empty plaintext.
46  * This allows a trivial attack where the attacker substitutes any message with an empty message.
47  *
48  * <p>The tests in this class have been adapted to this unfortunate situation. testEmptyPlaintext
49  * checks whether corrupting the tag of an empty message is detected. This test currently fails.
50  * All other tests run under the assumption that returning an empty plaintext is acceptable
51  * behaviour, so that the tests are able to catch additional problems.
52  */
53 
54 @RunWith(JUnit4.class)
55 public class CipherOutputStreamTest {
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   @SuppressWarnings("InsecureCryptoUsage")
78   public static class TestVector {
79     public String algorithm;
80     public SecretKeySpec key;
81     public AlgorithmParameterSpec params;
82     public byte[] pt;
83     public byte[] aad;
84     public byte[] ct;
85 
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       ByteArrayOutputStream os = new ByteArrayOutputStream();
131       CipherOutputStream cos = new CipherOutputStream(os, cipher);
132       cos.write(t.pt);
133       cos.close();
134       assertEquals(TestUtil.bytesToHex(t.ct), TestUtil.bytesToHex(os.toByteArray()));
135     }
136   }
137 
138   @SuppressWarnings("InsecureCryptoUsage")
testDecrypt(Iterable<TestVector> tests)139   public void testDecrypt(Iterable<TestVector> tests) throws Exception {
140     for (TestVector t : tests) {
141       Cipher cipher = Cipher.getInstance(t.algorithm);
142       cipher.init(Cipher.DECRYPT_MODE, t.key, t.params);
143       cipher.updateAAD(t.aad);
144       ByteArrayOutputStream os = new ByteArrayOutputStream();
145       CipherOutputStream cos = new CipherOutputStream(os, cipher);
146       cos.write(t.ct);
147       cos.close();
148       assertEquals(TestUtil.bytesToHex(t.pt), TestUtil.bytesToHex(os.toByteArray()));
149     }
150   }
151 
152   /**
153    * Tests decryption of corrupted ciphertext. The test may accept empty plaintext as valid
154    * result because of the problem with CipherOutputStream described in the header of this file.
155    * @param tests an iterable with valid test vectors, that will be corrupted for the test
156    * @param acceptEmptyPlaintext determines whether an empty plaintext instead of an exception
157    *     is acceptable.
158    */
159   @SuppressWarnings("InsecureCryptoUsage")
testCorruptDecrypt(Iterable<TestVector> tests, boolean acceptEmptyPlaintext)160   public void testCorruptDecrypt(Iterable<TestVector> tests, boolean acceptEmptyPlaintext)
161       throws Exception {
162     for (TestVector t : tests) {
163       Cipher cipher = Cipher.getInstance(t.algorithm);
164       cipher.init(Cipher.DECRYPT_MODE, t.key, t.params);
165       cipher.updateAAD(t.aad);
166       byte[] ct = Arrays.copyOf(t.ct, t.ct.length);
167       ct[ct.length - 1] ^= (byte) 1;
168       ByteArrayOutputStream os = new ByteArrayOutputStream();
169       CipherOutputStream cos = new CipherOutputStream(os, cipher);
170       cos.write(ct);
171       try {
172         // cos.close() should call cipher.doFinal().
173         cos.close();
174         byte[] decrypted = os.toByteArray();
175         // Unfortunately Oracle thinks that returning an empty array is valid behaviour.
176         // We accept empty results here, but flag them in the next test, so that we can distinguish
177         // between beheviour considered acceptable by Oracle and more serious flaws.
178         if (decrypted.length > 0) {
179           fail(
180               "this should fail; decrypted:"
181                   + TestUtil.bytesToHex(decrypted)
182                   + " pt: "
183                   + TestUtil.bytesToHex(t.pt));
184         } else if (decrypted.length == 0 && !acceptEmptyPlaintext) {
185           fail("Corrupted ciphertext returns empty plaintext");
186         }
187       } catch (IOException ex) {
188         // expected
189       }
190     }
191   }
192 
193   @Test
testAesGcm()194   public void testAesGcm() throws Exception {
195     final int[] keySizes = {16, 32};
196     final int[] ivSizes = {12};
197     final int[] tagSizes = {12, 16};
198     final int[] ptSizes = {8, 16, 65, 8100};
199     final int[] aadSizes = {0, 8, 24};
200     Iterable<TestVector> v =
201         getTestVectors("AES/GCM/NoPadding", keySizes, ivSizes, tagSizes, ptSizes, aadSizes);
202     testEncrypt(v);
203     testDecrypt(v);
204     boolean acceptEmptyPlaintext = true;
205     testCorruptDecrypt(v, acceptEmptyPlaintext);
206   }
207 
208   /**
209    * Tests the behaviour for corrupt plaintext more strictly than in the tests above.
210    * This test does not accept that an implementation returns an empty plaintext when the
211    * ciphertext has been corrupted.
212    */
213   @Test
testEmptyPlaintext()214   public void testEmptyPlaintext() throws Exception {
215     final int[] keySizes = {16, 32};
216     final int[] ivSizes = {12};
217     final int[] tagSizes = {12, 16};
218     final int[] ptSizes = {0};
219     final int[] aadSizes = {0, 8, 24};
220     Iterable<TestVector> v =
221         getTestVectors("AES/GCM/NoPadding", keySizes, ivSizes, tagSizes, ptSizes, aadSizes);
222     testEncrypt(v);
223     testDecrypt(v);
224     boolean acceptEmptyPlaintext = false;
225     testCorruptDecrypt(v, acceptEmptyPlaintext);
226   }
227 
228   /** Tests CipherOutputStream with AES-EAX if AES-EAS is supported by the provider. */
229   @SuppressWarnings("InsecureCryptoUsage")
230   @Test
testAesEax()231   public void testAesEax() throws Exception {
232     final String algorithm = "AES/EAX/NoPadding";
233     final int[] keySizes = {16, 32};
234     final int[] ivSizes = {12, 16};
235     final int[] tagSizes = {12, 16};
236     final int[] ptSizes = {8, 16, 65, 8100};
237     final int[] aadSizes = {0, 8, 24};
238     try {
239       Cipher.getInstance(algorithm);
240     } catch (NoSuchAlgorithmException ex) {
241       System.out.println("Skipping testAesEax");
242       return;
243     }
244     Iterable<TestVector> v =
245         getTestVectors(algorithm, keySizes, ivSizes, tagSizes, ptSizes, aadSizes);
246     testEncrypt(v);
247     testDecrypt(v);
248     boolean acceptEmptyPlaintext = true;
249     testCorruptDecrypt(v, acceptEmptyPlaintext);
250   }
251 }
252