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 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 def init(state) do if Map.has_key?(state, :socket) do GenServer.cast(self(), :server_handshake) else GenServer.cast(self(), :client_handshake) end {:ok, state} end def handle_call(:get_peer_info, _from, state) do {:reply, state.peer_info, state} end def handle_cast(:client_handshake, state) do {:inet, ip, port} = state.connect_to {:ok, socket} = :gen_tcp.connect(ip, port, [:binary, packet: 2, active: false]) net_key = Application.get_env(:shard, :network_key) %{public: cli_eph_pk, secret: cli_eph_sk} = :enacl.box_keypair # 1. Client hello 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) 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, } %SNet.Auth{my_pk: cli_longterm_pk, his_pk: srv_longterm_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) {:ok, _} = :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 = stream_param |> Map.put(:socket, socket) |> Map.put(:peer_info, {:inet, addr, port}) |> Map.put(:my_port, state.my_port) case GenServer.call(SNet.Manager, {:peer_up, self(), state.peer_info, state.auth}) do :ok -> Logger.info "New peer: #{print_id state} at #{inspect addr}:#{port}" {:noreply, state} :redundant -> exit :redundant end end def handle_cast(:server_handshake, state) do socket = state.socket net_key = Application.get_env(:shard, :network_key) %{public: srv_eph_pk, secret: srv_eph_sk} = :enacl.box_keypair # 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 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) # 3. Client authenticate {:ok, cli_auth} = :gen_tcp.recv(socket, 0) 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 = Enum.find( Shard.Keys.list_identities(), fn srv_longterm_pk -> 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) case :enacl.secretbox_open(cli_auth, <<0 :: 24*8>>, key3) do {:ok, _cli_auth_plain} -> true _ -> false end end) if srv_longterm_pk == nil do exit :bad_auth end 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) {:ok, _} = :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 # 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 = stream_param |> Map.put(:socket, socket) |> Map.put(:peer_info, {:inet, addr, his_port}) |> Map.put(:my_port, state.my_port) case GenServer.call(SNet.Manager, {:peer_up, self(), state.peer_info, state.auth}) do :ok -> Logger.info "New peer: #{print_id state} at #{inspect state.peer_info} (#{port})" {:noreply, state} :redundant -> exit(:redundant) end end def handle_cast({:send_msg, msg}, state) do msgbin = :erlang.term_to_binary msg enc = :enacl.secretbox(msgbin, state.nonce_send, state.secret_send) :gen_tcp.send(state.socket, enc) {:noreply, %{state | nonce_send: next_nonce(state.nonce_send) }} end def handle_info({:tcp, _socket, raw_data}, state) do {: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.peer_info}" exit(:normal) end defp next_nonce(nonce) do i = :crypto.bytes_to_integer(nonce) <> end defp print_id(state) do 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