2020 * =========================LICENSE_END==================================
2121 */
2222
23+ import com .google .common .collect .ImmutableSet ;
2324import com .google .common .collect .Lists ;
2425import com .google .common .primitives .UnsignedInteger ;
26+ import org .xrpl .xrpl4j .codec .addresses .exceptions .DecodeException ;
2527import org .xrpl .xrpl4j .codec .addresses .exceptions .EncodeException ;
2628import org .xrpl .xrpl4j .codec .addresses .exceptions .EncodingFormatException ;
2729
@@ -36,6 +38,11 @@ public class SeedCodec {
3638
3739 private static final SeedCodec INSTANCE = new SeedCodec ();
3840
41+ // A 16-byte ED25519 or SECP256K1 seed Base58Check-encodes to 31 or 29 characters, respectively; a 32-byte
42+ // SECP256K1 seed (used only for ElGamal seeds; see #encodeSeed) encodes to 51 characters. No other length is
43+ // decodable -- e.g. a 32-byte ED25519 seed would encode to 53 characters, but #encodeSeed refuses to produce one.
44+ private static final ImmutableSet <Integer > VALID_SEED_LENGTHS = ImmutableSet .of (29 , 31 , 51 );
45+
3946 public static SeedCodec getInstance () {
4047 return INSTANCE ;
4148 }
@@ -52,6 +59,24 @@ public static SeedCodec getInstance() {
5259 public Decoded decodeSeed (final String base58EncodedSeed ) throws EncodingFormatException {
5360 Objects .requireNonNull (base58EncodedSeed );
5461
62+ if (!VALID_SEED_LENGTHS .contains (base58EncodedSeed .length ())) {
63+ throw new DecodeException (
64+ String .format ("Invalid seed length %s; expected %s." , base58EncodedSeed .length (), VALID_SEED_LENGTHS )
65+ );
66+ }
67+
68+ // A 32-byte secp256k1 seed (used only for ElGamal seeds; see #encodeSeed) Base58Check-encodes to 51 characters,
69+ // versus 29 for the standard 16-byte payload. Detect that length here since AddressBase58.decode needs the
70+ // expected payload size up front to pick the right branch.
71+ if (base58EncodedSeed .length () == 51 ) {
72+ return AddressBase58 .decode (
73+ base58EncodedSeed ,
74+ Lists .newArrayList (KeyType .SECP256K1 ),
75+ Lists .newArrayList (Version .FAMILY_SEED ),
76+ Optional .of (UnsignedInteger .valueOf (32 ))
77+ );
78+ }
79+
5580 return AddressBase58 .decode (
5681 base58EncodedSeed ,
5782 Lists .newArrayList (KeyType .ED25519 , KeyType .SECP256K1 ),
@@ -72,11 +97,22 @@ public String encodeSeed(final UnsignedByteArray entropy, final KeyType type) {
7297 Objects .requireNonNull (entropy );
7398 Objects .requireNonNull (type );
7499
75- if (entropy .getUnsignedBytes ().size () != 16 ) {
76- throw new EncodeException ("entropy must have length 16." );
100+ if (entropy .getUnsignedBytes ().size () != 16 && entropy .getUnsignedBytes ().size () != 32 ) {
101+ throw new EncodeException ("entropy must have length 16 or 32." );
102+ }
103+
104+ // 32-byte entropy exists only to support ElGamal secp256k1 seeds (see Seed#elGamalSecp256k1SeedFromEntropy).
105+ // Encoding 32 bytes under any non-secp256k1 prefix yields a seed that decodeSeed cannot decode -- e.g. the ED25519
106+ // prefix produces a 53-character seed that matches neither the 51-character secp256k1 branch nor the 16-byte
107+ // fallback -- so it would be silently unrecoverable. Gate on SECP256K1 (rather than excluding ED25519) so a
108+ // future KeyType with 32-byte entropy is rejected by default rather than mistakenly accepted.
109+ if (entropy .getUnsignedBytes ().size () == 32 && !type .equals (KeyType .SECP256K1 )) {
110+ throw new EncodeException ("32-byte entropy is only supported for SECP256K1 seeds, but was " + type + "." );
77111 }
78112
79113 Version version = type .equals (KeyType .ED25519 ) ? Version .ED25519_SEED : Version .FAMILY_SEED ;
80- return AddressBase58 .encode (entropy , Lists .newArrayList (version ), UnsignedInteger .valueOf (16 ));
114+ return AddressBase58 .encode (
115+ entropy , Lists .newArrayList (version ), UnsignedInteger .valueOf (entropy .getUnsignedBytes ().size ())
116+ );
81117 }
82118}
0 commit comments