1
0
forked from QPQ-AG/enoise

27 Commits

Author SHA1 Message Date
zxq9 a6eaf3e17e WIP 2026-06-12 20:45:55 +09:00
zxq9 d7c8f1ec29 WIP: Reorientation 2026-06-12 11:02:47 +09:00
zxq9 a5da9d08f5 WIP 2026-06-11 23:31:39 +09:00
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: QPQ-AG/enoise#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: QPQ-AG/enoise#1
2025-03-08 00:28:12 +09:00
Hans Svensson 8acbce9269 Merge pull request #13 from aeternity/prepare_1.2.0
Bump version to 1.2.0
2021-10-28 15:46:01 +02:00
Hans Svensson be39bbc464 Bump version to 1.2.0 2021-10-28 15:35:00 +02:00
Hans Svensson dd94b371e6 Merge pull request #12 from aeternity/support-otp-24
Use new crypto:block_encrypt api
2021-10-28 15:05:56 +02:00
Hans Svensson 11ca32c72f Merge pull request #11 from lrascao/fix-rekey
Fix rekey, improve coverage
2021-10-28 15:00:58 +02:00
Sean Hinde 71300ba5b6 Use new crypto:block_encrypt api 2021-10-28 14:54:41 +02:00
Luis Rascao ffde489e53 Fix rekey, improve coverage
ChaChaPoly key is expected to be 256 bits long. It's safe to disregard
the MAC portion.
2021-04-27 15:40:58 +01:00
Hans Svensson 991d7390ea Merge pull request #10 from aeternity/prepare_1.1
Prepare version 1.1.0
2020-09-24 22:25:27 +02:00
Hans Svensson 83fa0d5a00 Bump version to 1.1.0 2020-09-24 22:21:47 +02:00
Hans Svensson 336b331b8a Introduce CHANGELOG.md 2020-09-24 22:21:25 +02:00
Hans Svensson fc4a41f13d Merge pull request #9 from aeternity/misc_improvements
Include cacaphony test vectors
2020-09-24 22:05:38 +02:00
Hans Svensson 3819ba5c0f Merge pull request #8 from helium/madninja/upgrade_enacl
Upgrade enacl to 1.1.1
2020-09-24 21:59:23 +02:00
Hans Svensson 98d18bcaa5 Use cacaphony test vectors 2020-09-24 21:52:14 +02:00
Marc Nijdam a3e803fc1a Upgrade enacl to 1.1.1 2020-09-22 17:44:54 -06:00
Hans Svensson 1e6ee6703f More improved typespecs 2019-01-29 09:08:17 +01:00
Hans Svensson 7c7ad54a6a Improve type specs 2019-01-28 15:05:21 +01:00
Hans Svensson c06bbae07d Bump version to 1.0.1 2018-12-21 13:27:23 +01:00
Hans Svensson 29420d6d63 Merge pull request #6 from aeternity/fix_bad_data_handshake
Fix bad data handshake
2018-12-21 11:00:41 +01:00
Hans Svensson 1ac27a035a Make read_message/read_token more robust 2018-12-21 09:37:52 +01:00
Hans Svensson 6de4d0bf71 Add (and refactor) tests for bad data in handshake 2018-12-21 09:37:52 +01:00
32 changed files with 31127 additions and 15687 deletions
+54
View File
@@ -0,0 +1,54 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [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
### Changed
- Use the new AEAD crypto interface introduced in OTP 22. This makes `enoise` OPT 24 compatible
but it also means it no longer works on OTP 21 and earlier. You can't win them all.
- Fixed ChaChaPoly20 rekey
### Removed
## [1.1.0] - 2020-09-24
### Added
Include [Cacaphony](https://github.com/centromere/cacophony) test vectors.
### Changed
Updated `enacl` to version [1.1.1](https://github.com/jlouis/enacl/releases/tag/v1.1.1).
Fixed some imprecise type specifications.
### Removed
## [1.0.1] - 2018-12-21
### Added
### Changed
Improved argument checks and error handling in handshake (in particular related to empty
hand shake messages).
### Removed
## [1.0] - 2018-10-09
Initial version the following map describe what is supported:
```
#{ hs_pattern => [nn, kn, nk, kk, nx, kx, xn, in, xk, ik, xx, ix]
, hash => [blake2b, sha256, sha512]
, cipher => ['ChaChaPoly', 'AESGCM']
, dh => [dh25519] }
```
[Unreleased]: https://github.com/aeternity/aesophia_cli/compare/v1.2.0...HEAD
[1.2.0]: https://github.com/aeternity/aesophia_cli/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/aeternity/aesophia_cli/compare/v1.0.1...v1.1.0
[1.0.1]: https://github.com/aeternity/aesophia_cli/compare/v1.0.0...v1.0.1
[1.0.0]: https://github.com/aeternity/enoise/releases/tag/v1.0.0
+1
View File
@@ -0,0 +1 @@
{"src/*", [debug_info, {i, "include/"}, {outdir, "ebin/"}]}.
+7 -8
View File
@@ -1,15 +1,14 @@
ISC License
Copyright (c) 2018, aeternity developers
Copyright 2026 Craig Everett <craigeverett@qpq.swiss>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
THE SOFTWARE IS PROVIDED AS IS AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
+2 -37
View File
@@ -1,41 +1,6 @@
enoise
zNoise
=====
An Erlang implementation of the [Noise protocol](https://noiseprotocol.org/)
`enoise` provides a generic handshake mechanism, that can be used in a couple
of different ways. There is also a plain `gen_tcp`-wrapper, where you can
"upgrade" a TCP socket to a Noise socket and use it in much the same way as you
would use `gen_tcp`.
Interactive handshake
---------------------
When using `enoise` to do an interactive handshake, `enoise` will only take
care of message composition/decompositiona and encryption/decryption - i.e. the
user has to do the actual sending and receiving.
An example of the interactive handshake can be seen in the `noise_interactive`
test in `test/enoise_tests.erl`.
Generic handshake
-----------------
There is also the option to use an automated handshake procedure. If provided
with a generic _Communication state_ that describe how data is sent and
received, the handshake procedure is done automatically. The result of a
successful handshake is two Cipher states that can be used to encrypt/decrypt a
RX channel and a TX channel respectively.
The provided `gen_tcp`-wrapper is implemented using the generic handshake, see
`src/enoise.erl`.
Build
-----
$ rebar3 compile
Test
----
$ rebar3 eunit
This is a fork of [enoise](https://git.qpq.swiss/QPQ-AG/enoise).
-11
View File
@@ -1,11 +0,0 @@
{erl_opts, [debug_info]}.
{plugins, [rebar3_hex]}.
{deps, [{enacl, "0.17.2"}]}.
{profiles, [{test, [{deps, [{jsx, {git, "https://github.com/talentdeficit/jsx.git", {tag, "2.8.0"}}}]}]}
]}.
{xref_checks, [undefined_function_calls, undefined_functions,
locals_not_used,
deprecated_function_calls, deprecated_functions]}.
-6
View File
@@ -1,6 +0,0 @@
{"1.1.0",
[{<<"enacl">>,{pkg,<<"enacl">>,<<"0.17.2">>},0}]}.
[
{pkg_hash,[
{<<"enacl">>, <<"4AD59142943E72D72C56E33C30DEDEF28ADD8EBEE79C51033562B0CB4B93EDE0">>}]}
].
-15
View File
@@ -1,15 +0,0 @@
{application, enoise,
[{description, "An Erlang implementation of the Noise protocol"},
{vsn, "1.0.0"},
{registered, []},
{applications,
[kernel,
stdlib,
enacl
]},
{env,[]},
{modules, []},
{maintainers, ["Hans Svensson"]},
{licenses, ["ISC"]},
{links, [{"Github", "https://github.com/aeternity/enoise"}]}
]}.
-325
View File
@@ -1,325 +0,0 @@
%%% ------------------------------------------------------------------
%%% @copyright 2018, Aeternity Anstalt
%%%
%%% @doc Module is an interface to the Noise protocol
%%% [https://noiseprotocol.org]
%%%
%%% The module implements Noise handshake in `handshake/3'.
%%%
%%% 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 ------------------------------------------------------------------
-module(enoise).
%% Main function with generic Noise handshake
-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 ]).
-record(enoise, { pid }).
-type noise_key() :: binary().
-type noise_keypair() :: enoise_keypair:keypair().
-type noise_options() :: [noise_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 noise_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 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() }.
%% Noise communication state - used to parameterize a handshake. Consists of a
%% send function one receive function and an internal state.
-type noise_split_state() :: #{ rx := enoise_cipher_state:state(),
tx := enoise_cipher_state:state(),
hs_hash := binary() }.
%% 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{}.
%% An abstract Noise socket - holds a reference to a socket that has completed
%% a Noise handshake.
-export_type([noise_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()}.
handshake(Options, Role) ->
HState = create_hstate(Options, Role),
{ok, HState}.
%% @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()}.
handshake(Options, Role, ComState) ->
HState = create_hstate(Options, Role),
Timeout = proplists:get_value(timeout, Options, infinity),
do_handshake(HState, ComState, Timeout).
%% @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()}.
connect(TcpSock, Options) ->
tcp_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.
%%
%% 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()} | {error, term()}.
accept(TcpSock, Options) ->
tcp_handshake(TcpSock, responder, Options).
%% @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).
%% @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).
%% @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
%%====================================================================
do_handshake(HState, ComState, Timeout) ->
case enoise_hs_state:next_message(HState) of
in ->
case hs_recv_msg(ComState, Timeout) of
{ok, Data, ComState1} ->
case enoise_hs_state:read_message(HState, Data) of
{ok, HState1, _Msg} ->
do_handshake(HState1, ComState1, Timeout);
Err = {error, _} ->
Err
end;
Err = {error, _} ->
Err
end;
out ->
{ok, HState1, Msg} = enoise_hs_state:write_message(HState, <<>>),
case hs_send_msg(ComState, Msg) of
{ok, ComState1} ->
do_handshake(HState1, ComState1, Timeout);
Err = {error, _} ->
Err
end;
done ->
{ok, Res} = enoise_hs_state:finalize(HState),
{ok, Res, ComState}
end.
hs_recv_msg(CS = #{ recv_msg := Recv, state := S }, Timeout) ->
case Recv(S, Timeout) of
{ok, Data, S1} -> {ok, Data, CS#{ state := S1 }};
Err = {error, _} -> Err
end.
hs_send_msg(CS = #{ send_msg := Send, state := S }, Data) ->
case Send(S, Data) of
{ok, S1} -> {ok, CS#{ state := S1 }};
Err = {error, _} -> Err
end.
do_step_handshake(HState, Data) ->
case {enoise_hs_state:next_message(HState), Data} of
{in, {rcvd, Encrypted}} ->
case enoise_hs_state:read_message(HState, Encrypted) of
{ok, HState1, Msg} ->
{ok, rcvd, Msg, HState1};
Err = {error, _} ->
Err
end;
{out, {send, Payload}} ->
{ok, HState1, Msg} = enoise_hs_state:write_message(HState, Payload),
{ok, send, Msg, HState1};
{done, done} ->
{ok, Res} = enoise_hs_state:finalize(HState),
{ok, done, Res};
{Next, _} ->
{error, {invalid_step, expected, Next, got, Data}}
end.
%% -- gen_tcp specific functions ---------------------------------------------
tcp_handshake(TcpSock, Role, Options) ->
case check_gen_tcp(TcpSock) of
ok ->
case inet:getopts(TcpSock, [active]) of
{ok, [{active, Active}]} ->
do_tcp_handshake(Options, Role, TcpSock, Active);
Err = {error, _} ->
Err
end;
Err = {error, _} ->
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, <<>>} },
case handshake(Options, Role, ComState) of
{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
end;
Err = {error, _} ->
Err
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
end,
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),
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
{ok, TcpOpts} ->
Packet = proplists:get_value(packet, TcpOpts, 0),
Active = proplists:get_value(active, TcpOpts, 0),
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)
andalso (Active == true orelse Active == once)
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
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
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
after Timeout ->
{error, timeout}
end.
-197
View File
@@ -1,197 +0,0 @@
%%% ------------------------------------------------------------------
%%% @copyright 2018, Aeternity Anstalt
%%%
%%% @doc Module implementing a gen_server for holding a handshaked
%%% Noise connection over gen_tcp.
%%%
%%% Some care is needed since the underlying transmission is broken up
%%% into Noise packets, so we need some buffering.
%%%
%%% @end
%%% ------------------------------------------------------------------
-module(enoise_connection).
-export([ controlling_process/2
, close/1
, send/2
, set_active/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, owner_ref, tcp_sock, active, msgbuf = [], rawbuf = <<>>}).
%% -- API --------------------------------------------------------------------
start_link(TcpSock, Rx, Tx, Owner, {Active0, Buf}) ->
Active = case Active0 of
true -> true;
once -> {once, false}
end,
State = #state{ rx = Rx, tx = Tx, owner = Owner,
tcp_sock = TcpSock, active = Active },
case gen_server:start_link(?MODULE, [State], []) of
{ok, Pid} ->
case gen_tcp:controlling_process(TcpSock, Pid) of
ok ->
%% Changing controlling process require a bit of
%% fiddling with already received and delivered content...
[ Pid ! {tcp, TcpSock, Buf} || Buf /= <<>> ],
flush_tcp(Pid, TcpSock),
{ok, Pid};
Err = {error, _} ->
close(Pid),
Err
end;
Err = {error, _} ->
Err
end.
send(Noise, Data) ->
gen_server:call(Noise, {send, Data}).
set_active(Noise, Active) ->
gen_server:call(Noise, {active, self(), Active}).
close(Noise) ->
gen_server:call(Noise, close).
controlling_process(Noise, NewPid) ->
gen_server:call(Noise, {controlling_process, self(), NewPid}, 100).
%% -- gen_server callbacks ---------------------------------------------------
init([#state{owner = Owner} = State]) ->
OwnerRef = erlang:monitor(process, Owner),
{ok, State#state{owner_ref = OwnerRef}}.
handle_call(close, _From, S) ->
{stop, normal, ok, S};
handle_call(_Call, _From, S = #state{ tcp_sock = closed }) ->
{reply, {error, closed}, S};
handle_call({send, Data}, _From, S) ->
{Res, S1} = handle_send(S, Data),
{reply, Res, S1};
handle_call({controlling_process, OldPid, NewPid}, _From, S) ->
{Res, S1} = handle_control_change(S, OldPid, NewPid),
{reply, Res, S1};
handle_call({active, Pid, NewActive}, _From, S) ->
{Res, S1} = handle_active(S, Pid, NewActive),
{reply, Res, S1}.
handle_cast(_Msg, S) ->
{noreply, S}.
handle_info({tcp, TS, Data}, S = #state{ tcp_sock = TS, owner = O }) ->
try
{S1, Msgs} = handle_data(S, Data),
S2 = handle_msgs(S1#state{ msgbuf = S1#state.msgbuf ++ Msgs }),
set_active(S2),
{noreply, S2}
catch error:{enoise_error, _} ->
%% We are not likely to recover, but leave the decision to upstream
O ! {enoise_error, TS, decrypt_error},
{noreply, S}
end;
handle_info({tcp_closed, TS}, S = #state{ tcp_sock = TS, owner = O }) ->
O ! {tcp_closed, TS},
{noreply, S#state{ tcp_sock = closed }};
handle_info({'DOWN', OwnerRef, process, _, normal},
S = #state { tcp_sock = TS, owner_ref = OwnerRef }) ->
close_tcp(TS),
{stop, normal, S#state{ tcp_sock = closed, owner_ref = undefined }};
handle_info({'DOWN', _, _, _, _}, S) ->
%% Ignore non-normal monitor messages - we are linked.
{noreply, S};
handle_info(_Msg, S) ->
{noreply, S}.
terminate(_Reason, #state{ tcp_sock = TcpSock, owner_ref = ORef }) ->
[ gen_tcp:close(TcpSock) || TcpSock /= closed ],
[ erlang:demonitor(ORef, [flush]) || ORef /= undefined ],
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% -- Local functions --------------------------------------------------------
handle_control_change(S = #state{ owner = Pid, owner_ref = OldRef }, Pid, NewPid) ->
NewRef = erlang:monitor(process, NewPid),
erlang:demonitor(OldRef, [flush]),
{ok, S#state{ owner = NewPid, owner_ref = NewRef }};
handle_control_change(S, _OldPid, _NewPid) ->
{{error, not_owner}, S}.
handle_active(S = #state{ owner = Pid, tcp_sock = TcpSock }, Pid, Active) ->
case Active of
true ->
inet:setopts(TcpSock, [{active, true}]),
{ok, handle_msgs(S#state{ active = true })};
once ->
S1 = handle_msgs(S#state{ active = {once, false} }),
set_active(S1),
{ok, S1}
end;
handle_active(S, _Pid, _NewActive) ->
{{error, not_owner}, S}.
handle_data(S = #state{ rawbuf = Buf, rx = Rx }, Data) ->
case <<Buf/binary, Data/binary>> of
B = <<Len:16, Rest/binary>> when Len > byte_size(Rest) ->
{S#state{ rawbuf = B }, []}; %% Not a full Noise message - save it
<<Len:16, Rest/binary>> ->
<<Msg:Len/binary, Rest2/binary>> = 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 = #state{ msgbuf = [] }) ->
S;
handle_msgs(S = #state{ msgbuf = Msgs, active = true, owner = Owner }) ->
[ Owner ! {noise, #enoise{ pid = self() }, Msg} || Msg <- Msgs ],
S#state{ msgbuf = [] };
handle_msgs(S = #state{ msgbuf = [Msg | Msgs], active = {once, Delivered}, owner = Owner }) ->
case Delivered of
true ->
S;
false ->
Owner ! {noise, #enoise{ pid = self() }, Msg},
S#state{ msgbuf = Msgs, active = {once, true} }
end.
handle_send(S = #state{ tcp_sock = TcpSock, tx = Tx }, Data) ->
{ok, Tx1, Msg} = enoise_cipher_state:encrypt_with_ad(Tx, <<>>, Data),
case gen_tcp:send(TcpSock, <<(byte_size(Msg)):16, Msg/binary>>) of
ok -> {ok, S#state{ tx = Tx1 }};
Err = {error, _} -> {Err, S}
end.
set_active(#state{ msgbuf = [], active = {once, _}, tcp_sock = TcpSock }) ->
inet:setopts(TcpSock, [{active, once}]);
set_active(_) ->
ok.
flush_tcp(Pid, TcpSock) ->
receive {tcp, TcpSock, Data} ->
Pid ! {tcp, TcpSock, Data},
flush_tcp(Pid, TcpSock)
after 1 -> ok
end.
close_tcp(closed) ->
ok;
close_tcp(Sock) ->
gen_tcp:close(Sock).
-135
View File
@@ -1,135 +0,0 @@
%%% ------------------------------------------------------------------
%%% @copyright 2018, Aeternity Anstalt
%%%
%%% @doc Module implementing crypto primitives needed by Noise protocol
%%%
%%% @end
%%% ------------------------------------------------------------------
-module(enoise_crypto).
-include("enoise.hrl").
-export([ decrypt/5
, dh/3
, dhlen/1
, encrypt/5
, hash/2
, hashlen/1
, hkdf/3
, hmac/3
, pad/3
, rekey/2
]).
-define(MAC_LEN, 16).
-type keypair() :: enoise_keypair:keypair().
%% @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));
dh(Type, _Key1, _Key2) ->
error({unsupported_diffie_hellman, Type}).
-spec hmac(Hash :: enoise_sym_state:noise_hash(),
Key :: binary(), Data :: binary()) -> binary().
hmac(Hash, Key, Data) ->
BLen = blocklen(Hash),
Block1 = hmac_format_key(Hash, Key, 16#36, BLen),
Hash1 = hash(Hash, <<Block1/binary, Data/binary>>),
Block2 = hmac_format_key(Hash, Key, 16#5C, BLen),
hash(Hash, <<Block2/binary, Hash1/binary>>).
-spec hkdf(Hash :: enoise_sym_state:noise_hash(),
Key :: binary(), Data :: binary()) -> [binary()].
hkdf(Hash, Key, Data) ->
TempKey = hmac(Hash, Key, Data),
Output1 = hmac(Hash, TempKey, <<1:8>>),
Output2 = hmac(Hash, TempKey, <<Output1/binary, 2:8>>),
Output3 = hmac(Hash, TempKey, <<Output2/binary, 3:8>>),
[Output1, Output2, Output3].
-spec rekey(Cipher :: enoise_cipher_state:noise_cipher(),
Key :: binary()) -> binary().
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) ->
enacl:aead_chacha20poly1305_encrypt(K, N, Ad, PlainText);
encrypt('AESGCM', K, N, Ad, PlainText) ->
Nonce = <<0:32, N:64>>,
{CipherText, CipherTag} = crypto:block_encrypt(aes_gcm, K, Nonce, {Ad, PlainText}),
<<CipherText/binary, CipherTag/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) ->
enacl:aead_chacha20poly1305_decrypt(K, N, Ad, CipherText);
decrypt('AESGCM', K, N, Ad, CipherText0) ->
CTLen = byte_size(CipherText0) - ?MAC_LEN,
<<CipherText:CTLen/binary, MAC:?MAC_LEN/binary>> = CipherText0,
Nonce = <<0:32, N:64>>,
crypto:block_decrypt(aes_gcm, K, Nonce, {Ad, CipherText, MAC}).
-spec hash(Hash :: enoise_sym_state:noise_hash(), Data :: binary()) -> binary().
hash(blake2b, Data) ->
{ok, Hash} = enacl:generichash(64, Data), Hash;
hash(sha256, Data) ->
crypto:hash(sha256, Data);
hash(sha512, Data) ->
crypto:hash(sha512, Data);
hash(Hash, _Data) ->
error({hash_not_implemented_yet, Hash}).
-spec pad(Data :: binary(), MinSize :: non_neg_integer(),
PadByte :: integer()) -> binary().
pad(Data, MinSize, PadByte) ->
case byte_size(Data) of
N when N >= MinSize ->
Data;
N ->
PadData = << <<PadByte:8>> || _ <- lists:seq(1, MinSize - N) >>,
<<Data/binary, PadData/binary>>
end.
-spec hashlen(Hash :: enoise_sym_state:noise_hash()) -> non_neg_integer().
hashlen(sha256) -> 32;
hashlen(sha512) -> 64;
hashlen(blake2s) -> 32;
hashlen(blake2b) -> 64.
-spec blocklen(Hash :: enoise_sym_state:noise_hash()) -> non_neg_integer().
blocklen(sha256) -> 64;
blocklen(sha512) -> 128;
blocklen(blake2s) -> 64;
blocklen(blake2b) -> 128.
-spec dhlen(DH :: enoise_hs_state:noise_dh()) -> non_neg_integer().
dhlen(dh25519) -> 32;
dhlen(dh448) -> 56.
%%% Local implementations
hmac_format_key(Hash, Key0, Pad, BLen) ->
Key1 =
case byte_size(Key0) =< BLen of
true -> Key0;
false -> hash(Hash, Key0)
end,
Key2 = pad(Key1, BLen, 0),
<<PadWord:32>> = <<Pad:8, Pad:8, Pad:8, Pad:8>>,
<< <<(Word bxor PadWord):32>> || <<Word:32>> <= Key2 >>.
-170
View File
@@ -1,170 +0,0 @@
%%% ------------------------------------------------------------------
%%% @copyright 2018, Aeternity Anstalt
%%%
%%% @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]).
-include("enoise.hrl").
-type noise_role() :: initiator | responder.
-type noise_dh() :: dh25519 | dh448.
-type noise_token() :: s | e | ee | ss | es | se.
-type keypair() :: enoise_keypair:keypair().
-record(noise_hs, { ss :: enoise_sym_state:state()
, s :: keypair() | undefined
, e :: keypair() | undefined
, rs :: keypair() | undefined
, re :: keypair() | undefined
, role = initiator :: noise_role()
, dh = dh25519 :: noise_dh()
, msgs = [] :: [enoise_protocol:noise_msg()] }).
-opaque state() :: #noise_hs{}.
-export_type([noise_dh/0, noise_role/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);
init(Protocol, Role, Prologue, {S, E, RS, RE}) ->
SS0 = enoise_sym_state:init(Protocol),
SS1 = enoise_sym_state:mix_hash(SS0, Prologue),
HS = #noise_hs{ ss = SS1
, s = S, e = E, rs = RS, re = RE
, role = Role
, 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).
-spec finalize(HS :: state()) -> {ok, map()} | {error, term()}.
finalize(HS = #noise_hs{ msgs = [], ss = SS, role = Role }) ->
{C1, C2} = enoise_sym_state:split(SS),
HSHash = enoise_sym_state:h(SS),
Final = #{ hs_hash => HSHash, final_state => HS },
case Role of
initiator -> {ok, Final#{ tx => C1, rx => C2 }};
responder -> {ok, Final#{ rx => C1, tx => C2 }}
end;
finalize(_) ->
error({bad_state, finalize}).
-spec next_message(HS :: state()) -> in | out | done.
next_message(#noise_hs{ msgs = [{Dir, _} | _] }) -> Dir;
next_message(_) -> done.
-spec write_message(HS :: state(), PayLoad :: binary()) -> {ok, state(), binary()}.
write_message(HS = #noise_hs{ msgs = [{out, Msg} | Msgs] }, PayLoad) ->
{HS1, MsgBuf1} = write_message(HS#noise_hs{ msgs = Msgs }, Msg, <<>>),
{ok, HS2, MsgBuf2} = encrypt_and_hash(HS1, PayLoad),
MsgBuf = <<MsgBuf1/binary, MsgBuf2/binary>>,
{ok, HS2, MsgBuf}.
-spec read_message(HS :: state(), Message :: binary()) ->
{ok, state(), binary()} | {error, term()}.
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).
remote_keys(#noise_hs{ rs = RS }) ->
RS.
write_message(HS, [], MsgBuf) ->
{HS, MsgBuf};
write_message(HS, [Token | Tokens], MsgBuf0) ->
{HS1, MsgBuf1} = write_token(HS, Token),
write_message(HS1, Tokens, <<MsgBuf0/binary, MsgBuf1/binary>>).
read_message(HS, [], Data) ->
{HS, Data};
read_message(HS, [Token | Tokens], Data0) ->
{HS1, Data1} = read_token(HS, Token, Data0),
read_message(HS1, Tokens, Data1).
write_token(HS = #noise_hs{ e = undefined }, e) ->
E = new_key_pair(HS),
PubE = enoise_keypair:pubkey(E),
{mix_hash(HS#noise_hs{ e = E }, PubE), PubE};
%% Should only apply during test - TODO: secure this
write_token(HS = #noise_hs{ e = E }, e) ->
PubE = enoise_keypair:pubkey(E),
{mix_hash(HS, PubE), PubE};
write_token(HS = #noise_hs{ s = S }, s) ->
{ok, HS1, Msg} = encrypt_and_hash(HS, enoise_keypair:pubkey(S)),
{HS1, Msg};
write_token(HS, Token) ->
{K1, K2} = dh_token(HS, Token),
{mix_key(HS, dh(HS, K1, K2)), <<>>}.
read_token(HS = #noise_hs{ re = undefined, dh = DH }, e, Data0) ->
DHLen = enoise_crypto:dhlen(DH),
<<REPub:DHLen/binary, Data1/binary>> = Data0,
RE = enoise_keypair:new(DH, REPub),
{mix_hash(HS#noise_hs{ re = RE }, REPub), Data1};
read_token(HS = #noise_hs{ rs = undefined, dh = DH }, s, Data0) ->
DHLen = case has_key(HS) of
true -> enoise_crypto:dhlen(DH) + 16;
false -> enoise_crypto:dhlen(DH)
end,
<<Temp:DHLen/binary, Data1/binary>> = Data0,
{ok, HS1, RSPub} = decrypt_and_hash(HS, Temp),
RS = enoise_keypair:new(DH, RSPub),
{HS1#noise_hs{ rs = RS }, Data1};
read_token(HS, Token, Data) ->
{K1, K2} = dh_token(HS, Token),
{mix_key(HS, dh(HS, K1, K2)), Data}.
dh_token(#noise_hs{ e = E, re = RE } , ee) -> {E, RE};
dh_token(#noise_hs{ e = E, rs = RS, role = initiator }, es) -> {E, RS};
dh_token(#noise_hs{ s = S, re = RE, role = responder }, es) -> {S, RE};
dh_token(#noise_hs{ s = S, re = RE, role = initiator }, se) -> {S, RE};
dh_token(#noise_hs{ e = E, rs = RS, role = responder }, se) -> {E, RS};
dh_token(#noise_hs{ s = S, rs = RS } , ss) -> {S, RS}.
%% Local wrappers
new_key_pair(#noise_hs{ dh = DH }) ->
enoise_keypair:new(DH).
dh(#noise_hs{ dh = DH }, Key1, Key2) ->
enoise_crypto:dh(DH, Key1, Key2).
has_key(#noise_hs{ ss = SS }) ->
CS = enoise_sym_state:cipher_state(SS),
enoise_cipher_state:has_key(CS).
mix_key(HS = #noise_hs{ ss = SS0 }, Data) ->
HS#noise_hs{ ss = enoise_sym_state:mix_key(SS0, Data) }.
mix_hash(HS = #noise_hs{ ss = SS0 }, Data) ->
HS#noise_hs{ ss = enoise_sym_state:mix_hash(SS0, Data) }.
encrypt_and_hash(HS = #noise_hs{ ss = SS0 }, PlainText) ->
{ok, SS1, CipherText} = enoise_sym_state:encrypt_and_hash(SS0, PlainText),
{ok, HS#noise_hs{ ss = SS1 }, CipherText}.
decrypt_and_hash(HS = #noise_hs{ ss = SS0 }, CipherText) ->
case enoise_sym_state:decrypt_and_hash(SS0, CipherText) of
{ok, SS1, PlainText} ->
{ok, HS#noise_hs{ ss = SS1 }, PlainText};
{error, Reason} ->
{error, Reason}
end.
+377
View File
@@ -0,0 +1,377 @@
%%% @copyright 2026, QPQ AG
%%% @copyright 2018, Aeternity Anstalt
%%%
%%% @doc
%%% Interface to the Noise protocol: https://noiseprotocol.org
%%%
%%% This is a fork of the `enoise' project: https://git.qpq.swiss/QPQ-AG/enoise
%%%
%%% 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 `znoise'-socket it has a
%%% similar API as `gen_tcp'.
%%% @end
-module(znoise).
-vsn("0.1.0").
-author("Craig Everett <craigeverett@qpq.swiss>").
-author("Hans Svensson <hanssv@gmail.com>").
-copyright("QPQ AG <info@qpq.swiss>").
-license("ISC").
%% Main function with generic Noise handshake
-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]).
-record(znoise,
{pid = none | pid()}).
-type key() :: binary().
-type keypair() :: znoise_keypair:keypair().
-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 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 protocol_option() :: znoise_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 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).
-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 split_state() :: znoise_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 socket() :: #znoise{}.
%% An abstract Noise socket - holds a reference to a socket that has completed
%% a Noise handshake.
-export_type([socket/0]).
%%% API functions
-spec handshake(Options, Role) -> Outcome
when Options :: options(),
Role :: znoise_hs_state:noise_role(),
Outcome :: {ok, znoise_hs_state:state()}
| {error, term()}.
%% @doc
%% Start an interactive handshake
handshake(Options, Role) ->
create_hstate(Options, Role).
-spec handshake(Options, Role, ComState) -> Outcome
when Options :: options(),
Role :: znoise_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} ->
Timeout = proplists:get_value(timeout, Options, infinity),
do_handshake(HState, ComState, Timeout);
Err = {error, _} ->
Err
end.
-spec step_handshake(HState, Data) -> Next
when HState :: znoise_hs_state:state(),
Data :: {rcvd, binary()}
| {send, binary()},
Next :: {ok, send, binary(), znoise_hs_state:state()}
| {ok, rcvd, binary(), znoise_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).
-spec connect(TcpSock, Options) -> Outcome
when TcpSock :: gen_tcp:socket(),
Options :: options(),
Outcome :: {ok, socket(), znoise_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 options()} is a proplist.
connect(TcpSock, Options) ->
tcp_handshake(TcpSock, initiator, Options).
-spec accept(TcpSock, Options) -> Outcome
when TcpSock :: gen_tcp:socket(),
Options :: options(),
Outcome :: {ok, socket(), znoise_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 options()} is a proplist.
accept(TcpSock, Options) ->
tcp_handshake(TcpSock, responder, Options).
-spec send(Socket, Data) -> Outcome
when Socket :: socket(),
Data :: binary(),
Outcome :: ok | {error, term()}.
%% @doc
%% Writes `Data' to `Socket'
send(#znoise{ pid = Pid }, Data) ->
znoise_tcp:send(Pid, Data).
-spec close(NoiseSock) -> Outcome
when NoiseSock :: socket()
Outcome :: ok | {error, term()}.
%% @doc
%% Closes a Noise connection.
close(#znoise{ pid = Pid }) ->
znoise_tcp:close(Pid).
-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.
controlling_process(#znoise{pid = PID}, NewPID) ->
znoise_tcp:controlling_process(PID, NewPID).
-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.
set_active(#znoise{ pid = Pid }, ActiveMode) ->
znoise_tcp:set_active(Pid, ActiveMode).
%%% Internal functions
do_handshake(HState, ComState, Timeout) ->
case znoise_hs_state:next_message(HState) of
in ->
case hs_recv_msg(ComState, Timeout) of
{ok, Data, ComState1} ->
case znoise_hs_state:read_message(HState, Data) of
{ok, HState1, _Msg} ->
do_handshake(HState1, ComState1, Timeout);
Err = {error, _} ->
Err
end;
Err = {error, _} ->
Err
end;
out ->
{ok, HState1, Msg} = znoise_hs_state:write_message(HState, <<>>),
case hs_send_msg(ComState, Msg) of
{ok, ComState1} ->
do_handshake(HState1, ComState1, Timeout);
Err = {error, _} ->
Err
end;
done ->
{ok, Res} = znoise_hs_state:finalize(HState),
{ok, Res, ComState}
end.
hs_recv_msg(CS = #{ recv_msg := Recv, state := S }, Timeout) ->
case Recv(S, Timeout) of
{ok, Data, S1} -> {ok, Data, CS#{ state := S1 }};
Err = {error, _} -> Err
end.
hs_send_msg(CS = #{ send_msg := Send, state := S }, Data) ->
case Send(S, Data) of
{ok, S1} -> {ok, CS#{ state := S1 }};
Err = {error, _} -> Err
end.
do_step_handshake(HState, Data) ->
case {znoise_hs_state:next_message(HState), Data} of
{in, {rcvd, Encrypted}} ->
case znoise_hs_state:read_message(HState, Encrypted) of
{ok, HState1, Msg} ->
{ok, rcvd, Msg, HState1};
Err = {error, _} ->
Err
end;
{out, {send, Payload}} ->
{ok, HState1, Msg} = znoise_hs_state:write_message(HState, Payload),
{ok, send, Msg, HState1};
{done, done} ->
{ok, Res} = znoise_hs_state:finalize(HState),
{ok, done, Res};
{Next, _} ->
{error, {invalid_step, expected, Next, got, Data}}
end.
%% -- gen_tcp specific functions ---------------------------------------------
tcp_handshake(TcpSock, Role, Options) ->
case check_gen_tcp(TcpSock) of
ok ->
case inet:getopts(TcpSock, [active]) of
{ok, [{active, Active}]} ->
do_tcp_handshake(Options, Role, TcpSock, Active);
Err = {error, _} ->
Err
end;
Err = {error, _} ->
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, <<>>}},
case handshake(Options, Role, ComState) of
{ok, #{rx := Rx, tx := Tx, final_state := FState}, #{state := {_, _, Buf}}} ->
case znoise_tcp:start_link(TcpSock, Rx, Tx, self(), {Active, Buf}) of
{ok, Pid} -> {ok, #znoise{ pid = Pid }, FState};
Error -> Error
end;
Error ->
Error
end.
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 -> znoise_protocol:from_name(X);
false -> Noise
end,
DH = znoise_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)),
znoise_hs_state:init(Protocol, Role, Prologue, {S, E, RS, RE}).
check_gen_tcp(TcpSock) ->
case inet:getopts(TcpSock, [mode, packet, active, header, packet_size]) of
{ok, TcpOpts} ->
Packet = proplists:get_value(packet, TcpOpts, 0),
Active = proplists:get_value(active, TcpOpts, 0),
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)
andalso (Active == true orelse Active == once)
andalso Header == 0
andalso PSize == 0
andalso Mode == binary of
true ->
gen_tcp:controlling_process(TcpSock, self());
false ->
{error, {invalid_tcp_options, TcpOpts}}
end;
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};
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
after Timeout ->
{error, timeout}
end.
remote_keypair(_DH, undefined) ->
undefined;
remote_keypair(DH, RemotePub) when is_binary(RemotePub) ->
znoise_keypair:new(DH, RemotePub).
@@ -1,12 +1,16 @@
%%% ------------------------------------------------------------------
%%% @copyright 2026, QPQ AG
%%% @copyright 2018, Aeternity Anstalt
%%%
%%% @doc Module encapsulating a Noise Cipher state
%%%
%%% @doc
%%% Module encapsulating a Noise Cipher state
%%% @end
%%% ------------------------------------------------------------------
-module(enoise_cipher_state).
-module(znoise_cipher_state).
-vsn("0.1.0").
-author("Craig Everett <craigeverett@qpq.swiss>").
-author("Hans Svensson <hanssv@gmail.com>").
-copyright("QPQ AG <info@qpq.swiss>").
-license("ISC").
-export([ cipher/1
, decrypt_with_ad/3
@@ -19,7 +23,7 @@
, set_nonce/2
]).
-include("enoise.hrl").
-include("znoise.hrl").
-type noise_cipher() :: 'ChaChaPoly' | 'AESGCM'.
-type nonce() :: non_neg_integer().
@@ -50,18 +54,19 @@ set_nonce(CState = #noise_cs{}, Nonce) ->
CState#noise_cs{ n = Nonce }.
-spec encrypt_with_ad(CState :: state(), AD :: binary(), PlainText :: binary()) ->
{ok, state(), binary()}.
{ok, state(), binary()} | {error, term()}.
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) ->
{ok, CState#noise_cs{ n = N+1 }, enoise_crypto:encrypt(Cipher, K, N, AD, PlainText)}.
CipherText = znoise_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()}.
decrypt_with_ad(CState = #noise_cs{ k = empty }, _AD, CipherText) ->
{ok, CState, CipherText};
decrypt_with_ad(CState = #noise_cs{ k = K, n = N, cipher = Cipher }, AD, CipherText) ->
case enoise_crypto:decrypt(Cipher, K, N, AD, CipherText) of
case znoise_crypto:decrypt(Cipher, K, N, AD, CipherText) of
PlainText when is_binary(PlainText) ->
{ok, CState#noise_cs{ n = N+1 }, PlainText};
Err = {error, _} ->
@@ -69,8 +74,10 @@ 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) }.
CState#noise_cs{ k = znoise_crypto:rekey(Cipher, K) }.
-spec cipher(CState :: state()) -> noise_cipher().
cipher(#noise_cs{ cipher = Cipher }) ->
+148
View File
@@ -0,0 +1,148 @@
%%% @copyright 2026, QPQ AG
%%% @copyright 2018, Aeternity Anstalt
%%%
%%% @doc
%%% Module implementing crypto primitives needed by Noise protocol
%%% @end
-module(znoise_crypto).
-vsn("0.1.0").
-author("Craig Everett <craigeverett@qpq.swiss>").
-author("Hans Svensson <hanssv@gmail.com>").
-copyright("QPQ AG <info@qpq.swiss>").
-license("ISC").
-include("znoise.hrl").
-export([ decrypt/5
, dh/3
, dhlen/1
, encrypt/5
, hash/2
, hashlen/1
, hkdf/3
, hmac/3
, pad/3
, rekey/2
]).
-define(MAC_LEN, 16).
-type keypair() :: znoise_keypair:keypair().
%% @doc Perform a Diffie-Hellman calculation with the secret key from `Key1'
%% and the public key from `Key2' with algorithm `Algo'.
-spec dh(Algo :: znoise_hs_state:noise_dh(),
Key1:: keypair(), Key2 :: keypair()) -> binary().
dh(Type, Key1, Key2) when Type == dh25519; Type == dh448 ->
dh_(ecdh_type(Type), znoise_keypair:pubkey(Key2), znoise_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 :: znoise_sym_state:noise_hash(),
Key :: binary(), Data :: binary()) -> binary().
hmac(Hash, Key, Data) ->
BLen = blocklen(Hash),
Block1 = hmac_format_key(Hash, Key, 16#36, BLen),
Hash1 = hash(Hash, <<Block1/binary, Data/binary>>),
Block2 = hmac_format_key(Hash, Key, 16#5C, BLen),
hash(Hash, <<Block2/binary, Hash1/binary>>).
-spec hkdf(Hash :: znoise_sym_state:noise_hash(),
Key :: binary(), Data :: binary()) -> [binary()].
hkdf(Hash, Key, Data) ->
TempKey = hmac(Hash, Key, Data),
Output1 = hmac(Hash, TempKey, <<1:8>>),
Output2 = hmac(Hash, TempKey, <<Output1/binary, 2:8>>),
Output3 = hmac(Hash, TempKey, <<Output2/binary, 3:8>>),
[Output1, Output2, Output3].
-spec rekey(Cipher :: znoise_cipher_state:noise_cipher(), Key :: binary()) -> binary().
rekey('ChaChaPoly', K0) ->
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 :: znoise_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 ::znoise_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,
<<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 :: znoise_sym_state:noise_hash(), Data :: binary()) -> binary().
hash(blake2s, Data) ->
crypto:hash(blake2s, Data);
hash(blake2b, Data) ->
crypto:hash(blake2b, Data);
hash(sha256, Data) ->
crypto:hash(sha256, Data);
hash(sha512, Data) ->
crypto:hash(sha512, Data);
hash(Hash, _Data) ->
error({hash_not_implemented_yet, Hash}).
-spec pad(Data :: binary(), MinSize :: non_neg_integer(),
PadByte :: integer()) -> binary().
pad(Data, MinSize, PadByte) ->
case byte_size(Data) of
N when N >= MinSize ->
Data;
N ->
PadData = << <<PadByte:8>> || _ <- lists:seq(1, MinSize - N) >>,
<<Data/binary, PadData/binary>>
end.
-spec hashlen(Hash :: znoise_sym_state:noise_hash()) -> non_neg_integer().
hashlen(sha256) -> 32;
hashlen(sha512) -> 64;
hashlen(blake2s) -> 32;
hashlen(blake2b) -> 64.
-spec blocklen(Hash :: znoise_sym_state:noise_hash()) -> non_neg_integer().
blocklen(sha256) -> 64;
blocklen(sha512) -> 128;
blocklen(blake2s) -> 64;
blocklen(blake2b) -> 128.
-spec dhlen(DH :: znoise_hs_state:noise_dh()) -> non_neg_integer().
dhlen(dh25519) -> 32;
dhlen(dh448) -> 56.
%%% Local implementations
hmac_format_key(Hash, Key0, Pad, BLen) ->
Key1 =
case byte_size(Key0) =< BLen of
true -> Key0;
false -> hash(Hash, Key0)
end,
Key2 = pad(Key1, BLen, 0),
<<PadWord:32>> = <<Pad:8, Pad:8, Pad:8, Pad:8>>,
<< <<(Word bxor PadWord):32>> || <<Word:32>> <= Key2 >>.
+203
View File
@@ -0,0 +1,203 @@
%%% @copyright 2026, QPQ AG
%%% @copyright 2018, Aeternity Anstalt
%%%
%%% @doc
%%% Module encapsulating a Noise handshake state
%%% @end
-module(znoise_hs_state).
-vsn("0.1.0").
-author("Craig Everett <craigeverett@qpq.swiss>").
-author("Hans Svensson <hanssv@gmail.com>").
-copyright("QPQ AG <info@qpq.swiss>").
-license("ISC").
-export([finalize/1,
init/4,
next_message/1,
read_message/2,
remote_keys/1,
write_message/2]).
-include("znoise.hrl").
-type noise_role() :: initiator | responder.
-type noise_dh() :: dh25519 | dh448.
-type noise_token() :: s | e | ee | ss | es | se.
-type keypair() :: znoise_keypair:keypair().
-type noise_split_state() :: #{rx := znoise_cipher_state:state(),
tx := znoise_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 :: znoise_sym_state:state()
, s :: keypair() | undefined
, e :: keypair() | undefined
, rs :: keypair() | undefined
, re :: keypair() | undefined
, role = initiator :: noise_role()
, dh = dh25519 :: noise_dh()
, msgs = [] :: [znoise_protocol:noise_msg()] }).
-opaque state() :: #noise_hs{}.
-export_type([noise_dh/0, noise_role/0, noise_split_state/0, noise_token/0, state/0]).
-spec init(Protocol :: znoise_protocol:protocol(), Role :: noise_role(),
Prologue :: binary(), Keys :: initial_keys()) -> {ok, state()} | {error, term()}.
init(Protocol, Role, Prologue, {S, E, RS, RE}) ->
SS0 = znoise_sym_state:init(Protocol),
SS1 = znoise_sym_state:mix_hash(SS0, Prologue),
HS = #noise_hs{ ss = SS1
, s = S, e = E, rs = RS, re = RE
, role = Role
, dh = znoise_protocol:dh(Protocol)
, msgs = znoise_protocol:msgs(Role, Protocol) },
PreMsgs = znoise_protocol:pre_msgs(Role, Protocol),
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, znoise_keypair:pubkey(S)));
pre_mix([{out, [e]} | Msgs], HS = #noise_hs{ e = E }) when E /= undefined ->
pre_mix(Msgs, mix_hash(HS, znoise_keypair:pubkey(E)));
pre_mix([{in, [s]} | Msgs], HS = #noise_hs{ rs = RS }) when RS /= undefined ->
pre_mix(Msgs, mix_hash(HS, znoise_keypair:pubkey(RS)));
pre_mix([{in, [e]} | Msgs], HS = #noise_hs{ re = RE }) when RE /= undefined ->
pre_mix(Msgs, mix_hash(HS, znoise_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 }) ->
{C1, C2} = znoise_sym_state:split(SS),
HSHash = znoise_sym_state:h(SS),
Final = #{ hs_hash => HSHash, final_state => HS },
case Role of
initiator -> {ok, Final#{ tx => C1, rx => C2 }};
responder -> {ok, Final#{ rx => C1, tx => C2 }}
end;
finalize(_) ->
error({bad_state, finalize}).
-spec next_message(HS :: state()) -> in | out | done.
next_message(#noise_hs{ msgs = [{Dir, _} | _] }) -> Dir;
next_message(#noise_hs{ }) -> done.
-spec write_message(HS :: state(), PayLoad :: binary()) -> {ok, state(), binary()}.
write_message(HS = #noise_hs{ msgs = [{out, Msg} | Msgs] }, PayLoad) ->
{HS1, MsgBuf1} = write_message(HS#noise_hs{ msgs = Msgs }, Msg, <<>>),
{ok, HS2, MsgBuf2} = encrypt_and_hash(HS1, PayLoad),
MsgBuf = <<MsgBuf1/binary, MsgBuf2/binary>>,
{ok, HS2, MsgBuf}.
-spec read_message(HS :: state(), Message :: binary()) ->
{ok, state(), binary()} | {error, term()}.
read_message(HS = #noise_hs{ msgs = [{in, Msg} | Msgs] }, Message) ->
case read_message(HS#noise_hs{ msgs = Msgs }, Msg, Message) of
{ok, HS1, RestBuf1} -> decrypt_and_hash(HS1, RestBuf1);
Err = {error, _} -> Err
end.
-spec remote_keys(HS :: state()) -> undefined | keypair().
remote_keys(#noise_hs{ rs = RS }) ->
RS.
write_message(HS, [], MsgBuf) ->
{HS, MsgBuf};
write_message(HS, [Token | Tokens], MsgBuf0) ->
{HS1, MsgBuf1} = write_token(HS, Token),
write_message(HS1, Tokens, <<MsgBuf0/binary, MsgBuf1/binary>>).
read_message(HS, [], Data) ->
{ok, HS, Data};
read_message(HS, [Token | Tokens], Data0) ->
case read_token(HS, Token, Data0) of
{ok, HS1, Data1} -> read_message(HS1, Tokens, Data1);
Err = {error, _} -> Err
end.
write_token(HS = #noise_hs{ e = undefined }, e) ->
E = new_key_pair(HS),
PubE = znoise_keypair:pubkey(E),
{mix_hash(HS#noise_hs{ e = E }, PubE), PubE};
%% Should only apply during test - TODO: secure this
write_token(HS = #noise_hs{ e = E }, e) ->
PubE = znoise_keypair:pubkey(E),
{mix_hash(HS, PubE), PubE};
write_token(HS = #noise_hs{ s = S }, s) ->
{ok, HS1, Msg} = encrypt_and_hash(HS, znoise_keypair:pubkey(S)),
{HS1, Msg};
write_token(HS, Token) ->
{K1, K2} = dh_token(HS, Token),
{mix_key(HS, dh(HS, K1, K2)), <<>>}.
read_token(HS = #noise_hs{ re = undefined, dh = DH }, e, Data0) ->
DHLen = znoise_crypto:dhlen(DH),
case Data0 of
<<REPub:DHLen/binary, Data1/binary>> ->
RE = znoise_keypair:new(DH, REPub),
{ok, mix_hash(HS#noise_hs{ re = RE }, REPub), Data1};
_ ->
{error, {bad_data, {failed_to_read_token, e, DHLen}}}
end;
read_token(HS = #noise_hs{ rs = undefined, dh = DH }, s, Data0) ->
DHLen = case has_key(HS) of
true -> znoise_crypto:dhlen(DH) + 16;
false -> znoise_crypto:dhlen(DH)
end,
case Data0 of
<<Temp:DHLen/binary, Data1/binary>> ->
case decrypt_and_hash(HS, Temp) of
{ok, HS1, RSPub} ->
RS = znoise_keypair:new(DH, RSPub),
{ok, HS1#noise_hs{ rs = RS }, Data1};
Err = {error, _} ->
Err
end;
_ ->
{error, {bad_data, {failed_to_read_token, s, DHLen}}}
end;
read_token(HS, Token, Data) ->
{K1, K2} = dh_token(HS, Token),
{ok, mix_key(HS, dh(HS, K1, K2)), Data}.
dh_token(#noise_hs{ e = E, re = RE } , ee) -> {E, RE};
dh_token(#noise_hs{ e = E, rs = RS, role = initiator }, es) -> {E, RS};
dh_token(#noise_hs{ s = S, re = RE, role = responder }, es) -> {S, RE};
dh_token(#noise_hs{ s = S, re = RE, role = initiator }, se) -> {S, RE};
dh_token(#noise_hs{ e = E, rs = RS, role = responder }, se) -> {E, RS};
dh_token(#noise_hs{ s = S, rs = RS } , ss) -> {S, RS}.
%% Local wrappers
new_key_pair(#noise_hs{ dh = DH }) ->
znoise_keypair:new(DH).
dh(#noise_hs{ dh = DH }, Key1, Key2) ->
znoise_crypto:dh(DH, Key1, Key2).
has_key(#noise_hs{ ss = SS }) ->
CS = znoise_sym_state:cipher_state(SS),
znoise_cipher_state:has_key(CS).
mix_key(HS = #noise_hs{ ss = SS0 }, Data) ->
HS#noise_hs{ ss = znoise_sym_state:mix_key(SS0, Data) }.
mix_hash(HS = #noise_hs{ ss = SS0 }, Data) ->
HS#noise_hs{ ss = znoise_sym_state:mix_hash(SS0, Data) }.
encrypt_and_hash(HS = #noise_hs{ ss = SS0 }, PlainText) ->
{ok, SS1, CipherText} = znoise_sym_state:encrypt_and_hash(SS0, PlainText),
{ok, HS#noise_hs{ ss = SS1 }, CipherText}.
decrypt_and_hash(HS = #noise_hs{ ss = SS0 }, CipherText) ->
case znoise_sym_state:decrypt_and_hash(SS0, CipherText) of
{ok, SS1, PlainText} ->
{ok, HS#noise_hs{ ss = SS1 }, PlainText};
{error, Reason} ->
{error, Reason}
end.
@@ -1,12 +1,16 @@
%%% ------------------------------------------------------------------
%%% @copyright 2026, QPQ AG
%%% @copyright 2018, Aeternity Anstalt
%%%
%%% @doc Module is an abstract data type for a key pair.
%%%
%%% @doc
%%% Module is an abstract data type for a key pair.
%%% @end
%%% ------------------------------------------------------------------
-module(enoise_keypair).
-module(znoise_keypair).
-vsn("0.1.0").
-author("Craig Everett <craigeverett@qpq.swiss>").
-author("Hans Svensson <hanssv@gmail.com>").
-copyright("QPQ AG <info@qpq.swiss>").
-license("ISC").
-export([ key_type/1
, new/1
@@ -30,13 +34,15 @@
%% @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'
%% it will be computed from the `Secret' (using the curve/algorithm
%% indicated by `Type').
-spec new(Type :: key_type(), Secret :: binary(), Public :: binary() | undefined) -> keypair().
-spec new(Type :: key_type(),
Secret :: binary() | undefined,
Public :: binary() | undefined) -> keypair().
new(Type, Secret, undefined) ->
new(Type, Secret, pubkey_from_secret(Type, Secret));
new(Type, Secret, Public) ->
@@ -67,12 +73,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.
@@ -1,12 +1,16 @@
%%% ------------------------------------------------------------------
%%% @copyright 2026, QPQ AG
%%% @copyright 2018, Aeternity Anstalt
%%%
%%% @doc Module defining Noise protocol configurations
%%%
%%% @doc
%%% Module defining Noise protocol configurations
%%% @end
%%% ------------------------------------------------------------------
-module(enoise_protocol).
-module(znoise_protocol).
-vsn("0.1.0").
-author("Craig Everett <craigeverett@qpq.swiss>").
-author("Hans Svensson <hanssv@gmail.com>").
-copyright("QPQ AG <info@qpq.swiss>").
-license("ISC").
-export([ cipher/1
, dh/1
@@ -19,32 +23,32 @@
, 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.
-type noise_msg() :: {in | out, [enoise_hs_state:noise_token()]}.
-type noise_msg() :: {in | out, [znoise_hs_state:noise_token()]}.
-record(noise_protocol,
{ hs_pattern = noiseNN :: noise_pattern()
, dh = dh25519 :: enoise_hs_state:noise_dh()
, cipher = 'ChaChaPoly' :: enoise_cipher_state:noise_cipher()
, hash = blake2b :: enoise_sym_state:noise_hash()
, dh = dh25519 :: znoise_hs_state:noise_dh()
, cipher = 'ChaChaPoly' :: znoise_cipher_state:noise_cipher()
, hash = blake2b :: znoise_sym_state:noise_hash()
}).
-opaque protocol() :: #noise_protocol{}.
-export_type([noise_msg/0, noise_pattern/0, protocol/0]).
-spec cipher(Protocol :: protocol()) -> enoise_cipher_state:noise_cipher().
-spec cipher(Protocol :: protocol()) -> znoise_cipher_state:noise_cipher().
cipher(#noise_protocol{ cipher = Cipher }) ->
Cipher.
-spec dh(Protocol :: protocol()) -> enoise_hs_state:noise_dh().
-spec dh(Protocol :: protocol()) -> znoise_hs_state:noise_dh().
dh(#noise_protocol{ dh = Dh }) ->
Dh.
-spec hash(Protocol :: protocol()) -> enoise_sym_state:noise_hash().
-spec hash(Protocol :: protocol()) -> znoise_sym_state:noise_hash().
hash(#noise_protocol{ hash = Hash }) ->
Hash.
@@ -80,16 +84,17 @@ from_name(String) ->
error({name_not_recognized, String})
end.
-spec msgs(Role :: enoise_hs_state:noise_role(), Protocol :: protocol()) -> [noise_msg()].
-spec msgs(Role :: znoise_hs_state:noise_role(), Protocol :: protocol()) -> [noise_msg()].
msgs(Role, #noise_protocol{ hs_pattern = Pattern }) ->
{_Pre, Msgs} = protocol(Pattern),
role_adapt(Role, Msgs).
-spec pre_msgs(Role :: enoise_hs_state:noise_role(), Protocol :: protocol()) -> [noise_msg()].
-spec pre_msgs(Role :: znoise_hs_state:noise_role(), Protocol :: protocol()) -> [noise_msg()].
pre_msgs(Role, #noise_protocol{ hs_pattern = Pattern }) ->
{PreMsgs, _Msgs} = protocol(Pattern),
role_adapt(Role, PreMsgs).
-spec role_adapt(Role :: znoise_hs_state:noise_role(), [noise_msg()]) -> [noise_msg()].
role_adapt(initiator, Msgs) ->
Msgs;
role_adapt(responder, Msgs) ->
@@ -136,9 +141,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) ->
@@ -147,16 +152,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".
@@ -1,12 +1,15 @@
%%% ------------------------------------------------------------------
%%% @copyright 2018, Aeternity Anstalt
%%%
%%% @doc Module encapsulating a Noise symmetric (hash) state
%%%
%%% @doc
%%% Module encapsulating a Noise symmetric (hash) state
%%% @end
%%% ------------------------------------------------------------------
-module(enoise_sym_state).
-module(znoise_sym_state).
-vsn("0.1.0").
-author("Craig Everett <craigeverett@qpq.swiss>").
-author("Hans Svensson <hanssv@gmail.com>").
-copyright("QPQ AG <info@qpq.swiss>").
-license("ISC").
-export([ cipher_state/1
, ck/1
@@ -21,11 +24,11 @@
, split/1
]).
-include("enoise.hrl").
-include("znoise.hrl").
-type noise_hash() :: sha256 | sha512 | blake2s | blake2b.
-record(noise_ss, { cs :: enoise_cipher_state:state()
-record(noise_ss, { cs :: znoise_cipher_state:state()
, ck = <<>> :: binary()
, h = <<>> :: binary()
, hash = blake2b :: noise_hash() }).
@@ -33,64 +36,64 @@
-opaque state() :: #noise_ss{}.
-export_type([noise_hash/0, state/0]).
-spec init(Protocol :: enoise_protocol:protocol()) -> state().
-spec init(Protocol :: znoise_protocol:protocol()) -> state().
init(Protocol) ->
Hash = enoise_protocol:hash(Protocol),
Cipher = enoise_protocol:cipher(Protocol),
Name = enoise_protocol:to_name(Protocol),
HashLen = enoise_crypto:hashlen(Hash),
Hash = znoise_protocol:hash(Protocol),
Cipher = znoise_protocol:cipher(Protocol),
Name = znoise_protocol:to_name(Protocol),
HashLen = znoise_crypto:hashlen(Hash),
H1 =
case byte_size(Name) > HashLen of
true -> enoise_crypto:hash(Hash, Name);
false -> enoise_crypto:pad(Name, HashLen, 16#00)
true -> znoise_crypto:hash(Hash, Name);
false -> znoise_crypto:pad(Name, HashLen, 16#00)
end,
#noise_ss{ h = H1
, ck = H1
, hash = Hash
, cs = enoise_cipher_state:init(empty, Cipher) }.
, cs = znoise_cipher_state:init(empty, Cipher) }.
-spec mix_key(SState :: state(), InputKeyMaterial :: binary()) -> state().
mix_key(SState = #noise_ss{ hash = Hash, ck = CK0, cs = CS0 }, InputKeyMaterial) ->
[CK1, <<TempK:32/binary, _/binary>> | _] =
enoise_crypto:hkdf(Hash, CK0, InputKeyMaterial),
CS1 = enoise_cipher_state:set_key(CS0, TempK),
znoise_crypto:hkdf(Hash, CK0, InputKeyMaterial),
CS1 = znoise_cipher_state:set_key(CS0, TempK),
SState#noise_ss{ ck = CK1, cs = CS1 }.
-spec mix_hash(SState :: state(), Data :: binary()) -> state().
mix_hash(SState = #noise_ss{ hash = Hash, h = H0 }, Data) ->
H1 = enoise_crypto:hash(Hash, <<H0/binary, Data/binary>>),
H1 = znoise_crypto:hash(Hash, <<H0/binary, Data/binary>>),
SState#noise_ss{ h = H1 }.
-spec mix_key_and_hash(SState :: state(), InputKeyMaterial :: binary()) -> state().
mix_key_and_hash(SState = #noise_ss{ hash = Hash, ck = CK0, cs = CS0 }, InputKeyMaterial) ->
[CK1, TempH, <<TempK:32/binary, _/binary>>] =
enoise_crypto:hkdf(Hash, CK0, InputKeyMaterial),
CS1 = enoise_cipher_state:set_key(CS0, TempK),
znoise_crypto:hkdf(Hash, CK0, InputKeyMaterial),
CS1 = znoise_cipher_state:set_key(CS0, TempK),
mix_hash(SState#noise_ss{ ck = CK1, cs = CS1 }, TempH).
-spec encrypt_and_hash(SState :: state(), PlainText :: binary()) -> {ok, state(), binary()}.
encrypt_and_hash(SState = #noise_ss{ cs = CS0, h = H }, PlainText) ->
{ok, CS1, CipherText} = enoise_cipher_state:encrypt_with_ad(CS0, H, PlainText),
{ok, CS1, CipherText} = znoise_cipher_state:encrypt_with_ad(CS0, H, PlainText),
{ok, mix_hash(SState#noise_ss{ cs = CS1 }, CipherText), CipherText}.
-spec decrypt_and_hash(SState :: state(), CipherText :: binary()) ->
{ok, state(), binary()} | {error, term()}.
decrypt_and_hash(SState = #noise_ss{ cs = CS0, h = H }, CipherText) ->
case enoise_cipher_state:decrypt_with_ad(CS0, H, CipherText) of
case znoise_cipher_state:decrypt_with_ad(CS0, H, CipherText) of
Err = {error, _} ->
Err;
{ok, CS1, PlainText} ->
{ok, mix_hash(SState#noise_ss{ cs = CS1 }, CipherText), PlainText}
end.
-spec split(SState :: state()) -> {enoise_cipher_state:state(), enoise_cipher_state:state()}.
-spec split(SState :: state()) -> {znoise_cipher_state:state(), znoise_cipher_state:state()}.
split(#noise_ss{ hash = Hash, ck = CK, cs = CS }) ->
[<<TempK1:32/binary, _/binary>>, <<TempK2:32/binary, _/binary>>, _] =
enoise_crypto:hkdf(Hash, CK, <<>>),
{enoise_cipher_state:set_key(CS, TempK1),
enoise_cipher_state:set_key(CS, TempK2)}.
znoise_crypto:hkdf(Hash, CK, <<>>),
{znoise_cipher_state:set_key(CS, TempK1),
znoise_cipher_state:set_key(CS, TempK2)}.
-spec cipher_state(SState :: state()) -> enoise_cipher_state:state().
-spec cipher_state(SState :: state()) -> znoise_cipher_state:state().
cipher_state(#noise_ss{ cs = CS }) ->
CS.
+243
View File
@@ -0,0 +1,243 @@
%%% @copyright 2026, QPQ AG
%%% @copyright 2018, Aeternity Anstalt
%%%
%%% @doc
%%% A gen_server for holding a Noise connection over gen_tcp.
%%%
%%% Some care is needed since the underlying transmission is broken up
%%% into Noise packets, so we need some buffering.
%%% @end
-module(znoise_tcp).
-vsn("0.1.0").
-author("Craig Everett <craigeverett@qpq.swiss>").
-author("Hans Svensson <hanssv@gmail.com>").
-copyright("QPQ AG <info@qpq.swiss>").
-license("ISC").
-export([controlling_process/2,
close/1,
send/2,
set_active/2,
start_link/5]).
%% gen_server
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-record(znoise,
{pid}).
-record(s,
{rx = ,
tx = ,
owner = none :: none | pid(),
owner_ref = none :: none | reference(),
tcp_sock = none :: none | gen_tcp:socket(),
active = once :: true | {once, boolean()},
msgbuf = [] :: list(),
rawbuf = <<>> :: binary()}).
start_link(TcpSock, Rx, Tx, Owner, {Active0, Buf}) ->
Active =
case Active0 of
true -> true;
once -> {once, false}
end,
State =
#s{rx = Rx,
tx = Tx,
owner = Owner,
tcp_sock = TcpSock,
active = Active},
case gen_server:start_link(?MODULE, [State], []) of
{ok, Pid} ->
case gen_tcp:controlling_process(TcpSock, Pid) of
ok ->
% Changing controlling process require a bit of
% fiddling with already received and delivered content...
ok =
case Buf =/= <<>> of
true -> Pid ! {tcp, TcpSock, Buf};
false -> ok
end,
flush_tcp(Pid, TcpSock),
{ok, Pid};
Error ->
close(Pid),
Error
end;
Error
Error
end.
-spec send(Noise :: pid(), Data :: binary()) -> ok | {error, term()}.
send(Noise, Data) ->
gen_server:call(Noise, {send, Data}).
-spec set_active(Noise :: pid(), Active :: true | once) -> ok | {error, term()}.
set_active(Noise, Active) ->
gen_server:call(Noise, {active, self(), Active}).
-spec close(Noise :: pid()) -> ok | {error, term()}.
close(Noise) ->
gen_server:call(Noise, close).
-spec controlling_process(Noise :: pid(), NewPid :: pid()) -> ok | {error, term()}.
controlling_process(Noise, NewPid) ->
gen_server:call(Noise, {controlling_process, self(), NewPid}, 100).
%% gen_server
init([#s{owner = Owner} = State]) ->
OwnerRef = erlang:monitor(process, Owner),
{ok, State#s{owner_ref = OwnerRef}}.
handle_call(close, _, State) ->
{stop, normal, ok, State};
handle_call(_Call, _, State = #s{tcp_sock = closed}) ->
{reply, {error, closed}, State};
handle_call({send, Data}, _, State) ->
{Result, NewState} = handle_send(State, Data),
{reply, Result, NewState};
handle_call({controlling_process, OldPID, NewPID}, _, State) ->
{Result, NewState} = handle_control_change(State, OldPID, NewPID),
{reply, Result, NewState};
handle_call({active, PID, NewActive}, _, State) ->
{Result, NewState} = handle_active(State, PID, NewActive),
{reply, Result, NewState}.
handle_cast(_, State) ->
{noreply, State}.
handle_info({tcp, TS, Data}, State = #s{tcp_sock = TS, owner = O}) ->
try
{NextState = #s{msgbuf = Buf}, Msgs} = handle_data(State, Data),
NewState = handle_msgs(NextState#s{msgbuf = Buf ++ Msgs}),
set_active(NewState),
{noreply, NewState}
catch error:{znoise_error, _} ->
%% We are not likely to recover, but leave the decision to upstream
O ! {znoise_error, TS, decrypt_error},
{noreply, State}
end;
handle_info({tcp_closed, TS}, State = #s{tcp_sock = TS, owner = O}) ->
O ! {tcp_closed, TS},
{noreply, State#s{tcp_sock = closed}};
handle_info({'DOWN', OwnerRef, process, _, normal},
State = #s{tcp_sock = TS, owner_ref = OwnerRef}) ->
close_tcp(TS),
{stop, normal, State#s{tcp_sock = closed, owner_ref = undefined}};
handle_info({'DOWN', _, _, _, _}, State) ->
%% Ignore non-normal monitor messages - we are linked.
{noreply, State};
handle_info(_Msg, State) ->
{noreply, State}.
terminate(_, #s{tcp_sock = TcpSock, owner_ref = ORef}) ->
[ gen_tcp:close(TcpSock) || TcpSock /= closed ],
[ erlang:demonitor(ORef, [flush]) || ORef /= undefined ],
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%% Handlers
handle_control_change(State = #s{owner = PID, owner_ref = OldRef}, PID, NewPID) ->
NewRef = erlang:monitor(process, NewPID),
erlang:demonitor(OldRef, [flush]),
{ok, State#s{owner = NewPID, owner_ref = NewRef}};
handle_control_change(State, _, _) ->
{{error, not_owner}, State}.
handle_active(State = #s{owner = PID, tcp_sock = TcpSock}, PID, Active) ->
case Active of
true ->
inet:setopts(TcpSock, [{active, true}]),
{ok, handle_msgs(State#s{active = true})};
once ->
NewState = handle_msgs(State#s{active = {once, false}}),
set_active(NewState),
{ok, NewState}
end;
handle_active(State, _, _) ->
{{error, not_owner}, State}.
handle_data(State = #s{rawbuf = Buf, rx = RX}, Data) ->
case <<Buf/binary, Data/binary>> of
B = <<Len:16, Rest/binary>> when Len > byte_size(Rest) ->
{State#s{rawbuf = B}, []}; %% Not a full Noise message - save it
<<Len:16, Rest/binary>> ->
<<Msg:Len/binary, Rest2/binary>> = Rest,
case znoise_cipher_state:decrypt_with_ad(RX, <<>>, Msg) of
{ok, NewRX, NewMsg} ->
{NewState, Msgs} = handle_data(State#s{rawbuf = Rest2, rx = NewRX}, <<>>),
{NewState, [NewMsg | Msgs]};
{error, _} ->
error({znoise_error, decrypt_input_failed})
end;
EmptyOrSingleByte ->
{State#s{rawbuf = EmptyOrSingleByte}, []}
end.
handle_msgs(State = #s{msgbuf = []}) ->
State;
handle_msgs(State = #s{msgbuf = Msgs, active = true, owner = Owner}) ->
[ Owner ! {noise, #znoise{pid = self()}, Msg} || Msg <- Msgs ],
State#s{msgbuf = []};
handle_msgs(State = #s{msgbuf = [Msg | Msgs], active = {once, Delivered}, owner = Owner}) ->
case Delivered of
true ->
State;
false ->
Owner ! {noise, #znoise{pid = self()}, Msg},
State#s{msgbuf = Msgs, active = {once, true}}
end.
handle_send(State = #s{tcp_sock = TcpSock, tx = TX}, Data) ->
{ok, MewTX, Msg} = znoise_cipher_state:encrypt_with_ad(TX, <<>>, Data),
case gen_tcp:send(TcpSock, <<(byte_size(Msg)):16, Msg/binary>>) of
ok -> {ok, State#s{tx = MewTX}};
Error -> {Error, State}
end.
set_active(#s{msgbuf = [], active = {once, _}, tcp_sock = TcpSock}) ->
inet:setopts(TcpSock, [{active, once}]);
set_active(_) ->
ok.
flush_tcp(Pid, TcpSock) ->
receive {tcp, TcpSock, Data} ->
Pid ! {tcp, TcpSock, Data},
flush_tcp(Pid, TcpSock)
after 1 -> ok
end.
close_tcp(closed) ->
ok;
close_tcp(Sock) ->
gen_tcp:close(Sock).
-11
View File
@@ -1,11 +0,0 @@
%%%-------------------------------------------------------------------
%%% @copyright (C) 2018, Aeternity Anstalt
%%%-------------------------------------------------------------------
-module(enoise_protocol_tests).
-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"))).
-56
View File
@@ -1,56 +0,0 @@
%%%-------------------------------------------------------------------
%%% @copyright (C) 2018, Aeternity Anstalt
%%%-------------------------------------------------------------------
-module(enoise_sym_state_tests).
-include_lib("eunit/include/eunit.hrl").
noise_XK_25519_ChaChaPoly_Blake2b_test() ->
Protocol = enoise_protocol:from_name("Noise_XK_25519_ChaChaPoly_BLAKE2b"),
SSE0 = enoise_sym_state:init(Protocol),
SSD0 = enoise_sym_state:init(Protocol),
Name = enoise_protocol:to_name(Protocol),
PadName = enoise_crypto:pad(Name, enoise_crypto:hashlen(blake2b), 0),
?assertMatch(PadName, enoise_sym_state:h(SSE0)),
?assertMatch(PadName, enoise_sym_state:ck(SSE0)),
?assertMatch(false, enoise_cipher_state:has_key(enoise_sym_state:cipher_state(SSE0))),
TestBin = h2b("0x6162636465666768696A6B6C6D6E6F707172737475767778797A"),
SSE1 = enoise_sym_state:mix_hash(SSE0, TestBin),
SSD1 = enoise_sym_state:mix_hash(SSD0, TestBin),
ExpHash1 = enoise_crypto:hash(blake2b, <<PadName/binary, TestBin/binary>>),
ExpHash2 = h2b("0x8DC23DE176F6B3581FB7E18F258A47B1E1A8090BF55978868F1AC88C672DC3918FA4D1828338FB5DF652F5C33D57C79537CB5D074057EF59C346D0B35A160F71"),
?assertMatch(ExpHash1, enoise_sym_state:h(SSE1)),
?assertMatch(ExpHash2, enoise_sym_state:h(SSD1)),
{ok, SSE2, TestBin} = enoise_sym_state:encrypt_and_hash(SSE1, TestBin),
{ok, SSD2, TestBin} = enoise_sym_state:decrypt_and_hash(SSD1, TestBin),
SSE3 = enoise_sym_state:mix_key(SSE2, TestBin),
SSD3 = enoise_sym_state:mix_key(SSD2, TestBin),
ExpEncrypt = h2b("0x24FB13758E6BA9901A4CEA117AE1D9AF757B02CAE96EFDFDA5ED3927BDD9FEA0239F7F673E924AAE81E6"),
{ok, SSE4, Encrypt} = enoise_sym_state:encrypt_and_hash(SSE3, TestBin),
?assertMatch(ExpEncrypt, Encrypt),
{ok, SSD4, Decrypt} = enoise_sym_state:decrypt_and_hash(SSD3, ExpEncrypt),
?assertMatch(TestBin, Decrypt),
Key1 = h2b("0x893FD190EDB611D9AF73868C8AB020F7A13C62F70F7F74C46859CF4A1E71BB74"),
Key2 = h2b("0x492E210AD0181CE70BF9CE80308DE45EAE1FA76E1ACE22A829EF6F1A01C6E2C8"),
{CSE1, CSE2} = enoise_sym_state:split(SSE4),
?assertMatch(Key1, enoise_cipher_state:key(CSE1)),
?assertMatch(Key2, enoise_cipher_state:key(CSE2)),
{CSD1, CSD2} = enoise_sym_state:split(SSD4),
?assertMatch(Key1, enoise_cipher_state:key(CSD1)),
?assertMatch(Key2, enoise_cipher_state:key(CSD2)),
ok.
h2b(S) -> test_utils:hex_str_to_bin(S).
+4 -3
View File
@@ -114,13 +114,14 @@ parse_test_vectors(File) ->
%% Only test supported configurations
noise_test_filter(Tests0) ->
Tests1 = [ T || T = #{ name := Name } <- Tests0, supported(Name) ],
Tests1 = [ T || T = #{ protocol_name := Name } <- Tests0, supported(Name) ],
case length(Tests1) < length(Tests0) of
true -> ?debugFmt("WARNING: ~p test vectors are unsupported", [length(Tests0) - length(Tests1)]);
true -> ?debugFmt("WARNING: ~p test vectors out of ~p are unsupported",
[length(Tests0) - length(Tests1), length(Tests0)]);
false -> ok
end,
Tests1.
supported(Name) ->
try enoise_protocol:from_name(Name), true
try znoise_protocol:from_name(Name), true
catch _:_ -> false end.
+29692 -14524
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
%%%-------------------------------------------------------------------
%%% @copyright (C) 2018, Aeternity Anstalt
%%%-------------------------------------------------------------------
-module(znoise_bad_data_tests).
-include_lib("eunit/include/eunit.hrl").
bad_data_hs_1_test() ->
SrvKeyPair = znoise_keypair:new(dh25519),
Proto = znoise_protocol:to_name(xk, dh25519, 'ChaChaPoly', blake2b),
Opts = [{echos, 1}, {reply, self()}],
Srv = znoise_utils:echo_srv_start(4567, Proto, SrvKeyPair, Opts),
bad_client(4567),
SrvRes =
receive {Srv, server_result, Res0} -> Res0
after 500 -> timeout end,
?assertMatch({error, {bad_data, _}}, SrvRes),
ok.
bad_client(Port) ->
{ok, Sock} = gen_tcp:connect("localhost", Port, [binary, {reuseaddr, true}], 100),
gen_tcp:send(Sock, <<0:256/unit:8>>),
timer:sleep(100),
gen_tcp:close(Sock).
@@ -2,7 +2,7 @@
%%% @copyright (C) 2018, Aeternity Anstalt
%%%-------------------------------------------------------------------
-module(enoise_chiper_state_tests).
-module(znoise_chiper_state_tests).
-include_lib("eunit/include/eunit.hrl").
@@ -13,18 +13,27 @@ chachapoly_test() ->
CTLen = byte_size(CipherText),
MACLen = byte_size(MAC),
CS0 = enoise_cipher_state:init(Key, 'ChaChaPoly'),
CS1 = enoise_cipher_state:set_nonce(CS0, Nonce),
CS0 = znoise_cipher_state:init(Key, 'ChaChaPoly'),
CS1 = znoise_cipher_state:set_nonce(CS0, Nonce),
{ok, _CS2, <<CipherText0:CTLen/binary, MAC0:MACLen/binary>>} =
enoise_cipher_state:encrypt_with_ad(CS1, AD, PlainText),
znoise_cipher_state:encrypt_with_ad(CS1, AD, PlainText),
?assertMatch(CipherText, CipherText0),
?assertMatch(MAC, MAC0),
{ok, _CS3, <<PlainText0:PTLen/binary>>} =
enoise_cipher_state:decrypt_with_ad(CS1, AD, <<CipherText/binary, MAC/binary>>),
znoise_cipher_state:decrypt_with_ad(CS1, AD, <<CipherText/binary, MAC/binary>>),
?assertMatch(PlainText, PlainText0),
% rekey test
CS4 = znoise_cipher_state:rekey(CS1),
{ok, _CS5, <<CipherText1:CTLen/binary, MAC1:MACLen/binary>>} =
znoise_cipher_state:encrypt_with_ad(CS4, AD, PlainText),
{ok, _CS6, <<PlainText1:PTLen/binary>>} =
znoise_cipher_state:decrypt_with_ad(CS4, AD, <<CipherText1/binary, MAC1/binary>>),
?assertMatch(PlainText, PlainText1),
ok.
@@ -2,25 +2,25 @@
%%% @copyright (C) 2018, Aeternity Anstalt
%%%-------------------------------------------------------------------
-module(enoise_crypto_tests).
-module(znoise_crypto_tests).
-include_lib("eunit/include/eunit.hrl").
curve25519_test() ->
KeyPair1 = enoise_keypair:new(dh25519),
KeyPair2 = enoise_keypair:new(dh25519),
KeyPair1 = znoise_keypair:new(dh25519),
KeyPair2 = znoise_keypair:new(dh25519),
SharedA = enoise_crypto:dh(dh25519, KeyPair1, KeyPair2),
SharedB = enoise_crypto:dh(dh25519, KeyPair2, KeyPair1),
SharedA = znoise_crypto:dh(dh25519, KeyPair1, KeyPair2),
SharedB = znoise_crypto:dh(dh25519, KeyPair2, KeyPair1),
?assertMatch(SharedA, SharedB),
#{ a_pub := APub, a_priv := APriv,
b_pub := BPub, b_priv := BPriv, shared := Shared } = test_utils:curve25519_data(),
KeyPair3 = enoise_keypair:new(dh25519, APriv, APub),
KeyPair4 = enoise_keypair:new(dh25519, BPriv, BPub),
?assertMatch(Shared, enoise_crypto:dh(dh25519, KeyPair3, KeyPair4)),
?assertMatch(Shared, enoise_crypto:dh(dh25519, KeyPair4, KeyPair3)),
KeyPair3 = znoise_keypair:new(dh25519, APriv, APub),
KeyPair4 = znoise_keypair:new(dh25519, BPriv, BPub),
?assertMatch(Shared, znoise_crypto:dh(dh25519, KeyPair3, KeyPair4)),
?assertMatch(Shared, znoise_crypto:dh(dh25519, KeyPair4, KeyPair3)),
ok.
@@ -35,36 +35,43 @@ chachapoly_test() ->
?assert(PTLen == CTLen),
<<CipherText0:CTLen/binary, MAC0:MACLen/binary>> =
enoise_crypto:encrypt('ChaChaPoly', Key, Nonce, AD, PlainText),
znoise_crypto:encrypt('ChaChaPoly', Key, Nonce, AD, PlainText),
?assertMatch(CipherText, CipherText0),
?assertMatch(MAC, MAC0),
<<PlainText0:PTLen/binary>> =
enoise_crypto:decrypt('ChaChaPoly', Key, Nonce, AD, <<CipherText/binary, MAC/binary>>),
znoise_crypto:decrypt('ChaChaPoly', Key, Nonce, AD, <<CipherText/binary, MAC/binary>>),
?assertMatch(PlainText, PlainText0),
Key1 = znoise_crypto:rekey('ChaChaPoly', Key),
<<CipherText1:CTLen/binary, MAC1:MACLen/binary>> =
znoise_crypto:encrypt('ChaChaPoly', Key1, Nonce, AD, PlainText),
<<PlainText1:PTLen/binary>> =
znoise_crypto:decrypt('ChaChaPoly', Key1, Nonce, AD, <<CipherText1/binary, MAC1/binary>>),
?assertMatch(PlainText, PlainText1),
ok.
blake2b_test() ->
Test = fun(#{ input := In, output := Out }) ->
?assertMatch(Out, enoise_crypto:hash(blake2b, In))
?assertMatch(Out, znoise_crypto:hash(blake2b, In))
end,
lists:foreach(Test, test_utils:blake2b_data()).
%% blake2s_test() ->
%% #{ input := In, output := Out } = test_utils:blake2s_data(),
%% ?assertMatch(Out, enoise_crypto:hash(blake2s, In)).
%% ?assertMatch(Out, znoise_crypto:hash(blake2s, In)).
blake2b_hmac_test() ->
Test = fun(#{ key := Key, data := Data, hmac := HMAC }) ->
?assertMatch(HMAC, enoise_crypto:hmac(blake2b, Key, Data))
?assertMatch(HMAC, znoise_crypto:hmac(blake2b, Key, Data))
end,
lists:foreach(Test, test_utils:blake2b_hmac_data()).
blake2b_hkdf_test() ->
Test = fun(#{ key := Key, data := Data, out1 := Out1, out2 := Out2 }) ->
?assertMatch([Out1, Out2, _], enoise_crypto:hkdf(blake2b, Key, Data))
?assertMatch([Out1, Out2, _], znoise_crypto:hkdf(blake2b, Key, Data))
end,
lists:foreach(Test, test_utils:blake2b_hkdf_data()).
@@ -2,7 +2,7 @@
%%% @copyright (C) 2018, Aeternity Anstalt
%%%-------------------------------------------------------------------
-module(enoise_hs_state_tests).
-module(znoise_hs_state_tests).
-include_lib("eunit/include/eunit.hrl").
@@ -12,13 +12,13 @@ noise_hs_test_() ->
fun() -> test_utils:noise_test_vectors() end,
fun(_X) -> ok end,
fun(Tests) ->
[ {maps:get(name, T), fun() -> noise_hs_test(T) end}
[ {maps:get(protocol_name, T), fun() -> noise_hs_test(T) end}
|| T <- test_utils:noise_test_filter(Tests) ]
end
}.
noise_hs_test(V = #{ name := Name }) ->
Protocol = enoise_protocol:from_name(Name),
noise_hs_test(V = #{ protocol_name := Name }) ->
Protocol = znoise_protocol:from_name(Name),
FixK = fun(undefined) -> undefined;
(Bin) -> test_utils:hex_str_to_bin("0x" ++ binary_to_list(Bin)) end,
@@ -39,11 +39,12 @@ noise_hs_test(V = #{ name := Name }) ->
ok.
noise_test(_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,
DH = znoise_protocol:dh(Protocol),
SecK = fun(undefined) -> undefined; (Sec) -> znoise_keypair:new(DH, Sec, undefined) end,
PubK = fun(undefined) -> undefined; (Pub) -> znoise_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} = znoise_hs_state:init(P, R, PL, {SecK(S), SecK(E), PubK(RS), undefined}),
HS
end,
InitHS = HSInit(Protocol, initiator, Init),
@@ -56,16 +57,16 @@ noise_test(_Name, Protocol, Init, Resp, Messages, HSHash) ->
noise_test([M = #{ payload := PL0, ciphertext := CT0 } | Msgs], SendHS, RecvHS, HSHash) ->
PL = test_utils:hex_str_to_bin("0x" ++ binary_to_list(PL0)),
CT = test_utils:hex_str_to_bin("0x" ++ binary_to_list(CT0)),
case {enoise_hs_state:next_message(SendHS), enoise_hs_state:next_message(RecvHS)} of
case {znoise_hs_state:next_message(SendHS), znoise_hs_state:next_message(RecvHS)} of
{out, in} ->
{ok, SendHS1, Message} = enoise_hs_state:write_message(SendHS, PL),
{ok, SendHS1, Message} = znoise_hs_state:write_message(SendHS, PL),
?assertEqual(CT, Message),
{ok, RecvHS1, PL1} = enoise_hs_state:read_message(RecvHS, Message),
{ok, RecvHS1, PL1} = znoise_hs_state:read_message(RecvHS, Message),
?assertEqual(PL, PL1),
noise_test(Msgs, RecvHS1, SendHS1, HSHash);
{done, done} ->
{ok, #{ rx := RX1, tx := TX1, hs_hash := HSHash1 }} = enoise_hs_state:finalize(SendHS),
{ok, #{ rx := RX2, tx := TX2, hs_hash := HSHash2 }} = enoise_hs_state:finalize(RecvHS),
{ok, #{ rx := RX1, tx := TX1, hs_hash := HSHash1 }} = znoise_hs_state:finalize(SendHS),
{ok, #{ rx := RX2, tx := TX2, hs_hash := HSHash2 }} = znoise_hs_state:finalize(RecvHS),
?assertEqual(RX1, TX2), ?assertEqual(RX2, TX1),
?assertEqual(HSHash, HSHash1), ?assertEqual(HSHash, HSHash2),
noise_test([M | Msgs], TX1, RX1);
@@ -76,9 +77,9 @@ noise_test([], _, _) -> ok;
noise_test([#{ payload := PL0, ciphertext := CT0 } | Msgs], CA, CB) ->
PL = test_utils:hex_str_to_bin("0x" ++ binary_to_list(PL0)),
CT = test_utils:hex_str_to_bin("0x" ++ binary_to_list(CT0)),
{ok, CA1, CT1} = enoise_cipher_state:encrypt_with_ad(CA, <<>>, PL),
{ok, CA1, CT1} = znoise_cipher_state:encrypt_with_ad(CA, <<>>, PL),
?assertEqual(CT, CT1),
{ok, CA2, PL1} = enoise_cipher_state:decrypt_with_ad(CA, <<>>, CT1),
{ok, CA2, PL1} = znoise_cipher_state:decrypt_with_ad(CA, <<>>, CT1),
?assertEqual(CA1, CA2),
?assertEqual(PL, PL1),
noise_test(Msgs, CB, CA1).
+24
View File
@@ -0,0 +1,24 @@
%%%-------------------------------------------------------------------
%%% @copyright (C) 2018, Aeternity Anstalt
%%%-------------------------------------------------------------------
-module(znoise_protocol_tests).
-include_lib("eunit/include/eunit.hrl").
name_test() ->
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}, znoise_protocol:from_name(Name)).
name_pattern_test() ->
Pat = "XKfallback+psk0",
RoundPat = znoise_protocol:to_name_pattern(znoise_protocol:from_name_pattern(Pat)),
?assertEqual(Pat, RoundPat).
roundtrip(Name) ->
ExpectedName = iolist_to_binary(Name),
?assertMatch(ExpectedName, znoise_protocol:to_name(znoise_protocol:from_name(Name))).
+56
View File
@@ -0,0 +1,56 @@
%%%-------------------------------------------------------------------
%%% @copyright (C) 2018, Aeternity Anstalt
%%%-------------------------------------------------------------------
-module(znoise_sym_state_tests).
-include_lib("eunit/include/eunit.hrl").
noise_XK_25519_ChaChaPoly_Blake2b_test() ->
Protocol = znoise_protocol:from_name("Noise_XK_25519_ChaChaPoly_BLAKE2b"),
SSE0 = znoise_sym_state:init(Protocol),
SSD0 = znoise_sym_state:init(Protocol),
Name = znoise_protocol:to_name(Protocol),
PadName = znoise_crypto:pad(Name, znoise_crypto:hashlen(blake2b), 0),
?assertMatch(PadName, znoise_sym_state:h(SSE0)),
?assertMatch(PadName, znoise_sym_state:ck(SSE0)),
?assertMatch(false, znoise_cipher_state:has_key(znoise_sym_state:cipher_state(SSE0))),
TestBin = h2b("0x6162636465666768696A6B6C6D6E6F707172737475767778797A"),
SSE1 = znoise_sym_state:mix_hash(SSE0, TestBin),
SSD1 = znoise_sym_state:mix_hash(SSD0, TestBin),
ExpHash1 = znoise_crypto:hash(blake2b, <<PadName/binary, TestBin/binary>>),
ExpHash2 = h2b("0x8DC23DE176F6B3581FB7E18F258A47B1E1A8090BF55978868F1AC88C672DC3918FA4D1828338FB5DF652F5C33D57C79537CB5D074057EF59C346D0B35A160F71"),
?assertMatch(ExpHash1, znoise_sym_state:h(SSE1)),
?assertMatch(ExpHash2, znoise_sym_state:h(SSD1)),
{ok, SSE2, TestBin} = znoise_sym_state:encrypt_and_hash(SSE1, TestBin),
{ok, SSD2, TestBin} = znoise_sym_state:decrypt_and_hash(SSD1, TestBin),
SSE3 = znoise_sym_state:mix_key(SSE2, TestBin),
SSD3 = znoise_sym_state:mix_key(SSD2, TestBin),
ExpEncrypt = h2b("0x24FB13758E6BA9901A4CEA117AE1D9AF757B02CAE96EFDFDA5ED3927BDD9FEA0239F7F673E924AAE81E6"),
{ok, SSE4, Encrypt} = znoise_sym_state:encrypt_and_hash(SSE3, TestBin),
?assertMatch(ExpEncrypt, Encrypt),
{ok, SSD4, Decrypt} = znoise_sym_state:decrypt_and_hash(SSD3, ExpEncrypt),
?assertMatch(TestBin, Decrypt),
Key1 = h2b("0x893FD190EDB611D9AF73868C8AB020F7A13C62F70F7F74C46859CF4A1E71BB74"),
Key2 = h2b("0x492E210AD0181CE70BF9CE80308DE45EAE1FA76E1ACE22A829EF6F1A01C6E2C8"),
{CSE1, CSE2} = znoise_sym_state:split(SSE4),
?assertMatch(Key1, znoise_cipher_state:key(CSE1)),
?assertMatch(Key2, znoise_cipher_state:key(CSE2)),
{CSD1, CSD2} = znoise_sym_state:split(SSD4),
?assertMatch(Key1, znoise_cipher_state:key(CSD1)),
?assertMatch(Key2, znoise_cipher_state:key(CSD2)),
ok.
h2b(S) -> test_utils:hex_str_to_bin(S).
+45 -83
View File
@@ -2,7 +2,7 @@
%%% @copyright (C) 2018, Aeternity Anstalt
%%%-------------------------------------------------------------------
-module(enoise_tests).
-module(znoise_tests).
-include_lib("eunit/include/eunit.hrl").
@@ -12,13 +12,13 @@ noise_interactive_test_() ->
fun() -> test_utils:noise_test_vectors() end,
fun(_X) -> ok end,
fun(Tests) ->
[ {maps:get(name, T), fun() -> noise_interactive(T) end}
[ {maps:get(protocol_name, T), fun() -> noise_interactive(T) end}
|| T <- test_utils:noise_test_filter(Tests) ]
end
}.
noise_interactive(V = #{ name := Name }) ->
Protocol = enoise_protocol:from_name(Name),
noise_interactive(V = #{ protocol_name := Name }) ->
Protocol = znoise_protocol:from_name(Name),
FixK = fun(undefined) -> undefined;
(Bin) -> test_utils:hex_str_to_bin("0x" ++ binary_to_list(Bin)) end,
@@ -39,13 +39,12 @@ noise_interactive(V = #{ name := Name }) ->
ok.
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,
DH = znoise_protocol:dh(Protocol),
SecK = fun(undefined) -> undefined; (Sec) -> znoise_keypair:new(DH, Sec, undefined) 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}],
enoise:handshake(Opts, R)
Opts = [{noise, Protocol}, {s, SecK(S)}, {e, SecK(E)}, {rs, RS}, {prologue, PL}],
znoise:handshake(Opts, R)
end,
{ok, InitHS} = HSInit(Init, initiator),
{ok, RespHS} = HSInit(Resp, responder),
@@ -55,22 +54,20 @@ noise_interactive(_Name, Protocol, Init, Resp, Messages, HSHash) ->
noise_interactive([#{ payload := PL0, ciphertext := CT0 } | Msgs], SendHS, RecvHS, HSHash) ->
PL = test_utils:hex_str_to_bin("0x" ++ binary_to_list(PL0)),
CT = test_utils:hex_str_to_bin("0x" ++ binary_to_list(CT0)),
case enoise_hs_state:next_message(SendHS) of
case znoise_hs_state:next_message(SendHS) of
out ->
{ok, send, Message, SendHS1} = enoise:step_handshake(SendHS, {send, PL}),
{ok, send, Message, SendHS1} = znoise:step_handshake(SendHS, {send, PL}),
?assertEqual(CT, Message),
{ok, rcvd, PL1, RecvHS1} = enoise:step_handshake(RecvHS, {rcvd, Message}),
{ok, rcvd, PL1, RecvHS1} = znoise:step_handshake(RecvHS, {rcvd, Message}),
?assertEqual(PL, PL1),
noise_interactive(Msgs, RecvHS1, SendHS1, HSHash);
done ->
{ok, done, #{ rx := RX1, tx := TX1, hs_hash := HSHash1 }} = enoise:step_handshake(SendHS, done),
{ok, done, #{ rx := RX2, tx := TX2, hs_hash := HSHash2 }} = enoise:step_handshake(RecvHS, done),
{ok, done, #{ rx := RX1, tx := TX1, hs_hash := HSHash1 }} = znoise:step_handshake(SendHS, done),
{ok, done, #{ rx := RX2, tx := TX2, hs_hash := HSHash2 }} = znoise:step_handshake(RecvHS, done),
?assertEqual(RX1, TX2), ?assertEqual(RX2, TX1),
?assertEqual(HSHash, HSHash1), ?assertEqual(HSHash, HSHash2)
end.
noise_dh25519_test_() ->
%% Test vectors from https://raw.githubusercontent.com/rweather/noise-c/master/tests/vector/noise-c-basic.txt
{setup,
@@ -85,26 +82,26 @@ noise_monitor_test_() ->
{setup,
fun() -> setup_dh25519() end,
fun(_X) -> ok end,
fun({[T|Tests] = _Tests, SKP, CKP}) ->
[ {T, fun() -> noise_monitor_test(T, SKP, CKP) end} ]
fun({Tests, SKP, CKP}) ->
[ {T, fun() -> noise_monitor_test(T, SKP, CKP) end} || T <- Tests ]
end
}.
setup_dh25519() ->
%% Generate a static key-pair for Client and Server
SrvKeyPair = enoise_keypair:new(dh25519),
CliKeyPair = enoise_keypair:new(dh25519),
SrvKeyPair = znoise_keypair:new(dh25519),
CliKeyPair = znoise_keypair:new(dh25519),
#{ hs_pattern := Ps, hash := Hs, cipher := Cs } = enoise_protocol:supported(),
Configurations = [ enoise_protocol:to_name(P, dh25519, C, H)
#{ hs_pattern := Ps, hash := Hs, cipher := Cs } = znoise_protocol:supported(),
Configurations = [ znoise_protocol:to_name(P, dh25519, C, H)
|| P <- Ps, C <- Cs, H <- Hs ],
%% Configurations = [ enoise_protocol:to_name(xk, dh25519, 'ChaChaPoly', blake2b) ],
%% Configurations = [ znoise_protocol:to_name(xk, dh25519, 'ChaChaPoly', blake2b) ],
{Configurations, SrvKeyPair, CliKeyPair}.
noise_test(Conf, SKP, CKP) ->
#{econn := EConn, echo_srv := EchoSrv} = noise_test_run(Conf, SKP, CKP),
enoise:close(EConn),
echo_srv_stop(EchoSrv),
znoise:close(EConn),
znoise_utils:echo_srv_stop(EchoSrv),
ok.
noise_test_run(Conf, SKP, CKP) ->
@@ -148,24 +145,25 @@ proxy_exec(P, F) when is_function(F, 0) ->
end.
noise_test_run_(Conf, SKP, CKP) ->
Protocol = enoise_protocol:from_name(Conf),
Protocol = znoise_protocol:from_name(Conf),
Port = 4556,
EchoSrv = echo_srv_start(Port, Protocol, SKP, CKP),
SrvOpts = [{echos, 2}, {cpub, znoise_keypair:pubkey(CKP)}],
EchoSrv = znoise_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} || need_rs(initiator, Conf) ],
{ok, EConn, _} = enoise:connect(TcpSock, Opts),
Opts = [{noise, Protocol}, {s, CKP}] ++ [{rs, znoise_keypair:pubkey(SKP)} || znoise_utils:need_rs(initiator, Conf) ],
{ok, EConn, _} = znoise:connect(TcpSock, Opts),
ok = enoise:send(EConn, <<"Hello World!">>),
ok = znoise:send(EConn, <<"Hello World!">>),
receive
{noise, _, <<"Hello World!">>} -> ok
after 100 -> error(timeout) end,
enoise:set_active(EConn, once),
znoise:set_active(EConn, once),
ok = enoise:send(EConn, <<"Goodbye!">>),
ok = znoise:send(EConn, <<"Goodbye!">>),
receive
{noise, _, <<"Goodbye!">>} -> ok
after 100 -> error(timeout) end,
@@ -174,9 +172,9 @@ noise_test_run_(Conf, SKP, CKP) ->
, echo_srv => EchoSrv }.
noise_monitor_test(Conf, SKP, CKP) ->
#{ econn := {enoise, EConnPid}
#{ econn := {znoise, EConnPid}
, proxy := Proxy
, tcp_sock := TcpSock } = noise_test_run(Conf, SKP, CKP),
, tcp_sock := _TcpSock } = noise_test_run(Conf, SKP, CKP),
try proxy_exec(Proxy, fun() -> exit(normal) end)
catch
error:normal ->
@@ -185,45 +183,9 @@ noise_monitor_test(Conf, SKP, CKP) ->
end
end.
echo_srv_start(Port, Protocol, SKP, CPub) ->
Pid = spawn(fun() -> echo_srv(Port, Protocol, SKP, CPub) end),
timer:sleep(10),
Pid.
echo_srv(Port, Protocol, SKP, CPub) ->
TcpOpts = [{active, true}, binary, {reuseaddr, true}],
{ok, LSock} = gen_tcp:listen(Port, TcpOpts),
{ok, TcpSock} = gen_tcp:accept(LSock, 500),
Opts = [{noise, Protocol}, {s, SKP}] ++ [{rs, CPub} || need_rs(responder, Protocol)],
{ok, EConn, _} = enoise:accept(TcpSock, Opts),
gen_tcp:close(LSock),
%% {ok, Msg} = enoise:recv(EConn, 0, 100),
Msg0 = receive {noise, EConn, Data0} -> Data0
after 200 -> error(timeout) end,
ok = enoise:send(EConn, Msg0),
%% {ok, Msg} = enoise:recv(EConn, 0, 100),
Msg1 = receive {noise, EConn, Data1} -> Data1
after 200 -> error(timeout) end,
ok = enoise:send(EConn, Msg1),
ok.
echo_srv_stop(Pid) ->
erlang:exit(Pid, kill).
need_rs(Role, Conf) when is_binary(Conf) -> need_rs(Role, enoise_protocol:from_name(Conf));
need_rs(Role, Protocol) ->
PreMsgs = enoise_protocol:pre_msgs(Role, Protocol),
lists:member({in, [s]}, PreMsgs).
%% Talks to local echo-server (noise-c)
%% client_test() ->
%% TestProtocol = enoise_protocol:from_name("Noise_XK_25519_ChaChaPoly_BLAKE2b"),
%% TestProtocol = znoise_protocol:from_name("Noise_XK_25519_ChaChaPoly_BLAKE2b"),
%% ClientPrivKey = <<64,168,119,119,151,194,94,141,86,245,144,220,78,53,243,231,168,216,66,199,49,148,202,117,98,40,61,109,170,37,133,122>>,
%% 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>>,
@@ -232,29 +194,29 @@ need_rs(Role, Protocol) ->
%% gen_tcp:send(TcpSock, <<0,8,0,0,3>>), %% "Noise_XK_25519_ChaChaPoly_Blake2b"
%% Opts = [ {noise, TestProtocol}
%% , {s, enoise_keypair:new(dh25519, ClientPrivKey, ClientPubKey)}
%% , {rs, enoise_keypair:new(dh25519, ServerPubKey)}
%% , {s, znoise_keypair:new(dh25519, ClientPrivKey, ClientPubKey)}
%% , {rs, znoise_keypair:new(dh25519, ServerPubKey)}
%% , {prologue, <<0,8,0,0,3>>}],
%% {ok, EConn} = enoise:connect(TcpSock, Opts),
%% ok = enoise:send(EConn, <<"ok\n">>),
%% {ok, EConn} = znoise:connect(TcpSock, Opts),
%% ok = znoise: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).
%% %% {ok, <<"ok\n">>} = znoise:recv(EConn, 3, 1000),
%% znoise:close(EConn).
%% Expects a call-in from a local echo-client (noise-c)
%% server_test_() ->
%% {timeout, 20, fun() ->
%% TestProtocol = enoise_protocol:from_name("Noise_XK_25519_ChaChaPoly_Blake2b"),
%% TestProtocol = znoise_protocol:from_name("Noise_XK_25519_ChaChaPoly_Blake2b"),
%% ServerPrivKey = <<200,81,196,192,228,196,182,200,181,83,169,255,242,54,99,113,8,49,129,92,225,220,99,50,93,96,253,250,116,196,137,103>>,
%% 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>>,
%% Opts = [ {noise, TestProtocol}
%% , {s, enoise_keypair:new(dh25519, ServerPrivKey, ServerPubKey)}
%% , {s, znoise_keypair:new(dh25519, ServerPrivKey, ServerPubKey)}
%% , {prologue, <<0,8,0,0,3>>}],
%% {ok, LSock} = gen_tcp:listen(7891, [{reuseaddr, true}, binary]),
@@ -264,12 +226,12 @@ need_rs(Role, Protocol) ->
%% receive {tcp, TcpSock, <<0,8,0,0,3>>} -> ok
%% after 1000 -> error(timeout) end,
%% {ok, EConn} = enoise:accept(TcpSock, Opts),
%% {ok, EConn} = znoise:accept(TcpSock, Opts),
%% {EConn1, Msg} = enoise:recv(EConn),
%% EConn2 = enoise:send(EConn1, Msg),
%% {EConn1, Msg} = znoise:recv(EConn),
%% EConn2 = znoise:send(EConn1, Msg),
%% enoise:close(EConn2)
%% znoise:close(EConn2)
%% end}.
+81
View File
@@ -0,0 +1,81 @@
%%%-------------------------------------------------------------------
%%% @copyright (C) 2018, Aeternity Anstalt
%%%-------------------------------------------------------------------
-module(znoise_utils).
-compile([export_all, nowarn_export_all]).
echo_srv_start(Port, Protocol, SKP, Opts) ->
Pid = spawn(fun() -> echo_srv(Port, Protocol, SKP, Opts) end),
timer:sleep(10),
Pid.
echo_srv_stop(Pid) ->
erlang:exit(Pid, kill).
echo_srv(Port, Protocol, SKP, SrvOpts) ->
TcpOpts = [{active, true}, binary, {reuseaddr, true}],
{ok, LSock} = gen_tcp:listen(Port, TcpOpts),
{ok, TcpSock} = gen_tcp:accept(LSock, 500),
Opts = [{noise, Protocol}, {s, SKP}] ++
[{rs, proplists:get_value(cpub, SrvOpts)} || need_rs(responder, Protocol)],
AcceptRes =
try
znoise:accept(TcpSock, Opts)
catch _:R:S -> gen_tcp:close(TcpSock), {error, {R, S}} end,
gen_tcp:close(LSock),
case AcceptRes of
{ok, EConn, _} -> echo_srv_loop(EConn, SrvOpts);
Err = {error, _} -> srv_reply(Err, SrvOpts)
end.
echo_srv_loop(EConn, SrvOpts) ->
Recv =
case proplists:get_value(mode, SrvOpts, passive) of
passive ->
fun() ->
receive {noise, EConn, Data} -> Data
after 200 -> error(timeout) end
end;
active ->
fun() ->
{ok, Msg} = znoise:recv(EConn, 0, 100),
Msg
end
end,
Echos = proplists:get_value(echos, SrvOpts, 2),
Res =
try
[ begin
Msg = Recv(),
ok = znoise:send(EConn, Msg)
end || _ <- lists:seq(1, Echos) ],
ok
catch _:R -> {error, R} end,
srv_reply(Res, SrvOpts),
znoise:close(EConn),
Res.
srv_reply(Reply, SrvOpts) ->
case proplists:get_value(reply, SrvOpts, undefined) of
undefined -> ok;
Pid -> Pid ! {self(), server_result, Reply}
end.
need_rs(Role, Conf) when is_binary(Conf) ->
need_rs(Role, znoise_protocol:from_name(Conf));
need_rs(Role, Protocol) ->
PreMsgs = znoise_protocol:pre_msgs(Role, Protocol),
lists:member({in, [s]}, PreMsgs).
+17
View File
@@ -0,0 +1,17 @@
{name,"zNoise"}.
{type,lib}.
{modules,[]}.
{prefix,none}.
{desc,"An Erlang implementation of the Noise protocol, adapted for zx"}.
{author,"Craig Everett"}.
{package_id,{"otpr","znoise",{0,1,0}}}.
{deps,[]}.
{key_name,none}.
{a_email,"craigeverett@qpq.swiss"}.
{c_email,"craigeverett@qpq.swiss"}.
{copyright,"Craig Everett"}.
{file_exts,[]}.
{license,"ISC"}.
{repo_url,"https://git.qpq.swiss/zxq9/zNoise"}.
{tags,[]}.
{ws_url,"https://git.qpq.swiss/zxq9/zNoise"}.