diff --git a/src/enoise.erl b/src/enoise.erl index d35fd90..7b5d4b3 100644 --- a/src/enoise.erl +++ b/src/enoise.erl @@ -1,16 +1,14 @@ -%%% ------------------------------------------------------------------ +%%% @copyright 2026, QPQ AG %%% @copyright 2018, Aeternity Anstalt %%% -%%% @doc Module is an interface to the Noise protocol -%%% [https://noiseprotocol.org] -%%% -%%% The module implements Noise handshake in `handshake/3'. +%%% @doc +%%% Interface to the [Noise protocol](https://noiseprotocol.org) %%% %%% For convenience there is also an API to use Noise over TCP (i.e. `gen_tcp') %%% and after "upgrading" a `gen_tcp'-socket into a `enoise'-socket it has a %%% similar API as `gen_tcp'. %%% -%%% @end ------------------------------------------------------------------ +%%% @end -module(enoise). @@ -18,95 +16,106 @@ -export([handshake/2, handshake/3, step_handshake/2]). %% API exports - Mainly mimicing gen_tcp --export([ accept/2 - , close/1 - , connect/2 - , controlling_process/2 - , send/2 - , set_active/2 ]). +-export([accept/2, + close/1, + connect/2, + controlling_process/2, + send/2, + set_active/2]). --record(enoise, { pid }). +-record(enoise, {pid}). --type noise_key() :: binary(). --type noise_keypair() :: enoise_keypair:keypair(). +-type key() :: binary(). +-type keypair() :: enoise_keypair:keypair(). --type noise_options() :: [noise_option()]. +-type options() :: [option()]. %% A list of Noise options is a proplist, it *must* contain a value `noise' %% that describes which Noise configuration to use. It is possible to give a %% `prologue' to the protocol. And for the protocol to work, the correct %% configuration of pre-defined keys (`s', `e', `rs', `re') should also be %% provided. --type noise_option() :: {noise, noise_protocol_option()} %% Required - | {e, noise_keypair()} %% Mandatary depending on `noise' - | {s, noise_keypair()} - | {re, noise_key()} - | {rs, noise_key()} - | {prologue, binary()} %% Optional - | {timeout, integer() | infinity}. %% Optional +-type option() :: {noise, protocol_option()} %% Required + | {e, keypair()} %% Mandatary depending on `noise' + | {s, keypair()} + | {re, key()} + | {rs, key()} + | {prologue, binary()} %% Optional + | {timeout, integer() | infinity}. %% Optional --type noise_protocol_option() :: enoise_protocol:protocol() | string() | -binary(). +-type protocol_option() :: enoise_protocol:protocol() + | string() + | binary(). %% Either an instantiated Noise protocol configuration or the name of a Noise %% configuration (either as a string or a binary string). -type com_state_state() :: term(). %% The state part of a communiction state --type recv_msg_fun() :: fun((com_state_state(), integer() | infinity) -> - {ok, binary(), com_state_state()} | {error, term()}). -%% Function that receive a message +-type timeout() :: pos_integer() | infinity. +-type recv_return() :: {ok, binary(), com_state_state()} + | {error, term()}). +-type recv_msg_fun() :: fun((com_state_state(), timeout()) -> recv_return(). -type send_msg_fun() :: fun((com_state_state(), binary()) -> ok). -%% Function that sends a message --type noise_com_state() :: #{ recv_msg := recv_msg_fun(), - send_msg := send_msg_fun(), - state := term() }. +-type com_state() :: #{recv_msg := recv_msg_fun(), + send_msg := send_msg_fun(), + state := term()}. %% Noise communication state - used to parameterize a handshake. Consists of a %% send function, one receive function, and an internal state. --type noise_split_state() :: enoise_hs_state:noise_split_state(). +-type split_state() :: enoise_hs_state:noise_split_state(). %% Return value from the final `split' operation. Provides a CipherState for %% receiving and a CipherState transmission. Also includes the final handshake %% hash for channel binding. --opaque noise_socket() :: #enoise{}. +-opaque socket() :: #enoise{}. %% An abstract Noise socket - holds a reference to a socket that has completed %% a Noise handshake. --export_type([noise_socket/0]). +-export_type([socket/0]). -%%==================================================================== -%% API functions -%%==================================================================== -%% @doc Start an interactive handshake -%% @end --spec handshake(Options :: noise_options(), - Role :: enoise_hs_state:noise_role()) -> - {ok, enoise_hs_state:state()} | {error, term()}. + +%%% API functions + +-spec handshake(Options, Role) -> Outcome + when Options :: options(), + Role :: enoise_hs_state:noise_role(), + Outcome :: {ok, enoise_hs_state:state()} + | {error, term()}. +%% @doc +%% Start an interactive handshake + handshake(Options, Role) -> create_hstate(Options, Role). -%% @doc Do a step (either `{send, Payload}', `{rcvd, EncryptedData}', -%% or `done') -%% @end --spec step_handshake(HState :: enoise_hs_state:state(), - Data :: {rcvd, binary()} | {send, binary()}) -> - {ok, send, binary(), enoise_hs_state:state()} - | {ok, rcvd, binary(), enoise_hs_state:state()} - | {ok, done, noise_split_state()} - | {error, term()}. + +-spec step_handshake(HState, Data) -> Next + when HState :: enoise_hs_state:state(), + Data :: {rcvd, binary()} + | {send, binary()}, + Next :: {ok, send, binary(), enoise_hs_state:state()} + | {ok, rcvd, binary(), enoise_hs_state:state()} + | {ok, done, split_state()} + | {error, term()}. +%% @doc +%% Do a step (one of `{send, Payload}', `{rcvd, EncryptedData}', or `done') + step_handshake(HState, Data) -> do_step_handshake(HState, Data). -%% @doc Perform a Noise handshake -%% @end --spec handshake(Options :: noise_options(), - Role :: enoise_hs_state:noise_role(), - ComState :: noise_com_state()) -> - {ok, noise_split_state(), noise_com_state()} | {error, term()}. + +-spec handshake(Options, Role, ComState) -> Outcome + when Options :: options(), + Role :: enoise_hs_state:noise_role(), + ComState :: com_state(), + Outcome :: {ok, split_state(), com_state()} + | {error, term()}. +%% @doc +%% Perform a Noise handshake + handshake(Options, Role, ComState) -> case create_hstate(Options, Role) of {ok, HState} -> @@ -116,66 +125,92 @@ handshake(Options, Role, ComState) -> Err end. -%% @doc Upgrades a gen_tcp, or equivalent, connected socket to a Noise socket, + +-spec connect(TcpSock, Options) -> Outcome + when TcpSock :: gen_tcp:socket(), + Options :: options(), + Outcome :: {ok, socket(), enoise_hs_state:state()} + | {error, term()}. +%% @doc +%% Upgrades a gen_tcp, or equivalent, connected socket to a Noise socket, %% that is, performs the client-side noise handshake. %% %% Note: The TCP socket has to be in mode `{active, true}' or `{active, once}', %% passive receive is not supported. %% -%% {@link noise_options()} is a proplist. -%% @end --spec connect(TcpSock :: gen_tcp:socket(), - Options :: noise_options()) -> - {ok, noise_socket(), enoise_hs_state:state()} | {error, term()}. +%% {@link options()} is a proplist. + connect(TcpSock, Options) -> tcp_handshake(TcpSock, initiator, Options). -%% @doc Upgrades a gen_tcp, or equivalent, connected socket to a Noise socket, + +-spec accept(TcpSock, Options) -> Outcome + when TcpSock :: gen_tcp:socket(), + Options :: options(), + Outcome :: {ok, socket(), enoise_hs_state:state()} + | {error, term()}. +%% @doc +%% Upgrades a gen_tcp, or equivalent, connected socket to a Noise socket, %% that is, performs the server-side noise handshake. %% %% Note: The TCP socket has to be in mode `{active, true}' or `{active, once}', %% passive receive is not supported. %% -%% {@link noise_options()} is a proplist. -%% @end --spec accept(TcpSock :: gen_tcp:socket(), - Options :: noise_options()) -> - {ok, noise_socket(), enoise_hs_state:state()} | {error, term()}. +%% {@link options()} is a proplist. + accept(TcpSock, Options) -> tcp_handshake(TcpSock, responder, Options). -%% @doc Writes `Data' to `Socket' -%% @end --spec send(Socket :: noise_socket(), Data :: binary()) -> ok | {error, term()}. + +-spec send(Socket, Data) -> Outcome + when Socket :: socket(), + Data :: binary(), + Outcome :: ok | {error, term()}. +%% @doc +%% Writes `Data' to `Socket' + send(#enoise{ pid = Pid }, Data) -> enoise_connection:send(Pid, Data). -%% @doc Closes a Noise connection. -%% @end --spec close(NoiseSock :: noise_socket()) -> ok | {error, term()}. + +-spec close(NoiseSock) -> Outcome + when NoiseSock :: socket() + Outcome :: ok | {error, term()}. +%% @doc +%% Closes a Noise connection. + close(#enoise{ pid = Pid }) -> enoise_connection:close(Pid). -%% @doc Assigns a new controlling process to the Noise socket. A controlling + +-spec controlling_process(Socket, Pid) -> Outcome + when Socket :: socket(), + Pid :: pid(), + Outcome :: ok | {error, term()}. +%% @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). -%% @doc Set the active option `true | once'. Note that `N' and `false' are + +-spec set_active(Socket, Mode) -> Outcome + when Socket :: socket(), + Mode :: true | once, + Outcome :: ok | {error, term()}. +%% @doc +%% Set the active option `true | once'. Note that `N' and `false' are %% not valid options for a Noise socket. -%% @end --spec set_active(Socket :: noise_socket(), Mode :: true | once) -> - ok | {error, term()}. + set_active(#enoise{ pid = Pid }, ActiveMode) -> enoise_connection:set_active(Pid, ActiveMode). -%%==================================================================== -%% Internal functions -%%==================================================================== + + +%%% Internal functions + do_handshake(HState, ComState, Timeout) -> case enoise_hs_state:next_message(HState) of in -> @@ -248,38 +283,36 @@ tcp_handshake(TcpSock, Role, Options) -> Err end. + do_tcp_handshake(Options, Role, TcpSock, Active) -> - ComState = #{ recv_msg => fun gen_tcp_rcv_msg/2, - send_msg => fun gen_tcp_snd_msg/2, - state => {TcpSock, Active, <<>>} }, + ComState = #{recv_msg => fun gen_tcp_rcv_msg/2, + send_msg => fun gen_tcp_snd_msg/2, + state => {TcpSock, Active, <<>>}}, case handshake(Options, Role, ComState) of - {ok, #{ rx := Rx, tx := Tx, final_state := FState }, #{ state := {_, _, Buf} }} -> + {ok, #{rx := Rx, tx := Tx, final_state := FState}, #{state := {_, _, Buf}}} -> case enoise_connection:start_link(TcpSock, Rx, Tx, self(), {Active, Buf}) of {ok, Pid} -> {ok, #enoise{ pid = Pid }, FState}; - Err = {error, _} -> Err + Error -> Error end; - Err = {error, _} -> - Err + Error -> + Error end. -create_hstate(Options, Role) -> - Prologue = proplists:get_value(prologue, Options, <<>>), - NoiseProtocol0 = proplists:get_value(noise, Options), - NoiseProtocol = - case NoiseProtocol0 of - X when is_binary(X); is_list(X) -> - enoise_protocol:from_name(X); - _ -> NoiseProtocol0 +create_hstate(Options, Role) -> + Prologue = proplists:get_value(prologue, Options, <<>>), + Noise = proplists:get_value(noise, Options), + Protocol = + case is_binary(Noise) orelse is_list(Noise) of + true -> enoise_protocol:from_name(X); + false -> Noise end, - DH = enoise_protocol:dh(NoiseProtocol), + DH = enoise_protocol:dh(Protocol), S = proplists:get_value(s, Options, undefined), E = proplists:get_value(e, Options, undefined), RS = remote_keypair(DH, proplists:get_value(rs, Options, undefined)), RE = remote_keypair(DH, proplists:get_value(re, Options, undefined)), - - enoise_hs_state:init(NoiseProtocol, Role, - Prologue, {S, E, RS, RE}). + enoise_hs_state:init(Protocol, Role, Prologue, {S, E, RS, RE}). check_gen_tcp(TcpSock) -> @@ -290,39 +323,48 @@ check_gen_tcp(TcpSock) -> Header = proplists:get_value(header, TcpOpts, 0), PSize = proplists:get_value(packet_size, TcpOpts, undefined), Mode = proplists:get_value(mode, TcpOpts, binary), - case (Packet == 0 orelse Packet == raw) + case + (Packet == 0 orelse Packet == raw) andalso (Active == true orelse Active == once) - andalso Header == 0 andalso PSize == 0 andalso Mode == binary of + andalso Header == 0 + andalso PSize == 0 + andalso Mode == binary of true -> gen_tcp:controlling_process(TcpSock, self()); false -> {error, {invalid_tcp_options, TcpOpts}} end; - Err = {error, _} -> - Err + Error -> + Error end. + gen_tcp_snd_msg(S = {TcpSock, _, _}, Msg) -> Len = byte_size(Msg), case gen_tcp:send(TcpSock, <>) of - ok -> {ok, S}; - Err = {error, _} -> Err + ok -> {ok, S}; + Error -> Error end. + gen_tcp_rcv_msg({TcpSock, Active, Buf}, Timeout) -> - receive {tcp, TcpSock, Data} -> - %% Immediately re-set {active, once} - [ inet:setopts(TcpSock, [{active, once}]) || Active == once ], - case <> of - Buf1 = <> when byte_size(Rest) < Len -> - gen_tcp_rcv_msg({TcpSock, true, Buf1}, Timeout); - <> -> - <> = Rest, - {ok, Data1, {TcpSock, true, Buf1}} - end + receive + {tcp, TcpSock, Data} -> + %% Immediately re-set {active, once} + [inet:setopts(TcpSock, [{active, once}]) || Active == once], + case <> of + Buf1 = <> when byte_size(Rest) < Len -> + gen_tcp_rcv_msg({TcpSock, true, Buf1}, Timeout); + <> -> + <> = Rest, + {ok, Data1, {TcpSock, true, Buf1}} + end after Timeout -> {error, timeout} end. -remote_keypair(_DH, undefined) -> undefined; -remote_keypair(DH, RemotePub) when is_binary(RemotePub) -> enoise_keypair:new(DH, RemotePub). + +remote_keypair(_DH, undefined) -> + undefined; +remote_keypair(DH, RemotePub) when is_binary(RemotePub) -> + enoise_keypair:new(DH, RemotePub). diff --git a/src/enoise_hs_state.erl b/src/enoise_hs_state.erl index abdfb43..095a7a5 100644 --- a/src/enoise_hs_state.erl +++ b/src/enoise_hs_state.erl @@ -1,19 +1,18 @@ -%%% ------------------------------------------------------------------ +%%% @copyright 2026, QPQ AG %%% @copyright 2018, Aeternity Anstalt %%% -%%% @doc Module encapsulating a Noise handshake state -%%% +%%% @doc +%%% Module encapsulating a Noise handshake state %%% @end -%%% ------------------------------------------------------------------ -module(enoise_hs_state). --export([ finalize/1 - , init/4 - , next_message/1 - , read_message/2 - , remote_keys/1 - , write_message/2]). +-export([finalize/1, + init/4, + next_message/1, + read_message/2, + remote_keys/1, + write_message/2]). -include("enoise.hrl"). @@ -21,10 +20,10 @@ -type noise_dh() :: dh25519 | dh448. -type noise_token() :: s | e | ee | ss | es | se. -type keypair() :: enoise_keypair:keypair(). --type noise_split_state() :: #{ rx := enoise_cipher_state:state(), - tx := enoise_cipher_state:state(), - hs_hash := binary(), - final_state => state() }. +-type noise_split_state() :: #{rx := enoise_cipher_state:state(), + tx := enoise_cipher_state:state(), + hs_hash := binary(), + final_state => state() }. -type optional_key() :: undefined | keypair(). -type initial_keys() :: {optional_key(), optional_key(), optional_key(), optional_key()}.