Add keypair encoding, fix seckey size checks #57

Merged
uwiger merged 2 commits from uw-account_seckey into master 2026-03-30 05:26:10 +09:00
2 changed files with 98 additions and 7 deletions
Showing only changes of commit 4f97dd1bd1 - Show all commits

View File

@ -13,6 +13,13 @@
safe_decode/2, safe_decode/2,
byte_size_for_type/1]). 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, -export_type([encoded/0,
known_type/0]). known_type/0]).
@ -53,14 +60,66 @@
-type payload() :: binary(). -type payload() :: binary().
-type encoded() :: 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(BASE58, 1).
-define(BASE64, 2). -define(BASE64, 2).
-spec encode_keypair(keypair()) -> encoded_keypair().
encode_keypair(#{public := Pub, secret := Sec}) ->
case Sec of
<<Seed:32/binary, Pub1:32/binary>> 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, <<Seed:32/binary, Pub:32/binary>>} ->
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(). -spec encode(known_type(), payload() | gmser_id:id()) -> encoded().
encode(id_hash, Payload) -> encode(id_hash, Payload) ->
{IdType, Val} = gmser_id:specialize(Payload), {IdType, Val} = gmser_id:specialize(Payload),
encode(id2type(IdType), Val); encode(id2type(IdType), Val);
encode(Type, Payload) -> 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), Pfx = type2pfx(Type),
Enc = case type2enc(Type) of Enc = case type2enc(Type) of
?BASE58 -> base58_check(Payload); ?BASE58 -> base58_check(Payload);
@ -68,6 +127,7 @@ encode(Type, Payload) ->
end, end,
<<Pfx/binary, "_", Enc/binary>>. <<Pfx/binary, "_", Enc/binary>>.
-spec decode(binary()) -> {known_type(), payload()}. -spec decode(binary()) -> {known_type(), payload()}.
decode(Bin0) -> decode(Bin0) ->
case split(Bin0) of case split(Bin0) of
@ -83,6 +143,13 @@ decode(Bin0) ->
erlang:error(missing_prefix) erlang:error(missing_prefix)
end. 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) -> type_size_check(Type, Bin) ->
case byte_size_for_type(Type) of case byte_size_for_type(Type) of
not_applicable -> ok; not_applicable -> ok;

View File

@ -87,15 +87,16 @@ encode_decode_test_(Types) ->
lists:foreach( lists:foreach(
fun({_Type, not_applicable}) -> ok; fun({_Type, not_applicable}) -> ok;
({Type, ByteSize}) -> ({Type, ByteSize}) ->
CheckIlligalSize = CheckIllegalSize =
fun(S) -> fun(S) ->
Key = <<42:S/unit:8>>, 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) {error, invalid_encoding} = ?TEST_MODULE:safe_decode(Type, EncodedKey)
end, end,
CheckIlligalSize(0), CheckIllegalSize(0),
CheckIlligalSize(ByteSize - 1), CheckIllegalSize(ByteSize - 1),
CheckIlligalSize(ByteSize + 1) CheckIllegalSize(ByteSize + 1)
end, end,
Types) Types)
end end
@ -163,7 +164,30 @@ encode_decode_test_(Types) ->
end, Types) end, Types)
end, end,
Bins) 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(),
<<Seed:32/binary, Pub:32/binary>> = 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() -> known_types() ->