Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 16 additions & 160 deletions lib/kryptonite/aes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ defmodule Kryptonite.AES do
illustrated such as:

iex> {key, iv} = {generate_key!(), Random.bytes!(16)}
iex> {:ok, cypher} = encrypt_cbc(key, iv, "Message...")
iex> decrypt_cbc(key, iv, cypher)
iex> {:ok, cypher} = encrypt_ctr(key, iv, "Message...")
iex> decrypt_ctr(key, iv, cypher)
"Message..."

In GCM mode, the same flow could be performed like so:
Expand All @@ -34,16 +34,7 @@ defmodule Kryptonite.AES do
{:error, :decryption_error}
"""

defmodule StreamIntegrityError do
defexception message: "Stream integrity failed to check"

@moduledoc """
Error when checking integrity of AES encrypted stream
"""
end

@key_byte_size 32
@hmac_type :sha256

@typedoc "A key is a 256 bit length bitstring."
@type key :: <<_::256>>
Expand Down Expand Up @@ -91,13 +82,13 @@ defmodule Kryptonite.AES do

## Examples

iex> {:ok, cypher} = encrypt_cbc(generate_key!(), Random.bytes!(16), "Message...")
iex> {:ok, cypher} = encrypt_ctr(generate_key!(), Random.bytes!(16), "Message...")
iex> is_bitstring(cypher)
true
"""
@spec encrypt_cbc(key, iv, binary) :: {:ok, cypher} | {:error, any}
def encrypt_cbc(key, iv, msg) do
{:ok, :crypto.block_encrypt(:aes_cbc256, key, iv, pad(msg))}
@spec encrypt_ctr(key, iv, binary) :: {:ok, cypher} | {:error, any}
def encrypt_ctr(key, iv, msg) do
{:ok, :aes_256_ctr |> :crypto.crypto_one_time(key, iv, pad(msg), true)}
catch
_, e -> {:error, e}
end
Expand All @@ -119,102 +110,13 @@ defmodule Kryptonite.AES do
"""
@spec encrypt_gcm(key, iv, binary, binary) :: {:ok, cypher, tag} | {:error, any}
def encrypt_gcm(key, iv, ad, msg) do
:aes_gcm
|> :crypto.block_encrypt(key, iv, {ad, msg})
:aes_256_gcm
|> :crypto.crypto_one_time_aead(key, iv, msg, ad, true)
|> Tuple.insert_at(0, :ok)
catch
_, e -> {:error, e}
end

@doc """
Encrypts a stream using AES in CTR mode.

## Examples

iex> {key, iv} = {generate_key!(), Random.bytes!(16)}
iex> 'This is a secret'
...> |> stream_encrypt(key, iv)
...> |> Enum.to_list()
...> |> is_list()
true
"""
@spec stream_encrypt(Enumerable.t(), key, iv) :: Enumerable.t()
def stream_encrypt(stream, key, iv) do
acc0 = :crypto.stream_init(:aes_ctr, key, iv)

reduce = fn elem, acc ->
{acc, cypher} = :crypto.stream_encrypt(acc, elem |> List.wrap())
{[cypher], acc}
end

Stream.transform(stream, acc0, reduce)
end

@doc """
Encrypts + HMAC a stream into a Collectable

## Examples

iex> {key, iv} = {generate_key!(), Random.bytes!(16)}
iex> fid = 1000000 |> :rand.uniform() |> to_string
iex> {plain, enc} = {"/tmp/\#{fid}.txt", "/tmp/\#{fid}.aes"}
iex> File.write!(plain, "This is a secret")
iex> {:ok, tag} = plain
...> |> File.stream!()
...> |> stream_encrypt(File.stream!(enc), key, iv, "Auth...")
iex> Enum.each([plain, enc], &File.rm!/1)
iex> is_binary(tag)
true
"""
@spec stream_encrypt(Enumerable.t(), Collectable.t(), key, iv, binary) :: {:ok, tag}
def stream_encrypt(in_stream, out_stream, key, iv, ad) do
acc = :crypto.stream_init(:aes_ctr, key, iv)

enc_stream =
in_stream
|> Stream.transform(acc, &do_stream_encrypt/2)
|> Stream.into(out_stream)

tag =
[iv]
|> Stream.concat(enc_stream)
|> stream_tag(ad)

{:ok, tag}
end

@doc """
Check integrity then decrypts a stream encrypted with `stream_encrypt/5`

Raise `Kryptonite.AES.StreamIntegrityError` in case of integrity checking error.

## Examples

iex> {key, iv} = {generate_key!(), Random.bytes!(16)}
iex> fid = 1000000 |> :rand.uniform() |> to_string
iex> {plain, enc} = {"/tmp/\#{fid}.txt", "/tmp/\#{fid}.aes"}
iex> File.write!(plain, "This is a secret")
iex> {:ok, tag} = plain
...> |> File.stream!()
...> |> stream_encrypt(File.stream!(enc), key, iv, "Auth...")
iex> enc
...> |> File.stream!()
...> |> stream_decrypt!(key, iv, "Auth...", tag)
...> |> Enum.to_list()
...> |> IO.iodata_to_binary()
"This is a secret"
"""
@spec stream_decrypt!(Enumerable.t(), key, binary, iv, tag) :: Enumerable.t()
def stream_decrypt!(in_stream, key, iv, ad, tag) do
[iv]
|> Stream.concat(in_stream)
|> stream_tag(ad)
|> case do
^tag -> stream_decrypt(in_stream, key, iv)
_ -> raise StreamIntegrityError
end
end

@doc """
Decrypts a `cypher` using AES in CBC mode.

Expand All @@ -225,15 +127,15 @@ defmodule Kryptonite.AES do

iex> {key, iv} = {generate_key!(), Random.bytes!(16)}
iex> msg = "Message..."
iex> {:ok, cypher} = encrypt_cbc(key, iv, msg)
iex> msg == decrypt_cbc(key, iv, cypher)
iex> {:ok, cypher} = encrypt_ctr(key, iv, msg)
iex> msg == decrypt_ctr(key, iv, cypher)
true
"""
@spec decrypt_cbc(key, iv, cypher) :: binary
def decrypt_cbc(key, iv, cypher),
@spec decrypt_ctr(key, iv, cypher) :: binary
def decrypt_ctr(key, iv, cypher),
do:
:aes_cbc256
|> :crypto.block_decrypt(key, iv, cypher)
:aes_256_ctr
|> :crypto.crypto_one_time(key, iv, cypher, false)
|> unpad()

@doc """
Expand All @@ -249,55 +151,14 @@ defmodule Kryptonite.AES do
"""
@spec decrypt_gcm(key, iv, binary, cypher, tag) :: {:ok, binary} | {:error, any}
def decrypt_gcm(key, iv, ad, cypher, tag) do
:aes_gcm
|> :crypto.block_decrypt(key, iv, {ad, cypher, tag})
:aes_256_gcm
|> :crypto.crypto_one_time_aead(key, iv, cypher, ad, tag, false)
|> case do
:error -> {:error, :decryption_error}
msg when is_binary(msg) -> {:ok, msg}
end
end

@doc """
Decrypts a stream using AES in CTR mode.

## Examples

iex> {key, iv} = {generate_key!(), Random.bytes!(16)}
iex> "This is a secret..."
...> |> String.to_charlist()
...> |> stream_encrypt(key, iv)
...> |> Enum.to_list()
...> |> stream_decrypt(key, iv)
...> |> Enum.to_list()
...> |> :erlang.iolist_to_binary
"This is a secret..."
"""
@spec stream_decrypt(Enumerable.t(), key, iv) :: Enumerable.t()
def stream_decrypt(stream, key, iv) do
acc0 =
:aes_ctr
|> :crypto.stream_init(key, iv)

reduce = fn elem, acc ->
{acc, cypher} = :crypto.stream_decrypt(acc, elem)
{[cypher], acc}
end

Stream.transform(stream, acc0, reduce)
end

@doc """
Returns computed AES + HMAC encoded stream tag
"""
@spec stream_tag(Enumerable.t(), binary) :: tag
def stream_tag(stream, key) do
stream
|> Enum.reduce(:crypto.hmac_init(@hmac_type, key), fn data, acc ->
:crypto.hmac_update(acc, data)
end)
|> :crypto.hmac_final()
end

# Private stuff.

@spec pad(binary) :: padded
Expand All @@ -317,9 +178,4 @@ defmodule Kryptonite.AES do

@spec cut_key(binary) :: binary
defp cut_key(<<key::binary-size(@key_byte_size), _::binary>>), do: key

defp do_stream_encrypt(elem, acc) do
{acc, cypher} = :crypto.stream_encrypt(acc, List.wrap(elem))
{[cypher], acc}
end
end
13 changes: 6 additions & 7 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule Kryptonite.MixProject do
def project do
[
app: :kryptonite,
version: "0.1.12",
version: "1.0.0",
elixir: "~> 1.5",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
Expand All @@ -25,7 +25,7 @@ defmodule Kryptonite.MixProject do
end

def application do
[extra_applications: [:logger]]
[extra_applications: [:logger, :crypto, :public_key]]
end

defp elixirc_paths(:test), do: ["lib", "test/support"]
Expand All @@ -34,12 +34,11 @@ defmodule Kryptonite.MixProject do
defp deps do
[
# Dev and Test only.
{:credo, "~> 0.8", only: [:dev, :test], runtime: false},
{:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false},
{:credo, "~> 1.5", only: [:dev, :test], runtime: false},
{:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false},
# Dev only.
{:mix_test_watch, "~> 0.5", only: :dev, runtime: false},
{:ex_doc, "~> 0.16", only: :dev, runtime: false},
{:excoveralls, "~> 0.8", only: :test, runtime: false}
{:ex_doc, "~> 0.25", only: :dev, runtime: false},
{:excoveralls, "~> 0.14", only: :test, runtime: false}
]
end

Expand Down
41 changes: 21 additions & 20 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
%{
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
"certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
"credo": {:hex, :credo, "0.10.2", "03ad3a1eff79a16664ed42fc2975b5e5d0ce243d69318060c626c34720a49512", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"},
"earmark": {:hex, :earmark, "1.3.0", "17f0c38eaafb4800f746b457313af4b2442a8c2405b49c645768680f900be603", [:mix], [], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"excoveralls": {:hex, :excoveralls, "0.10.3", "b090a3fbcb3cfa136f0427d038c92a9051f840953ec11b40ee74d9d4eac04d1e", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"file_system": {:hex, :file_system, "0.2.6", "fd4dc3af89b9ab1dc8ccbcc214a0e60c41f34be251d9307920748a14bf41f1d3", [:mix], [], "hexpm"},
"hackney": {:hex, :hackney, "1.14.3", "b5f6f5dcc4f1fba340762738759209e21914516df6be440d85772542d4a5e412", [:rebar3], [{:certifi, "2.4.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
"makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"},
"mix_test_watch": {:hex, :mix_test_watch, "0.9.0", "c72132a6071261893518fa08e121e911c9358713f62794a90c95db59042af375", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"certifi": {:hex, :certifi, "2.8.0", "d4fb0a6bb20b7c9c3643e22507e42f356ac090a1dcea9ab99e27e0376d695eba", [:rebar3], [], "hexpm", "6ac7efc1c6f8600b08d625292d4bbf584e14847ce1b6b5c44d983d273e1097ea"},
"credo": {:hex, :credo, "1.5.6", "e04cc0fdc236fefbb578e0c04bd01a471081616e741d386909e527ac146016c6", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "4b52a3e558bd64e30de62a648518a5ea2b6e3e5d2b164ef5296244753fc7eb17"},
"dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"},
"earmark_parser": {:hex, :earmark_parser, "1.4.15", "b29e8e729f4aa4a00436580dcc2c9c5c51890613457c193cc8525c388ccb2f06", [:mix], [], "hexpm", "044523d6438ea19c1b8ec877ec221b008661d3c27e3b848f4c879f500421ca5c"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"ex_doc": {:hex, :ex_doc, "0.25.3", "3edf6a0d70a39d2eafde030b8895501b1c93692effcbd21347296c18e47618ce", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "9ebebc2169ec732a38e9e779fd0418c9189b3ca93f4a676c961be6c1527913f5"},
"excoveralls": {:hex, :excoveralls, "0.14.3", "d17dc249ad32e469afd2bc656b58e810109d4367ec6bd467bed57a84dc4a3e02", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b5aecdfdcf48e9d5e1c210841589b30981a5e7e66055cb8691a6f90b1601c108"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"hackney": {:hex, :hackney, "1.18.0", "c4443d960bb9fba6d01161d01cd81173089686717d9490e5d3606644c48d121f", [:rebar3], [{:certifi, "~>2.8.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "9afcda620704d720db8c6a3123e9848d09c87586dc1c10479c42627b905b5c5e"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
"makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
"makeup_elixir": {:hex, :makeup_elixir, "0.15.1", "b5888c880d17d1cc3e598f05cdb5b5a91b7b17ac4eaf5f297cb697663a1094dd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "db68c173234b07ab2a07f645a5acdc117b9f99d69ebf521821d89690ae6c6ec8"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
}
Loading