I spent quite some time and wrote a single class for the purpose of encrypting/decrypting plaintext bytes with a passphrase. The goal is to not have any further dependencies apart from java.*
and javax.*
.
It shall provide confidentiality and integrity for encrypted plaintext in the order of kilobytes and should be indistinguishable under a chosen plaintext attack (IND-CPA) (apart from the leading constant byte).
package cryptor;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
/**
* @author trichner
* @created 19.02.18
*/
public class AesGcmCryptor {
// https://crypto.stackexchange.com/questions/26783/ciphertext-and-tag-size-and-iv-transmission-with-aes-in-gcm-mode
private static final byte[] VERSION_BYTE = new byte[] { (byte) 0x01 };
private static final int AES_KEY_BITS_LENGTH = 128;
private static final int GCM_IV_BYTES_LENGTH = 12;
private static final int GCM_TAG_BYTES_LENGTH = 16;
private static final int PBKDF2_ITERATIONS = 16384;
private static final byte[] PBKDF2_SALT = hexStringToByteArray("4d3fe0d71d2abd2828e7a3196ea450d4");
/**
* Decrypts an AES-GCM encrypted ciphertext and is
* the reverse operation of {@link AesGcmCryptor#encrypt(char[], byte[])}
*
* @param password passphrase for decryption
* @param ciphertext encrypted bytes
*
* @return plaintext bytes
*
* @throws NoSuchPaddingException
* @throws NoSuchAlgorithmException
* @throws NoSuchProviderException
* @throws InvalidKeySpecException
* @throws InvalidAlgorithmParameterException
* @throws InvalidKeyException
* @throws BadPaddingException
* @throws IllegalBlockSizeException
* @throws IllegalArgumentException if the length or format of the ciphertext is bad
*/
public byte[] decrypt(char[] password, byte[] ciphertext)
throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException,
InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException,
BadVersionException {
// input validation
if (ciphertext == null) {
throw new IllegalArgumentException("Ciphertext cannot be null.");
}
if (ciphertext.length <= VERSION_BYTE.length + GCM_IV_BYTES_LENGTH + GCM_TAG_BYTES_LENGTH) {
throw new IllegalArgumentException("Ciphertext too short.");
}
// The version byte must have a 0 MSB in this version,
// this allows us to expand the header to multiple bytes if ever necessary.
// The MSB indicates if the current octet is the last octet of the header.
if ((ciphertext[0] & (1 << 7)) != 0) {
throw new BadVersionException();
}
// The version must match.
for (int i = 0; i < VERSION_BYTE.length; i++) {
if (VERSION_BYTE[i] != ciphertext[i]) {
throw new BadVersionException();
}
}
// input seems legit, lets decrypt and check integrity
// derive key from password
SecretKey key = deriveAesKey(password, PBKDF2_SALT, AES_KEY_BITS_LENGTH);
// init cipher
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "SunJCE");
GCMParameterSpec params = new GCMParameterSpec(GCM_TAG_BYTES_LENGTH * 8,
ciphertext,
VERSION_BYTE.length,
GCM_IV_BYTES_LENGTH
);
cipher.init(Cipher.DECRYPT_MODE, key, params);
// add version and IV to MAC
cipher.updateAAD(ciphertext, 0, GCM_IV_BYTES_LENGTH + VERSION_BYTE.length);
// decipher and check MAC
return cipher.doFinal(ciphertext, 13, ciphertext.length - GCM_IV_BYTES_LENGTH - VERSION_BYTE.length);
}
/**
* Encrypts a plaintext with a password.
*
* The encryption provides the following security properties:
* Confidentiality + Integrity
*
* This is achieved my using the AES-GCM AEAD blockmode with a randomized IV.
*
* The tag is calculated over the version byte, the IV as well as the ciphertext.
*
* Finally the encrypted bytes have the following structure:
* <pre>
* +-------------------------------------------------------------------+
* | | | | |
* | version | IV bytes | ciphertext bytes | tag |
* | | | | |
* +-------------------------------------------------------------------+
* Length: 1B 12B len(plaintext) bytes 16B
* </pre>
* Note: There is no padding required for AES-GCM, but this also implies that
* the exact plaintext length is revealed.
*
* @param password password to use for encryption
* @param plaintext plaintext to encrypt
*
* @throws NoSuchAlgorithmException
* @throws NoSuchProviderException
* @throws NoSuchPaddingException
* @throws InvalidAlgorithmParameterException
* @throws InvalidKeyException
* @throws BadPaddingException
* @throws IllegalBlockSizeException
* @throws InvalidKeySpecException
*/
public byte[] encrypt(char[] password, byte[] plaintext)
throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException,
InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException,
InvalidKeySpecException {
// initialise random and generate IV (initialisation vector)
SecretKey key = deriveAesKey(password, PBKDF2_SALT, AES_KEY_BITS_LENGTH);
final byte[] iv = new byte[GCM_IV_BYTES_LENGTH];
SecureRandom random = SecureRandom.getInstanceStrong();
random.nextBytes(iv);
// encrypt
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "SunJCE");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_BYTES_LENGTH * 8, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
// add IV to MAC
cipher.updateAAD(VERSION_BYTE);
cipher.updateAAD(iv);
// encrypt and MAC plaintext
byte[] ciphertext = cipher.doFinal(plaintext);
// prepend VERSION and IV to ciphertext
byte[] encrypted = new byte[1 + GCM_IV_BYTES_LENGTH + ciphertext.length];
int pos = 0;
System.arraycopy(VERSION_BYTE, 0, encrypted, 0, VERSION_BYTE.length);
pos += VERSION_BYTE.length;
System.arraycopy(iv, 0, encrypted, pos, iv.length);
pos += iv.length;
System.arraycopy(ciphertext, 0, encrypted, pos, ciphertext.length);
return encrypted;
}
/**
* We derive a fixed length AES key with uniform entropy from a provided
* passphrase. This is done with PBKDF2/HMAC256 with a fixed count
* of iterations and a provided salt.
*
* @param password passphrase to derive key from
* @param salt salt for PBKDF2 if possible use a per-key salt, alternatively
* a random constant salt is better than no salt.
* @param keyLen number of key bits to output
*
* @return a SecretKey for AES derived from a passphrase
*
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
*/
private SecretKey deriveAesKey(char[] password, byte[] salt, int keyLen)
throws NoSuchAlgorithmException, InvalidKeySpecException {
if (password == null || salt == null || keyLen <= 0) {
throw new IllegalArgumentException();
}
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password, salt, PBKDF2_ITERATIONS, keyLen);
SecretKey pbeKey = factory.generateSecret(spec);
return new SecretKeySpec(pbeKey.getEncoded(), "AES");
}
/**
* Helper to convert hex strings to bytes.
*
* This is neither null save nor does it go well with invalid hex strings.
* Therefore it is important that this method is not used with user provided strings.
*/
private static byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len - 1; i++) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
}
return data;
}
}
Why not use a library?
- Bouncy Castle is a huge dependency for simple encryption
- Jasypt might have an easy API, but I couldn't even find out if it provides integrity or indistinguishability under chosen plaintext attack
Why not copy something from Stackoverflow?
- All encryption related code I found on Stackoverlow provided either no integrity at all or was broken in some way or another, e.g. not using a KDF for the password, not checking the integrity of the IV, ...
EDIT
I fixed some minor issues, you may find the entire source here: https://github.com/trichner/tcrypt