aboutsummaryrefslogtreecommitdiff
path: root/shard/lib/net/tcpconn.ex
diff options
context:
space:
mode:
Diffstat (limited to 'shard/lib/net/tcpconn.ex')
-rw-r--r--shard/lib/net/tcpconn.ex211
1 files changed, 165 insertions, 46 deletions
diff --git a/shard/lib/net/tcpconn.ex b/shard/lib/net/tcpconn.ex
index 543341a..35bf9d1 100644
--- a/shard/lib/net/tcpconn.ex
+++ b/shard/lib/net/tcpconn.ex
@@ -1,7 +1,15 @@
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 Salty.Box.Curve25519xchacha20poly1305, as: Box
- require Salty.Sign.Ed25519, as: Sign
+
require Logger
def start_link(state) do
@@ -9,60 +17,166 @@ defmodule SNet.TCPConn do
end
def init(state) do
- GenServer.cast(self(), :handshake)
- {:ok, state}
- end
+ if state.is_client do
+ GenServer.cast(self(), :client_handshake)
+ else
+ GenServer.cast(self(), :server_handshake)
+ end
- def handle_call(:get_host_str, _from, state) do
- {:reply, "#{state.his_pkey|>Base.encode16|>String.downcase}@#{to_string(:inet_parse.ntoa(state.addr))}:#{state.port}", state}
+ {:ok, {addr, port}} = :inet.peername state.socket
+ {:ok, %{state | addr: addr, port: port}}
end
- def handle_cast(:handshake, state) do
- socket = state.socket
- {srv_pkey, srv_skey} = Shard.Keys.get_peer_keypair
- {:ok, sess_pkey, sess_skey} = Box.keypair
- {:ok, challenge} = Salty.Random.buf 32
+ def handle_call(:get_peer_info, _from, state) do
+ {:reply, {:tcp4, state.addr, state.port}, state}
+ end
- # Exchange public keys and challenge
- hello = {srv_pkey, sess_pkey, challenge, state.my_port}
- :gen_tcp.send(socket, :erlang.term_to_binary hello)
- {:ok, pkt} = :gen_tcp.recv(socket, 0)
- {cli_pkey, cli_sess_pkey, cli_challenge, his_port} = :erlang.binary_to_term(pkt, [:safe])
- # Do challenge and check their challenge
- {:ok, cli_challenge_sign} = Sign.sign_detached(cli_challenge, srv_skey)
- pkt = encode_pkt(cli_challenge_sign, cli_sess_pkey, sess_skey)
- :gen_tcp.send(socket, pkt)
+ def handle_cast(:client_handshake, state) do
+ socket = state.socket
- {:ok, pkt} = :gen_tcp.recv(socket, 0)
- challenge_sign = decode_pkt(pkt, cli_sess_pkey, sess_skey)
- :ok = Sign.verify_detached(challenge_sign, challenge, cli_pkey)
+ 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}"
- expected_suffix = Application.get_env(:shard, :peer_id_suffix)
- len = byte_size(expected_suffix)
- ^len = :binary.longest_common_suffix([cli_pkey, expected_suffix])
+ {:noreply, state}
+ end
- if srv_pkey == cli_pkey do
- exit :normal
- end
+ def handle_cast(:server_handshake, state) do
+ socket = state.socket
- # Connected
+ 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_pkey: srv_pkey,
- my_skey: srv_skey,
- his_pkey: cli_pkey,
- conn_my_pkey: sess_pkey,
- conn_my_skey: sess_skey,
- conn_his_pkey: cli_sess_pkey,
- addr: addr,
- port: port,
- his_port: his_port
- }
- GenServer.cast(Shard.Manager, {:peer_up, cli_pkey, self(), addr, his_port})
+ 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}
@@ -75,6 +189,11 @@ defmodule SNet.TCPConn do
{: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)
@@ -91,7 +210,7 @@ defmodule SNet.TCPConn do
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.dispatch(state.his_pkey, msg_data)
+ Shard.Manager.incoming(state.his_pkey, msg_data)
{:noreply, state}
end
@@ -102,7 +221,7 @@ defmodule SNet.TCPConn do
end
defp print_id(state) do
- state.his_pkey
+ state.his_pk
|> binary_part(0, 8)
|> Base.encode16
|> String.downcase