/*
* Syncany, www.syncany.org
* Copyright (C) 2011-2015 Philipp C. Heckel <philipp.heckel@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.syncany.tests.unit.crypto;
import static org.junit.Assert.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.Security;
import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.io.InvalidCipherTextIOException;
import org.bouncycastle.crypto.modes.AEADBlockCipher;
import org.bouncycastle.crypto.modes.GCMBlockCipher;
import org.bouncycastle.crypto.params.AEADParameters;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.Arrays;
import org.junit.Test;
import org.syncany.config.Logging;
/**
* Tests to test the Bouncy Castle implementation of the CipherInputStream
*
* @see https://github.com/binwiederhier/cipherinputstream-aes-gcm/blob/e9759ca71557e5d1da26ae72f6ce5aac918e34b0/src/CipherInputStreamIssuesTests.java
* @see http://blog.philippheckel.com/2014/03/01/cipherinputstream-for-aead-modes-is-broken-in-jdk7-gcm/
*/
public class AesGcmWithBcInputStreamTest {
private static final SecureRandom secureRandom = new SecureRandom();
static {
Logging.init();
Security.addProvider(new BouncyCastleProvider());
}
@Test
public void testD_BouncyCastleCipherInputStreamWithAesGcm() throws InvalidKeyException, InvalidAlgorithmParameterException, IOException, NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException {
// Encrypt (not interesting in this example)
byte[] randomKey = createRandomArray(16);
byte[] randomIv = createRandomArray(16);
byte[] originalPlaintext = "Confirm 100$ pay".getBytes("ASCII");
byte[] originalCiphertext = encryptWithAesGcm(originalPlaintext, randomKey, randomIv);
// Attack / alter ciphertext (an attacker would do this!)
byte[] alteredCiphertext = Arrays.clone(originalCiphertext);
alteredCiphertext[8] = (byte) (alteredCiphertext[8] ^ 0x08); // <<< Change 100$ to 900$
// Decrypt with BouncyCastle implementation of CipherInputStream
AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
cipher.init(false, new AEADParameters(new KeyParameter(randomKey), 128, randomIv));
try {
readFromStream(new org.bouncycastle.crypto.io.CipherInputStream(new ByteArrayInputStream(alteredCiphertext), cipher));
// ^^^^^^^^^^^^^^^ INTERESTING PART ^^^^^^^^^^^^^^^^
//
// The BouncyCastle implementation of the CipherInputStream detects MAC verification errors and
// throws a InvalidCipherTextIOException if an error occurs. Nice! A more or less minor issue
// however is that it is incompatible with the standard JCE Cipher class from the javax.crypto
// package. The new interface AEADBlockCipher must be used. The code below is not executed.
fail("Test D: org.bouncycastle.crypto.io.CipherInputStream: NOT OK, tampering not detected");
}
catch (InvalidCipherTextIOException e) {
System.out.println("Test D: org.bouncycastle.crypto.io.CipherInputStream: OK, tampering detected");
}
}
@Test
public void testE_BouncyCastleCipherInputStreamWithAesGcmLongPlaintext() throws InvalidKeyException, InvalidAlgorithmParameterException, IOException, NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException {
// Encrypt (not interesting in this example)
byte[] randomKey = createRandomArray(16);
byte[] randomIv = createRandomArray(16);
byte[] originalPlaintext = createRandomArray(4080); // <<<< 4080 bytes fails, 4079 bytes works!
byte[] originalCiphertext = encryptWithAesGcm(originalPlaintext, randomKey, randomIv);
// Decrypt with BouncyCastle implementation of CipherInputStream
AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
cipher.init(false, new AEADParameters(new KeyParameter(randomKey), 128, randomIv));
try {
readFromStream(new org.bouncycastle.crypto.io.CipherInputStream(new ByteArrayInputStream(originalCiphertext), cipher));
// ^^^^^^^^^^^^^^^ INTERESTING PART ^^^^^^^^^^^^^^^^
//
// In this example, the BouncyCastle implementation of the CipherInputStream throws an ArrayIndexOutOfBoundsException.
// The only difference to the example above is that the plaintext is now 4080 bytes long! For 4079 bytes plaintexts,
// everything works just fine.
System.out.println("Test E: org.bouncycastle.crypto.io.CipherInputStream: OK, throws no exception");
}
catch (IOException e) {
fail("Test E: org.bouncycastle.crypto.io.CipherInputStream: NOT OK throws: "+e.getMessage());
}
}
private static byte[] readFromStream(InputStream inputStream) throws IOException {
ByteArrayOutputStream decryptedPlaintextOutputStream = new ByteArrayOutputStream();
int read = -1;
byte[] buffer = new byte[16];
while (-1 != (read = inputStream.read(buffer))) {
decryptedPlaintextOutputStream.write(buffer, 0, read);
}
inputStream.close();
decryptedPlaintextOutputStream.close();
return decryptedPlaintextOutputStream.toByteArray();
}
private static byte[] encryptWithAesGcm(byte[] plaintext, byte[] randomKeyBytes, byte[] randomIvBytes) throws IOException, InvalidKeyException,
InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException {
SecretKey randomKey = new SecretKeySpec(randomKeyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
cipher.init(Cipher.ENCRYPT_MODE, randomKey, new IvParameterSpec(randomIvBytes));
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
CipherOutputStream cipherOutputStream = new CipherOutputStream(byteArrayOutputStream, cipher);
cipherOutputStream.write(plaintext);
cipherOutputStream.close();
return byteArrayOutputStream.toByteArray();
}
private static byte[] createRandomArray(int size) {
byte[] randomByteArray = new byte[size];
secureRandom.nextBytes(randomByteArray);
return randomByteArray;
}
}