diff options
-rw-r--r-- | shard/lib/app/chat.ex | 22 | ||||
-rw-r--r-- | shard/lib/app/identity.ex | 10 | ||||
-rw-r--r-- | shard/lib/app/pagestore.ex | 10 | ||||
-rw-r--r-- | shard/lib/keys.ex | 20 | ||||
-rw-r--r-- | shard/lib/manager.ex | 139 | ||||
-rw-r--r-- | shard/lib/net/auth.ex | 3 | ||||
-rw-r--r-- | shard/lib/net/tcpconn.ex | 323 | ||||
-rw-r--r-- | shardweb/mix.lock | 1 |
8 files changed, 299 insertions, 229 deletions
diff --git a/shard/lib/app/chat.ex b/shard/lib/app/chat.ex index 054d7f7..0f4d573 100644 --- a/shard/lib/app/chat.ex +++ b/shard/lib/app/chat.ex @@ -101,7 +101,7 @@ defmodule SApp.Chat do send data for this channel if they have some. """ def handle_cast(:init_pull, state) do - for {_, pid, _, _} <- Shard.Manager.list_peers do + for {_, pid, _} <- Shard.Manager.list_connections do GenServer.cast(pid, {:send_msg, {:interested, [state.id]}}) end {:noreply, state} @@ -129,8 +129,8 @@ defmodule SApp.Chat do end notif = {:append, prev_root, msgitem, mst.root} - for peer_id <- Shard.Manager.get_shard_peers(state.id) do - Shard.Manager.send(peer_id, {state.id, nil, notif}) + for peer_info <- Shard.Manager.get_shard_peers(state.id) do + Shard.Manager.send(peer_info, {state.id, nil, notif}) end {:noreply, state} @@ -140,8 +140,8 @@ defmodule SApp.Chat do Implementation of the :interested handler, this is called when a peer we are connected to asks to recieve data for this channel. """ - def handle_cast({:interested, peer_id}, state) do - Shard.Manager.send(peer_id, {state.id, nil, {:root, state.mst.root}}) + def handle_cast({:interested, conn_pid, _auth}, state) do + Shard.Manager.send_pid(conn_pid, {state.id, nil, {:root, state.mst.root}}) {:noreply, state} end @@ -160,10 +160,10 @@ defmodule SApp.Chat do - `{:info, start, list, rest}`: put some messages and informs of the Merkle hash of the store of older messages. """ - def handle_cast({:msg, peer_id, _shard_id, nil, msg}, state) do + def handle_cast({:msg, conn_pid, _auth, _shard_id, nil, msg}, state) do state = case msg do {:get_manifest} -> - Shard.Manager.send(peer_id, {state.id, nil, {:manifest, state.manifest}}) + Shard.Manager.send_pid(conn_pid, {state.id, nil, {:manifest, state.manifest}}) state {:append, prev_root, msgitem, new_root} -> # Append message: one single mesage has arrived @@ -189,7 +189,7 @@ defmodule SApp.Chat do end else # Not a simple one-insertion transition, look at the whole tree - init_merge(state, new_root, peer_id) + init_merge(state, new_root, conn_pid) end else Logger.warn("Received message with invalid signature") @@ -201,7 +201,7 @@ defmodule SApp.Chat do # already up to date, ignore state else - init_merge(state, new_root, peer_id) + init_merge(state, new_root, conn_pid) end x -> Logger.info("Unhandled message: #{inspect x}") @@ -210,7 +210,7 @@ defmodule SApp.Chat do {:noreply, state} end - defp init_merge(state, new_root, source_peer) do + defp init_merge(state, new_root, source_peer_pid) do if new_root == nil do state else @@ -221,7 +221,7 @@ defmodule SApp.Chat do prev_last = for {x, true} <- MST.last(state.mst, nil, 100), into: MapSet.new, do: x mgmst = %{state.mst | root: new_root} - mgmst = put_in(mgmst.store.prefer_ask, [source_peer]) + mgmst = put_in(mgmst.store.prefer_ask, [source_peer_pid]) mst = MST.merge(state.mst, mgmst) new = for {x, true} <- MST.last(mst, nil, 100), diff --git a/shard/lib/app/identity.ex b/shard/lib/app/identity.ex index 204dfb1..de39c6d 100644 --- a/shard/lib/app/identity.ex +++ b/shard/lib/app/identity.ex @@ -87,25 +87,25 @@ defmodule SApp.Identity do end def handle_cast(:init_pull, state) do - for {_, pid, _, _} <- Shard.Manager.list_peers do + for {_, pid, _} <- Shard.Manager.list_connections do GenServer.cast(pid, {:send_msg, {:interested, [state.id]}}) end {:noreply, state} end - def handle_cast({:interested, peer_id}, state) do - Shard.Manager.send(peer_id, {state.id, nil, {:update, SData.SignRev.signed(state.state)}}) + def handle_cast({:interested, peer_pid, _auth}, state) do + Shard.Manager.send_pid(peer_pid, {state.id, nil, {:update, SData.SignRev.signed(state.state)}}) {:noreply, state} end - def handle_cast({:msg, peer_id, _shard_id, nil, msg}, state) do + def handle_cast({:msg, conn_pid, _auth, _shard_id, nil, msg}, state) do state = case msg do {:update, signed} when signed != nil -> case SData.SignRev.merge(state.state, signed, state.pk) do {true, st2} -> Shard.Manager.save_state(state.id, st2) state = put_in(state.state, st2) - bcast_state(state, [peer_id]) + bcast_state(state, [conn_pid]) state {false, _} -> state diff --git a/shard/lib/app/pagestore.ex b/shard/lib/app/pagestore.ex index f093ed4..e09c513 100644 --- a/shard/lib/app/pagestore.ex +++ b/shard/lib/app/pagestore.ex @@ -107,7 +107,7 @@ defmodule SApp.PageStore do case prefer_ask do [_|_] -> for peer <- prefer_ask do - Shard.Manager.send(peer, {state.shard_id, state.path, {:get, key}}) + Shard.Manager.send_pid(peer, {state.shard_id, state.path, {:get, key}}) end _ -> ask_random_peers(state, key) @@ -123,14 +123,14 @@ defmodule SApp.PageStore do {:noreply, state} end - def handle_cast({:msg, peer_id, _shard_id, _path, msg}, state) do + def handle_cast({:msg, conn_pid, _auth, _shard_id, _path, msg}, state) do state = case msg do {:get, key} -> case :dets.lookup state.store, key do [{_, _, bin}] -> - Shard.Manager.send(peer_id, {state.shard_id, state.path, {:info, key, bin}}) + Shard.Manager.send_pid(conn_pid, {state.shard_id, state.path, {:info, key, bin}}) _ -> - Shard.Manager.send(peer_id, {state.shard_id, state.path, {:not_found, key}}) + Shard.Manager.send_pid(conn_pid, {state.shard_id, state.path, {:not_found, key}}) end state {:info, hash, bin} -> @@ -157,7 +157,7 @@ defmodule SApp.PageStore do value = SData.term_unbin bin for dep <- SData.Page.refs value do if :dets.lookup state.store, dep == [] do - init_rec_pull(state, dep, sub_why, [peer_id]) + init_rec_pull(state, dep, sub_why, [conn_pid]) end end end diff --git a/shard/lib/keys.ex b/shard/lib/keys.ex index fe63148..d021242 100644 --- a/shard/lib/keys.ex +++ b/shard/lib/keys.ex @@ -4,7 +4,6 @@ defmodule Shard.Keys do """ use Agent - require Salty.Sign.Ed25519, as: Sign require Logger @key_db [Application.get_env(:shard, :data_path), "key_db"] |> Path.join |> String.to_atom @@ -20,7 +19,7 @@ defmodule Shard.Keys do end defp gen_keypair(suffix, n \\ 0) do - {:ok, pk, sk} = Sign.keypair + %{public: pk, secret: sk} = :enacl.sign_keypair if rem(n, 10000) == 0 do Logger.info "#{n}... expected #{:math.pow(256, byte_size(suffix))}" end @@ -51,7 +50,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} - Shard.Manager.Manifest.start %SApp.Identity.Manifest{pk: pk} + Shard.Manifest.start %SApp.Identity.Manifest{pk: pk} pk end @@ -72,7 +71,7 @@ defmodule Shard.Keys do def sign(pk, bin) do case :dets.lookup @key_db, pk do [{^pk, sk}] -> - Sign.sign(bin, sk) + :enacl.sign(bin, sk) _ -> {:error, :not_found} end end @@ -84,7 +83,7 @@ defmodule Shard.Keys do """ def open(pk, signed) do if valid_identity_pk? pk do - Sign.open(signed, pk) + :enacl.sign_open(signed, pk) else {:error, :invalid_pk_suffix} end @@ -97,6 +96,13 @@ defmodule Shard.Keys do end end + def get_sk(pk) do + case :dets.lookup @key_db, pk do + [{^pk, sk}] -> sk + _ -> nil + end + end + @doc""" Lookup the secret key for a pk and generate a detached signature for a message. @@ -108,7 +114,7 @@ defmodule Shard.Keys do def sign_detached(pk, bin) do case :dets.lookup @key_db, pk do [{^pk, sk}] -> - Sign.sign_detached(bin, sk) + :enacl.sign_detached(bin, sk) _ -> {:error, :not_found} end end @@ -120,7 +126,7 @@ defmodule Shard.Keys do """ def verify(pk, bin, sign) do if valid_identity_pk? pk do - Sign.verify_detached(sign, bin, pk) + :enacl.sign_verify_detached(sign, bin, pk) else {:error, :invalid_pk_suffix} end diff --git a/shard/lib/manager.ex b/shard/lib/manager.ex index ccb750f..dd602bf 100644 --- a/shard/lib/manager.ex +++ b/shard/lib/manager.ex @@ -44,7 +44,7 @@ defmodule Shard.Manager do - :connections (not persistent) List of - { nil | his_pk, nil | my_pk, pid, peer_info } + { peer_info, pid, nil | {my_pk, his_pk} } And an internal table : @@ -108,69 +108,62 @@ defmodule Shard.Manager do {:noreply, state} end - def handle_cast({:interested, peer_info, shards}, state) do + def handle_cast({:interested, conn_pid, peer_info, auth, shards}, state) do for shard_id <- shards do case :dets.lookup(@shard_db, shard_id) do [{ ^shard_id, _, pid }] -> :dets.insert(@peer_db, {shard_id, peer_info}) - GenServer.cast(pid, {:interested, peer_info}) + GenServer.cast(pid, {:interested, conn_pid, auth}) [] -> nil end end {:noreply, state} end - def handle_cast({:not_interested, peer_id, shard_id}, state) do - :dets.match_delete(@shard_peer_db, {shard_id, peer_id}) + def handle_cast({:not_interested, peer_info, shard_id}, state) do + :dets.match_delete(@peer_db, {shard_id, peer_info}) {:noreply, state} end - def handle_cast({:shard_peer_db_insert, shard_id, peer_id}, state) do - :dets.insert(@shard_peer_db, {shard_id, peer_id}) + def handle_cast({:shard_peer_db_insert, shard_id, peer_info}, state) do + :dets.insert(@peer_db, {shard_id, peer_info}) {:noreply, state} end - def handle_cast({:peer_up, pk, pid, ip, port}, state) do - for [pk2] <- :dets.match(@peer_db, {:'$1', :_, ip, port}) do - if pk2 != pk do - # obsolete peer information - :dets.delete(@peer_db, pk2) - :dets.match_delete(@shard_peer_db, {:_, pk2}) - end - end - :dets.insert(@peer_db, {pk, pid, ip, port}) + def handle_cast({:peer_up, pid, peer_info, auth}, state) do + :ets.insert(:connections, {peer_info, pid, auth}) # Send interested message for all our shards id_list = (for [{id, _, _}] <- :dets.match(@shard_db, :"$1"), do: id) GenServer.cast(pid, {:send_msg, {:interested, id_list}}) - # Send queued messages - for {_, msg, _} <- :ets.lookup(state.outbox, pk) do - GenServer.cast(pid, {:send_msg, msg}) - end - :ets.delete(state.outbox, pk) + # # Send queued messages + # for {_, msg, _} <- :ets.lookup(state.outbox, pk) do + # GenServer.cast(pid, {:send_msg, msg}) + # end + # :ets.delete(state.outbox, pk) {:noreply, state} end - def handle_cast({:peer_down, pk, ip, port}, state) do - :dets.insert(@peer_db, {pk, nil, ip, port}) + def handle_cast({:peer_down, peer_pid, peer_info, auth}, state) do + :ets.match_delete(:connections, {peer_info, peer_pid, auth}) {:noreply, state} end - def handle_cast({:connect_and_send, peer_id, msg}, state) do - case :dets.lookup(@peer_db, peer_id) do - [{^peer_id, nil, ip, port}] -> - add_peer(ip, port, state) - currtime = System.os_time :second - :ets.insert(state.outbox, {peer_id, msg, currtime}) - outbox_cleanup = [ {{:_, :_, :'$1'}, [{:<, :'$1', currtime - 60}], [true]} ] - :ets.select_delete(state.outbox, outbox_cleanup) - _ -> - Logger.info "Dropping message #{inspect msg} for peer #{inspect peer_id}: peer not in database" - end - {:noreply, state} - end + # def handle_cast({:connect_and_send, peer_id, msg}, state) do + # case :dets.lookup(@peer_db, peer_id) do + # [{^peer_id, nil, ip, port}] -> + # add_peer(ip, port, state) + # currtime = System.os_time :second + # :ets.insert(state.outbox, {peer_id, msg, currtime}) + # outbox_cleanup = [ {{:_, :_, :'$1'}, [{:<, :'$1', currtime - 60}], [true]} ] + # :ets.select_delete(state.outbox, outbox_cleanup) + # _ -> + # Logger.info "Dropping message #{inspect msg} for peer #{inspect peer_id}: peer not in database" + # end + # {:noreply, state} + # end def handle_cast({:try_connect, pk_list}, state) do for pk <- pk_list do @@ -197,7 +190,7 @@ defmodule Shard.Manager do spawn fn -> case :gen_tcp.connect(ip, port, [:binary, packet: 2, active: false]) do {:ok, client} -> - {:ok, pid} = DynamicSupervisor.start_child(Shard.DynamicSupervisor, {SNet.TCPConn, %{socket: client, my_port: state.my_port}}) + {:ok, pid} = DynamicSupervisor.start_child(Shard.DynamicSupervisor, {SNet.TCPConn, %{socket: client, my_port: state.my_port, is_client: true, auth: nil}}) :ok = :gen_tcp.controlling_process(client, pid) _ -> Logger.info "Could not connect to #{inspect ip}:#{port}, some messages may be dropped" @@ -206,35 +199,34 @@ defmodule Shard.Manager do end - # ==================== - # INTERFACE WITH PEERS - # ==================== + # ====================== + # CALLED BY SNet.TcpConn + # ====================== - def incoming(conn_pid, {:interested, shards}) do - GenServer.cast(__MODULE__, {:interested, peer_id, shards}) + @doc""" + Dispatch incoming message to correct shard process + """ + def incoming(conn_pid, peer_info, auth, {:interested, shards}) do + GenServer.cast(__MODULE__, {:interested, conn_pid, peer_info, auth, shards}) end - def incoming(conn_pid, {:not_interested, shard}) do - GenServer.cast(__MODULE__, {:not_interested, peer_id, shard}) + def incoming(_conn_pid, peer_info, _auth, {:not_interested, shard}) do + GenServer.cast(__MODULE__, {:not_interested, peer_info, shard}) end - @doc""" - Dispatch incoming message to correct shard process - """ - defp dispatch(conn_pid, {shard_id, path, msg}) do - # TODO: auth + def incoming(conn_pid, peer_info, auth, {shard_id, path, msg}) do case :dets.lookup(@shard_db, shard_id) do [] -> - __MODULE__.send(peer_id, {:not_interested, shard_id}) + GenServer.cast(conn_pid, {:send_msg, {:not_interested, shard_id}}) [_] -> - case :dets.match(@shard_peer_db, {shard_id, peer_id}) do + case :dets.match(@peer_db, {shard_id, peer_info}) do [] -> - GenServer.cast(__MODULE__, {:shard_peer_db_insert, shard_id, peer_id}) + GenServer.cast(__MODULE__, {:shard_peer_db_insert, shard_id, peer_info}) _ -> nil end case :ets.lookup(:shard_procs, {shard_id, path}) do [{ {^shard_id, ^path}, pid }] -> - GenServer.cast(pid, {:msg, peer_id, shard_id, path, msg}) + GenServer.cast(pid, {:msg, conn_pid, auth, shard_id, path, msg}) [] -> Logger.info("Warning: dropping message for #{inspect shard_id}/#{inspect path}, no handler running.\n\t#{inspect msg}") end @@ -242,31 +234,22 @@ defmodule Shard.Manager do end - # ===================== - # INTERFACE WITH SHARDS - # ===================== + # ================ + # CALLED BY Sapp.* + # ================ @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 send_pid(pid, msg) do + GenServer.cast(pid, {:send_msg, msg}) end @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. + Send message to a peer specified by peer info. + Opens a connection if necessary. """ - def send(peer_id, my_auth, his_auth, msg) do + def send(_peer_info, _msg) do # TODO end @@ -287,10 +270,10 @@ defmodule Shard.Manager do end @doc""" - Return the list of all peer IDs that are interested in a certain shard + Return the list of all peer info for peers that are interested in a certain shard """ def get_shard_peers(shard_id) do - for [x] <- :dets.match(@shard_peer_db, {shard_id, :"$1"}), do: x + for [x] <- :dets.match(@peer_db, {shard_id, :"$1"}), do: x end @doc""" @@ -311,9 +294,9 @@ defmodule Shard.Manager do end - # ========================== - # INTERFACE FOR OTHER THINGS - # ========================== + # ================ + # CALLED BY ANYONE + # ================ @doc""" Connect to a peer specified by ip address and port @@ -340,9 +323,9 @@ defmodule Shard.Manager do end @doc""" - Return the list of all peers + Return the list of all connected peers """ - def list_peers() do - for [x] <- :dets.match(@peer_db, :"$1"), do: x + def list_connections() do + for [x] <- :dets.match(:connections, :"$1"), do: x end end diff --git a/shard/lib/net/auth.ex b/shard/lib/net/auth.ex new file mode 100644 index 0000000..c903093 --- /dev/null +++ b/shard/lib/net/auth.ex @@ -0,0 +1,3 @@ +defmodule SNet.Auth do + defstruct [:my_pk, :his_pk] +end diff --git a/shard/lib/net/tcpconn.ex b/shard/lib/net/tcpconn.ex index 35bf9d1..aaab9e1 100644 --- a/shard/lib/net/tcpconn.ex +++ b/shard/lib/net/tcpconn.ex @@ -12,6 +12,22 @@ defmodule SNet.TCPConn do require Logger + @doc""" + Start a connection handler on a given socket. + The socket is assumed to be already open. + + Expected initial state: a dict with the following keys: + + - socket: the socket + - is_client: true if we are the initiator of the connection, false otherwise + - my_port: if we are the client, what port should the other dial to recontact us + + Optionnally, and only if we are the initiator of the connection, the following key: + + - auth: nil | {my_pk, list_accepted_his_pk} + + If we are initiator of the connection, we will use crypto if and only if auth is not nil. + """ def start_link(state) do GenServer.start_link(__MODULE__, state) end @@ -23,13 +39,12 @@ defmodule SNet.TCPConn do GenServer.cast(self(), :server_handshake) end - {:ok, {addr, port}} = :inet.peername state.socket - {:ok, %{state | addr: addr, port: port}} + {:ok, state} end def handle_call(:get_peer_info, _from, state) do - {:reply, {:tcp4, state.addr, state.port}, state} + {:reply, state.peer_info, state} end @@ -37,14 +52,10 @@ defmodule SNet.TCPConn do socket = state.socket 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 + %{public: cli_eph_pk, secret: cli_eph_sk} = :enacl.box_keypair # 1. Client hello - {:ok, cli_hello_hmac} = :enacl.auth(cli_eph_pk, net_key) + cli_hello_hmac = :enacl.auth(cli_eph_pk, net_key) cli_hello = cli_hello_hmac <> cli_eph_pk :gen_tcp.send(socket, cli_hello) @@ -57,50 +68,89 @@ defmodule SNet.TCPConn do # 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) + stream_param = case state.auth do + nil -> + # 3. Client doesn't authenticate + bytes = :enacl.randombytes(32) + key3 = :crypto.hash(:sha256, net_key <> sh_sec_ab) + cli_noauth = :enacl.secretbox(bytes, <<0 :: 24*8>>, key3) + :gen_tcp.send(socket, cli_noauth) + + # 4. Server accept + {:ok, srv_accept} = :gen_tcp.recv(socket, 0) + key4 = :crypto.hash(:sha256, sh_sec_ab <> net_key) + {:ok, ^bytes} = :enacl.secretbox_open(srv_accept, <<0 :: 24*8>>, key4) + + # Derive secrets and initial nonces bla bla bla + secret_common = :crypto.hash(:sha256, :crypto.hash(:sha256, net_key <> sh_sec_ab)) + secret_cli2srv = :crypto.hash(:sha256, secret_common <> srv_eph_pk) + secret_srv2cli = :crypto.hash(:sha256, secret_common <> cli_eph_pk) + hmac1 = :enacl.auth(srv_eph_pk, net_key) + nonce_cli2srv = :binary.part(hmac1, 0, 24) + hmac2 = :enacl.auth(cli_eph_pk, net_key) + nonce_srv2cli = :binary.part(hmac2, 0, 24) + %{ + secret_send: secret_cli2srv, + secret_recv: secret_srv2cli, + nonce_send: nonce_cli2srv, + nonce_recv: nonce_srv2cli, + auth: nil, + } + + {cli_longterm_pk, srv_list_pk} -> + [srv_longterm_pk] = srv_list_pk + cli_longterm_sk = Shard.Keys.get_sk cli_longterm_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 = :enacl.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.hash(: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) + hmac1 = :enacl.auth(srv_eph_pk, net_key) + nonce_cli2srv = :binary.part(hmac1, 0, 24) + hmac2 = :enacl.auth(cli_eph_pk, net_key) + nonce_srv2cli = :binary.part(hmac2, 0, 24) + %{ + secret_send: secret_cli2srv, + secret_recv: secret_srv2cli, + nonce_send: nonce_cli2srv, + nonce_recv: nonce_srv2cli, + auth: %SNet.Auth{my_pk: cli_longterm_pk, his_pk: srv_longterm_pk}, + } + end + + # Tell our port + port_msg = :enacl.secretbox(<<state.my_port::16>>, stream_param.nonce_send, stream_param.secret_send) + stream_param = %{stream_param | nonce_send: next_nonce(stream_param.nonce_send)} + :gen_tcp.send(socket, port_msg) # 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}) + state = stream_param + |> Map.put(:socket, socket) + |> Map.put(:peer_info, {:tcp4, addr, port}) + |> Map.put(:my_port, state.my_port) + + GenServer.cast(Shard.Manager, {:peer_up, self(), state.peer_info, state.auth}) Logger.info "New peer: #{print_id state} at #{inspect addr}:#{port}" {:noreply, state} @@ -110,10 +160,7 @@ defmodule SNet.TCPConn do socket = state.socket 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 + %{public: srv_eph_pk, secret: srv_eph_sk} = :enacl.box_keypair # 1. Client hello {:ok, cli_hello} = :gen_tcp.recv(socket, 0) @@ -123,107 +170,137 @@ defmodule SNet.TCPConn do 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_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) + stream_param = case byte_size cli_auth do + 48 -> + # 3. Client does not authenticate + key3 = :crypto.hash(:sha256, net_key <> sh_sec_ab) + {:ok, randbytes} = :enacl.secretbox_open(cli_auth, <<0 :: 24*8>>, key3) + + # 4. Server accept + key4 = :crypto.hash(:sha256, sh_sec_ab <> net_key) + srv_accept = :enacl.secretbox(randbytes, <<0 :: 24*8>>, key4) + :gen_tcp.send(socket, srv_accept) + + # Derive secrets and initial nonces bla bla bla + secret_common = :crypto.hash(:sha256, :crypto.hash(:sha256, net_key <> sh_sec_ab)) + secret_cli2srv = :crypto.hash(:sha256, secret_common <> srv_eph_pk) + secret_srv2cli = :crypto.hash(:sha256, secret_common <> cli_eph_pk) + hmac1 = :enacl.auth(srv_eph_pk, net_key) + nonce_cli2srv = :binary.part(hmac1, 0, 24) + hmac2 = :enacl.auth(cli_eph_pk, net_key) + nonce_srv2cli = :binary.part(hmac2, 0, 24) + %{ + secret_recv: secret_cli2srv, + secret_send: secret_srv2cli, + nonce_recv: nonce_cli2srv, + nonce_send: nonce_srv2cli, + auth: nil, + } + + _ -> + # Client authenticates + srv_longterm_pk = state.my_auth # TODO this is not ok + srv_longterm_sk = Shard.Keys.get_sk srv_longterm_pk + + sh_sec_aB = :enacl.curve25519_scalarmult(:enacl.crypto_sign_ed25519_secret_to_curve25519(srv_longterm_sk), cli_eph_pk) + + 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_detached(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) + hmac1 = :enacl.auth(srv_eph_pk, net_key) + nonce_cli2srv = :binary.part(hmac1, 0, 24) + hmac2 = :enacl.auth(cli_eph_pk, net_key) + nonce_srv2cli = :binary.part(hmac2, 0, 24) + %{ + secret_send: secret_srv2cli, + secret_recv: secret_cli2srv, + nonce_send: nonce_srv2cli, + nonce_recv: nonce_cli2srv, + auth: %SNet.Auth{my_pk: srv_longterm_pk, his_pk: cli_longterm_pk}, + } + end - # 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) + # Receive his actual port + {:ok, port_msg} = :gen_tcp.recv(socket, 0) + {:ok, <<his_port::16>>} = :enacl.secretbox_open(port_msg, stream_param.nonce_recv, stream_param.secret_recv) + stream_param = %{stream_param | nonce_recv: next_nonce(stream_param.nonce_recv)} # 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_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}" + state = stream_param + |> Map.put(:socket, socket) + |> Map.put(:peer_info, {:tcp4, addr, his_port}) + |> Map.put(:my_port, state.my_port) + + GenServer.cast(Shard.Manager, {:peer_up, self(), state.peer_info, state.auth}) + Logger.info "New peer: #{print_id state} at #{inspect state.peer_info} (#{port})" {:noreply, state} end def handle_cast({:send_msg, msg}, state) do msgbin = :erlang.term_to_binary msg - enc = encode_pkt(msgbin, state.conn_his_pkey, state.conn_my_skey) + enc = :enacl.secretbox(msgbin, state.nonce_send, state.secret_send) :gen_tcp.send(state.socket, enc) - {: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) - n <> msg - end - - defp decode_pkt(pkt, pk, sk) do - n = binary_part(pkt, 0, Box.noncebytes) - enc = binary_part(pkt, Box.noncebytes, (byte_size pkt) - Box.noncebytes) - {:ok, msg} = Box.open_easy(enc, n, pk, sk) - msg + {:noreply, %{state | nonce_send: next_nonce(state.nonce_send) }} end 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.incoming(state.his_pkey, msg_data) - {:noreply, state} + {:ok, msgbin} = :enacl.secretbox_open(raw_data, state.nonce_recv, state.secret_recv) + msg_data = :erlang.binary_to_term(msgbin, [:safe]) + Shard.Manager.incoming(self(), state.peer_info, state.auth, msg_data) + {:noreply, %{state | nonce_recv: next_nonce(state.nonce_recv) }} end def handle_info({:tcp_closed, _socket}, state) do - Logger.info "Disconnected: #{print_id state} at #{inspect state.addr}:#{state.port}" - GenServer.cast(Shard.Manager, {:peer_down, state.his_pkey, state.addr, state.his_port}) + Logger.info "Disconnected: #{print_id state} at #{inspect state.peer_info}" + GenServer.cast(Shard.Manager, {:peer_down, self(), state.peer_info, state.auth}) exit(:normal) end + defp next_nonce(nonce) do + i = :crypto.bytes_to_integer(nonce) + <<i+1 :: 24*8>> + end + defp print_id(state) do - state.his_pk - |> binary_part(0, 8) - |> Base.encode16 - |> String.downcase + case state.auth do + nil -> "(no auth)" + %SNet.Auth{my_pk: _my_pk, his_pk: his_pk} -> + his_pk + |> binary_part(0, 8) + |> Base.encode16 + |> String.downcase + end end end diff --git a/shardweb/mix.lock b/shardweb/mix.lock index d6f9fcd..506f863 100644 --- a/shardweb/mix.lock +++ b/shardweb/mix.lock @@ -2,6 +2,7 @@ "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "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"]}, "file_system": {:hex, :file_system, "0.2.6", "fd4dc3af89b9ab1dc8ccbcc214a0e60c41f34be251d9307920748a14bf41f1d3", [:mix], [], "hexpm"}, "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"}, "mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"}, |