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

Andrew Kozlik 1 month ago
parent 82e62cf41c
commit 116935003e

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

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

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

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

@ -122,6 +122,10 @@ async def _finish_recovery_dry_run(secret: bytes, backup_type: BackupType) -> Su
storage_device.get_slip39_identifier()
== storage_recovery.get_slip39_identifier()
)
result &= (
storage_device.get_slip39_extendable()
== storage_recovery.get_slip39_extendable()
)
result &= (
storage_device.get_slip39_iteration_exponent()
== storage_recovery.get_slip39_iteration_exponent()
@ -149,11 +153,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()

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

@ -77,6 +77,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.
@ -187,6 +188,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
@ -196,6 +198,7 @@ def _get_slip39_mnemonics(
group_threshold,
groups,
identifier,
extendable,
iteration_exponent,
encrypted_master_secret,
)

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

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

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

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

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

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

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

@ -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,
)
# 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)
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,
)
# 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,
)
# 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, _, _, 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:

@ -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.
@ -75,8 +75,9 @@ 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`.
3. Store the default iteration exponent `1` as `_SLIP39_ITERATION_EXPONENT`.
4. The storage now contains all parameters required for seed derivation.
3. Store the default extendable backup flag `True` as `_SLIP39_EXTENDABLE`.
4. Store the default iteration exponent `1` as `_SLIP39_ITERATION_EXPONENT`.
5. The storage now contains all parameters required for seed derivation.
### Seed derivation
@ -101,5 +102,6 @@ This process does not use passphrase.
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.
5. Store the extendable backup flag as `_SLIP39_EXTENDABLE`.
6. Store the iteration exponent as `_SLIP39_ITERATION_EXPONENT`.
7. The storage now contains all parameters required for seed derivation.

Loading…
Cancel
Save