/** * Copyright (c) 2012, Lindsay Bradford and other Contributors. * All rights reserved. * * This program and the accompanying materials are made available * under the terms of the BSD 3-Clause licence which accompanies * this distribution, and is available at * http://opensource.org/licenses/BSD-3-Clause */ package blacksmyth.personalfinancier.dependencies.encryption; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.Security; import java.security.spec.InvalidKeySpecException; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import org.bouncycastle.crypto.DataLengthException; import org.bouncycastle.crypto.InvalidCipherTextException; import org.bouncycastle.crypto.engines.AESEngine; import org.bouncycastle.crypto.modes.CBCBlockCipher; import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.crypto.params.ParametersWithIV; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.pqc.math.linearalgebra.ByteUtils; import blacksmyth.general.ByteUtilities; /** * A class implementing the 'Concrete Implementor class' of the Bridge pattern, allowing a bridge * of all needed functionality between the application and the open-source security provider * BouncyCastle (https://www.bouncycastle.org/) */ final class BouncyCastleEncryptionBridge implements IEncryptionBridge { private static PaddedBufferedBlockCipher CIPHER; /** * Implemented as a singleton to ensure we register the BouncyCastle * security provider only once. */ private static BouncyCastleEncryptionBridge instance; public static BouncyCastleEncryptionBridge getInstance() { if (instance == null) { instance = new BouncyCastleEncryptionBridge(); } return instance; } protected BouncyCastleEncryptionBridge() { Security.addProvider( new BouncyCastleProvider() ); CIPHER = AESBuilder.buildCipher(); } @Override public String encrypt(char[] password, String content) { return ByteUtils.toHexString( encrypt( password, ByteUtilities.StringToBytes( content ) ) ); } @Override public byte[] encrypt(char[] password, byte[] content) { AESElements e = AESBuilder.buildElements(password); byte[] encryptedContent = processEncryption(e, content); byte[] header = ByteUtils.concatenate(e.salt, e.iv); return ByteUtils.concatenate(header, encryptedContent); } private byte[] processEncryption(AESElements e, byte[] content) { ParametersWithIV parameterIV = new ParametersWithIV(new KeyParameter(e.key),e.iv); /* System.out.println("Encrypt salt: " + ByteUtils.toHexString(e.salt)); System.out.println("Encrypt key: " + ByteUtils.toHexString(e.key)); System.out.println("Encrypt IV: " + ByteUtils.toHexString(e.iv)); */ CIPHER.init(true, parameterIV); return processContent(content); } @Override public String decrypt(char[] password, String content) { return ByteUtilities.BytesToString( decrypt( password, ByteUtils.fromHexString( content ) ) ); } @Override public byte[] decrypt(char[] password, byte[] content) { AESElements e = new AESElements(); e.salt = ByteUtils.subArray(content, 0, AESBuilder.SALT_BYTES); e.iv = ByteUtils.subArray(content, AESBuilder.SALT_BYTES, AESBuilder.SALT_BYTES + AESBuilder.IV_BYTES); byte[] encryptedContent = ByteUtils.subArray(content, AESBuilder.SALT_BYTES + AESBuilder.IV_BYTES); try { e.key = AESBuilder.buildKey(password, e.salt); } catch (Exception ex) { return null; } byte[] decryptedContent = processDecryption(e,encryptedContent); if (decryptedContent == null) { return null; } return ByteUtilities.TrimBytes(decryptedContent); } private byte[] processDecryption(AESElements e, byte[] encryptedContent) { ParametersWithIV parameterIV = new ParametersWithIV(new KeyParameter(e.key),e.iv); /* System.out.println("Decrypt salt: " + ByteUtils.toHexString(e.salt)); System.out.println("Decrypt key: " + ByteUtils.toHexString(e.key)); System.out.println("Decrypt IV: " + ByteUtils.toHexString(e.iv)); */ CIPHER.init(false, parameterIV); return processContent(encryptedContent); } private byte[] processContent(byte[] content) { byte[] processedBytes = new byte[CIPHER.getOutputSize(content.length)]; int numProcessed = CIPHER.processBytes(content, 0, content.length, processedBytes, 0); try { CIPHER.doFinal(processedBytes, numProcessed); } catch (DataLengthException e) { return null; } catch (IllegalStateException e) { return null; } catch (InvalidCipherTextException e) { return null; } return processedBytes; } @Override public boolean encryptionAvailable() { return true; } } /** * Key salting and hashing based loosely on discussion here: * https://crackstation.net/hashing-security.htm#javasourcecode */ final class AESBuilder { private static SecureRandom RANDOM = generateRANDOM(); public static final int BLOCK_BITS = 128; public static final int BLOCK_BYTES = BLOCK_BITS / Byte.SIZE; public static final int IV_BYTES = BLOCK_BYTES;; public static final int KEY_BITS = 256; public static final int KEY_BYTES = KEY_BITS / Byte.SIZE; private static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1"; public static final int SALT_BYTES = 32; public static final int HASH_ITERATIONS = 5000; private static SecureRandom generateRANDOM() { SecureRandom sr = new SecureRandom(); sr.setSeed( System.currentTimeMillis() ); return sr; } /** * Returns an AES block ciphers in CBC mode with padding * @return AES padded buffered block cipher */ public static PaddedBufferedBlockCipher buildCipher() { return new PaddedBufferedBlockCipher( new CBCBlockCipher( new AESEngine() ) ); } /** * Builds the various random elements needed for encrypting and decryypting * content using AES encryption with a user-supplied password. * @param password * @return */ public static AESElements buildElements(char[] password) { try { AESElements elements = buildKeyAndSalt(password); elements.iv = buildIV(); return elements; } catch (Exception e) { return null; } } private static AESElements buildKeyAndSalt(char[] password) throws NoSuchAlgorithmException, InvalidKeySpecException { AESElements e = new AESElements(); e.salt = buildSalt(); e.key = buildKey(password, e.salt); return e; } /** * Builds a hashed key from the supplied password and salt. * @param password * @param salt * @return * @throws NoSuchAlgorithmException * @throws InvalidKeySpecException */ public static byte[] buildKey(char[] password, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException { PBEKeySpec spec = new PBEKeySpec(password, salt, HASH_ITERATIONS, KEY_BYTES * 8); SecretKeyFactory skf = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM); return skf.generateSecret(spec).getEncoded(); } private static byte[] buildIV() { return buildRandomByteArray(BLOCK_BYTES); } private static byte[] buildSalt() { return buildRandomByteArray(SALT_BYTES); } private static byte[] buildRandomByteArray(int arraySize) { byte[] array = new byte[arraySize]; RANDOM.nextBytes(array); return array; } } /** * Convenience state storage for all needed random AES elements. */ final class AESElements { public byte[] salt; // salt as byte-array used to hash a password. Said hash becomes the AES key. public byte[] key; // AES key as byte-array public byte[] iv; // AES Initialisation Vector as a byte-array }