forked from QPQ-AG/enoise
WIP
This commit is contained in:
+1
-1
@@ -18,7 +18,7 @@
|
||||
-copyright("QPQ AG <info@qpq.swiss>").
|
||||
-license("ISC").
|
||||
|
||||
%% Main function with generic Noise handshake
|
||||
%% Generic Noise handshake
|
||||
-export([handshake/2, handshake/3, step_handshake/2]).
|
||||
|
||||
%% API exports - Mainly mimicing gen_tcp
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
%%% @copyright 2026, QPQ AG
|
||||
%%% @copyright 2018, Aeternity Anstalt
|
||||
%%%
|
||||
%%% @doc
|
||||
%%% Module encapsulating a Noise Cipher state
|
||||
%%% @end
|
||||
|
||||
-module(znoise_cipher).
|
||||
-vsn("0.1.0").
|
||||
-author("Craig Everett <craigeverett@qpq.swiss>").
|
||||
-author("Hans Svensson <hanssv@gmail.com>").
|
||||
-copyright("QPQ AG <info@qpq.swiss>").
|
||||
-license("ISC").
|
||||
|
||||
-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("znoise.hrl").
|
||||
|
||||
-type cipher() :: 'ChaChaPoly' | 'AESGCM'.
|
||||
-type nonce() :: non_neg_integer().
|
||||
-type key() :: empty | binary().
|
||||
|
||||
% cs: "cipher state"
|
||||
-record(cs,
|
||||
{k = empty :: key(),
|
||||
n = 0 :: nonce(),
|
||||
cipher = 'ChaChaPoly' :: cipher()}).
|
||||
|
||||
-opaque state() :: #cs{}.
|
||||
|
||||
-export_type([cipher/0, state/0]).
|
||||
|
||||
|
||||
-spec init(key(), cipher()) -> state().
|
||||
|
||||
init(Key, Cipher) ->
|
||||
#cs{k = Key, n = 0, cipher = Cipher}.
|
||||
|
||||
|
||||
-spec set_key(CState, NewKey) -> NewCState
|
||||
when CState :: state(),
|
||||
NewKey :: key(),
|
||||
NewCState :: state().
|
||||
|
||||
set_key(CState, NewKey) ->
|
||||
CState#cs{k = NewKey, n = 0}.
|
||||
|
||||
|
||||
-spec has_key(state()) -> boolean().
|
||||
|
||||
has_key(#cs{k = Key}) ->
|
||||
Key =/= empty.
|
||||
|
||||
|
||||
-spec set_nonce(CState, NewNonce) -> NewCState
|
||||
when CState :: state(),
|
||||
NewNonce :: nonce(),
|
||||
NewCState :: state().
|
||||
|
||||
set_nonce(CState, Nonce) ->
|
||||
CState#cs{n = Nonce}.
|
||||
|
||||
|
||||
-spec encrypt_with_ad(CState, AD, PlainText) -> Outcome
|
||||
when CState :: state(),
|
||||
AD :: binary(),
|
||||
PlainText :: binary(),
|
||||
Outcome :: {ok, NewCState, CipherText} | {error, Reason},
|
||||
NewCState :: state(),
|
||||
CipherText :: binary(),
|
||||
Reason :: term().
|
||||
|
||||
encrypt_with_ad(CState = #cs{k = empty}, _AD, PlainText) ->
|
||||
{ok, CState, PlainText};
|
||||
encrypt_with_ad(CState = #cs{k = K, n = N, cipher = Cipher}, AD, PlainText) ->
|
||||
CipherText = znoise_crypto:encrypt(Cipher, K, N, AD, PlainText),
|
||||
{ok, CState#cs{n = N + 1}, CipherText}.
|
||||
|
||||
|
||||
-spec decrypt_with_ad(CState, AD, CipherText) -> Outcome
|
||||
when CState :: state(),
|
||||
AD :: binary(),
|
||||
CipherText :: binary(),
|
||||
Outcome :: {ok, NewCState, PlainText} | {error, Reason},
|
||||
NewCState :: state(),
|
||||
PlainText :: binary(),
|
||||
Reason :: term().
|
||||
|
||||
decrypt_with_ad(CState = #cs{k = empty}, _AD, CipherText) ->
|
||||
{ok, CState, CipherText};
|
||||
decrypt_with_ad(CState = #cs{k = K, n = N, cipher = Cipher}, AD, CipherText) ->
|
||||
case znoise_crypto:decrypt(Cipher, K, N, AD, CipherText) of
|
||||
PlainText when is_binary(PlainText) ->
|
||||
{ok, CState#cs{n = N + 1}, PlainText};
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec rekey(CState :: state()) -> state().
|
||||
|
||||
rekey(CState = #cs{k = empty}) ->
|
||||
CState;
|
||||
rekey(CState = #cs{k = K, cipher = Cipher}) ->
|
||||
CState#cs{k = znoise_crypto:rekey(Cipher, K)}.
|
||||
|
||||
|
||||
-spec cipher(CState :: state()) -> cipher().
|
||||
|
||||
cipher(#cs{cipher = Cipher}) ->
|
||||
Cipher.
|
||||
|
||||
-spec key(CState :: state()) -> key().
|
||||
|
||||
key(#cs{k = K}) ->
|
||||
K.
|
||||
@@ -1,88 +0,0 @@
|
||||
%%% @copyright 2026, QPQ AG
|
||||
%%% @copyright 2018, Aeternity Anstalt
|
||||
%%%
|
||||
%%% @doc
|
||||
%%% Module encapsulating a Noise Cipher state
|
||||
%%% @end
|
||||
|
||||
-module(znoise_cipher_state).
|
||||
-vsn("0.1.0").
|
||||
-author("Craig Everett <craigeverett@qpq.swiss>").
|
||||
-author("Hans Svensson <hanssv@gmail.com>").
|
||||
-copyright("QPQ AG <info@qpq.swiss>").
|
||||
-license("ISC").
|
||||
|
||||
-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("znoise.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 = znoise_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 znoise_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 = znoise_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.
|
||||
@@ -63,7 +63,7 @@ hkdf(Hash, Key, Data) ->
|
||||
Output3 = hmac(Hash, TempKey, <<Output2/binary, 3:8>>),
|
||||
[Output1, Output2, Output3].
|
||||
|
||||
-spec rekey(Cipher :: znoise_cipher_state:noise_cipher(), Key :: binary()) -> binary().
|
||||
-spec rekey(Cipher :: znoise_cipher:noise_cipher(), Key :: binary()) -> binary().
|
||||
rekey('ChaChaPoly', K0) ->
|
||||
KLen = 32,
|
||||
<<K:KLen/binary, _/binary>> = encrypt('ChaChaPoly', K0, ?MAX_NONCE, <<>>, <<0:(32*8)>>),
|
||||
@@ -71,13 +71,13 @@ rekey('ChaChaPoly', K0) ->
|
||||
rekey(Cipher, K) ->
|
||||
encrypt(Cipher, K, ?MAX_NONCE, <<>>, <<0:(32*8)>>).
|
||||
|
||||
-spec encrypt(Cipher :: znoise_cipher_state:noise_cipher(), Key :: binary(),
|
||||
-spec encrypt(Cipher :: znoise_cipher: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),
|
||||
<<CText/binary, CTag/binary>>.
|
||||
|
||||
-spec decrypt(Cipher ::znoise_cipher_state:noise_cipher(), Key :: binary(),
|
||||
-spec decrypt(Cipher ::znoise_cipher:noise_cipher(), Key :: binary(),
|
||||
Nonce :: non_neg_integer(), AD :: binary(),
|
||||
CipherText :: binary()) -> binary() | {error, term()}.
|
||||
decrypt(Cipher, K, N, Ad, CipherText0) ->
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
-type noise_dh() :: dh25519 | dh448.
|
||||
-type noise_token() :: s | e | ee | ss | es | se.
|
||||
-type keypair() :: znoise_keypair:keypair().
|
||||
-type noise_split_state() :: #{rx := znoise_cipher_state:state(),
|
||||
tx := znoise_cipher_state:state(),
|
||||
-type noise_split_state() :: #{rx := znoise_cipher:state(),
|
||||
tx := znoise_cipher:state(),
|
||||
hs_hash := binary(),
|
||||
final_state => state() }.
|
||||
|
||||
@@ -180,7 +180,7 @@ dh(#noise_hs{ dh = DH }, Key1, Key2) ->
|
||||
|
||||
has_key(#noise_hs{ ss = SS }) ->
|
||||
CS = znoise_sym_state:cipher_state(SS),
|
||||
znoise_cipher_state:has_key(CS).
|
||||
znoise_cipher:has_key(CS).
|
||||
|
||||
mix_key(HS = #noise_hs{ ss = SS0 }, Data) ->
|
||||
HS#noise_hs{ ss = znoise_sym_state:mix_key(SS0, Data) }.
|
||||
|
||||
+14
-12
@@ -12,30 +12,32 @@
|
||||
-copyright("QPQ AG <info@qpq.swiss>").
|
||||
-license("ISC").
|
||||
|
||||
-export([ key_type/1
|
||||
, new/1
|
||||
, new/2
|
||||
, new/3
|
||||
, pubkey/1
|
||||
, seckey/1
|
||||
]).
|
||||
-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() }).
|
||||
-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().
|
||||
%% @doc Generate a new keypair of type `Type'.
|
||||
|
||||
new(Type) ->
|
||||
{Pub, Sec} = new_key_pair(Type),
|
||||
#kp{ type = Type, sec = Sec, pub = Pub }.
|
||||
#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
|
||||
|
||||
+14
-15
@@ -12,15 +12,15 @@
|
||||
-copyright("QPQ AG <info@qpq.swiss>").
|
||||
-license("ISC").
|
||||
|
||||
-export([ cipher/1
|
||||
, dh/1
|
||||
, from_name/1
|
||||
, hash/1
|
||||
, msgs/2
|
||||
, pattern/1
|
||||
, pre_msgs/2
|
||||
, supported/0
|
||||
, to_name/1]).
|
||||
-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]).
|
||||
@@ -30,17 +30,16 @@
|
||||
-type noise_msg() :: {in | out, [znoise_hs_state:noise_token()]}.
|
||||
|
||||
-record(noise_protocol,
|
||||
{ hs_pattern = noiseNN :: noise_pattern()
|
||||
, dh = dh25519 :: znoise_hs_state:noise_dh()
|
||||
, cipher = 'ChaChaPoly' :: znoise_cipher_state:noise_cipher()
|
||||
, hash = blake2b :: znoise_sym_state:noise_hash()
|
||||
}).
|
||||
{hs_pattern = noiseNN :: noise_pattern()
|
||||
dh = dh25519 :: znoise_hs_state:noise_dh()
|
||||
cipher = 'ChaChaPoly' :: znoise_cipher:noise_cipher()
|
||||
hash = blake2b :: znoise_sym_state:noise_hash()}).
|
||||
|
||||
-opaque protocol() :: #noise_protocol{}.
|
||||
|
||||
-export_type([noise_msg/0, noise_pattern/0, protocol/0]).
|
||||
|
||||
-spec cipher(Protocol :: protocol()) -> znoise_cipher_state:noise_cipher().
|
||||
-spec cipher(Protocol :: protocol()) -> znoise_cipher:noise_cipher().
|
||||
cipher(#noise_protocol{ cipher = Cipher }) ->
|
||||
Cipher.
|
||||
|
||||
|
||||
+10
-10
@@ -28,7 +28,7 @@
|
||||
|
||||
-type noise_hash() :: sha256 | sha512 | blake2s | blake2b.
|
||||
|
||||
-record(noise_ss, { cs :: znoise_cipher_state:state()
|
||||
-record(noise_ss, { cs :: znoise_cipher:state()
|
||||
, ck = <<>> :: binary()
|
||||
, h = <<>> :: binary()
|
||||
, hash = blake2b :: noise_hash() }).
|
||||
@@ -50,13 +50,13 @@ init(Protocol) ->
|
||||
#noise_ss{ h = H1
|
||||
, ck = H1
|
||||
, hash = Hash
|
||||
, cs = znoise_cipher_state:init(empty, Cipher) }.
|
||||
, cs = znoise_cipher:init(empty, Cipher) }.
|
||||
|
||||
-spec mix_key(SState :: state(), InputKeyMaterial :: binary()) -> state().
|
||||
mix_key(SState = #noise_ss{ hash = Hash, ck = CK0, cs = CS0 }, InputKeyMaterial) ->
|
||||
[CK1, <<TempK:32/binary, _/binary>> | _] =
|
||||
znoise_crypto:hkdf(Hash, CK0, InputKeyMaterial),
|
||||
CS1 = znoise_cipher_state:set_key(CS0, TempK),
|
||||
CS1 = znoise_cipher:set_key(CS0, TempK),
|
||||
SState#noise_ss{ ck = CK1, cs = CS1 }.
|
||||
|
||||
-spec mix_hash(SState :: state(), Data :: binary()) -> state().
|
||||
@@ -68,32 +68,32 @@ mix_hash(SState = #noise_ss{ hash = Hash, h = H0 }, Data) ->
|
||||
mix_key_and_hash(SState = #noise_ss{ hash = Hash, ck = CK0, cs = CS0 }, InputKeyMaterial) ->
|
||||
[CK1, TempH, <<TempK:32/binary, _/binary>>] =
|
||||
znoise_crypto:hkdf(Hash, CK0, InputKeyMaterial),
|
||||
CS1 = znoise_cipher_state:set_key(CS0, TempK),
|
||||
CS1 = znoise_cipher: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} = znoise_cipher_state:encrypt_with_ad(CS0, H, PlainText),
|
||||
{ok, CS1, CipherText} = znoise_cipher: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 znoise_cipher_state:decrypt_with_ad(CS0, H, CipherText) of
|
||||
case znoise_cipher: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()) -> {znoise_cipher_state:state(), znoise_cipher_state:state()}.
|
||||
-spec split(SState :: state()) -> {znoise_cipher:state(), znoise_cipher:state()}.
|
||||
split(#noise_ss{ hash = Hash, ck = CK, cs = CS }) ->
|
||||
[<<TempK1:32/binary, _/binary>>, <<TempK2:32/binary, _/binary>>, _] =
|
||||
znoise_crypto:hkdf(Hash, CK, <<>>),
|
||||
{znoise_cipher_state:set_key(CS, TempK1),
|
||||
znoise_cipher_state:set_key(CS, TempK2)}.
|
||||
{znoise_cipher:set_key(CS, TempK1),
|
||||
znoise_cipher:set_key(CS, TempK2)}.
|
||||
|
||||
-spec cipher_state(SState :: state()) -> znoise_cipher_state:state().
|
||||
-spec cipher_state(SState :: state()) -> znoise_cipher:state().
|
||||
cipher_state(#noise_ss{ cs = CS }) ->
|
||||
CS.
|
||||
|
||||
|
||||
+141
-195
@@ -4,8 +4,8 @@
|
||||
%%% @doc
|
||||
%%% A gen_server for holding a Noise connection over gen_tcp.
|
||||
%%%
|
||||
%%% Some care is needed since the underlying transmission is broken up
|
||||
%%% into Noise packets, so we need some buffering.
|
||||
%%% Currently only "raw" mode is supported, but a `{packet, N}'
|
||||
%%% option would be very convenient in the future.
|
||||
%%% @end
|
||||
|
||||
-module(znoise_tcp).
|
||||
@@ -15,229 +15,175 @@
|
||||
-copyright("QPQ AG <info@qpq.swiss>").
|
||||
-license("ISC").
|
||||
|
||||
-export([controlling_process/2,
|
||||
close/1,
|
||||
-export([start_listen/2,
|
||||
start_connect/2,
|
||||
send/2,
|
||||
set_active/2,
|
||||
start_link/5]).
|
||||
close/1]).
|
||||
|
||||
%% gen_server
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
||||
terminate/2, code_change/3]).
|
||||
-include("$zx_include/zx_logger.hrl").
|
||||
|
||||
-record(znoise,
|
||||
{pid}).
|
||||
|
||||
% TODO: provide gen_tcp's packet option or 'raw'
|
||||
-record(s,
|
||||
{rx = ,
|
||||
tx = ,
|
||||
owner = none :: none | pid(),
|
||||
owner_ref = none :: none | reference(),
|
||||
tcp_sock = none :: none | gen_tcp:socket(),
|
||||
active = once :: true | {once, boolean()},
|
||||
msgbuf = [] :: list(),
|
||||
rawbuf = <<>> :: binary()}).
|
||||
{rx = none :: none | znoise_cipher:state(),
|
||||
tx = none :: none | znoise_cipher:state(),
|
||||
sock = none :: none | gen_tcp:socket(),
|
||||
mode = raw :: raw | {packet, 1..4},
|
||||
buff = <<>> :: binary()}).
|
||||
|
||||
|
||||
start_link(TcpSock, Rx, Tx, Owner, {Active0, Buf}) ->
|
||||
Active =
|
||||
case Active0 of
|
||||
true -> true;
|
||||
once -> {once, false}
|
||||
end,
|
||||
State =
|
||||
#s{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...
|
||||
ok =
|
||||
case Buf =/= <<>> of
|
||||
true -> Pid ! {tcp, TcpSock, Buf};
|
||||
false -> ok
|
||||
end,
|
||||
flush_tcp(Pid, TcpSock),
|
||||
{ok, Pid};
|
||||
Error ->
|
||||
close(Pid),
|
||||
Error
|
||||
end;
|
||||
Error
|
||||
Error
|
||||
%%% Initializers
|
||||
|
||||
-spec start_listen(ListenSock, Options, Noise) -> Outcome
|
||||
when ListenSock ::
|
||||
Options ::
|
||||
Noise ::
|
||||
Outcome :: {ok, pid()} | {error, Reason :: term()}.
|
||||
|
||||
start_listen(ListenSock, Options, Noise) ->
|
||||
proc_lib:start_link(?MODULE, init_listen, [self(), ListenSock, Options, Noise]).
|
||||
|
||||
|
||||
init_listen(Parent, ListenSock, Options, Noise) ->
|
||||
Debug = sys:debug_options(proplists:get_value(debug, Options, [])),
|
||||
ok = proc_lib:init_ack(Parent, {ok, self()}),
|
||||
listen(Parent, Debug, ListenSock, Options, Noise).
|
||||
|
||||
|
||||
listen(Parent, Debug, ListenSock, Options, Noise) ->
|
||||
|
||||
|
||||
start_connect(Host, Options, Noise) ->
|
||||
proc_lib:start_link(?MODULE, init_connect, [self(), Host, Options, Noise]).
|
||||
|
||||
|
||||
init_connect(Parent, Host, Options, Noise) ->
|
||||
Debug = sys:debug_options(proplists:get_value(debug, Options, [])),
|
||||
ok = proc_lib:init_ack(Parent, {ok, self()}),
|
||||
connect(Parent, Debug, Host, Options, Noise).
|
||||
|
||||
|
||||
connect(Parent, Debug, Host, Options, Noise) ->
|
||||
|
||||
|
||||
|
||||
%%% Interface
|
||||
|
||||
-spec send(Conn, Data) -> Outcome
|
||||
when Conn :: pid(),
|
||||
Data :: binary() | {file, file:filename()},
|
||||
Outcome :: ok | {error, Reason :: term()}.
|
||||
|
||||
send(Conn, Data) ->
|
||||
call(Conn, {send, Data}).
|
||||
|
||||
|
||||
-spec close(Conn) -> Outcome
|
||||
case Conn :: pid(),
|
||||
Outcome :: ok | {error, Reason :: term()}.
|
||||
|
||||
close(Conn) ->
|
||||
call(Conn, close).
|
||||
|
||||
|
||||
%% Non-system calls should only ever come from the parent
|
||||
|
||||
call(Conn, Data) ->
|
||||
call(Conn, Data, 5000).
|
||||
|
||||
call(Conn, Data, Timeout) ->
|
||||
Ref = make_ref(),
|
||||
Conn ! {call, Ref, Data},
|
||||
receive
|
||||
{resp, Ref, Result} -> Result
|
||||
after Timeout -> {error, timeout}
|
||||
end.
|
||||
|
||||
|
||||
-spec send(Noise :: pid(), Data :: binary()) -> ok | {error, term()}.
|
||||
-spec loop(Parent, Debug, State) -> no_return()
|
||||
when Parent :: pid(),
|
||||
Debug :: [sys:dbg_opt()],
|
||||
State :: state().
|
||||
|
||||
send(Noise, Data) ->
|
||||
gen_server:call(Noise, {send, Data}).
|
||||
loop(Parent, Debug, State = #s{sock = Sock}) ->
|
||||
case inet:setopts(Sock, [{active, once}]) of
|
||||
ok -> wait(Parent, Debug, State);
|
||||
{error, einval} -> exit(normal)
|
||||
end.
|
||||
|
||||
wait(Parent, Debug, State = #s{sock = Sock}) ->
|
||||
receive
|
||||
{tcp, Sock, Binary} ->
|
||||
Newtate = get_bytes(Parent, Binary, State),
|
||||
loop(Parent, Debug, NewState);
|
||||
{call, Ref, {send, Data}} ->
|
||||
{Result, NewState} = send_bytes(Data, State),
|
||||
Parent ! {resp, Ref, Result},
|
||||
wait(Parent, Debug, NewState);
|
||||
{call, Ref, close} ->
|
||||
Result = gen_tcp:close(Sock),
|
||||
Parent ! {resp, Ref, Result},
|
||||
exit(normal);
|
||||
{tcp_closed, Sock} ->
|
||||
Parent ! {noise_closed, self()},
|
||||
ok = tell(info, "Socket closed!"),
|
||||
exit(normal);
|
||||
{tcp_error, Sock, Info} ->
|
||||
Parent ! {noise_error, Info},
|
||||
ok = tell(info, "Socket error: ~tp", [Info]),
|
||||
exit(normal);
|
||||
{system, From, Request} ->
|
||||
sys:handle_system_msg(Request, From, Parent, ?MODULE, Debug, State);
|
||||
Unexpected ->
|
||||
ok = tell(info, "~p Unexpected message: ~tp", [self(), Unexpected]),
|
||||
wait(Parent, Debug, State)
|
||||
end.
|
||||
|
||||
|
||||
-spec set_active(Noise :: pid(), Active :: true | once) -> ok | {error, term()}.
|
||||
|
||||
set_active(Noise, Active) ->
|
||||
gen_server:call(Noise, {active, self(), Active}).
|
||||
system_continue(Parent, Debug, State) ->
|
||||
wait(Parent, Debug, State).
|
||||
|
||||
|
||||
-spec close(Noise :: pid()) -> ok | {error, term()}.
|
||||
|
||||
close(Noise) ->
|
||||
gen_server:call(Noise, close).
|
||||
system_terminate(Reason, _Parent, _Debug, _State) ->
|
||||
exit(Reason).
|
||||
|
||||
|
||||
-spec controlling_process(Noise :: pid(), NewPid :: pid()) -> ok | {error, term()}.
|
||||
|
||||
controlling_process(Noise, NewPid) ->
|
||||
gen_server:call(Noise, {controlling_process, self(), NewPid}, 100).
|
||||
system_get_state(State) ->
|
||||
{ok, State#s{rx = nope, tx = nope}}.
|
||||
|
||||
|
||||
%% gen_server
|
||||
|
||||
init([#s{owner = Owner} = State]) ->
|
||||
OwnerRef = erlang:monitor(process, Owner),
|
||||
{ok, State#s{owner_ref = OwnerRef}}.
|
||||
system_replace_state(SusFun, State) ->
|
||||
ok = tell(info, "Attempt to run system_replace_state/2 with ~tp", [SusFun]),
|
||||
{ok, State, State}.
|
||||
|
||||
|
||||
handle_call(close, _, State) ->
|
||||
{stop, normal, ok, State};
|
||||
handle_call(_Call, _, State = #s{tcp_sock = closed}) ->
|
||||
{reply, {error, closed}, State};
|
||||
handle_call({send, Data}, _, State) ->
|
||||
{Result, NewState} = handle_send(State, Data),
|
||||
{reply, Result, NewState};
|
||||
handle_call({controlling_process, OldPID, NewPID}, _, State) ->
|
||||
{Result, NewState} = handle_control_change(State, OldPID, NewPID),
|
||||
{reply, Result, NewState};
|
||||
handle_call({active, PID, NewActive}, _, State) ->
|
||||
{Result, NewState} = handle_active(State, PID, NewActive),
|
||||
{reply, Result, NewState}.
|
||||
|
||||
|
||||
handle_cast(_, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
|
||||
handle_info({tcp, TS, Data}, State = #s{tcp_sock = TS, owner = O}) ->
|
||||
try
|
||||
{NextState = #s{msgbuf = Buf}, Msgs} = handle_data(State, Data),
|
||||
NewState = handle_msgs(NextState#s{msgbuf = Buf ++ Msgs}),
|
||||
set_active(NewState),
|
||||
{noreply, NewState}
|
||||
catch error:{znoise_error, _} ->
|
||||
%% We are not likely to recover, but leave the decision to upstream
|
||||
O ! {znoise_error, TS, decrypt_error},
|
||||
{noreply, State}
|
||||
end;
|
||||
handle_info({tcp_closed, TS}, State = #s{tcp_sock = TS, owner = O}) ->
|
||||
O ! {tcp_closed, TS},
|
||||
{noreply, State#s{tcp_sock = closed}};
|
||||
handle_info({'DOWN', OwnerRef, process, _, normal},
|
||||
State = #s{tcp_sock = TS, owner_ref = OwnerRef}) ->
|
||||
close_tcp(TS),
|
||||
{stop, normal, State#s{tcp_sock = closed, owner_ref = undefined}};
|
||||
handle_info({'DOWN', _, _, _, _}, State) ->
|
||||
%% Ignore non-normal monitor messages - we are linked.
|
||||
{noreply, State};
|
||||
handle_info(_Msg, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
|
||||
terminate(_, #s{tcp_sock = TcpSock, owner_ref = ORef}) ->
|
||||
[ gen_tcp:close(TcpSock) || TcpSock /= closed ],
|
||||
[ erlang:demonitor(ORef, [flush]) || ORef /= undefined ],
|
||||
ok.
|
||||
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
system_code_change(State, _Module, _OldVsn, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
|
||||
|
||||
%%% Handlers
|
||||
|
||||
handle_control_change(State = #s{owner = PID, owner_ref = OldRef}, PID, NewPID) ->
|
||||
NewRef = erlang:monitor(process, NewPID),
|
||||
erlang:demonitor(OldRef, [flush]),
|
||||
{ok, State#s{owner = NewPID, owner_ref = NewRef}};
|
||||
handle_control_change(State, _, _) ->
|
||||
{{error, not_owner}, State}.
|
||||
|
||||
|
||||
handle_active(State = #s{owner = PID, tcp_sock = TcpSock}, PID, Active) ->
|
||||
case Active of
|
||||
true ->
|
||||
inet:setopts(TcpSock, [{active, true}]),
|
||||
{ok, handle_msgs(State#s{active = true})};
|
||||
once ->
|
||||
NewState = handle_msgs(State#s{active = {once, false}}),
|
||||
set_active(NewState),
|
||||
{ok, NewState}
|
||||
read_bytes(Parent, <<Len:16, CT:Len/binary, Rest/binary>>, State = #s{buff = <<>>, rx = RX}) ->
|
||||
case znoise_cipher:decrypt_with_ad(RX, <<>>, CT) of
|
||||
{ok, NewRX, PT} ->
|
||||
Parent ! {noise, PT},
|
||||
read_bytes(Parent, Rest, State#s{rx = NewRX});
|
||||
{error, _} ->
|
||||
Parent ! {noise_error, decrypt_input_failed}
|
||||
exit(normal)
|
||||
end;
|
||||
handle_active(State, _, _) ->
|
||||
{{error, not_owner}, State}.
|
||||
|
||||
|
||||
handle_data(State = #s{rawbuf = Buf, rx = RX}, Data) ->
|
||||
case <<Buf/binary, Data/binary>> of
|
||||
B = <<Len:16, Rest/binary>> when Len > byte_size(Rest) ->
|
||||
{State#s{rawbuf = B}, []}; %% Not a full Noise message - save it
|
||||
<<Len:16, Rest/binary>> ->
|
||||
<<Msg:Len/binary, Rest2/binary>> = Rest,
|
||||
case znoise_cipher_state:decrypt_with_ad(RX, <<>>, Msg) of
|
||||
{ok, NewRX, NewMsg} ->
|
||||
{NewState, Msgs} = handle_data(State#s{rawbuf = Rest2, rx = NewRX}, <<>>),
|
||||
{NewState, [NewMsg | Msgs]};
|
||||
{error, _} ->
|
||||
error({znoise_error, decrypt_input_failed})
|
||||
end;
|
||||
EmptyOrSingleByte ->
|
||||
{State#s{rawbuf = EmptyOrSingleByte}, []}
|
||||
read_bytes(Parent, Received, State = #s{buff = <<>>}) ->
|
||||
State#s{buff = Received};
|
||||
read_bytes(Parent, Received, State = #s{buff = Buff}) ->
|
||||
case <<Buff/binary, Received/binary>> of
|
||||
<<>> -> State;
|
||||
Data -> read_bytes(Parent, Data, State#s{buff = <<>>})
|
||||
end.
|
||||
|
||||
|
||||
handle_msgs(State = #s{msgbuf = []}) ->
|
||||
State;
|
||||
handle_msgs(State = #s{msgbuf = Msgs, active = true, owner = Owner}) ->
|
||||
[ Owner ! {noise, #znoise{pid = self()}, Msg} || Msg <- Msgs ],
|
||||
State#s{msgbuf = []};
|
||||
handle_msgs(State = #s{msgbuf = [Msg | Msgs], active = {once, Delivered}, owner = Owner}) ->
|
||||
case Delivered of
|
||||
true ->
|
||||
State;
|
||||
false ->
|
||||
Owner ! {noise, #znoise{pid = self()}, Msg},
|
||||
State#s{msgbuf = Msgs, active = {once, true}}
|
||||
end.
|
||||
|
||||
|
||||
handle_send(State = #s{tcp_sock = TcpSock, tx = TX}, Data) ->
|
||||
{ok, MewTX, Msg} = znoise_cipher_state:encrypt_with_ad(TX, <<>>, Data),
|
||||
send_bytes(Data, State = #s{sock = Sock, tx = TX}) ->
|
||||
{ok, NewTX, Msg} = znoise_cipher:encrypt_with_ad(TX, <<>>, Data),
|
||||
case gen_tcp:send(TcpSock, <<(byte_size(Msg)):16, Msg/binary>>) of
|
||||
ok -> {ok, State#s{tx = MewTX}};
|
||||
ok -> {ok, State#s{tx = NewTX}};
|
||||
Error -> {Error, State}
|
||||
end.
|
||||
|
||||
|
||||
set_active(#s{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).
|
||||
|
||||
Reference in New Issue
Block a user