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 def start_link(state) do GenServer.start_link(__MODULE__, state) end def init(state) do if state.is_client do GenServer.cast(self(), :client_handshake) else GenServer.cast(self(), :server_handshake) end {:ok, {addr, port}} = :inet.peername state.socket {:ok, %{state | addr: addr, port: port}} end def handle_call(:get_peer_info, _from, state) do {:reply, {:tcp4, state.addr, state.port}, state} end def handle_cast(:client_handshake, state) 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 # 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}" {:noreply, state} end def handle_cast(:server_handshake, state) 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 # 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_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} 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) :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 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} 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}) exit(:normal) end defp print_id(state) do state.his_pk |> binary_part(0, 8) |> Base.encode16 |> String.downcase end end