All crypto and top level in place

Limited support for protocols, virtually no error handling
This commit is contained in:
Hans Svensson 2018-03-02 14:24:59 +01:00
parent d0723eb247
commit 4d2af24250
6 changed files with 327 additions and 20 deletions

View File

@ -1,20 +1,5 @@
-define(MAX_NONCE, 16#FFFFFFFFFFFFFFFF). -define(MAX_NONCE, 16#FFFFFFFFFFFFFFFF).
-define(AD_LEN, 16). -define(AD_LEN, 16).
-record(noise_protocol,
{ hs_pattern = noiseNN %:: noise_hs_pattern()
, dh = dh25519 %:: noise_dh()
, cipher = 'ChaChaPoly' %:: noise_cipher()
, hash = blake2b %:: noise_hash()
}).
-record(key_pair, { puk, pik }). -record(key_pair, { puk, pik }).
-record(noise_hs, { ss :: enoise_sym_state: state()
, s :: #key_pair{} | undefined
, e :: #key_pair{} | undefined
, rs :: binary() | undefined
, re :: binary() | undefined
, role = initiatior :: initiator | responder
, dh
, msgs = [] }).

121
src/enoise.erl Normal file
View File

@ -0,0 +1,121 @@
%%%-------------------------------------------------------------------
%%% @copyright (C) 2018, Aeternity Anstalt
%%%-------------------------------------------------------------------
-module(enoise).
%% API exports - Mainly mimicing gen_tcp
%% -export([ accept/1
%% , accept/2
%% , close/1
%% , connect/3
%% , connect/4
%% , controlling_process/2
%% , listen/2
%% , recv/2
%% , recv/3
%% , send/2
%% , shutdown/2 ]).
-compile([export_all, nowarn_export_all]).
-include("enoise.hrl").
-record(enoise, { tcp_sock, rx, tx }).
%% -type noise_hs_pattern() :: noiseNN | noiseKN.
%% -type noise_dh() :: dh448 | dh25519.
%% -type noise_cipher() :: 'AESGCM' | 'ChaChaPoly'.
%% -type noise_hash() :: sha256 | sha512 | blake2s | blake2b.
%% -type noise_protocol() :: #noise_protocol{}.
%%====================================================================
%% API functions
%%====================================================================
connect(Address, Port, Options) ->
connect(Address, Port, Options, infinity).
connect(Address, Port, Options, Timeout) ->
case initiate_handshake(initiator, Options) of
{ok, HS} ->
TcpOpts = enoise_opts:tcp_opts(Options),
case gen_tcp:connect(Address, Port, TcpOpts, Timeout) of
{ok, TcpSock} ->
do_handshake(TcpSock, HS, Options);
Err = {error, _Reason} ->
Err
end;
Err = {error, _Reason} ->
Err
end.
send(E = #enoise{ tcp_sock = TcpSock, rx = RX0 }, Msg0) ->
{ok, RX1, Msg1} = enoise_cipher_state:encrypt_with_ad(RX0, <<>>, Msg0),
gen_tcp:send(TcpSock, <<(byte_size(Msg1)):16, Msg1/binary>>),
E#enoise{ rx = RX1 }.
recv(E = #enoise{ tcp_sock = TcpSock, tx = TX0 }) ->
receive {tcp, TcpSock, <<Size:16, Data/binary>>} ->
Size = byte_size(Data),
{ok, TX1, Msg1} = enoise_cipher_state:decrypt_with_ad(TX0, <<>>, Data),
{E#enoise{ tx = TX1 }, Msg1}
after 1000 -> error(timeout) end.
close(#enoise{ tcp_sock = TcpSock }) ->
gen_tcp:close(TcpSock).
%%====================================================================
%% Internal functions
%%====================================================================
initiate_handshake(Role, Options) ->
Prologue = proplists:get_value(prologue, Options, <<>>),
NoiseProtocol = proplists:get_value(noise, Options),
S = proplists:get_value(s, Options, undefined),
E = proplists:get_value(e, Options, undefined),
RS = proplists:get_value(rs, Options, undefined),
RE = proplists:get_value(re, Options, undefined),
HSState = enoise_hs_state:init(NoiseProtocol, Role, Prologue, {S, E, RS, RE}),
{ok, HSState}.
do_handshake(TcpSock, HState, Options) ->
PreComm = proplists:get_value(pre_comm, Options, <<>>), %% TODO: Not standard!
gen_tcp:send(TcpSock, PreComm),
do_handshake(TcpSock, HState).
do_handshake(TcpSock, HState) ->
case enoise_hs_state:next_message(HState) of
in ->
receive {tcp, TcpSock, Data} ->
case enoise_hs_state:read_message(HState, Data) of
{ok, HState1, _Msg} ->
do_handshake(TcpSock, HState1);
{done, _HState1, _Msg, {C1, C2}} ->
{ok, #enoise{ tcp_sock = TcpSock, rx = C1, tx = C2 }}
end
after 1000 ->
error(timeout)
end;
out ->
case enoise_hs_state:write_message(HState, <<>>) of
{ok, HState1, Msg} ->
io:format("Sending: ~p\n", [add_len(Msg)]),
gen_tcp:send(TcpSock, add_len(Msg)),
do_handshake(TcpSock, HState1);
{done, _HState1, Msg, {C1, C2}} ->
io:format("Sending: ~p\n", [add_len(Msg)]),
gen_tcp:send(TcpSock, add_len(Msg)),
{ok, #enoise{ tcp_sock = TcpSock, rx = C1, tx = C2 }}
end
end.
add_len(Msg) ->
Len = byte_size(Msg),
<<Len:16, Msg/binary>>.

151
src/enoise_hs_state.erl Normal file
View File

@ -0,0 +1,151 @@
%%%-------------------------------------------------------------------
%%% @copyright (C) 2018, Aeternity Anstalt
%%%-------------------------------------------------------------------
-module(enoise_hs_state).
-export([init/4, next_message/1, read_message/2, 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 noise_msg() :: {in | out, [noise_token()]}.
-record(noise_hs, { ss :: enoise_sym_state:state()
, s :: #key_pair{} | undefined
, e :: #key_pair{} | undefined
, rs :: binary() | undefined
, re :: binary() | undefined
, role = initiatior :: noise_role()
, dh = dh25519 :: noise_dh()
, msgs = [] :: [noise_msg()] }).
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 = msgs(Role, enoise_protocol:pattern(Protocol)) },
PreMsgs = pre_msgs(Role, enoise_protocol:pattern(Protocol)),
lists:foldl(fun({out, [s]}, HS0) -> mix_hash(HS0, S#key_pair.puk);
({out, [e]}, HS0) -> mix_hash(HS0, E#key_pair.puk);
({in, [s]}, HS0) -> mix_hash(HS0, RS);
({in, [e]}, HS0) -> mix_hash(HS0, RE)
end, HS, PreMsgs).
next_message(#noise_hs{ msgs = [{Dir, _} | _] }) -> Dir;
next_message(_) -> done.
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>>,
case Msgs of
[] -> {done, HS2, MsgBuf, enoise_sym_state:split(HS2#noise_hs.ss)};
_ -> {ok, HS2, MsgBuf}
end.
read_message(HS = #noise_hs{ msgs = [{in, Msg} | Msgs] }, <<Size:16, Message/binary>>) ->
Size = byte_size(Message),
{HS1, RestBuf1} = read_message(HS#noise_hs{ msgs = Msgs }, Msg, Message),
{ok, HS2, PlainBuf} = decrypt_and_hash(HS1, RestBuf1),
case Msgs of
[] -> {done, HS2, PlainBuf, enoise_sym_state:split(HS2#noise_hs.ss)};
_ -> {ok, HS2, PlainBuf}
end.
write_message(HS, [], MsgBuf) ->
{HS, MsgBuf};
write_message(HS, [Token | Tokens], MsgBuf0) ->
{HS1, MsgBuf1} = write_token(HS, Token),
write_message(HS1, Tokens, <<MsgBuf0/binary, MsgBuf1/binary>>).
read_message(HS, [], Data) ->
{HS, Data};
read_message(HS, [Token | Tokens], Data0) ->
{HS1, Data1} = read_token(HS, Token, Data0),
read_message(HS1, Tokens, Data1).
write_token(HS = #noise_hs{ e = undefined }, e) ->
E = #key_pair{ puk = PubE } = new_key_pair(HS),
{mix_hash(HS#noise_hs{ e = E }, PubE), PubE};
write_token(HS = #noise_hs{ s = S }, s) ->
{ok, HS1, Msg} = encrypt_and_hash(HS, S#key_pair.puk),
{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 }, e, Data0) ->
DHLen = dhlen(HS),
<<RE:DHLen/binary, Data1/binary>> = Data0,
{mix_hash(HS#noise_hs{ re = RE }, RE), Data1};
read_token(HS = #noise_hs{ rs = undefined }, s, Data0) ->
DHLen = case has_key(HS) of
true -> dhlen(HS) + 16;
false -> dhlen(HS)
end,
<<Temp:DHLen/binary, Data1/binary>> = Data0,
{ok, HS1, RS} = decrypt_and_hash(HS, Temp),
{HS1#noise_hs{ rs = RS }, Data1};
read_token(HS, Token, Data) ->
{K1, K2} = dh_token(HS, Token),
{mix_key(HS, dh(HS, K1, K2)), Data}.
dh_token(#noise_hs{ e = E, re = RE } , ee) -> {E, RE};
dh_token(#noise_hs{ e = E, rs = RS, role = initiator }, es) -> {E, RS};
dh_token(#noise_hs{ s = S, re = RE, role = responder }, es) -> {S, RE};
dh_token(#noise_hs{ s = S, re = RE, role = initiator }, se) -> {S, RE};
dh_token(#noise_hs{ e = E, rs = RS, role = responder }, se) -> {E, RS};
dh_token(#noise_hs{ s = S, rs = RS } , ss) -> {S, RS}.
%% Local wrappers
new_key_pair(#noise_hs{ dh = DH }) ->
enoise_crypto:new_key_pair(DH).
dh(#noise_hs{ dh = DH }, KeyPair, PubKey) ->
enoise_crypto:dh(DH, KeyPair, PubKey).
dhlen(#noise_hs{ dh = DH }) ->
enoise_crypto:dhlen(DH).
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) ->
{ok, SS1, PlainText} = enoise_sym_state:decrypt_and_hash(SS0, CipherText),
{ok, HS#noise_hs{ ss = SS1 }, PlainText}.
msgs(Role, Protocol) ->
{_Pre, Msgs} = protocol(Protocol),
role_adapt(Role, Msgs).
pre_msgs(Role, Protocol) ->
{PreMsgs, _Msgs} = protocol(Protocol),
role_adapt(Role, PreMsgs).
role_adapt(initiator, Msgs) ->
Msgs;
role_adapt(responder, Msgs) ->
Flip = fun(in) -> out; (out) -> in end,
lists:map(Flip, Msgs).
protocol(nn) ->
{[], [{out, [e]}, {in, [e, ee]}]};
protocol(xk) ->
{[{in, [s]}], [{out, [e, es]}, {in, [e, ee]}, {out, [s, se]}]}.

10
src/enoise_opts.erl Normal file
View File

@ -0,0 +1,10 @@
%%%-------------------------------------------------------------------
%%% @copyright (C) 2018, Aeternity Anstalt
%%%-------------------------------------------------------------------
-module(enoise_opts).
-export([tcp_opts/1]).
tcp_opts(_Options) ->
[{active, true}, binary, {reuseaddr, true}].

View File

@ -4,9 +4,47 @@
-module(enoise_protocol). -module(enoise_protocol).
-include("enoise.hrl"). -export([ cipher/1
, dh/1
, from_name/1
, hash/1
, pattern/1
, to_name/1]).
-export([to_name/1]). -type noise_pattern() :: nn | xk.
-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_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.
to_name(_Protocol) -> to_name(_Protocol) ->
<<"Noise_XK_25519_ChaChaPoly_BLAKE2b">>. <<"Noise_XK_25519_ChaChaPoly_BLAKE2b">>.
from_name("Noise_XK_25519_ChaChaPoly_Blake2b") ->
#noise_protocol{ hs_pattern = xk, dh = dh25519, cipher = 'ChaChaPoly', hash = blake2b };
from_name(Name) ->
error({protocol_not_implemented_yet, Name}).

View File

@ -29,8 +29,10 @@
-opaque state() :: #noise_ss{}. -opaque state() :: #noise_ss{}.
-export_type([noise_hash/0, state/0]). -export_type([noise_hash/0, state/0]).
-spec init(Protocol :: #noise_protocol{}) -> state(). -spec init(Protocol :: enoise_protocol:protocol()) -> state().
init(Protocol = #noise_protocol{ hash = Hash, cipher = Cipher }) -> init(Protocol) ->
Hash = enoise_protocol:hash(Protocol),
Cipher = enoise_protocol:cipher(Protocol),
Name = enoise_protocol:to_name(Protocol), Name = enoise_protocol:to_name(Protocol),
HashLen = enoise_crypto:hashlen(Hash), HashLen = enoise_crypto:hashlen(Hash),
H1 = H1 =