Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions slip39/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1372,18 +1372,20 @@ def mnemonics(
master_secret = random_secret(( strength + 7 ) // 8 )

if isinstance( master_secret, EncryptedMasterSecret ):
assert not passphrase, \
"No passphrase required/allowed for encrypted master seed"
if passphrase:
raise ValueError( "No passphrase required/allowed for encrypted master seed" )
encrypted_secret = master_secret
else:
if passphrase is None:
passphrase = ""
if isinstance( passphrase, str ):
passphrase = passphrase.encode( 'UTF-8' )
if not all( 32 <= c <= 126 for c in passphrase ):
raise ValueError( "Passphrase must contain only printable ASCII characters (code points 32-126)" )
extendable = False if extendable is False else True
identifier = _random_identifier() if identifier is None else int( identifier )
assert isinstance( identifier, int) and 0 <= identifier < (1 << ID_LENGTH_BITS), \
"Identifier must be an {ID_LENGTH_BITS}-bit unsigned integer: {identifier}"
if not 0 <= identifier < (1 << ID_LENGTH_BITS):
raise ValueError( f"Identifier must be a {ID_LENGTH_BITS}-bit unsigned integer: {identifier}" )
encrypted_secret = EncryptedMasterSecret.from_master_secret(
master_secret = master_secret,
passphrase = passphrase,
Expand Down
41 changes: 40 additions & 1 deletion slip39/api_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
scrypt = None

import shamir_mnemonic
from shamir_mnemonic.constants import ID_LENGTH_BITS

from . import account, create, addresses, addressgroups, accountgroups, Account
from . import account, create, addresses, addressgroups, accountgroups, Account, mnemonics
from .recovery import recover

from .dependency_test import substitute, nonrandom_bytes, SEED_XMAS, SEED_ONES
Expand Down Expand Up @@ -630,3 +631,41 @@ def test_accountgroups():
))
# print( json.dumps( acctgrps, default=repr ))
assert len(acctgrps) == 4


def test_mnemonics_passphrase_printable_ascii():
"""SLIP-39 allows only printable ASCII (code points 32-126) in a passphrase."""
master_secret = b'\0' * 16
for passphrase in ( "café", b"\x1f", b"\x7f" ):
with pytest.raises( ValueError ):
mnemonics( 1, [(1, 1)], master_secret=master_secret, passphrase=passphrase )

[[mnemonic]] = mnemonics( 1, [(1, 1)], master_secret=master_secret, passphrase=" ~" )
assert shamir_mnemonic.combine_mnemonics( [mnemonic], b" ~" ) == master_secret


def test_mnemonics_identifier_range():
"""An identifier out of range raises ValueError, also under python -O."""
master_secret = b'\0' * 16
for identifier in ( -1, 1 << ID_LENGTH_BITS ):
for extendable in ( False, True ):
with pytest.raises( ValueError ):
mnemonics( 1, [(1, 1)], master_secret=master_secret, passphrase="x",
identifier=identifier, extendable=extendable )

[[mnemonic]] = mnemonics( 1, [(1, 1)], master_secret=master_secret, passphrase="x",
identifier=(1 << ID_LENGTH_BITS) - 1, extendable=False )
assert shamir_mnemonic.combine_mnemonics( [mnemonic], b"x" ) == master_secret


def test_mnemonics_encrypted_secret_passphrase():
"""An encrypted master secret is already protected by its passphrase; another one is refused."""
encrypted_secret = shamir_mnemonic.EncryptedMasterSecret.from_master_secret(
master_secret = b'\0' * 16,
passphrase = b"x",
identifier = 0,
extendable = True,
iteration_exponent = 1,
)
with pytest.raises( ValueError ):
mnemonics( 1, [(1, 1)], master_secret=encrypted_secret, passphrase="y" )