diff options
Diffstat (limited to 'shard/lib/net/tcpconn.ex')
-rw-r--r-- | shard/lib/net/tcpconn.ex | 211 |
1 files changed, 165 insertions, 46 deletions
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 |