From 9d0d1b3402bffc9f65a74de7fdb24d8862d9a652 Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Tue, 30 Apr 2024 20:26:46 +0200 Subject: [PATCH] feat(core): Support extendable backup flag in SLIP-39. --- core/.changelog.d/+slip39.added | 1 + core/embed/rust/src/storage/mod.rs | 1 + core/src/apps/common/mnemonic.py | 2 + core/src/apps/debug/load_device.py | 5 +- .../management/recovery_device/homescreen.py | 13 +- .../management/recovery_device/recover.py | 5 +- .../apps/management/reset_device/__init__.py | 3 + core/src/storage/device.py | 17 +- core/src/storage/recovery.py | 11 + core/src/trezor/crypto/slip39.py | 102 ++++++--- core/tests/slip39_vectors.py | 34 +++ core/tests/test_apps.cardano.address.py | 8 +- .../tests/test_apps.cardano.get_public_key.py | 8 +- .../test_apps.management.recovery_device.py | 5 + core/tests/test_trezor.crypto.slip39.py | 200 +++++++++--------- docs/core/misc/slip0039.md | 27 +-- poetry.lock | 111 ++++++++-- pyproject.toml | 2 +- 18 files changed, 393 insertions(+), 162 deletions(-) create mode 100644 core/.changelog.d/+slip39.added diff --git a/core/.changelog.d/+slip39.added b/core/.changelog.d/+slip39.added new file mode 100644 index 0000000000..0ed8594913 --- /dev/null +++ b/core/.changelog.d/+slip39.added @@ -0,0 +1 @@ +Support extendable backup flag in SLIP-39. diff --git a/core/embed/rust/src/storage/mod.rs b/core/embed/rust/src/storage/mod.rs index 96eab3df72..3358a03d3f 100644 --- a/core/embed/rust/src/storage/mod.rs +++ b/core/embed/rust/src/storage/mod.rs @@ -36,6 +36,7 @@ const INITIALIZED: u16 = FLAG_PUBLIC | APP_DEVICE | 0x0013; const SAFETY_CHECK_LEVEL: u16 = APP_DEVICE | 0x0014; const EXPERIMENTAL_FEATURES: u16 = APP_DEVICE | 0x0015; const HIDE_PASSPHRASE_FROM_HOST: u16 = APP_DEVICE | 0x0016; +const SLIP39_EXTENDABLE: u16 = APP_DEVICE | 0x0017; pub fn get_avatar_len() -> StorageResult { get_length(HOMESCREEN) diff --git a/core/src/apps/common/mnemonic.py b/core/src/apps/common/mnemonic.py index 9e6c037813..58239deff5 100644 --- a/core/src/apps/common/mnemonic.py +++ b/core/src/apps/common/mnemonic.py @@ -49,6 +49,7 @@ def get_seed(passphrase: str = "", progress_bar: bool = True) -> bytes: from trezor.crypto import slip39 identifier = storage_device.get_slip39_identifier() + extendable = storage_device.get_slip39_extendable() iteration_exponent = storage_device.get_slip39_iteration_exponent() if identifier is None or iteration_exponent is None: # Identifier or exponent expected but not found @@ -58,6 +59,7 @@ def get_seed(passphrase: str = "", progress_bar: bool = True) -> bytes: passphrase.encode(), iteration_exponent, identifier, + extendable, render_func, ) diff --git a/core/src/apps/debug/load_device.py b/core/src/apps/debug/load_device.py index 6f95575eca..c1e02ed11d 100644 --- a/core/src/apps/debug/load_device.py +++ b/core/src/apps/debug/load_device.py @@ -49,7 +49,9 @@ async def load_device(msg: LoadDevice) -> Success: secret = msg.mnemonics[0].encode() backup_type = BackupType.Bip39 else: - identifier, iteration_exponent, secret = slip39.recover_ems(mnemonics) + identifier, extendable, iteration_exponent, secret = slip39.recover_ems( + mnemonics + ) # this must succeed if the recover_ems call succeeded share = slip39.decode_mnemonic(mnemonics[0]) @@ -61,6 +63,7 @@ async def load_device(msg: LoadDevice) -> Success: raise ProcessError("Invalid group count") storage_device.set_slip39_identifier(identifier) + storage_device.set_slip39_extendable(extendable) storage_device.set_slip39_iteration_exponent(iteration_exponent) storage_device.store_mnemonic_secret( diff --git a/core/src/apps/management/recovery_device/homescreen.py b/core/src/apps/management/recovery_device/homescreen.py index 2691c6cbb0..478d895bee 100644 --- a/core/src/apps/management/recovery_device/homescreen.py +++ b/core/src/apps/management/recovery_device/homescreen.py @@ -118,9 +118,14 @@ async def _finish_recovery_dry_run(secret: bytes, backup_type: BackupType) -> Su is_slip39 = backup_types.is_slip39_backup_type(backup_type) # Check that the identifier and iteration exponent match as well if is_slip39: + if not backup_types.is_extendable_backup_type(backup_type): + result &= ( + storage_device.get_slip39_identifier() + == storage_recovery.get_slip39_identifier() + ) result &= ( - storage_device.get_slip39_identifier() - == storage_recovery.get_slip39_identifier() + storage_device.get_slip39_extendable() + == storage_recovery.get_slip39_extendable() ) result &= ( storage_device.get_slip39_iteration_exponent() @@ -149,11 +154,13 @@ async def _finish_recovery(secret: bytes, backup_type: BackupType) -> Success: ) if backup_type in (BackupType.Slip39_Basic, BackupType.Slip39_Advanced): identifier = storage_recovery.get_slip39_identifier() + extendable = storage_recovery.get_slip39_extendable() exponent = storage_recovery.get_slip39_iteration_exponent() - if identifier is None or exponent is None: + if identifier is None or extendable is None or exponent is None: # Identifier and exponent need to be stored in storage at this point raise RuntimeError storage_device.set_slip39_identifier(identifier) + storage_device.set_slip39_extendable(extendable) storage_device.set_slip39_iteration_exponent(exponent) storage_recovery.end_progress() diff --git a/core/src/apps/management/recovery_device/recover.py b/core/src/apps/management/recovery_device/recover.py index 7e43cb1ad4..e60d484cf6 100644 --- a/core/src/apps/management/recovery_device/recover.py +++ b/core/src/apps/management/recovery_device/recover.py @@ -41,13 +41,14 @@ def process_slip39(words: str) -> tuple[bytes | None, slip39.Share]: storage_recovery.set_slip39_group_count(share.group_count) storage_recovery.set_slip39_iteration_exponent(share.iteration_exponent) storage_recovery.set_slip39_identifier(share.identifier) + storage_recovery.set_slip39_extendable(share.extendable) storage_recovery.set_slip39_remaining_shares(share.threshold - 1, group_index) storage_recovery_shares.set(share.index, group_index, words) # if share threshold and group threshold are 1 # we can calculate the secret right away if share.threshold == 1 and share.group_threshold == 1: - _, _, secret = slip39.recover_ems([words]) + _, _, _, secret = slip39.recover_ems([words]) return secret, share else: # we need more shares @@ -85,7 +86,7 @@ def process_slip39(words: str) -> tuple[bytes | None, slip39.Share]: # in case of slip39 basic we only need the first and only group mnemonics = storage_recovery_shares.fetch_group(0) - _, _, secret = slip39.recover_ems(mnemonics) + _, _, _, secret = slip39.recover_ems(mnemonics) return secret, share diff --git a/core/src/apps/management/reset_device/__init__.py b/core/src/apps/management/reset_device/__init__.py index 49f9fcfdfd..3502dd3928 100644 --- a/core/src/apps/management/reset_device/__init__.py +++ b/core/src/apps/management/reset_device/__init__.py @@ -73,6 +73,7 @@ async def reset_device(msg: ResetDevice) -> Success: elif backup_type in (BAK_T_SLIP39_BASIC, BAK_T_SLIP39_ADVANCED): # generate and set SLIP39 parameters storage_device.set_slip39_identifier(slip39.generate_random_identifier()) + storage_device.set_slip39_extendable(slip39.DEFAULT_EXTENDABLE_FLAG) storage_device.set_slip39_iteration_exponent(slip39.DEFAULT_ITERATION_EXPONENT) else: # Unknown backup type. @@ -183,6 +184,7 @@ def _get_slip39_mnemonics( groups: Sequence[tuple[int, int]], ): identifier = storage_device.get_slip39_identifier() + extendable = storage_device.get_slip39_extendable() iteration_exponent = storage_device.get_slip39_iteration_exponent() if identifier is None or iteration_exponent is None: raise ValueError @@ -192,6 +194,7 @@ def _get_slip39_mnemonics( group_threshold, groups, identifier, + extendable, iteration_exponent, encrypted_master_secret, ) diff --git a/core/src/storage/device.py b/core/src/storage/device.py index 215acc39c8..0c55347601 100644 --- a/core/src/storage/device.py +++ b/core/src/storage/device.py @@ -35,6 +35,7 @@ INITIALIZED = const(0x13) # bool (0x01 or empty) _SAFETY_CHECK_LEVEL = const(0x14) # int _EXPERIMENTAL_FEATURES = const(0x15) # bool (0x01 or empty) _HIDE_PASSPHRASE_FROM_HOST = const(0x16) # bool (0x01 or empty) +_SLIP39_EXTENDABLE = const(0x17) # bool (0x01 or empty) SAFETY_CHECK_LEVEL_STRICT : Literal[0] = const(0) SAFETY_CHECK_LEVEL_PROMPT : Literal[1] = const(1) @@ -256,10 +257,24 @@ def set_slip39_identifier(identifier: int) -> None: def get_slip39_identifier() -> int | None: - """The device's actual SLIP-39 identifier used in passphrase derivation.""" + """The device's actual SLIP-39 identifier used in legacy passphrase derivation.""" return common.get_uint16(_NAMESPACE, _SLIP39_IDENTIFIER) +def set_slip39_extendable(extendable: bool) -> None: + """ + The device's actual SLIP-39 extendable backup flag. + Not to be confused with recovery.extendable, which is stored only during + the recovery process and it is copied here upon success. + """ + common.set_bool(_NAMESPACE, _SLIP39_EXTENDABLE, extendable) + + +def get_slip39_extendable() -> bool: + """The device's actual SLIP-39 extendable backup flag.""" + return common.get_bool(_NAMESPACE, _SLIP39_EXTENDABLE) + + def set_slip39_iteration_exponent(exponent: int) -> None: """ The device's actual SLIP-39 iteration exponent used in passphrase derivation. diff --git a/core/src/storage/recovery.py b/core/src/storage/recovery.py index 5e7324aa26..dfdfe1421a 100644 --- a/core/src/storage/recovery.py +++ b/core/src/storage/recovery.py @@ -13,6 +13,7 @@ _SLIP39_IDENTIFIER = const(0x03) # bytes _REMAINING = const(0x05) # int _SLIP39_ITERATION_EXPONENT = const(0x06) # int _SLIP39_GROUP_COUNT = const(0x07) # int +_SLIP39_EXTENDABLE = const(0x08) # bool # Deprecated Keys: # _WORD_COUNT = const(0x02) # int @@ -56,6 +57,16 @@ def get_slip39_identifier() -> int | None: return common.get_uint16(_NAMESPACE, _SLIP39_IDENTIFIER) +def set_slip39_extendable(extendable: bool) -> None: + _require_progress() + common.set_bool(_NAMESPACE, _SLIP39_EXTENDABLE, extendable) + + +def get_slip39_extendable() -> bool: + _require_progress() + return common.get_bool(_NAMESPACE, _SLIP39_EXTENDABLE) + + def set_slip39_iteration_exponent(exponent: int) -> None: _require_progress() common.set_uint8(_NAMESPACE, _SLIP39_ITERATION_EXPONENT, exponent) diff --git a/core/src/trezor/crypto/slip39.py b/core/src/trezor/crypto/slip39.py index b2ee985979..8d6570d92b 100644 --- a/core/src/trezor/crypto/slip39.py +++ b/core/src/trezor/crypto/slip39.py @@ -67,10 +67,15 @@ def _xor(a: bytes, b: bytes) -> bytes: _ID_LENGTH_BITS = const(15) """The length of the random identifier in bits.""" -_ITERATION_EXP_LENGTH_BITS = const(5) +_EXTENDABLE_FLAG_LENGTH_BITS = const(1) +"""The length of the extendable backup flag in bits.""" + +_ITERATION_EXP_LENGTH_BITS = const(4) """The length of the iteration exponent in bits.""" -_ID_EXP_LENGTH_WORDS = _bits_to_words(_ID_LENGTH_BITS + _ITERATION_EXP_LENGTH_BITS) +_ID_EXP_LENGTH_WORDS = _bits_to_words( + _ID_LENGTH_BITS + _EXTENDABLE_FLAG_LENGTH_BITS + _ITERATION_EXP_LENGTH_BITS +) """The length of the random identifier and iteration exponent in words.""" _CHECKSUM_LENGTH_WORDS = const(3) @@ -79,8 +84,13 @@ _CHECKSUM_LENGTH_WORDS = const(3) _DIGEST_LENGTH_BYTES = const(4) """The length of the digest of the shared secret in bytes.""" -_CUSTOMIZATION_STRING = b"shamir" -"""The customization string used in the RS1024 checksum and in the PBKDF2 salt.""" +_CUSTOMIZATION_STRING_ORIG = b"shamir" +"""The customization string used in the RS1024 checksum and in the PBKDF2 salt for shares +_without_ the extendable backup flag.""" + +_CUSTOMIZATION_STRING_EXTENDABLE = b"shamir_extendable" +"""The customization string used in the RS1024 checksum for shares _with_ the extendable +backup flag.""" _METADATA_LENGTH_WORDS = _ID_EXP_LENGTH_WORDS + 2 + _CHECKSUM_LENGTH_WORDS """The length of the mnemonic in words without the share value.""" @@ -111,6 +121,7 @@ MAX_GROUP_COUNT = const(16) """The maximum number of groups that can be created.""" DEFAULT_ITERATION_EXPONENT = const(1) +DEFAULT_EXTENDABLE_FLAG = True class Share: @@ -121,6 +132,7 @@ class Share: def __init__( self, identifier: int, + extendable: bool, iteration_exponent: int, group_index: int, group_threshold: int, @@ -130,6 +142,7 @@ class Share: share_value: bytes, ): self.identifier = identifier + self.extendable = extendable self.iteration_exponent = iteration_exponent self.group_index = group_index self.group_threshold = group_threshold @@ -144,6 +157,7 @@ def decrypt( passphrase: bytes, iteration_exponent: int, identifier: int, + extendable: bool, progress_callback: Callable[[int, int], None] | None = None, ) -> bytes: """ @@ -154,7 +168,7 @@ def decrypt( """ l = encrypted_master_secret[: len(encrypted_master_secret) // 2] r = encrypted_master_secret[len(encrypted_master_secret) // 2 :] - salt = _get_salt(identifier) + salt = _get_salt(identifier, extendable) for i in reversed(range(_ROUND_COUNT)): (l, r) = ( r, @@ -178,6 +192,7 @@ def split_ems( tuple[int, int] ], # A collection of (member_threshold, member_count). identifier: int, + extendable: bool, iteration_exponent: int, encrypted_master_secret: bytes, # The encrypted master secret to split. ) -> list[list[str]]: @@ -218,6 +233,7 @@ def split_ems( group_mnemonics.append( _encode_mnemonic( identifier, + extendable, iteration_exponent, group_index, group_threshold, @@ -231,11 +247,11 @@ def split_ems( return mnemonics -def recover_ems(mnemonics: list[str]) -> tuple[int, int, bytes]: +def recover_ems(mnemonics: list[str]) -> tuple[int, bool, int, bytes]: """ Combines mnemonic shares to obtain the encrypted master secret which was previously split using Shamir's secret sharing scheme. - Returns identifier, iteration exponent and the encrypted master secret. + Returns identifier, extendable backup flag, iteration exponent and the encrypted master secret. """ if not mnemonics: @@ -243,6 +259,7 @@ def recover_ems(mnemonics: list[str]) -> tuple[int, int, bytes]: ( identifier, + extendable, iteration_exponent, group_threshold, _group_count, @@ -266,7 +283,7 @@ def recover_ems(mnemonics: list[str]) -> tuple[int, int, bytes]: ] encrypted_master_secret = _recover_secret(group_threshold, group_shares) - return identifier, iteration_exponent, encrypted_master_secret + return identifier, extendable, iteration_exponent, encrypted_master_secret def decode_mnemonic(mnemonic: str) -> Share: @@ -283,12 +300,16 @@ def decode_mnemonic(mnemonic: str) -> Share: if padding_len > 8: raise MnemonicError("Invalid mnemonic length.") - if not _rs1024_verify_checksum(mnemonic_data): + id_exp_int = _int_from_indices(mnemonic_data[:_ID_EXP_LENGTH_WORDS]) + identifier = id_exp_int >> ( + _EXTENDABLE_FLAG_LENGTH_BITS + _ITERATION_EXP_LENGTH_BITS + ) + extendable = bool((id_exp_int >> _ITERATION_EXP_LENGTH_BITS) & 1) + iteration_exponent = id_exp_int & ((1 << _ITERATION_EXP_LENGTH_BITS) - 1) + + if not _rs1024_verify_checksum(mnemonic_data, extendable): raise MnemonicError("Invalid mnemonic checksum.") - id_exp_int = _int_from_indices(mnemonic_data[:_ID_EXP_LENGTH_WORDS]) - identifier = id_exp_int >> _ITERATION_EXP_LENGTH_BITS - iteration_exponent = id_exp_int & ((1 << _ITERATION_EXP_LENGTH_BITS) - 1) tmp = _int_from_indices( mnemonic_data[_ID_EXP_LENGTH_WORDS : _ID_EXP_LENGTH_WORDS + 2] ) @@ -314,6 +335,7 @@ def decode_mnemonic(mnemonic: str) -> Share: return Share( identifier, + extendable, iteration_exponent, group_index, group_threshold + 1, @@ -352,13 +374,22 @@ def _mnemonic_to_indices(mnemonic: str) -> Iterable[int]: # === Checksum functions === -def _rs1024_create_checksum(data: Indices) -> Indices: +def _customization_string(extendable: bool) -> bytes: + if extendable: + return _CUSTOMIZATION_STRING_EXTENDABLE + else: + return _CUSTOMIZATION_STRING_ORIG + + +def _rs1024_create_checksum(data: Indices, extendable: bool) -> Indices: """ This implements the checksum - a Reed-Solomon code over GF(1024) that guarantees detection of any error affecting at most 3 words and has less than a 1 in 10^9 chance of failing to detect more errors. """ - values = tuple(_CUSTOMIZATION_STRING) + data + _CHECKSUM_LENGTH_WORDS * (0,) + values = ( + tuple(_customization_string(extendable)) + data + _CHECKSUM_LENGTH_WORDS * (0,) + ) polymod = _rs1024_polymod(values) ^ 1 return tuple( (polymod >> 10 * i) & 1023 for i in reversed(range(_CHECKSUM_LENGTH_WORDS)) @@ -387,11 +418,11 @@ def _rs1024_polymod(values: Indices) -> int: return chk -def _rs1024_verify_checksum(data: Indices) -> bool: +def _rs1024_verify_checksum(data: Indices, extendable: bool) -> bool: """ Verifies a checksum of the given mnemonic, which was already parsed into Indices. """ - return _rs1024_polymod(tuple(_CUSTOMIZATION_STRING) + data) == 1 + return _rs1024_polymod(tuple(_customization_string(extendable)) + data) == 1 # === Internal functions === @@ -409,10 +440,13 @@ def _round_function(i: int, passphrase: bytes, e: int, salt: bytes, r: bytes) -> ).key()[: len(r)] -def _get_salt(identifier: int) -> bytes: - return _CUSTOMIZATION_STRING + identifier.to_bytes( - _bits_to_bytes(_ID_LENGTH_BITS), "big" - ) +def _get_salt(identifier: int, extendable: bool) -> bytes: + if extendable: + return bytes() + else: + return _CUSTOMIZATION_STRING_ORIG + identifier.to_bytes( + _bits_to_bytes(_ID_LENGTH_BITS), "big" + ) def _create_digest(random_data: bytes, shared_secret: bytes) -> bytes: @@ -481,12 +515,17 @@ def _recover_secret(threshold: int, shares: list[tuple[int, bytes]]) -> bytes: def _group_prefix( identifier: int, + extendable: bool, iteration_exponent: int, group_index: int, group_threshold: int, group_count: int, ) -> Indices: - id_exp_int = (identifier << _ITERATION_EXP_LENGTH_BITS) + iteration_exponent + id_exp_int = ( + (identifier << (_EXTENDABLE_FLAG_LENGTH_BITS + _ITERATION_EXP_LENGTH_BITS)) + + (int(extendable) << _ITERATION_EXP_LENGTH_BITS) + + iteration_exponent + ) return tuple(_int_to_indices(id_exp_int, _ID_EXP_LENGTH_WORDS, _RADIX_BITS)) + ( (group_index << 6) + ((group_threshold - 1) << 2) + ((group_count - 1) >> 2), ) @@ -494,6 +533,7 @@ def _group_prefix( def _encode_mnemonic( identifier: int, + extendable: bool, iteration_exponent: int, group_index: int, # The x coordinate of the group share. group_threshold: int, # The number of group shares needed to reconstruct the encrypted master secret. @@ -513,7 +553,12 @@ def _encode_mnemonic( share_data = ( _group_prefix( - identifier, iteration_exponent, group_index, group_threshold, group_count + identifier, + extendable, + iteration_exponent, + group_index, + group_threshold, + group_count, ) + ( (((group_count - 1) & 3) << 8) @@ -522,15 +567,16 @@ def _encode_mnemonic( ) + tuple(_int_to_indices(value_int, value_word_count, _RADIX_BITS)) ) - checksum = _rs1024_create_checksum(share_data) + checksum = _rs1024_create_checksum(share_data, extendable) return _mnemonic_from_indices(share_data + checksum) def _decode_mnemonics( mnemonics: list[str], -) -> tuple[int, int, int, int, MnemonicGroups]: +) -> tuple[int, bool, int, int, int, MnemonicGroups]: identifiers = set() + extendable_flags = set() iteration_exponents = set() group_thresholds = set() group_counts = set() @@ -540,6 +586,7 @@ def _decode_mnemonics( for mnemonic in mnemonics: share = decode_mnemonic(mnemonic) identifiers.add(share.identifier) + extendable_flags.add(share.extendable) iteration_exponents.add(share.iteration_exponent) group_thresholds.add(share.group_threshold) group_counts.add(share.group_count) @@ -550,7 +597,11 @@ def _decode_mnemonics( ) group[1].add((share.index, share.share_value)) - if len(identifiers) != 1 or len(iteration_exponents) != 1: + if ( + len(identifiers) != 1 + or len(extendable_flags) != 1 + or len(iteration_exponents) != 1 + ): raise MnemonicError( f"Invalid set of mnemonics. All mnemonics must begin with the same {_ID_EXP_LENGTH_WORDS} words." ) @@ -573,6 +624,7 @@ def _decode_mnemonics( return ( identifiers.pop(), + extendable_flags.pop(), iteration_exponents.pop(), group_thresholds.pop(), group_counts.pop(), diff --git a/core/tests/slip39_vectors.py b/core/tests/slip39_vectors.py index b92a5be3ec..bd89c0c05c 100644 --- a/core/tests/slip39_vectors.py +++ b/core/tests/slip39_vectors.py @@ -279,4 +279,38 @@ vectors = [ ], "", ], + [ + [ + "herald flea academic cage avoid space trend estate dryer hairy evoke eyebrow improve airline artwork garlic premium duration prevent oven", + "herald flea academic client blue skunk class goat luxury deny presence impulse graduate clay join blanket bulge survive dish necklace", + "herald flea academic acne advance fused brother frozen broken game ranked ajar already believe check install theory angry exercise adult" + ], + "ad6f2ad8b59bbbaa01369b9006208d9a", + ], + [ + [ + "testify swimming academic academic column loyalty smear include exotic bedroom exotic wrist lobe cover grief golden smart junior estimate learn" + ], + "1679b4516e0ee5954351d288a838f45e", + ], + [ + [ + "enemy favorite academic acid cowboy phrase havoc level response walnut budget painting inside trash adjust froth kitchen learn tidy punish", + "enemy favorite academic always academic sniff script carpet romp kind promise scatter center unfair training emphasis evening belong fake enforce" + ], + "48b1a4b80b8c209ad42c33672bdaa428", + ], + [ + [ + "impulse calcium academic academic alcohol sugar lyrics pajamas column facility finance tension extend space birthday rainbow swimming purple syndrome facility trial warn duration snapshot shadow hormone rhyme public spine counter easy hawk album" + ], + "8340611602fe91af634a5f4608377b5235fa2d757c51d720c0c7656249a3035f", + ], + [ + [ + "western apart academic always artist resident briefing sugar woman oven coding club ajar merit pecan answer prisoner artist fraction amount desktop mild false necklace muscle photo wealthy alpha category unwrap spew losing making", + "western apart academic acid answer ancient auction flip image penalty oasis beaver multiple thunder problem switch alive heat inherit superior teaspoon explain blanket pencil numb lend punish endless aunt garlic humidity kidney observe" + ], + "8dc652d6d6cd370d8c963141f6d79ba440300f25c467302c1d966bff8f62300d", + ], ] diff --git a/core/tests/test_apps.cardano.address.py b/core/tests/test_apps.cardano.address.py index 77a295fbc9..140992bc13 100644 --- a/core/tests/test_apps.cardano.address.py +++ b/core/tests/test_apps.cardano.address.py @@ -190,8 +190,8 @@ class TestCardanoAddress(unittest.TestCase): "talent drug much home firefly toxic analysis idea umbrella slice", ] passphrase = b"TREZOR" - identifier, exponent, ems = slip39.recover_ems(mnemonics) - master_secret = slip39.decrypt(ems, passphrase, exponent, identifier) + identifier, extendable, exponent, ems = slip39.recover_ems(mnemonics) + master_secret = slip39.decrypt(ems, passphrase, exponent, identifier, extendable) node = cardano.from_seed_slip23(master_secret) @@ -264,8 +264,8 @@ class TestCardanoAddress(unittest.TestCase): "quick silent downtown oral critical step remove says rhythm venture aunt", ] passphrase = b"TREZOR" - identifier, exponent, ems = slip39.recover_ems(mnemonics) - master_secret = slip39.decrypt(ems, passphrase, exponent, identifier) + identifier, extendable, exponent, ems = slip39.recover_ems(mnemonics) + master_secret = slip39.decrypt(ems, passphrase, exponent, identifier, extendable) node = cardano.from_seed_slip23(master_secret) diff --git a/core/tests/test_apps.cardano.get_public_key.py b/core/tests/test_apps.cardano.get_public_key.py index 4d8635b0cb..846219bbae 100644 --- a/core/tests/test_apps.cardano.get_public_key.py +++ b/core/tests/test_apps.cardano.get_public_key.py @@ -144,8 +144,8 @@ class TestCardanoGetPublicKey(unittest.TestCase): "talent drug much home firefly toxic analysis idea umbrella slice", ] passphrase = b"TREZOR" - identifier, exponent, ems = slip39.recover_ems(mnemonics) - master_secret = slip39.decrypt(ems, passphrase, exponent, identifier) + identifier, extendable, exponent, ems = slip39.recover_ems(mnemonics) + master_secret = slip39.decrypt(ems, passphrase, exponent, identifier, extendable) node = cardano.from_seed_slip23(master_secret) @@ -193,8 +193,8 @@ class TestCardanoGetPublicKey(unittest.TestCase): "quick silent downtown oral critical step remove says rhythm venture aunt", ] passphrase = b"TREZOR" - identifier, exponent, ems = slip39.recover_ems(mnemonics) - master_secret = slip39.decrypt(ems, passphrase, exponent, identifier) + identifier, extendable, exponent, ems = slip39.recover_ems(mnemonics) + master_secret = slip39.decrypt(ems, passphrase, exponent, identifier, extendable) node = cardano.from_seed_slip23(master_secret) diff --git a/core/tests/test_apps.management.recovery_device.py b/core/tests/test_apps.management.recovery_device.py index ebfd4d7127..a1809d2d13 100644 --- a/core/tests/test_apps.management.recovery_device.py +++ b/core/tests/test_apps.management.recovery_device.py @@ -41,6 +41,7 @@ class TestSlip39(unittest.TestCase): share.iteration_exponent, storage.recovery.get_slip39_iteration_exponent() ) self.assertEqual(share.identifier, storage.recovery.get_slip39_identifier()) + self.assertEqual(share.extendable, storage.recovery.get_slip39_extendable()) self.assertEqual(storage.recovery.get_slip39_remaining_shares(0), 2) self.assertEqual( storage.recovery_shares.get(share.index, share.group_index), first @@ -84,6 +85,7 @@ class TestSlip39(unittest.TestCase): share.iteration_exponent, storage.recovery.get_slip39_iteration_exponent() ) self.assertEqual(share.identifier, storage.recovery.get_slip39_identifier()) + self.assertEqual(share.extendable, storage.recovery.get_slip39_extendable()) self.assertEqual( storage.recovery.fetch_slip39_remaining_shares(), [16, 0, 16, 16] ) @@ -100,6 +102,7 @@ class TestSlip39(unittest.TestCase): share.iteration_exponent, storage.recovery.get_slip39_iteration_exponent() ) self.assertEqual(share.identifier, storage.recovery.get_slip39_identifier()) + self.assertEqual(share.extendable, storage.recovery.get_slip39_extendable()) self.assertEqual( storage.recovery_shares.get(share.index, share.group_index), words ) @@ -122,6 +125,7 @@ class TestSlip39(unittest.TestCase): share.iteration_exponent, storage.recovery.get_slip39_iteration_exponent() ) self.assertEqual(share.identifier, storage.recovery.get_slip39_identifier()) + self.assertEqual(share.extendable, storage.recovery.get_slip39_extendable()) self.assertEqual( storage.recovery_shares.get(share.index, share.group_index), words ) @@ -146,6 +150,7 @@ class TestSlip39(unittest.TestCase): share.iteration_exponent, storage.recovery.get_slip39_iteration_exponent() ) self.assertEqual(share.identifier, storage.recovery.get_slip39_identifier()) + self.assertEqual(share.extendable, storage.recovery.get_slip39_extendable()) self.assertEqual( storage.recovery_shares.get(share.index, share.group_index), words ) diff --git a/core/tests/test_trezor.crypto.slip39.py b/core/tests/test_trezor.crypto.slip39.py index c04c88520f..7b99184ae7 100644 --- a/core/tests/test_trezor.crypto.slip39.py +++ b/core/tests/test_trezor.crypto.slip39.py @@ -30,139 +30,149 @@ class TestCryptoSlip39(unittest.TestCase): def test_basic_sharing_random(self): ems = random.bytes(32) identifier = slip39.generate_random_identifier() - mnemonics = slip39.split_ems(1, [(3, 5)], identifier, 1, ems) - mnemonics = mnemonics[0] - self.assertEqual( - slip39.recover_ems(mnemonics[:3]), slip39.recover_ems(mnemonics[2:]) - ) + for extendable in (False, True): + mnemonics = slip39.split_ems(1, [(3, 5)], identifier, extendable, 1, ems) + mnemonics = mnemonics[0] + self.assertEqual( + slip39.recover_ems(mnemonics[:3]), slip39.recover_ems(mnemonics[2:]) + ) def test_basic_sharing_fixed(self): - generated_identifier = slip39.generate_random_identifier() - mnemonics = slip39.split_ems(1, [(3, 5)], generated_identifier, 1, self.EMS) - mnemonics = mnemonics[0] - identifier, exponent, ems = slip39.recover_ems(mnemonics[:3]) - self.assertEqual(ems, self.EMS) - self.assertEqual(generated_identifier, identifier) - self.assertEqual(slip39.recover_ems(mnemonics[1:4])[2], ems) - with self.assertRaises(slip39.MnemonicError): - slip39.recover_ems(mnemonics[1:3]) + for extendable in (False, True): + generated_identifier = slip39.generate_random_identifier() + mnemonics = slip39.split_ems(1, [(3, 5)], generated_identifier, extendable, 1, self.EMS) + mnemonics = mnemonics[0] + identifier, _, _, ems = slip39.recover_ems(mnemonics[:3]) + self.assertEqual(ems, self.EMS) + self.assertEqual(generated_identifier, identifier) + self.assertEqual(slip39.recover_ems(mnemonics[1:4])[3], ems) + with self.assertRaises(slip39.MnemonicError): + slip39.recover_ems(mnemonics[1:3]) def test_iteration_exponent(self): - identifier = slip39.generate_random_identifier() - mnemonics = slip39.split_ems(1, [(3, 5)], identifier, 1, self.EMS) - mnemonics = mnemonics[0] - identifier, exponent, ems = slip39.recover_ems(mnemonics[1:4]) - self.assertEqual(ems, self.EMS) + for extendable in (False, True): + identifier = slip39.generate_random_identifier() + mnemonics = slip39.split_ems(1, [(3, 5)], identifier, extendable, 1, self.EMS) + mnemonics = mnemonics[0] + identifier, extendable, exponent, ems = slip39.recover_ems(mnemonics[1:4]) + self.assertEqual(ems, self.EMS) - identifier = slip39.generate_random_identifier() - mnemonics = slip39.split_ems(1, [(3, 5)], identifier, 2, self.EMS) - mnemonics = mnemonics[0] - identifier, exponent, ems = slip39.recover_ems(mnemonics[1:4]) - self.assertEqual(ems, self.EMS) + identifier = slip39.generate_random_identifier() + mnemonics = slip39.split_ems(1, [(3, 5)], identifier, extendable, 2, self.EMS) + mnemonics = mnemonics[0] + identifier, extendable, exponent, ems = slip39.recover_ems(mnemonics[1:4]) + self.assertEqual(ems, self.EMS) def test_group_sharing(self): group_threshold = 2 group_sizes = (5, 3, 5, 1) member_thresholds = (3, 2, 2, 1) - identifier = slip39.generate_random_identifier() - mnemonics = slip39.split_ems( - group_threshold, - list(zip(member_thresholds, group_sizes)), - identifier, - 1, - self.EMS, - ) + for extendable in (False, True): + identifier = slip39.generate_random_identifier() + mnemonics = slip39.split_ems( + group_threshold, + list(zip(member_thresholds, group_sizes)), + identifier, + extendable, + 1, + self.EMS, + ) - # Test all valid combinations of mnemonics. - for groups in combinations(zip(mnemonics, member_thresholds), group_threshold): - for group1_subset in combinations(groups[0][0], groups[0][1]): - for group2_subset in combinations(groups[1][0], groups[1][1]): - mnemonic_subset = list(group1_subset + group2_subset) - random.shuffle(mnemonic_subset) - identifier, exponent, ems = slip39.recover_ems(mnemonic_subset) - self.assertEqual(ems, self.EMS) + # Test all valid combinations of mnemonics. + for groups in combinations(zip(mnemonics, member_thresholds), group_threshold): + for group1_subset in combinations(groups[0][0], groups[0][1]): + for group2_subset in combinations(groups[1][0], groups[1][1]): + mnemonic_subset = list(group1_subset + group2_subset) + random.shuffle(mnemonic_subset) + identifier, _, _, ems = slip39.recover_ems(mnemonic_subset) + self.assertEqual(ems, self.EMS) - # Minimal sets of mnemonics. - identifier, exponent, ems = slip39.recover_ems( - [mnemonics[2][0], mnemonics[2][2], mnemonics[3][0]] - ) - self.assertEqual(ems, self.EMS) - self.assertEqual( - slip39.recover_ems([mnemonics[2][3], mnemonics[3][0], mnemonics[2][4]])[2], - ems, - ) + # Minimal sets of mnemonics. + identifier, _, _, ems = slip39.recover_ems( + [mnemonics[2][0], mnemonics[2][2], mnemonics[3][0]] + ) + self.assertEqual(ems, self.EMS) + self.assertEqual( + slip39.recover_ems([mnemonics[2][3], mnemonics[3][0], mnemonics[2][4]])[3], + ems, + ) - # One complete group and one incomplete group out of two groups required. - with self.assertRaises(slip39.MnemonicError): - slip39.recover_ems(mnemonics[0][2:] + [mnemonics[1][0]]) + # One complete group and one incomplete group out of two groups required. + with self.assertRaises(slip39.MnemonicError): + slip39.recover_ems(mnemonics[0][2:] + [mnemonics[1][0]]) - # One group of two required. - with self.assertRaises(slip39.MnemonicError): - slip39.recover_ems(mnemonics[0][1:4]) + # One group of two required. + with self.assertRaises(slip39.MnemonicError): + slip39.recover_ems(mnemonics[0][1:4]) def test_group_sharing_threshold_1(self): group_threshold = 1 group_sizes = (5, 3, 5, 1) member_thresholds = (3, 2, 2, 1) - identifier = slip39.generate_random_identifier() - mnemonics = slip39.split_ems( - group_threshold, - list(zip(member_thresholds, group_sizes)), - identifier, - 1, - self.EMS, - ) - - # Test all valid combinations of mnemonics. - for group, threshold in zip(mnemonics, member_thresholds): - for group_subset in combinations(group, threshold): - mnemonic_subset = list(group_subset) - random.shuffle(mnemonic_subset) - identifier, exponent, ems = slip39.recover_ems(mnemonic_subset) - self.assertEqual(ems, self.EMS) - - def test_all_groups_exist(self): - for group_threshold in (1, 2, 5): + for extendable in (False, True): identifier = slip39.generate_random_identifier() mnemonics = slip39.split_ems( group_threshold, - [(3, 5), (1, 1), (2, 3), (2, 5), (3, 5)], + list(zip(member_thresholds, group_sizes)), identifier, + extendable, 1, self.EMS, ) - self.assertEqual(len(mnemonics), 5) - self.assertEqual(len(sum(mnemonics, [])), 19) + + # Test all valid combinations of mnemonics. + for group, threshold in zip(mnemonics, member_thresholds): + for group_subset in combinations(group, threshold): + mnemonic_subset = list(group_subset) + random.shuffle(mnemonic_subset) + identifier, _, _, ems = slip39.recover_ems(mnemonic_subset) + self.assertEqual(ems, self.EMS) + + def test_all_groups_exist(self): + for extendable in (False, True): + for group_threshold in (1, 2, 5): + identifier = slip39.generate_random_identifier() + mnemonics = slip39.split_ems( + group_threshold, + [(3, 5), (1, 1), (2, 3), (2, 5), (3, 5)], + identifier, + extendable, + 1, + self.EMS, + ) + self.assertEqual(len(mnemonics), 5) + self.assertEqual(len(sum(mnemonics, [])), 19) def test_invalid_sharing(self): - identifier = slip39.generate_random_identifier() + for extendable in (False, True): + identifier = slip39.generate_random_identifier() - # Group threshold exceeds number of groups. - with self.assertRaises(ValueError): - slip39.split_ems(3, [(3, 5), (2, 5)], identifier, 1, self.EMS) + # Group threshold exceeds number of groups. + with self.assertRaises(ValueError): + slip39.split_ems(3, [(3, 5), (2, 5)], identifier, extendable, 1, self.EMS) - # Invalid group threshold. - with self.assertRaises(ValueError): - slip39.split_ems(0, [(3, 5), (2, 5)], identifier, 1, self.EMS) + # Invalid group threshold. + with self.assertRaises(ValueError): + slip39.split_ems(0, [(3, 5), (2, 5)], identifier, extendable, 1, self.EMS) - # Member threshold exceeds number of members. - with self.assertRaises(ValueError): - slip39.split_ems(2, [(3, 2), (2, 5)], identifier, 1, self.EMS) + # Member threshold exceeds number of members. + with self.assertRaises(ValueError): + slip39.split_ems(2, [(3, 2), (2, 5)], identifier, extendable, 1, self.EMS) - # Invalid member threshold. - with self.assertRaises(ValueError): - slip39.split_ems(2, [(0, 2), (2, 5)], identifier, 1, self.EMS) + # Invalid member threshold. + with self.assertRaises(ValueError): + slip39.split_ems(2, [(0, 2), (2, 5)], identifier, extendable, 1, self.EMS) - # Group with multiple members and threshold 1. - with self.assertRaises(ValueError): - slip39.split_ems(2, [(3, 5), (1, 3), (2, 5)], identifier, 1, self.EMS) + # Group with multiple members and threshold 1. + with self.assertRaises(ValueError): + slip39.split_ems(2, [(3, 5), (1, 3), (2, 5)], identifier, extendable, 1, self.EMS) def test_vectors(self): for mnemonics, secret in vectors: if secret: - identifier, exponent, ems = slip39.recover_ems(mnemonics) + identifier, extendable, exponent, ems = slip39.recover_ems(mnemonics) self.assertEqual( - slip39.decrypt(ems, b"TREZOR", exponent, identifier), + slip39.decrypt(ems, b"TREZOR", exponent, identifier, extendable), unhexlify(secret), ) else: diff --git a/docs/core/misc/slip0039.md b/docs/core/misc/slip0039.md index 7c81fd96e2..be4507e10c 100644 --- a/docs/core/misc/slip0039.md +++ b/docs/core/misc/slip0039.md @@ -31,10 +31,10 @@ applying PBKDF2 to the mnemonic secret plus passphrase. For SLIP-39 it is not practical to store the raw data of the recovery shares. During device initialization, a random Encrypted Master Secret is generated and stored as -`_MNEMONIC_SECRET`. SLIP-39 encryption parameters (a random identifier and an iteration -exponent) are stored alongside the mnemonic secret in their own storage fields. Whenever -the root node is required, it is derived by "decrypting" the stored mnemonic secret with -the provided passphrase. +`_MNEMONIC_SECRET`. SLIP-39 encryption parameters (a random identifier, extendable backup +flag and an iteration exponent) are stored alongside the mnemonic secret in their own +storage fields. Whenever the root node is required, it is derived by "decrypting" the +stored mnemonic secret with the provided passphrase. ## SLIP-39 implementation @@ -46,17 +46,17 @@ SLIP-39 provides the following high-level API: Secret with the provided passphrase, and split into a number of shares defined via the group parameters. Implemented using the following: - - `encrypt(master_secret, passphrase, iteration_exponent, identifier)`: Encrypt the - Master Secret with the given passphrase and parameters. - - **`split_ems(group parameters, identifier, iteration_exponent, encrypted_master_secret)`**: + - `encrypt(master_secret, passphrase, iteration_exponent, identifier, extendable)`: + Encrypt the Master Secret with the given passphrase and parameters. + - **`split_ems(group parameters, identifier, extendable, iteration_exponent, encrypted_master_secret)`**: Split the encrypted secret and encode the metadata into a set of shares defined via the group parameters. * `combine_mnemonics(set of shares, passphrase)`: Combine the given set of shares to reconstruct the secret, then decrypt it with the provided passphrase. Implemented using the following: - **`recover_ems(set of shares)`**: Combine the given set of shares to obtain the - encrypted master secret, identifier and iteration exponent. - - **`decrypt(encrypted_master_secret, passphrase, iteration_exponent, identifier)`**: + encrypted master secret, identifier, extendable backup flag and iteration exponent. + - **`decrypt(encrypted_master_secret, passphrase, iteration_exponent, identifier, extendable)`**: Decrypt the secret with the given passphrase and parameters, to obtain the original Master Secret. @@ -74,7 +74,7 @@ This process does not use passphrase. 1. Generate the required number of random bits (128 or 256), and store as `_MNEMONIC_SECRET`. -2. Generate a random identifier and store as `_SLIP39_IDENTIFIER`. +2. Translate the host-specified backup type to an extendable backup type and store it as `_BACKUP_TYPE`. 3. Store the default iteration exponent `1` as `_SLIP39_ITERATION_EXPONENT`. 4. The storage now contains all parameters required for seed derivation. @@ -100,6 +100,7 @@ This process does not use passphrase. 1. Prompt the user to enter enough shares. 2. Use `slip39.recover_ems(shares)` to combine the shares and get metadata. 3. Store the Encrypted Master Secret as `_MNEMONIC_SECRET`. -4. Store the identifier as `_SLIP39_IDENTIFIER`. -5. Store the iteration exponent as `_SLIP39_ITERATION_EXPONENT`. -6. The storage now contains all parameters required for seed derivation. +4. Infer the backup type and store it as `_BACKUP_TYPE`. +5. If the backup type is not extendable, then store the identifier as `_SLIP39_IDENTIFIER`. +6. Store the iteration exponent as `_SLIP39_ITERATION_EXPONENT`. +7. The storage now contains all parameters required for seed derivation. diff --git a/poetry.lock b/poetry.lock index a4ec6e36ab..d8b22b8ba4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,9 +1,10 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. [[package]] name = "astroid" version = "2.15.8" description = "An abstract syntax tree for Python with inference support." +category = "main" optional = false python-versions = ">=3.7.2" files = [ @@ -23,6 +24,7 @@ wrapt = [ name = "atomicwrites" version = "1.4.0" description = "Atomic file writes." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -34,6 +36,7 @@ files = [ name = "attrs" version = "21.4.0" description = "Classes Without Boilerplate" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -51,6 +54,7 @@ tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy" name = "autoflake" version = "1.4" description = "Removes unused imports and unused variables" +category = "main" optional = false python-versions = "*" files = [ @@ -64,6 +68,7 @@ pyflakes = ">=1.1.0" name = "binsize" version = "0.1.3" description = "Tool to analyze the size of a binary from .elf file" +category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -81,6 +86,7 @@ typing-extensions = "*" name = "black" version = "24.3.0" description = "The uncompromising code formatter." +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -127,6 +133,7 @@ uvloop = ["uvloop (>=0.15.2)"] name = "certifi" version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -138,6 +145,7 @@ files = [ name = "cffi" version = "1.15.0" description = "Foreign Function Interface for Python calling C code." +category = "main" optional = false python-versions = "*" files = [ @@ -200,6 +208,7 @@ pycparser = "*" name = "charset-normalizer" version = "2.0.11" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" optional = false python-versions = ">=3.5.0" files = [ @@ -214,6 +223,7 @@ unicode-backport = ["unicodedata2"] name = "click" version = "8.1.3" description = "Composable command line interface toolkit" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -228,6 +238,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "click-default-group" version = "1.2.2" description = "Extends click.Group to invoke a command without explicit subcommand name" +category = "dev" optional = false python-versions = "*" files = [ @@ -241,6 +252,7 @@ click = "*" name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -252,6 +264,7 @@ files = [ name = "construct" version = "2.10.67" description = "A powerful declarative symmetric parser/builder for binary data" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -265,6 +278,7 @@ extras = ["arrow", "cloudpickle", "enum34", "lz4", "numpy", "ruamel.yaml"] name = "construct-classes" version = "0.1.2" description = "Parse your binary structs into dataclasses" +category = "main" optional = false python-versions = ">=3.6.2,<4.0" files = [ @@ -279,6 +293,7 @@ construct = ">=2.10,<3.0" name = "coverage" version = "4.5.4" description = "Code coverage measurement for Python" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" files = [ @@ -320,6 +335,7 @@ files = [ name = "cryptography" version = "42.0.4" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -374,6 +390,7 @@ test-randomorder = ["pytest-randomly"] name = "demjson3" version = "3.0.5" description = "encoder, decoder, and lint/validator for JSON (JavaScript Object Notation) compliant with RFC 7159" +category = "main" optional = false python-versions = "*" files = [ @@ -384,6 +401,7 @@ files = [ name = "dill" version = "0.3.8" description = "serialize all of Python" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -399,6 +417,7 @@ profile = ["gprof2dot (>=2022.7.29)"] name = "distlib" version = "0.3.4" description = "Distribution utilities" +category = "main" optional = false python-versions = "*" files = [ @@ -410,6 +429,7 @@ files = [ name = "dominate" version = "2.6.0" description = "Dominate is a Python library for creating and manipulating HTML documents using an elegant DOM API." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -421,6 +441,7 @@ files = [ name = "ecdsa" version = "0.16.1" description = "ECDSA cryptographic signature library (pure python)" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -439,6 +460,7 @@ gmpy2 = ["gmpy2"] name = "execnet" version = "1.9.0" description = "execnet: rapid multi-Python deployment" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -453,6 +475,7 @@ testing = ["pre-commit"] name = "fido2" version = "0.8.1" description = "Python based FIDO 2.0 library" +category = "main" optional = false python-versions = ">=2.7.6,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" files = [ @@ -471,6 +494,7 @@ pcsc = ["pyscard"] name = "filelock" version = "3.4.2" description = "A platform independent file lock." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -486,6 +510,7 @@ testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-co name = "flake8" version = "7.0.0" description = "the modular source code checker: pep8 pyflakes and co" +category = "main" optional = false python-versions = ">=3.8.1" files = [ @@ -502,6 +527,7 @@ pyflakes = ">=3.2.0,<3.3.0" name = "flake8-requirements" version = "2.1.0" description = "Package requirements checker, plugin for flake8" +category = "main" optional = false python-versions = "*" files = [ @@ -520,6 +546,7 @@ pyproject = ["Flake8-pyproject"] name = "flaky" version = "3.7.0" description = "Plugin for nose or pytest that automatically reruns flaky tests." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -531,6 +558,7 @@ files = [ name = "graphviz" version = "0.19.1" description = "Simple Python interface for Graphviz" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -547,6 +575,7 @@ test = ["coverage", "mock (>=4)", "pytest (>=6)", "pytest-cov", "pytest-mock (>= name = "hypothesis" version = "6.36.1" description = "A library for property-based testing" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -578,6 +607,7 @@ zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2021.5)"] name = "idna" version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -589,6 +619,7 @@ files = [ name = "importlib-resources" version = "5.12.0" description = "Read resources from Python packages" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -607,6 +638,7 @@ testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-chec name = "incremental" version = "21.3.0" description = "A small library that versions your Python projects." +category = "dev" optional = false python-versions = "*" files = [ @@ -621,6 +653,7 @@ scripts = ["click (>=6.0)", "twisted (>=16.4.0)"] name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" +category = "main" optional = false python-versions = "*" files = [ @@ -632,6 +665,7 @@ files = [ name = "inotify" version = "0.2.10" description = "An adapter to Linux kernel support for inotify directory-watching." +category = "main" optional = false python-versions = "*" files = [ @@ -646,6 +680,7 @@ nose = "*" name = "isort" version = "5.11.5" description = "A Python utility / library to sort Python imports." +category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -663,6 +698,7 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"] name = "jinja2" version = "3.1.4" description = "A very fast and expressive template engine." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -680,6 +716,7 @@ i18n = ["Babel (>=2.7)"] name = "lazy-object-proxy" version = "1.7.1" description = "A fast and thorough lazy object proxy." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -726,6 +763,7 @@ files = [ name = "libusb1" version = "3.0.0" description = "Pure-python wrapper for libusb-1.0" +category = "main" optional = false python-versions = "*" files = [ @@ -739,6 +777,7 @@ files = [ name = "mako" version = "1.1.6" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -757,6 +796,7 @@ lingua = ["lingua"] name = "markupsafe" version = "2.0.1" description = "Safely add untrusted strings to HTML/XML markup." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -835,6 +875,7 @@ files = [ name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -846,6 +887,7 @@ files = [ name = "mnemonic" version = "0.20" description = "Implementation of Bitcoin BIP-0039" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -857,6 +899,7 @@ files = [ name = "munch" version = "2.5.0" description = "A dot-accessible dictionary (a la JavaScript objects)" +category = "main" optional = false python-versions = "*" files = [ @@ -875,6 +918,7 @@ yaml = ["PyYAML (>=5.1.0)"] name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "main" optional = false python-versions = "*" files = [ @@ -886,6 +930,7 @@ files = [ name = "nanopb" version = "0.4.5.post1" description = "Nanopb is a small code-size Protocol Buffers implementation in ansi C. It is especially suitable for use in microcontrollers, but fits any memory restricted system." +category = "main" optional = false python-versions = ">=2.7" files = [ @@ -903,6 +948,7 @@ grpcio-tools = ["grpcio-tools (>=1.26.0rc1)"] name = "nose" version = "1.3.7" description = "nose extends unittest to make testing easier" +category = "main" optional = false python-versions = "*" files = [ @@ -915,6 +961,7 @@ files = [ name = "packaging" version = "23.2" description = "Core utilities for Python packages" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -926,6 +973,7 @@ files = [ name = "pathspec" version = "0.9.0" description = "Utility library for gitignore style pattern matching of file paths." +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ @@ -937,6 +985,7 @@ files = [ name = "pillow" version = "10.3.0" description = "Python Imaging Library (Fork)" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1023,6 +1072,7 @@ xmp = ["defusedxml"] name = "platformdirs" version = "2.6.1" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1038,6 +1088,7 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest- name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1053,6 +1104,7 @@ testing = ["pytest", "pytest-benchmark"] name = "protobuf" version = "3.19.4" description = "Protocol Buffers" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1088,6 +1140,7 @@ files = [ name = "py" version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1099,6 +1152,7 @@ files = [ name = "pyasn1" version = "0.4.8" description = "ASN.1 types and codecs" +category = "main" optional = false python-versions = "*" files = [ @@ -1110,6 +1164,7 @@ files = [ name = "pycodestyle" version = "2.11.1" description = "Python style guide checker" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1121,6 +1176,7 @@ files = [ name = "pycparser" version = "2.21" description = "C parser in Python" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1132,6 +1188,7 @@ files = [ name = "pyflakes" version = "3.2.0" description = "passive checker of Python programs" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1143,6 +1200,7 @@ files = [ name = "pylint" version = "2.17.7" description = "python code static checker" +category = "main" optional = false python-versions = ">=3.7.2" files = [ @@ -1172,6 +1230,7 @@ testutils = ["gitpython (>3)"] name = "pyserial" version = "3.5" description = "Python Serial Port Extension" +category = "main" optional = false python-versions = "*" files = [ @@ -1186,6 +1245,7 @@ cp2110 = ["hidapi"] name = "pytest" version = "6.2.5" description = "pytest: simple powerful testing with Python" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1210,6 +1270,7 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xm name = "pytest-forked" version = "1.4.0" description = "run tests in isolated forked subprocesses" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1225,6 +1286,7 @@ pytest = ">=3.10" name = "pytest-ordering" version = "0.6" description = "pytest plugin to run your tests in a specific order" +category = "main" optional = false python-versions = "*" files = [ @@ -1240,6 +1302,7 @@ pytest = "*" name = "pytest-random-order" version = "1.0.4" description = "Randomise the order in which pytest tests are run with some control over the randomness" +category = "main" optional = false python-versions = ">=3.5.0" files = [ @@ -1254,6 +1317,7 @@ pytest = ">=3.0.0" name = "pytest-timeout" version = "2.1.0" description = "pytest plugin to abort hanging tests" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1268,6 +1332,7 @@ pytest = ">=5.0.0" name = "pytest-xdist" version = "2.5.0" description = "pytest xdist plugin for distributed testing and loop-on-failing modes" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1289,6 +1354,7 @@ testing = ["filelock"] name = "python-bitcoinlib" version = "0.11.0" description = "The Swiss Army Knife of the Bitcoin protocol." +category = "main" optional = false python-versions = "*" files = [ @@ -1300,6 +1366,7 @@ files = [ name = "pyyaml" version = "6.0.1" description = "YAML parser and emitter for Python" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1360,6 +1427,7 @@ files = [ name = "requests" version = "2.32.0" description = "Python HTTP for Humans." +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1381,6 +1449,7 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "scan-build" version = "2.0.20" description = "static code analyzer wrapper for Clang." +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1392,6 +1461,7 @@ files = [ name = "scons" version = "4.3.0" description = "Open Source next-generation build tool." +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1406,6 +1476,7 @@ setuptools = "*" name = "setuptools" version = "67.6.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1420,28 +1491,24 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs ( [[package]] name = "shamir-mnemonic" -version = "0.2.2" +version = "0.3.0" description = "SLIP-39 Shamir Mnemonics" +category = "main" optional = false -python-versions = ">=3.6" +python-versions = "<4.0,>=3.6" files = [ - {file = "shamir-mnemonic-0.2.2.tar.gz", hash = "sha256:7fb9b592e5c518192c0b0caa2c2d82e342fddd186693bc64be9647eace1b9182"}, - {file = "shamir_mnemonic-0.2.2-py3-none-any.whl", hash = "sha256:7d9facea70379cad02bab18d4572c0fcd033c9d7effe5da095b9e0944bf5fbbf"}, + {file = "shamir_mnemonic-0.3.0-py3-none-any.whl", hash = "sha256:188c6b5bd00d5e756e12e2b186c3cb7c98ff7ff44df608d4c1d2077f6b6e730f"}, + {file = "shamir_mnemonic-0.3.0.tar.gz", hash = "sha256:bc04886a1ddfe2a64d8a3ec51abf0f664d98d5b557cc7e78a8ad2d10a1d87438"}, ] -[package.dependencies] -attrs = "*" -click = ">=7,<9" -colorama = "*" - [package.extras] -dev = ["black", "flake8", "isort"] -tests = ["pytest"] +cli = ["click (>=7,<9)"] [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1453,6 +1520,7 @@ files = [ name = "sortedcontainers" version = "2.4.0" description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +category = "main" optional = false python-versions = "*" files = [ @@ -1464,6 +1532,7 @@ files = [ name = "termcolor" version = "2.3.0" description = "ANSI color formatting for output in terminal" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1478,6 +1547,7 @@ tests = ["pytest", "pytest-cov"] name = "toiftool" version = "0.1.0" description = "" +category = "main" optional = false python-versions = ">=3.8" files = [] @@ -1496,6 +1566,7 @@ url = "python/tools/toiftool" name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1507,6 +1578,7 @@ files = [ name = "tomli" version = "1.2.3" description = "A lil' TOML parser" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1518,6 +1590,7 @@ files = [ name = "tomlkit" version = "0.12.4" description = "Style preserving TOML library" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1529,6 +1602,7 @@ files = [ name = "towncrier" version = "23.6.0" description = "Building newsfiles for your project." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1551,6 +1625,7 @@ dev = ["furo", "packaging", "sphinx (>=5)", "twisted"] name = "tox" version = "3.24.5" description = "tox is a generic virtualenv management and test command line tool" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ @@ -1576,6 +1651,7 @@ testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psu name = "trezor" version = "0.13.9" description = "Python library for communicating with Trezor Hardware Wallet" +category = "main" optional = false python-versions = ">=3.6" files = [] @@ -1607,6 +1683,7 @@ url = "python" name = "trezor-pylint-plugin" version = "0.1.0" description = "" +category = "main" optional = false python-versions = "^3.8" files = [] @@ -1624,6 +1701,7 @@ url = "tools/trezor-pylint-plugin" name = "typing-extensions" version = "4.0.1" description = "Backported and Experimental Type Hints for Python 3.6+" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1635,6 +1713,7 @@ files = [ name = "uhid-freebsd" version = "1.2.2" description = "Get information on FreeBSD uhid devices." +category = "main" optional = false python-versions = "*" files = [ @@ -1645,6 +1724,7 @@ files = [ name = "urllib3" version = "1.26.18" description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ @@ -1661,6 +1741,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] name = "virtualenv" version = "20.13.1" description = "Virtual Python Environment builder" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ @@ -1682,6 +1763,7 @@ testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", name = "vulture" version = "2.6" description = "Find dead code" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1696,6 +1778,7 @@ toml = "*" name = "wrapt" version = "1.16.0" description = "Module for decorators, wrappers and monkey patching." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1775,6 +1858,7 @@ files = [ name = "yamllint" version = "1.26.3" description = "A linter for YAML files." +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1790,6 +1874,7 @@ setuptools = "*" name = "zipp" version = "3.7.0" description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1804,4 +1889,4 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "30cec0ead12f35e594f7f69e789e0183aeb74394ab81c5b4e2c84de9929fc4b0" +content-hash = "089373acc7cc350a441e8a935c011b76c1b31202a8972a42dc80bb8e2562c0e9" diff --git a/pyproject.toml b/pyproject.toml index 728db6fd60..e4e7dfd448 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dominate = "*" pyserial = "*" ## test requirements -shamir-mnemonic = "^0.2.1" +shamir-mnemonic = "^0.3.0" fido2 = "^0.8.0" python-bitcoinlib = "^0.11.0"