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