From ebb9870ae71e205a71bed383448bc5c97398d57a Mon Sep 17 00:00:00 2001 From: Ferdinando Ametrano Date: Sun, 18 Oct 2026 22:43:28 +0200 Subject: [PATCH] Validate passphrase and identifier in mnemonics with ValueError SLIP-39 allows only printable ASCII in a passphrase, as shamir_mnemonic.generate_mnemonics checks; mnemonics now checks it too. The identifier and encrypted-secret passphrase checks were asserts, which python -O removes; they now raise ValueError. --- slip39/api.py | 10 ++++++---- slip39/api_test.py | 41 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/slip39/api.py b/slip39/api.py index 6e5131e..6694623 100644 --- a/slip39/api.py +++ b/slip39/api.py @@ -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, diff --git a/slip39/api_test.py b/slip39/api_test.py index 192f29c..0d8950b 100644 --- a/slip39/api_test.py +++ b/slip39/api_test.py @@ -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 @@ -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" )