How to perform secure 2-way password encryption in Fantom

  1. Overview
  2. AES Encryption
  3. Key Generation
  4. Unlimited Strength Jurisdiction Policy
  5. Encryption
  6. Decryption
  7. Full Example
  8. 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_iBFSgSY9C36aUQTR6QQ
secretKey := 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.

select all
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.

select all
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:

select all
crypto := Crypto()

secretKey := crypto.generateKey("Fanny the Fantom", "Escape the Mainframe")
echo(secretKey)  // --> g5_iBFSgSY9C36aUQTR6QQ

encode := crypto.encode(secretKey, "Hello Mum!")
echo(encode)     // --> dX_PCw_XxUrc-EEh_6Q9Yw::7mR_15i-koItcc8Nbt_xvxG5FPL6ayvrWApRfTRPUxo

decode := crypto.decode(secretKey, encode)
echo(decode)     // --> Hello Mum!

Example Crypto code:

select all
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 okay
        specClass   := 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 Cipher
        return 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 MemBuf
        Buf().writeBuf(Interop.toFan(ByteBuffer.wrap(array))).flip
    }
}

References

Edits

  • 10 April 2017 - Original article.

Discuss