AES GCM timed encryption with Elixir
While developing elixir web apps sometimes we’ll find the necessity of generating encrypted tokens, these can be used in transactional emails (account confirmation, password resets, etc.) or for other purposes. While there are packages out there than can help us with this, I wanted to explore more in dept what are the bare options:
Cryptography in Elixir
While I’m no expert in cryptography by any means, it’s always a good idea to know what our options are in this matter, fortunately in Elixir we don’t have to reinvent the wheel (which is always a bad idea when dealing with security concerns), the Beam includes a very helpful module called crypto.
If you’re not used to Erlang, it’s documentation may seem a little fuzzy when compared to Elixir’s. crypto offers us helpful functions to work with different hashing and encrypting functions that go from MD5, DES, AES, RSA and others.
For the purpose of this experiment we’ll use the AES (Advanced Encryption Standard), while for simple experimenting DES could help, it’s not considered secure. So we’ll go the AES in GCM (Galois Counter Mode) way, you can learn more about it here.
The implementation
Let’s get started, create a new application:
Since we want to make tokens with an expiration time, let’s add Timex as a dependency to work comfortably with dates and time.
Install it with mix deps.get
and we’re all set to start our
implementation.
As we’re using symmetric encryption we need a single key with which we can encrypt and decrypt our messages, AES allows us to use 128, 192 or 256 bits keys, we’ll create 256 bits keys. Open your Crypto module and add the following function:
Here we’re using Erlang’s crypto module to generate a 256 bits binary and then encoding it with base 64 so it’s more user friendly.
Now we can easily generate keys by running Crypto.generate_key()
on an iex
session.
We can use the returned String and add it to our environment configuration:
Encryption
Now we’re ready to start encrypting messages. Let’s create 2 functions
encrypt/1
and encrypt/2
, both will allow us to encrypt a String
but one
will allow us to add an expiration time in minutes.
Now let’s create a private function that will actually encrypt the message:
Here we’re adding 2 module attributes, the first one @key
is getting the key we
previously set in the configuration files, the second one @auth_data
is an
arbitrary String that’s known as AAD (Associated Authenticated Data) and is part
of the AES GCM specification. The AAD has nothing to do with making it “more secure”.
The aim of AAD is to attach information to the ciphertext (the encrypted message)
that is not encrypted, but is bound to it in the sense that it
cannot be changed or separated.
Inside the function we’re generating a variable iv
that refers to the
Initialization Vector and it must always be random, according to the GCM
information, part of the strength of our encryption depends on it. Here the iv
is a 128 bits random String.
Then we’ll use the crypto function block_encrypt/4, the first argument
is the algorithm we’re using, in this case :aes_gcm
, next our @key
, then the
iv
and last a tuple which contains our @auth_data
, the message we want to
encrypt and lastly the length of the cipher_tag
to generate, it can be in the
range of 1..16
the bigger the better.
If all goes well, the crypto function will return a tuple with the
cipher_text
and the cipher_tag
, in our implementation we’ll return:
or an error tuple if it occurs.
Going back to our encrypt/1
function we can now add its implementation:
Here if the encryption takes place we’re returning {:ok, payload}
where
payload is a 3 item tuple. We can do better, lets encode our tuple so we can
work with it:
The encode_payload/1
function is first concatenating our 3 items and then
encoding them to base 64. Notice how we’re changing the cipher_text
and
cipher_tag
order, this is important as you’ll see later.
Lets update the encrypt/1
function to return our encoded data:
Now our function will return fully working Tokens that we can use in the outside!
What about the encrypt/2
function? Lets work on that now:
Here we’re using the Timex library to generate a ttl
variable which will
be the time our token expires, then we concatenate it to our data after
transforming it to a String. (Notice we’re using “|” as a separator)
And we’re all set to encrypting messages and generating tokens, next: decryption.
Decryption
Any encryption would be pointless if we can’t decrypt the messages generated,
lets create a decrypt/1
function to do it.
Remember our Token is base 64 encoded, and that we concatenated the data, lets create a function to decode it:
This is our first line of defense, if Token is not correctly base 64 encoded
we can return an error. If decoding takes place then we can pattern match
against it to split the previously concatenated info. Remember we changed the
cipher_text
and cipher_tag
order? Well, here’s why we did it: the iv
and
the cipher_tag
have a length of 16 while our message String has an arbitrary
and variable length, so in order to pattern match against it the order is important.
If all goes well we return {:ok, {iv, cipher_text, cipher_tag}}
, as you can
see it reverts the order as we had it previously to base 64 encoding.
With the decoded payload we can now attempt to decrypt it, lets implement a
block_decrypt/1
function:
This function accepts a tuple which resembles our decoded payload, and then it will use Erlang’s crypto module to decrypt it using block_decrypt/4.
If decryption works we’ll get a {:ok, data}
response, where data is the
decrypted message.
Now lets implement our decrypt/1
function:
First we attempt to decode the Token, then to decrypt it, if this 2 steps went well we can now pattern match against the data after we try to split it where the “|” is found.
If an expiration is found first we parse it so we can work with it using Timex, then we compare the expiration to the current time. If the token is not expired we return the data, or an error if it is.
If no expiration is found, we just return the data.
The complete module should look something like this:
Now we can use our module like this:
As long as 5 minutes haven’t passed, our token will be valid, after that we’ll
get a {:error, :expired}
error.
Conclusion
This experiment shows us that Elixir has the capacity and tools for using encryption thanks to the Erlang/Beam environment.
While this code is probably not production ready, it’s been fun to implement, specially the pattern match parts which makes the code more concise and friendlier to read.
If you have any observation or something to add please don’t hesitate and leave a comment below.