From 1116e99829e6cf5522f7c74d1cc9c49f9c5ef254 Mon Sep 17 00:00:00 2001 From: alvroble <50918598+alvroble@users.noreply.github.com> Date: Wed, 9 Jul 2025 17:40:59 +0200 Subject: [PATCH 1/2] Update test_slip39 with bip32 recover * Add xprvs as per shamir spec --- tests/tests/test_slip39.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/tests/test_slip39.py b/tests/tests/test_slip39.py index 088257d..131c19d 100644 --- a/tests/tests/test_slip39.py +++ b/tests/tests/test_slip39.py @@ -262,6 +262,7 @@ def test_recover(self): "duckling enlarge academic academic agency result length solution fridge kidney coal piece deal husband erode duke ajar critical decision keyboard" ], "bb54aac4b89dc868ba37d9cc21b2cece", + "xprv9s21ZrQH143K4QViKpwKCpS2zVbz8GrZgpEchMDg6KME9HZtjfL7iThE9w5muQA4YPHKN1u5VM1w8D4pvnjxa2BmpGMfXr7hnRrRHZ93awZ", ], [ "4. Basic sharing 2-of-3 (128 bits)", @@ -270,6 +271,7 @@ def test_recover(self): "shadow pistol academic acid actress prayer class unknown daughter sweater depict flip twice unkind craft early superior advocate guest smoking", ], "b43ceb7e57a0ea8766221624d01b0864", + "xprv9s21ZrQH143K2nNuAbfWPHBtfiSCS14XQgb3otW4pX655q58EEZeC8zmjEUwucBu9dPnxdpbZLCn57yx45RBkwJHnwHFjZK4XPJ8SyeYjYg", ], [ "17. Threshold number of groups and members in each group (128 bits, case 1)", @@ -281,6 +283,7 @@ def test_recover(self): "eraser senior decision smug corner ruin rescue cubic angel tackle skin skunk program roster trash rumor slush angel flea amazing", ], "7c3397a292a5941682d7a4ae2d898d11", + "xprv9s21ZrQH143K3dzDLfeY3cMp23u5vDeFYftu5RPYZPucKc99mNEddU4w99GxdgUGcSfMpVDxhnR1XpJzZNXRN1m6xNgnzFS5MwMP6QyBRKV", ], [ "18. Threshold number of groups and members in each group (128 bits, case 2)", @@ -290,6 +293,7 @@ def test_recover(self): "eraser senior decision scared cargo theory device idea deliver modify curly include pancake both news skin realize vitamins away join", ], "7c3397a292a5941682d7a4ae2d898d11", + "xprv9s21ZrQH143K3dzDLfeY3cMp23u5vDeFYftu5RPYZPucKc99mNEddU4w99GxdgUGcSfMpVDxhnR1XpJzZNXRN1m6xNgnzFS5MwMP6QyBRKV", ], [ "19. Threshold number of groups and members in each group (128 bits, case 3)", @@ -298,6 +302,7 @@ def test_recover(self): "eraser senior acrobat romp bishop medical gesture pumps secret alive ultimate quarter priest subject class dictate spew material endless market", ], "7c3397a292a5941682d7a4ae2d898d11", + "xprv9s21ZrQH143K3dzDLfeY3cMp23u5vDeFYftu5RPYZPucKc99mNEddU4w99GxdgUGcSfMpVDxhnR1XpJzZNXRN1m6xNgnzFS5MwMP6QyBRKV", ], [ "20. Valid mnemonic without sharing (256 bits)", @@ -305,6 +310,7 @@ def test_recover(self): "theory painting academic academic armed sweater year military elder discuss acne wildlife boring employer fused large satoshi bundle carbon diagnose anatomy hamster leaves tracks paces beyond phantom capital marvel lips brave detect luck" ], "989baf9dcaad5b10ca33dfd8cc75e42477025dce88ae83e75a230086a0e00e92", + "xprv9s21ZrQH143K41mrxxMT2FpiheQ9MFNmWVK4tvX2s28KLZAhuXWskJCKVRQprq9TnjzzzEYePpt764csiCxTt22xwGPiRmUjYUUdjaut8RM", ], [ "23. Basic sharing 2-of-3 (256 bits)", @@ -313,6 +319,7 @@ def test_recover(self): "humidity disease academic agency actress jacket gross physics cylinder solution fake mortgage benefit public busy prepare sharp friar change work slow purchase ruler again tricycle involve viral wireless mixture anatomy desert cargo upgrade", ], "c938b319067687e990e05e0da0ecce1278f75ff58d9853f19dcaeed5de104aae", + "xprv9s21ZrQH143K3a4GRMgK8WnawupkwkP6gyHxRsXnMsYPTPH21fWwNcAytijtfyftqNfiaY8LgQVdBQvHZ9FBvtwdjC7LCYxjYruJFuLzyMQ", ], [ "36. Threshold number of groups and members in each group (256 bits, case 1)", @@ -324,6 +331,7 @@ def test_recover(self): "wildlife deal decision shadow analysis adjust bulb skunk muscle mandate obesity total guitar coal gravity carve slim jacket ruin rebuild ancestor numerous hour mortgage require herd maiden public ceiling pecan pickup shadow club", ], "5385577c8cfc6c1a8aa0f7f10ecde0a3318493262591e78b8c14c6686167123b", + "xprv9s21ZrQH143K2UspC9FRPfQC9NcDB4HPkx1XG9UEtuceYtpcCZ6ypNZWdgfxQ9dAFVeD1F4Zg4roY7nZm2LB7THPD6kaCege3M7EuS8v85c", ], [ "37. Threshold number of groups and members in each group (256 bits, case 2)", @@ -333,6 +341,7 @@ def test_recover(self): "wildlife deal decision smug ancestor genuine move huge cubic strategy smell game costume extend swimming false desire fake traffic vegan senior twice timber submit leader payroll fraction apart exact forward pulse tidy install", ], "5385577c8cfc6c1a8aa0f7f10ecde0a3318493262591e78b8c14c6686167123b", + "xprv9s21ZrQH143K2UspC9FRPfQC9NcDB4HPkx1XG9UEtuceYtpcCZ6ypNZWdgfxQ9dAFVeD1F4Zg4roY7nZm2LB7THPD6kaCege3M7EuS8v85c", ], [ "38. Threshold number of groups and members in each group (256 bits, case 3)", @@ -341,12 +350,17 @@ def test_recover(self): "wildlife deal acrobat romp anxiety axis starting require metric flexible geology game drove editor edge screw helpful have huge holy making pitch unknown carve holiday numb glasses survive already tenant adapt goat fangs", ], "5385577c8cfc6c1a8aa0f7f10ecde0a3318493262591e78b8c14c6686167123b", + "xprv9s21ZrQH143K2UspC9FRPfQC9NcDB4HPkx1XG9UEtuceYtpcCZ6ypNZWdgfxQ9dAFVeD1F4Zg4roY7nZm2LB7THPD6kaCege3M7EuS8v85c", ], ] - for test_name, mnemonics, expected in test_cases: + for test_name, mnemonics, expected_secret, expected_xprv in test_cases: share_set = ShareSet([Share.parse(m) for m in mnemonics]) + recovered_secret = share_set.recover(b"TREZOR") self.assertEqual( - share_set.recover(b"TREZOR"), unhexlify(expected), test_name + recovered_secret, unhexlify(expected_secret), test_name + ) + self.assertEqual( + HDKey.from_seed(recovered_secret).to_base58(), expected_xprv, test_name ) def test_split(self): From 463e74ce1673eedc4ce15cb1bed7418afcc644ac Mon Sep 17 00:00:00 2001 From: alvroble <50918598+alvroble@users.noreply.github.com> Date: Wed, 9 Jul 2025 18:02:00 +0200 Subject: [PATCH 2/2] Add warning comments * recover_mnemonic: future deletion as it's misleading * generate_shares: note on the implications of this method --- src/embit/slip39.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/embit/slip39.py b/src/embit/slip39.py index 86927b3..4fe6daa 100644 --- a/src/embit/slip39.py +++ b/src/embit/slip39.py @@ -320,8 +320,18 @@ def split_secret(cls, secret, k, n, randint=secure_randint): def generate_shares( cls, mnemonic, k, n, passphrase=b"", exponent=0, randint=secure_randint ): - """Takes a BIP39 mnemonic along with k, n, passphrase and exponent. - Returns a list of SLIP39 mnemonics, any k of of which, along with the passphrase, recover the secret + """ + Takes a BIP39 mnemonic along with k, n, passphrase and exponent. + Returns a list of SLIP39 mnemonics, any k of which, along with the passphrase, recover the secret. + + NOTE: This function is designed for creating NEW wallets from scratch using the BIP39 + mnemonic as the source entropy. It does NOT convert an existing BIP39-backed wallet + to SLIP39 format while preserving the same addresses. + + For existing BIP39 wallets, converting to SLIP39 while maintaining the same addresses + would require using the BIP39 seed (from mnemonic_to_seed) as input, which generates + 512-bit seeds requiring 59-word SLIP39 shares. This conversion is not supported by + embit and is not recommended. """ # convert mnemonic to a shared secret secret = mnemonic_to_bytes(mnemonic) @@ -352,7 +362,20 @@ def generate_shares( @classmethod def recover_mnemonic(cls, share_mnemonics, passphrase=b""): - """Recovers the BIP39 mnemonic from a bunch of SLIP39 mnemonics""" + """ + WARNING: This function will be removed in a future version as it is misleading. + + This function recovers the original BIP32 seed in BIP39 mnemonic format from + SLIP39 shares, but this is NOT the same as a proper BIP39 mnemonic recovery. + Using the returned mnemonic with standard BIP39 seed derivation (mnemonic_to_seed) + will generate a different seed than the original BIP32 seed, resulting in + a completely different wallet with different addresses. + + The function technically returns the correct raw seed data formatted as a BIP39 + mnemonic, but this creates confusion as users expect BIP39 mnemonics to work with + standard BIP39 derivation processes. This will cause users to lose access to their + original wallet addresses. + """ shares = [Share.parse(m) for m in share_mnemonics] share_set = ShareSet(shares) secret = share_set.recover(passphrase)