I wrote a utility class that has simple methods for:
- En/decrypting data with 256-bit AES-GCM using a password
- Generating RSA keypair
- Saving RSA keypair and storing private key encrypted
En/decrypting data with RSA (data with AES-GCM & random key, key with RSA)
import javax.crypto.*; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; import java.io.FileOutputStream; import java.nio.file.Files; import java.nio.file.Paths; import java.security.*; import java.security.spec.*; import java.util.Arrays; public class GCM { private final int SALT_LENGTH = 256/8; private final int IV_LENGTH = 12; private final int KEY_LENGTH = 256; private final int GCM_TAG_LENGTH = 16; private KeyPair keyPair; public GCM() { } private SecretKeySpec generateKey(char[] password, byte[] salt) throws InvalidKeySpecException, NoSuchAlgorithmException { SecretKeyFactory factory = SecretKeyFactory.getInstance(/*"PBKDF2WithHmacSHA3-512"*/"PBKDF2WithHmacSHA256"); KeySpec spec = new PBEKeySpec(password, salt, 65536, KEY_LENGTH); SecretKey tmp = factory.generateSecret(spec); SecretKeySpec secretKeySpec = new SecretKeySpec(tmp.getEncoded(), "AES"); return secretKeySpec; } public void generateKeyPair() //generate 4096-bit RSA keypair throws NoSuchAlgorithmException { KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); kpg.initialize(4096, new SecureRandom()); keyPair = kpg.genKeyPair(); } public void loadKeyPair(char[] password) { //loads RSA keypair from file try { byte[] publicKeyBytes = Files.readAllBytes(Paths.get("public.key")); byte[] privateKeyBytes = decrypt(Files.readAllBytes(Paths.get("private.key")), password); X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec); PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes); PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec); this.keyPair = new KeyPair(publicKey, privateKey); } catch (Exception e) { e.printStackTrace(); } } public void saveKeyPair(char[] password) { //saves currently used keypair to file try { FileOutputStream fos = new FileOutputStream("private.key"); fos.write(encrypt(keyPair.getPrivate().getEncoded(), password)); fos.close(); fos = new FileOutputStream("public.key"); fos.write(keyPair.getPublic().getEncoded()); fos.close(); } catch (Exception e) { e.printStackTrace(); } } public byte[] getPublicKey() { //returns currently used public key return keyPair.getPublic().getEncoded(); } public byte[] encrypt(byte[] data, char[] password) { //encrypts data with password-generated key byte[] salt = new byte[SALT_LENGTH]; byte[] iv = new byte[IV_LENGTH]; byte[] result = null; new SecureRandom().nextBytes(salt); new SecureRandom().nextBytes(iv); try { SecretKeySpec secretKeySpec = generateKey(password, salt); byte[] encrypted = encrypt(data, secretKeySpec, iv); result = new byte[encrypted.length + salt.length + iv.length]; System.arraycopy(salt, 0, result, 0, salt.length); System.arraycopy(iv, 0, result, salt.length, iv.length); System.arraycopy(encrypted,0, result, salt.length + iv.length, encrypted.length); } catch (Exception e) { e.printStackTrace(); } return result; } public byte[] encrypt(byte[] data, byte[] publicKey) { //encrypts data with recipient public key byte[] iv = new byte[IV_LENGTH]; byte[] result = null; new SecureRandom().nextBytes(iv); try { KeyGenerator keyGen = KeyGenerator.getInstance("AES"); keyGen.init(256, new SecureRandom()); SecretKey secretKey = keyGen.generateKey(); SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), "AES"); byte[] encrypted = encrypt(data, secretKeySpec, iv); Cipher cipher = Cipher.getInstance("RSA/NONE/OAEPWithSHA3-512AndMGF1Padding"); X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKey); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE, keyFactory.generatePublic(keySpec)); byte[] symmetricKey = cipher.doFinal(secretKeySpec.getEncoded()); result = new byte[encrypted.length + iv.length + symmetricKey.length]; System.arraycopy(symmetricKey, 0, result, 0, symmetricKey.length); System.arraycopy(iv, 0, result, symmetricKey.length, iv.length); System.arraycopy(encrypted,0, result, iv.length + symmetricKey.length, encrypted.length); } catch (Exception e) { e.printStackTrace(); } return result; } private byte[] encrypt(byte[] data, SecretKeySpec secretKeySpec, byte[] iv) //encryption utility method throws BadPaddingException, IllegalBlockSizeException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException { Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv); cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmParameterSpec); return cipher.doFinal(data); } public byte[] decrypt(byte[] data, char[] password) { //decrypts data with password-generated key byte[] salt = Arrays.copyOfRange(data, 0, SALT_LENGTH); byte[] iv = Arrays.copyOfRange(data, SALT_LENGTH, SALT_LENGTH + IV_LENGTH); byte[] encryptedData = Arrays.copyOfRange(data, SALT_LENGTH + IV_LENGTH, data.length); byte[] result = null; try { SecretKeySpec secretKeySpec = generateKey(password, salt); result = decrypt(encryptedData, secretKeySpec, iv); } catch (Exception e) { e.printStackTrace(); } return result; } public byte[] decrypt(byte[] data) { //decrypts data with currently loaded private key byte[] symmetricKey = Arrays.copyOfRange(data, 0, 4096/8); byte[] iv = Arrays.copyOfRange(data, 4096/8, 4096/8 + IV_LENGTH); byte[] encryptedData = Arrays.copyOfRange(data, 4096/8 + IV_LENGTH, data.length); byte[] decrypted = null; try { Cipher cipher = Cipher.getInstance("RSA/NONE/OAEPWithSHA3-512AndMGF1Padding"); cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate()); byte[] decryptedSymmetricKey = cipher.doFinal(symmetricKey); SecretKeySpec secretKeySpec = new SecretKeySpec(decryptedSymmetricKey, "AES"); decrypted = decrypt(encryptedData, secretKeySpec, iv); } catch (Exception e) { e.printStackTrace(); } return decrypted; } private byte[] decrypt(byte[] data, SecretKeySpec secretKeySpec, byte[] iv) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { //decryption utility method Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv); cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, gcmParameterSpec); return cipher.doFinal(data); } }
The IV, and either RSA-encrypted symmetric key or salt used for key derivation are prepended to the output byte[].
It is used as follows:
GCM gcm = new GCM();
//symmetric encryption
byte[] encrypted = gcm.encrypt(byte[] data, char[] password);
byte[] decrypted = gcm.decrypt(byte[] encrypted, char[] password);
//asymmetric encryption
gcm.generateKeyPair();
gcm.saveKeyPair(char[] password);
gcm.loadKeyPair(char[] password);
byte[] encrypted = gcm.encrypt(byte[] data, byte[] publicKey);
byte[] decrypted = gcm.decrypt(byte[] data);
Since it is to be used in a closed system, compatibility isn't all that important (for example with key storage), but feel free to suggest alternatives.
Now I have the following questions about my code:
- Does it have any security flaws?
- Are AES-GCM's features used properly (eg. Authenticated Encryption)?
- Is the IV generation random enough to use with AES-GCM?
- Does it follow best practices?