/* 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) {} } }