165 lines
4.8 KiB
Erlang
165 lines
4.8 KiB
Erlang
%%% @doc
|
|
%%% Key functions go here.
|
|
%%%
|
|
%%% 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(gmc_key_master).
|
|
-vsn("0.4.0").
|
|
|
|
|
|
-export([make_key/2, encode/1, decode/1]).
|
|
-export([lcg/1]).
|
|
-include("gmc.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) ->
|
|
<<Number:(32 * 8)>> = Bin,
|
|
DictSize = 4096,
|
|
Words = read_words(),
|
|
% Width = chunksize(DictSize - 1, 2),
|
|
Width = 12,
|
|
Chunks = chunksize(Number, DictSize),
|
|
Binary = <<Number:(Chunks * Width)>>,
|
|
encode(Width, Binary, Words).
|
|
|
|
encode(Width, Bits, Words) ->
|
|
CheckSum = checksum(Width, Bits),
|
|
encode(Width, <<CheckSum:Width, Bits/bitstring>>, Words, []).
|
|
|
|
encode(_, <<>>, _, Acc) ->
|
|
unicode:characters_to_list(lists:join(" ", lists:reverse(Acc)));
|
|
encode(Width, Bits, Words, Acc) ->
|
|
<<I:Width, Rest/bitstring>> = 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, <<Acc/bitstring, N:Width>>);
|
|
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) ->
|
|
<<N:Width, Rest/bitstring>> = Bits,
|
|
checksum(Width, Rest, N bxor Sum).
|
|
|
|
|
|
sumcheck(Width, Bits) ->
|
|
<<CheckSum:Width, Binary/bitstring>> = Bits,
|
|
case checksum(Width, Binary) =:= CheckSum of
|
|
true ->
|
|
<<N:(bit_size(Binary))>> = Binary,
|
|
{ok, <<N:(32 * 8)>>};
|
|
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.
|