mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-01-03 03:50:58 +00:00
feat(core): derive Cardano seed together with normal seed, if requested
This commit is contained in:
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)
|
@cache.stored_async(cache.APP_CARDANO_SECRET)
|
||||||
async def _get_passphrase(ctx: wire.Context) -> bytes:
|
async def _get_secret(ctx: wire.Context) -> bytes:
|
||||||
return (await get_passphrase(ctx)).encode()
|
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…
Reference in New Issue
Block a user