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