3131from collections import namedtuple
3232from typing import Dict , List , Sequence , Tuple , Optional , Union , Callable
3333
34- from shamir_mnemonic import generate_mnemonics
35- from shamir_mnemonic .shamir import RANDOM_BYTES
34+ from shamir_mnemonic import EncryptedMasterSecret , split_ems
35+ from shamir_mnemonic .shamir import _random_identifier , RANDOM_BYTES
3636
3737import hdwallet
3838from hdwallet import cryptocurrencies
@@ -867,18 +867,24 @@ def random_secret(
867867
868868def stretch_seed_entropy ( entropy , n , bits , encoding = None ):
869869 """To support the generation of a number of Seeds, each subsequent seed *must* be independent of
870- the prior seed. The Seed Data supplied (ie. recovered from BIP/SLIP-39 Mnemonics, or from fixed/random
871- data) is of course unchanging for the subsequent seeds to be produced; only the "extra" Seed Entropy
872- is useful for producing multiple sequential Seeds. Returns the designated number of bits
873- (rounded up to bytes).
870+ the prior seed: thus, if a number of seeds are produced from the same entropy, it *must* extend
871+ beyond the amount used by the seed, so the additional entropy beyond the end of that used is
872+ stretched into the subsequent batch of 'bits' worth of entropy returned. So, for 128-bit or
873+ 256-bit seeds, supply entropy longer than this bit amount if more than one seed is to be
874+ enhanced using this entropy source.
875+
876+ The Seed Data supplied (ie. recovered from BIP/SLIP-39 Mnemonics, or from fixed/random data) is
877+ of course unchanging for the subsequent seeds to be produced; only the "extra" Seed Entropy is
878+ useful for producing multiple sequential Seeds. Returns the designated number of bits (rounded
879+ up to bytes).
874880
875881 If non-binary hex data is supplied, encoding should be 'hex_codec' (0-filled/truncated on the
876882 right up to the required number of bits); otherwise probably 'UTF-8' (and we'll always stretch
877883 other encoded Entropy, even for the first (ie. 0th) seed).
878884
879885 If binary data is supplied, it must be sufficient to provide the required number of bits for the
880886 first and subsequent Seeds (SHA-512 is used to stretch, so any encoded and stretched entropy
881- data will be sufficient)
887+ data will be sufficient) for 128- and 256-bit seeds.
882888
883889 """
884890 assert n == 0 or ( entropy and n >= 0 ), \
@@ -982,8 +988,8 @@ def create(
982988 using_bip39 : Optional [bool ] = None , # Produce wallet Seed from master_secret Entropy using BIP-39 generation
983989 iteration_exponent : int = 1 ,
984990 cryptopaths : Optional [Sequence [Union [str ,Tuple [str ,str ],Tuple [str ,str ,str ]]]] = None , # default: ETH, BTC at default path, format
985- strength : Optional [int ] = None , # Default: 128
986- extendable : bool = True ,
991+ strength : Optional [int ] = None , # Default: 128
992+ extendable : Optional [ Union [ bool , int ]] = None , # Default: True w/ random identifier
987993) -> Tuple [str ,int ,Dict [str ,Tuple [int ,List [str ]]], Sequence [Sequence [Account ]], bool ]:
988994 """Creates a SLIP-39 encoding for supplied master_secret Entropy, and 1 or more Cryptocurrency
989995 accounts. Returns the Details, in a form directly compatible with the layout.produce_pdf API.
@@ -1110,14 +1116,14 @@ def create(
11101116def mnemonics (
11111117 group_threshold : Optional [int ], # Default: 1/2 of groups, rounded up
11121118 groups : Sequence [Tuple [int , int ]],
1113- master_secret : Optional [Union [str , bytes ]] = None ,
1119+ master_secret : Optional [Union [bytes , EncryptedMasterSecret ]] = None ,
11141120 passphrase : Optional [Union [bytes ,str ]] = None ,
11151121 iteration_exponent : int = 1 ,
11161122 strength : int = BITS_DEFAULT ,
1117- extendable : bool = True ,
1123+ extendable : Optional [ Tuple [ bool , int ]] = None ,
11181124) -> List [List [str ]]:
1119- """Generate SLIP39 mnemonics for the supplied group_threshold of the given groups. Will generate a
1120- random master_secret, if necessary.
1125+ """Generate SLIP39 mnemonics for the supplied master_secret for group_threshold of the given
1126+ groups. Will generate a random master_secret, if necessary.
11211127
11221128 If you have BIP-39/SLIP-39 Mnemonic(s), use recovery.recover or .recover_bip39 first. To
11231129 "backup" a BIP-39 Mnemonic Phrase, you probably want to use .recover_bip39( ..., as_entropy=True
@@ -1126,30 +1132,71 @@ def mnemonics(
11261132 Later, supply these SLIP-39 Mnemonics to any of the .account... functions with using_bip39=True,
11271133 to derive the original BIP-39 wallets.
11281134
1135+ An encrypted master seed may be supplied, recovered from SLIP-39 mnemonics. This allows the
1136+ caller to convert an existing encrypted seed to produce another set of SLIP-39 Mnemonics. If
1137+ extendable, and the group_threshold, number of groups and group minimums are not changed, then
1138+ the result will be an extension of an existing set of SLIP-39 Mnemonics. Otherwise, it will be
1139+ a new (but incompatible) set of Mnemonics.
1140+
11291141 """
11301142 if master_secret is None :
1131- assert strength in BITS , f"Invalid { strength } -bit secret length specified"
1132- master_secret = random_secret ( strength // 8 )
1133- if len ( master_secret ) * 8 not in BITS :
1143+ master_secret = random_secret (( strength + 7 ) // 8 )
1144+
1145+ if isinstance ( master_secret , EncryptedMasterSecret ):
1146+ assert not passphrase , \
1147+ "No passphrase required/allowed for encrypted master seed"
1148+ encrypted_secret = master_secret
1149+ else :
1150+ if passphrase is None :
1151+ passphrase = ""
1152+ if isinstance ( passphrase , str ):
1153+ passphrase = passphrase .encode ( 'UTF-8' )
1154+ encrypted_secret = EncryptedMasterSecret .from_master_secret (
1155+ master_secret = master_secret ,
1156+ passphrase = passphrase ,
1157+ identifier = _random_identifier () if extendable in (None , False , True ) else extendable ,
1158+ extendable = False if extendable is False else True ,
1159+ iteration_exponent = iteration_exponent ,
1160+ )
1161+
1162+ if len ( encrypted_secret .ciphertext ) * 8 not in BITS :
11341163 raise ValueError (
1135- f"Only { commas ( BITS , final = 'and' )} -bit seeds supported; { len (master_secret )* 8 } -bit seed supplied" )
1136- if isinstance ( passphrase , str ):
1137- passphrase = passphrase .encode ( 'UTF-8' )
1164+ f"Only { commas ( BITS , final = 'and' )} -bit seeds supported; { len (encrypted_secret .ciphertext )* 8 } -bit seed supplied" )
11381165
1139- groups = list ( groups )
1140- if not group_threshold :
1141- group_threshold = math .ceil ( len ( groups ) * GROUP_THRESHOLD_RATIO )
1142- return generate_mnemonics (
1166+ return mnemonics_encrypted (
11431167 group_threshold = group_threshold ,
11441168 groups = groups ,
1145- master_secret = master_secret ,
1146- passphrase = passphrase or b"" , # python-shamir-mnemonic requires bytes
1147- extendable = False ,
1148- iteration_exponent = iteration_exponent ,
1149- extendable = extendable ,
1169+ encrypted_secret = encrypted_secret ,
11501170 )
11511171
11521172
1173+ def mnemonics_encrypted (
1174+ group_threshold : Optional [int ],
1175+ groups : Sequence [Tuple [int , int ]],
1176+ encrypted_secret : EncryptedMasterSecret ,
1177+ ) -> List [List [str ]]:
1178+ """Generate SLIP-39 mnemonics for the supplied encrypted_secret. To reliably generate mnemonics
1179+ to extend existing encrypted SLIP-39 gruop(s), supply an EncryptedMasterSecret with an
1180+ 'extendable' SLIP-39 identifier.
1181+
1182+ If the group threshold, count and group minimum requirements are consistent (just the group
1183+ size(s) are increased), then the Mnemonics will be an extension of the existing group(s) --
1184+ producing more potential recovery options for 1 or more group(s). Since SLIP-39 doesn't allow
1185+ "1 of X" groups (for anything other than "1 of 1"), you cannot extend existing "1 of 1" groups.
1186+
1187+ """
1188+ groups = list ( groups )
1189+ if not group_threshold :
1190+ group_threshold = math .ceil ( len ( groups ) * GROUP_THRESHOLD_RATIO )
1191+
1192+ grouped_shares = split_ems ( group_threshold , groups , encrypted_secret )
1193+ log .warning (
1194+ f"Generated { len (encrypted_secret .ciphertext )* 8 } -bit SLIP-39 Mnemonics w/ identifier { encrypted_secret .identifier } requiring { group_threshold } "
1195+ f" of { len (grouped_shares )} { '(extendable)' if encrypted_secret .extendable else '' } groups to recover" )
1196+
1197+ return [[share .mnemonic () for share in group ] for group in grouped_shares ]
1198+
1199+
11531200def account (
11541201 master_secret : Union [str ,bytes ],
11551202 crypto : Optional [str ] = None , # default 'ETH'
0 commit comments