diff --git a/utils/vw/src/ebip39.erl b/utils/vw/src/ebip39.erl new file mode 100644 index 0000000..041986e --- /dev/null +++ b/utils/vw/src/ebip39.erl @@ -0,0 +1,96 @@ +%%% Author : Hans Svensson +%%% Description : Implement BIP0039 +%%% Created : 12 Jan 2022 by Hans Svensson +-module(ebip39). + +-ifdef(TEST). +-export([read_wordlist/0, gen_mnemonic/2]). +-endif. + +-export([generate_mnemonic/1, + mnemonic_to_seed/2, + validate_mnemonic/1]). + +-define(WORDLIST, "wordlist_en.txt"). + +-spec mnemonic_to_seed(Mnemonic :: binary(), + PassPhrase :: iodata()) -> <<_:512>>. +mnemonic_to_seed(Mnemonic, PassPhrase) -> + assert_mnemonic(Mnemonic), + {ok, BinSeed} = epbkdf2:pbkdf2(sha512, + Mnemonic, + iolist_to_binary(["mnemonic", PassPhrase]), + 2048), + BinSeed. + +-spec generate_mnemonic(Size :: non_neg_integer()) -> binary(). +generate_mnemonic(Size) -> + case Size rem 32 of + 0 -> gen_mnemonic(Size); + _ -> error({bad_mnemonic_size, Size, not_a_multiple_of_32}) + end. + +-spec validate_mnemonic(Mnemonic :: binary()) -> ok | {error, Reason :: atom()}. +validate_mnemonic(Mnemonic) -> + Table = word_table(), + Ixs = [ maps:get(W, Table) || W <- string:lexemes(Mnemonic, " ") ], + Size = length(Ixs) * 11, + CSize = Size rem 32, + SSize = (Size - CSize) div 8, + <> = << <> || Ix <- Ixs >>, + case crypto:hash(sha256, Seed) of + <> -> ok; + _ -> {error, checksum_error} + end. + +%% --- Internal functions +assert_mnemonic(Mnemonic) -> + case validate_mnemonic(Mnemonic) of + ok -> ok; + {error, R} -> error({bad_mnemonic_string, R}) + end. + + +gen_mnemonic(Size) -> + gen_mnemonic(Size, crypto:strong_rand_bytes(Size div 8)). + +gen_mnemonic(Size, Seed) -> + CSize = Size div 32, + <> = crypto:hash(sha256, Seed), + Table = ix_table(), + Words = [ maps:get(G, Table) || <> <= <> ], + iolist_to_binary(lists:join(<<" ">>, Words)). + +word_table() -> + Words = read_wordlist(), + maps:from_list(lists:zip(Words, lists:seq(0, 2047))). + +ix_table() -> + Words = read_wordlist(), + maps:from_list(lists:zip(lists:seq(0, 2047), Words)). + +read_wordlist() -> + File = filename:join(code:priv_dir(ebip39), ?WORDLIST), + case file:read_file(File) of + {ok, BinData} -> + read_wordlist(BinData); + {error, enotdir} -> %% In escript? + try + Escript = escript:script_name(), + {ok, Sections} = escript:extract(Escript, []), + Archive = proplists:get_value(archive, Sections), + FileName = filename:join([ebip39, priv, ?WORDLIST]), + case zip:extract(Archive, [{file_list, [FileName]}, memory]) of + {ok, [{_, BinData}]} -> read_wordlist(BinData); + _ -> error(wordlist_not_found) + end + catch _E:_R:_S -> + error(wordlist_not_found) + end + end. + +read_wordlist(BinData) -> + Words = string:lexemes(BinData, "\n"), + 2048 = length(Words), + Words. + diff --git a/utils/vw/src/epbkdf2.erl b/utils/vw/src/epbkdf2.erl new file mode 100644 index 0000000..96114d2 --- /dev/null +++ b/utils/vw/src/epbkdf2.erl @@ -0,0 +1,55 @@ +%%% File : epbkdf2.erl +%%% Author : Hans Svensson +%%% Description : Simplistic implementation of PBKDF2 on top of `crypto` +%%% Created : 12 Jan 2022 by Hans Svensson +-module(epbkdf2). + +-export([pbkdf2/4, + pbkdf2/5, + hmac/3]). + +-spec pbkdf2(HMacHash :: crypto:hmac_hash_algorithm(), + Pwd :: binary(), + Salt :: binary(), + Iterations :: non_neg_integer()) -> {ok, Key :: binary()}. +pbkdf2(HMacHash, Password, Salt, Iterations) -> + pbkdf2(HMacHash, Password, Salt, Iterations, 64). + +-spec pbkdf2(HMacHash :: crypto:hmac_hash_algorithm(), + Pwd :: binary(), + Salt :: binary(), + Iterations :: non_neg_integer(), + DerivedLength :: non_neg_integer()) -> {ok, Key :: binary()}. +pbkdf2(HMacHash, Password, Salt, Iterations, DerivedLength) -> + Bin = pbkdf2(HMacHash, Password, Salt, Iterations, DerivedLength, 1, []), + {ok, Bin}. + +-spec hmac(HMacHash :: crypto:hmac_hash_algorithm(), + Key :: binary(), + Data :: binary()) -> binary(). +hmac(HMacHash, Key, Data) -> + HMAC = crypto:mac_init(hmac, HMacHash, Key), + HMAC1 = crypto:mac_update(HMAC, Data), + crypto:mac_final(HMAC1). + +%% --- internal functions + +pbkdf2(HMacHash, Password, Salt, Iterations, DerivedLength, BlockIndex, Acc) -> + case iolist_size(Acc) > DerivedLength of + true -> + <> = iolist_to_binary(lists:reverse(Acc)), + Bin; + false -> + Block = pbkdf2(HMacHash, Password, Salt, Iterations, BlockIndex, 1, <<>>, <<>>), + pbkdf2(HMacHash, Password, Salt, Iterations, DerivedLength, BlockIndex + 1, [Block | Acc]) + end. + +pbkdf2(_HMacHash, _Password, _Salt, Iterations, _BlockIndex, Iteration, _Prev, Acc) + when Iteration > Iterations -> + Acc; +pbkdf2(HMacHash, Password, Salt, Iterations, BlockIndex, 1, _Prev, _Acc) -> + InitialBlock = hmac(HMacHash, Password, <>), + pbkdf2(HMacHash, Password, Salt, Iterations, BlockIndex, 2, InitialBlock, InitialBlock); +pbkdf2(HMacHash, Password, Salt, Iterations, BlockIndex, Iteration, Prev, Acc) -> + Next = hmac(HMacHash, Password, Prev), + pbkdf2(HMacHash, Password, Salt, Iterations, BlockIndex, Iteration + 1, Next, crypto:exor(Next, Acc)).