1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-18 04:18:10 +00:00

feat(core): Support extendable backup flag in SLIP-39.

This commit is contained in:
Andrew Kozlik 2024-04-30 20:26:46 +02:00 committed by Andrew Kozlik
parent c0b3a2c26b
commit 9d0d1b3402
18 changed files with 393 additions and 162 deletions

View File

@ -0,0 +1 @@
Support extendable backup flag in SLIP-39.

View File

@ -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<usize> {
get_length(HOMESCREEN)

View File

@ -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,
)

View File

@ -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(

View File

@ -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()

View File

@ -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

View File

@ -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,
)

View File

@ -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.

View File

@ -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)

View File

@ -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(),

View File

@ -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",
],
]

View File

@ -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)

View File

@ -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)

View File

@ -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
)

View File

@ -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:

View File

@ -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.

111
poetry.lock generated
View File

@ -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"

View File

@ -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"