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