aboutsummaryrefslogblamecommitdiff
path: root/shard/lib/net/tcpconn.ex
blob: 35bf9d1cc618c8be56810993fe62fd43238825ff (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

  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)
    <<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)
    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