From d81f1eb32ef91567e79779a74c8566d163659fda Mon Sep 17 00:00:00 2001 From: Hans Svensson Date: Tue, 6 Mar 2018 16:19:25 +0100 Subject: [PATCH] Introduce enoise_connection This will put the control into a (gen_server) process that wraps the functionality in much the same way as ssl does for gen_tcp, etc. Some features are still missing (like setopts) --- src/enoise.erl | 165 +++++++++++++++++++++++++++----------- src/enoise_connection.erl | 129 +++++++++++++++++++++++++++++ src/enoise_hs_state.erl | 5 +- src/enoise_protocol.erl | 2 +- test/enoise_tests.erl | 13 +-- 5 files changed, 258 insertions(+), 56 deletions(-) create mode 100644 src/enoise_connection.erl diff --git a/src/enoise.erl b/src/enoise.erl index a1c6e67..49f5df5 100644 --- a/src/enoise.erl +++ b/src/enoise.erl @@ -18,73 +18,144 @@ %% , shutdown/2 ]). -compile([export_all, nowarn_export_all]). --include("enoise.hrl"). +-record(enoise, { pid }). --record(enoise, { tcp_sock, rx, tx }). +-type noise_options() :: [{atom(), term()}]. +-opaque noise_socket() :: #enoise{}. -%% -type noise_hs_pattern() :: noiseNN | noiseKN. -%% -type noise_dh() :: dh448 | dh25519. -%% -type noise_cipher() :: 'AESGCM' | 'ChaChaPoly'. -%% -type noise_hash() :: sha256 | sha512 | blake2s | blake2b. - -%% -type noise_protocol() :: #noise_protocol{}. +-export_type([noise_socket/0]). %%==================================================================== %% API functions %%==================================================================== + +%% @doc Upgrades a gen_tcp, or equivalent, connected socket to a Noise socket, +%% that is, performs the client-side noise handshake. +%% @end +-spec connect(TcpSock :: gen_tcp:socket(), + Options :: noise_options()) -> + {ok, noise_socket()} | {error, term()}. connect(TcpSock, Options) -> - do_handshake(TcpSock, initiator, Options). + start_handshake(TcpSock, initiator, Options). +%% @doc Upgrades a gen_tcp, or equivalent, connected socket to a Noise socket, +%% that is, performs the server-side noise handshake. +%% @end +-spec accept(TcpSock :: gen_tcp:socket(), + Options :: noise_options()) -> + {ok, noise_socket()} | {error, term()}. accept(TcpSock, Options) -> - do_handshake(TcpSock, responder, Options). + start_handshake(TcpSock, responder, Options). -send(E = #enoise{ tcp_sock = TcpSock, tx = TX0 }, Msg0) -> - {ok, TX1, Msg1} = enoise_cipher_state:encrypt_with_ad(TX0, <<>>, Msg0), - gen_tcp:send(TcpSock, <<(byte_size(Msg1)):16, Msg1/binary>>), - E#enoise{ tx = TX1 }. +%% @doc Writes `Data` to `Socket` +%% @end +-spec send(Socket :: noise_socket(), Data :: binary()) -> ok | {error, term()}. +send(#enoise{ pid = Pid }, Data) -> + enoise_connection:send(Pid, Data). -recv(E = #enoise{ tcp_sock = TcpSock, rx = RX0 }) -> - receive {tcp, TcpSock, <>} -> - Size = byte_size(Data), - {ok, RX1, Msg1} = enoise_cipher_state:decrypt_with_ad(RX0, <<>>, Data), - {E#enoise{ rx = RX1 }, Msg1} - after 5000 -> error(timeout) end. +%% @doc Receives a packet from a socket in passive mode. A closed socket is +%% indicated by return value `{error, closed}`. +%% +%% Argument `Length` denotes the number of bytes to read. If Length = 0, all +%% available bytes are returned. If Length > 0, exactly Length bytes are +%% returned, or an error; possibly discarding less than Length bytes of data +%% when the socket gets closed from the other side. +%% +%% Optional argument `Timeout` specifies a time-out in milliseconds. The +%% default value is `infinity`. +%% @end +-spec recv(Socket :: noise_socket(), Length :: integer()) -> + {ok, binary()} | {error, term()}. +recv(Socket, Length) -> + recv(Socket, Length, infinity). -close(#enoise{ tcp_sock = TcpSock }) -> - gen_tcp:close(TcpSock). +-spec recv(Socket :: noise_socket(), Length :: integer(), + Timeout :: integer() | infinity) -> + {ok, binary()} | {error, term()}. +recv(#enoise{ pid = Pid }, Length, Timeout) -> + enoise_connection:recv(Pid, Length, Timeout). +%% @doc Closes a Noise connection. +%% @end +-spec close(NoiseSock :: noise_socket()) -> ok | {error, term()}. +close(#enoise{ pid = Pid }) -> + enoise_connection:close(Pid). + +%% @doc Assigns a new controlling process to the Noise socket. A controlling +%% process is the owner of an Noise socket, and receives all messages from the +%% socket. +%% @end +-spec controlling_process(Socket :: noise_socket(), Pid :: pid()) -> + ok | {error, term()}. +controlling_process(#enoise{ pid = Pid }, NewPid) -> + enoise_connection:controlling_process(Pid, NewPid). %%==================================================================== %% Internal functions %%==================================================================== -do_handshake(TcpSock, Role, Options) -> - Prologue = proplists:get_value(prologue, Options, <<>>), - NoiseProtocol = proplists:get_value(noise, Options), +start_handshake(TcpSock, Role, Options) -> + case check_tcp(TcpSock) of + {ok, WasActive} -> + inet:setopts(TcpSock, [{active, false}]), %% False for handshake + Prologue = proplists:get_value(prologue, Options, <<>>), + NoiseProtocol = proplists:get_value(noise, Options), - S = proplists:get_value(s, Options, undefined), - E = proplists:get_value(e, Options, undefined), - RS = proplists:get_value(rs, Options, undefined), - RE = proplists:get_value(re, Options, undefined), + S = proplists:get_value(s, Options, undefined), + E = proplists:get_value(e, Options, undefined), + RS = proplists:get_value(rs, Options, undefined), + RE = proplists:get_value(re, Options, undefined), - HSState = enoise_hs_state:init(NoiseProtocol, Role, Prologue, {S, E, RS, RE}), - do_handshake(TcpSock, HSState). + HSState = enoise_hs_state:init(NoiseProtocol, Role, + Prologue, {S, E, RS, RE}), -do_handshake(TcpSock, HState) -> - case enoise_hs_state:next_message(HState) of - in -> - receive {tcp, TcpSock, Data} -> - {ok, HState1, _Msg} = enoise_hs_state:read_message(HState, Data), - do_handshake(TcpSock, HState1) - after 1000 -> error(timeout) end; - out -> - {ok, HState1, Msg} = enoise_hs_state:write_message(HState, <<>>), - gen_tcp:send(TcpSock, add_len(Msg)), - do_handshake(TcpSock, HState1); - done -> - {ok, #{ rx := Rx, tx := Tx }} = enoise_hs_state:finalize(HState), - {ok, #enoise{ tcp_sock = TcpSock, rx = Rx, tx = Tx }} + do_handshake(TcpSock, HSState, WasActive); + Err = {error, _} -> + Err end. -add_len(Msg) -> +do_handshake(TcpSock, HState, WasActive) -> + case enoise_hs_state:next_message(HState) of + in -> + case hs_recv(TcpSock) of + {ok, Data} -> + {ok, HState1, _Msg} = enoise_hs_state:read_message(HState, Data), + do_handshake(TcpSock, HState1, WasActive); + Err = {error, _} -> + Err + end; + out -> + {ok, HState1, Msg} = enoise_hs_state:write_message(HState, <<>>), + hs_send(TcpSock, Msg), + do_handshake(TcpSock, HState1, WasActive); + done -> + {ok, #{ rx := Rx, tx := Tx }} = enoise_hs_state:finalize(HState), + {ok, Pid} = enoise_connection:start_link(TcpSock, Rx, Tx, self(), WasActive), + {ok, #enoise{ pid = Pid }} + end. + +check_tcp(TcpSock) -> + {ok, TcpOpts} = inet:getopts(TcpSock, [mode, packet, active, header, packet_size]), + Packet = proplists:get_value(packet, TcpOpts, 0), + Header = proplists:get_value(header, TcpOpts, 0), + Active = proplists:get_value(active, TcpOpts, true), + PSize = proplists:get_value(packet_size, TcpOpts, undefined), + Mode = proplists:get_value(mode, TcpOpts, binary), + case (Packet == 0 orelse Packet == raw) + andalso Header == 0 andalso PSize == 0 andalso Mode == binary of + true -> + case gen_tcp:controlling_process(TcpSock, self()) of + ok -> {ok, Active}; + Err = {error, _} -> Err + end; + false -> + {error, {invalid_tcp_options, proplists:delete(active, TcpOpts)}} + end. + +hs_send(TcpSock, Msg) -> Len = byte_size(Msg), - <>. + gen_tcp:send(TcpSock, <>). + +hs_recv(TcpSock) -> + {ok, <>} = gen_tcp:recv(TcpSock, 2, 1000), + gen_tcp:recv(TcpSock, Len, 1000). + diff --git a/src/enoise_connection.erl b/src/enoise_connection.erl new file mode 100644 index 0000000..b90358a --- /dev/null +++ b/src/enoise_connection.erl @@ -0,0 +1,129 @@ +%%%------------------------------------------------------------------- +%%% @copyright (C) 2018, Aeternity Anstalt +%%%------------------------------------------------------------------- + +-module(enoise_connection). + +-export([ close/1 + , recv/3 + , send/2 + , start_link/5 + ]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-record(enoise, { pid }). +-record(state, {rx, tx, owner, tcp_sock, active, buf = <<>>, rawbuf = <<>>}). + +%% -- API -------------------------------------------------------------------- +start_link(TcpSock, Rx, Tx, Owner, Active) -> + inet:setopts(TcpSock, [{active, Active}]), + State = #state{ rx = Rx, tx = Tx, owner = Owner, + tcp_sock = TcpSock, active = Active }, + case gen_server:start_link(?MODULE, [State], []) of + {ok, Pid} -> + ok = gen_tcp:controlling_process(TcpSock, Pid), + {ok, Pid}; + Err = {error, _} -> + Err + end. + +send(Noise, Data) -> + gen_server:call(Noise, {send, Data}). + +recv(Noise, Length, Timeout) -> + gen_server:call(Noise, {recv, Length, Timeout}, Timeout + 100). + +close(Noise) -> + gen_server:call(Noise, close). + +%% -- gen_server callbacks --------------------------------------------------- +init([State]) -> + {ok, State}. + +handle_call({send, Data}, _From, S) -> + {Res, S1} = handle_send(S, Data), + {reply, Res, S1}; +handle_call({recv, _Length, _Timeout}, _From, S = #state{ active = true }) -> + {reply, {error, active_socket}, S}; +handle_call({recv, Length, Timeout}, _From, S) -> + {Res, S1} = handle_recv(S, Length, Timeout), + {reply, Res, S1}; +handle_call(close, _From, S) -> + {stop, normal, ok, S}. + +handle_cast(_Msg, S) -> + {noreply, S}. + +handle_info({tcp, TS, Data}, S = #state{ tcp_sock = TS }) -> + {S1, Msgs} = handle_data(S, Data), + S2 = handle_msgs(S1, Msgs), + {noreply, S2}; +handle_info({tcp_closed, TS}, S = #state{ tcp_sock = TS, active = A, owner = O }) -> + [ O ! {tcp_closed, TS} || A ], + {stop, tcp_closed, S}; +handle_info(Msg, S) -> + io:format("Unexpected info: ~p\n", [Msg]), + {noreply, S}. + +terminate(Reason, #state{ tcp_sock = TcpSock }) -> + [ gen_tcp:close(TcpSock) || Reason /= tcp_closed ], + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + + +%% -- Local functions -------------------------------------------------------- +handle_data(S = #state{ rawbuf = Buf, rx = Rx }, Data) -> + case <> of + B = <> when Len < byte_size(Rest) -> + {S#state{ rawbuf = B }, []}; %% Not a full message - save it + <> -> + <> = Rest, + case enoise_cipher_state:decrypt_with_ad(Rx, <<>>, Msg) of + {ok, Rx1, Msg1} -> + {S1, Msgs} = handle_data(S#state{ rawbuf = Rest2, rx = Rx1 }, <<>>), + {S1, [Msg1 | Msgs]}; + {error, _} -> + error({enoise_error, decrypt_input_failed}) + end; + EmptyOrSingleByte -> + {S#state{ rawbuf = EmptyOrSingleByte }, []} + end. + +handle_msgs(S, []) -> + S; +handle_msgs(S = #state{ active = true, owner = Owner, buf = <<>> }, Msgs) -> + [ Owner ! {noise, #enoise{ pid = self() }, Msg} || Msg <- Msgs ], + S; +handle_msgs(S = #state{ active = true, owner = Owner, buf = Buf }, Msgs) -> + %% First send stuff in buffer (only when switching to active true) + Owner ! {noise, #enoise{ pid = self() }, Buf}, + handle_msgs(S#state{ buf = <<>> }, Msgs); +handle_msgs(S = #state{ buf = Buf }, Msgs) -> + NewBuf = lists:foldl(fun(Msg, B) -> <> end, Buf, Msgs), + S#state{ buf = NewBuf }. + +handle_send(S = #state{ tcp_sock = TcpSock, tx = Tx }, Data) -> + {ok, Tx1, Msg} = enoise_cipher_state:encrypt_with_ad(Tx, <<>>, Data), + gen_tcp:send(TcpSock, <<(byte_size(Msg)):16, Msg/binary>>), + {ok, S#state{ tx = Tx1 }}. + +handle_recv(S = #state{ buf = Buf, rx = Rx, tcp_sock = TcpSock }, Len, TO) + when byte_size(Buf) < Len -> + {ok, <>} = gen_tcp:recv(TcpSock, 2, TO), + {ok, Data} = gen_tcp:recv(TcpSock, MsgLen, TO), + case enoise_cipher_state:decrypt_with_ad(Rx, <<>>, Data) of + {ok, Rx1, Msg1} -> + handle_recv(S#state{ buf = <>, rx = Rx1 }, Len, TO); + {error, _} -> + error({enoise_error, decrypt_input_failed}) + end; +handle_recv(S = #state{ buf = Buf }, Len, _TO) -> + <> = Buf, + {{ok, Data}, S#state{ buf = Buf }}. + + diff --git a/src/enoise_hs_state.erl b/src/enoise_hs_state.erl index 9ada8f5..1e68fb3 100644 --- a/src/enoise_hs_state.erl +++ b/src/enoise_hs_state.erl @@ -17,7 +17,7 @@ , e :: enoise_crypto:key_pair() | undefined , rs :: binary() | undefined , re :: binary() | undefined - , role = initiatior :: noise_role() + , role = initiator :: noise_role() , dh = dh25519 :: noise_dh() , msgs = [] :: [enoise_protocol:noise_msg()] }). @@ -61,8 +61,7 @@ write_message(HS = #noise_hs{ msgs = [{out, Msg} | Msgs] }, PayLoad) -> MsgBuf = <>, {ok, HS2, MsgBuf}. -read_message(HS = #noise_hs{ msgs = [{in, Msg} | Msgs] }, <>) -> - Size = byte_size(Message), +read_message(HS = #noise_hs{ msgs = [{in, Msg} | Msgs] }, Message) -> {HS1, RestBuf1} = read_message(HS#noise_hs{ msgs = Msgs }, Msg, Message), decrypt_and_hash(HS1, RestBuf1). diff --git a/src/enoise_protocol.erl b/src/enoise_protocol.erl index 225b5b3..71581f8 100644 --- a/src/enoise_protocol.erl +++ b/src/enoise_protocol.erl @@ -12,7 +12,7 @@ , pattern/1 , pre_msgs/2 , to_name/1]). --compile(export_all). + -type noise_pattern() :: nn | xk. -type noise_msg() :: {in | out, [enoise_hs_state:noise_token()]}. diff --git a/test/enoise_tests.erl b/test/enoise_tests.erl index c377889..339d253 100644 --- a/test/enoise_tests.erl +++ b/test/enoise_tests.erl @@ -74,7 +74,7 @@ noise_test([M = #{ payload := PL0, ciphertext := CT0 } | Msgs], SendHS, RecvHS, {out, in} -> {ok, SendHS1, Message} = enoise_hs_state:write_message(SendHS, PL), ?assertEqual(CT, Message), - {ok, RecvHS1, PL1} = enoise_hs_state:read_message(RecvHS, <<(byte_size(Message)):16, Message/binary>>), + {ok, RecvHS1, PL1} = enoise_hs_state:read_message(RecvHS, Message), ?assertEqual(PL, PL1), noise_test(Msgs, RecvHS1, SendHS1, HSHash); {done, done} -> @@ -104,7 +104,7 @@ client_test() -> ClientPubKey = <<115,39,86,77,44,85,192,176,202,11,4,6,194,144,127,123, 34,67,62,180,190,232,251,5,216,168,192,190,134,65,13,64>>, ServerPubKey = <<112,91,141,253,183,66,217,102,211,40,13,249,238,51,77,114,163,159,32,1,162,219,76,106,89,164,34,71,149,2,103,59>>, - {ok, TcpSock} = gen_tcp:connect("localhost", 7890, [{active, true}, binary, {reuseaddr, true}], 1000), + {ok, TcpSock} = gen_tcp:connect("localhost", 7890, [{active, false}, binary, {reuseaddr, true}], 1000), gen_tcp:send(TcpSock, <<0,8,0,0,3>>), %% "Noise_XK_25519_ChaChaPoly_Blake2b" Opts = [ {noise, TestProtocol} @@ -113,9 +113,12 @@ client_test() -> , {prologue, <<0,8,0,0,3>>}], {ok, EConn} = enoise:connect(TcpSock, Opts), - EConn1 = enoise:send(EConn, <<"ok\n">>), - {EConn2, <<"ok\n">>} = enoise:recv(EConn1), - enoise:close(EConn2). + ok = enoise:send(EConn, <<"ok\n">>), + %% receive + %% {noise, EConn, <<"ok\n">>} -> ok + %% after 1000 -> error(timeout) end, + {ok, <<"ok\n">>} = enoise:recv(EConn, 3, 1000), + enoise:close(EConn). %% Expects a call-in from a local echo-client (noise-c)