Skip to content

Commit

Permalink
Merge pull request #5 from stellaservice/master
Browse files Browse the repository at this point in the history
v1 refactor to include AES256CBC, pending redesign/breaking API changes for new version
  • Loading branch information
ntrepid8 authored Feb 13, 2017
2 parents d4dbed9 + a29f5b8 commit 5d27541
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 26 deletions.
162 changes: 138 additions & 24 deletions lib/ex_crypto.ex
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
defmodule ExCrypto do
@moduledoc """
The ExCrypto module exposes a subset of functionality from the Erlang `crypto`
The ExCrypto module exposes a subset of functionality from the Erlang `crypto`
module with the goal of making it easier to include strong cryptography in your
Elixir applications.
This module provides functions for symmetric-key cryptographic operations using
AES in GCM mode. The ExCrypto module attempts to reduce complexity by providing
This module provides functions for symmetric-key cryptographic operations using
AES in GCM and CBC mode. The ExCrypto module attempts to reduce complexity by providing
some sane default values for common operations.
"""

@aes_block_size 16
@iv_bit_length 128
@bitlength_error "IV must be exactly 128 bits and key must be exactly 128, 192 or 256 bits"
defmacro __using__(_) do
quote do
import ExCrypto
Expand All @@ -23,11 +25,14 @@ defmodule ExCrypto do
end
end

defp normalize_error(kind, error) do
case Exception.normalize(kind, error) do
%{message: message} ->
defp normalize_error(kind, error, key_and_iv \\ nil) do
key_error = test_key_and_iv_bitlength(key_and_iv)
cond do
key_error ->
key_error
%{message: message} = Exception.normalize(kind, error) ->
{:error, message}
x ->
x = Exception.normalize(kind, error) ->
{kind, x, System.stacktrace}
end
end
Expand All @@ -36,6 +41,13 @@ defmodule ExCrypto do
{kind, Exception.normalize(kind, error), System.stacktrace}
end

defp test_key_and_iv_bitlength(nil), do: nil
defp test_key_and_iv_bitlength({key, iv}) when bit_size(iv) != 128, do: {:error, @bitlength_error}
defp test_key_and_iv_bitlength({key, iv}) when rem(bit_size(key), 128) == 0, do: nil
defp test_key_and_iv_bitlength({key, iv}) when rem(bit_size(key), 192) == 0, do: nil
defp test_key_and_iv_bitlength({key, iv}) when rem(bit_size(key), 256) == 0, do: nil
defp test_key_and_iv_bitlength({key, iv}), do: {:error, @bitlength_error}

@doc """
Returns random characters. Each character represents 6 bits of entropy.
Expand All @@ -46,11 +58,11 @@ defmodule ExCrypto do
iex> rand_string = ExCrypto.rand_chars(24)
iex> assert(String.length(rand_string) == 24)
true
iex> rand_string = ExCrypto.rand_chars(32)
iex> assert(String.length(rand_string) == 32)
true
iex> rand_string = ExCrypto.rand_chars(44)
iex> assert(String.length(rand_string) == 44)
true
Expand All @@ -71,7 +83,7 @@ defmodule ExCrypto do
@doc """
Returns a random integer between `low` and `high`.
Accepts two `integer` arguments for the `low` and `high` boundaries. The `low` argument
Accepts two `integer` arguments for the `low` and `high` boundaries. The `low` argument
must be less than the `high` argument.
## Examples
Expand All @@ -81,13 +93,13 @@ defmodule ExCrypto do
true
iex> assert(rand_int < 21)
true
iex> rand_int = ExCrypto.rand_int(23, 99)
iex> assert(rand_int > 22)
true
iex> assert(rand_int < 99)
true
iex> rand_int = ExCrypto.rand_int(212, 736)
iex> assert(rand_int > 211)
true
Expand All @@ -109,7 +121,7 @@ defmodule ExCrypto do
true
iex> assert(bit_size(rand_bytes) == 128)
true
iex> {:ok, rand_bytes} = ExCrypto.rand_bytes(24)
iex> assert(byte_size(rand_bytes) == 24)
true
Expand Down Expand Up @@ -140,7 +152,7 @@ defmodule ExCrypto do
@doc """
Returns an AES key.
Accepts a `key_type` (`:aes_128`|`:aes_192`|`:aes_256`) and `key_format`
Accepts a `key_type` (`:aes_128`|`:aes_192`|`:aes_256`) and `key_format`
(`:base64`|`:bytes`) to determine type of key to produce.
## Examples
Expand All @@ -160,7 +172,7 @@ defmodule ExCrypto do
iex> {:ok, key} = ExCrypto.generate_aes_key(:aes_192, :base64)
iex> assert String.length(key) == 32
true
iex> {:ok, key} = ExCrypto.generate_aes_key(:aes_128, :bytes)
iex> assert bit_size(key) == 128
true
Expand Down Expand Up @@ -211,18 +223,91 @@ defmodule ExCrypto do
"""
@spec encrypt(binary, binary, binary, binary) :: {:ok, {binary, {binary, binary, binary}}} | {:error, binary}
def encrypt(key, authentication_data, initialization_vector, clear_text) do
case :crypto.block_encrypt(:aes_gcm, key, initialization_vector, {authentication_data, clear_text}) do
{cipher_text, cipher_tag} -> {:ok, {authentication_data, {initialization_vector, cipher_text, cipher_tag}}}
x -> {:error, x}
end
_encrypt(key, initialization_vector, {authentication_data, clear_text}, :aes_gcm)
catch
kind, error -> normalize_error(kind, error)
end


@doc """
Encrypt a `binary` with AES in CBC mode.
Returns a tuple containing the `initialization_vector`, and `cipher_text`.
At a high level encryption using AES in CBC mode looks like this:
key + clear_text -> init_vec + cipher_text
## Examples
iex> clear_text = "my-clear-text"
iex> {:ok, aes_256_key} = ExCrypto.generate_aes_key(:aes_256, :bytes)
iex> {:ok, {iv, cipher_text}} = ExCrypto.encrypt(aes_256_key, clear_text)
iex> assert(is_bitstring(cipher_text))
true
"""
@spec encrypt(binary, binary, binary) :: {:ok, {binary, {binary, binary}}} | {:error, binary}
def encrypt(key, clear_text) do
{:ok, initialization_vector} = rand_bytes(16) # new 128 bit random initialization_vector
_encrypt(key, initialization_vector, pad(clear_text, @aes_block_size), :aes_cbc256)
catch
kind, error ->
{:ok, initialization_vector} = rand_bytes(16)
normalize_error(kind, error, {key, initialization_vector})
end


@doc """
Encrypt a `binary` with AES in CBC mode providing explicit IV via map.
Returns a tuple containing the `initialization_vector`, and `cipher_text`.
At a high level encryption using AES in CBC mode looks like this:
key + clear_text + map -> init_vec + cipher_text
## Examples
iex> clear_text = "my-clear-text"
iex> {:ok, aes_256_key} = ExCrypto.generate_aes_key(:aes_256, :bytes)
iex> {:ok, init_vec} = ExCrypto.rand_bytes(16)
iex> {:ok, {iv, cipher_text}} = ExCrypto.encrypt(aes_256_key, clear_text, %{initialization_vector: init_vec})
iex> assert(is_bitstring(cipher_text))
true
"""
def encrypt(key, clear_text, %{initialization_vector: initialization_vector}) do
_encrypt(key, initialization_vector, pad(clear_text, @aes_block_size), :aes_cbc256)
catch
kind, error -> normalize_error(kind, error, {key, initialization_vector})
end

defp _encrypt(key, initialization_vector, encryption_payload, algorithm) do
case :crypto.block_encrypt(algorithm, key, initialization_vector, encryption_payload) do
{cipher_text, cipher_tag} ->
{authentication_data, _clear_text} = encryption_payload
{:ok, {authentication_data, {initialization_vector, cipher_text, cipher_tag}}}
<<cipher_text::binary>> ->
{:ok, {initialization_vector, cipher_text }}
x -> {:error, x}
end
end

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

@doc """
Same as `encrypt/4` except the `initialization_vector` is automatically.
Same as `encrypt/4` except the `initialization_vector` is automatically generated.
A 128 bit `initialization_vector` is generated automatically by `encrypt/3`. It returns a tuple
A 128 bit `initialization_vector` is generated automatically by `encrypt/3`. It returns a tuple
containing the `initialization_vector`, the `cipher_text` and the `cipher_tag`.
## Examples
Expand All @@ -241,7 +326,7 @@ defmodule ExCrypto do
@spec encrypt(binary, binary, binary) :: {:ok, {binary, {binary, binary, binary}}} | {:error, binary}
def encrypt(key, authentication_data, clear_text) do
{:ok, initialization_vector} = rand_bytes(16) # new 128 bit random initialization_vector
encrypt(key, authentication_data, initialization_vector, clear_text)
_encrypt(key, initialization_vector, {authentication_data, clear_text}, :aes_gcm)
end

@doc """
Expand All @@ -264,7 +349,36 @@ defmodule ExCrypto do
"""
@spec decrypt(binary, binary, binary, binary, binary) :: {:ok, binary} | {:error, binary}
def decrypt(key, authentication_data, initialization_vector, cipher_text, cipher_tag) do
{:ok, :crypto.block_decrypt(:aes_gcm, key, initialization_vector, {authentication_data, cipher_text, cipher_tag})}
_decrypt(key, initialization_vector, {authentication_data, cipher_text, cipher_tag}, :aes_gcm)
end


@doc """
Returns a clear-text string decrypted with AES256 in CBC mode.
At a high level decryption using AES in CBC mode looks like this:
key + init_vec + cipher_text -> clear_text
## Examples
iex> clear_text = "my-clear-text"
iex> {:ok, aes_256_key} = ExCrypto.generate_aes_key(:aes_256, :bytes)
iex> {:ok, {init_vec, cipher_text}} = ExCrypto.encrypt(aes_256_key, clear_text)
iex> {:ok, val} = ExCrypto.decrypt(aes_256_key, init_vec, cipher_text)
iex> assert(val == clear_text)
true
"""
@spec decrypt(binary, binary, binary) :: {:ok, binary} | {:error, binary}
def decrypt(key, initialization_vector, cipher_text) do
{:ok, padded_cleartext} = _decrypt(key, initialization_vector, cipher_text, :aes_cbc256)
{:ok, unpad(padded_cleartext)}
catch
kind, error -> normalize_error(kind, error, {key, initialization_vector})
end

defp _decrypt(key, initialization_vector, cipher_data, algorithm) do
{:ok, :crypto.block_decrypt(algorithm, key, initialization_vector, cipher_data)}
catch
kind, error -> normalize_error(kind, error)
end
Expand Down
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ defmodule ExCrypto.Mixfile do
{:poison, ">= 1.0.0"},
{:timex, ">= 0.19.0", only: :test},
{:earmark, "~> 0.1", only: :dev},
{:dialyxir, "~> 0.4", only: [:dev], runtime: false},
{:ex_doc, "~> 0.10", only: :dev}
]
end
Expand Down
3 changes: 2 additions & 1 deletion mix.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
%{"certifi": {:hex, :certifi, "0.4.0", "a7966efb868b179023618d29a407548f70c52466bf1849b9e8ebd0e34b7ea11f", [:rebar3], []},
"combine": {:hex, :combine, "0.7.0", "2ac6ae852a9835fe8189af18121cddd5bed2677f5df706dc0d208af668ab845d", [:mix], []},
"combine": {:hex, :combine, "0.9.3", "192e609b48b3f2210494e26f85db1712657be1a8f15795656710317ea43fc449", [:mix], []},
"dialyxir": {:hex, :dialyxir, "0.4.1", "236056d6acd25f740f336756c0f3b5dd6e2f0156074bc15f3b779aeee15390c8", [:mix], []},
"earmark": {:hex, :earmark, "0.1.19", "ffec54f520a11b711532c23d8a52b75a74c09697062d10613fa2dbdf8a9db36e", [:mix], []},
"ex_doc": {:hex, :ex_doc, "0.10.0", "f49c237250b829df986486b38f043e6f8e19d19b41101987f7214543f75947ec", [:mix], [{:earmark, "~> 0.1.17 or ~> 0.2", [hex: :earmark, optional: true]}]},
"gettext": {:hex, :gettext, "0.11.0", "80c1dd42d270482418fa158ec5ba073d2980e3718bacad86f3d4ad71d5667679", [:mix], []},
Expand Down
34 changes: 33 additions & 1 deletion test/ex_crypto_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ defmodule ExCryptoTest do
rand_string = ExCrypto.rand_chars(rand_char_count)
assert(String.length(rand_string) == rand_char_count)
end

test "generate random characters" do
for n <- 1..100, do: run_rand_char_test()
end
Expand Down Expand Up @@ -140,5 +140,37 @@ defmodule ExCryptoTest do
assert(cipher_tag == pc_tag)
end

test "test aes_cbc encrypt with auto-IV (256 bit key)" do
{:ok, aes_256_key} = ExCrypto.generate_aes_key(:aes_256, :bytes)

clear_text = "secret_message"
# encrypt
{:ok, {iv, cipher_text}} = ExCrypto.encrypt(aes_256_key, clear_text)
assert(byte_size(iv) == 16)
assert(clear_text != cipher_text)

# decrypt
{:ok, decrypted_clear_text} = ExCrypto.decrypt(aes_256_key, iv, cipher_text)
assert(decrypted_clear_text == clear_text)
end

test "errors with bad key length" do
{:ok, aes_bad_raw_key} = ExCrypto.rand_bytes(27)
aes_bad_key = Base.url_encode64(aes_bad_raw_key)

clear_text = "secret_message"
# encrypt
{:error, error_message} = ExCrypto.encrypt(aes_bad_key, clear_text)
assert(is_binary(error_message))
end

test "errors with bad iv length " do
{:ok, aes_256_key} = ExCrypto.generate_aes_key(:aes_256, :bytes)
{:ok, bad_iv} = ExCrypto.rand_bytes(17)
clear_text = "secret_message"
# encrypt
{:error, error_message} = ExCrypto.encrypt(aes_256_key, clear_text, %{initialization_vector: bad_iv})
assert(is_binary(error_message))
end

end

0 comments on commit 5d27541

Please sign in to comment.