From 5c37e8dae272b191eeb9e3c20da56ebf8b980f4f Mon Sep 17 00:00:00 2001 From: Tomas Susanka Date: Thu, 7 Mar 2019 14:49:23 +0100 Subject: [PATCH] common: introduce mnemonic module To provide another layer between seed and mnemonic. First step to introduce SLIP-39 --- src/apps/cardano/seed.py | 4 +-- src/apps/common/mnemonic.py | 46 ++++++++++++++++++++++++++ src/apps/common/seed.py | 23 +++---------- src/apps/common/storage.py | 31 +++++++++++++---- src/apps/debug/__init__.py | 4 +-- src/apps/management/backup_device.py | 8 ++--- src/apps/management/load_device.py | 10 ++++-- src/apps/management/recovery_device.py | 20 +++++++---- src/apps/management/reset_device.py | 19 ++++++----- 9 files changed, 116 insertions(+), 49 deletions(-) create mode 100644 src/apps/common/mnemonic.py diff --git a/src/apps/cardano/seed.py b/src/apps/cardano/seed.py index 102c19b69..26b32ada7 100644 --- a/src/apps/cardano/seed.py +++ b/src/apps/cardano/seed.py @@ -2,7 +2,7 @@ from trezor import wire from trezor.crypto import bip32 from apps.cardano import SEED_NAMESPACE -from apps.common import cache, storage +from apps.common import cache, mnemonic, storage from apps.common.request_passphrase import protect_by_passphrase @@ -33,7 +33,7 @@ async def get_keychain(ctx: wire.Context) -> Keychain: if passphrase is None: passphrase = await protect_by_passphrase(ctx) cache.set_passphrase(passphrase) - root = bip32.from_mnemonic_cardano(storage.get_mnemonic(), passphrase) + root = bip32.from_mnemonic_cardano(mnemonic.restore(), passphrase) # derive the namespaced root node for i in SEED_NAMESPACE[0]: diff --git a/src/apps/common/mnemonic.py b/src/apps/common/mnemonic.py new file mode 100644 index 000000000..301fa23a8 --- /dev/null +++ b/src/apps/common/mnemonic.py @@ -0,0 +1,46 @@ +from trezor import ui +from trezor.crypto import bip39 + +from apps.common import storage + +TYPE_BIP39 = 0 + + +def get() -> (bytes, int): + mnemonic_secret = storage.get_mnemonic_secret() + mnemonic_type = storage.get_mnemonic_type() + return mnemonic_secret, mnemonic_type + + +def get_seed(passphrase: str = ""): + secret, mnemonic_type = get() + _start_progress() + if mnemonic_type == TYPE_BIP39: + return bip39.seed(secret.decode(), passphrase, _render_progress) + + +def process(mnemonics: list, mnemonic_type: int): + if mnemonic_type == TYPE_BIP39: + return mnemonics[0].encode() + else: + raise RuntimeError("Unknown mnemonic type") + + +def restore() -> str: + secret, mnemonic_type = get() + if mnemonic_type == TYPE_BIP39: + return secret.decode() + + +def _start_progress(): + ui.backlight_slide_sync(ui.BACKLIGHT_DIM) + ui.display.clear() + ui.header("Please wait") + ui.display.refresh() + ui.backlight_slide_sync(ui.BACKLIGHT_NORMAL) + + +def _render_progress(progress: int, total: int): + p = int(1000 * progress / total) + ui.display.loader(p, 18, ui.WHITE, ui.BG) + ui.display.refresh() diff --git a/src/apps/common/seed.py b/src/apps/common/seed.py index 54e0c6b76..4898c89f6 100644 --- a/src/apps/common/seed.py +++ b/src/apps/common/seed.py @@ -1,7 +1,7 @@ from trezor import ui, wire -from trezor.crypto import bip32, bip39 +from trezor.crypto import bip32 -from apps.common import cache, storage +from apps.common import cache, mnemonic, storage from apps.common.request_passphrase import protect_by_passphrase allow = list @@ -66,32 +66,17 @@ async def _compute_seed(ctx: wire.Context) -> bytes: if passphrase is None: passphrase = await protect_by_passphrase(ctx) cache.set_passphrase(passphrase) - _start_bip39_progress() - seed = bip39.seed(storage.get_mnemonic(), passphrase, _render_bip39_progress) + seed = mnemonic.get_seed(passphrase) cache.set_seed(seed) return seed -def _start_bip39_progress(): - ui.backlight_slide_sync(ui.BACKLIGHT_DIM) - ui.display.clear() - ui.header("Please wait") - ui.display.refresh() - ui.backlight_slide_sync(ui.BACKLIGHT_NORMAL) - - -def _render_bip39_progress(progress: int, total: int): - p = int(1000 * progress / total) - ui.display.loader(p, 18, ui.WHITE, ui.BG) - ui.display.refresh() - - def derive_node_without_passphrase( path: list, curve_name: str = "secp256k1" ) -> bip32.HDNode: if not storage.is_initialized(): raise Exception("Device is not initialized") - seed = bip39.seed(storage.get_mnemonic(), "") + seed = mnemonic.get_seed() node = bip32.from_seed(seed, curve_name) node.derive_path(path) return node diff --git a/src/apps/common/storage.py b/src/apps/common/storage.py index e30f263dc..f477439f8 100644 --- a/src/apps/common/storage.py +++ b/src/apps/common/storage.py @@ -18,7 +18,7 @@ _COUNTER_TAIL_LEN = 8 _APP = const(0x01) # app namespace _DEVICE_ID = const(0x00) # bytes _VERSION = const(0x01) # int -_MNEMONIC = const(0x02) # str +_MNEMONIC_SECRET = const(0x02) # bytes _LANGUAGE = const(0x03) # str _LABEL = const(0x04) # str _USE_PASSPHRASE = const(0x05) # bool (0x01 or empty) @@ -30,6 +30,7 @@ _PASSPHRASE_SOURCE = const(0x0A) # int _UNFINISHED_BACKUP = const(0x0B) # bool (0x01 or empty) _AUTOLOCK_DELAY_MS = const(0x0C) # int _NO_BACKUP = const(0x0D) # bool (0x01 or empty) +_MNEMONIC_TYPE = const(0x0E) # int # fmt: on @@ -44,6 +45,17 @@ def _get_bool(app: int, key: int, public: bool = False) -> bool: return config.get(app, key, public) == _TRUE_BYTE +def _set_uint8(app: int, key: int, val: int): + config.set(app, key, val.to_bytes(1, "big")) + + +def _get_uint8(app: int, key: int) -> int: + val = config.get(app, key) + if not val: + return None + return int.from_bytes(val, "big") + + def _new_device_id() -> str: return hexlify(random.bytes(12)).decode().upper() @@ -67,11 +79,15 @@ def get_label() -> str: return label.decode() -def get_mnemonic() -> str: - mnemonic = config.get(_APP, _MNEMONIC) +def get_mnemonic_secret() -> bytes: + mnemonic = config.get(_APP, _MNEMONIC_SECRET) if mnemonic is None: return None - return mnemonic.decode() + return mnemonic + + +def get_mnemonic_type() -> int: + return _get_uint8(_APP, _MNEMONIC_TYPE) def has_passphrase() -> bool: @@ -82,8 +98,11 @@ def get_homescreen() -> bytes: return config.get(_APP, _HOMESCREEN, True) # public -def load_mnemonic(mnemonic: str, needs_backup: bool, no_backup: bool) -> None: - config.set(_APP, _MNEMONIC, mnemonic.encode()) +def store_mnemonic( + secret: bytes, mnemonic_type: int, needs_backup: bool, no_backup: bool +) -> None: + config.set(_APP, _MNEMONIC_SECRET, secret) + _set_uint8(_APP, _MNEMONIC_TYPE, mnemonic_type) config.set(_APP, _VERSION, _STORAGE_VERSION) _set_bool(_APP, _NO_BACKUP, no_backup) if not no_backup: diff --git a/src/apps/debug/__init__.py b/src/apps/debug/__init__.py index ea47f5367..58c031bef 100644 --- a/src/apps/debug/__init__.py +++ b/src/apps/debug/__init__.py @@ -9,7 +9,7 @@ if __debug__: from trezor.messages.DebugLinkState import DebugLinkState from trezor.ui import confirm, swipe from trezor.wire import register, protobuf_workflow - from apps.common import storage + from apps.common import storage, mnemonic reset_internal_entropy = None reset_current_words = None @@ -29,7 +29,7 @@ if __debug__: async def dispatch_DebugLinkGetState(ctx, msg): m = DebugLinkState() - m.mnemonic = storage.get_mnemonic() + m.mnemonic_secret, m.mnemonic_type = mnemonic.get() m.passphrase_protection = storage.has_passphrase() m.reset_word_pos = reset_word_index m.reset_entropy = reset_internal_entropy diff --git a/src/apps/management/backup_device.py b/src/apps/management/backup_device.py index 4acd929c6..d8c3b7344 100644 --- a/src/apps/management/backup_device.py +++ b/src/apps/management/backup_device.py @@ -1,7 +1,7 @@ from trezor import wire from trezor.messages.Success import Success -from apps.common import storage +from apps.common import mnemonic, storage from apps.management.reset_device import ( check_mnemonic, show_mnemonic, @@ -16,7 +16,7 @@ async def backup_device(ctx, msg): if not storage.needs_backup(): raise wire.ProcessError("Seed already backed up") - mnemonic = storage.get_mnemonic() + words = mnemonic.restore() # warn user about mnemonic safety await show_warning(ctx) @@ -26,8 +26,8 @@ async def backup_device(ctx, msg): while True: # show mnemonic and require confirmation of a random word - await show_mnemonic(ctx, mnemonic) - if await check_mnemonic(ctx, mnemonic): + await show_mnemonic(ctx, words) + if await check_mnemonic(ctx, words): break await show_wrong_entry(ctx) diff --git a/src/apps/management/load_device.py b/src/apps/management/load_device.py index 377a69843..a0b1b971a 100644 --- a/src/apps/management/load_device.py +++ b/src/apps/management/load_device.py @@ -4,7 +4,7 @@ from trezor.messages.Success import Success from trezor.pin import pin_to_int from trezor.ui.text import Text -from apps.common import storage +from apps.common import mnemonic, storage from apps.common.confirm import require_confirm @@ -24,7 +24,13 @@ async def load_device(ctx, msg): text.normal("Continue only if you", "know what you are doing!") await require_confirm(ctx, text) - storage.load_mnemonic(mnemonic=msg.mnemonic, needs_backup=True, no_backup=False) + secret = mnemonic.process([msg.mnemonic], mnemonic.TYPE_BIP39) + storage.store_mnemonic( + secret=secret, + mnemonic_type=mnemonic.TYPE_BIP39, + needs_backup=True, + no_backup=False, + ) storage.load_settings(use_passphrase=msg.passphrase_protection, label=msg.label) if msg.pin: config.change_pin(pin_to_int(""), pin_to_int(msg.pin)) diff --git a/src/apps/management/recovery_device.py b/src/apps/management/recovery_device.py index 271fc8de3..c62dd2301 100644 --- a/src/apps/management/recovery_device.py +++ b/src/apps/management/recovery_device.py @@ -15,7 +15,7 @@ from trezor.ui.text import Text from trezor.ui.word_select import WordSelector from trezor.utils import consteq, format_ordinal -from apps.common import storage +from apps.common import mnemonic, storage from apps.common.confirm import require_confirm from apps.management.change_pin import request_pin_ack, request_pin_confirm @@ -47,11 +47,11 @@ async def recovery_device(ctx, msg): wordcount = await request_wordcount(ctx) # ask for mnemonic words one by one - mnemonic = await request_mnemonic(ctx, wordcount) + words = await request_mnemonic(ctx, wordcount) # check mnemonic validity if msg.enforce_wordlist or msg.dry_run: - if not bip39.check(mnemonic): + if not bip39.check(words): raise wire.ProcessError("Mnemonic is not valid") # ask for pin repeatedly @@ -60,10 +60,13 @@ async def recovery_device(ctx, msg): else: newpin = "" + secret = mnemonic.process([words], mnemonic.TYPE_BIP39) + # dry run if msg.dry_run: - digest_input = sha256(mnemonic).digest() - digest_stored = sha256(storage.get_mnemonic()).digest() + digest_input = sha256(secret).digest() + stored, _ = mnemonic.get() + digest_stored = sha256(stored).digest() if consteq(digest_stored, digest_input): return Success( message="The seed is valid and matches the one in the device" @@ -78,7 +81,12 @@ async def recovery_device(ctx, msg): config.change_pin(pin_to_int(""), pin_to_int(newpin)) storage.set_u2f_counter(msg.u2f_counter) storage.load_settings(label=msg.label, use_passphrase=msg.passphrase_protection) - storage.load_mnemonic(mnemonic=mnemonic, needs_backup=False, no_backup=False) + storage.store_mnemonic( + secret=secret, + mnemonic_type=mnemonic.TYPE_BIP39, + needs_backup=False, + no_backup=False, + ) return Success(message="Device recovered") diff --git a/src/apps/management/reset_device.py b/src/apps/management/reset_device.py index becc2c27f..6d4862f3a 100644 --- a/src/apps/management/reset_device.py +++ b/src/apps/management/reset_device.py @@ -14,7 +14,7 @@ from trezor.ui.scroll import Scrollpage, animate_swipe, paginate from trezor.ui.text import Text from trezor.utils import chunks, format_ordinal -from apps.common import storage +from apps.common import mnemonic, storage from apps.common.confirm import require_confirm from apps.management.change_pin import request_pin_confirm @@ -59,7 +59,7 @@ async def reset_device(ctx, msg): # request external entropy and compute mnemonic ent_ack = await ctx.call(EntropyRequest(), MessageType.EntropyAck) - mnemonic = generate_mnemonic(msg.strength, internal_ent, ent_ack.entropy) + words = generate_mnemonic(msg.strength, internal_ent, ent_ack.entropy) if not msg.skip_backup and not msg.no_backup: # require confirmation of the mnemonic safety @@ -67,8 +67,8 @@ async def reset_device(ctx, msg): # show mnemonic and require confirmation of a random word while True: - await show_mnemonic(ctx, mnemonic) - if await check_mnemonic(ctx, mnemonic): + await show_mnemonic(ctx, words) + if await check_mnemonic(ctx, words): break await show_wrong_entry(ctx) @@ -77,10 +77,14 @@ async def reset_device(ctx, msg): if not config.change_pin(pin_to_int(""), pin_to_int(newpin)): raise wire.ProcessError("Could not change PIN") + secret = mnemonic.process([words], mnemonic.TYPE_BIP39) # write settings and mnemonic into storage storage.load_settings(label=msg.label, use_passphrase=msg.passphrase_protection) - storage.load_mnemonic( - mnemonic=mnemonic, needs_backup=msg.skip_backup, no_backup=msg.no_backup + storage.store_mnemonic( + secret=secret, + mnemonic_type=mnemonic.TYPE_BIP39, + needs_backup=msg.skip_backup, + no_backup=msg.no_backup, ) # show success message. if we skipped backup, it's possible that homescreen @@ -98,8 +102,7 @@ def generate_mnemonic(strength: int, int_entropy: bytes, ext_entropy: bytes) -> ehash.update(int_entropy) ehash.update(ext_entropy) entropy = ehash.digest() - mnemonic = bip39.from_data(entropy[: strength // 8]) - return mnemonic + return bip39.from_data(entropy[: strength // 8]) async def show_warning(ctx):