How to perform secure 2-way password encryption in Fantom
- Overview
- AES Encryption
- Key Generation
- Unlimited Strength Jurisdiction Policy
- Encryption
- Decryption
- Full Example
- References
Overview
If you're managing passwords then usually the first rule is, don't manage passwords! (Keep a secure one-way hash instead.) If in doubt, this great little article tells it all in simple terms:
3 Wrong Ways to Store a Password
But some times, you just need to keep hold of the original password.
For instance, if you're acting as the middle man and need to send the password up to a remote server to login to it; as I did recently.
I decided to keep the password in a plain properties file for easy access, but I didn't want it stored as plain text for casual prying eyes. Hmmm....
So enter 2 way password encryption!
AES Encryption
Advanced Encryption Standard (AES) Encryption is pretty good, and certainly good enough for what I wanted to use it for.
Key Generation
AES requires a secret key. The more secure the secret key, the more secure the encryption.
A lot of code resources tell you how encrypt / decrypt text, but not many explain how to generate a decent secret key! It turns out that the Password-Based Key Derivation Function #2 is way to go, requiring a pass phrase and a salt:
Str generateKey(Str passPhrase, Str salt) { noOfBits := 128 noOfBytes := noOfBits / 8 iterations := 0x10000 return Buf.pbk("PBKDF2WithHmacSHA256", passPhrase, Buf().print(salt), iterations, noOfBytes).toBase64Uri }// --> g5_iBFSgSY9C36aUQTR6QQsecretKey := generateKey("Fanny the Fantom", "Escape the Mainframe")
We pass a plain text string in for the salt. A better salt would be a securely generated random binary number (at least 8 bytes) - but a string is easier to remember! (Note you need to pass in the same pass phrase and the same salt to generate the same secret key.)
Note the large iteration count above of 0x10000
/ 65536
. This is how many times the key derivation algorithm is run; the larger the number, the more computational effort it takes to generate the key. This is a good thing as it prevents attackers from quickly trying many different passwords.
And then there is the generated key size - 128 bits. This is how long the returned secret key will be. From this we will have 128 bit AES encryption; which is pretty strong.
"Pah!" I hear you say. "I want a 256 bit key and 256 bit AES encryption!"
Well, you could. There's nothing stopping you... except for the:
Unlimited Strength Jurisdiction Policy
It turns out that US export laws prohibit Oracle from selling strong encryption. For your own comfort and safety, the government don't want Johnny Foreigner hiding their secrets. If you attempt to perform 256 bit AES encryption with a standard Java installation you'll receive a non-descript error of:
java.security.InvalidKeyException: Illegal key size
To get round it you need to install the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy. It's freely available (see References below) but it's pain to do so and greatly restricts the distribution of your Java / Fantom program.
To play it safe, I opt to just using strong 128 bit AES encryption.
Note I don't believe this is an issue if using the free OpenJDK, just Oracle's JRE.
Encryption
Interestingly, AES encryption produces 2 outputs:
- the cipher text, and
- an initialisation vector
Initialisation vector is just a fancy word for seed, or setup parameters. It sets up AES so it can decrypt the cipher text. Note that both are required to decrypt the cipher text.
It is safe to distribute the initialisation vector, as it (and the cipher text) are useless without the original secret key. So I just base64 the two numbers and concatenate them with ::
characters. That way it's easy to separate them when it comes to decrypting.
using [java] fanx.interop::Interop using [java] fanx.interop::ByteArray using [java] java.lang::Class using [java] java.nio::ByteBuffer using [java] java.security::AlgorithmParameters using [java] javax.crypto::Cipher using [java] javax.crypto.spec::IvParameterSpec using [java] javax.crypto.spec::SecretKeySpec ... Str encode(Str secretKey, Str msg) { keyBuf := Buf.fromBase64(secretKey) keySpec := SecretKeySpec(toBytes(keyBuf), "AES") cipher := Cipher.getInstance("AES/CBC/PKCS5Padding") cipher.init(Cipher.ENCRYPT_MODE, keySpec) specClass := Class.forName("javax.crypto.spec.IvParameterSpec") initVector := ((IvParameterSpec) AlgorithmParameters#getParameterSpec.call(cipher.getParameters, specClass)).getIV cipherText := cipher.doFinal(toBytes(msg.toBuf)) return toBuf(initVector).toBase64Uri + "::" + toBuf(cipherText).toBase64Uri } private static ByteArray toBytes(Buf buf) { Interop.toJava(buf).array } private static Buf toBuf(ByteArray array) { Buf().writeBuf(Interop.toFan(ByteBuffer.wrap(array))).flip }
Decryption
To decrypt, we then just separate the initialisation vector from the actual cipher text.
using [java] fanx.interop::Interop using [java] fanx.interop::ByteArray using [java] java.nio::ByteBuffer using [java] javax.crypto::Cipher using [java] javax.crypto.spec::IvParameterSpec using [java] javax.crypto.spec::SecretKeySpec Str decode(Str secretKey, Str encoded) { keyBuf := Buf.fromBase64(secretKey) initVector := encoded[0..<encoded.index("::")] cipherText := encoded[encoded.index("::")+2..-1] keySpec := SecretKeySpec(toBytes(keyBuf), "AES") cipher := Cipher.getInstance("AES/CBC/PKCS5Padding") ivSpec := IvParameterSpec(toBytes(Buf.fromBase64(initVector))) cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec) plainText := cipher.doFinal(toBytes(Buf.fromBase64(cipherText))) return toBuf(plainText).readAllStr.trim } private static ByteArray toBytes(Buf buf) { Interop.toJava(buf).array } private static Buf toBuf(ByteArray array) { Buf().writeBuf(Interop.toFan(ByteBuffer.wrap(array))).flip }
And that's all you need for 2 way encryption!
Full Example
Example usage:
crypto := Crypto() secretKey := crypto.generateKey("Fanny the Fantom", "Escape the Mainframe") echo(secretKey)// --> g5_iBFSgSY9C36aUQTR6QQencode := crypto.encode(secretKey, "Hello Mum!") echo(encode)// --> dX_PCw_XxUrc-EEh_6Q9Yw::7mR_15i-koItcc8Nbt_xvxG5FPL6ayvrWApRfTRPUxodecode := crypto.decode(secretKey, encode) echo(decode)// --> Hello Mum!
Example Crypto code:
using [java] fanx.interop::Interop using [java] fanx.interop::ByteArray using [java] java.lang::Class using [java] java.nio::ByteBuffer using [java] java.security::AlgorithmParameters using [java] javax.crypto::Cipher using [java] javax.crypto.spec::IvParameterSpec using [java] javax.crypto.spec::SecretKeySpec const class Crypto { Str secretKey(Str passPhrase, Str salt) { noOfBits := 128 noOfBytes := noOfBits / 8 iterations := 0x10000 return Buf.pbk("PBKDF2WithHmacSHA256", passPhrase, Buf().print(salt), iterations, noOfBytes).toBase64Uri } Str encode(Str secretKey, Str msg) { keyBuf := Buf.fromBase64(secretKey) keySpec := SecretKeySpec(toBytes(keyBuf), "AES") cipher := Cipher.getInstance("AES/CBC/PKCS5Padding") cipher.init(Cipher.ENCRYPT_MODE, keySpec)// getParameterSpec() has some knarly Java generics which I can't figure out how to create in Fantom : "<T extends AlgorithmParameterSpec>"// Note, java.lang.Class.asSubclass() does seem to work - maybe 'cos Fantom then assigns to a general 'Class' obj// Anyway, just invoke it via reflection and all is okayspecClass := Class.forName("javax.crypto.spec.IvParameterSpec") initVector := ((IvParameterSpec) AlgorithmParameters#getParameterSpec.call(cipher.getParameters, specClass)).getIV cipherText := cipher.doFinal(toBytes(msg.toBuf))// note the initVector is the same for repeated calls to getParameterSpec() but different for each instance of Cipherreturn toBuf(initVector).toBase64Uri + "::" + toBuf(cipherText).toBase64Uri } Str decode(Str secretKey, Str encoded) { keyBuf := Buf.fromBase64(secretKey) initVector := encoded[0..<encoded.index("::")] cipherText := encoded[encoded.index("::")+2..-1] keySpec := SecretKeySpec(toBytes(keyBuf), "AES") cipher := Cipher.getInstance("AES/CBC/PKCS5Padding") ivSpec := IvParameterSpec(toBytes(Buf.fromBase64(initVector))) cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec) plainText := cipher.doFinal(toBytes(Buf.fromBase64(cipherText))) return toBuf(plainText).readAllStr.trim } private static ByteArray toBytes(Buf buf) { Interop.toJava(buf).array } private static Buf toBuf(ByteArray array) {// we can't base64 a NioBuf, so copy the contents to a standard Fantom MemBufBuf().writeBuf(Interop.toFan(ByteBuffer.wrap(array))).flip } }
References
- 3 Wrong Ways to Store a Password by Adam Bard
- Java 256-bit AES Password-Based Encryption on StackOverflow
- Advanced Encryption Standard on Wikipedia
- (JCE) Unlimited Strength Jurisdiction Policy Files for Java 6 on Oracle
- (JCE) Unlimited Strength Jurisdiction Policy Files for Java 7 on Oracle
- (JCE) Unlimited Strength Jurisdiction Policy Files for Java 8 on Oracle
Edits
- 10 April 2017 - Original article.