/* 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 java.security.GeneralSecurityException; import java.security.InvalidKeyException; import java.security.SecureRandom; import javax.crypto.Cipher; import javax.crypto.Mac; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; /** * This {@link JNCryptor} instance produces data in version 3 format. It can * read data in any format since version 2. * <p> * * <pre> * | version | options | encryption salt | HMAC salt | IV | ... ciphertext ... | HMAC | * | 0 | 1 | 2->9 | 10->17 | 18->33 | <- ... -> | (n-32) -> n | * </pre> * * <ul> * <li><b>version</b> (1 byte): Data format version.</li> * <li><b>options</b> (1 byte): {@code 0x00} if keys are used, {@code 0x01} if a * password is used.</li> * <li><b>encryption salt</b> (8 bytes)</li> * <li><b>HMAC salt</b> (8 bytes)</li> * <li><b>IV</b> (16 bytes)</li> * <li><b>ciphertext</b> (variable): 256-bit AES encrypted, CBC-mode with * PKCS #5 padding.</li> * <li><b>HMAC</b> (32 bytes)</li> * </ul> * * <p> * The encryption key is derived using the PKBDF2 function, using a random * eight-byte encryption salt, the supplied password and 10,000 iterations. The * iteration count can be changed using the {@link #setPBKDFIterations(int)} * method. The HMAC key is derived in a similar fashion, using its own random * eight-byte HMAC salt. Both salt values are stored in the ciphertext output * (as shown above). * * <p> * The ciphertext is AES-256-CBC encrypted, using a randomly generated IV and * the encryption key (described above), with PKCS #5 padding. * <p> * The HMAC is calculated across all the data (except the HMAC itself, of * course), generated using the HMAC key described above and the SHA-256 PRF. * <p> * <p> * This class is thread-safe. Multiple threads may share one instance of this * class, or each thread may have its own instance. * </p> * See <a * href="https://github.com/rnapier/RNCryptor/wiki/Data-Format">https://github * .com/rnapier/RNCryptor/wiki/Data-Format</a>, from which most of the * information above was shamelessly copied. * * @since 0.5 */ public class AES256JNCryptor implements JNCryptor { /** * AES encryption/decryption algorithm. */ static final String AES_CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding"; /** * HMAC algorithm */ static final String HMAC_ALGORITHM = "HmacSHA256"; /** * AES algorithm name. */ static final String AES_NAME = "AES"; /** * The key derivation algorith name */ static final String KEY_DERIVATION_ALGORITHM = "PBKDF2WithHmacSHA1"; /** * The default number of PBKDF2 iterations. */ static final int PBKDF_DEFAULT_ITERATIONS = 10000; /** * The data format version number. */ static final int VERSION = 3; /** * The size of an AES key (useful constant). */ static final int AES_256_KEY_SIZE = 256 / 8; /** * AES block size in bytes. */ static final int AES_BLOCK_SIZE = 16; /** * Size of the salt (in bytes) */ static final int SALT_LENGTH = 8; // SecureRandom is threadsafe private static final SecureRandom SECURE_RANDOM = new SecureRandom(); // Access to this variable must be synchronized private int iterations = PBKDF_DEFAULT_ITERATIONS; /** * Creates a new {@code AES256JNCryptor} instance. Uses the default number of * PBKDF iterations. */ public AES256JNCryptor() { } /** * Creates a new {@code AES256JNCryptor} instance that uses a specific number * of PBKDF iterations. * * @param iterations * the number of PBKDF iterations to perform */ public AES256JNCryptor(int iterations) { Validate.isTrue(iterations > 0, "Iteration value must be positive."); this.iterations = iterations; } @Override public SecretKey keyForPassword(char[] password, byte[] salt) throws CryptorException { Validate.notNull(salt, "Salt value cannot be null."); Validate.isTrue(salt.length == SALT_LENGTH, "Salt value must be %d bytes.", SALT_LENGTH); Validate.notNull(password, "Password cannot be null."); Validate.isTrue(password.length > 0, "Password cannot be empty."); try { SecretKeyFactory factory = SecretKeyFactory .getInstance(KEY_DERIVATION_ALGORITHM); SecretKey tmp = factory.generateSecret(new PBEKeySpec(password, salt, getPBKDFIterations(), AES_256_KEY_SIZE * 8)); return new SecretKeySpec(tmp.getEncoded(), AES_NAME); } catch (GeneralSecurityException e) { throw new CryptorException(String.format( "Failed to generate key from password using %s.", KEY_DERIVATION_ALGORITHM), e); } } @Override public synchronized int getPBKDFIterations() { return iterations; } @Override public synchronized void setPBKDFIterations(int iterations) { Validate.isTrue(iterations > 0, "Number of iterations must be greater than zero."); this.iterations = iterations; } /** * Decrypts data. * * @param aesCiphertext * the ciphertext from the message * @param decryptionKey * the key to decrypt * @param hmacKey * the key to recalculate the HMAC * @return the decrypted data * @throws CryptorException * if a JCE error occurs */ private byte[] decryptV2Data(AES256v2Ciphertext aesCiphertext, SecretKey decryptionKey, SecretKey hmacKey) throws CryptorException { try { Mac mac = Mac.getInstance(HMAC_ALGORITHM); mac.init(hmacKey); byte[] hmacValue = mac.doFinal(aesCiphertext.getDataToHMAC()); if (!arraysEqual(hmacValue, aesCiphertext.getHmac())) { throw new InvalidHMACException("Incorrect HMAC value."); } Cipher cipher = Cipher.getInstance(AES_CIPHER_ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, decryptionKey, new IvParameterSpec( aesCiphertext.getIv())); return cipher.doFinal(aesCiphertext.getCiphertext()); } catch (InvalidKeyException e) { throw new CryptorException( "Caught InvalidKeyException. Do you have unlimited strength jurisdiction files installed?", e); } catch (GeneralSecurityException e) { throw new CryptorException("Failed to decrypt message.", e); } } /** * Decrypts data. * * @param aesCiphertext * the ciphertext from the message * @param decryptionKey * the key to decrypt * @param hmacKey * the key to recalculate the HMAC * @return the decrypted data * @throws CryptorException * if a JCE error occurs */ private byte[] decryptV3Data(AES256v3Ciphertext aesCiphertext, SecretKey decryptionKey, SecretKey hmacKey) throws CryptorException { try { Mac mac = Mac.getInstance(HMAC_ALGORITHM); mac.init(hmacKey); byte[] hmacValue = mac.doFinal(aesCiphertext.getDataToHMAC()); if (!arraysEqual(hmacValue, aesCiphertext.getHmac())) { throw new InvalidHMACException("Incorrect HMAC value."); } Cipher cipher = Cipher.getInstance(AES_CIPHER_ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, decryptionKey, new IvParameterSpec( aesCiphertext.getIv())); return cipher.doFinal(aesCiphertext.getCiphertext()); } catch (InvalidKeyException e) { throw new CryptorException( "Caught InvalidKeyException. Do you have unlimited strength jurisdiction files installed?", e); } catch (GeneralSecurityException e) { throw new CryptorException("Failed to decrypt message.", e); } } @Override public byte[] decryptData(byte[] ciphertext, char[] password) throws CryptorException { Validate.notNull(ciphertext, "Ciphertext cannot be null."); Validate.notNull(password, "Password cannot be null."); Validate.isTrue(password.length > 0, "Password cannot be empty."); // I don't like the magic numbers here, but can't think of a pleasant way // to solve this int version = readVersionNumber(ciphertext); switch (version) { case 2: return decryptV2Data(ciphertext, password); case 3: return decryptV3Data(ciphertext, password); default: throw new CryptorException(String.format( "Unrecognised version number: %d.", version)); } } private byte[] decryptV2Data(byte[] ciphertext, char[] password) throws CryptorException { try { AES256v2Ciphertext aesCiphertext = new AES256v2Ciphertext(ciphertext); if (!aesCiphertext.isPasswordBased()) { throw new IllegalArgumentException( "Ciphertext was not encrypted with a password."); } SecretKey decryptionKey = keyForPassword(password, aesCiphertext.getEncryptionSalt()); SecretKey hmacKey = keyForPassword(password, aesCiphertext.getHmacSalt()); return decryptV2Data(aesCiphertext, decryptionKey, hmacKey); } catch (InvalidDataException e) { throw new CryptorException("Unable to parse ciphertext.", e); } } private byte[] decryptV3Data(byte[] ciphertext, char[] password) throws CryptorException { try { AES256v3Ciphertext aesCiphertext = new AES256v3Ciphertext(ciphertext); if (!aesCiphertext.isPasswordBased()) { throw new IllegalArgumentException( "Ciphertext was not encrypted with a password."); } SecretKey decryptionKey = keyForPassword(password, aesCiphertext.getEncryptionSalt()); SecretKey hmacKey = keyForPassword(password, aesCiphertext.getHmacSalt()); return decryptV3Data(aesCiphertext, decryptionKey, hmacKey); } catch (InvalidDataException e) { throw new CryptorException("Unable to parse ciphertext.", e); } } @Override public byte[] encryptData(byte[] plaintext, char[] password, byte[] encryptionSalt, byte[] hmacSalt, byte[] iv) throws CryptorException { Validate.notNull(plaintext, "Plaintext cannot be null."); Validate.notNull(password, "Password cannot be null."); Validate.isTrue(password.length > 0, "Password cannot be empty."); Validate.isCorrectLength(encryptionSalt, SALT_LENGTH, "Encryption salt"); Validate.isCorrectLength(hmacSalt, SALT_LENGTH, "HMAC salt"); Validate.isCorrectLength(iv, AES_BLOCK_SIZE, "IV"); SecretKey encryptionKey = keyForPassword(password, encryptionSalt); SecretKey hmacKey = keyForPassword(password, hmacSalt); return encryptData(plaintext, new PasswordKey(encryptionKey, encryptionSalt), new PasswordKey( hmacKey, hmacSalt), iv); } @Override public byte[] encryptData(byte[] plaintext, char[] password) throws CryptorException { Validate.notNull(plaintext, "Plaintext cannot be null."); Validate.notNull(password, "Password cannot be null."); Validate.isTrue(password.length > 0, "Password cannot be empty."); byte[] encryptionSalt = getSecureRandomData(SALT_LENGTH); byte[] hmacSalt = getSecureRandomData(SALT_LENGTH); byte[] iv = getSecureRandomData(AES_BLOCK_SIZE); return encryptData(plaintext, password, encryptionSalt, hmacSalt, iv); } /** * Returns random data supplied by this class' {@link SecureRandom} instance. * * @param length * the number of bytes to return * @return random bytes */ static byte[] getSecureRandomData(int length) { byte[] result = new byte[length]; SECURE_RANDOM.nextBytes(result); return result; } @Override public int getVersionNumber() { return VERSION; } @Override public byte[] decryptData(byte[] ciphertext, SecretKey decryptionKey, SecretKey hmacKey) throws CryptorException, InvalidHMACException { Validate.notNull(ciphertext, "Ciphertext cannot be null."); Validate.notNull(decryptionKey, "Decryption key cannot be null."); Validate.notNull(hmacKey, "HMAC key cannot be null."); try { int version = readVersionNumber(ciphertext); switch (version) { case 2: return decryptV2Data(new AES256v2Ciphertext(ciphertext), decryptionKey, hmacKey); case 3: return decryptV3Data(new AES256v3Ciphertext(ciphertext), decryptionKey, hmacKey); default: throw new CryptorException(String.format( "Unrecognised version number: %d.", version)); } } catch (InvalidDataException e) { throw new CryptorException("Unable to parse ciphertext.", e); } } @Override public byte[] encryptData(byte[] plaintext, SecretKey encryptionKey, SecretKey hmacKey) throws CryptorException { Validate.notNull(plaintext, "Plaintext cannot be null."); Validate.notNull(encryptionKey, "Encryption key cannot be null."); Validate.notNull(hmacKey, "HMAC key cannot be null."); byte[] iv = getSecureRandomData(AES_BLOCK_SIZE); try { Cipher cipher = Cipher.getInstance(AES_CIPHER_ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, new IvParameterSpec(iv)); byte[] ciphertext = cipher.doFinal(plaintext); AES256v3Ciphertext output = new AES256v3Ciphertext(iv, ciphertext); Mac mac = Mac.getInstance(HMAC_ALGORITHM); mac.init(hmacKey); byte[] hmac = mac.doFinal(output.getDataToHMAC()); output.setHmac(hmac); return output.getRawData(); } catch (GeneralSecurityException e) { throw new CryptorException("Failed to generate ciphertext.", e); } } private static int readVersionNumber(byte[] data) { Validate.isTrue(data.length > 0, "Data must be at least one byte long to read version number."); return data[0]; } /** * Compares arrays for equality in constant time. This avoids * certain types of timing attacks. * * @param array1 the first array * @param array2 the second array * @return whether the arrays match */ static boolean arraysEqual(byte[] array1, byte[] array2) { if (array1.length != array2.length) { return false; } boolean isEqual = true; for (int i = 0; i < array1.length; i++) { if (array1[i] != array2[i]) { isEqual = false; } } return isEqual; } @Override public PasswordKey getPasswordKey(char[] password) throws CryptorException { Validate.notNull(password, "Password cannot be null."); Validate.isTrue(password.length > 0, "Password cannot be empty."); byte[] salt = new byte[SALT_LENGTH]; SECURE_RANDOM.nextBytes(salt); SecretKey secretKey = keyForPassword(password, salt); return new PasswordKey(secretKey, salt); } byte[] encryptData(byte[] plaintext, PasswordKey encryptionKey, PasswordKey hmacKey, byte[] iv) throws CryptorException { try { Cipher cipher = Cipher.getInstance(AES_CIPHER_ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, encryptionKey.getKey(), new IvParameterSpec(iv)); byte[] ciphertext = cipher.doFinal(plaintext); AES256v3Ciphertext output = new AES256v3Ciphertext(encryptionKey.getSalt(), hmacKey.getSalt(), iv, ciphertext); Mac mac = Mac.getInstance(HMAC_ALGORITHM); mac.init(hmacKey.getKey()); byte[] hmac = mac.doFinal(output.getDataToHMAC()); output.setHmac(hmac); return output.getRawData(); } catch (InvalidKeyException e) { throw new CryptorException( "Caught InvalidKeyException. Do you have unlimited strength jurisdiction files installed?", e); } catch (GeneralSecurityException e) { throw new CryptorException("Failed to generate ciphertext.", e); } } @Override public byte[] encryptData(byte[] plaintext, PasswordKey encryptionKey, PasswordKey hmacKey) throws CryptorException { byte[] iv = new byte[AES_BLOCK_SIZE]; SECURE_RANDOM.nextBytes(iv); return encryptData(plaintext, encryptionKey, hmacKey, iv); } }