package org.bouncycastle.jce.provider.test; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.Key; import java.security.Security; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.spec.IvParameterSpec; import org.bouncycastle.crypto.io.InvalidCipherTextIOException; import org.bouncycastle.jcajce.io.CipherInputStream; import org.bouncycastle.jcajce.io.CipherOutputStream; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.util.Arrays; import org.bouncycastle.util.test.SimpleTest; public class CipherStreamTest2 extends SimpleTest { private int streamSize; public String getName() { return "CipherStreamTest2"; } private void testModes(String algo, String[] transforms, boolean authenticated) throws Exception { Key key = generateKey(algo); for (int i = 0; i != transforms.length; i++) { String transform = transforms[i]; String cipherName = algo + transform; boolean cts = transform.indexOf("CTS") > -1; if (cts && streamSize < Cipher.getInstance(cipherName, "BC").getBlockSize()) { continue; } testWriteRead(cipherName, key, authenticated, true, false); testWriteRead(cipherName, key, authenticated, true, true); testWriteRead(cipherName, key, authenticated, false, false); testWriteRead(cipherName, key, authenticated, false, true); testReadWrite(cipherName, key, authenticated, true, false); testReadWrite(cipherName, key, authenticated, true, true); testReadWrite(cipherName, key, authenticated, false, false); testReadWrite(cipherName, key, authenticated, false, true); if (!cts) { testWriteReadEmpty(cipherName, key, authenticated, true, false); testWriteReadEmpty(cipherName, key, authenticated, true, true); testWriteReadEmpty(cipherName, key, authenticated, false, false); testWriteReadEmpty(cipherName, key, authenticated, false, true); } if (authenticated) { testTamperedRead(cipherName, key, true, true); testTamperedRead(cipherName, key, true, false); testTruncatedRead(cipherName, key, true, true); testTruncatedRead(cipherName, key, true, false); testTamperedWrite(cipherName, key, true, true); testTamperedWrite(cipherName, key, true, false); } } } private InputStream createInputStream(byte[] data, Cipher cipher, boolean useBc) { ByteArrayInputStream bytes = new ByteArrayInputStream(data); // cast required for earlier JDK return useBc ? (InputStream)new CipherInputStream(bytes, cipher) : (InputStream)new javax.crypto.CipherInputStream(bytes, cipher); } private OutputStream createOutputStream(ByteArrayOutputStream bytes, Cipher cipher, boolean useBc) { // cast required for earlier JDK return useBc ? (OutputStream)new CipherOutputStream(bytes, cipher) : (OutputStream)new javax.crypto.CipherOutputStream(bytes, cipher); } /** * Test tampering of ciphertext followed by read from decrypting CipherInputStream */ private void testTamperedRead(String name, Key key, boolean authenticated, boolean useBc) throws Exception { Cipher encrypt = Cipher.getInstance(name, "BC"); Cipher decrypt = Cipher.getInstance(name, "BC"); encrypt.init(Cipher.ENCRYPT_MODE, key); if (encrypt.getIV() != null) { decrypt.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(encrypt.getIV())); } else { decrypt.init(Cipher.DECRYPT_MODE, key); } byte[] ciphertext = encrypt.doFinal(new byte[streamSize]); // Tamper ciphertext[0] += 1; InputStream input = createInputStream(ciphertext, decrypt, useBc); try { while (input.read() >= 0) { } fail("Expected invalid ciphertext after tamper and read : " + name, authenticated, useBc); } catch (InvalidCipherTextIOException e) { // Expected } catch (IOException e) // cause will be AEADBadTagException { // Expected } try { input.close(); } catch (Exception e) { fail("Unexpected exception : " + name, e, authenticated, useBc); } } /** * Test truncation of ciphertext to make tag calculation impossible, followed by read from * decrypting CipherInputStream */ private void testTruncatedRead(String name, Key key, boolean authenticated, boolean useBc) throws Exception { Cipher encrypt = Cipher.getInstance(name, "BC"); Cipher decrypt = Cipher.getInstance(name, "BC"); encrypt.init(Cipher.ENCRYPT_MODE, key); if (encrypt.getIV() != null) { decrypt.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(encrypt.getIV())); } else { decrypt.init(Cipher.DECRYPT_MODE, key); } byte[] ciphertext = encrypt.doFinal(new byte[streamSize]); // Truncate to just smaller than complete tag byte[] truncated = new byte[ciphertext.length - streamSize - 1]; System.arraycopy(ciphertext, 0, truncated, 0, truncated.length); // Tamper ciphertext[0] += 1; InputStream input = createInputStream(truncated, decrypt, useBc); while (true) { int read = 0; try { read = input.read(); } catch (InvalidCipherTextIOException e) { // Expected break; } catch (IOException e) { // Expected from JDK 1.7 on break; } catch (Exception e) { fail("Unexpected exception : " + name, e, authenticated, useBc); break; } if (read < 0) { fail("Expected invalid ciphertext after truncate and read : " + name, authenticated, useBc); break; } } try { input.close(); } catch (Exception e) { fail("Unexpected exception : " + name, e, authenticated, useBc); } } /** * Test tampering of ciphertext followed by write to decrypting CipherOutputStream */ private void testTamperedWrite(String name, Key key, boolean authenticated, boolean useBc) throws Exception { Cipher encrypt = Cipher.getInstance(name, "BC"); Cipher decrypt = Cipher.getInstance(name, "BC"); encrypt.init(Cipher.ENCRYPT_MODE, key); if (encrypt.getIV() != null) { decrypt.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(encrypt.getIV())); } else { decrypt.init(Cipher.DECRYPT_MODE, key); } byte[] ciphertext = encrypt.doFinal(new byte[streamSize]); // Tamper ciphertext[0] += 1; ByteArrayOutputStream plaintext = new ByteArrayOutputStream(); OutputStream output = createOutputStream(plaintext, decrypt, useBc); for (int i = 0; i < ciphertext.length; i++) { output.write(ciphertext[i]); } try { output.close(); fail("Expected invalid ciphertext after tamper and write : " + name, authenticated, useBc); } catch (InvalidCipherTextIOException e) { // Expected } } /** * Test CipherOutputStream in ENCRYPT_MODE, CipherInputStream in DECRYPT_MODE */ private void testWriteRead(String name, Key key, boolean authenticated, boolean useBc, boolean blocks) throws Exception { byte[] data = new byte[streamSize]; for (int i = 0; i < data.length; i++) { data[i] = (byte)(i % 255); } testWriteRead(name, key, authenticated, useBc, blocks, data); } /** * Test CipherOutputStream in ENCRYPT_MODE, CipherInputStream in DECRYPT_MODE */ private void testWriteReadEmpty(String name, Key key, boolean authenticated, boolean useBc, boolean blocks) throws Exception { byte[] data = new byte[0]; testWriteRead(name, key, authenticated, useBc, blocks, data); } private void testWriteRead(String name, Key key, boolean authenticated, boolean useBc, boolean blocks, byte[] data) { ByteArrayOutputStream bOut = new ByteArrayOutputStream(); try { Cipher encrypt = Cipher.getInstance(name, "BC"); Cipher decrypt = Cipher.getInstance(name, "BC"); encrypt.init(Cipher.ENCRYPT_MODE, key); if (encrypt.getIV() != null) { decrypt.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(encrypt.getIV())); } else { decrypt.init(Cipher.DECRYPT_MODE, key); } OutputStream cOut = createOutputStream(bOut, encrypt, useBc); if (blocks) { int chunkSize = Math.max(1, data.length / 8); for (int i = 0; i < data.length; i += chunkSize) { cOut.write(data, i, Math.min(chunkSize, data.length - i)); } } else { for (int i = 0; i < data.length; i++) { cOut.write(data[i]); } } cOut.close(); byte[] cipherText = bOut.toByteArray(); bOut.reset(); InputStream cIn = createInputStream(cipherText, decrypt, useBc); if (blocks) { byte[] block = new byte[encrypt.getBlockSize() + 1]; int c; while ((c = cIn.read(block)) >= 0) { bOut.write(block, 0, c); } } else { int c; while ((c = cIn.read()) >= 0) { bOut.write(c); } } cIn.close(); } catch (Exception e) { fail("Unexpected exception " + name, e, authenticated, useBc); } byte[] decrypted = bOut.toByteArray(); if (!Arrays.areEqual(data, decrypted)) { fail("Failed - decrypted data doesn't match: " + name, authenticated, useBc); } } protected void fail(String message, boolean authenticated, boolean bc) { if (bc || !authenticated) { super.fail(message); } else { // javax.crypto.CipherInputStream/CipherOutputStream // are broken wrt handling AEAD failures // System.err.println("Broken JCE Streams: " + message); } } protected void fail(String message, Throwable throwable, boolean authenticated, boolean bc) { if (bc || !authenticated) { super.fail(message, throwable); } else { // javax.crypto.CipherInputStream/CipherOutputStream // are broken wrt handling AEAD failures //System.err.println("Broken JCE Streams: " + message + " : " + throwable); throwable.printStackTrace(); } } /** * Test CipherInputStream in ENCRYPT_MODE, CipherOutputStream in DECRYPT_MODE */ private void testReadWrite(String name, Key key, boolean authenticated, boolean useBc, boolean blocks) throws Exception { String lCode = "ABCDEFGHIJKLMNOPQRSTU"; ByteArrayOutputStream bOut = new ByteArrayOutputStream(); try { Cipher in = Cipher.getInstance(name, "BC"); Cipher out = Cipher.getInstance(name, "BC"); in.init(Cipher.ENCRYPT_MODE, key); if (in.getIV() != null) { out.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(in.getIV())); } else { out.init(Cipher.DECRYPT_MODE, key); } InputStream cIn = createInputStream(lCode.getBytes(), in, useBc); OutputStream cOut = createOutputStream(bOut, out, useBc); if (blocks) { byte[] block = new byte[in.getBlockSize() + 1]; int c; while ((c = cIn.read(block)) >= 0) { cOut.write(block, 0, c); } } else { int c; while ((c = cIn.read()) >= 0) { cOut.write(c); } } cIn.close(); cOut.flush(); cOut.close(); } catch (Exception e) { fail("Unexpected exception " + name, e, authenticated, useBc); } String res = new String(bOut.toByteArray()); if (!res.equals(lCode)) { fail("Failed - decrypted data doesn't match: " + name, authenticated, useBc); } } private static Key generateKey(String name) throws Exception { KeyGenerator kGen; if (name.indexOf('/') < 0) { kGen = KeyGenerator.getInstance(name, "BC"); } else { kGen = KeyGenerator.getInstance(name.substring(0, name.indexOf('/')), "BC"); } return kGen.generateKey(); } public void performTest() throws Exception { int[] testSizes = new int[]{0, 1, 7, 8, 9, 15, 16, 17, 1023, 1024, 1025, 2047, 2048, 2049, 4095, 4096, 4097}; for (int i = 0; i < testSizes.length; i++) { this.streamSize = testSizes[i]; performTests(); } } private void performTests() throws Exception { final String[] blockCiphers64 = new String[]{"BLOWFISH", "DES", "DESEDE", "TEA", "CAST5", "RC2", "XTEA"}; for (int i = 0; i != blockCiphers64.length; i++) { testModes(blockCiphers64[i], new String[]{ "/ECB/PKCS5Padding", "/CBC/PKCS5Padding", "/OFB/NoPadding", "/CFB/NoPadding", "/CTS/NoPadding",}, false); testModes(blockCiphers64[i], new String[]{"/EAX/NoPadding"}, true); } final String[] blockCiphers128 = new String[]{ "AES", "NOEKEON", "Twofish", "CAST6", "SEED", "Serpent", "RC6", "CAMELLIA"}; for (int i = 0; i != blockCiphers128.length; i++) { testModes(blockCiphers128[i], new String[]{ "/ECB/PKCS5Padding", "/CBC/PKCS5Padding", "/OFB/NoPadding", "/CFB/NoPadding", "/CTS/NoPadding", "/CTR/NoPadding", "/SIC/NoPadding"}, false); testModes(blockCiphers128[i], new String[]{"/CCM/NoPadding", "/EAX/NoPadding", "/GCM/NoPadding", "/OCB/NoPadding"}, true); } final String[] streamCiphers = new String[]{ "ARC4", "SALSA20", "XSalsa20", "ChaCha", "ChaCha7539", "Grainv1", "Grain128", "HC128", "HC256"}; for (int i = 0; i != streamCiphers.length; i++) { testModes(streamCiphers[i], new String[]{""}, false); } } public static void main(String[] args) { Security.addProvider(new BouncyCastleProvider()); runTest(new CipherStreamTest2()); } }