diff options
Diffstat (limited to 'shard')
-rw-r--r-- | shard/config/config.exs | 13 | ||||
-rw-r--r-- | shard/lib/keys.ex | 17 | ||||
-rw-r--r-- | shard/lib/manager.ex | 169 | ||||
-rw-r--r-- | shard/lib/net/chan.ex_ (renamed from shard/lib/net/chan.ex) | 0 | ||||
-rw-r--r-- | shard/lib/net/tcpconn.ex | 211 | ||||
-rw-r--r-- | shard/lib/net/tcpserver.ex | 3 | ||||
-rw-r--r-- | shard/mix.exs | 3 | ||||
-rw-r--r-- | shard/mix.lock | 1 |
8 files changed, 271 insertions, 146 deletions
diff --git a/shard/config/config.exs b/shard/config/config.exs index a5aa071..55eda61 100644 --- a/shard/config/config.exs +++ b/shard/config/config.exs @@ -22,15 +22,12 @@ use Mix.Config # -# Peer id suffix +# Network secret # ============== -# This Shard instance will only connect to other instances that use -# the same suffix. -# -# On first run, the instance will try to generate a peer id that -# has this suffix. This is done by brute-force testing, therefore -# it is not recommended to use long suffixes. -config :shard, peer_id_suffix: "SH" +# The secret value that identifies the shard network this peer +# participates in. Used in the secure handshake protocol. +# Must be 32 bytes long. +config :shard, network_key: :crypto.hash(:sha256, "ShardTestNet") # Identity suffix # =============== diff --git a/shard/lib/keys.ex b/shard/lib/keys.ex index b6ff461..fe63148 100644 --- a/shard/lib/keys.ex +++ b/shard/lib/keys.ex @@ -16,16 +16,7 @@ defmodule Shard.Keys do def init() do :dets.start {:ok, @key_db} = :dets.open_file(@key_db, [type: :set]) - - case :dets.lookup(@key_db, :peer) do - [] -> - Logger.info "Generating peer keypair..." - {pk, sk} = gen_keypair(Application.get_env(:shard, :peer_id_suffix)) - :dets.insert @key_db, {:peer, pk, sk} - {pk, sk} - [{:peer, pk, sk}] -> - {pk, sk} - end + nil end defp gen_keypair(suffix, n \\ 0) do @@ -44,10 +35,6 @@ defmodule Shard.Keys do :binary.longest_common_suffix([pk, suffix]) == byte_size(suffix) end - def get_peer_keypair() do - Agent.get(__MODULE__, &(&1)) - end - def get_any_identity() do Agent.get(__MODULE__, fn _ -> case list_identities() do @@ -64,7 +51,7 @@ defmodule Shard.Keys do {pk, sk} = gen_keypair(Application.get_env(:shard, :identity_suffix)) Logger.info "New identity: #{pk|>Base.encode16}" :dets.insert @key_db, {pk, sk} - SApp.Identity.start_link(pk) + Shard.Manager.Manifest.start %SApp.Identity.Manifest{pk: pk} pk end diff --git a/shard/lib/manager.ex b/shard/lib/manager.ex index 617378c..ccb750f 100644 --- a/shard/lib/manager.ex +++ b/shard/lib/manager.ex @@ -19,38 +19,42 @@ defmodule Shard.Manager do @moduledoc""" Maintains several important tables : - - :peer_db - - List of - { id, pid | nil, ip, port } - - - :shard_db + - :shard_db (persistent with DETS) List of { id, manifest, pid | nil } - - :shard_state + - :shard_state (persistent with DETS) List of { id, state } - - :shard_procs + - :peer_db (persistent with DETS) + + Mult-list of + { shard_id, peer_info } # TODO: add health info (last seen, ping, etc) + + peer_info := {:inet4, ip, port} | {:inet6, ip, port} | {:onion, name} + + - :shard_procs (not persistent) List of { {id, path}, pid } - - :shard_peer_db - - Mult-list of - { shard_id, peer_id } + - :connections (not persistent) + List of + { nil | his_pk, nil | my_pk, pid, peer_info } And an internal table : - - :outbox + - :outbox (not persistent) Multi-list of - { peer_id, message, time_inserted } + { dest, auth_info, message, time_inserted } + + dest := peer_info + auth_info := nil | { his_pk, my_pk_list } """ @@ -58,23 +62,15 @@ defmodule Shard.Manager do require Logger - @peer_db [Application.get_env(:shard, :data_path), "peer_db"] |> Path.join |> String.to_atom @shard_db [Application.get_env(:shard, :data_path), "shard_db"] |> Path.join |> String.to_atom @shard_state [Application.get_env(:shard, :data_path), "shard_state"] |> Path.join |> String.to_atom - @shard_peer_db [Application.get_env(:shard, :data_path), "shard_peer_db"] |> Path.join |> String.to_atom + @peer_db [Application.get_env(:shard, :data_path), "peer_db"] |> Path.join |> String.to_atom def start_link(my_port) do GenServer.start_link(__MODULE__, my_port, name: __MODULE__) end def init(my_port) do - :dets.open_file(@peer_db, [type: :set]) - for [{id, _pid, ip, port}] <- :dets.match @peer_db, :"$1" do - :dets.insert @peer_db, {id, nil, ip, port} - # connect blindly to everyone - add_peer(ip, port) - end - :dets.open_file(@shard_db, [type: :set]) for [{id, manifest, _pid}] <- :dets.match @shard_db, :"$1" do :dets.insert @shard_db, {id, manifest, nil} @@ -82,9 +78,10 @@ defmodule Shard.Manager do end :dets.open_file(@shard_state, [type: :set]) - :dets.open_file(@shard_peer_db, [type: :bag]) + :dets.open_file(@peer_db, [type: :bag]) :ets.new(:shard_procs, [:set, :protected, :named_table]) + :ets.new(:connections, [:bag, :protected, :named_table]) outbox = :ets.new(:outbox, [:bag, :private]) {:ok, %{my_port: my_port, outbox: outbox} } @@ -111,12 +108,12 @@ defmodule Shard.Manager do {:noreply, state} end - def handle_cast({:interested, peer_id, shards}, state) do + def handle_cast({:interested, peer_info, shards}, state) do for shard_id <- shards do case :dets.lookup(@shard_db, shard_id) do [{ ^shard_id, _, pid }] -> - :dets.insert(@shard_peer_db, {shard_id, peer_id}) - GenServer.cast(pid, {:interested, peer_id}) + :dets.insert(@peer_db, {shard_id, peer_info}) + GenServer.cast(pid, {:interested, peer_info}) [] -> nil end end @@ -209,34 +206,23 @@ defmodule Shard.Manager do end - # ================ - # PUBLIC INTERFACE - # ================ - + # ==================== + # INTERFACE WITH PEERS + # ==================== - @doc""" - Connect to a peer specified by ip address and port - """ - def add_peer(ip, port) do - GenServer.cast(__MODULE__, {:add_peer, ip, port}) + def incoming(conn_pid, {:interested, shards}) do + GenServer.cast(__MODULE__, {:interested, peer_id, shards}) end - @doc""" - Send message to a peer specified by peer id - """ - def send(peer_id, msg) do - case :dets.lookup(@peer_db, peer_id) do - [{ ^peer_id, pid, _, _}] when pid != nil-> - GenServer.cast(pid, {:send_msg, msg}) - _ -> - GenServer.cast(__MODULE__, {:connect_and_send, peer_id, msg}) - end + def incoming(conn_pid, {:not_interested, shard}) do + GenServer.cast(__MODULE__, {:not_interested, peer_id, shard}) end @doc""" Dispatch incoming message to correct shard process """ - def dispatch(peer_id, {shard_id, path, msg}) do + defp dispatch(conn_pid, {shard_id, path, msg}) do + # TODO: auth case :dets.lookup(@shard_db, shard_id) do [] -> __MODULE__.send(peer_id, {:not_interested, shard_id}) @@ -255,12 +241,33 @@ defmodule Shard.Manager do end end - def dispatch(peer_id, {:interested, shards}) do - GenServer.cast(__MODULE__, {:interested, peer_id, shards}) + + # ===================== + # INTERFACE WITH SHARDS + # ===================== + + @doc""" + Send message to a peer specified by peer id + """ + def send(peer_id, msg) do + case :dets.lookup(@peer_db, peer_id) do + [{ ^peer_id, pid, _, _}] when pid != nil-> + GenServer.cast(pid, {:send_msg, msg}) + _ -> + GenServer.cast(__MODULE__, {:connect_and_send, peer_id, msg}) + end end - def dispatch(peer_id, {:not_interested, shard}) do - GenServer.cast(__MODULE__, {:not_interested, peer_id, shard}) + @doc""" + Send message to a peer through an authenticated channel + + his_auth: accepted users to talk to, either single pk or list of pk + + Returns true if a corresponding channel was open and msg was sent, + false otherwise. + """ + def send(peer_id, my_auth, his_auth, msg) do + # TODO end @doc""" @@ -273,16 +280,6 @@ defmodule Shard.Manager do end @doc""" - Returns the pid for a shard if it exists - """ - def find_proc(shard_id) do - case :dets.lookup(@shard_db, shard_id) do - [{^shard_id, _, pid}] -> pid - _ -> nil - end - end - - @doc""" Register a process as the handler for shard packets for a given path. """ def dispatch_to(shard_id, path, pid) do @@ -290,20 +287,6 @@ defmodule Shard.Manager do end @doc""" - Return the list of all shards. - """ - def list_shards() do - for [x] <- :dets.match(@shard_db, :"$1"), do: x - end - - @doc""" - Return the list of all peers - """ - def list_peers() do - for [x] <- :dets.match(@peer_db, :"$1"), do: x - end - - @doc""" Return the list of all peer IDs that are interested in a certain shard """ def get_shard_peers(shard_id) do @@ -326,4 +309,40 @@ defmodule Shard.Manager do def save_state(shard_id, state) do :dets.insert(@shard_state, {shard_id, state}) end + + + # ========================== + # INTERFACE FOR OTHER THINGS + # ========================== + + @doc""" + Connect to a peer specified by ip address and port + """ + def add_peer(ip, port) do + GenServer.cast(__MODULE__, {:add_peer, ip, port}) + end + + @doc""" + Returns the pid for a shard if it exists + """ + def find_proc(shard_id) do + case :dets.lookup(@shard_db, shard_id) do + [{^shard_id, _, pid}] -> pid + _ -> nil + end + end + + @doc""" + Return the list of all shards. + """ + def list_shards() do + for [x] <- :dets.match(@shard_db, :"$1"), do: x + end + + @doc""" + Return the list of all peers + """ + def list_peers() do + for [x] <- :dets.match(@peer_db, :"$1"), do: x + end end diff --git a/shard/lib/net/chan.ex b/shard/lib/net/chan.ex_ index 5aba960..5aba960 100644 --- a/shard/lib/net/chan.ex +++ b/shard/lib/net/chan.ex_ diff --git a/shard/lib/net/tcpconn.ex b/shard/lib/net/tcpconn.ex index 543341a..35bf9d1 100644 --- a/shard/lib/net/tcpconn.ex +++ b/shard/lib/net/tcpconn.ex @@ -1,7 +1,15 @@ defmodule SNet.TCPConn do + @moduledoc""" + Secret handshake as described in this document: + https://ssbc.github.io/scuttlebutt-protocol-guide/#peer-connections + + Does not implement the stream protocol, we don't hide the length of packets. + (TODO ^) + """ + + use GenServer, restart: :temporary - require Salty.Box.Curve25519xchacha20poly1305, as: Box - require Salty.Sign.Ed25519, as: Sign + require Logger def start_link(state) do @@ -9,60 +17,166 @@ defmodule SNet.TCPConn do end def init(state) do - GenServer.cast(self(), :handshake) - {:ok, state} - end + if state.is_client do + GenServer.cast(self(), :client_handshake) + else + GenServer.cast(self(), :server_handshake) + end - def handle_call(:get_host_str, _from, state) do - {:reply, "#{state.his_pkey|>Base.encode16|>String.downcase}@#{to_string(:inet_parse.ntoa(state.addr))}:#{state.port}", state} + {:ok, {addr, port}} = :inet.peername state.socket + {:ok, %{state | addr: addr, port: port}} end - def handle_cast(:handshake, state) do - socket = state.socket - {srv_pkey, srv_skey} = Shard.Keys.get_peer_keypair - {:ok, sess_pkey, sess_skey} = Box.keypair - {:ok, challenge} = Salty.Random.buf 32 + def handle_call(:get_peer_info, _from, state) do + {:reply, {:tcp4, state.addr, state.port}, state} + end - # Exchange public keys and challenge - hello = {srv_pkey, sess_pkey, challenge, state.my_port} - :gen_tcp.send(socket, :erlang.term_to_binary hello) - {:ok, pkt} = :gen_tcp.recv(socket, 0) - {cli_pkey, cli_sess_pkey, cli_challenge, his_port} = :erlang.binary_to_term(pkt, [:safe]) - # Do challenge and check their challenge - {:ok, cli_challenge_sign} = Sign.sign_detached(cli_challenge, srv_skey) - pkt = encode_pkt(cli_challenge_sign, cli_sess_pkey, sess_skey) - :gen_tcp.send(socket, pkt) + def handle_cast(:client_handshake, state) do + socket = state.socket - {:ok, pkt} = :gen_tcp.recv(socket, 0) - challenge_sign = decode_pkt(pkt, cli_sess_pkey, sess_skey) - :ok = Sign.verify_detached(challenge_sign, challenge, cli_pkey) + net_key = Application.get_env(:shard, :network_key) + {:ok, cli_eph_pk, cli_eph_sk} = :enacl.box_keypair + + [srv_longterm_pk] = state.his_auth + cli_longterm_pk = state.my_auth + cli_longterm_sk = Keys.get_sk cli_longterm_pk + + # 1. Client hello + {:ok, cli_hello_hmac} = :enacl.auth(cli_eph_pk, net_key) + cli_hello = cli_hello_hmac <> cli_eph_pk + :gen_tcp.send(socket, cli_hello) + + # 2. Server hello + {:ok, srv_hello} = :gen_tcp.recv(socket, 0) + 64 = byte_size srv_hello + srv_hmac = :binary.part srv_hello, 0, 32 + srv_eph_pk = :binary.part srv_hello, 32, 32 + true = :enacl.auth_verify(srv_hmac, srv_eph_pk, net_key) + + # Shared secret derivation + sh_sec_ab = :enacl.curve25519_scalarmult(cli_eph_sk, srv_eph_pk) + sh_sec_aB = :enacl.curve25519_scalarmult(cli_eph_sk, :enacl.crypto_sign_ed25519_public_to_curve25519(srv_longterm_pk)) + + # 3. Client authenticate + msg1 = net_key <> srv_longterm_pk <> :crypto.hash(:sha256, sh_sec_ab) + det_sign_A = :enacl.sign_detached(msg1, cli_longterm_sk) + key3 = :crypto.hash(:sha256, net_key <> sh_sec_ab <> sh_sec_aB) + cli_auth = :crypto.secretbox(det_sign_A <> cli_longterm_pk, <<0 :: 24*8>>, key3) + :gen_tcp.send(socket, cli_auth) + + # Shared secret derivation, again + sh_sec_Ab = :enacl.curve25519_scalarmult(:enacl.crypto_sign_ed25519_secret_to_curve25519(cli_longterm_sk), srv_eph_pk) + + # 4. Server accept + {:ok, srv_accept} = :gen_tcp.recv(socket, 0) + key4 = :crypto.hash(:sha256, net_key <> sh_sec_ab <> sh_sec_aB <> sh_sec_Ab) + {:ok, det_sign_B} = :enacl.secretbox_open(srv_accept, <<0 :: 24*8>>, key4) + true = :enacl.sign_verify_detached(det_sign_B, net_key <> det_sign_A <> cli_longterm_pk <> :crypto.sha256(sh_sec_ab), srv_longterm_pk) + + # Derive secrets and initial nonces for stream communication + secret_common = :crypto.hash(:sha256, :crypto.hash(:sha256, net_key <> sh_sec_ab <> sh_sec_aB <> sh_sec_Ab)) + secret_cli2srv = :crypto.hash(:sha256, secret_common <> srv_longterm_pk) + secret_srv2cli = :crypto.hash(:sha256, secret_common <> cli_longterm_pk) + {:ok, hmac1} = :enacl.auth(srv_eph_pk, net_key) + nonce_cli2srv = :binary.part(hmac1, 0, 24) + {:ok, hmac2} = :enacl.auth(cli_eph_pk, net_key) + nonce_srv2cli = :binary.part(hmac2, 0, 24) + + # Set up the rest + :inet.setopts(socket, [active: true]) + {:ok, {addr, port}} = :inet.peername socket + state = %{ + socket: socket, + my_pk: cli_longterm_pk, + his_pk: srv_longterm_pk, + secret_send: secret_cli2srv, + secret_recv: secret_srv2cli, + nonce_send: nonce_cli2srv, + nonce_recv: nonce_srv2cli, + addr: addr, + port: port, + # his_port: his_port + } + + GenServer.cast(Shard.Manager, {:peer_up, state.his_pk, self(), addr, port}) + Logger.info "New peer: #{print_id state} at #{inspect addr}:#{port}" - expected_suffix = Application.get_env(:shard, :peer_id_suffix) - len = byte_size(expected_suffix) - ^len = :binary.longest_common_suffix([cli_pkey, expected_suffix]) + {:noreply, state} + end - if srv_pkey == cli_pkey do - exit :normal - end + def handle_cast(:server_handshake, state) do + socket = state.socket - # Connected + net_key = Application.get_env(:shard, :network_key) + {:ok, srv_eph_pk, srv_eph_sk} = :enacl.box_keypair + + srv_longterm_pk = state.my_auth + srv_longterm_sk = Keys.get_sk srv_longterm_pk + + # 1. Client hello + {:ok, cli_hello} = :gen_tcp.recv(socket, 0) + 64 = byte_size cli_hello + cli_hmac = :binary.part cli_hello, 0, 32 + cli_eph_pk = :binary.part cli_hello, 32, 32 + true = :enacl.auth_verify(cli_hmac, cli_eph_pk, net_key) + + # 2. Server hello + {:ok, srv_hello_hmac} = :enacl.auth(srv_eph_pk, net_key) + srv_hello = srv_hello_hmac <> srv_eph_pk + :gen_tcp.send(socket, srv_hello) + + # Shared secret derivation + sh_sec_ab = :enacl.curve25519_scalarmult(srv_eph_sk, cli_eph_pk) + sh_sec_aB = :enacl.curve25519_scalarmult(:enacl.crypto_sign_ed25519_secret_to_curve25519(srv_longterm_sk), cli_eph_pk) + + # 3. Client authenticate + {:ok, cli_auth} = :gen_tcp.recv(socket, 0) + key3 = :crypto.hash(:sha256, net_key <> sh_sec_ab <> sh_sec_aB) + {:ok, cli_auth_plain} = :enacl.secretbox_open(cli_auth, <<0 :: 24*8>>, key3) + 96 = byte_size cli_auth_plain + det_sign_A = :binary.part(cli_auth_plain, 0, 64) + cli_longterm_pk = :binary.part(cli_auth_plain, 64, 32) + true = :enacl.sign_verify_deteached(det_sign_A, net_key <> srv_longterm_pk <> :crypto.hash(:sha256, sh_sec_ab), cli_longterm_pk) + + # Shared secret derivation + sh_sec_Ab = :enacl.curve25519_scalarmult(srv_eph_sk, :enacl.crypto_sign_ed25519_public_to_curve25519(cli_longterm_pk)) + + # TODO: here we can stop if we don't like the client's longterm pk + + # 4. Server accept + det_sign_B = :enacl.sign_detached(net_key <> det_sign_A <> cli_longterm_pk <> :crypto.hash(:sha256, sh_sec_ab), srv_longterm_sk) + key4 = :crypto.hash(:sha256, net_key <> sh_sec_ab <> sh_sec_aB <> sh_sec_Ab) + msg4 = :enacl.secretbox(det_sign_B, <<0 :: 24*8>>, key4) + :gen_tcp.send(socket, msg4) + + # Derive secrets and initial nonces for stream communication + secret_common = :crypto.hash(:sha256, :crypto.hash(:sha256, net_key <> sh_sec_ab <> sh_sec_aB <> sh_sec_Ab)) + secret_cli2srv = :crypto.hash(:sha256, secret_common <> srv_longterm_pk) + secret_srv2cli = :crypto.hash(:sha256, secret_common <> cli_longterm_pk) + {:ok, hmac1} = :enacl.auth(srv_eph_pk, net_key) + nonce_cli2srv = :binary.part(hmac1, 0, 24) + {:ok, hmac2} = :enacl.auth(cli_eph_pk, net_key) + nonce_srv2cli = :binary.part(hmac2, 0, 24) + + # Set up the rest :inet.setopts(socket, [active: true]) - {:ok, {addr, port}} = :inet.peername socket - state =%{ socket: socket, - my_pkey: srv_pkey, - my_skey: srv_skey, - his_pkey: cli_pkey, - conn_my_pkey: sess_pkey, - conn_my_skey: sess_skey, - conn_his_pkey: cli_sess_pkey, - addr: addr, - port: port, - his_port: his_port - } - GenServer.cast(Shard.Manager, {:peer_up, cli_pkey, self(), addr, his_port}) + state = %{ + socket: socket, + my_pk: cli_longterm_pk, + his_pk: srv_longterm_pk, + secret_send: secret_srv2cli, + secret_recv: secret_cli2srv, + nonce_send: nonce_srv2cli, + nonce_recv: nonce_cli2srv, + addr: addr, + port: port, + # his_port: his_port + } + + GenServer.cast(Shard.Manager, {:peer_up, state.his_pk, self(), addr, port}) Logger.info "New peer: #{print_id state} at #{inspect addr}:#{port}" {:noreply, state} @@ -75,6 +189,11 @@ defmodule SNet.TCPConn do {:noreply, state} end + defp next_nonce(nonce) do + i = :crypto.bytes_to_integer(nonce) + <<i+1 :: 24*8>> + end + defp encode_pkt(pkt, pk, sk) do {:ok, n} = Salty.Random.buf Box.noncebytes {:ok, msg} = Box.easy(pkt, n, pk, sk) @@ -91,7 +210,7 @@ defmodule SNet.TCPConn do def handle_info({:tcp, _socket, raw_data}, state) do msg = decode_pkt(raw_data, state.conn_his_pkey, state.conn_my_skey) msg_data = :erlang.binary_to_term(msg, [:safe]) - Shard.Manager.dispatch(state.his_pkey, msg_data) + Shard.Manager.incoming(state.his_pkey, msg_data) {:noreply, state} end @@ -102,7 +221,7 @@ defmodule SNet.TCPConn do end defp print_id(state) do - state.his_pkey + state.his_pk |> binary_part(0, 8) |> Base.encode16 |> String.downcase diff --git a/shard/lib/net/tcpserver.ex b/shard/lib/net/tcpserver.ex index 46552a4..1aa5738 100644 --- a/shard/lib/net/tcpserver.ex +++ b/shard/lib/net/tcpserver.ex @@ -18,7 +18,8 @@ defmodule SNet.TCPServer do defp loop_acceptor(socket, my_port) do {:ok, client} = :gen_tcp.accept(socket) - {:ok, pid} = DynamicSupervisor.start_child(Shard.DynamicSupervisor, {SNet.TCPConn, %{socket: client, my_port: my_port}}) + {:ok, pid} = DynamicSupervisor.start_child(Shard.DynamicSupervisor, + {SNet.TCPConn, %{socket: client, my_port: my_port, is_client: false}}) :ok = :gen_tcp.controlling_process(client, pid) loop_acceptor(socket, my_port) end diff --git a/shard/mix.exs b/shard/mix.exs index 14d0581..5adda9e 100644 --- a/shard/mix.exs +++ b/shard/mix.exs @@ -28,7 +28,8 @@ defmodule Shard.MixProject do {:excoveralls, "~> 0.10", only: :test}, {:ex_doc, "~> 0.19", only: :dev, runtime: false}, - {:salty, "~> 0.1.3", hex: :libsalty}, + {:enacl, git: "https://github.com/jlouis/enacl.git", tag: "0.16.0"}, + # {:salty, "~> 0.1.3", hex: :libsalty}, ] end end diff --git a/shard/mix.lock b/shard/mix.lock index d9fe6a9..e849c04 100644 --- a/shard/mix.lock +++ b/shard/mix.lock @@ -4,6 +4,7 @@ "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"}, "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"}, "elixir_make": {:hex, :elixir_make, "0.4.2", "332c649d08c18bc1ecc73b1befc68c647136de4f340b548844efc796405743bf", [:mix], [], "hexpm"}, + "enacl": {:git, "https://github.com/jlouis/enacl.git", "61be95caadaaceae9f8d0cad7f5149ce3f44b65f", [tag: "0.16.0"]}, "ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [: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.0", "a4508bdd408829f38e7b2519f234b7fd5c83846099cda348efcb5291b081200c", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, |