All crypto and top level in place
Limited support for protocols, virtually no error handling
This commit is contained in:
parent
d0723eb247
commit
4d2af24250
@ -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
121
src/enoise.erl
Normal 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
151
src/enoise_hs_state.erl
Normal 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
10
src/enoise_opts.erl
Normal 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}].
|
@ -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}).
|
||||||
|
|
||||||
|
@ -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 =
|
||||||
|
Loading…
x
Reference in New Issue
Block a user