diff --git a/x04-noisy-echo/ansi.hrl b/x04-noisy-echo/ansi.hrl new file mode 100644 index 0000000..bee7090 --- /dev/null +++ b/x04-noisy-echo/ansi.hrl @@ -0,0 +1,210 @@ +% ANSI screen drawing macros in erlang +% +% Author: Peter Harpending +% 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_ diff --git a/x04-noisy-echo/enoise.erl b/x04-noisy-echo/enoise.erl new file mode 100644 index 0000000..d35fd90 --- /dev/null +++ b/x04-noisy-echo/enoise.erl @@ -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, <>) 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 <> of + Buf1 = <> when byte_size(Rest) < Len -> + gen_tcp_rcv_msg({TcpSock, true, Buf1}, Timeout); + <> -> + <> = Rest, + {ok, Data1, {TcpSock, true, Buf1}} + end + after Timeout -> + {error, timeout} + end. + +remote_keypair(_DH, undefined) -> undefined; +remote_keypair(DH, RemotePub) when is_binary(RemotePub) -> enoise_keypair:new(DH, RemotePub). diff --git a/x04-noisy-echo/enoise.hrl b/x04-noisy-echo/enoise.hrl new file mode 100644 index 0000000..6d12af9 --- /dev/null +++ b/x04-noisy-echo/enoise.hrl @@ -0,0 +1,3 @@ +-define(MAX_NONCE, 16#FFFFFFFFFFFFFFFF). +-define(MAX_AD_LEN, 16). + diff --git a/x04-noisy-echo/enoise_cipher_state.erl b/x04-noisy-echo/enoise_cipher_state.erl new file mode 100644 index 0000000..50eb853 --- /dev/null +++ b/x04-noisy-echo/enoise_cipher_state.erl @@ -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. diff --git a/x04-noisy-echo/enoise_connection.erl b/x04-noisy-echo/enoise_connection.erl new file mode 100644 index 0000000..53c12dd --- /dev/null +++ b/x04-noisy-echo/enoise_connection.erl @@ -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 <> of + B = <> when Len > byte_size(Rest) -> + {S#state{ rawbuf = B }, []}; %% Not a full Noise message - save it + <> -> + <> = 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). diff --git a/x04-noisy-echo/enoise_crypto.erl b/x04-noisy-echo/enoise_crypto.erl new file mode 100644 index 0000000..eb71ba5 --- /dev/null +++ b/x04-noisy-echo/enoise_crypto.erl @@ -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, <>), + Block2 = hmac_format_key(Hash, Key, 16#5C, BLen), + hash(Hash, <>). + +-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, <>), + Output3 = hmac(Hash, TempKey, <>), + [Output1, Output2, Output3]. + +-spec rekey(Cipher :: enoise_cipher_state:noise_cipher(), Key :: binary()) -> binary(). +rekey('ChaChaPoly', K0) -> + KLen = 32, + <> = 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), + <>. + +-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, + <> = 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 = << <> || _ <- lists:seq(1, MinSize - N) >>, + <> + 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), + <> = <>, + << <<(Word bxor PadWord):32>> || <> <= Key2 >>. + diff --git a/x04-noisy-echo/enoise_hs_state.erl b/x04-noisy-echo/enoise_hs_state.erl new file mode 100644 index 0000000..abdfb43 --- /dev/null +++ b/x04-noisy-echo/enoise_hs_state.erl @@ -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 = <>, + {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, <>). + +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 + <> -> + 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 + <> -> + 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. + diff --git a/x04-noisy-echo/enoise_keypair.erl b/x04-noisy-echo/enoise_keypair.erl new file mode 100644 index 0000000..780fa52 --- /dev/null +++ b/x04-noisy-echo/enoise_keypair.erl @@ -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. diff --git a/x04-noisy-echo/enoise_protocol.erl b/x04-noisy-echo/enoise_protocol.erl new file mode 100644 index 0000000..7bc0b0d --- /dev/null +++ b/x04-noisy-echo/enoise_protocol.erl @@ -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)). diff --git a/x04-noisy-echo/enoise_sym_state.erl b/x04-noisy-echo/enoise_sym_state.erl new file mode 100644 index 0000000..07a4dd7 --- /dev/null +++ b/x04-noisy-echo/enoise_sym_state.erl @@ -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, <> | _] = + 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, <>), + 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, <>] = + 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 }) -> + [<>, <>, _] = + 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. diff --git a/x04-noisy-echo/nechoc.erl b/x04-noisy-echo/nechoc.erl new file mode 100644 index 0000000..5c99506 --- /dev/null +++ b/x04-noisy-echo/nechoc.erl @@ -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. + + diff --git a/x04-noisy-echo/nechod.erl b/x04-noisy-echo/nechod.erl new file mode 100644 index 0000000..dcc02ca --- /dev/null +++ b/x04-noisy-echo/nechod.erl @@ -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: +%% +%%
    +%%
  • Takes listen socket as arg
  • +%%
  • spawns next ho
  • +%%
+%% +%% ``` +%% 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]}. diff --git a/x04-noisy-echo/scratch.erl b/x04-noisy-echo/scratch.erl new file mode 100644 index 0000000..c629f18 --- /dev/null +++ b/x04-noisy-echo/scratch.erl @@ -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"). + +