diff --git a/CHANGELOG.md b/CHANGELOG.md index 097e5c9..515d48a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Support for 448 DH function and Blake2s hash function. ### Changed +- Using `crypto` over `enacl` (and removing a call to `get_stacktrace/1`) makes `enoise` + up to date for (at least) OTP-27. +- Added test dependency `eqwalizer_support` to enable checking types with Eqwalizer. ### Removed +- The dependency on `enacl` is not needed anymore, OTP's `crypto` library now cover all + necessary operations. ## [1.2.0] - 2021-10-28 ### Added diff --git a/README.md b/README.md index ca2781b..7e25295 100644 --- a/README.md +++ b/README.md @@ -39,3 +39,9 @@ Test ---- $ rebar3 eunit + +Typecheck +--------- + + $ rebar3 dialyzer + $ elp --eqwalize-all --rebar diff --git a/rebar.config b/rebar.config index 4407b81..fc28bcf 100644 --- a/rebar.config +++ b/rebar.config @@ -1,8 +1,10 @@ {erl_opts, [debug_info]}. {plugins, [rebar3_hex]}. -{deps, [{enacl, "1.1.1"}]}. -{profiles, [{test, [{deps, [{jsx, {git, "https://github.com/talentdeficit/jsx.git", {tag, "2.8.0"}}}]}]} +{profiles, [{test, [{deps, [ {jsx, {git, "https://github.com/talentdeficit/jsx.git", {tag, "2.8.0"}}} + , {eqwalizer_support, {git_subdir, "https://github.com/whatsapp/eqwalizer.git", {branch, "main"}, "eqwalizer_support"}} + ]} + ]} ]}. {xref_checks, [undefined_function_calls, undefined_functions, diff --git a/rebar.lock b/rebar.lock index 22d6918..57afcca 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,6 +1 @@ -{"1.2.0", -[{<<"enacl">>,{pkg,<<"enacl">>,<<"1.1.1">>},0}]}. -[ -{pkg_hash,[ - {<<"enacl">>, <<"F65DC64D9BFF2D8A534CB77AEF14DA5E7A2FA148987D87856F79A4745C9C2627">>}]} -]. +[]. diff --git a/src/enoise.app.src b/src/enoise.app.src index e89b9df..71008c1 100644 --- a/src/enoise.app.src +++ b/src/enoise.app.src @@ -5,7 +5,7 @@ {applications, [kernel, stdlib, - enacl + crypto ]}, {env,[]}, {modules, []}, diff --git a/src/enoise.erl b/src/enoise.erl index 3fa55b9..d35fd90 100644 --- a/src/enoise.erl +++ b/src/enoise.erl @@ -87,8 +87,7 @@ binary(). Role :: enoise_hs_state:noise_role()) -> {ok, enoise_hs_state:state()} | {error, term()}. handshake(Options, Role) -> - HState = create_hstate(Options, Role), - {ok, HState}. + create_hstate(Options, Role). %% @doc Do a step (either `{send, Payload}', `{rcvd, EncryptedData}', %% or `done') @@ -109,10 +108,13 @@ step_handshake(HState, Data) -> ComState :: noise_com_state()) -> {ok, noise_split_state(), noise_com_state()} | {error, term()}. handshake(Options, Role, ComState) -> - HState = create_hstate(Options, Role), - Timeout = proplists:get_value(timeout, Options, infinity), - do_handshake(HState, ComState, Timeout). - + 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. @@ -270,15 +272,16 @@ create_hstate(Options, Role) -> 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 = proplists:get_value(rs, Options, undefined), - RE = proplists:get_value(re, 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} -> @@ -321,3 +324,5 @@ gen_tcp_rcv_msg({TcpSock, Active, Buf}, 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/src/enoise_cipher_state.erl b/src/enoise_cipher_state.erl index 7a5d784..50eb853 100644 --- a/src/enoise_cipher_state.erl +++ b/src/enoise_cipher_state.erl @@ -54,12 +54,8 @@ set_nonce(CState = #noise_cs{}, Nonce) -> 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) -> - case enoise_crypto:encrypt(Cipher, K, N, AD, PlainText) of - Encrypted when is_binary(Encrypted) -> - {ok, CState#noise_cs{ n = N+1 }, Encrypted}; - Err = {error, _} -> - Err - end. + 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()}. @@ -74,6 +70,8 @@ decrypt_with_ad(CState = #noise_cs{ k = K, n = N, cipher = Cipher }, AD, CipherT 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) }. diff --git a/src/enoise_crypto.erl b/src/enoise_crypto.erl index bdb81c9..eb71ba5 100644 --- a/src/enoise_crypto.erl +++ b/src/enoise_crypto.erl @@ -29,13 +29,18 @@ %% @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(dh25519, Key1, Key2) -> - enacl:curve25519_scalarmult( enoise_keypair:seckey(Key1) - , enoise_keypair:pubkey(Key2)); + 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) -> @@ -54,47 +59,42 @@ hkdf(Hash, Key, Data) -> Output3 = hmac(Hash, TempKey, <>), [Output1, Output2, Output3]. --spec rekey(Cipher :: enoise_cipher_state:noise_cipher(), - Key :: binary()) -> binary() | {error, term()}. +-spec rekey(Cipher :: enoise_cipher_state:noise_cipher(), Key :: binary()) -> binary(). rekey('ChaChaPoly', K0) -> - KLen = enacl:aead_chacha20poly1305_ietf_KEYBYTES(), + 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() | {error, term()}. -encrypt('ChaChaPoly', K, N, Ad, PlainText) -> - Nonce = <<0:32, N:64/little-unsigned-integer>>, - enacl:aead_chacha20poly1305_ietf_encrypt(PlainText, Ad, Nonce, K); -encrypt('AESGCM', K, N, Ad, PlainText) -> - Nonce = <<0:32, N:64>>, - {CipherText, CipherTag} = crypto:crypto_one_time_aead(aes_256_gcm, K, Nonce, PlainText, Ad, true), - <>. +-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('ChaChaPoly', K, N, Ad, CipherText) -> - Nonce = <<0:32, N:64/little-unsigned-integer>>, - enacl:aead_chacha20poly1305_ietf_decrypt(CipherText, Ad, Nonce, K); -decrypt('AESGCM', K, N, Ad, CipherText0) -> +-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, - Nonce = <<0:32, N:64>>, - case crypto:crypto_one_time_aead(aes_256_gcm, K, Nonce, CipherText, Ad, MAC, false) of + <> = 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) -> - Hash = enacl:generichash(64, Data), Hash; + crypto:hash(blake2b, Data); hash(sha256, Data) -> crypto:hash(sha256, Data); hash(sha512, Data) -> diff --git a/src/enoise_hs_state.erl b/src/enoise_hs_state.erl index 301a936..abdfb43 100644 --- a/src/enoise_hs_state.erl +++ b/src/enoise_hs_state.erl @@ -26,6 +26,8 @@ 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 @@ -39,11 +41,8 @@ -opaque state() :: #noise_hs{}. -export_type([noise_dh/0, noise_role/0, noise_split_state/0, noise_token/0, state/0]). --spec init(Protocol :: string() | enoise_protocol:protocol(), - Role :: noise_role(), Prologue :: binary(), - Keys :: term()) -> state(). -init(ProtocolName, Role, Prologue, Keys) when is_list(ProtocolName) -> - init(enoise_protocol:from_name(ProtocolName), Role, Prologue, Keys); +-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), @@ -53,11 +52,19 @@ init(Protocol, Role, Prologue, {S, E, RS, RE}) -> , dh = enoise_protocol:dh(Protocol) , msgs = enoise_protocol:msgs(Role, Protocol) }, PreMsgs = enoise_protocol:pre_msgs(Role, Protocol), - lists:foldl(fun({out, [s]}, HS0) -> mix_hash(HS0, enoise_keypair:pubkey(S)); - ({out, [e]}, HS0) -> mix_hash(HS0, enoise_keypair:pubkey(E)); - ({in, [s]}, HS0) -> mix_hash(HS0, enoise_keypair:pubkey(RS)); - ({in, [e]}, HS0) -> mix_hash(HS0, enoise_keypair:pubkey(RE)) - end, HS, PreMsgs). + 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 }) -> @@ -90,7 +97,7 @@ read_message(HS = #noise_hs{ msgs = [{in, Msg} | Msgs] }, Message) -> Err = {error, _} -> Err end. --spec remote_keys(HS :: state()) -> keypair(). +-spec remote_keys(HS :: state()) -> undefined | keypair(). remote_keys(#noise_hs{ rs = RS }) -> RS. diff --git a/src/enoise_keypair.erl b/src/enoise_keypair.erl index 313e8a2..780fa52 100644 --- a/src/enoise_keypair.erl +++ b/src/enoise_keypair.erl @@ -30,7 +30,7 @@ %% @doc Generate a new keypair of type `Type'. -spec new(Type :: key_type()) -> keypair(). new(Type) -> - {Sec, Pub} = new_key_pair(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' @@ -69,12 +69,14 @@ seckey(#kp{ sec = S }) -> S. %% -- Local functions -------------------------------------------------------- -new_key_pair(dh25519) -> - KeyPair = enacl:crypto_sign_ed25519_keypair(), - {enacl:crypto_sign_ed25519_secret_to_curve25519(maps:get(secret, KeyPair)), - enacl:crypto_sign_ed25519_public_to_curve25519(maps:get(public, KeyPair))}; +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(dh25519, Secret) -> - enacl:curve25519_scalarmult_base(Secret). +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/src/enoise_protocol.erl b/src/enoise_protocol.erl index 21369dc..7bc0b0d 100644 --- a/src/enoise_protocol.erl +++ b/src/enoise_protocol.erl @@ -19,7 +19,7 @@ , to_name/1]). -ifdef(TEST). --export([to_name/4]). +-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. @@ -137,9 +137,9 @@ supported_dh(Dh) -> -spec supported() -> map(). supported() -> #{ hs_pattern => [nn, kn, nk, kk, nx, kx, xn, in, xk, ik, xx, ix] - , hash => [blake2b, sha256, sha512] + , hash => [blake2s, blake2b, sha256, sha512] , cipher => ['ChaChaPoly', 'AESGCM'] - , dh => [dh25519] + , dh => [dh25519, dh448] }. to_name(Pattern, Dh, Cipher, Hash) -> @@ -148,16 +148,16 @@ to_name(Pattern, Dh, Cipher, Hash) -> to_name_pattern(Atom) -> [Simple | Rest] = string:lexemes(atom_to_list(Atom), "_"), - string:uppercase(Simple) ++ lists:join("+", Rest). + 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(string:lowercase(Simple) ++ + list_to_atom(lists:flatten(string:lowercase(Simple) ++ case Mod1 of "" -> ""; - _ -> "_" ++ lists:join([Mod1 | Mod2], "_") - end). + _ -> "_" ++ lists:join("_", [Mod1 | Mod2]) + end)). to_name_dh(dh25519) -> "25519"; to_name_dh(dh448) -> "448". diff --git a/test/enoise_hs_state_tests.erl b/test/enoise_hs_state_tests.erl index 7677de4..cd1fc7c 100644 --- a/test/enoise_hs_state_tests.erl +++ b/test/enoise_hs_state_tests.erl @@ -43,7 +43,8 @@ noise_test(_Name, Protocol, Init, Resp, Messages, HSHash) -> SecK = fun(undefined) -> undefined; (Sec) -> enoise_keypair:new(DH, Sec, undefined) end, PubK = fun(undefined) -> undefined; (Pub) -> enoise_keypair:new(DH, Pub) end, HSInit = fun(P, R, #{ e := E, s := S, rs := RS, prologue := PL }) -> - enoise_hs_state:init(P, R, PL, {SecK(S), SecK(E), PubK(RS), undefined}) + {ok, HS} = enoise_hs_state:init(P, R, PL, {SecK(S), SecK(E), PubK(RS), undefined}), + HS end, InitHS = HSInit(Protocol, initiator, Init), diff --git a/test/enoise_protocol_tests.erl b/test/enoise_protocol_tests.erl index c43813c..3b93465 100644 --- a/test/enoise_protocol_tests.erl +++ b/test/enoise_protocol_tests.erl @@ -7,5 +7,18 @@ -include_lib("eunit/include/eunit.hrl"). name_test() -> - ?assertMatch(<<"Noise_XK_25519_ChaChaPoly_SHA512">>, - enoise_protocol:to_name(enoise_protocol:from_name("Noise_XK_25519_ChaChaPoly_SHA512"))). + roundtrip("Noise_XK_25519_ChaChaPoly_SHA512"), + roundtrip("Noise_NN_25519_AESGCM_BLAKE2b"). + +name2_test() -> + Name = "Noise_NXpsk2_25519_AESGCM_SHA512", + ?assertError({name_not_recognized, Name}, enoise_protocol:from_name(Name)). + +name_pattern_test() -> + Pat = "XKfallback+psk0", + RoundPat = enoise_protocol:to_name_pattern(enoise_protocol:from_name_pattern(Pat)), + ?assertEqual(Pat, RoundPat). + +roundtrip(Name) -> + ExpectedName = iolist_to_binary(Name), + ?assertMatch(ExpectedName, enoise_protocol:to_name(enoise_protocol:from_name(Name))). diff --git a/test/enoise_tests.erl b/test/enoise_tests.erl index eb657b0..a193c2c 100644 --- a/test/enoise_tests.erl +++ b/test/enoise_tests.erl @@ -41,10 +41,9 @@ noise_interactive(V = #{ protocol_name := Name }) -> noise_interactive(_Name, Protocol, Init, Resp, Messages, HSHash) -> DH = enoise_protocol:dh(Protocol), SecK = fun(undefined) -> undefined; (Sec) -> enoise_keypair:new(DH, Sec, undefined) end, - PubK = fun(undefined) -> undefined; (Pub) -> enoise_keypair:new(DH, Pub) end, HSInit = fun(#{ e := E, s := S, rs := RS, prologue := PL }, R) -> - Opts = [{noise, Protocol}, {s, SecK(S)}, {e, SecK(E)}, {rs, PubK(RS)}, {prologue, PL}], + Opts = [{noise, Protocol}, {s, SecK(S)}, {e, SecK(E)}, {rs, RS}, {prologue, PL}], enoise:handshake(Opts, R) end, {ok, InitHS} = HSInit(Init, initiator), @@ -149,12 +148,12 @@ noise_test_run_(Conf, SKP, CKP) -> Protocol = enoise_protocol:from_name(Conf), Port = 4556, - SrvOpts = [{echos, 2}, {cpub, CKP}], + SrvOpts = [{echos, 2}, {cpub, enoise_keypair:pubkey(CKP)}], EchoSrv = enoise_utils:echo_srv_start(Port, Protocol, SKP, SrvOpts), {ok, TcpSock} = gen_tcp:connect("localhost", Port, [{active, once}, binary, {reuseaddr, true}], 100), - Opts = [{noise, Protocol}, {s, CKP}] ++ [{rs, SKP} || enoise_utils:need_rs(initiator, Conf) ], + Opts = [{noise, Protocol}, {s, CKP}] ++ [{rs, enoise_keypair:pubkey(SKP)} || enoise_utils:need_rs(initiator, Conf) ], {ok, EConn, _} = enoise:connect(TcpSock, Opts), ok = enoise:send(EConn, <<"Hello World!">>), diff --git a/test/enoise_utils.erl b/test/enoise_utils.erl index fe5377b..c01e85b 100644 --- a/test/enoise_utils.erl +++ b/test/enoise_utils.erl @@ -26,7 +26,7 @@ echo_srv(Port, Protocol, SKP, SrvOpts) -> AcceptRes = try enoise:accept(TcpSock, Opts) - catch _:R -> gen_tcp:close(TcpSock), {error, {R, erlang:get_stacktrace()}} end, + catch _:R:S -> gen_tcp:close(TcpSock), {error, {R, S}} end, gen_tcp:close(LSock),