rezam
rezam

Reputation: 627

how to encrypt and decrypt with AES CBC 128 in Elixir

I have an app in Rails with following methods to encrypt and decrypt a text and communicate with java clients.

def encrypt(string, key)
    cipher = OpenSSL::Cipher::AES.new(128, :CBC)
    cipher.encrypt
    cipher.padding = 1
    cipher.key = hex_to_bin(Digest::SHA1.hexdigest(key)[0..32])
    cipher_text = cipher.update(string)
    cipher_text << cipher.final
    return bin_to_hex(cipher_text).upcase
end

def decrypt(encrypted, key)
    encrypted = hex_to_bin(encrypted.downcase)
    cipher = OpenSSL::Cipher::AES.new(128, :CBC)
    cipher.decrypt
    cipher.padding = 1
    cipher.key = hex_to_bin(Digest::SHA1.hexdigest(key)[0..32])
    d = cipher.update(encrypted)
    d << cipher.final
rescue Exception => exc
end

def hex_to_bin(str)
    [str].pack "H*"
end

def bin_to_hex(str)
    str.unpack('C*').map{ |b| "%02X" % b }.join('')
end

I need do the same in Elixir for phoenix framework. Since I'm new to Elixir I couldn't find a way for that. I Found that Elixir uses Erlang's :crypto Module for that. In documentations there was no method for AES CBC encryption.

Upvotes: 8

Views: 9600

Answers (4)

matt
matt

Reputation: 79803

The block_encrypt/4 function from the Erlang crypto module is the function you want. Unlike the Ruby OpenSSL bindings, the Erlang code doesn’t handle padding, so you will need to do that yourself before encrypting (and remove it after decrypting).

NOTE: As of Erlang v23, the block_encrypt/4 and block_decrypt/4 functions (and their /3 sisters) are deprecated and will be removed from the Erlang crypto module in Erlang v24. The new API functions that have replaced them are crypto_one_time/4 and crypto_one_time/5 and these functions should be used for all new Erlang/Elixir programs. The new API functions support IVs and other improvements over the old functions.

However, unless this is just a toy app for learning purposes, I would recommend not doing this kind of crypto stuff yourself if you can avoid it. Rather you should find a higher level API that takes care of the various details where you can go wrong. I have listed some potential issues with your code as it is below, as well as a suggestion of what to do instead.


The padding that OpenSSL uses (sometimes called PKCS7 padding) is fairly simple. First you need to work out how many bytes you need to add to your data to make the length into a multiple of the block size (16 for AES). Then you simply add that many bytes of that value to the end. For example if your data was 14 bytes long then you would need to add two bytes, and each of those bytes would have the value 0x02 (2 bytes each with value 2). Note that you always add padding, so if your data is already a multiple of 16 byte then you add another 16 bytes (all with value 0x10).

To strip the padding you simply look at the value of the last byte and remove that many bytes from the end (you should probably check that the padding is correct too, i.e. all the bytes have the expected value).

Here is a simple implementation in Elixir (there may be a better / clearer / more idiomatic way to do this):

# These will need to be in a module of course
def pad(data, block_size) do
  to_add = block_size - rem(byte_size(data), block_size)
  data <> to_string(:string.chars(to_add, to_add))
end

def unpad(data) do
  to_remove = :binary.last(data)
  :binary.part(data, 0, byte_size(data) - to_remove)
end

You can now use these along with the :crypto.block_encrypt function to get AES CBC encryption like your Ruby code:

# BAD, don't do this!
# This is just to reproduce your code, where you are not using 
# an initialisation vector.
@zero_iv to_string(:string.chars(0, 16))
@aes_block_size 16

def encrypt(data, key) do
  :crypto.block_encrypt(:aes_cbc128, key, @zero_iv, pad(data, @aes_block_size))
end

def decrypt(data, key) do
  padded = :crypto.block_decrypt(:aes_cbc128, key, @zero_iv, data)
  unpad(padded)
end

Some issues

Here are some potential problems with your code. This is not an exhaustive list, just some things I noticed (I am not an expert in crypto).

  1. No authentication. Unless you’re checking the authentication in another method before the code you show, then you don’t have any authentication of the messages. This is very bad. You are exposing yourself to potential padding oracle attacks (where an attacker could decrypt the messages) and things like bit-flipping attacks, where an attacker can send specially modified messages that your code might not recognise as bad, and cause some undesired action to take place.

You should be using something like HMAC. But even if you decide to use a HMAC, there are still several questions you need to work out. Where does the HMAC key come from? Can we use the same key for encryption and authentication? Do we calculate the HMAC over the plaintext or the ciphertext? Should it cover the IV as well?

  1. No Initialisation Vector. CBC mode should make use of an initialisation vector, or IV. In the Ruby OpenSSL bindings if you don’t specify one it just uses zero bytes (which is why we needed to create the @zero_iv in the code above. Each message should have its own IV. This can just be a random series of bytes, and doesn’t need to be kept secret (it can just be sent prepended to the ciphertext).

  2. Weak key generation. I could be wrong with this one, but since you are calculating the SHA1 hash of the provided key argument to use as the encryption/decryption key it suggest that this argument is actually a password. If this is the case then you should be using a better key derivation function (and if not then what’s the purpose of the hashing?). If you are using an easy for a human to remember password (or a single hash of one) you could be vulnerable to brute force attacks where an attacker tries lots of dictionary words as the key.

    You should be using a proper key derivation function, such as PBKDF2. Even then you will still have complications since you might need two keys (encryption and authentication), so you need to work out how to generate them both.


What to use instead

If possible you should look for a higher level library that takes into account these factors and provides a simpler API. I would recommend Libsodium, which has bindings for many languages including Ruby, Elixir, Erlang, and Java/Android.

Upvotes: 20

Dan Draper
Dan Draper

Reputation: 1121

I'd recommend not using CBC mode directly but use GCM mode as this will provide authentication as well.

In Elixir (for a 256bit AES key)

# Gen once (see also https://hexdocs.pm/plug/Plug.Crypto.KeyGenerator.html#content)
k = :crypto.strong_rand_bytes(32)

# Gen every time you encrypt a message
iv = :crypto.strong_rand_bytes(32)
{ct, tag} = :crypto.block_encrypt(:aes_gcm, k, iv, {"AES128GCM", msg})
payload = Base.encode16(iv <> tag <> ct)

To decrypt:

<<iv::binary-32, tag::binary-16, ct::binary>> = Base.decode16!(payload)
:crypto.block_decrypt(:aes_gcm, k, iv, {"AES128GCM", ct, tag})

Upvotes: 4

chris
chris

Reputation: 2863

Thanks @matt, I wrote my AES_ECB in Elixir.

Hope that it helps you, CBC should be the same.

  def encrypt(data, key) do
    :crypto.block_encrypt(:aes_ecb, key, pad(data, @aes_block_size))
  end

  # PKCS5Padding
  defp pad(data, block_size) do
    to_add = block_size - rem(byte_size(data), block_size)
    data <> :binary.copy(<<to_add>>, to_add)
  end

  def decrypt(data, key) do
    padded = :crypto.block_decrypt(:aes_ecb, key, data)
    unpad(padded)
  end

  defp unpad(data) do
    to_remove = :binary.last(data)
    :binary.part(data, 0, byte_size(data) - to_remove)
  end

Upvotes: 0

Vans S
Vans S

Reputation: 1813

Here is what I use for ECB, CBC should be the same with the added need to pass the previous block cipher in the accumulator. Don't forget that you also need to write a function to pad the term to 16 byte blocks(the ruby seems to do that automatically).

Key = "12345678"
AES_ECB_Encrypt = 
    fun Crypt(<<Block:16/binary, Rest/binary>>, Acc) ->
            NewAcc = erlang:iolist_to_binary( [Acc, crypto:block_encrypt(aes_ecb, Key, Block)] ),
            Crypt(Rest, NewAcc);
        Crypt(_, Acc) ->
            Acc
end,
AES_ECB_Encrypt(<<"hello00000000000">>, <<>>)

Upvotes: 3

Related Questions