Encryption_BASE64_AES_CBC_PKCS5Padding.java
/*
* Copyright 2019 Gregory Graham.
*
* Commercial licenses are available, please contact info@gregs.co.nz for details.
*
* This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
* To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/
* or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
*
* You are free to:
* Share - copy and redistribute the material in any medium or format
* Adapt - remix, transform, and build upon the material
*
* The licensor cannot revoke these freedoms as long as you follow the license terms.
* Under the following terms:
*
* Attribution -
* You must give appropriate credit, provide a link to the license, and indicate if changes were made.
* You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
* NonCommercial -
* You may not use the material for commercial purposes.
* ShareAlike -
* If you remix, transform, or build upon the material,
* you must distribute your contributions under the same license as the original.
* No additional restrictions -
* You may not apply legal terms or technological measures that legally restrict others from doing anything the
* license permits.
*
* Check the Creative Commons website for any details, legalese, and updates.
*/
package nz.co.gregs.dbvolution.utility.encryption;
import nz.co.gregs.dbvolution.exceptions.UnableToDecryptInput;
import nz.co.gregs.dbvolution.exceptions.CannotEncryptInputException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import nz.co.gregs.dbvolution.utility.Random;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.crypto.cipher.CryptoCipher;
import org.apache.commons.crypto.utils.Utils;
/**
*
* @author gregorygraham
*/
public class Encryption_BASE64_AES_CBC_PKCS5Padding {
private static final String TRANSFORM = "AES/CBC/PKCS5Padding";
private static final String ENCRYPTED_PREAMPLE = "BASE64_AES_CBC_PKCS5Padding";
public static String encrypt(String passphrase, String inputString) throws CannotEncryptInputException {
Properties properties = new Properties();
//Creates a CryptoCipher instance with the transformation and properties.
final int updateBytes;
final int finalBytes;
try ( CryptoCipher encipher = Utils.getCipherInstance(TRANSFORM, properties)) {
final byte[] utF8Bytes = getUTF8Bytes(inputString);
int bufferSize = utF8Bytes.length;
ByteBuffer inBuffer = ByteBuffer.allocateDirect(bufferSize);
ByteBuffer outBuffer = ByteBuffer.allocateDirect(bufferSize * 2);
inBuffer.put(utF8Bytes);
inBuffer.flip(); // ready for the cipher to read it
// Initializes the cipher with ENCRYPT_MODE,key and iv.
byte[] salt = makeSalt();
final SecretKey secretKeySpec = getSecretKeySpec(passphrase, salt);
final IvParameterSpec iv = getIV();
encipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, iv);
// Continues a multiple-part encryption/decryption operation for byte buffer.
updateBytes = encipher.update(inBuffer, outBuffer);
// We should call do final at the end of encryption/decryption.
finalBytes = encipher.doFinal(inBuffer, outBuffer);
outBuffer.flip(); // ready for use as decrypt
byte[] encoded = new byte[updateBytes + finalBytes];
outBuffer.duplicate().get(encoded);
final String base64Encoded = new String(Base64.encodeBase64(encoded), StandardCharsets.UTF_8);
final String base64Salt = new String(Base64.encodeBase64(salt));
final String base64IV = new String(Base64.encodeBase64(iv.getIV()));
return ENCRYPTED_PREAMPLE + "|" + base64Salt + "|" + base64IV + "|" + base64Encoded;
} catch (IOException | InvalidKeyException | InvalidAlgorithmParameterException | ShortBufferException | IllegalBlockSizeException | BadPaddingException | NoSuchAlgorithmException | InvalidKeySpecException ex) {
Logger.getLogger(Encryption_BASE64_AES_CBC_PKCS5Padding.class.getName()).log(Level.SEVERE, null, ex);
throw new CannotEncryptInputException(ex);
}
}
private static IvParameterSpec getIV() {
return getIV(Random.string(16));
}
private static IvParameterSpec getIV(String ivString) {
byte[] iv = getUTF8Bytes(ivString);
return getIV(iv);
}
private static IvParameterSpec getIV(byte[] iv) {
final IvParameterSpec ivParameterSpec = new IvParameterSpec(Arrays.copyOf(iv, iv.length));
return ivParameterSpec;
}
private static SecretKey getSecretKeySpec(String passphrase, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(passphrase.toCharArray(), salt, 65536, 256);
SecretKey tmp = factory.generateSecret(spec);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
return secret;
}
private static byte[] makeSalt() {
return Random.bytes(16);
}
public static String decrypt(String passphrase, String encryptedString) throws UnableToDecryptInput {
InterpretedString interpreted = InterpretedString.interpret(encryptedString);
if (interpreted.isEncryptedString()) {
Properties properties = new Properties();
// decode the base64 encoded input string
byte[] decodedBytes = Base64.decodeBase64(interpreted.encryptedPart);
int bufferSize = decodedBytes.length;
//Creates a CryptoCipher instance with the transformation and properties.
final ByteBuffer outBuffer = ByteBuffer.allocateDirect(bufferSize);
// push the decoded input into the buffer
outBuffer.put(decodedBytes);
// reverse the buffer to output mode for processing
outBuffer.flip();
try ( CryptoCipher decipher = Utils.getCipherInstance(TRANSFORM, properties)) {
final SecretKey secretKeySpec = getSecretKeySpec(passphrase, interpreted.getSalt());
final IvParameterSpec iv = getIV(interpreted.getIV());
decipher.init(Cipher.DECRYPT_MODE, secretKeySpec, iv);
ByteBuffer decoded = ByteBuffer.allocateDirect(bufferSize * 2);
decipher.update(outBuffer, decoded);
decipher.doFinal(outBuffer, decoded);
decoded.flip(); // ready for use
final String decrypted = asString(decoded);
return decrypted;
} catch (IOException | ShortBufferException | IllegalBlockSizeException | BadPaddingException | NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException | InvalidAlgorithmParameterException ex) {
Logger.getLogger(Encryption_BASE64_AES_CBC_PKCS5Padding.class.getName()).log(Level.SEVERE, null, ex);
throw new UnableToDecryptInput(ex);
}
} else {
throw new UnableToDecryptInput();
}
}
private static byte[] getUTF8Bytes(String input) {
return input.getBytes(StandardCharsets.UTF_8);
}
private static String asString(ByteBuffer buffer) {
final ByteBuffer copy = buffer.duplicate();
final byte[] bytes = new byte[copy.remaining()];
copy.get(bytes);
return new String(bytes, StandardCharsets.UTF_8);
}
private Encryption_BASE64_AES_CBC_PKCS5Padding() {
}
private static class InterpretedString {
private final boolean isGood;
private final byte[] salt;
private final byte[] iv;
private final String encryptedPart;
public InterpretedString(Boolean bool) {
this.isGood = false;
iv = null;
encryptedPart = null;
salt = null;
}
public InterpretedString(String preamble, byte[] salt, byte[] iv, String encryptedPart) {
this.isGood = true;
this.salt = salt;
this.iv = iv;
this.encryptedPart = encryptedPart;
}
private boolean isEncryptedString() {
return encryptedPart != null
&& !encryptedPart.isEmpty()
&& isGood;
}
private byte[] getIV() {
return iv;
}
private byte[] getSalt() {
return salt;
}
public static InterpretedString interpret(String encryptedString) {
if (encryptedString == null || encryptedString.isEmpty()) {
return new InterpretedString(null, null, null, null);
} else if (encryptedString.startsWith(ENCRYPTED_PREAMPLE)) {
String[] split = encryptedString.split("\\|", 4);
if (split.length == 4) {
return new InterpretedString(
split[0],
Base64.decodeBase64(split[1]),
Base64.decodeBase64(split[2]),
split[3]
);
}
}
return new InterpretedString(false);
}
}
}