wip noisy echo
This commit is contained in:
@@ -0,0 +1,210 @@
|
|||||||
|
% ANSI screen drawing macros in erlang
|
||||||
|
%
|
||||||
|
% Author: Peter Harpending <peterharpending@qpq.swiss>
|
||||||
|
% Date: 2026-04-10
|
||||||
|
%
|
||||||
|
% Copyright (C) 2026, QPQ AG
|
||||||
|
|
||||||
|
% Not exhaustive, just what I need for the moment
|
||||||
|
% ref: https://gist.github.com/ConnerWill/d4b6c776b509add763e17f9f113fd25b
|
||||||
|
-define(ANSI_ESC, [27]).
|
||||||
|
-define(ANSI_CRLF, "\r\n").
|
||||||
|
-define(ANSI_FF, [12]).
|
||||||
|
-define(ANSI_CLEAR, [12]).
|
||||||
|
|
||||||
|
-define(ANSI_LINE(X), [X, ?ANSI_CRLF]).
|
||||||
|
|
||||||
|
% MARKDOWN TIER TEXT FORMATTING
|
||||||
|
|
||||||
|
% resets all formatting
|
||||||
|
-define(ANSI_RESET, [?ANSI_ESC, "[0m"]).
|
||||||
|
|
||||||
|
-define(ANSI_BOLD, [?ANSI_ESC, "[1m"]).
|
||||||
|
-define(ANSI_DIM, [?ANSI_ESC, "[2m"]).
|
||||||
|
-define(ANSI_ITALIC, [?ANSI_ESC, "[3m"]).
|
||||||
|
-define(ANSI_ULINE, [?ANSI_ESC, "[4m"]).
|
||||||
|
-define(ANSI_BLINK, [?ANSI_ESC, "[5m"]).
|
||||||
|
-define(ANSI_INVERT, [?ANSI_ESC, "[7m"]).
|
||||||
|
-define(ANSI_INVIS, [?ANSI_ESC, "[8m"]).
|
||||||
|
-define(ANSI_STRIKE, [?ANSI_ESC, "[9m"]).
|
||||||
|
|
||||||
|
% > Note: Both dim and bold modes are reset with the ESC[22m sequence. The
|
||||||
|
% > ESC[21m sequence is a non-specified sequence for double underline mode and
|
||||||
|
% > only work in some terminals and is reset with ESC[24m.
|
||||||
|
-define(ANSI_UNBOLD, [?ANSI_ESC, "[22m"]).
|
||||||
|
-define(ANSI_UNDIM, [?ANSI_ESC, "[22m"]).
|
||||||
|
-define(ANSI_UNITALIC, [?ANSI_ESC, "[23m"]).
|
||||||
|
-define(ANSI_UNULINE, [?ANSI_ESC, "[24m"]).
|
||||||
|
-define(ANSI_UNBLINK, [?ANSI_ESC, "[25m"]).
|
||||||
|
-define(ANSI_UNINVERT, [?ANSI_ESC, "[27m"]).
|
||||||
|
-define(ANSI_UNINVIS, [?ANSI_ESC, "[28m"]).
|
||||||
|
-define(ANSI_UNSTRIKE, [?ANSI_ESC, "[29m"]).
|
||||||
|
|
||||||
|
-define(ANSI_BOLD(X), [?ANSI_BOLD, X, ?ANSI_UNBOLD]).
|
||||||
|
-define(ANSI_DIM(X), [?ANSI_DIM, X, ?ANSI_UNDIM]).
|
||||||
|
-define(ANSI_ITALIC(X), [?ANSI_ITALIC, X, ?ANSI_UNITALIC]).
|
||||||
|
-define(ANSI_ULINE(X), [?ANSI_ULINE, X, ?ANSI_UNULINE]).
|
||||||
|
-define(ANSI_BLINK(X), [?ANSI_BLINK, X, ?ANSI_UNBLINK]).
|
||||||
|
-define(ANSI_INVERT(X), [?ANSI_INVERT, X, ?ANSI_UNINVERT]).
|
||||||
|
-define(ANSI_INVIS(X), [?ANSI_INVIS, X, ?ANSI_UNINVIS]).
|
||||||
|
-define(ANSI_STRIKE(X), [?ANSI_STRIKE, X, ?ANSI_UNSTRIKE]).
|
||||||
|
|
||||||
|
|
||||||
|
% COLORS
|
||||||
|
%
|
||||||
|
% COLOR SetFG SetBG
|
||||||
|
% -----------------------------
|
||||||
|
% Black 30 40
|
||||||
|
% Red 31 41
|
||||||
|
% Green 32 42
|
||||||
|
% Yellow 33 43
|
||||||
|
% Blue 34 44
|
||||||
|
% Magenta 35 45
|
||||||
|
% Cyan 36 46
|
||||||
|
% White 37 47
|
||||||
|
% Default 39 49
|
||||||
|
|
||||||
|
-define(ANSI_FG_RESET, [?ANSI_ESC, "[39m"]).
|
||||||
|
-define(ANSI_BG_RESET, [?ANSI_ESC, "[49m"]).
|
||||||
|
|
||||||
|
-define(ANSI_FG_BLACK, [?ANSI_ESC, "[30m"]).
|
||||||
|
-define(ANSI_FG_RED, [?ANSI_ESC, "[31m"]).
|
||||||
|
-define(ANSI_FG_GREEN, [?ANSI_ESC, "[32m"]).
|
||||||
|
-define(ANSI_FG_YELLOW, [?ANSI_ESC, "[33m"]).
|
||||||
|
-define(ANSI_FG_BLUE, [?ANSI_ESC, "[34m"]).
|
||||||
|
-define(ANSI_FG_MAGENTA, [?ANSI_ESC, "[35m"]).
|
||||||
|
-define(ANSI_FG_CYAN, [?ANSI_ESC, "[36m"]).
|
||||||
|
-define(ANSI_FG_WHITE, [?ANSI_ESC, "[37m"]).
|
||||||
|
|
||||||
|
-define(ANSI_BG_BLACK, [?ANSI_ESC, "[40m"]).
|
||||||
|
-define(ANSI_BG_RED, [?ANSI_ESC, "[41m"]).
|
||||||
|
-define(ANSI_BG_GREEN, [?ANSI_ESC, "[42m"]).
|
||||||
|
-define(ANSI_BG_YELLOW, [?ANSI_ESC, "[43m"]).
|
||||||
|
-define(ANSI_BG_BLUE, [?ANSI_ESC, "[44m"]).
|
||||||
|
-define(ANSI_BG_MAGENTA, [?ANSI_ESC, "[45m"]).
|
||||||
|
-define(ANSI_BG_CYAN, [?ANSI_ESC, "[46m"]).
|
||||||
|
-define(ANSI_BG_WHITE, [?ANSI_ESC, "[47m"]).
|
||||||
|
|
||||||
|
|
||||||
|
-define(ANSI_FG_BLACK(X), [?ANSI_FG_BLACK, X, ?ANSI_FG_RESET]).
|
||||||
|
-define(ANSI_FG_RED(X), [?ANSI_FG_RED, X, ?ANSI_FG_RESET]).
|
||||||
|
-define(ANSI_FG_GREEN(X), [?ANSI_FG_GREEN, X, ?ANSI_FG_RESET]).
|
||||||
|
-define(ANSI_FG_YELLOW(X), [?ANSI_FG_YELLOW, X, ?ANSI_FG_RESET]).
|
||||||
|
-define(ANSI_FG_BLUE(X), [?ANSI_FG_BLUE, X, ?ANSI_FG_RESET]).
|
||||||
|
-define(ANSI_FG_MAGENTA(X), [?ANSI_FG_MAGENTA, X, ?ANSI_FG_RESET]).
|
||||||
|
-define(ANSI_FG_CYAN(X), [?ANSI_FG_CYAN, X, ?ANSI_FG_RESET]).
|
||||||
|
-define(ANSI_FG_WHITE(X), [?ANSI_FG_WHITE, X, ?ANSI_FG_RESET]).
|
||||||
|
|
||||||
|
-define(ANSI_BG_BLACK(X), [?ANSI_BG_BLACK, X, ?ANSI_BG_RESET]).
|
||||||
|
-define(ANSI_BG_RED(X), [?ANSI_BG_RED, X, ?ANSI_BG_RESET]).
|
||||||
|
-define(ANSI_BG_GREEN(X), [?ANSI_BG_GREEN, X, ?ANSI_BG_RESET]).
|
||||||
|
-define(ANSI_BG_YELLOW(X), [?ANSI_BG_YELLOW, X, ?ANSI_BG_RESET]).
|
||||||
|
-define(ANSI_BG_BLUE(X), [?ANSI_BG_BLUE, X, ?ANSI_BG_RESET]).
|
||||||
|
-define(ANSI_BG_MAGENTA(X), [?ANSI_BG_MAGENTA, X, ?ANSI_BG_RESET]).
|
||||||
|
-define(ANSI_BG_CYAN(X), [?ANSI_BG_CYAN, X, ?ANSI_BG_RESET]).
|
||||||
|
-define(ANSI_BG_WHITE(X), [?ANSI_BG_WHITE, X, ?ANSI_BG_RESET]).
|
||||||
|
|
||||||
|
% bright colors
|
||||||
|
-define(ANSI_FG_BBLACK, [?ANSI_ESC, "[90m"]).
|
||||||
|
-define(ANSI_FG_BRED, [?ANSI_ESC, "[91m"]).
|
||||||
|
-define(ANSI_FG_BGREEN, [?ANSI_ESC, "[92m"]).
|
||||||
|
-define(ANSI_FG_BYELLOW, [?ANSI_ESC, "[93m"]).
|
||||||
|
-define(ANSI_FG_BBLUE, [?ANSI_ESC, "[94m"]).
|
||||||
|
-define(ANSI_FG_BMAGENTA, [?ANSI_ESC, "[95m"]).
|
||||||
|
-define(ANSI_FG_BCYAN, [?ANSI_ESC, "[96m"]).
|
||||||
|
-define(ANSI_FG_BWHITE, [?ANSI_ESC, "[97m"]).
|
||||||
|
|
||||||
|
-define(ANSI_BG_BBLACK, [?ANSI_ESC, "[100m"]).
|
||||||
|
-define(ANSI_BG_BRED, [?ANSI_ESC, "[101m"]).
|
||||||
|
-define(ANSI_BG_BGREEN, [?ANSI_ESC, "[102m"]).
|
||||||
|
-define(ANSI_BG_BYELLOW, [?ANSI_ESC, "[103m"]).
|
||||||
|
-define(ANSI_BG_BBLUE, [?ANSI_ESC, "[104m"]).
|
||||||
|
-define(ANSI_BG_BMAGENTA, [?ANSI_ESC, "[105m"]).
|
||||||
|
-define(ANSI_BG_BCYAN, [?ANSI_ESC, "[106m"]).
|
||||||
|
-define(ANSI_BG_BWHITE, [?ANSI_ESC, "[107m"]).
|
||||||
|
|
||||||
|
-define(ANSI_FG_BBLACK(X), [?ANSI_FG_BBLACK, X, ?ANSI_FG_RESET]).
|
||||||
|
-define(ANSI_FG_BRED(X), [?ANSI_FG_BRED, X, ?ANSI_FG_RESET]).
|
||||||
|
-define(ANSI_FG_BGREEN(X), [?ANSI_FG_BGREEN, X, ?ANSI_FG_RESET]).
|
||||||
|
-define(ANSI_FG_BYELLOW(X), [?ANSI_FG_BYELLOW, X, ?ANSI_FG_RESET]).
|
||||||
|
-define(ANSI_FG_BBLUE(X), [?ANSI_FG_BBLUE, X, ?ANSI_FG_RESET]).
|
||||||
|
-define(ANSI_FG_BMAGENTA(X), [?ANSI_FG_BMAGENTA, X, ?ANSI_FG_RESET]).
|
||||||
|
-define(ANSI_FG_BCYAN(X), [?ANSI_FG_BCYAN, X, ?ANSI_FG_RESET]).
|
||||||
|
-define(ANSI_FG_BWHITE(X), [?ANSI_FG_BWHITE, X, ?ANSI_FG_RESET]).
|
||||||
|
|
||||||
|
-define(ANSI_BG_BBLACK(X), [?ANSI_BG_BBLACK, X, ?ANSI_BG_RESET]).
|
||||||
|
-define(ANSI_BG_BRED(X), [?ANSI_BG_BRED, X, ?ANSI_BG_RESET]).
|
||||||
|
-define(ANSI_BG_BGREEN(X), [?ANSI_BG_BGREEN, X, ?ANSI_BG_RESET]).
|
||||||
|
-define(ANSI_BG_BYELLOW(X), [?ANSI_BG_BYELLOW, X, ?ANSI_BG_RESET]).
|
||||||
|
-define(ANSI_BG_BBLUE(X), [?ANSI_BG_BBLUE, X, ?ANSI_BG_RESET]).
|
||||||
|
-define(ANSI_BG_BMAGENTA(X), [?ANSI_BG_BMAGENTA, X, ?ANSI_BG_RESET]).
|
||||||
|
-define(ANSI_BG_BCYAN(X), [?ANSI_BG_BCYAN, X, ?ANSI_BG_RESET]).
|
||||||
|
-define(ANSI_BG_BWHITE(X), [?ANSI_BG_BWHITE, X, ?ANSI_BG_RESET]).
|
||||||
|
|
||||||
|
-define(ANSI_FG_RGB(R,G,B),
|
||||||
|
[?ANSI_ESC,
|
||||||
|
"[38;2;",
|
||||||
|
integer_to_list(R),";",
|
||||||
|
integer_to_list(G),";",
|
||||||
|
integer_to_list(B),"m"]
|
||||||
|
).
|
||||||
|
-define(ANSI_BG_RGB(R,G,B),
|
||||||
|
[?ANSI_ESC,
|
||||||
|
"[48;2;",
|
||||||
|
integer_to_list(R),";",
|
||||||
|
integer_to_list(G),";",
|
||||||
|
integer_to_list(B),"m"]
|
||||||
|
).
|
||||||
|
|
||||||
|
-define(ANSI_FG_RGB(R,G,B,Chars), [?ANSI_FG_RGB(R,G,B), Chars, ?ANSI_FG_RESET]).
|
||||||
|
-define(ANSI_BG_RGB(R,G,B,Chars), [?ANSI_BG_RGB(R,G,B), Chars, ?ANSI_BG_RESET]).
|
||||||
|
|
||||||
|
|
||||||
|
% cursor controls
|
||||||
|
-define(ANSI_CUR_HOME, [?ANSI_ESC, "[H"]).
|
||||||
|
|
||||||
|
-define(ANSI_CUR_XY(X, Y), [?ANSI_ESC, "[", integer_to_list(Y), ";", integer_to_list(X), "H"]).
|
||||||
|
-define(ANSI_CUR_UP(N), [?ANSI_ESC, "[", integer_to_list(N), "A"]).
|
||||||
|
-define(ANSI_CUR_DOWN(N), [?ANSI_ESC, "[", integer_to_list(N), "B"]).
|
||||||
|
-define(ANSI_CUR_RIGHT(N), [?ANSI_ESC, "[", integer_to_list(N), "C"]).
|
||||||
|
-define(ANSI_CUR_LEFT(N), [?ANSI_ESC, "[", integer_to_list(N), "D"]).
|
||||||
|
-define(ANSI_CUR_SAVE, [?ANSI_ESC, "7"]).
|
||||||
|
-define(ANSI_CUR_RESTORE, [?ANSI_ESC, "8"]).
|
||||||
|
-define(ANSI_CUR_QUERY, [?ANSI_ESC, "[6n"]).
|
||||||
|
|
||||||
|
-define(ANSI_CUR_UP, ?ANSI_CUR_UP(1)).
|
||||||
|
-define(ANSI_CUR_DOWN, ?ANSI_CUR_DOWN(1)).
|
||||||
|
-define(ANSI_CUR_RIGHT, ?ANSI_CUR_RIGHT(1)).
|
||||||
|
-define(ANSI_CUR_LEFT, ?ANSI_CUR_LEFT(1)).
|
||||||
|
|
||||||
|
% aliases
|
||||||
|
-define(ANSI_CURXY(X, Y), ?ANSI_CUR_XY(X, Y)).
|
||||||
|
-define(ANSI_CUR_RC(R, C), ?ANSI_CUR_XY(C, R)).
|
||||||
|
-define(ANSI_CURRC(R, C), ?ANSI_CUR_XY(C, R)).
|
||||||
|
|
||||||
|
|
||||||
|
% relative movement "forward" +X=right, +Y=down
|
||||||
|
-define(ANSI_CUR_VECT(X, Y),
|
||||||
|
if X =< 0, Y =< 0 -> [?ANSI_CUR_LEFT(-1*X), ?ANSI_CUR_UP(-1*Y)];
|
||||||
|
X =< 0, 0 < Y -> [?ANSI_CUR_LEFT(-1*X), ?ANSI_CUR_DOWN(Y)];
|
||||||
|
0 < X, Y =< 0 -> [?ANSI_CUR_RIGHT(X), ?ANSI_CUR_UP(-1*Y)];
|
||||||
|
0 < X, 0 < Y -> [?ANSI_CUR_RIGHT(X), ?ANSI_CUR_DOWN(Y)]
|
||||||
|
end
|
||||||
|
).
|
||||||
|
|
||||||
|
|
||||||
|
-define(ANSI_ALTBUF, [?ANSI_ESC, "[?1049h"]).
|
||||||
|
-define(ANSI_UNALTBUF, [?ANSI_ESC, "[?1049l"]).
|
||||||
|
|
||||||
|
-define(ANSI_CUR_INVIS, [?ANSI_ESC, "[?25l"]).
|
||||||
|
-define(ANSI_CUR_VIS, [?ANSI_ESC, "[?25h"]).
|
||||||
|
|
||||||
|
-define(ANSI_WRAP, [?ANSI_ESC, "[=7h"]).
|
||||||
|
-define(ANSI_NOWRAP, [?ANSI_ESC, "[=7l"]).
|
||||||
|
|
||||||
|
% XTERM control sequences.
|
||||||
|
% References:
|
||||||
|
% 1. `man xterm`
|
||||||
|
% 2. Control sequences reference: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||||
|
% 3. Historical background: https://invisible-island.net/xterm/modified-keys.html
|
||||||
|
%-define(XTERM_
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
%%% ------------------------------------------------------------------
|
||||||
|
%%% @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() :: enoise_hs_state:noise_split_state().
|
||||||
|
%% Return value from the final `split' operation. Provides a CipherState for
|
||||||
|
%% receiving and a CipherState transmission. Also includes the final handshake
|
||||||
|
%% hash for channel binding.
|
||||||
|
|
||||||
|
-opaque noise_socket() :: #enoise{}.
|
||||||
|
%% 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) ->
|
||||||
|
create_hstate(Options, Role).
|
||||||
|
|
||||||
|
%% @doc Do a step (either `{send, Payload}', `{rcvd, EncryptedData}',
|
||||||
|
%% or `done')
|
||||||
|
%% @end
|
||||||
|
-spec step_handshake(HState :: enoise_hs_state:state(),
|
||||||
|
Data :: {rcvd, binary()} | {send, binary()}) ->
|
||||||
|
{ok, send, binary(), enoise_hs_state:state()}
|
||||||
|
| {ok, rcvd, binary(), enoise_hs_state:state()}
|
||||||
|
| {ok, done, noise_split_state()}
|
||||||
|
| {error, term()}.
|
||||||
|
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) ->
|
||||||
|
case create_hstate(Options, Role) of
|
||||||
|
{ok, HState} ->
|
||||||
|
Timeout = proplists:get_value(timeout, Options, infinity),
|
||||||
|
do_handshake(HState, ComState, Timeout);
|
||||||
|
Err = {error, _} ->
|
||||||
|
Err
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc Upgrades a gen_tcp, or equivalent, connected socket to a Noise socket,
|
||||||
|
%% 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(), enoise_hs_state:state()} | {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,
|
||||||
|
DH = enoise_protocol:dh(NoiseProtocol),
|
||||||
|
S = proplists:get_value(s, Options, undefined),
|
||||||
|
E = proplists:get_value(e, Options, undefined),
|
||||||
|
RS = remote_keypair(DH, proplists:get_value(rs, Options, undefined)),
|
||||||
|
RE = remote_keypair(DH, proplists:get_value(re, Options, undefined)),
|
||||||
|
|
||||||
|
enoise_hs_state:init(NoiseProtocol, Role,
|
||||||
|
Prologue, {S, E, RS, RE}).
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
remote_keypair(_DH, undefined) -> undefined;
|
||||||
|
remote_keypair(DH, RemotePub) when is_binary(RemotePub) -> enoise_keypair:new(DH, RemotePub).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-define(MAX_NONCE, 16#FFFFFFFFFFFFFFFF).
|
||||||
|
-define(MAX_AD_LEN, 16).
|
||||||
|
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
%%% ------------------------------------------------------------------
|
||||||
|
%%% @copyright 2018, Aeternity Anstalt
|
||||||
|
%%%
|
||||||
|
%%% @doc Module encapsulating a Noise Cipher state
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% ------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(enoise_cipher_state).
|
||||||
|
|
||||||
|
-export([ cipher/1
|
||||||
|
, decrypt_with_ad/3
|
||||||
|
, encrypt_with_ad/3
|
||||||
|
, has_key/1
|
||||||
|
, init/2
|
||||||
|
, key/1
|
||||||
|
, rekey/1
|
||||||
|
, set_key/2
|
||||||
|
, set_nonce/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-include("enoise.hrl").
|
||||||
|
|
||||||
|
-type noise_cipher() :: 'ChaChaPoly' | 'AESGCM'.
|
||||||
|
-type nonce() :: non_neg_integer().
|
||||||
|
-type key() :: empty | binary().
|
||||||
|
|
||||||
|
-record(noise_cs, { k = empty :: key()
|
||||||
|
, n = 0 :: nonce()
|
||||||
|
, cipher = 'ChaChaPoly' :: noise_cipher() }).
|
||||||
|
|
||||||
|
-opaque state() :: #noise_cs{}.
|
||||||
|
|
||||||
|
-export_type([noise_cipher/0, state/0]).
|
||||||
|
|
||||||
|
-spec init(Key :: key(), Cipher :: noise_cipher()) -> state().
|
||||||
|
init(Key, Cipher) ->
|
||||||
|
#noise_cs{ k = Key, n = 0, cipher = Cipher }.
|
||||||
|
|
||||||
|
-spec set_key(CState :: state(), NewKey :: key()) -> state().
|
||||||
|
set_key(CState, NewKey) ->
|
||||||
|
CState#noise_cs{ k = NewKey, n = 0 }.
|
||||||
|
|
||||||
|
-spec has_key(CState :: state()) -> boolean().
|
||||||
|
has_key(#noise_cs{ k = Key }) ->
|
||||||
|
Key =/= empty.
|
||||||
|
|
||||||
|
-spec set_nonce(CState :: state(), NewNonce :: nonce()) -> state().
|
||||||
|
set_nonce(CState = #noise_cs{}, Nonce) ->
|
||||||
|
CState#noise_cs{ n = Nonce }.
|
||||||
|
|
||||||
|
-spec encrypt_with_ad(CState :: state(), AD :: binary(), PlainText :: 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) ->
|
||||||
|
CipherText = enoise_crypto:encrypt(Cipher, K, N, AD, PlainText),
|
||||||
|
{ok, CState#noise_cs{ n = N+1 }, CipherText}.
|
||||||
|
|
||||||
|
-spec decrypt_with_ad(CState :: state(), AD :: binary(), CipherText :: binary()) ->
|
||||||
|
{ok, state(), binary()} | {error, term()}.
|
||||||
|
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
|
||||||
|
PlainText when is_binary(PlainText) ->
|
||||||
|
{ok, CState#noise_cs{ n = N+1 }, PlainText};
|
||||||
|
Err = {error, _} ->
|
||||||
|
Err
|
||||||
|
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) }.
|
||||||
|
|
||||||
|
-spec cipher(CState :: state()) -> noise_cipher().
|
||||||
|
cipher(#noise_cs{ cipher = Cipher }) ->
|
||||||
|
Cipher.
|
||||||
|
|
||||||
|
-spec key(CState :: state()) -> key().
|
||||||
|
key(#noise_cs{ k = K }) ->
|
||||||
|
K.
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
%%% ------------------------------------------------------------------
|
||||||
|
%%% @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.
|
||||||
|
|
||||||
|
-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 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).
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
%%% ------------------------------------------------------------------
|
||||||
|
%%% @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(Type, Key1, Key2) when Type == dh25519; Type == dh448 ->
|
||||||
|
dh_(ecdh_type(Type), enoise_keypair:pubkey(Key2), enoise_keypair:seckey(Key1));
|
||||||
|
dh(Type, _Key1, _Key2) ->
|
||||||
|
error({unsupported_diffie_hellman, Type}).
|
||||||
|
|
||||||
|
ecdh_type(dh25519) -> x25519;
|
||||||
|
ecdh_type(dh448) -> x448.
|
||||||
|
|
||||||
|
dh_(DHType, OtherPub, MyPriv) ->
|
||||||
|
crypto:compute_key(ecdh, OtherPub, MyPriv, DHType).
|
||||||
|
|
||||||
|
-spec hmac(Hash :: enoise_sym_state:noise_hash(),
|
||||||
|
Key :: binary(), Data :: binary()) -> binary().
|
||||||
|
hmac(Hash, Key, Data) ->
|
||||||
|
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('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 :: enoise_cipher_state:noise_cipher(), Key :: binary(),
|
||||||
|
Nonce :: non_neg_integer(), Ad :: binary(), PlainText :: binary()) -> binary().
|
||||||
|
encrypt(Cipher, K, N, Ad, PlainText) ->
|
||||||
|
{CText, CTag} = crypto:crypto_one_time_aead(cipher(Cipher), K, nonce(Cipher, N), PlainText, Ad, true),
|
||||||
|
<<CText/binary, CTag/binary>>.
|
||||||
|
|
||||||
|
-spec decrypt(Cipher ::enoise_cipher_state:noise_cipher(), Key :: binary(),
|
||||||
|
Nonce :: non_neg_integer(), AD :: binary(),
|
||||||
|
CipherText :: binary()) -> binary() | {error, term()}.
|
||||||
|
decrypt(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 :: enoise_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 :: 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 >>.
|
||||||
|
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
%%% ------------------------------------------------------------------
|
||||||
|
%%% @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().
|
||||||
|
-type noise_split_state() :: #{ rx := enoise_cipher_state:state(),
|
||||||
|
tx := enoise_cipher_state:state(),
|
||||||
|
hs_hash := binary(),
|
||||||
|
final_state => state() }.
|
||||||
|
|
||||||
|
-type optional_key() :: undefined | keypair().
|
||||||
|
-type initial_keys() :: {optional_key(), optional_key(), optional_key(), optional_key()}.
|
||||||
|
|
||||||
|
-record(noise_hs, { ss :: enoise_sym_state:state()
|
||||||
|
, s :: keypair() | undefined
|
||||||
|
, 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_split_state/0, noise_token/0, state/0]).
|
||||||
|
|
||||||
|
-spec init(Protocol :: enoise_protocol:protocol(), Role :: noise_role(),
|
||||||
|
Prologue :: binary(), Keys :: initial_keys()) -> {ok, state()} | {error, term()}.
|
||||||
|
init(Protocol, Role, Prologue, {S, E, RS, RE}) ->
|
||||||
|
SS0 = enoise_sym_state:init(Protocol),
|
||||||
|
SS1 = enoise_sym_state:mix_hash(SS0, Prologue),
|
||||||
|
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),
|
||||||
|
pre_mix(PreMsgs, HS).
|
||||||
|
|
||||||
|
pre_mix([], HS) -> {ok, HS};
|
||||||
|
pre_mix([{out, [s]} | Msgs], HS = #noise_hs{ s = S }) when S /= undefined ->
|
||||||
|
pre_mix(Msgs, mix_hash(HS, enoise_keypair:pubkey(S)));
|
||||||
|
pre_mix([{out, [e]} | Msgs], HS = #noise_hs{ e = E }) when E /= undefined ->
|
||||||
|
pre_mix(Msgs, mix_hash(HS, enoise_keypair:pubkey(E)));
|
||||||
|
pre_mix([{in, [s]} | Msgs], HS = #noise_hs{ rs = RS }) when RS /= undefined ->
|
||||||
|
pre_mix(Msgs, mix_hash(HS, enoise_keypair:pubkey(RS)));
|
||||||
|
pre_mix([{in, [e]} | Msgs], HS = #noise_hs{ re = RE }) when RE /= undefined ->
|
||||||
|
pre_mix(Msgs, mix_hash(HS, enoise_keypair:pubkey(RE)));
|
||||||
|
pre_mix(_Msg, _HS) ->
|
||||||
|
{error, invalid_noise_setup}.
|
||||||
|
|
||||||
|
-spec finalize(HS :: state()) -> {ok, noise_split_state()} | {error, term()}.
|
||||||
|
finalize(HS = #noise_hs{ msgs = [], ss = SS, role = Role }) ->
|
||||||
|
{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(#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 = 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),
|
||||||
|
case Data0 of
|
||||||
|
<<REPub:DHLen/binary, Data1/binary>> ->
|
||||||
|
RE = enoise_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 -> enoise_crypto:dhlen(DH) + 16;
|
||||||
|
false -> enoise_crypto:dhlen(DH)
|
||||||
|
end,
|
||||||
|
case Data0 of
|
||||||
|
<<Temp:DHLen/binary, Data1/binary>> ->
|
||||||
|
case decrypt_and_hash(HS, Temp) of
|
||||||
|
{ok, HS1, RSPub} ->
|
||||||
|
RS = enoise_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 }) ->
|
||||||
|
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.
|
||||||
|
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
%%% ------------------------------------------------------------------
|
||||||
|
%%% @copyright 2018, Aeternity Anstalt
|
||||||
|
%%%
|
||||||
|
%%% @doc Module is an abstract data type for a key pair.
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% ------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(enoise_keypair).
|
||||||
|
|
||||||
|
-export([ key_type/1
|
||||||
|
, new/1
|
||||||
|
, new/2
|
||||||
|
, new/3
|
||||||
|
, pubkey/1
|
||||||
|
, seckey/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
-type key_type() :: dh25519 | dh448.
|
||||||
|
|
||||||
|
-record(kp, { type :: key_type()
|
||||||
|
, sec :: binary() | undefined
|
||||||
|
, pub :: binary() }).
|
||||||
|
|
||||||
|
-opaque keypair() :: #kp{}.
|
||||||
|
%% Abstract keypair holding a secret key/public key pair and its type.
|
||||||
|
|
||||||
|
-export_type([keypair/0]).
|
||||||
|
|
||||||
|
%% @doc Generate a new keypair of type `Type'.
|
||||||
|
-spec new(Type :: key_type()) -> keypair().
|
||||||
|
new(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() | undefined,
|
||||||
|
Public :: binary() | undefined) -> keypair().
|
||||||
|
new(Type, Secret, undefined) ->
|
||||||
|
new(Type, Secret, pubkey_from_secret(Type, Secret));
|
||||||
|
new(Type, Secret, Public) ->
|
||||||
|
#kp{ type = Type, sec = Secret, pub = Public }.
|
||||||
|
|
||||||
|
%% @doc Define a "public only" keypair - holding just a public key and
|
||||||
|
%% `undefined' for secret key.
|
||||||
|
-spec new(Type :: key_type(), Public :: binary()) -> keypair().
|
||||||
|
new(Type, Public) ->
|
||||||
|
#kp{ type = Type, sec = undefined, pub = Public }.
|
||||||
|
|
||||||
|
%% @doc Accessor function - return the key type of the key pair.
|
||||||
|
-spec key_type(KeyPair :: keypair()) -> key_type().
|
||||||
|
key_type(#kp{ type = T }) ->
|
||||||
|
T.
|
||||||
|
|
||||||
|
%% @doc Accessor function - return the public key of the key pair.
|
||||||
|
-spec pubkey(KeyPair :: keypair()) -> binary().
|
||||||
|
pubkey(#kp{ pub = P }) ->
|
||||||
|
P.
|
||||||
|
|
||||||
|
%% @doc Accessor function - return the secret key of the key pair.
|
||||||
|
%% This function will throw an error if the key pair is "public only".
|
||||||
|
-spec seckey(KeyPair :: keypair()) -> binary().
|
||||||
|
seckey(#kp{ sec = undefined }) ->
|
||||||
|
error(keypair_is_public_only);
|
||||||
|
seckey(#kp{ sec = S }) ->
|
||||||
|
S.
|
||||||
|
|
||||||
|
%% -- Local functions --------------------------------------------------------
|
||||||
|
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(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.
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
%%% ------------------------------------------------------------------
|
||||||
|
%%% @copyright 2018, Aeternity Anstalt
|
||||||
|
%%%
|
||||||
|
%%% @doc Module defining Noise protocol configurations
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% ------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(enoise_protocol).
|
||||||
|
|
||||||
|
-export([ cipher/1
|
||||||
|
, dh/1
|
||||||
|
, from_name/1
|
||||||
|
, hash/1
|
||||||
|
, msgs/2
|
||||||
|
, pattern/1
|
||||||
|
, pre_msgs/2
|
||||||
|
, supported/0
|
||||||
|
, to_name/1]).
|
||||||
|
|
||||||
|
-ifdef(TEST).
|
||||||
|
-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()]}.
|
||||||
|
|
||||||
|
-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()
|
||||||
|
}).
|
||||||
|
|
||||||
|
-opaque protocol() :: #noise_protocol{}.
|
||||||
|
|
||||||
|
-export_type([noise_msg/0, noise_pattern/0, protocol/0]).
|
||||||
|
|
||||||
|
-spec cipher(Protocol :: protocol()) -> enoise_cipher_state:noise_cipher().
|
||||||
|
cipher(#noise_protocol{ cipher = Cipher }) ->
|
||||||
|
Cipher.
|
||||||
|
|
||||||
|
-spec dh(Protocol :: protocol()) -> enoise_hs_state:noise_dh().
|
||||||
|
dh(#noise_protocol{ dh = Dh }) ->
|
||||||
|
Dh.
|
||||||
|
|
||||||
|
-spec hash(Protocol :: protocol()) -> enoise_sym_state:noise_hash().
|
||||||
|
hash(#noise_protocol{ hash = Hash }) ->
|
||||||
|
Hash.
|
||||||
|
|
||||||
|
-spec pattern(Protocol :: protocol()) -> noise_pattern().
|
||||||
|
pattern(#noise_protocol{ hs_pattern = Pattern }) ->
|
||||||
|
Pattern.
|
||||||
|
|
||||||
|
-spec to_name(Protocol :: protocol()) -> binary().
|
||||||
|
to_name(Protocol = #noise_protocol{ hs_pattern = Pattern, dh = Dh
|
||||||
|
, cipher = Cipher, hash = Hash }) ->
|
||||||
|
case supported_pattern(Pattern) andalso supported_dh(Dh) andalso
|
||||||
|
supported_cipher(Cipher) andalso supported_hash(Hash) of
|
||||||
|
true -> to_name(Pattern, Dh, Cipher, Hash);
|
||||||
|
false -> error({protocol_not_recognized, Protocol})
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec from_name(Name :: string() | binary()) -> protocol().
|
||||||
|
from_name(Bin) when is_binary(Bin) -> from_name(binary_to_list(Bin));
|
||||||
|
from_name(String) ->
|
||||||
|
case string:lexemes(String, "_") of
|
||||||
|
["Noise", PatStr, DhStr, CipStr, HashStr] ->
|
||||||
|
Pattern = from_name_pattern(PatStr),
|
||||||
|
Dh = from_name_dh(DhStr),
|
||||||
|
Cipher = from_name_cipher(CipStr),
|
||||||
|
Hash = from_name_hash(HashStr),
|
||||||
|
case supported_pattern(Pattern) andalso supported_dh(Dh) andalso
|
||||||
|
supported_cipher(Cipher) andalso supported_hash(Hash) of
|
||||||
|
true -> #noise_protocol{ hs_pattern = Pattern, dh = Dh
|
||||||
|
, cipher = Cipher, hash = Hash };
|
||||||
|
false -> error({name_not_recognized, String})
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
error({name_not_recognized, String})
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec msgs(Role :: enoise_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()].
|
||||||
|
pre_msgs(Role, #noise_protocol{ hs_pattern = Pattern }) ->
|
||||||
|
{PreMsgs, _Msgs} = protocol(Pattern),
|
||||||
|
role_adapt(Role, PreMsgs).
|
||||||
|
|
||||||
|
-spec role_adapt(Role :: enoise_hs_state:noise_role(), [noise_msg()]) -> [noise_msg()].
|
||||||
|
role_adapt(initiator, Msgs) ->
|
||||||
|
Msgs;
|
||||||
|
role_adapt(responder, Msgs) ->
|
||||||
|
Flip = fun({in, Msg}) -> {out, Msg}; ({out, Msg}) -> {in, Msg} end,
|
||||||
|
lists:map(Flip, Msgs).
|
||||||
|
|
||||||
|
protocol(nn) ->
|
||||||
|
{[], [{out, [e]}, {in, [e, ee]}]};
|
||||||
|
protocol(kn) ->
|
||||||
|
{[{out, [s]}], [{out, [e]}, {in, [e, ee, se]}]};
|
||||||
|
protocol(nk) ->
|
||||||
|
{[{in, [s]}], [{out, [e, es]}, {in, [e, ee]}]};
|
||||||
|
protocol(kk) ->
|
||||||
|
{[{out, [s]}, {in, [s]}], [{out, [e, es, ss]}, {in, [e, ee, se]}]};
|
||||||
|
protocol(nx) ->
|
||||||
|
{[], [{out, [e]}, {in, [e, ee, s, es]}]};
|
||||||
|
protocol(kx) ->
|
||||||
|
{[{out, [s]}], [{out, [e]}, {in, [e, ee, se, s, es]}]};
|
||||||
|
protocol(xn) ->
|
||||||
|
{[], [{out, [e]}, {in, [e, ee]}, {out, [s, se]}]};
|
||||||
|
protocol(in) ->
|
||||||
|
{[], [{out, [e, s]}, {in, [e, ee, se]}]};
|
||||||
|
protocol(xk) ->
|
||||||
|
{[{in, [s]}], [{out, [e, es]}, {in, [e, ee]}, {out, [s, se]}]};
|
||||||
|
protocol(ik) ->
|
||||||
|
{[{in, [s]}], [{out, [e, es, s, ss]}, {in, [e, ee, se]}]};
|
||||||
|
protocol(xx) ->
|
||||||
|
{[], [{out, [e]}, {in, [e, ee, s, es]}, {out, [s, se]}]};
|
||||||
|
protocol(ix) ->
|
||||||
|
{[], [{out, [e, s]}, {in, [e, ee, se, s, es]}]}.
|
||||||
|
|
||||||
|
supported_pattern(P) ->
|
||||||
|
lists:member(P, maps:get(hs_pattern, supported())).
|
||||||
|
|
||||||
|
supported_hash(Hash) ->
|
||||||
|
lists:member(Hash, maps:get(hash, supported())).
|
||||||
|
|
||||||
|
supported_cipher(Cipher) ->
|
||||||
|
lists:member(Cipher, maps:get(cipher, supported())).
|
||||||
|
|
||||||
|
supported_dh(Dh) ->
|
||||||
|
lists:member(Dh, maps:get(dh, supported())).
|
||||||
|
|
||||||
|
-spec supported() -> map().
|
||||||
|
supported() ->
|
||||||
|
#{ hs_pattern => [nn, kn, nk, kk, nx, kx, xn, in, xk, ik, xx, ix]
|
||||||
|
, hash => [blake2s, blake2b, sha256, sha512]
|
||||||
|
, cipher => ['ChaChaPoly', 'AESGCM']
|
||||||
|
, dh => [dh25519, dh448]
|
||||||
|
}.
|
||||||
|
|
||||||
|
to_name(Pattern, Dh, Cipher, Hash) ->
|
||||||
|
list_to_binary(lists:join("_", ["Noise", to_name_pattern(Pattern), to_name_dh(Dh),
|
||||||
|
to_name_cipher(Cipher), to_name_hash(Hash)])).
|
||||||
|
|
||||||
|
to_name_pattern(Atom) ->
|
||||||
|
[Simple | Rest] = string:lexemes(atom_to_list(Atom), "_"),
|
||||||
|
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(lists:flatten(string:lowercase(Simple) ++
|
||||||
|
case Mod1 of
|
||||||
|
"" -> "";
|
||||||
|
_ -> "_" ++ lists:join("_", [Mod1 | Mod2])
|
||||||
|
end)).
|
||||||
|
|
||||||
|
to_name_dh(dh25519) -> "25519";
|
||||||
|
to_name_dh(dh448) -> "448".
|
||||||
|
|
||||||
|
from_name_dh(Dh) -> list_to_atom("dh" ++ Dh).
|
||||||
|
|
||||||
|
to_name_cipher(Cipher) -> atom_to_list(Cipher).
|
||||||
|
|
||||||
|
from_name_cipher(Cipher) -> list_to_atom(Cipher).
|
||||||
|
|
||||||
|
to_name_hash(sha256) -> "SHA256";
|
||||||
|
to_name_hash(sha512) -> "SHA512";
|
||||||
|
to_name_hash(blake2s) -> "BLAKE2s";
|
||||||
|
to_name_hash(blake2b) -> "BLAKE2b".
|
||||||
|
|
||||||
|
from_name_hash(Hash) -> list_to_atom(string:lowercase(Hash)).
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
%%% ------------------------------------------------------------------
|
||||||
|
%%% @copyright 2018, Aeternity Anstalt
|
||||||
|
%%%
|
||||||
|
%%% @doc Module encapsulating a Noise symmetric (hash) state
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% ------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(enoise_sym_state).
|
||||||
|
|
||||||
|
-export([ cipher_state/1
|
||||||
|
, ck/1
|
||||||
|
, decrypt_and_hash/2
|
||||||
|
, encrypt_and_hash/2
|
||||||
|
, h/1
|
||||||
|
, hash/1
|
||||||
|
, init/1
|
||||||
|
, mix_hash/2
|
||||||
|
, mix_key/2
|
||||||
|
, mix_key_and_hash/2
|
||||||
|
, split/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
-include("enoise.hrl").
|
||||||
|
|
||||||
|
-type noise_hash() :: sha256 | sha512 | blake2s | blake2b.
|
||||||
|
|
||||||
|
-record(noise_ss, { cs :: enoise_cipher_state:state()
|
||||||
|
, ck = <<>> :: binary()
|
||||||
|
, h = <<>> :: binary()
|
||||||
|
, hash = blake2b :: noise_hash() }).
|
||||||
|
|
||||||
|
-opaque state() :: #noise_ss{}.
|
||||||
|
-export_type([noise_hash/0, state/0]).
|
||||||
|
|
||||||
|
-spec init(Protocol :: enoise_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),
|
||||||
|
H1 =
|
||||||
|
case byte_size(Name) > HashLen of
|
||||||
|
true -> enoise_crypto:hash(Hash, Name);
|
||||||
|
false -> enoise_crypto:pad(Name, HashLen, 16#00)
|
||||||
|
end,
|
||||||
|
#noise_ss{ h = H1
|
||||||
|
, ck = H1
|
||||||
|
, hash = Hash
|
||||||
|
, cs = enoise_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),
|
||||||
|
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>>),
|
||||||
|
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),
|
||||||
|
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, 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
|
||||||
|
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()}.
|
||||||
|
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)}.
|
||||||
|
|
||||||
|
-spec cipher_state(SState :: state()) -> enoise_cipher_state:state().
|
||||||
|
cipher_state(#noise_ss{ cs = CS }) ->
|
||||||
|
CS.
|
||||||
|
|
||||||
|
-spec ck(SState :: state()) -> binary().
|
||||||
|
ck(#noise_ss{ ck = CK }) ->
|
||||||
|
CK.
|
||||||
|
|
||||||
|
-spec h(SState :: state()) -> binary().
|
||||||
|
h(#noise_ss{ h = H }) ->
|
||||||
|
H.
|
||||||
|
|
||||||
|
-spec hash(SState :: state()) -> noise_hash().
|
||||||
|
hash(#noise_ss{ hash = Hash }) ->
|
||||||
|
Hash.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
% @doc echo server client
|
||||||
|
-module(nechoc).
|
||||||
|
|
||||||
|
-export([main/0]).
|
||||||
|
|
||||||
|
main() ->
|
||||||
|
{ok, TcpSocket} = gen_tcp:connect("localhost", 6969, [binary, {packet, 0}]),
|
||||||
|
ok = inet:setopts(Socket, [{active, once}]),
|
||||||
|
{ok, NoiseSock, _HandshakeState} = enoise:connect(TcpSocket, noise_options()),
|
||||||
|
loop(Socket).
|
||||||
|
|
||||||
|
noise_options() ->
|
||||||
|
% totally safe crypto
|
||||||
|
KP = enoise_keypair:new(dh25519),
|
||||||
|
[{noise, "Noise_NN_25519_ChaChaPoly_BLAKE2b"},
|
||||||
|
{e, KP}].
|
||||||
|
|
||||||
|
|
||||||
|
j_loop(NSock) ->
|
||||||
|
ok = inet:setopts(NSock, [{active, once}]),
|
||||||
|
receive
|
||||||
|
{tcp, Socket, Bytes} ->
|
||||||
|
ok = io:format("< ~tp~n", [Bytes]),
|
||||||
|
loop(Socket);
|
||||||
|
{tcp_closed, Socket} ->
|
||||||
|
ok = io:format("connection terminated~n", []),
|
||||||
|
exit(normal)
|
||||||
|
end.
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
%% @doc
|
||||||
|
%% super simple telnet echo server daemon
|
||||||
|
%%
|
||||||
|
%% no noise yet
|
||||||
|
%%
|
||||||
|
%% pimps and hos model (from trash talk template):
|
||||||
|
%%
|
||||||
|
%% ```
|
||||||
|
%% t_sup
|
||||||
|
%% |
|
||||||
|
%% t_clients
|
||||||
|
%% |
|
||||||
|
%% +--- t_client_man pimp
|
||||||
|
%% |
|
||||||
|
%% +--- t_client_sup bottom bitch
|
||||||
|
%% |
|
||||||
|
%% +--- <0.1.0> (t_client) ho
|
||||||
|
%% +--- <0.2.0> (t_client) ho
|
||||||
|
%% +--- <0.3.0> (t_client) ho
|
||||||
|
%% '''
|
||||||
|
%%
|
||||||
|
%% each ho:
|
||||||
|
%%
|
||||||
|
%% <ul>
|
||||||
|
%% <li>Takes listen socket as arg</li>
|
||||||
|
%% <li>spawns next ho</li>
|
||||||
|
%% </ul>
|
||||||
|
%%
|
||||||
|
%% ```
|
||||||
|
%% start(ListenSocket) ->
|
||||||
|
%% t_client_sup:start_acceptor(ListenSocket).
|
||||||
|
%%
|
||||||
|
%% start_link(ListenSocket) ->
|
||||||
|
%% proc_lib:start_link(?MODULE, init, [self(), ListenSocket]).
|
||||||
|
%%
|
||||||
|
%% init(Parent, ListenSocket) ->
|
||||||
|
%% ok = io:format("~p Listening.~n", [self()]),
|
||||||
|
%% Debug = sys:debug_options([]),
|
||||||
|
%% ok = proc_lib:init_ack(Parent, {ok, self()}),
|
||||||
|
%% listen(Parent, Debug, ListenSocket).
|
||||||
|
%%
|
||||||
|
%% listen(Parent, Debug, ListenSocket) ->
|
||||||
|
%% case gen_tcp:accept(ListenSocket) of
|
||||||
|
%% {ok, Socket} ->
|
||||||
|
%% {ok, _} = start(ListenSocket),
|
||||||
|
%% {ok, Peer} = inet:peername(Socket),
|
||||||
|
%% ok = io:format("~p Connection accepted from: ~p~n", [self(), Peer]),
|
||||||
|
%% ok = t_client_man:enroll(),
|
||||||
|
%% State = #s{socket = Socket},
|
||||||
|
%% loop(Parent, Debug, State);
|
||||||
|
%% {error, closed} ->
|
||||||
|
%% ok = io:format("~p Retiring: Listen socket closed.~n", [self()]),
|
||||||
|
%% exit(normal)
|
||||||
|
%% end.
|
||||||
|
%% '''
|
||||||
|
%%
|
||||||
|
%% @end
|
||||||
|
-module(echod).
|
||||||
|
|
||||||
|
-include("./ansi.hrl").
|
||||||
|
-export([main/0]).
|
||||||
|
|
||||||
|
|
||||||
|
% ho from pimp pov
|
||||||
|
-record(pho,
|
||||||
|
{pid :: pid(),
|
||||||
|
ref :: reference(),
|
||||||
|
name :: string()}).
|
||||||
|
|
||||||
|
%% pimp state
|
||||||
|
-record(ps,
|
||||||
|
{lsock = none :: gen_tcp:socket(),
|
||||||
|
hos = [] :: [#pho{}],
|
||||||
|
ho_names = init_ho_names() :: [string()]}).
|
||||||
|
|
||||||
|
|
||||||
|
%% ho state
|
||||||
|
-record(hs,
|
||||||
|
{name = none :: string(),
|
||||||
|
daddy = none :: pid(),
|
||||||
|
nsock = none :: enoise:noise_socket()}).
|
||||||
|
|
||||||
|
|
||||||
|
main() ->
|
||||||
|
io:format("RBX example 4 telnet echo server daemon~n"
|
||||||
|
"`one' part of the many-to-one relationship~n",
|
||||||
|
[]),
|
||||||
|
p_init().
|
||||||
|
|
||||||
|
|
||||||
|
noise_options() ->
|
||||||
|
% totally safe crypto
|
||||||
|
Secret = crypto:hash(sha3_256, <<"alice">>),
|
||||||
|
Public = <<116, 159, 91, 248, 138, 250, 73, 40, 231, 50, 81, 110,
|
||||||
|
137, 163, 44, 76, 48, 130, 225, 95, 168, 121, 93, 44,
|
||||||
|
148, 42, 180, 103, 11, 40, 168, 96>>,
|
||||||
|
KP = enoise_keypair:new(dh25519, Secret, Public),
|
||||||
|
[{noise, "Noise_NN_25519_ChaChaPoly_BLAKE2b"},
|
||||||
|
{e, KP}].
|
||||||
|
|
||||||
|
|
||||||
|
% pimp process startup
|
||||||
|
p_init() ->
|
||||||
|
p_logln("pimp startup (p_init)"),
|
||||||
|
Port = 6969,
|
||||||
|
Opts = [binary, {packet, 0}, {active, once}],
|
||||||
|
case gen_tcp:listen(Port, Opts) of
|
||||||
|
{ok, LSock} ->
|
||||||
|
ok = p_logfln("pimp starting: ~tp", [self()]),
|
||||||
|
PS_I = #ps{lsock = LSock},
|
||||||
|
PS_II = p_spawn_ho(PS_I),
|
||||||
|
p_loop(PS_II);
|
||||||
|
{error, closed} ->
|
||||||
|
%% dont be pimp
|
||||||
|
p_logln("listen socket closed")
|
||||||
|
end.
|
||||||
|
|
||||||
|
p_loop(PS = #ps{hos = Hos}) ->
|
||||||
|
receive
|
||||||
|
{ho, _HoPid, taken} ->
|
||||||
|
NewPS = p_spawn_ho(PS),
|
||||||
|
p_loop(NewPS);
|
||||||
|
{ho, HoPid, sent, Data} ->
|
||||||
|
HoName = p_ho_name(HoPid, PS),
|
||||||
|
SendMessage = io_lib:format("~ts :: ~ts\r\n", [HoName, Data]),
|
||||||
|
ok = p_echo_except(HoPid, HoName, SendMessage, Hos),
|
||||||
|
p_loop(PS);
|
||||||
|
Down = {'DOWN', _, _, _, _} ->
|
||||||
|
NewPS = p_ho_down(Down, PS),
|
||||||
|
p_loop(NewPS);
|
||||||
|
Unknown ->
|
||||||
|
ok = p_logfln("unknown message: ~tw", [Unknown]),
|
||||||
|
p_loop(PS)
|
||||||
|
end.
|
||||||
|
|
||||||
|
% given pid of ho, get her name
|
||||||
|
p_ho_name(HoPid, #ps{hos = Hos}) ->
|
||||||
|
case lists:keyfind(HoPid, #pho.pid, Hos) of
|
||||||
|
#pho{name = Name} -> Name;
|
||||||
|
false -> "JaneDoe"
|
||||||
|
end.
|
||||||
|
|
||||||
|
% pimp spawns new ho
|
||||||
|
p_spawn_ho(PS = #ps{lsock = LSock, hos = Hos, ho_names = Names}) ->
|
||||||
|
% new name for ho, initial ho state
|
||||||
|
{N, NewNames} = recycle(Names),
|
||||||
|
InitHS = #hs{name = N, daddy = self()},
|
||||||
|
% spawn the new ho
|
||||||
|
ok = p_logfln("spawning new ho: ~ts", [N]),
|
||||||
|
SpawnHo = fun() -> h_init(LSock, InitHS) end,
|
||||||
|
{HPid, HRef} = erlang:spawn_monitor(SpawnHo),
|
||||||
|
% new ho record from Pimp POV
|
||||||
|
NewHo = #pho{pid = HPid, ref = HRef, name = N},
|
||||||
|
ok = p_logfln("spawned new ho: ~tw", [NewHo]),
|
||||||
|
NewPS = PS#ps{hos = [NewHo | Hos], ho_names = NewNames},
|
||||||
|
NewPS.
|
||||||
|
|
||||||
|
% ho down, remove her from roster
|
||||||
|
p_ho_down({'DOWN', _Ref, process, Pid, Info}, PS = #ps{hos = Hos}) ->
|
||||||
|
ok = p_logfln("ho down: ~tp; reason: ~tw", [Pid, Info]),
|
||||||
|
NewHos = lists:keydelete(Pid, #pho.pid, Hos),
|
||||||
|
NewPS = PS#ps{hos = NewHos},
|
||||||
|
NewPS.
|
||||||
|
|
||||||
|
|
||||||
|
% echo message from one ho to all the rest
|
||||||
|
% skip if equal
|
||||||
|
p_echo_except(SrcPid, SrcName, Data, [#pho{pid = SrcPid} | Rest]) ->
|
||||||
|
p_echo_except(SrcPid, SrcName, Data, Rest);
|
||||||
|
p_echo_except(SrcPid, SrcName, Data, [#pho{pid = DstPid} | Rest]) ->
|
||||||
|
DstPid ! {pimp, send, Data},
|
||||||
|
p_echo_except(SrcPid, SrcName, Data, Rest);
|
||||||
|
p_echo_except(_, _, _, []) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% runs in ho context
|
||||||
|
h_init(LSock, HS = #hs{name = Name, daddy = Daddy, nsock = none}) ->
|
||||||
|
h_logln(Name, "h_init"),
|
||||||
|
% this is key: accept needs to run in ho context, otherwise she
|
||||||
|
% doesn't own the acceptor socket
|
||||||
|
%
|
||||||
|
% blocks until client connects
|
||||||
|
case enoise:accept(LSock, noise_options()) of
|
||||||
|
{ok, NSock, _} ->
|
||||||
|
h_logln(Name, "taken"),
|
||||||
|
% tell daddy to spawn the next ho
|
||||||
|
Daddy ! {ho, self(), taken},
|
||||||
|
Welcome = io_lib:format("HI! I'm ~ts. What's your name?\r\n",
|
||||||
|
[Name]),
|
||||||
|
ok = h_send(Name, NSock, Welcome),
|
||||||
|
HS_II = HS#hs{nsock = ASock},
|
||||||
|
h_loop(HS_II);
|
||||||
|
{error, closed} ->
|
||||||
|
h_logln(Name, "didn't get taken")
|
||||||
|
end.
|
||||||
|
|
||||||
|
|
||||||
|
h_loop(HS = #hs{name = Name, daddy = Daddy, nsock = ASock}) ->
|
||||||
|
ok = inet:setopts(ASock, [{active, once}]),
|
||||||
|
receive
|
||||||
|
{tcp, ASock, Data} ->
|
||||||
|
Data_II = string:chomp(Data),
|
||||||
|
ok = h_rcvd(Name, Data_II),
|
||||||
|
Daddy ! {ho, self(), sent, Data_II},
|
||||||
|
h_loop(HS);
|
||||||
|
{pimp, send, Data} ->
|
||||||
|
ok = h_send(Name, ASock, Data),
|
||||||
|
h_loop(HS);
|
||||||
|
{tcp_closed, ASock} ->
|
||||||
|
exit(its_fine);
|
||||||
|
Unknown ->
|
||||||
|
ok = h_logfln(Name, "unknown message: ~tw", [Unknown]),
|
||||||
|
h_loop(HS)
|
||||||
|
end.
|
||||||
|
|
||||||
|
|
||||||
|
% pimp log in bold
|
||||||
|
p_logln(LogStr) ->
|
||||||
|
Msg = io_lib:format("!! Daddy :: ~ts~n", [LogStr]),
|
||||||
|
PimpChars = ?ANSI_BOLD([Msg]),
|
||||||
|
io:put_chars(PimpChars).
|
||||||
|
|
||||||
|
p_logfln(LogStr, Args) ->
|
||||||
|
p_logln(io_lib:format(LogStr, Args)).
|
||||||
|
|
||||||
|
|
||||||
|
% ho log dimmed
|
||||||
|
h_logln(HoName, LogStr) ->
|
||||||
|
FMsg = io_lib:format("!! ~ts :: ~ts~n", [HoName, LogStr]),
|
||||||
|
HoChars = ?ANSI_DIM([FMsg]),
|
||||||
|
io:put_chars(HoChars).
|
||||||
|
|
||||||
|
h_logfln(HoName, MsgStr, Args) ->
|
||||||
|
h_logln(HoName, io_lib:format(MsgStr, Args)).
|
||||||
|
|
||||||
|
% normal: john messages
|
||||||
|
h_rcvd(Name, MsgStr) ->
|
||||||
|
Msg = io_lib:format("<< ~ts :: ~ts~n", [Name, MsgStr]),
|
||||||
|
io:put_chars(Msg).
|
||||||
|
|
||||||
|
h_send(Name, Socket, Msg) ->
|
||||||
|
ok = io:format(">> ~ts :: ~ts~n", [Name, Msg]),
|
||||||
|
enoise:send(Socket, Msg).
|
||||||
|
|
||||||
|
|
||||||
|
init_ho_names() ->
|
||||||
|
["Crystal", "Tiffany", "Amber", "Brandy", "Lola", "Angel",
|
||||||
|
"Ginger", "Candy", "Charity", "Anastasia", "Cherry", "Kitty",
|
||||||
|
"Jade", "Destiny", "Devon", "Chastity", "Raven", "Scarlett",
|
||||||
|
"Bambi", "Star", "Paris", "Dallas", "Diamond", "Skye",
|
||||||
|
"Trinity", "Tawny", "Layla", "Lexie", "Roxy", "Porsche",
|
||||||
|
"Nevaeh", "Ashlynn", "Aspen", "Chyna", "Lexus", "Unique",
|
||||||
|
"Chardonnay", "Houston", "London", "Coco", "Luscious",
|
||||||
|
"Delight", "Capri", "Trixie", "Cinnamon"].
|
||||||
|
|
||||||
|
|
||||||
|
recycle([N | Ns]) ->
|
||||||
|
{N, Ns ++ [N]}.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
liquidate_hos([{HoPid, _HoRef} | Hos]) ->
|
||||||
|
p_logfln("liquidating ~tp", [HoPid]),
|
||||||
|
exit(HoPid, kill),
|
||||||
|
liquidate_hos(Hos);
|
||||||
|
liquidate_hos([]) ->
|
||||||
|
p_logln("it's done").
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user