forked from QPQ-AG/enoise
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a6eaf3e17e | |||
| d7c8f1ec29 | |||
| a5da9d08f5 | |||
| 853f6b5a8d | |||
| 029292817e | |||
| 2b5f08e156 | |||
| 91916908a0 | |||
| 479ec70870 | |||
| 8acbce9269 | |||
| be39bbc464 | |||
| dd94b371e6 | |||
| 11ca32c72f | |||
| 71300ba5b6 | |||
| ffde489e53 | |||
| 991d7390ea | |||
| 83fa0d5a00 | |||
| 336b331b8a | |||
| fc4a41f13d | |||
| 3819ba5c0f | |||
| 98d18bcaa5 | |||
| a3e803fc1a | |||
| 1e6ee6703f | |||
| 7c7ad54a6a | |||
| c06bbae07d | |||
| 29420d6d63 | |||
| 1ac27a035a | |||
| 6de4d0bf71 |
@@ -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,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.
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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]}.
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{"1.1.0",
|
||||
[{<<"enacl">>,{pkg,<<"enacl">>,<<"0.17.2">>},0}]}.
|
||||
[
|
||||
{pkg_hash,[
|
||||
{<<"enacl">>, <<"4AD59142943E72D72C56E33C30DEDEF28ADD8EBEE79C51033562B0CB4B93EDE0">>}]}
|
||||
].
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
@@ -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 >>.
|
||||
|
||||
@@ -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
@@ -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 }) ->
|
||||
@@ -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 >>.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
@@ -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"))).
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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).
|
||||
@@ -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))).
|
||||
@@ -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).
|
||||
@@ -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}.
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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"}.
|
||||
Reference in New Issue
Block a user