%%%-------------------------------------------------------------------
%% @doc enoise_chat public API
%% @end
%%%-------------------------------------------------------------------
-module(enoise_chat).
-export([start/0, main/1]).
start() ->
ok.
main(Args) ->
_ ->
usage(),
halt(1)
end.
%% -----------------------
%% Client
%% -----------------------
{error, Reason} ->
io:format("TCP connect failed: ~p~n", [Reason]),
halt(1)
end.
%% -----------------------
%% Server
%% -----------------------
io:format("Listening on port ~p~n", [Port]),
{error, Reason} ->
io:format("Listen failed: ~p~n", [Reason]),
halt(1)
end.
{error, Reason} ->
%% -----------------------
%% Key Loading
%% -----------------------
load_keypair(File) ->
case file:consult(File) of
{ok, Terms} ->
case lists:keyfind(my_keys, 1, Terms) of
{my_keys, #{priv := SK, pub := PK}} ->
_ ->
error(badkey)
end;
{error, Reason} ->
io:format("Could not load keys from ~s: ~p~n", [File, Reason]),
halt(1)
end.
%% -----------------------
%% Args & Usage
%% -----------------------
parse_args(Args) ->
parse_args(Args, #{}).
parse_args(["-mode", "client" | Rest], Acc) ->
parse_args(Rest, Acc#{mode => client});
parse_args(["-mode", "server" | Rest], Acc) ->
parse_args(Rest, Acc#{mode => server});
parse_args(["-host", Host | Rest], Acc) ->
parse_args(Rest, Acc#{host => Host});
parse_args(["-port", PortStr | Rest], Acc) ->
parse_args(Rest, Acc#{port => list_to_integer(PortStr)});
parse_args(["-keys", KeyFile | Rest], Acc) ->
parse_args(Rest, Acc#{keys => KeyFile});
parse_args([_ | Rest], Acc) ->
parse_args(Rest, Acc).
usage() ->
io:format("Usage:~n"),
io:format(" -host <host> (client only)~n"),
io:format(" -port <number>~n"),
io:format(" -keys <file> (path to key config)~n"),
io:format("~nExample (server):~n"),
io:format("~nExample (client):~n"),
io:format(" enoise_chat -mode client (-host localhost) (-port 7891) (-keys "
"keys.txt) (-psk hexstring)~n").
io:format(" enoise_chat -mode server (-port 7891) (-keys keys.txt) (-psk "
"hexstring)~n"),
io:format(" -mode keygen|client|server~n"),
parse_args(["-psk", PSK | Rest], Acc) ->
Bin = hex:hexstr_to_bin(PSK),
32 = byte_size(Bin),
parse_args(Rest, Acc#{psk => Bin});
parse_args(["-mode", "keygen" | Rest], Acc) ->
parse_args(Rest, Acc#{mode => keygen});
parse_args([], Acc) ->
Acc;
enoise_keypair:new(dh25519, hex:hexstr_to_bin(SK), hex:hexstr_to_bin(PK));
io:format("Accept failed: ~p~n", [Reason])
end,
accept_loop(ListenPid, KeyPair, PSK).
accept_loop(ListenPid, KeyPair, PSK) ->
Opts = [{keypair, KeyPair}, {psk, PSK}],
case enoise_cable:accept(ListenPid, Opts) of
{ok, ConnPid} ->
io:format("Handshake successful~n"),
init_loop(ConnPid),
io:format("Chat ended~n");
accept_loop(ListenPid, KeyPair, PSK);
server_listen(Port, KeyPair, PSK) ->
case enoise_cable:listen(Port) of
{ok, ListenPid} ->
client_connect(Host, Port, KeyPair, PSK) ->
Opts = [{keypair, KeyPair}, {psk, PSK}],
case enoise_cable:connect(Host, Port, Opts) of
{ok, ConnPid} ->
io:format("Noise handshake complete~n"),
init_loop(ConnPid);
generate_keys(File) ->
% Generate client keypair
% Convert to hex strings for storage
case file:write_file(File, Content) of
ok ->
io:format("Keys generated and saved to ~s~n", [File]);
{error, Reason} ->
io:format("Failed to write keys to ~s: ~p~n", [File, Reason]),
halt(1)
end.
%% -----------------------
end.
%% Chat
Self = self(),
io:setopts([{echo, false}]),
input_loop(EvtLoopPid) ->
% TODO: we need better drawing to not trample this prompt with incoming messages but lets not get too crazy here
case io:get_line("> ") of
eof ->
EvtLoopPid ! {input_close, eof},
exit(normal);
{error, Err} ->
EvtLoopPid ! {input_close, {error, Err}},
exit(error);
Line ->
input_loop(EvtLoopPid)
end.
receive
io:format("💬 ~s~n", [string:chomp(Msg)]),
{input_line, InputLine} ->
"quit" ->
exit(normal);
Clean ->
{input_close, Why} ->
io:format("Closing ~p~n", [Why]),
exit(normal);
{'EXIT', _Input, _} ->
io:format("Input process died~n"),
enoise_cable:close(ConnPid)
enoise_cable:close(ConnPid),
end
end;
case enoise_cable:send(ConnPid, list_to_binary(Clean ++ "\n")) of
ok ->
chat_loop(ConnPid);
{error, Reason} ->
io:format("Send failed: ~p~n", [Reason]),
halt(1)
enoise_cable:close(ConnPid),
case InputLine of
chat_loop(ConnPid);
{cable_transport, _EConn, Msg} ->
chat_loop(ConnPid) ->
EvtLoopPid ! {input_line, string:chomp(Line)},
chat_loop(ConnPid).
spawn(fun() -> input_loop(Self) end),
init_loop(ConnPid) ->
Content =
io_lib:format("%% Generated Noise keypairs~n~n{my_keys, #{~n priv => \"~s\",~n "
" pub => \"~s\"~n}}.~n",
[SKHex, PKHex]),
SKHex = hex:bin_to_hexstr(Secret),
PKHex = hex:bin_to_hexstr(Public),
{kp, dh25519, Secret, Public} = enoise_keypair:new(dh25519),
%% -----------------------
%% Key Generation
#{mode := server,
port := Port,
keys := KeyFile,
psk := PSK} ->
KeyPair = load_keypair(KeyFile),
server_listen(Port, KeyPair, PSK);
#{mode := keygen, keys := KeyFile} ->
generate_keys(KeyFile);
#{mode := client,
host := Host,
port := Port,
keys := KeyFile,
psk := PSK} ->
KeyPair = load_keypair(KeyFile),
client_connect(Host, Port, KeyPair, PSK);
Defaults =
#{port => 7891,
keys => "keys.txt",
host => "localhost",
psk =>
hex:hexstr_to_bin("0808080808080808080808080808080808080808080808080808080808080808")},
Parsed = parse_args(Args),
Merged = maps:merge(Defaults, Parsed),
case Merged of