diff --git a/ebin/hakuzaru.app b/ebin/hakuzaru.app index f457c88..b9f3753 100644 --- a/ebin/hakuzaru.app +++ b/ebin/hakuzaru.app @@ -4,5 +4,5 @@ {applications,[stdlib,kernel]}, {description,"Gajumaru interoperation library"}, {vsn,"0.6.0"}, - {modules,[hakuzaru,hz,hz_fetcher,hz_man,hz_sup]}, + {modules,[hakuzaru,hz,hz_fetcher,hz_grids,hz_man,hz_sup]}, {mod,{hakuzaru,[]}}]}. diff --git a/src/hz_key_master.erl b/src/hz_key_master.erl new file mode 100644 index 0000000..065d909 --- /dev/null +++ b/src/hz_key_master.erl @@ -0,0 +1,164 @@ +%%% @doc +%%% Key functions +%%% +%%% The main reason this is a module of its own is that in the original architecture +%%% it was a process rather than just a library of functions. Now that it exists, though, +%%% there is little motivation to cram everything here into the controller process's +%%% code. +%%% @end + +-module(hz_key_master). +-vsn("0.6.0"). + + +-export([make_key/2, encode/1, decode/1]). +-export([lcg/1]). +-include("gd.hrl"). + + +make_key("", <<>>) -> + Pair = #{public := Public} = ecu_eddsa:sign_keypair(), + ID = gmser_api_encoder:encode(account_pubkey, Public), + Name = binary_to_list(ID), + #key{name = Name, id = ID, pair = Pair}; +make_key("", Seed) -> + Pair = #{public := Public} = ecu_eddsa:sign_seed_keypair(Seed), + ID = gmser_api_encoder:encode(account_pubkey, Public), + Name = binary_to_list(ID), + #key{name = Name, id = ID, pair = Pair}; +make_key(Name, <<>>) -> + Pair = #{public := Public} = ecu_eddsa:sign_keypair(), + ID = gmser_api_encoder:encode(account_pubkey, Public), + #key{name = Name, id = ID, pair = Pair}; +make_key(Name, Seed) -> + Pair = #{public := Public} = ecu_eddsa:sign_seed_keypair(Seed), + ID = gmser_api_encoder:encode(account_pubkey, Public), + #key{name = Name, id = ID, pair = Pair}. + + +-spec encode(Secret) -> Phrase + when Secret :: binary(), + Phrase :: string(). +%% @doc +%% The encoding and decoding procesures are written to be able to handle any +%% width of bitstring or binary and a variable size dictionary. The magic numbers +%% 32, 4096 and 12 have been dropped in because currently these are known, but that +%% will change in the future if the key size or type changes. + +encode(Bin) -> + <> = Bin, + DictSize = 4096, + Words = read_words(), +% Width = chunksize(DictSize - 1, 2), + Width = 12, + Chunks = chunksize(Number, DictSize), + Binary = <>, + encode(Width, Binary, Words). + +encode(Width, Bits, Words) -> + CheckSum = checksum(Width, Bits), + encode(Width, <>, Words, []). + +encode(_, <<>>, _, Acc) -> + unicode:characters_to_list(lists:join(" ", lists:reverse(Acc))); +encode(Width, Bits, Words, Acc) -> + <> = Bits, + Word = lists:nth(I + 1, Words), + encode(Width, Rest, Words, [Word | Acc]). + + +-spec decode(Phrase) -> {ok, Secret} | {error, Reason} + when Phrase :: string(), + Secret :: binary(), + Reason :: bad_phrase | bad_word. +%% @doc +%% Reverses the encoded secret string back into its binary representation. + +decode(Encoded) -> + DictSize = 4096, + Words = read_words(), + Width = chunksize(DictSize - 1, 2), + decode(Width, Words, Encoded). + +decode(Width, Words, Encoded) when is_list(Encoded) -> + decode(Width, Words, list_to_binary(Encoded)); +decode(Width, Words, Encoded) -> + Split = string:lexemes(Encoded, " "), + decode(Width, Words, Split, <<>>). + +decode(Width, Words, [Word | Rest], Acc) -> + case find(Word, Words) of + {ok, N} -> decode(Width, Words, Rest, <>); + Error -> Error + end; +decode(Width, _, [], Acc) -> + sumcheck(Width, Acc). + + +chunksize(N, C) -> + chunksize(N, C, 0). + +chunksize(0, _, A) -> A; +chunksize(N, C, A) -> chunksize(N div C, C, A + 1). + + +read_words() -> + Path = filename:join([zx:get_home(), "priv", "words4096.txt"]), + {ok, Bin} = file:read_file(Path), + string:lexemes(Bin, "\n"). + + +find(Word, Words) -> + find(Word, Words, 0). + +find(Word, [Word | _], N) -> {ok, N}; +find(Word, [_ | Rest], N) -> find(Word, Rest, N + 1); +find(Word, [], _) -> {error, {bad_word, Word}}. + + +checksum(Width, Bits) -> + checksum(Width, Bits, 0). + +checksum(_, <<>>, Sum) -> + Sum; +checksum(Width, Bits, Sum) -> + <> = Bits, + checksum(Width, Rest, N bxor Sum). + + +sumcheck(Width, Bits) -> + <> = Bits, + case checksum(Width, Binary) =:= CheckSum of + true -> + <> = Binary, + {ok, <>}; + false -> + {error, bad_phrase} + end. + + + +-spec lcg(integer()) -> integer(). +%% A simple PRNG that fits into 32 bits and is easy to implement anywhere (Kotlin). +%% Specifically, it is a "linear congruential generator" of the Lehmer variety. +%% The constants used are based on recommendations from Park, Miller and Stockmeyer: +%% https://www.firstpr.com.au/dsp/rand31/p105-crawford.pdf#page=4 +%% +%% The input value should be between 1 and 2^31-1. +%% +%% The purpose of this PRNG is for password-based dictionary shuffling. + +lcg(N) -> + M = 16#7FFFFFFF, + A = 48271, + Q = 44488, % M div A + R = 3399, % M rem A + Div = N div Q, + Rem = N rem Q, + S = Rem * A, + T = Div * R, + Result = S - T, + case Result < 0 of + false -> Result; + true -> Result + M + end.