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