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.
128-bit AES encryption refers to the process of concealing plaintext data using an AES key length of 128 bits. 128-bit AES encryption uses 10 transformation rounds to convert plaintext into ciphertext and is approved by the National Security Agency (NSA) to protect secret but not top-secret government information.
To encrypt using AES-CBC: Instantiate the CBC block cipher class with the AES implementation class. Initialize it with the key and Initialization Vector (IV) for encryption. Process each block of the padded plaintext being encrypted.
We can use some algorithms for padding block when the plaintext is not enough a block, like PKCS5 or PKCS7, it also can defend against PA attack, if we use ECB or CBC mode. Or we can use the mode of AES which support a stream of plaintext, like CFB, OFB, CTR mode.
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
andblock_decrypt/4
functions (and their/3
sisters) are deprecated and will be removed from the Erlangcrypto
module in Erlang v24. The new API functions that have replaced them arecrypto_one_time/4
andcrypto_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
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).
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?
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).
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.
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.
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})
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">>, <<>>)
JOSE.JWA component from JOSE package has block_decrypt/4 and block_encrypt/4 functions.
iex> JOSE.JWA.crypto_supports()
[ciphers: [aes_cbc: 128, aes_cbc: 192, aes_cbc: 256, aes_ecb: 128, aes_ecb: 192,
aes_ecb: 256, aes_gcm: 128, aes_gcm: 192, aes_gcm: 256,
chacha20_poly1305: 256],
hashs: [:md5, :poly1305, :sha, :sha256, :sha384, :sha512, :shake256],
public_keys: [:ec_gf2m, :ecdh, :ecdsa, :ed25519, :ed25519ph, :ed448, :ed448ph,
:rsa, :x25519, :x448], rsa_crypt: [:rsa1_5, :rsa_oaep, :rsa_oaep_256],
rsa_sign: [:rsa_pkcs1_padding, :rsa_pkcs1_pss_padding]]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With