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

                                                 
                                                                       
 
                                                                                    


     
                                    
 

                





                                                         


                                                                                                  


                                                                                     
                                                 
 
                                                                                             
     




                                           
                                      
                                               

                                               
       
                

     
                                                  
                                    
     
 
                                              
                                        



                                                                                    
 
                                                       
                                                                  

                     
                                                     











                                                                    
 




























                                                                                          
                                                                    

















                                                                                                                              
                                                                                                                                                         





















                                                                                                                    



                                               

                                       
                                                       

                                               




                                                                                         
                     
       
     
 

                                              
 
                                                       
                                                                  








                                                            
                                                     




                                                                    


                                              
 




























                                                                                          














                                                                                                                                  
                                                           






                                                                                                                              
                                                                                                                                           



























                                                                                                                                        
 



                                                                                                                

                     
                                         
                                               

                                       
                                                           

                                               




                                                                                         
                     
       

     




                                            
                                             
                                       
                                                                       
                                    
                                                                    

     




                                                                          
                                                      



                                                                                        


                                                   
                                                                               


                 




                                       
                         







                                                  

     
defmodule SNet.TCPConn do
  @moduledoc"""
  Secret handshake as described in this document:
  <https://ssbc.github.io/scuttlebutt-protocol-guide/#peer-connections>

  TODO: Does not implement the stream protocol, we don't hide the length of packets.
  """


  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, if we are responding to a connection
  - `connect_to`: the IP and port we want to connect to, if we are the initiator of the connection
  - `my_port`: if we are the initiator, 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
    socket = case :gen_tcp.connect(ip, port, [:binary, packet: 2, active: false]) do
      {:ok, socket} -> socket
      _ -> exit(:normal) # ignore connection errors
    end

    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(<<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, {: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(:normal)
    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, <<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, {: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(:normal)
    end
  end

  def handle_cast({:callback, cb}, state) do
    cb.(self())
    {: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_cast(:close, state) do
    Logger.info "Closing: #{print_id state} at #{inspect state.peer_info}"
    exit(:normal)
  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)
    <<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