5 Commits

Author SHA1 Message Date
zxq9 853f6b5a8d WIP 2026-06-10 16:53:57 +09:00
uwiger 029292817e Merge pull request 'Ditch enacl, support DH448 and Blake2s, and fix types (#14)' (#2) from hanssv-remove-enoise into master
Reviewed-on: #2
2025-03-30 05:02:52 +09:00
Hans Svensson 2b5f08e156 Ditch enacl, support DH448 and Blake2s, and fix types (#14)
* Remove get_stacktrace (deprecated since OTP-24)

* Add DH448 support and switch to crypto:generate_key for DH25519

* Switch to crypto:hash/2 for Blake2b and support Blake2s

* Switch last enacl calls to crypto - no more enacl

* Eqwalizer fixes

Ewqalizer fix

Eqwalizer fix

Eqwalizer fix

Eqwalizer fix

Eqwalizer support

Eqwalizer fix

Fix tests to follow types (remote keys)

* More error handling on setup

* Dialyzer fix

* Write CHANGELOG

* Note about type-checking in README
2025-03-30 05:02:35 +09:00
Ulf Wiger 91916908a0 Revert "Update enacl dep and fix some minor details (#1)"
This reverts commit 479ec70870.
2025-03-29 20:57:45 +01:00
uwiger 479ec70870 Update enacl dep and fix some minor details (#1)
Co-authored-by: Ulf Wiger <ulf@wiger.net>
Reviewed-on: #1
2025-03-08 00:28:12 +09:00
15 changed files with 295 additions and 220 deletions
+6
View File
@@ -6,8 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Support for 448 DH function and Blake2s hash function.
### Changed
- Using `crypto` over `enacl` (and removing a call to `get_stacktrace/1`) makes `enoise`
up to date for (at least) OTP-27.
- Added test dependency `eqwalizer_support` to enable checking types with Eqwalizer.
### Removed
- The dependency on `enacl` is not needed anymore, OTP's `crypto` library now cover all
necessary operations.
## [1.2.0] - 2021-10-28
### Added
+6
View File
@@ -39,3 +39,9 @@ Test
----
$ rebar3 eunit
Typecheck
---------
$ rebar3 dialyzer
$ elp --eqwalize-all --rebar
+4 -2
View File
@@ -1,8 +1,10 @@
{erl_opts, [debug_info]}.
{plugins, [rebar3_hex]}.
{deps, [{enacl, "1.1.1"}]}.
{profiles, [{test, [{deps, [{jsx, {git, "https://github.com/talentdeficit/jsx.git", {tag, "2.8.0"}}}]}]}
{profiles, [{test, [{deps, [ {jsx, {git, "https://github.com/talentdeficit/jsx.git", {tag, "2.8.0"}}}
, {eqwalizer_support, {git_subdir, "https://github.com/whatsapp/eqwalizer.git", {branch, "main"}, "eqwalizer_support"}}
]}
]}
]}.
{xref_checks, [undefined_function_calls, undefined_functions,
+1 -6
View File
@@ -1,6 +1 @@
{"1.2.0",
[{<<"enacl">>,{pkg,<<"enacl">>,<<"1.1.1">>},0}]}.
[
{pkg_hash,[
{<<"enacl">>, <<"F65DC64D9BFF2D8A534CB77AEF14DA5E7A2FA148987D87856F79A4745C9C2627">>}]}
].
[].
+1 -1
View File
@@ -5,7 +5,7 @@
{applications,
[kernel,
stdlib,
enacl
crypto
]},
{env,[]},
{modules, []},
+175 -128
View File
@@ -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,162 +16,201 @@
-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) ->
HState = create_hstate(Options, Role),
{ok, HState}.
create_hstate(Options, Role).
-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')
%% @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()}.
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) ->
HState = create_hstate(Options, Role),
Timeout = proplists:get_value(timeout, Options, infinity),
do_handshake(HState, ComState, Timeout).
case create_hstate(Options, Role) of
{ok, HState} ->
Timeout = proplists:get_value(timeout, Options, infinity),
do_handshake(HState, ComState, Timeout);
Err = {error, _} ->
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 ->
@@ -246,38 +283,37 @@ 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
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(Protocol),
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),
RS = remote_keypair(DH, proplists:get_value(rs, Options, undefined)),
RE = remote_keypair(DH, proplists:get_value(re, Options, undefined)),
enoise_hs_state:init(Protocol, Role, Prologue, {S, E, RS, RE}).
enoise_hs_state:init(NoiseProtocol, Role,
Prologue, {S, E, RS, RE}).
check_gen_tcp(TcpSock) ->
case inet:getopts(TcpSock, [mode, packet, active, header, packet_size]) of
@@ -287,37 +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, <<Len:16, Msg/binary>>) 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 <<Buf/binary, Data/binary>> of
Buf1 = <<Len:16, Rest/binary>> when byte_size(Rest) < Len ->
gen_tcp_rcv_msg({TcpSock, true, Buf1}, Timeout);
<<Len:16, Rest/binary>> ->
<<Data1:Len/binary, Buf1/binary>> = Rest,
{ok, Data1, {TcpSock, true, Buf1}}
end
receive
{tcp, TcpSock, Data} ->
%% Immediately re-set {active, once}
[inet:setopts(TcpSock, [{active, once}]) || Active == once],
case <<Buf/binary, Data/binary>> of
Buf1 = <<Len:16, Rest/binary>> when byte_size(Rest) < Len ->
gen_tcp_rcv_msg({TcpSock, true, Buf1}, Timeout);
<<Len:16, Rest/binary>> ->
<<Data1:Len/binary, Buf1/binary>> = 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).
+4 -6
View File
@@ -54,12 +54,8 @@ set_nonce(CState = #noise_cs{}, Nonce) ->
encrypt_with_ad(CState = #noise_cs{ k = empty }, _AD, PlainText) ->
{ok, CState, PlainText};
encrypt_with_ad(CState = #noise_cs{ k = K, n = N, cipher = Cipher }, AD, PlainText) ->
case enoise_crypto:encrypt(Cipher, K, N, AD, PlainText) of
Encrypted when is_binary(Encrypted) ->
{ok, CState#noise_cs{ n = N+1 }, Encrypted};
Err = {error, _} ->
Err
end.
CipherText = enoise_crypto:encrypt(Cipher, K, N, AD, PlainText),
{ok, CState#noise_cs{ n = N+1 }, CipherText}.
-spec decrypt_with_ad(CState :: state(), AD :: binary(), CipherText :: binary()) ->
{ok, state(), binary()} | {error, term()}.
@@ -74,6 +70,8 @@ decrypt_with_ad(CState = #noise_cs{ k = K, n = N, cipher = Cipher }, AD, CipherT
end.
-spec rekey(CState :: state()) -> state().
rekey(CState = #noise_cs{ k = empty }) ->
CState;
rekey(CState = #noise_cs{ k = K, cipher = Cipher }) ->
CState#noise_cs{ k = enoise_crypto:rekey(Cipher, K) }.
+30 -30
View File
@@ -29,13 +29,18 @@
%% @doc Perform a Diffie-Hellman calculation with the secret key from `Key1'
%% and the public key from `Key2' with algorithm `Algo'.
-spec dh(Algo :: enoise_hs_state:noise_dh(),
Key1:: keypair(), Key2 :: keypair()) -> binary().
dh(dh25519, Key1, Key2) ->
enacl:curve25519_scalarmult( enoise_keypair:seckey(Key1)
, enoise_keypair:pubkey(Key2));
Key1:: keypair(), Key2 :: keypair()) -> binary().
dh(Type, Key1, Key2) when Type == dh25519; Type == dh448 ->
dh_(ecdh_type(Type), enoise_keypair:pubkey(Key2), enoise_keypair:seckey(Key1));
dh(Type, _Key1, _Key2) ->
error({unsupported_diffie_hellman, Type}).
ecdh_type(dh25519) -> x25519;
ecdh_type(dh448) -> x448.
dh_(DHType, OtherPub, MyPriv) ->
crypto:compute_key(ecdh, OtherPub, MyPriv, DHType).
-spec hmac(Hash :: enoise_sym_state:noise_hash(),
Key :: binary(), Data :: binary()) -> binary().
hmac(Hash, Key, Data) ->
@@ -54,47 +59,42 @@ hkdf(Hash, Key, Data) ->
Output3 = hmac(Hash, TempKey, <<Output2/binary, 3:8>>),
[Output1, Output2, Output3].
-spec rekey(Cipher :: enoise_cipher_state:noise_cipher(),
Key :: binary()) -> binary() | {error, term()}.
-spec rekey(Cipher :: enoise_cipher_state:noise_cipher(), Key :: binary()) -> binary().
rekey('ChaChaPoly', K0) ->
KLen = enacl:aead_chacha20poly1305_ietf_KEYBYTES(),
KLen = 32,
<<K:KLen/binary, _/binary>> = encrypt('ChaChaPoly', K0, ?MAX_NONCE, <<>>, <<0:(32*8)>>),
K;
rekey(Cipher, K) ->
encrypt(Cipher, K, ?MAX_NONCE, <<>>, <<0:(32*8)>>).
-spec encrypt(Cipher :: enoise_cipher_state:noise_cipher(),
Key :: binary(), Nonce :: non_neg_integer(),
Ad :: binary(), PlainText :: binary()) ->
binary() | {error, term()}.
encrypt('ChaChaPoly', K, N, Ad, PlainText) ->
Nonce = <<0:32, N:64/little-unsigned-integer>>,
enacl:aead_chacha20poly1305_ietf_encrypt(PlainText, Ad, Nonce, K);
encrypt('AESGCM', K, N, Ad, PlainText) ->
Nonce = <<0:32, N:64>>,
{CipherText, CipherTag} = crypto:crypto_one_time_aead(aes_256_gcm, K, Nonce, PlainText, Ad, true),
<<CipherText/binary, CipherTag/binary>>.
-spec encrypt(Cipher :: enoise_cipher_state:noise_cipher(), Key :: binary(),
Nonce :: non_neg_integer(), Ad :: binary(), PlainText :: binary()) -> binary().
encrypt(Cipher, K, N, Ad, PlainText) ->
{CText, CTag} = crypto:crypto_one_time_aead(cipher(Cipher), K, nonce(Cipher, N), PlainText, Ad, true),
<<CText/binary, CTag/binary>>.
-spec decrypt(Cipher ::enoise_cipher_state:noise_cipher(),
Key :: binary(), Nonce :: non_neg_integer(),
AD :: binary(), CipherText :: binary()) ->
binary() | {error, term()}.
decrypt('ChaChaPoly', K, N, Ad, CipherText) ->
Nonce = <<0:32, N:64/little-unsigned-integer>>,
enacl:aead_chacha20poly1305_ietf_decrypt(CipherText, Ad, Nonce, K);
decrypt('AESGCM', K, N, Ad, CipherText0) ->
-spec decrypt(Cipher ::enoise_cipher_state:noise_cipher(), Key :: binary(),
Nonce :: non_neg_integer(), AD :: binary(),
CipherText :: binary()) -> binary() | {error, term()}.
decrypt(Cipher, K, N, Ad, CipherText0) ->
CTLen = byte_size(CipherText0) - ?MAC_LEN,
<<CipherText:CTLen/binary, MAC:?MAC_LEN/binary>> = CipherText0,
Nonce = <<0:32, N:64>>,
case crypto:crypto_one_time_aead(aes_256_gcm, K, Nonce, CipherText, Ad, MAC, false) of
<<CText:CTLen/binary, MAC:?MAC_LEN/binary>> = CipherText0,
case crypto:crypto_one_time_aead(cipher(Cipher), K, nonce(Cipher, N), CText, Ad, MAC, false) of
error -> {error, decrypt_failed};
Data -> Data
end.
nonce('ChaChaPoly', N) -> <<0:32, N:64/little-unsigned-integer>>;
nonce('AESGCM', N) -> <<0:32, N:64/big-unsigned-integer>>.
cipher('ChaChaPoly') -> chacha20_poly1305;
cipher('AESGCM') -> aes_256_gcm.
-spec hash(Hash :: enoise_sym_state:noise_hash(), Data :: binary()) -> binary().
hash(blake2s, Data) ->
crypto:hash(blake2s, Data);
hash(blake2b, Data) ->
Hash = enacl:generichash(64, Data), Hash;
crypto:hash(blake2b, Data);
hash(sha256, Data) ->
crypto:hash(sha256, Data);
hash(sha512, Data) ->
+31 -25
View File
@@ -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,11 +20,13 @@
-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()}.
-record(noise_hs, { ss :: enoise_sym_state:state()
, s :: keypair() | undefined
@@ -39,11 +40,8 @@
-opaque state() :: #noise_hs{}.
-export_type([noise_dh/0, noise_role/0, noise_split_state/0, noise_token/0, state/0]).
-spec init(Protocol :: string() | enoise_protocol:protocol(),
Role :: noise_role(), Prologue :: binary(),
Keys :: term()) -> state().
init(ProtocolName, Role, Prologue, Keys) when is_list(ProtocolName) ->
init(enoise_protocol:from_name(ProtocolName), Role, Prologue, Keys);
-spec init(Protocol :: enoise_protocol:protocol(), Role :: noise_role(),
Prologue :: binary(), Keys :: initial_keys()) -> {ok, state()} | {error, term()}.
init(Protocol, Role, Prologue, {S, E, RS, RE}) ->
SS0 = enoise_sym_state:init(Protocol),
SS1 = enoise_sym_state:mix_hash(SS0, Prologue),
@@ -53,11 +51,19 @@ init(Protocol, Role, Prologue, {S, E, RS, RE}) ->
, dh = enoise_protocol:dh(Protocol)
, msgs = enoise_protocol:msgs(Role, Protocol) },
PreMsgs = enoise_protocol:pre_msgs(Role, Protocol),
lists:foldl(fun({out, [s]}, HS0) -> mix_hash(HS0, enoise_keypair:pubkey(S));
({out, [e]}, HS0) -> mix_hash(HS0, enoise_keypair:pubkey(E));
({in, [s]}, HS0) -> mix_hash(HS0, enoise_keypair:pubkey(RS));
({in, [e]}, HS0) -> mix_hash(HS0, enoise_keypair:pubkey(RE))
end, HS, PreMsgs).
pre_mix(PreMsgs, HS).
pre_mix([], HS) -> {ok, HS};
pre_mix([{out, [s]} | Msgs], HS = #noise_hs{ s = S }) when S /= undefined ->
pre_mix(Msgs, mix_hash(HS, enoise_keypair:pubkey(S)));
pre_mix([{out, [e]} | Msgs], HS = #noise_hs{ e = E }) when E /= undefined ->
pre_mix(Msgs, mix_hash(HS, enoise_keypair:pubkey(E)));
pre_mix([{in, [s]} | Msgs], HS = #noise_hs{ rs = RS }) when RS /= undefined ->
pre_mix(Msgs, mix_hash(HS, enoise_keypair:pubkey(RS)));
pre_mix([{in, [e]} | Msgs], HS = #noise_hs{ re = RE }) when RE /= undefined ->
pre_mix(Msgs, mix_hash(HS, enoise_keypair:pubkey(RE)));
pre_mix(_Msg, _HS) ->
{error, invalid_noise_setup}.
-spec finalize(HS :: state()) -> {ok, noise_split_state()} | {error, term()}.
finalize(HS = #noise_hs{ msgs = [], ss = SS, role = Role }) ->
@@ -90,7 +96,7 @@ read_message(HS = #noise_hs{ msgs = [{in, Msg} | Msgs] }, Message) ->
Err = {error, _} -> Err
end.
-spec remote_keys(HS :: state()) -> keypair().
-spec remote_keys(HS :: state()) -> undefined | keypair().
remote_keys(#noise_hs{ rs = RS }) ->
RS.
+9 -7
View File
@@ -30,7 +30,7 @@
%% @doc Generate a new keypair of type `Type'.
-spec new(Type :: key_type()) -> keypair().
new(Type) ->
{Sec, Pub} = new_key_pair(Type),
{Pub, Sec} = new_key_pair(Type),
#kp{ type = Type, sec = Sec, pub = Pub }.
%% @doc Create a new keypair of type `Type'. If `Public' is `undefined'
@@ -69,12 +69,14 @@ seckey(#kp{ sec = S }) ->
S.
%% -- Local functions --------------------------------------------------------
new_key_pair(dh25519) ->
KeyPair = enacl:crypto_sign_ed25519_keypair(),
{enacl:crypto_sign_ed25519_secret_to_curve25519(maps:get(secret, KeyPair)),
enacl:crypto_sign_ed25519_public_to_curve25519(maps:get(public, KeyPair))};
new_key_pair(Type) when Type == dh25519; Type == dh448 ->
crypto:generate_key(ecdh, ecdh_type(Type));
new_key_pair(Type) ->
error({unsupported_key_type, Type}).
pubkey_from_secret(dh25519, Secret) ->
enacl:curve25519_scalarmult_base(Secret).
pubkey_from_secret(Type, Secret) when Type == dh25519; Type == dh448 ->
{Public, Secret} = crypto:generate_key(ecdh, ecdh_type(Type), Secret),
Public.
ecdh_type(dh25519) -> x25519;
ecdh_type(dh448) -> x448.
+7 -7
View File
@@ -19,7 +19,7 @@
, to_name/1]).
-ifdef(TEST).
-export([to_name/4]).
-export([to_name/4, from_name_pattern/1, to_name_pattern/1]).
-endif.
-type noise_pattern() :: nn | kn | nk | kk | nx | kx | xn | in | xk | ik | xx | ix.
@@ -137,9 +137,9 @@ supported_dh(Dh) ->
-spec supported() -> map().
supported() ->
#{ hs_pattern => [nn, kn, nk, kk, nx, kx, xn, in, xk, ik, xx, ix]
, hash => [blake2b, sha256, sha512]
, hash => [blake2s, blake2b, sha256, sha512]
, cipher => ['ChaChaPoly', 'AESGCM']
, dh => [dh25519]
, dh => [dh25519, dh448]
}.
to_name(Pattern, Dh, Cipher, Hash) ->
@@ -148,16 +148,16 @@ to_name(Pattern, Dh, Cipher, Hash) ->
to_name_pattern(Atom) ->
[Simple | Rest] = string:lexemes(atom_to_list(Atom), "_"),
string:uppercase(Simple) ++ lists:join("+", Rest).
lists:flatten(string:uppercase(Simple) ++ lists:join("+", Rest)).
from_name_pattern(String) ->
[Init | Mod2] = string:lexemes(String, "+"),
{Simple, Mod1} = lists:splitwith(fun(C) -> C >= $A andalso C =< $Z end, Init),
list_to_atom(string:lowercase(Simple) ++
list_to_atom(lists:flatten(string:lowercase(Simple) ++
case Mod1 of
"" -> "";
_ -> "_" ++ lists:join([Mod1 | Mod2], "_")
end).
_ -> "_" ++ lists:join("_", [Mod1 | Mod2])
end)).
to_name_dh(dh25519) -> "25519";
to_name_dh(dh448) -> "448".
+2 -1
View File
@@ -43,7 +43,8 @@ noise_test(_Name, Protocol, Init, Resp, Messages, HSHash) ->
SecK = fun(undefined) -> undefined; (Sec) -> enoise_keypair:new(DH, Sec, undefined) end,
PubK = fun(undefined) -> undefined; (Pub) -> enoise_keypair:new(DH, Pub) end,
HSInit = fun(P, R, #{ e := E, s := S, rs := RS, prologue := PL }) ->
enoise_hs_state:init(P, R, PL, {SecK(S), SecK(E), PubK(RS), undefined})
{ok, HS} = enoise_hs_state:init(P, R, PL, {SecK(S), SecK(E), PubK(RS), undefined}),
HS
end,
InitHS = HSInit(Protocol, initiator, Init),
+15 -2
View File
@@ -7,5 +7,18 @@
-include_lib("eunit/include/eunit.hrl").
name_test() ->
?assertMatch(<<"Noise_XK_25519_ChaChaPoly_SHA512">>,
enoise_protocol:to_name(enoise_protocol:from_name("Noise_XK_25519_ChaChaPoly_SHA512"))).
roundtrip("Noise_XK_25519_ChaChaPoly_SHA512"),
roundtrip("Noise_NN_25519_AESGCM_BLAKE2b").
name2_test() ->
Name = "Noise_NXpsk2_25519_AESGCM_SHA512",
?assertError({name_not_recognized, Name}, enoise_protocol:from_name(Name)).
name_pattern_test() ->
Pat = "XKfallback+psk0",
RoundPat = enoise_protocol:to_name_pattern(enoise_protocol:from_name_pattern(Pat)),
?assertEqual(Pat, RoundPat).
roundtrip(Name) ->
ExpectedName = iolist_to_binary(Name),
?assertMatch(ExpectedName, enoise_protocol:to_name(enoise_protocol:from_name(Name))).
+3 -4
View File
@@ -41,10 +41,9 @@ noise_interactive(V = #{ protocol_name := Name }) ->
noise_interactive(_Name, Protocol, Init, Resp, Messages, HSHash) ->
DH = enoise_protocol:dh(Protocol),
SecK = fun(undefined) -> undefined; (Sec) -> enoise_keypair:new(DH, Sec, undefined) end,
PubK = fun(undefined) -> undefined; (Pub) -> enoise_keypair:new(DH, Pub) end,
HSInit = fun(#{ e := E, s := S, rs := RS, prologue := PL }, R) ->
Opts = [{noise, Protocol}, {s, SecK(S)}, {e, SecK(E)}, {rs, PubK(RS)}, {prologue, PL}],
Opts = [{noise, Protocol}, {s, SecK(S)}, {e, SecK(E)}, {rs, RS}, {prologue, PL}],
enoise:handshake(Opts, R)
end,
{ok, InitHS} = HSInit(Init, initiator),
@@ -149,12 +148,12 @@ noise_test_run_(Conf, SKP, CKP) ->
Protocol = enoise_protocol:from_name(Conf),
Port = 4556,
SrvOpts = [{echos, 2}, {cpub, CKP}],
SrvOpts = [{echos, 2}, {cpub, enoise_keypair:pubkey(CKP)}],
EchoSrv = enoise_utils:echo_srv_start(Port, Protocol, SKP, SrvOpts),
{ok, TcpSock} = gen_tcp:connect("localhost", Port, [{active, once}, binary, {reuseaddr, true}], 100),
Opts = [{noise, Protocol}, {s, CKP}] ++ [{rs, SKP} || enoise_utils:need_rs(initiator, Conf) ],
Opts = [{noise, Protocol}, {s, CKP}] ++ [{rs, enoise_keypair:pubkey(SKP)} || enoise_utils:need_rs(initiator, Conf) ],
{ok, EConn, _} = enoise:connect(TcpSock, Opts),
ok = enoise:send(EConn, <<"Hello World!">>),
+1 -1
View File
@@ -26,7 +26,7 @@ echo_srv(Port, Protocol, SKP, SrvOpts) ->
AcceptRes =
try
enoise:accept(TcpSock, Opts)
catch _:R -> gen_tcp:close(TcpSock), {error, {R, erlang:get_stacktrace()}} end,
catch _:R:S -> gen_tcp:close(TcpSock), {error, {R, S}} end,
gen_tcp:close(LSock),