blob: 8f4cf01236b104e63ba632be035f8868384131e3 [file] [log] [blame]
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());
}
}