~umgeher/utsession

af09dfecd07b4fde368d98627f338d7324fc9e46 — Umgeher Torgersen 1 year, 4 months ago 54e981a master 1.0
new utsession

+ using PID as last arg
+ RSA replaced by ECDSA
+ auto-gen keys enable by default

Squashed commit of the following:

commit 559f9e10d7e889b7823a77c62f31b488fc27d9b2
Author: Umgeher Torgersen <me@umgeher.org>
Date:   Mon Feb 20 14:52:16 2023 +0000

    rsa stuff is no more... genkeys recipe removed

commit 6e992064bfc0932ebeedae206f88729d4ea259f4
Author: Umgeher Torgersen <me@umgeher.org>
Date:   Mon Feb 20 14:51:29 2023 +0000

    using ecdsa to sign and verify sessions
3 files changed, 79 insertions(+), 132 deletions(-)

M Makefile
M src/utsession.erl
M test/actor_test.erl
M Makefile => Makefile +0 -4
@@ 14,7 14,3 @@ DEP_PLUGINS = gitversion.mk
dep_gitversion.mk = git https://git.sr.ht/~umgeher/gitversion.mk master

include erlang.mk

genkeys:
	@openssl genrsa -out private.pem 2048
	@openssl rsa -in private.pem -out public.pem -outform PEM -pubout

M src/utsession.erl => src/utsession.erl +50 -84
@@ 6,12 6,11 @@
-export([data_get/1, data_get/2]).
-export([data_merge/2]).
-export([data_set/2, data_set/3]).
-export([load/2]).
-export([mtable_get/1]).
-export([mtable_load_file/2]).
-export([mtable_start/0]).
-export([start/0]).
-export([start/1]).
-export([generate_keys/0]).
-export([keys_get/1]).
-export([load/3]).
-export([sign/1]).
-export([start/0, start/1]).
-export([stop/1]).
-export([store/1]).
-export([rt/1]).


@@ 36,60 35,41 @@

%% API.

data_del(PID, Key) ->
generate_keys() ->
  crypto:generate_key(ecdh, prime256v1).

data_del(Key, PID) ->
  gen_server:call(PID, {data, del, Key}).

data_get(PID) ->
  gen_server:call(PID, {data, get}).

data_get(PID, Key) ->
data_get(Key, PID) ->
  gen_server:call(PID, {data, get, Key}).

data_merge(PID, Map) ->
data_merge(Map, PID) ->
  gen_server:call(PID, {data, merge, Map}).

data_set(PID, Map) when is_map(Map) ->
data_set(Map, PID) when is_map(Map) ->
  gen_server:call(PID, {data, set, Map}).

data_set(PID, Key, Value) ->
data_set(Key, Value, PID) ->
  gen_server:call(PID, {data, set, Key, Value}).

load(PID, <<Bin/binary>>) ->
  gen_server:call(PID, {load, Bin}).

mtable_get(Name) ->
  case ets:lookup(?MODULE, Name) of
    [] ->
      {error, not_found};
    [{Name, Key}] ->
      {ok, Key}
  end.

mtable_load_file(Name, File) ->
  case ets:lookup(?MODULE, Name) of
    [] ->
      {ok, PK} = file:read_file(File),
      [D] = public_key:pem_decode(PK),
      ets:insert(?MODULE, [{Name, public_key:pem_entry_decode(D)}]);
    _ ->
      ok
  end,
  ok.
keys_get(PID) when is_pid(PID) ->
  gen_server:call(PID, {keys, get}).

mtable_start() ->
  case ets:info(?MODULE) of
    undefined ->
      ets:new(?MODULE, [set, public, named_table, {read_concurrency, true}]);
    _ ->
      ok
  end,
  ok.
load(<<Sign/binary>>, <<Data/binary>>, PID) when is_pid(PID) ->
  gen_server:call(PID, {load, Sign, Data}).

sign(PID) when is_pid(PID) ->
  gen_server:call(PID, sign).

start() ->
  gen_server:start(?MODULE, [], []).
  start(generate_keys()).

start(Options) ->
  gen_server:start(?MODULE, Options, []).
start({Public, Priv}) ->
  gen_server:start(?MODULE, [{key_private, Priv}, {key_public, Public}], []).

stop(PID) ->
  gen_server:stop(PID).


@@ 100,7 80,7 @@ store(PID) ->
rt(PID) ->
  gen_server:call(PID, {rt, get}).

rt_update(PID, RT) ->
rt_update(RT, PID) ->
  gen_server:call(PID, {rt, update, RT}).

ttl(PID) ->


@@ 115,64 95,48 @@ ttl_validate(PID) ->
%% gen_server.

init(Options) ->
  mtable_start(),
  gen_server:cast(self(), init),
  {ok, init_options(Options, #state{data = utsession_struct:new()})}.

init_options([], S) ->
  S;
init_options([{data, <<Bin/binary>>} | T], S) ->
  init_options(T, S#state{data = Bin});
init_options([{private, {file, Name, File}} | T], S) ->
  mtable_load_file(Name, File),
  {ok, Key} = mtable_get(Name),
  init_options(T, S#state{private = Key});
init_options([{private, Name} | T], S) ->
  {ok, Key} = mtable_get(Name),  
  init_options(T, S#state{private = Key});
init_options([{public, {file, Name, File}} | T], S) ->
  mtable_load_file(Name, File),
  {ok, Key} = mtable_get(Name),
  init_options(T, S#state{public = Key});
init_options([{public, Name} | T], S) ->
  {ok, Key} = mtable_get(Name),
  init_options(T, S#state{public = Key});
init_options([{key_private, <<Priv/binary>>} | T], S) ->
  init_options(T, S#state{private = Priv});
init_options([{key_public, <<Public/binary>>} | T], S) ->
  init_options(T, S#state{public = Public});
init_options([_ | T], S) ->
  init_options(T, S).

handle_call({data, del, Key}, _, S) ->
  {reply, {ok, del}, S#state{data = utsession_struct:kdel(Key, S#state.data)}};
  {reply, ok, S#state{data = utsession_struct:kdel(Key, S#state.data)}};
handle_call({data, get}, _, S) ->
  {reply, utsession_struct:kget(S#state.data), S};
handle_call({data, get, Key}, _, S) ->
  {reply, utsession_struct:kget(Key, S#state.data), S};
handle_call({data, merge, Map}, _, S) ->
  {reply, {ok, merge}, S#state{data = utsession_struct:merge(Map, S#state.data)}};
  {reply, ok, S#state{data = utsession_struct:merge(Map, S#state.data)}};
handle_call({data, set, Map}, _, S) when is_map(Map) ->
  {reply, {ok, set}, S#state{data = utsession_struct:kset(Map, S#state.data)}};
  {reply, ok, S#state{data = utsession_struct:kset(Map, S#state.data)}};
handle_call({data, set, Key, Value}, _, S) ->
  {reply, {ok, set}, S#state{data = utsession_struct:kset(Key, Value, S#state.data)}};
handle_call({load, <<Bin/binary>>}, _, S) ->
  try erlang:binary_to_term(public_key:decrypt_private(base64:decode(Bin), S#state.private)) of
    {utsession, M, RT, TTL} ->
      {reply, {ok, load}, S#state{data = {utsession, M, RT, TTL}}};
    E ->
      {reply, {error, baddata, E}, S}
  {reply, ok, S#state{data = utsession_struct:kset(Key, Value, S#state.data)}};
handle_call({load, <<Sign/binary>>, <<Data/binary>>}, _, S) ->
  try crypto:verify(ecdsa, sha256, Data, Sign, [S#state.public, prime256v1]) of
    true ->
      {reply, ok, S#state{data = erlang:binary_to_term(Data)}};
    false ->
      {reply, {error, baddata}, S}
  catch
    error:{error, {"pkey.c", _}, "Couldn't get the result"} ->
      {reply, {error, baddata}, S};
    error:function_clause ->
      {reply, {error, badkey}, S};
    C:E ->
      {reply, {tryerror, {C, E}}, S}
      {reply, {error, {C, E}}, S}
  end;
handle_call({keys, get}, _, S) ->
  {reply, {S#state.public, S#state.private}, S};
handle_call(store, _, S) ->
  try public_key:encrypt_public(erlang:term_to_binary(S#state.data), S#state.public) of
    <<Bin/binary>> ->
      {reply, {ok, base64:encode(Bin)}, S}
  Data = erlang:term_to_binary(S#state.data),
  try crypto:sign(ecdsa, sha256, Data, [S#state.private, prime256v1]) of
    <<Sign/binary>> ->
      {reply, {Sign, Data}, S}
  catch
    error:function_clause ->
      {reply, {error, badkey}, S};
    C:E ->
      {reply, {error, {C, E}}, S}
  end;


@@ 181,21 145,23 @@ handle_call({rt, get}, _, S) ->
handle_call({rt, update, RT}, _, S) ->
  case utsession_struct:rt_validate(RT, S#state.data) of
    true ->
      {reply, {ok, set}, S#state{data = utsession_struct:rt_update(RT, S#state.data)}};
      {reply, ok, S#state{data = utsession_struct:rt_update(RT, S#state.data)}};
    false ->
      {reply, {error, baddata}, S}
  end;
handle_call(sign, _, S) ->
  {reply, crypto:sign(ecdsa, sha256, erlang:term_to_binary(S#state.data), [S#state.private, prime256v1]), S};
handle_call({ttl, get}, _, S) ->
  {reply, utsession_struct:ttl(S#state.data), S};
handle_call({ttl, update}, _, S) ->
  {reply, {ok, set}, S#state{data = utsession_struct:ttl_update(S#state.data)}};
  {reply, ok, S#state{data = utsession_struct:ttl_update(S#state.data)}};
handle_call({ttl, validate}, _, S) ->
  {reply, utsession_struct:ttl_validate(S#state.data), S};
handle_call(_Request, _From, State) ->
  {reply, ignored, State}.

handle_cast(init, #state{data = <<Bin/binary>>} = S) ->
  {reply, {ok, load}, S_} = handle_call({load, Bin}, none, S),
  {reply, ok, S_} = handle_call({load, Bin}, none, S),
  {noreply, S_};
handle_cast(_Msg, State) ->
  {noreply, State}.

M test/actor_test.erl => test/actor_test.erl +29 -44
@@ 7,68 7,53 @@
qdate_start_test() ->
  qdate:start().

mtable_start_test() ->
  ?assertEqual(undefined, ets:info(utsession)),
  ?assertEqual(ok, utsession:mtable_start()),
  ?assertNotEqual(undefined, ets:info(utsession)).
generate_keys_test() ->
  {<<Public/binary>>, <<Priv/binary>>} = utsession:generate_keys(),
  {<<Public2/binary>>, <<Priv2/binary>>} = utsession:generate_keys(),
  ?assertNotEqual(Public, Public2),
  ?assertNotEqual(Priv, Priv2).

mtable_load_test() ->
  N = <<"test-public">>,
  ?assertNotEqual(undefined, ets:info(utsession)),
  ?assertEqual({error, not_found}, utsession:mtable_get(N)),
  ?assertEqual(ok, utsession:mtable_load_file(N, ?PUBLIC)),
  ?assertMatch({ok, _}, utsession:mtable_get(N)).
start_stop_without_keys_test() ->
  {ok, PID} = utsession:start(),
  utsession:stop(PID).

start_stop_test() ->
  {ok, PID} = utsession:start(),
  {ok, PID} = utsession:start(utsession:generate_keys()),
  utsession:stop(PID).
  
store_error_test() ->
  {ok, PID} = utsession:start(),
  ?assertEqual({error, badkey}, utsession:store(PID)),
keys_test() ->
  {Public, Private} = utsession:generate_keys(),
  {ok, PID} = utsession:start({Public, Private}),
  ?assertEqual({Public, Private}, utsession:keys_get(PID)),
  utsession:stop(PID).

data_set_test() ->
  {ok, PID} = utsession:start(),
  ?assertEqual({ok, set}, utsession:data_set(PID, <<"name">>, <<"test">>)),
  ?assertEqual(<<"test">>, utsession:data_get(PID, <<"name">>)),
  ?assertEqual({ok, set}, utsession:data_set(PID, <<"@">>, <<"t@t.com">>)),
  ?assertEqual(<<"t@t.com">>, utsession:data_get(PID, <<"@">>)),
  ?assertEqual(ok, utsession:data_set(<<"name">>, <<"test">>, PID)),
  ?assertEqual(<<"test">>, utsession:data_get(<<"name">>, PID)),
  ?assertEqual(ok, utsession:data_set(<<"@">>, <<"t@t.com">>, PID)),
  ?assertEqual(<<"t@t.com">>, utsession:data_get(<<"@">>, PID)),
  ?assertEqual(#{<<"name">> => <<"test">>, <<"@">> => <<"t@t.com">>}, utsession:data_get(PID)),
  ?assertEqual({ok, set}, utsession:data_set(PID, #{<<"d">> => 0})),
  ?assertEqual(ok, utsession:data_set(#{<<"d">> => 0}, PID)),
  ?assertEqual(#{<<"d">> => 0}, utsession:data_get(PID)),
  ?assertEqual({ok, del}, utsession:data_del(PID, <<"d">>)),
  ?assertEqual(ok, utsession:data_del(<<"d">>, PID)),
  ?assertEqual(#{}, utsession:data_get(PID)),
  utsession:stop(PID).

merge_test() ->
  {ok, PID} = utsession:start(),
  ?assertEqual({ok, set}, utsession:data_set(PID, <<"name">>, <<"test">>)),
  ?assertEqual({ok, merge}, utsession:data_merge(PID, #{<<"@">> => <<"t@t.com">>})),
  ?assertEqual(ok, utsession:data_set(<<"name">>, <<"test">>, PID)),
  ?assertEqual(ok, utsession:data_merge(#{<<"@">> => <<"t@t.com">>}, PID)),
  ?assertEqual(#{<<"name">> => <<"test">>, <<"@">> => <<"t@t.com">>}, utsession:data_get(PID)),
  utsession:stop(PID).

big_test() ->
  Options = [
             {private, {file, private, ?PRIVATE}},
             {public, {file, public, ?PUBLIC}}
            ],
  {ok, PID} = utsession:start(Options),
  ?assertEqual({ok, set}, utsession:data_set(PID, <<"name">>, <<"test">>)),
  ?assertEqual(<<"test">>, utsession:data_get(PID, <<"name">>)),
store_load_test() ->
  {ok, PID} = utsession:start(),
  ?assertEqual(ok, utsession:data_set(<<"name">>, <<"test">>, PID)),
  ?assertEqual(#{<<"name">> => <<"test">>}, utsession:data_get(PID)),
  ?assertEqual(false, utsession:ttl_validate(PID)),
  ?assertEqual({ok, set}, utsession:ttl_update(PID)),
  ?assertEqual(true, utsession:ttl_validate(PID)),
  {ok, Data} = utsession:store(PID),
  ?assertNotEqual(none, utsession:rt(PID)),
  {<<Sign/binary>>, <<Data/binary>>} = utsession:store(PID),
  {ok, NID} = utsession:start(utsession:keys_get(PID)),
  ?assertEqual(ok, utsession:load(Sign, Data, NID)),
  ?assertEqual(utsession:data_get(PID), utsession:data_get(NID)),
  utsession:stop(PID),
  {ok, PID_} = utsession:start(Options),
  ?assertEqual({error, baddata}, utsession:load(PID_, base64:encode(<<"some">>))),
  ?assertEqual({ok, load}, utsession:load(PID_, Data)),
  ?assertEqual(true, utsession:ttl_validate(PID_)),  
  ?assertEqual(<<"test">>, utsession:data_get(PID_, <<"name">>)),
  utsession:stop(PID_),
  {ok, PID__} = utsession:start(),
  ?assertEqual({error, badkey}, utsession:load(PID__, Data)),
  utsession:stop(PID__).
  utsession:stop(NID).