From 7bdd60c7f99df417b2589f0e99ff16abc8e925c5 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 10 Oct 2018 16:48:38 +0200 Subject: Connecting works, but the rest probably doesnt --- shard/lib/net/auth.ex | 3 + shard/lib/net/tcpconn.ex | 323 +++++++++++++++++++++++++++++------------------ 2 files changed, 203 insertions(+), 123 deletions(-) create mode 100644 shard/lib/net/auth.ex (limited to 'shard/lib/net') 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(<>, 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, <>} = :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) - <> - 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) + <> + 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 -- cgit v1.2.3