| /** |
| * @license |
| * Copyright 2016 Google Inc. All rights reserved. |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.google.security.wycheproof; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.SecureRandom; |
| import java.security.spec.AlgorithmParameterSpec; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import javax.crypto.Cipher; |
| import javax.crypto.CipherInputStream; |
| import javax.crypto.spec.GCMParameterSpec; |
| import javax.crypto.spec.SecretKeySpec; |
| import junit.framework.TestCase; |
| |
| /** CipherInputStream tests */ |
| public class CipherInputStreamTest extends TestCase { |
| static final SecureRandom rand = new SecureRandom(); |
| |
| static byte[] randomBytes(int size) { |
| byte[] bytes = new byte[size]; |
| rand.nextBytes(bytes); |
| return bytes; |
| } |
| |
| static SecretKeySpec randomKey(String algorithm, int keySizeInBytes) { |
| return new SecretKeySpec(randomBytes(keySizeInBytes), "AES"); |
| } |
| |
| static AlgorithmParameterSpec randomParameters( |
| String algorithm, int ivSizeInBytes, int tagSizeInBytes) { |
| if ("AES/GCM/NoPadding".equals(algorithm) || "AES/EAX/NoPadding".equals(algorithm)) { |
| return new GCMParameterSpec(8 * tagSizeInBytes, randomBytes(ivSizeInBytes)); |
| } |
| return null; |
| } |
| |
| /** Test vectors */ |
| public static class TestVector { |
| public String algorithm; |
| public SecretKeySpec key; |
| public AlgorithmParameterSpec params; |
| public byte[] pt; |
| public byte[] aad; |
| public byte[] ct; |
| |
| @SuppressWarnings("InsecureCipherMode") |
| public TestVector( |
| String algorithm, int keySize, int ivSize, int tagSize, int ptSize, int aadSize) |
| throws Exception { |
| this.algorithm = algorithm; |
| this.key = randomKey(algorithm, keySize); |
| this.params = randomParameters(algorithm, ivSize, tagSize); |
| this.pt = randomBytes(ptSize); |
| this.aad = randomBytes(aadSize); |
| Cipher cipher = Cipher.getInstance(algorithm); |
| cipher.init(Cipher.ENCRYPT_MODE, this.key, this.params); |
| cipher.updateAAD(aad); |
| this.ct = cipher.doFinal(pt); |
| } |
| } |
| |
| Iterable<TestVector> getTestVectors( |
| String algorithm, |
| int[] keySizes, |
| int[] ivSizes, |
| int[] tagSizes, |
| int[] ptSizes, |
| int[] aadSizes) |
| throws Exception { |
| ArrayList<TestVector> result = new ArrayList<TestVector>(); |
| for (int keySize : keySizes) { |
| for (int ivSize : ivSizes) { |
| for (int tagSize : tagSizes) { |
| for (int ptSize : ptSizes) { |
| for (int aadSize : aadSizes) { |
| result.add(new TestVector(algorithm, keySize, ivSize, tagSize, ptSize, aadSize)); |
| } |
| } |
| } |
| } |
| } |
| return result; |
| } |
| |
| @SuppressWarnings("InsecureCipherMode") |
| public void testEncrypt(Iterable<TestVector> tests) throws Exception { |
| for (TestVector t : tests) { |
| Cipher cipher = Cipher.getInstance(t.algorithm); |
| cipher.init(Cipher.ENCRYPT_MODE, t.key, t.params); |
| cipher.updateAAD(t.aad); |
| InputStream is = new ByteArrayInputStream(t.pt); |
| CipherInputStream cis = new CipherInputStream(is, cipher); |
| byte[] result = new byte[t.ct.length]; |
| int totalLength = 0; |
| int length = 0; |
| do { |
| length = cis.read(result, totalLength, result.length - totalLength); |
| if (length > 0) { |
| totalLength += length; |
| } |
| } while (length >= 0 && totalLength != result.length); |
| assertEquals(-1, cis.read()); |
| assertEquals(TestUtil.bytesToHex(t.ct), TestUtil.bytesToHex(result)); |
| cis.close(); |
| } |
| } |
| |
| /** JDK-8016249: CipherInputStream in decrypt mode fails on close with AEAD ciphers */ |
| @SuppressWarnings("InsecureCipherMode") |
| public void testDecrypt(Iterable<TestVector> tests) throws Exception { |
| for (TestVector t : tests) { |
| Cipher cipher = Cipher.getInstance(t.algorithm); |
| cipher.init(Cipher.DECRYPT_MODE, t.key, t.params); |
| cipher.updateAAD(t.aad); |
| InputStream is = new ByteArrayInputStream(t.ct); |
| CipherInputStream cis = new CipherInputStream(is, cipher); |
| byte[] result = new byte[t.pt.length]; |
| int totalLength = 0; |
| int length = 0; |
| do { |
| length = cis.read(result, totalLength, result.length - totalLength); |
| if (length > 0) { |
| totalLength += length; |
| } |
| } while (length >= 0 && totalLength != result.length); |
| assertEquals(-1, cis.read()); |
| cis.close(); |
| assertEquals(TestUtil.bytesToHex(t.pt), TestUtil.bytesToHex(result)); |
| } |
| } |
| |
| /** |
| * JDK-8016171 : CipherInputStream masks ciphertext tampering with AEAD ciphers in decrypt mode |
| * Further description of the bug is here: |
| * https://blog.heckel.xyz/2014/03/01/cipherinputstream-for-aead-modes-is-broken-in-jdk7-gcm/ |
| * BouncyCastle claims that this bug is fixed in version 1.51. However, the test below still fails |
| * with BouncyCastle v 1.52. A possible explanation is that BouncyCastle has its own |
| * implemenatation of CipherInputStream (org.bouncycastle.crypto.io.CipherInputStream). |
| */ |
| @SuppressWarnings("InsecureCipherMode") |
| public void testCorruptDecrypt(Iterable<TestVector> tests) throws Exception { |
| for (TestVector t : tests) { |
| Cipher cipher = Cipher.getInstance(t.algorithm); |
| cipher.init(Cipher.DECRYPT_MODE, t.key, t.params); |
| cipher.updateAAD(t.aad); |
| byte[] ct = Arrays.copyOf(t.ct, t.ct.length); |
| ct[ct.length - 1] ^= (byte) 1; |
| InputStream is = new ByteArrayInputStream(ct); |
| CipherInputStream cis = new CipherInputStream(is, cipher); |
| try { |
| byte[] result = new byte[t.pt.length]; |
| int totalLength = 0; |
| int length = 0; |
| do { |
| length = cis.read(result, totalLength, result.length - totalLength); |
| if (length > 0) { |
| totalLength += length; |
| } |
| } while (length >= 0 && totalLength != result.length); |
| cis.close(); |
| if (result.length > 0) { |
| fail( |
| "this should fail; decrypted:" |
| + TestUtil.bytesToHex(result) |
| + " pt: " |
| + TestUtil.bytesToHex(t.pt)); |
| } |
| } catch (IOException ex) { |
| // expected |
| } |
| } |
| } |
| |
| @SuppressWarnings("InsecureCipherMode") |
| public void testCorruptDecryptEmpty(Iterable<TestVector> tests) throws Exception { |
| for (TestVector t : tests) { |
| Cipher cipher = Cipher.getInstance(t.algorithm); |
| cipher.init(Cipher.DECRYPT_MODE, t.key, t.params); |
| cipher.updateAAD(t.aad); |
| byte[] ct = Arrays.copyOf(t.ct, t.ct.length); |
| ct[ct.length - 1] ^= (byte) 1; |
| InputStream is = new ByteArrayInputStream(ct); |
| CipherInputStream cis = new CipherInputStream(is, cipher); |
| try { |
| byte[] result = new byte[t.pt.length]; |
| int totalLength = 0; |
| int length = 0; |
| do { |
| length = cis.read(result, totalLength, result.length - totalLength); |
| if (length > 0) { |
| totalLength += length; |
| } |
| } while (length >= 0 && totalLength != result.length); |
| cis.close(); |
| fail("this should fail"); |
| } catch (IOException ex) { |
| // expected |
| } |
| } |
| } |
| |
| public void testAesGcm() throws Exception { |
| final int[] keySizes = {16, 32}; |
| final int[] ivSizes = {12}; |
| final int[] tagSizes = {12, 16}; |
| final int[] ptSizes = {0, 8, 16, 65, 8100}; |
| final int[] aadSizes = {0, 8, 24}; |
| Iterable<TestVector> v = |
| getTestVectors("AES/GCM/NoPadding", keySizes, ivSizes, tagSizes, ptSizes, aadSizes); |
| testEncrypt(v); |
| testDecrypt(v); |
| } |
| |
| public void testCorruptAesGcm() throws Exception { |
| final int[] keySizes = {16, 32}; |
| final int[] ivSizes = {12}; |
| final int[] tagSizes = {12, 16}; |
| final int[] ptSizes = {8, 16, 65, 8100}; |
| final int[] aadSizes = {0, 8, 24}; |
| Iterable<TestVector> v = |
| getTestVectors("AES/GCM/NoPadding", keySizes, ivSizes, tagSizes, ptSizes, aadSizes); |
| testCorruptDecrypt(v); |
| } |
| |
| /** |
| * Unfortunately Oracle thinks that returning an empty array is valid behaviour for corrupt |
| * ciphertexts. Because of this we test empty plaintext separately to distinguish behaviour |
| * considered acceptable by Oracle from other behaviour. |
| */ |
| public void testEmptyPlaintext() throws Exception { |
| final int[] keySizes = {16, 32}; |
| final int[] ivSizes = {12}; |
| final int[] tagSizes = {12, 16}; |
| final int[] ptSizes = {0}; |
| final int[] aadSizes = {0, 8, 24}; |
| Iterable<TestVector> v = |
| getTestVectors("AES/GCM/NoPadding", keySizes, ivSizes, tagSizes, ptSizes, aadSizes); |
| testCorruptDecryptEmpty(v); |
| } |
| |
| /** Tests CipherOutputStream with AES-EAX if this algorithm is supported by the provider. */ |
| public void testAesEax() throws Exception { |
| final String algorithm = "AES/EAX/NoPadding"; |
| final int[] keySizes = {16, 32}; |
| final int[] ivSizes = {12, 16}; |
| final int[] tagSizes = {12, 16}; |
| final int[] ptSizes = {0, 8, 16, 65, 8100}; |
| final int[] aadSizes = {0, 8, 24}; |
| try { |
| Cipher.getInstance(algorithm); |
| } catch (NoSuchAlgorithmException ex) { |
| System.out.println("Skipping testAesEax"); |
| return; |
| } |
| Iterable<TestVector> v = |
| getTestVectors(algorithm, keySizes, ivSizes, tagSizes, ptSizes, aadSizes); |
| testEncrypt(v); |
| testDecrypt(v); |
| testCorruptDecrypt(v); |
| } |
| } |