feat(core): derive Cardano seed together with normal seed, if requested

pull/1882/head
matejcik 3 years ago committed by matejcik
parent 387466e073
commit f818f4bc23

@ -1,10 +1,10 @@
from storage import cache, device from storage import cache, device
from trezor import wire from trezor import wire
from trezor.crypto import bip32 from trezor.crypto import bip32, cardano
from apps.common import mnemonic from apps.common import mnemonic
from apps.common.passphrase import get as get_passphrase 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 from .helpers import paths
@ -24,12 +24,19 @@ class Keychain:
""" """
def __init__(self, root: bip32.HDNode) -> None: def __init__(self, root: bip32.HDNode) -> None:
self.byron_root = derive_path_cardano(root, paths.BYRON_ROOT) self.byron_root = self._derive_path(root, paths.BYRON_ROOT)
self.shelley_root = derive_path_cardano(root, paths.SHELLEY_ROOT) self.shelley_root = self._derive_path(root, paths.SHELLEY_ROOT)
self.multisig_root = derive_path_cardano(root, paths.MULTISIG_ROOT) self.multisig_root = self._derive_path(root, paths.MULTISIG_ROOT)
self.minting_root = derive_path_cardano(root, paths.MINTING_ROOT) self.minting_root = self._derive_path(root, paths.MINTING_ROOT)
root.__del__() 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: def verify_path(self, path: Bip32Path) -> None:
if not self.is_in_keychain(path): if not self.is_in_keychain(path):
raise wire.DataError("Forbidden key path") raise wire.DataError("Forbidden key path")
@ -67,7 +74,7 @@ class Keychain:
suffix = node_path[len(paths.SHELLEY_ROOT) :] suffix = node_path[len(paths.SHELLEY_ROOT) :]
# derive child node from the 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 # XXX the root node remains in session cache so we should not delete it
# def __del__(self) -> None: # def __del__(self) -> None:
@ -90,28 +97,35 @@ def is_minting_path(path: Bip32Path) -> bool:
return path[: len(paths.MINTING_ROOT)] == paths.MINTING_ROOT return path[: len(paths.MINTING_ROOT)] == paths.MINTING_ROOT
def derive_path_cardano(root: bip32.HDNode, path: Bip32Path) -> bip32.HDNode: def derive_and_store_secret(passphrase: str) -> None:
node = root.clone() assert device.is_initialized()
for i in path: assert cache.get(cache.APP_COMMON_DERIVE_CARDANO)
node.derive_cardano(i)
return node 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: @cache.stored_async(cache.APP_CARDANO_SECRET)
return (await get_passphrase(ctx)).encode() 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: async def _get_keychain_bip39(ctx: wire.Context) -> Keychain:
if not device.is_initialized(): if not device.is_initialized():
raise wire.NotInitialized("Device is not initialized") raise wire.NotInitialized("Device is not initialized")
# ask for passphrase, loading from cache if necessary if not cache.get(cache.APP_COMMON_DERIVE_CARDANO):
passphrase = await _get_passphrase(ctx) raise wire.ProcessError("Cardano derivation is not enabled for this session")
# derive the root node from mnemonic and passphrase via Cardano Icarus algorithm
secret_bytes = mnemonic.get_secret() secret = await _get_secret(ctx)
assert secret_bytes is not None root = cardano.from_secret(secret)
root = bip32.from_mnemonic_cardano(secret_bytes.decode(), passphrase.decode())
return Keychain(root) return Keychain(root)
@ -121,7 +135,7 @@ async def get_keychain(ctx: wire.Context) -> Keychain:
else: else:
# derive the root node via SLIP-0023 https://github.com/satoshilabs/slips/blob/master/slip-0022.md # derive the root node via SLIP-0023 https://github.com/satoshilabs/slips/blob/master/slip-0022.md
seed = await get_seed(ctx) 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]: def with_keychain(func: HandlerWithKeychain[MsgIn, MsgOut]) -> Handler[MsgIn, MsgOut]:

@ -26,7 +26,7 @@ def is_bip39() -> bool:
def get_seed(passphrase: str = "", progress_bar: bool = True) -> bytes: def get_seed(passphrase: str = "", progress_bar: bool = True) -> bytes:
mnemonic_secret = get_secret() mnemonic_secret = get_secret()
if mnemonic_secret is None: if mnemonic_secret is None:
raise ValueError("Mnemonic not set") raise ValueError # Mnemonic not set
render_func = None render_func = None
if progress_bar and not utils.DISABLE_ANIMATION: if progress_bar and not utils.DISABLE_ANIMATION:
@ -57,6 +57,30 @@ def get_seed(passphrase: str = "", progress_bar: bool = True) -> bytes:
return seed 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: def _start_progress() -> None:
from trezor.ui.layouts import draw_simple_text from trezor.ui.layouts import draw_simple_text

@ -1,5 +1,5 @@
from storage import cache, device from storage import cache, device
from trezor import wire from trezor import utils, wire
from trezor.crypto import bip32, hmac from trezor.crypto import bip32, hmac
from . import mnemonic from . import mnemonic
@ -40,12 +40,50 @@ class Slip21Node:
return Slip21Node(data=self.data) return Slip21Node(data=self.data)
@cache.stored_async(cache.APP_COMMON_SEED) if not utils.BITCOIN_ONLY:
async def get_seed(ctx: wire.Context) -> bytes: # === Cardano variant ===
if not device.is_initialized(): # We want to derive both the normal seed and the Cardano seed together, AND
raise wire.NotInitialized("Device is not initialized") # expose a method for Cardano to do the same
passphrase = await get_passphrase(ctx)
return mnemonic.get_seed(passphrase) 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) @cache.stored(cache.APP_COMMON_SEED_WITHOUT_PASSPHRASE)

@ -21,7 +21,7 @@ _SESSION_ID_LENGTH = 32
# Traditional cache keys # Traditional cache keys
APP_COMMON_SEED = 0 APP_COMMON_SEED = 0
APP_COMMON_DERIVE_CARDANO = 1 APP_COMMON_DERIVE_CARDANO = 1
APP_CARDANO_XPRV = 2 APP_CARDANO_SECRET = 2
APP_MONERO_LIVE_REFRESH = 3 APP_MONERO_LIVE_REFRESH = 3
APP_COMMON_AUTHORIZATION_TYPE = 4 APP_COMMON_AUTHORIZATION_TYPE = 4
APP_COMMON_AUTHORIZATION_DATA = 5 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 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: if self.data[key][0] != 1:
return default return default
return bytes(self.data[key][1:]) 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: def delete(self, key: int) -> None:
utils.ensure(key < len(self.fields)) utils.ensure(key < len(self.fields))
self.data[key][:] = b"\x00" self.data[key][:] = b"\x00"
@ -89,7 +93,7 @@ class SessionCache(DataCache):
self.fields = ( self.fields = (
64, # APP_COMMON_SEED 64, # APP_COMMON_SEED
1, # APP_COMMON_DERIVE_CARDANO 1, # APP_COMMON_DERIVE_CARDANO
96, # APP_CARDANO_XPRV 96, # APP_CARDANO_SECRET
1, # APP_MONERO_LIVE_REFRESH 1, # APP_MONERO_LIVE_REFRESH
2, # APP_COMMON_AUTHORIZATION_TYPE 2, # APP_COMMON_AUTHORIZATION_TYPE
128, # APP_COMMON_AUTHORIZATION_DATA 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) 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: def delete(key: int) -> None:
if key & _SESSIONLESS_FLAG: if key & _SESSIONLESS_FLAG:
return _SESSIONLESS_CACHE.delete(key ^ _SESSIONLESS_FLAG) return _SESSIONLESS_CACHE.delete(key ^ _SESSIONLESS_FLAG)

Loading…
Cancel
Save