diff --git a/src/main/java/net/knarcraft/bookswithoutborders/encryption/AES.java b/src/main/java/net/knarcraft/bookswithoutborders/encryption/AES.java new file mode 100644 index 0000000..6f3f6f5 --- /dev/null +++ b/src/main/java/net/knarcraft/bookswithoutborders/encryption/AES.java @@ -0,0 +1,159 @@ +package net.knarcraft.bookswithoutborders.encryption; + +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.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.util.Base64; + +/** + * This class represents and AES encryptor/decryptor + * + *
The AES encryptor/decryptor can encrypt/decrypt strings using a supplied password. It has a function for + * generating the IV, if a new one is necessary.
+ * + * @author EpicKnarvik97 + */ +public class AES { + + //TODO: Generate salt for each installation, and figure out what to to with the iv parameter + private final IvParameterSpec ivParameterSpec; + private final byte[] passwordSalt; + + /** + * Instantiates a new AES encryptor + * @param initializationVectorThe initialization vector to use for CBC
+ * @param passwordSaltThe password salt to use
+ */ + public AES(byte[] initializationVector, byte[] passwordSalt) { + this.ivParameterSpec = new IvParameterSpec(initializationVector); + this.passwordSalt = passwordSalt; + } + + /** + * Encrypts or decrypts the given text + * @param inputThe input to encrypt or decrypt
+ * @param passwordThe password to use for key generation
+ * @param encryptWhether to encrypt or decrypt the input
+ * @returnThe encrypted/decrypted input, or null if anything went wrong
+ */ + public String encryptDecryptText(String input, String password, boolean encrypt) { + //Make a key from the password + SecretKeySpec secretKeySpec = getKeyFromPassword(password); + //Get cipher instance + Cipher aes = getAESCipher(); + if (aes == null || secretKeySpec == null) { + return null; + } + int mode; + if (encrypt) { + mode = Cipher.ENCRYPT_MODE; + } else { + mode = Cipher.DECRYPT_MODE; + } + //Initialize cipher + try { + aes.init(mode, secretKeySpec, ivParameterSpec); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + e.printStackTrace(); + return null; + } + //Perform encryption/decryption and output result + try { + byte[] output = aes.doFinal(getInputBytes(input, encrypt)); + return createResult(output, encrypt); + } catch (IllegalBlockSizeException | BadPaddingException e) { + e.printStackTrace(); + return null; + } + } + + /** + * Generates a 16-byte initialization vector + * @returnAn initialization vector
+ */ + public static byte[] generateIV() { + byte[] initializationVector = new byte[16]; + SecureRandom secureRandom = new SecureRandom(); + secureRandom.nextBytes(initializationVector); + return initializationVector; + } + + /** + * Transforms the input string into bytes + * @param inputThe input to encrypt or decrypt
+ * @param encryptionWhether the input should be encrypted or decrypted
+ * @returnThe input in byte format
+ */ + private byte[] getInputBytes(String input, boolean encryption) { + if (encryption) { + return input.getBytes(); + } else { + return Base64.getDecoder().decode(input); + } + } + + /** + * Transforms the result bytes into a string + * @param outputThe output from encryption or decryption
+ * @param encryptionWhether the output came from encryption or decryption
+ * @returnThe output as a string
+ */ + private String createResult(byte[] output, boolean encryption) { + if (encryption) { + return Base64.getEncoder().encodeToString(output); + } else { + return new String(output); + } + } + + /** + * Gets an AES cipher instance + * @returnAn AES cipher instance, or null if something went wrong
+ */ + private Cipher getAESCipher() { + Cipher aes; + try { + aes = Cipher.getInstance("AES/CBC/PKCS5Padding"); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + e.printStackTrace(); + return null; + } + return aes; + } + + /** + * Gets an encryption key from a supplied password + * @param passwordA user supplied password
+ * @returnA secret key spec or null if something went wrong
+ */ + private SecretKeySpec getKeyFromPassword(String password) { + PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), this.passwordSalt, 1000, 128); + SecretKeyFactory keyFactory; + try { + keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + return null; + } + SecretKey tmp; + try { + tmp = keyFactory.generateSecret(spec); + } catch (InvalidKeySpecException e) { + e.printStackTrace(); + return null; + } + return new SecretKeySpec(tmp.getEncoded(), "AES"); + } + +} diff --git a/src/test/java/net/knarcraft/bookswithoutborders/encryption/AESTest.java b/src/test/java/net/knarcraft/bookswithoutborders/encryption/AESTest.java new file mode 100644 index 0000000..d44b73d --- /dev/null +++ b/src/test/java/net/knarcraft/bookswithoutborders/encryption/AESTest.java @@ -0,0 +1,23 @@ +package net.knarcraft.bookswithoutborders.encryption; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class AESTest { + + @Test + public void encryptDecryptTest() { + String plainText = "A lot of text"; + String password = "abc123"; + + AES aes = new AES(AES.generateIV(), AES.generateIV()); + + String encrypted = aes.encryptDecryptText(plainText, password, true); + assertFalse(encrypted.equals(plainText)); + String decrypted = aes.encryptDecryptText(encrypted, password, false); + assertEquals(plainText, decrypted); + } + +}