Encryption_BASE64_AES_GCM_NoPadding.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.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 javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.spec.GCMParameterSpec;
import java.util.Base64;
import java.util.logging.Level;
import java.util.logging.Logger;
import nz.co.gregs.dbvolution.utility.Random;
/**
*
* @author gregorygraham
*/
public class Encryption_BASE64_AES_GCM_NoPadding {
private static final String ENCRYPTED_PREAMPLE = "BASE64_AES/GCM/NoPadding";
private final static String ALGORITHM_NAME = "AES/GCM/NoPadding";
private final static int ALGORITHM_NONCE_SIZE = 12;
private final static int ALGORITHM_TAG_SIZE = 128;
private final static int ALGORITHM_KEY_SIZE = 128;
private final static String PBKDF2_NAME = "PBKDF2WithHmacSHA256";
private final static int PBKDF2_SALT_SIZE = 16;
private final static int PBKDF2_ITERATIONS = 32767;
public static String encrypt(String password, String plaintext) throws CannotEncryptInputException {
try {
// make a salt
byte[] salt = Random.bytes(PBKDF2_SALT_SIZE);
// Create an instance of PBKDF2 and derive a key.
PBEKeySpec pwSpec = new PBEKeySpec(password.toCharArray(), salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(PBKDF2_NAME);
byte[] key = keyFactory.generateSecret(pwSpec).getEncoded();
// make a nonce
byte[] nonce = Random.bytes(ALGORITHM_NONCE_SIZE);
// Create the cipher instance and initialize.
Cipher cipher = getEncryptCipher(key, nonce);
// Encrypt and prepend nonce.
byte[] ciphertext = cipher.doFinal(getUTF8Bytes(plaintext));
// Return as base64 string.
return ENCRYPTED_PREAMPLE + "|" + getBase64String(salt) + "|" + getBase64String(nonce) + "|" + getBase64String(ciphertext);
} catch (InvalidAlgorithmParameterException | InvalidKeyException | NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException | NoSuchAlgorithmException | InvalidKeySpecException ex) {
Logger.getLogger(Encryption_BASE64_AES_GCM_NoPadding.class.getName()).log(Level.SEVERE, null, ex);
throw new CannotEncryptInputException(ex);
}
}
private static SecretKeySpec getSecretKeySpec(byte[] key) {
return new SecretKeySpec(key, "AES");
}
private static GCMParameterSpec getParameterSpec(byte[] nonce) {
return new GCMParameterSpec(ALGORITHM_TAG_SIZE, nonce);
}
public static String getBase64String(byte[] bytes) {
return new String(Base64.getEncoder().encode(bytes), StandardCharsets.UTF_8);
}
public static byte[] getBytesFromBase64String(String base64String) {
return Base64.getDecoder().decode(base64String);
}
private static byte[] getSecretKey(String passphrase, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException {
// Create an instance of PBKDF2 and derive the key.
PBEKeySpec pwSpec = new PBEKeySpec(passphrase.toCharArray(), salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(PBKDF2_NAME);
byte[] key = keyFactory.generateSecret(pwSpec).getEncoded();
return key;
}
public static String decrypt(String passphrase, String encryptedString) throws UnableToDecryptInput {
InterpretedString interpreted = InterpretedString.interpret(encryptedString);
if (interpreted.isEncryptedString()) {
try {
// Retrieve the salt, nonce, and ciphertext.
byte[] salt = interpreted.salt;
byte[] nonce = interpreted.nonce;
String ciphertext = interpreted.encryptedPart;
// Create an instance of PBKDF2 and derive the key.
byte[] key = getSecretKey(passphrase, salt);
Cipher cipher = getDecryptCipher(key, nonce);
// Decrypt and return result.
final byte[] finalText = cipher.doFinal(getBytesFromBase64String(ciphertext));
return new String(finalText, StandardCharsets.UTF_8);
} catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException | NoSuchPaddingException ex) {
Logger.getLogger(Encryption_BASE64_AES_GCM_NoPadding.class.getName()).log(Level.SEVERE, null, ex);
throw new UnableToDecryptInput(ex);
}
} else {
throw new UnableToDecryptInput();
}
}
private static Cipher getEncryptCipher(byte[] key, byte[] nonce) throws NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException, NoSuchPaddingException {
return getCipher(Cipher.ENCRYPT_MODE, key, nonce);
}
private static Cipher getDecryptCipher(byte[] key, byte[] nonce) throws NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException, NoSuchPaddingException {
return getCipher(Cipher.DECRYPT_MODE, key, nonce);
}
private static Cipher getCipher(int mode, byte[] key, byte[] nonce) throws NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException, NoSuchPaddingException {
Cipher cipher = Cipher.getInstance(ALGORITHM_NAME);
cipher.init(mode, getSecretKeySpec(key), getParameterSpec(nonce));
return cipher;
}
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_GCM_NoPadding() {
}
private static class InterpretedString {
private final boolean isGood;
private final String preamble;
private final byte[] salt;
private final byte[] nonce;
private final String encryptedPart;
public InterpretedString(Boolean bool) {
this.isGood = false;
preamble = null;
nonce = null;
encryptedPart = null;
salt = null;
}
public InterpretedString(String preamble, byte[] salt, byte[] nonce, String encryptedPart) {
this.isGood = true;
this.preamble = preamble;
this.salt = salt;
this.nonce = nonce;
this.encryptedPart = encryptedPart;
}
private boolean isEncryptedString() {
return encryptedPart != null
&& !encryptedPart.isEmpty()
&& isGood;
}
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) {
final Base64.Decoder decoder = Base64.getDecoder();
return new InterpretedString(
split[0],
decoder.decode(split[1]),
decoder.decode(split[2]),
split[3]
);
}
}
return new InterpretedString(false);
}
}
}