/* Copyright 2014 Duncan Jones
*
* 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 org.cryptonode.jncryptor;
import static org.junit.Assert.*;
import java.util.Arrays;
import java.util.Random;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import org.junit.Assert;
import org.junit.Test;
/**
*
*/
public class AES256JNCryptorTest {
private static final Random RANDOM = new Random();
public void testNullOrEmptyPassword() throws Exception {
JNCryptor cryptor = new AES256JNCryptor();
try {
cryptor.encryptData("foo".getBytes(), null);
fail("Null password should fail on encrypt");
} catch (IllegalArgumentException e) {
// expected
}
try {
cryptor.encryptData("foo".getBytes(), new char[0]);
fail("Empty password should fail on encrypt");
} catch (IllegalArgumentException e) {
// expected
}
try {
cryptor.decryptData("foo".getBytes(), null);
fail("Null password should fail on decrypt");
} catch (IllegalArgumentException e) {
// expected
}
try {
cryptor.decryptData("foo".getBytes(), new char[0]);
fail("Empty password should fail on decrypt");
} catch (IllegalArgumentException e) {
// expected
}
try {
cryptor.keyForPassword(null, new byte[8]);
fail("Null password should fail on key-for-password");
} catch (IllegalArgumentException e) {
// expected
}
try {
cryptor.keyForPassword(new char[0], new byte[8]);
fail("Empty password should fail on key-for-password");
} catch (IllegalArgumentException e) {
// expected
}
}
/**
* Tests the constructor sets the iteration count
*
* @throws Exception
*/
@Test
public void testIterationConstructor() throws Exception {
int iterations = 42;
AES256JNCryptor cryptor = new AES256JNCryptor(iterations);
assertEquals(iterations, cryptor.getPBKDFIterations());
}
/**
* Performs a simple round-trip encryption and decryption.
*
* @throws Exception
*/
@Test
public void testEncryptionAndDecryption() throws Exception {
String password = "1234";
byte[] plaintext = "Hello, World!".getBytes();
AES256JNCryptor cryptor = new AES256JNCryptor();
byte[] ciphertext = cryptor.encryptData(plaintext, password.toCharArray());
// Check version
assertEquals(AES256v3Ciphertext.EXPECTED_VERSION, ciphertext[0]);
byte[] plaintext2 = cryptor.decryptData(ciphertext, password.toCharArray());
Assert.assertArrayEquals(plaintext, plaintext2);
}
/**
* Creates a valid ciphertext, modifies the MAC and verifies that the
* decryption fails.
*
* @throws Exception
*/
@Test(expected = InvalidHMACException.class)
public void testBrokenHMAC() throws Exception {
String password = "1234";
byte[] plaintext = "Hello, World!".getBytes();
AES256JNCryptor cryptor = new AES256JNCryptor();
byte[] ciphertext = cryptor.encryptData(plaintext, password.toCharArray());
// Change one byte in the HMAC
ciphertext[ciphertext.length - 1] = (byte) (ciphertext[ciphertext.length - 1] + 1);
cryptor.decryptData(ciphertext, password.toCharArray());
}
/**
* Tests an exception is thrown when the ciphertext is in a bad format.
*
* @throws Exception
*/
@Test(expected = CryptorException.class)
public void testBadInput() throws Exception {
final byte[] nonsenseData = new byte[] { 0x45, 0x55 };
new AES256JNCryptor().decryptData(nonsenseData, "blah".toCharArray());
}
/**
* Tests an exception is thrown when the iteration count is zero.
*
* @throws Exception
*/
@Test(expected = IllegalArgumentException.class)
public void testBadIterations() throws Exception {
new AES256JNCryptor(0);
}
/**
* Tests decryption of a known (v2) ciphertext.
*
* @throws Exception
*/
@Test
public void testKnownCiphertext() throws Exception {
final String password = "P@ssw0rd!";
final String expectedPlaintextString = "Hello, World! Let's use a few blocks "
+ "with a longer sentence.";
String knownCiphertext = "02013F194AA9969CF70C8ACB76824DE4CB6CDCF78B7449A87C679FB8EDB6"
+ "A0109C513481DE877F3A855A184C4947F2B3E8FEF7E916E4739F9F889A717FCAF277402866341008A"
+ "09FD3EBAC7FA26C969DD7EE72CFB695547C971A75D8BF1CC5980E0C727BD9F97F6B7489F687813BEB"
+ "94DEB61031260C246B9B0A78C2A52017AA8C92";
byte[] ciphertext = DatatypeConverter.parseHexBinary(knownCiphertext);
AES256JNCryptor cryptor = new AES256JNCryptor();
byte[] plaintext = cryptor.decryptData(ciphertext, password.toCharArray());
String plaintextString = new String(plaintext, "UTF-8");
assertEquals(expectedPlaintextString, plaintextString);
}
// @Test
// public void makeKnownCiphertext() throws Exception {
// final String password = "P@ssw0rd!";
// final String plaintextString = "Hello, World! Let's use a few blocks "
// + "with a longer sentence.";
//
// final byte[] plaintext = plaintextString.getBytes("US-ASCII");
//
// AES256JNCryptor cryptor = new AES256JNCryptor();
// byte[] ciphertext = cryptor.encryptData(plaintext, password.toCharArray());
//
// System.out.println(DatatypeConverter.printHexBinary(plaintext));
// System.out.println(DatatypeConverter.printHexBinary(ciphertext));
// }
/**
* Tests a {@link NullPointerException} is thrown when the ciphertext is null
* during decryption.
*
* @throws Exception
*/
@Test(expected = NullPointerException.class)
public void testNullCiphertextInDecrypt() throws Exception {
new AES256JNCryptor().decryptData(null, "blah".toCharArray());
}
/**
* Tests a {@link NullPointerException} is thrown when the plaintext is null
* during encryption.
*
* @throws Exception
*/
@Test(expected = NullPointerException.class)
public void testNullPlaintextInEncrypt() throws Exception {
new AES256JNCryptor().encryptData(null, "blah".toCharArray());
}
/**
* Performs an encryption followed by a decryption and confirms the data is
* the same. Uses the key-based methods.
*
* @throws Exception
*/
@Test
public void testKeyBasedEncryptionAndDecryption() throws Exception {
SecretKey encryptionKey = makeRandomAESKey();
SecretKey hmacKey = makeRandomAESKey();
final byte[] plaintext = "Hello, World!".getBytes();
AES256JNCryptor cryptor = new AES256JNCryptor();
byte[] ciphertext = cryptor.encryptData(plaintext, encryptionKey, hmacKey);
// Check version
assertEquals(AES256v3Ciphertext.EXPECTED_VERSION, ciphertext[0]);
byte[] newPlaintext = cryptor.decryptData(ciphertext, encryptionKey,
hmacKey);
assertArrayEquals(plaintext, newPlaintext);
}
private static SecretKey makeRandomAESKey() {
byte[] keyBytes = new byte[16];
RANDOM.nextBytes(keyBytes);
return new SecretKeySpec(keyBytes, "AES");
}
/**
* Checks an exception is thrown when a bad salt length is suggested.
*
* @throws Exception
*/
@Test(expected = IllegalArgumentException.class)
public void testBadSaltLengthForKey() throws Exception {
new AES256JNCryptor().keyForPassword(null,
new byte[AES256JNCryptor.SALT_LENGTH + 1]);
}
/**
* Tests we return the correct version number.
*
* @throws Exception
*/
@Test
public void testVersionNumber() throws Exception {
assertEquals(3, new AES256JNCryptor().getVersionNumber());
}
/**
* Tests we get an exception if we try to decrypt a key-based ciphertext with
* a password.
*
* @throws Exception
*/
@Test(expected = IllegalArgumentException.class)
public void testDecryptionMismatch() throws Exception {
SecretKey encryptionKey = makeRandomAESKey();
SecretKey hmacKey = makeRandomAESKey();
final byte[] plaintext = "Hello, World!".getBytes();
AES256JNCryptor cryptor = new AES256JNCryptor();
byte[] ciphertext = cryptor.encryptData(plaintext, encryptionKey, hmacKey);
cryptor.decryptData(ciphertext, "whoops!".toCharArray());
}
/**
* Tests we get an exception if we try to set an invalid iteration value.
*
* @throws Exception
*/
@Test(expected = IllegalArgumentException.class)
public void testBadIterationValue() throws Exception {
AES256JNCryptor cryptor = new AES256JNCryptor();
cryptor.setPBKDFIterations(0);
}
/**
* Tests decryption of a known ciphertext but with the wrong iterations.
*
* @throws Exception
*/
@Test(expected = InvalidHMACException.class)
public void testKnownCiphertextWithWrongIterations() throws Exception {
final String password = "P@ssw0rd!";
String knownCiphertext = "02013F194AA9969CF70C8ACB76824DE4CB6CDCF78B7449A87C679FB8EDB6"
+ "A0109C513481DE877F3A855A184C4947F2B3E8FEF7E916E4739F9F889A717FCAF277402866341008A"
+ "09FD3EBAC7FA26C969DD7EE72CFB695547C971A75D8BF1CC5980E0C727BD9F97F6B7489F687813BEB"
+ "94DEB61031260C246B9B0A78C2A52017AA8C92";
byte[] ciphertext = DatatypeConverter.parseHexBinary(knownCiphertext);
AES256JNCryptor cryptor = new AES256JNCryptor();
cryptor.setPBKDFIterations(1);
cryptor.decryptData(ciphertext, password.toCharArray());
}
@Test
public void testArrayEquals() throws Exception {
assertTrue(AES256JNCryptor.arraysEqual(new byte[] {1,2,3}, new byte[] {1,2,3}));
assertFalse(AES256JNCryptor.arraysEqual(new byte[] {1,2,3}, new byte[] {1,2}));
assertFalse(AES256JNCryptor.arraysEqual(new byte[] {1,2}, new byte[] {1,2,3}));
assertFalse(AES256JNCryptor.arraysEqual(new byte[] {1,2,3}, new byte[] {1,2,4}));
}
@Test
public void testPasswordKeyUsesRandomSalts() throws Exception {
final char[] password = "foo".toCharArray();
PasswordKey key1 = new AES256JNCryptor().getPasswordKey(password);
PasswordKey key2 = new AES256JNCryptor().getPasswordKey(password);
assertFalse(Arrays.equals(key1.getSalt(), key2.getSalt()));
}
@Test(expected = NullPointerException.class)
public void testPasswordKeyNullPassword() throws Exception {
new AES256JNCryptor().getPasswordKey(null);
}
@Test(expected = IllegalArgumentException.class)
public void testPasswordKeyEmptyPassword() throws Exception {
new AES256JNCryptor().getPasswordKey(new char[0]);
}
@Test
public void testPasswordKeyEncryption() throws Exception {
char[] password = "foo".toCharArray();
JNCryptor cryptor = new AES256JNCryptor();
PasswordKey encryptionKey = cryptor.getPasswordKey(password);
PasswordKey hmacKey = cryptor.getPasswordKey(password);
final byte[] plaintext = "Hello, World!".getBytes();
final byte[] ciphertext = cryptor.encryptData(plaintext, encryptionKey, hmacKey);
assertArrayEquals(plaintext, cryptor.decryptData(ciphertext, password));
}
@Test
public void testInvalidArgumentsToEncryptWithIVAndSalts() throws Exception {
final byte[] plaintext = new byte[1];
final char[] goodPassword = new char[1];
final char[] badPassword = new char[0];
final byte[] goodSaltArray = new byte[AES256JNCryptor.SALT_LENGTH];
final byte[] badSaltArray = new byte[AES256JNCryptor.SALT_LENGTH - 1];
final byte[] goodIVArray = new byte[AES256JNCryptor.AES_BLOCK_SIZE];
final byte[] badIVArray = new byte[AES256JNCryptor.AES_BLOCK_SIZE - 1];
JNCryptor cryptor = new AES256JNCryptor();
try {
cryptor.encryptData(null, goodPassword, goodSaltArray, goodSaltArray, goodIVArray);
fail();
} catch (NullPointerException e) {}
try {
cryptor.encryptData(plaintext, null, goodSaltArray, goodSaltArray, goodIVArray);
fail();
} catch (NullPointerException e) {}
try {
cryptor.encryptData(plaintext, goodPassword, null, goodSaltArray, goodIVArray);
fail();
} catch (NullPointerException e) {}
try {
cryptor.encryptData(plaintext, goodPassword, goodSaltArray, null, goodIVArray);
fail();
} catch (NullPointerException e) {}
try {
cryptor.encryptData(plaintext, goodPassword, goodSaltArray, goodSaltArray, null);
fail();
} catch (NullPointerException e) {}
try {
cryptor.encryptData(plaintext, badPassword, goodSaltArray, goodSaltArray, goodIVArray);
fail();
} catch (IllegalArgumentException e) {}
try {
cryptor.encryptData(plaintext, goodPassword, badSaltArray, goodSaltArray, goodIVArray);
fail();
} catch (IllegalArgumentException e) {}
try {
cryptor.encryptData(plaintext, goodPassword, goodSaltArray, badSaltArray, goodIVArray);
fail();
} catch (IllegalArgumentException e) {}
try {
cryptor.encryptData(plaintext, goodPassword, goodSaltArray, goodSaltArray, badIVArray);
fail();
} catch (IllegalArgumentException e) {}
}
}