aboutsummaryrefslogblamecommitdiff
path: root/shard/lib/net/tcpconn.ex
blob: 5dbf42b3785286853efdc3ac830d9f045bcbf2cf (plain) (tree)
1
2
3
4
5
6
7
8
9
10
                         








                                                                              
                                    
 

                















                                                                                           




                                           




                                               
 
                

     
 
                                                  
                                    
     
 
 

                                              
 
                                                       
                                                                  

                     
                                                     











                                                                    
 
















































                                                                                                                              
                                                                                                                                                         





















                                                                                                                    



                                               





                                                                                  
                                                                        
 

                     
 

                                              
 
                                                       
                                                                  








                                                            
                                                     




                                                                    


                                              
 






































                                                                                                                              
                                                                                                                                           



























                                                                                                                                        
 



                                                                                                                

                     
                                         
                                               






                                                                                     




                                             
                                       
                                                                       
                                    
                                                                    

     
                                                      



                                                                                        


                                                   

                                                                                    


                 




                                       
                         







                                                  

     
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 state.is_client do
      GenServer.cast(self(), :client_handshake)
    else
      GenServer.cast(self(), :server_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
    socket = state.socket

    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,
        }

      {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)
        {: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(<<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 = 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}
  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 = 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)
        {: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, <<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 = 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 = :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}"
    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
    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