Skip to content

Commit 2191ccd

Browse files
committed
Support for extendable flag
1 parent da709e7 commit 2191ccd

File tree

8 files changed

+512
-131
lines changed

8 files changed

+512
-131
lines changed

GNUmakefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ else
6464
endif
6565

6666
# To see all pytest output, uncomment --capture=no, ...
67-
PYTESTOPTS = --capture=no --log-cli-level=WARNING # --doctest-modules
67+
PYTESTOPTS = -v --capture=no --log-cli-level=WARNING # --doctest-modules
6868

6969
PY3TEST = $(PY3) -m pytest $(PYTESTOPTS)
7070

pytest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[pytest]
22
testpaths = slip39
3-
addopts = -v --ignore-glob=**/__main__.py --ignore-glob=**/main.py --ignore-glob=**/ethereum.py --cov=slip39 --cov-config=.coveragerc
3+
addopts = -vv --capture=no --ignore-glob=**/__main__.py --ignore-glob=**/main.py --ignore-glob=**/ethereum.py --cov=slip39 --cov-config=.coveragerc

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ fpdf2 >=2.7.6,<3
77
#hdwallet >=2.3.0,<3
88
hdwallet @ git+https://github.com/pjkundert/python-hdwallet.git@python-slip39#egg=hdwallet
99
tabulate @ git+https://github.com/pjkundert/python-tabulate.git@python-slip39#egg=tabulate
10-
mnemonic >=0.2, <1
10+
mnemonic >=0.21, <1
1111
qrcode >=7.3
1212
shamir-mnemonic >=0.3.0,<0.4

slip39/api.py

Lines changed: 75 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
from collections import namedtuple
3232
from 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

3737
import hdwallet
3838
from hdwallet import cryptocurrencies
@@ -867,18 +867,24 @@ def random_secret(
867867

868868
def 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(
11101116
def 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+
11531200
def account(
11541201
master_secret: Union[str,bytes],
11551202
crypto: Optional[str] = None, # default 'ETH'

slip39/api_passphrase_test.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def test_passphrase():
5555
group_threshold = group_threshold,
5656
groups = groups,
5757
master_secret = SEED_FF,
58+
extendable = False,
5859
)
5960
#print( json.dumps( details_nonpass.groups, indent=4 ))
6061
assert details_nonpass.groups == {
@@ -125,6 +126,7 @@ def test_passphrase():
125126
groups = groups,
126127
master_secret = SEED_FF,
127128
passphrase = badpass,
129+
extendable = False,
128130
)
129131
#print( json.dumps( details_badpass.groups, indent=4 ))
130132
assert details_badpass.groups == {
@@ -176,7 +178,8 @@ def test_passphrase():
176178
) == SEED_FF
177179

178180
# Mixing SLIP39 recovery groups should fail to recover, both without and with the password,
179-
# since SLIP39 confirms the digest of the recovered "encrypted" seed, before decryption.
181+
# since SLIP39 confirms the digest of the recovered "encrypted" seed, before decryption. This
182+
# was the default, before SLIP-39 added the concept of "extendable".
180183
with pytest.raises( shamir_mnemonic.utils.MnemonicError ):
181184
assert recover(
182185
details_nonpass.groups['Fam'][1][:2] + details_badpass.groups['Frens'][1][:3],

slip39/api_test.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,8 @@ def test_slip39_non_extendable_compatibility():
233233
"""Test that SLIP-39 non-extendable backup of a wallet generated by Trezor can be recevered"""
234234
# The 4th vector from https://github.com/trezor/python-shamir-mnemonic/blob/master/vectors.json
235235
mnemonics = [
236-
"shadow pistol academic always adequate wildlife fancy gross oasis cylinder mustang wrist rescue view short owner flip making coding armed",
237-
"shadow pistol academic acid actress prayer class unknown daughter sweater depict flip twice unkind craft early superior advocate guest smoking"
236+
"shadow pistol academic always adequate wildlife fancy gross oasis cylinder mustang wrist rescue view short owner flip making coding armed",
237+
"shadow pistol academic acid actress prayer class unknown daughter sweater depict flip twice unkind craft early superior advocate guest smoking"
238238
]
239239
account = Account( crypto="Bitcoin", format="legacy" )
240240
account.from_mnemonic( "\n".join(mnemonics), passphrase = 'TREZOR', path="m/" )
@@ -245,8 +245,8 @@ def test_slip39_extendable_trezor_compatibility():
245245
"""Test that SLIP-39 extendable backup of a wallet generated by Trezor can be recevered"""
246246
# The 43th vector from https://github.com/trezor/python-shamir-mnemonic/blob/master/vectors.json
247247
mnemonics = [
248-
"enemy favorite academic acid cowboy phrase havoc level response walnut budget painting inside trash adjust froth kitchen learn tidy punish",
249-
"enemy favorite academic always academic sniff script carpet romp kind promise scatter center unfair training emphasis evening belong fake enforce"
248+
"enemy favorite academic acid cowboy phrase havoc level response walnut budget painting inside trash adjust froth kitchen learn tidy punish",
249+
"enemy favorite academic always academic sniff script carpet romp kind promise scatter center unfair training emphasis evening belong fake enforce"
250250
]
251251
account = Account( crypto="Bitcoin", format="legacy" )
252252
account.from_mnemonic( "\n".join(mnemonics), passphrase = 'TREZOR', path="m/" )

0 commit comments

Comments
 (0)