diff --git a/src/gmser_api_encoder.erl b/src/gmser_api_encoder.erl index 4b1eedc..0a123c1 100644 --- a/src/gmser_api_encoder.erl +++ b/src/gmser_api_encoder.erl @@ -13,8 +13,15 @@ safe_decode/2, byte_size_for_type/1]). +-export([encode_keypair/1, + safe_decode_keypair/1]). + +-ifdef(TEST). +-export([encode_/2]). %% Encode without size checks +-endif. + -export_type([encoded/0, - known_type/0]). + known_type/0]). -type known_type() :: key_block_hash | micro_block_hash @@ -53,14 +60,66 @@ -type payload() :: binary(). -type encoded() :: binary(). +-type keypair() :: #{public := <<_:(32*8)>>, secret := <<_:(64*8)>>}. +-type encoded_keypair() :: #{binary() => binary()}. + +-export_type([ keypair/0 + , encoded_keypair/0 ]). + -define(BASE58, 1). -define(BASE64, 2). +-spec encode_keypair(keypair()) -> encoded_keypair(). +encode_keypair(#{public := Pub, secret := Sec}) -> + case Sec of + <> when Pub1 =:= Pub -> + #{ <<"pub">> => encode(account_pubkey, Pub) + , <<"priv">> => encode(account_seckey, Seed) }; + _ -> + erlang:error(invalid_keypair) + end. + +-spec safe_decode_keypair(encoded_keypair()) -> {'ok', keypair()} | {'error', atom()}. +safe_decode_keypair(#{<<"pub">> := EncPub, <<"priv">> := EncPriv}) -> + case safe_decode(account_pubkey, EncPub) of + {ok, Pub} -> + case safe_decode(account_seckey, EncPriv) of + {ok, Seed} when byte_size(Seed) =:= 32 -> + case enacl:sign_seed_keypair(Seed) of + #{public := Pub, secret := _} = KP -> + {ok, KP}; + _ -> + {error, illegal_encoding} + end; + {ok, <>} -> + case enacl:sign_seed_keypair(Seed) of + #{public := Pub} = KP -> + {ok, KP}; + _ -> + {error, illegal_encoding} + end; + {ok, _} -> + {error, illegal_encoding}; + {error, _} = Error1 -> + Error1 + end; + Error -> + Error + end. + -spec encode(known_type(), payload() | gmser_id:id()) -> encoded(). encode(id_hash, Payload) -> {IdType, Val} = gmser_id:specialize(Payload), encode(id2type(IdType), Val); encode(Type, Payload) -> + case type_size_check(Type, Payload) of + ok -> + encode_(Type, Payload); + {error, Reason} -> + erlang:error(Reason) + end. + +encode_(Type, Payload) -> Pfx = type2pfx(Type), Enc = case type2enc(Type) of ?BASE58 -> base58_check(Payload); @@ -68,6 +127,7 @@ encode(Type, Payload) -> end, <>. + -spec decode(binary()) -> {known_type(), payload()}. decode(Bin0) -> case split(Bin0) of @@ -83,6 +143,13 @@ decode(Bin0) -> erlang:error(missing_prefix) end. +type_size_check(account_seckey, Bin) -> + case byte_size(Bin) of + Sz when Sz =:= 32; Sz =:= 64 -> + ok; + _ -> + {error, incorrect_size} + end; type_size_check(Type, Bin) -> case byte_size_for_type(Type) of not_applicable -> ok; diff --git a/test/gmser_api_encoder_tests.erl b/test/gmser_api_encoder_tests.erl index 6ec7c74..9a85e05 100644 --- a/test/gmser_api_encoder_tests.erl +++ b/test/gmser_api_encoder_tests.erl @@ -87,15 +87,16 @@ encode_decode_test_(Types) -> lists:foreach( fun({_Type, not_applicable}) -> ok; ({Type, ByteSize}) -> - CheckIlligalSize = + CheckIllegalSize = fun(S) -> Key = <<42:S/unit:8>>, - EncodedKey = ?TEST_MODULE:encode(Type, Key), + ?assertError(incorrect_size, ?TEST_MODULE:encode(Type, Key)), + EncodedKey = ?TEST_MODULE:encode_(Type, Key), %% no size check {error, invalid_encoding} = ?TEST_MODULE:safe_decode(Type, EncodedKey) end, - CheckIlligalSize(0), - CheckIlligalSize(ByteSize - 1), - CheckIlligalSize(ByteSize + 1) + CheckIllegalSize(0), + CheckIllegalSize(ByteSize - 1), + CheckIllegalSize(ByteSize + 1) end, Types) end @@ -163,7 +164,30 @@ encode_decode_test_(Types) -> end, Types) end, Bins) - end} + end}, + {"Encode/decode keypairs", + fun() -> + KP1 = enacl:sign_keypair(), + Enc1 = ?TEST_MODULE:encode_keypair(KP1), + {ok, KP1} = ?TEST_MODULE:safe_decode_keypair(Enc1), + KP2 = enacl:sign_keypair(), + Enc2 = ?TEST_MODULE:encode_keypair(KP2), + {ok, KP2} = ?TEST_MODULE:safe_decode_keypair(Enc2), + BadEnc = Enc1#{~"priv" => maps:get(~"priv", Enc2)}, + {error, illegal_encoding} = ?TEST_MODULE:safe_decode_keypair(BadEnc) + end + }, + {"Encode AND decode both 32-byte and 64-byte account_seckey", + fun() -> + %% Originally, we could encode a 64-byte seckey, but decode would fail. + #{public := Pub, secret := Sec} = enacl:sign_keypair(), + <> = Sec, + EncSeed = ?TEST_MODULE:encode(account_seckey, Seed), + EncSec = ?TEST_MODULE:encode(account_seckey, Sec), + {ok, Seed} = ?TEST_MODULE:safe_decode(account_seckey, EncSeed), + {ok, Sec} = ?TEST_MODULE:safe_decode(account_seckey, EncSec) + end + } ]. known_types() ->