import ustruct from micropython import const from ubinascii import hexlify from trezor import log, utils from trezor.crypto import bip32, chacha20poly1305, hashlib, hmac, random from apps.common import HARDENED, cbor, seed, storage if False: from typing import Optional # Credential ID values _CRED_ID_VERSION = b"\xf1\xd0\x02\x00" _CRED_ID_MIN_LENGTH = const(33) # Credential ID keys _CRED_ID_RP_ID = const(0x01) _CRED_ID_RP_NAME = const(0x02) _CRED_ID_USER_ID = const(0x03) _CRED_ID_USER_NAME = const(0x04) _CRED_ID_USER_DISPLAY_NAME = const(0x05) _CRED_ID_CREATION_TIME = const(0x06) _CRED_ID_HMAC_SECRET = const(0x07) class Credential: def __init__(self) -> None: self.id = b"" # type: bytes self.rp_id = "" # type: str self.rp_id_hash = b"" # type: bytes self.user_id = None # type: Optional[bytes] def app_name(self) -> str: return "" def account_name(self) -> Optional[str]: return None def private_key(self) -> bytes: return b"" def hmac_secret_key(self) -> Optional[bytes]: return None @staticmethod def from_bytes(data: bytes, rp_id_hash: bytes) -> Optional["Credential"]: cred = Fido2Credential.from_cred_id( data, rp_id_hash ) # type: Optional[Credential] if cred is None: cred = U2fCredential.from_key_handle(data, rp_id_hash) return cred # SLIP-0022: FIDO2 credential ID format for HD wallets class Fido2Credential(Credential): def __init__(self) -> None: super().__init__() self.rp_name = None # type: Optional[str] self.user_name = None # type: Optional[str] self.user_display_name = None # type: Optional[str] self._creation_time = 0 # type: int self.hmac_secret = False # type: bool def __lt__(self, other: Credential) -> bool: # Sort FIDO2 credentials newest first amongst each other. if isinstance(other, Fido2Credential): return self._creation_time > other._creation_time # Sort FIDO2 credentials before U2F credentials. return True def generate_id(self) -> None: self._creation_time = storage.device.next_u2f_counter() or 0 data = cbor.encode( { key: value for key, value in ( (_CRED_ID_RP_ID, self.rp_id), (_CRED_ID_RP_NAME, self.rp_name), (_CRED_ID_USER_ID, self.user_id), (_CRED_ID_USER_NAME, self.user_name), (_CRED_ID_USER_DISPLAY_NAME, self.user_display_name), (_CRED_ID_CREATION_TIME, self._creation_time), (_CRED_ID_HMAC_SECRET, self.hmac_secret), ) if value } ) key = seed.derive_slip21_node_without_passphrase( [b"SLIP-0022", _CRED_ID_VERSION, b"Encryption key"] ).key() iv = random.bytes(12) ctx = chacha20poly1305(key, iv) ctx.auth(self.rp_id_hash) ciphertext = ctx.encrypt(data) tag = ctx.finish() self.id = _CRED_ID_VERSION + iv + ciphertext + tag @staticmethod def from_cred_id(cred_id: bytes, rp_id_hash: bytes) -> Optional["Fido2Credential"]: if len(cred_id) < _CRED_ID_MIN_LENGTH or cred_id[0:4] != _CRED_ID_VERSION: return None key = seed.derive_slip21_node_without_passphrase( [b"SLIP-0022", cred_id[0:4], b"Encryption key"] ).key() iv = cred_id[4:16] ciphertext = cred_id[16:-16] tag = cred_id[-16:] ctx = chacha20poly1305(key, iv) ctx.auth(rp_id_hash) data = ctx.decrypt(ciphertext) if not utils.consteq(ctx.finish(), tag): return None try: data = cbor.decode(data) except Exception: return None if not isinstance(data, dict): return None cred = Fido2Credential() cred.rp_id = data.get(_CRED_ID_RP_ID, None) cred.rp_id_hash = rp_id_hash cred.rp_name = data.get(_CRED_ID_RP_NAME, None) cred.user_id = data.get(_CRED_ID_USER_ID, None) cred.user_name = data.get(_CRED_ID_USER_NAME, None) cred.user_display_name = data.get(_CRED_ID_USER_DISPLAY_NAME, None) cred._creation_time = data.get(_CRED_ID_CREATION_TIME, 0) cred.hmac_secret = data.get(_CRED_ID_HMAC_SECRET, False) cred.id = cred_id if ( not cred.check_required_fields() or not cred.check_data_types() or hashlib.sha256(cred.rp_id).digest() != rp_id_hash ): return None return cred def check_required_fields(self) -> bool: return ( self.rp_id is not None and self.user_id is not None and self._creation_time is not None ) def check_data_types(self) -> bool: return ( isinstance(self.rp_id, str) and isinstance(self.rp_name, (str, type(None))) and isinstance(self.user_id, (bytes, bytearray)) and isinstance(self.user_name, (str, type(None))) and isinstance(self.user_display_name, (str, type(None))) and isinstance(self.hmac_secret, bool) and isinstance(self._creation_time, (int, type(None))) and isinstance(self.id, (bytes, bytearray)) ) def app_name(self) -> str: return self.rp_id def account_name(self) -> Optional[str]: if self.user_name: return self.user_name elif self.user_display_name: return self.user_display_name elif self.user_id: return hexlify(self.user_id).decode() else: return None def private_key(self) -> bytes: path = [HARDENED | 10022, HARDENED | int.from_bytes(self.id[:4], "big")] + [ HARDENED | i for i in ustruct.unpack(">4L", self.id[-16:]) ] node = seed.derive_node_without_passphrase(path, "nist256p1") return node.private_key() def hmac_secret_key(self) -> Optional[bytes]: # Returns the symmetric key for the hmac-secret extension also known as CredRandom. if not self.hmac_secret: return None node = seed.derive_slip21_node_without_passphrase( [b"SLIP-0022", self.id[0:4], b"hmac-secret", self.id] ) return node.key()