From 4d2af24250321103c5168c7afed8d828fa32130a Mon Sep 17 00:00:00 2001 From: Hans Svensson Date: Fri, 2 Mar 2018 14:24:59 +0100 Subject: [PATCH] All crypto and top level in place Limited support for protocols, virtually no error handling --- include/enoise.hrl | 15 ---- src/enoise.erl | 121 +++++++++++++++++++++++++++++++ src/enoise_hs_state.erl | 151 +++++++++++++++++++++++++++++++++++++++ src/enoise_opts.erl | 10 +++ src/enoise_protocol.erl | 42 ++++++++++- src/enoise_sym_state.erl | 8 ++- 6 files changed, 327 insertions(+), 20 deletions(-) create mode 100644 src/enoise.erl create mode 100644 src/enoise_hs_state.erl create mode 100644 src/enoise_opts.erl diff --git a/include/enoise.hrl b/include/enoise.hrl index 2c40c46..0375756 100644 --- a/include/enoise.hrl +++ b/include/enoise.hrl @@ -1,20 +1,5 @@ -define(MAX_NONCE, 16#FFFFFFFFFFFFFFFF). -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(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 = [] }). diff --git a/src/enoise.erl b/src/enoise.erl new file mode 100644 index 0000000..52ed772 --- /dev/null +++ b/src/enoise.erl @@ -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 = 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), + <>. diff --git a/src/enoise_hs_state.erl b/src/enoise_hs_state.erl new file mode 100644 index 0000000..f9a96a1 --- /dev/null +++ b/src/enoise_hs_state.erl @@ -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 = <>, + 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 = 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, <>). + +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), + <> = 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, + <> = 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]}]}. diff --git a/src/enoise_opts.erl b/src/enoise_opts.erl new file mode 100644 index 0000000..7ac290d --- /dev/null +++ b/src/enoise_opts.erl @@ -0,0 +1,10 @@ +%%%------------------------------------------------------------------- +%%% @copyright (C) 2018, Aeternity Anstalt +%%%------------------------------------------------------------------- + +-module(enoise_opts). + +-export([tcp_opts/1]). + +tcp_opts(_Options) -> + [{active, true}, binary, {reuseaddr, true}]. diff --git a/src/enoise_protocol.erl b/src/enoise_protocol.erl index 9a6c305..91c709b 100644 --- a/src/enoise_protocol.erl +++ b/src/enoise_protocol.erl @@ -4,9 +4,47 @@ -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) -> <<"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}). + diff --git a/src/enoise_sym_state.erl b/src/enoise_sym_state.erl index b3d0a31..380c782 100644 --- a/src/enoise_sym_state.erl +++ b/src/enoise_sym_state.erl @@ -29,9 +29,11 @@ -opaque state() :: #noise_ss{}. -export_type([noise_hash/0, state/0]). --spec init(Protocol :: #noise_protocol{}) -> state(). -init(Protocol = #noise_protocol{ hash = Hash, cipher = Cipher }) -> - Name = enoise_protocol:to_name(Protocol), +-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