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

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

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

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

Loading…
Cancel
Save