diff --git a/core/src/apps/cardano/seed.py b/core/src/apps/cardano/seed.py index c5290dba7..5cd83de9d 100644 --- a/core/src/apps/cardano/seed.py +++ b/core/src/apps/cardano/seed.py @@ -1,10 +1,10 @@ from storage import cache, device from trezor import wire -from trezor.crypto import bip32 +from trezor.crypto import bip32, cardano from apps.common import mnemonic from apps.common.passphrase import get as get_passphrase -from apps.common.seed import get_seed +from apps.common.seed import get_seed, derive_and_store_roots from .helpers import paths @@ -24,12 +24,19 @@ class Keychain: """ def __init__(self, root: bip32.HDNode) -> None: - self.byron_root = derive_path_cardano(root, paths.BYRON_ROOT) - self.shelley_root = derive_path_cardano(root, paths.SHELLEY_ROOT) - self.multisig_root = derive_path_cardano(root, paths.MULTISIG_ROOT) - self.minting_root = derive_path_cardano(root, paths.MINTING_ROOT) + self.byron_root = self._derive_path(root, paths.BYRON_ROOT) + self.shelley_root = self._derive_path(root, paths.SHELLEY_ROOT) + self.multisig_root = self._derive_path(root, paths.MULTISIG_ROOT) + self.minting_root = self._derive_path(root, paths.MINTING_ROOT) root.__del__() + @staticmethod + def _derive_path(root: bip32.HDNode, path: Bip32Path) -> bip32.HDNode: + """Clone and derive path from the root.""" + node = root.clone() + node.derive_path(path) + return node + def verify_path(self, path: Bip32Path) -> None: if not self.is_in_keychain(path): raise wire.DataError("Forbidden key path") @@ -67,7 +74,7 @@ class Keychain: suffix = node_path[len(paths.SHELLEY_ROOT) :] # derive child node from the root - return derive_path_cardano(path_root, suffix) + return self._derive_path(path_root, suffix) # XXX the root node remains in session cache so we should not delete it # def __del__(self) -> None: @@ -90,28 +97,35 @@ def is_minting_path(path: Bip32Path) -> bool: return path[: len(paths.MINTING_ROOT)] == paths.MINTING_ROOT -def derive_path_cardano(root: bip32.HDNode, path: Bip32Path) -> bip32.HDNode: - node = root.clone() - for i in path: - node.derive_cardano(i) - return node +def derive_and_store_secret(passphrase: str) -> None: + assert device.is_initialized() + assert cache.get(cache.APP_COMMON_DERIVE_CARDANO) + + if not mnemonic.is_bip39(): + # nothing to do for SLIP-39, where we can derive the root from the main seed + return + icarus_trezor_secret = mnemonic.derive_cardano_icarus_trezor(passphrase) + cache.set(cache.APP_CARDANO_SECRET, icarus_trezor_secret) -@cache.stored_async(cache.APP_CARDANO_PASSPHRASE) -async def _get_passphrase(ctx: wire.Context) -> bytes: - return (await get_passphrase(ctx)).encode() + +@cache.stored_async(cache.APP_CARDANO_SECRET) +async def _get_secret(ctx: wire.Context) -> bytes: + await derive_and_store_roots(ctx) + secret = cache.get(cache.APP_CARDANO_SECRET) + assert secret is not None + return secret async def _get_keychain_bip39(ctx: wire.Context) -> Keychain: if not device.is_initialized(): raise wire.NotInitialized("Device is not initialized") - # ask for passphrase, loading from cache if necessary - passphrase = await _get_passphrase(ctx) - # derive the root node from mnemonic and passphrase via Cardano Icarus algorithm - secret_bytes = mnemonic.get_secret() - assert secret_bytes is not None - root = bip32.from_mnemonic_cardano(secret_bytes.decode(), passphrase.decode()) + if not cache.get(cache.APP_COMMON_DERIVE_CARDANO): + raise wire.ProcessError("Cardano derivation is not enabled for this session") + + secret = await _get_secret(ctx) + root = cardano.from_secret(secret) return Keychain(root) @@ -121,7 +135,7 @@ async def get_keychain(ctx: wire.Context) -> Keychain: else: # derive the root node via SLIP-0023 https://github.com/satoshilabs/slips/blob/master/slip-0022.md seed = await get_seed(ctx) - return Keychain(bip32.from_seed(seed, "ed25519 cardano seed")) + return Keychain(cardano.from_seed_slip23(seed)) def with_keychain(func: HandlerWithKeychain[MsgIn, MsgOut]) -> Handler[MsgIn, MsgOut]: diff --git a/core/src/apps/common/mnemonic.py b/core/src/apps/common/mnemonic.py index 3e2bd571e..32e108fd5 100644 --- a/core/src/apps/common/mnemonic.py +++ b/core/src/apps/common/mnemonic.py @@ -26,7 +26,7 @@ def is_bip39() -> bool: def get_seed(passphrase: str = "", progress_bar: bool = True) -> bytes: mnemonic_secret = get_secret() if mnemonic_secret is None: - raise ValueError("Mnemonic not set") + raise ValueError # Mnemonic not set render_func = None if progress_bar and not utils.DISABLE_ANIMATION: @@ -57,6 +57,30 @@ def get_seed(passphrase: str = "", progress_bar: bool = True) -> bytes: return seed +if not utils.BITCOIN_ONLY: + + def derive_cardano_icarus_trezor( + passphrase: str = "", progress_bar: bool = True + ) -> bytes: + if not is_bip39(): + raise ValueError # should not be called for SLIP-39 + + mnemonic_secret = get_secret() + if mnemonic_secret is None: + raise ValueError("Mnemonic not set") + + render_func = None + if progress_bar and not utils.DISABLE_ANIMATION: + _start_progress() + render_func = _render_progress + + from trezor.crypto import cardano + + return cardano.derive_icarus_trezor( + mnemonic_secret, passphrase, render_func + ) + + def _start_progress() -> None: from trezor.ui.layouts import draw_simple_text diff --git a/core/src/apps/common/seed.py b/core/src/apps/common/seed.py index 0a7948249..6269b88b0 100644 --- a/core/src/apps/common/seed.py +++ b/core/src/apps/common/seed.py @@ -1,5 +1,5 @@ from storage import cache, device -from trezor import wire +from trezor import utils, wire from trezor.crypto import bip32, hmac from . import mnemonic @@ -40,12 +40,50 @@ class Slip21Node: return Slip21Node(data=self.data) -@cache.stored_async(cache.APP_COMMON_SEED) -async def get_seed(ctx: wire.Context) -> bytes: - if not device.is_initialized(): - raise wire.NotInitialized("Device is not initialized") - passphrase = await get_passphrase(ctx) - return mnemonic.get_seed(passphrase) +if not utils.BITCOIN_ONLY: + # === Cardano variant === + # We want to derive both the normal seed and the Cardano seed together, AND + # expose a method for Cardano to do the same + + async def derive_and_store_roots(ctx: wire.Context) -> None: + if not device.is_initialized(): + raise wire.NotInitialized("Device is not initialized") + + need_seed = not cache.is_set(cache.APP_COMMON_SEED) + need_cardano_secret = cache.get( + cache.APP_COMMON_DERIVE_CARDANO + ) and not cache.is_set(cache.APP_CARDANO_SECRET) + + if not need_seed and not need_cardano_secret: + return + + passphrase = await get_passphrase(ctx) + + if need_seed: + common_seed = mnemonic.get_seed(passphrase) + cache.set(cache.APP_COMMON_SEED, common_seed) + + if need_cardano_secret: + from apps.cardano.seed import derive_and_store_secret + + derive_and_store_secret(passphrase) + + @cache.stored_async(cache.APP_COMMON_SEED) + async def get_seed(ctx: wire.Context) -> bytes: + await derive_and_store_roots(ctx) + common_seed = cache.get(cache.APP_COMMON_SEED) + assert common_seed is not None + return common_seed + + +else: + # === Bitcoin-only variant === + # We use the simple version of `get_seed` that never needs to derive anything else. + + @cache.stored_async(cache.APP_COMMON_SEED) + async def get_seed(ctx: wire.Context) -> bytes: + passphrase = await get_passphrase(ctx) + return mnemonic.get_seed(passphrase) @cache.stored(cache.APP_COMMON_SEED_WITHOUT_PASSPHRASE) diff --git a/core/src/storage/cache.py b/core/src/storage/cache.py index f18328453..77693a7b2 100644 --- a/core/src/storage/cache.py +++ b/core/src/storage/cache.py @@ -21,7 +21,7 @@ _SESSION_ID_LENGTH = 32 # Traditional cache keys APP_COMMON_SEED = 0 APP_COMMON_DERIVE_CARDANO = 1 -APP_CARDANO_XPRV = 2 +APP_CARDANO_SECRET = 2 APP_MONERO_LIVE_REFRESH = 3 APP_COMMON_AUTHORIZATION_TYPE = 4 APP_COMMON_AUTHORIZATION_DATA = 5 @@ -69,11 +69,15 @@ class DataCache: ... def get(self, key: int, default: T | None = None) -> bytes | T | None: # noqa: F811 - utils.ensure(key < len(self.fields), f"failed to load key {key}") + utils.ensure(key < len(self.fields)) if self.data[key][0] != 1: return default return bytes(self.data[key][1:]) + def is_set(self, key: int) -> bool: + utils.ensure(key < len(self.fields)) + return self.data[key][0] == 1 + def delete(self, key: int) -> None: utils.ensure(key < len(self.fields)) self.data[key][:] = b"\x00" @@ -89,7 +93,7 @@ class SessionCache(DataCache): self.fields = ( 64, # APP_COMMON_SEED 1, # APP_COMMON_DERIVE_CARDANO - 96, # APP_CARDANO_XPRV + 96, # APP_CARDANO_SECRET 1, # APP_MONERO_LIVE_REFRESH 2, # APP_COMMON_AUTHORIZATION_TYPE 128, # APP_COMMON_AUTHORIZATION_DATA @@ -227,6 +231,14 @@ def get(key: int, default: T | None = None) -> bytes | T | None: # noqa: F811 return _SESSIONS[_active_session_idx].get(key, default) +def is_set(key: int) -> bool: + if key & _SESSIONLESS_FLAG: + return _SESSIONLESS_CACHE.is_set(key ^ _SESSIONLESS_FLAG) + if _active_session_idx is None: + raise InvalidSessionError + return _SESSIONS[_active_session_idx].is_set(key) + + def delete(key: int) -> None: if key & _SESSIONLESS_FLAG: return _SESSIONLESS_CACHE.delete(key ^ _SESSIONLESS_FLAG)