From b89a9dc590f375584a35b1a7c63c9d9724e60a08 Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Thu, 5 Sep 2019 12:27:49 +0200 Subject: [PATCH] core: Implement credential management. --- core/src/apps/common/storage/webauthn.py | 81 ++++++++++++------- core/src/apps/webauthn/__init__.py | 77 ++++-------------- .../apps/webauthn/add_resident_credential.py | 54 +++++++++++++ core/src/apps/webauthn/confirm.py | 62 ++++++++++++++ core/src/apps/webauthn/credential.py | 15 +++- .../webauthn/list_resident_credentials.py | 38 +++++++++ .../webauthn/remove_resident_credential.py | 48 +++++++++++ 7 files changed, 283 insertions(+), 92 deletions(-) create mode 100644 core/src/apps/webauthn/add_resident_credential.py create mode 100644 core/src/apps/webauthn/confirm.py create mode 100644 core/src/apps/webauthn/list_resident_credentials.py create mode 100644 core/src/apps/webauthn/remove_resident_credential.py diff --git a/core/src/apps/common/storage/webauthn.py b/core/src/apps/common/storage/webauthn.py index 5c25f533fa..faf2d35852 100644 --- a/core/src/apps/common/storage/webauthn.py +++ b/core/src/apps/common/storage/webauthn.py @@ -10,37 +10,48 @@ _RESIDENT_CREDENTIAL_START_KEY = const(1) _MAX_RESIDENT_CREDENTIALS = const(16) -def get_resident_credentials(rp_id_hash: Optional[bytes]) -> List[Credential]: +def get_resident_credentials(rp_id_hash: Optional[bytes] = None) -> List[Credential]: creds = [] # type: List[Credential] - for i in range( - _RESIDENT_CREDENTIAL_START_KEY, - _RESIDENT_CREDENTIAL_START_KEY + _MAX_RESIDENT_CREDENTIALS, - ): - stored_cred_data = common._get(common._APP_FIDO2, i) - if stored_cred_data is None: - continue - - stored_rp_id_hash = stored_cred_data[:32] - stored_cred_id = stored_cred_data[32:] - - if rp_id_hash is not None and rp_id_hash != stored_rp_id_hash: - # Stored credential is not for this RP ID. - continue - - stored_cred = Fido2Credential.from_cred_id(stored_cred_id, stored_rp_id_hash) - if stored_cred is not None: - creds.append(stored_cred) - + for i in range(_MAX_RESIDENT_CREDENTIALS): + cred = get_resident_credential(i, rp_id_hash) + if cred is not None: + creds.append(cred) return creds +def get_resident_credential( + index: int, rp_id_hash: Optional[bytes] = None +) -> Optional[Credential]: + if not (0 <= index < _MAX_RESIDENT_CREDENTIALS): + return None + + stored_cred_data = common.get( + common.APP_FIDO2, index + _RESIDENT_CREDENTIAL_START_KEY + ) + if stored_cred_data is None: + return None + + stored_rp_id_hash = stored_cred_data[:32] + stored_cred_id = stored_cred_data[32:] + + if rp_id_hash is not None and rp_id_hash != stored_rp_id_hash: + # Stored credential is not for this RP ID. + return None + + stored_cred = Fido2Credential.from_cred_id(stored_cred_id, stored_rp_id_hash) + if stored_cred is None: + return None + + stored_cred.index = index + return stored_cred + + def store_resident_credential(cred: Fido2Credential) -> bool: slot = None - for i in range( - _RESIDENT_CREDENTIAL_START_KEY, - _RESIDENT_CREDENTIAL_START_KEY + _MAX_RESIDENT_CREDENTIALS, - ): - stored_cred_data = common._get(common._APP_FIDO2, i) + for i in range(_MAX_RESIDENT_CREDENTIALS): + stored_cred_data = common.get( + common.APP_FIDO2, i + _RESIDENT_CREDENTIAL_START_KEY + ) if stored_cred_data is None: if slot is None: slot = i @@ -66,13 +77,21 @@ def store_resident_credential(cred: Fido2Credential) -> bool: if slot is None: return False - common._set(common._APP_FIDO2, slot, cred.rp_id_hash + cred.id) + common.set( + common.APP_FIDO2, + slot + _RESIDENT_CREDENTIAL_START_KEY, + cred.rp_id_hash + cred.id, + ) return True def erase_resident_credentials() -> None: - for i in range( - _RESIDENT_CREDENTIAL_START_KEY, - _RESIDENT_CREDENTIAL_START_KEY + _MAX_RESIDENT_CREDENTIALS, - ): - common._delete(common._APP_FIDO2, i) + for i in range(_MAX_RESIDENT_CREDENTIALS): + common.delete(common.APP_FIDO2, i + _RESIDENT_CREDENTIAL_START_KEY) + + +def erase_resident_credential(index: int) -> bool: + if not (0 <= index < _MAX_RESIDENT_CREDENTIALS): + return False + common.delete(common.APP_FIDO2, index + _RESIDENT_CREDENTIAL_START_KEY) + return True diff --git a/core/src/apps/webauthn/__init__.py b/core/src/apps/webauthn/__init__.py index 94ad16b505..71a7efff0c 100644 --- a/core/src/apps/webauthn/__init__.py +++ b/core/src/apps/webauthn/__init__.py @@ -3,11 +3,12 @@ import ustruct import utime from micropython import const -from trezor import config, io, log, loop, ui, utils, workflow +from trezor import config, io, log, loop, ui, utils, wire, workflow from trezor.crypto import aes, der, hashlib, hmac, random from trezor.crypto.curve import nist256p1 +from trezor.messages import MessageType from trezor.ui.confirm import CONFIRMED, Confirm, ConfirmPageable, Pageable -from trezor.ui.text import Text, text_center_trim_left, text_center_trim_right +from trezor.ui.text import Text from apps.common import cbor, storage from apps.common.storage.webauthn import ( @@ -15,6 +16,7 @@ from apps.common.storage.webauthn import ( get_resident_credentials, store_resident_credential, ) +from apps.webauthn.confirm import ConfirmContent, ConfirmInfo from apps.webauthn.credential import Credential, Fido2Credential, U2fCredential if __debug__: @@ -464,6 +466,19 @@ def send_cmd_sync(cmd: Cmd, iface: io.HID) -> None: def boot(iface: io.HID) -> None: + wire.add( + MessageType.WebAuthnListResidentCredentials, + __name__, + "list_resident_credentials", + ) + wire.add( + MessageType.WebAuthnAddResidentCredential, __name__, "add_resident_credential" + ) + wire.add( + MessageType.WebAuthnRemoveResidentCredential, + __name__, + "remove_resident_credential", + ) loop.schedule(handle_reports(iface)) @@ -526,64 +541,6 @@ async def confirm(*args: Any, **kwargs: Any) -> bool: return await dialog is CONFIRMED -class ConfirmInfo: - def __init__(self) -> None: - self.app_icon = None # type: Optional[bytes] - - def get_header(self) -> Optional[str]: - return None - - def app_name(self) -> str: - raise NotImplementedError - - def account_name(self) -> Optional[str]: - return None - - def load_icon(self, rp_id_hash: bytes) -> None: - from trezor import res - from apps.webauthn import knownapps - - try: - namepart = knownapps.knownapps[rp_id_hash].lower().replace(" ", "_") - icon = res.load("apps/webauthn/res/icon_%s.toif" % namepart) - except Exception as e: - icon = res.load("apps/webauthn/res/icon_webauthn.toif") - if __debug__: - log.exception(__name__, e) - self.app_icon = icon - - -class ConfirmContent(ui.Component): - def __init__(self, info: ConfirmInfo) -> None: - self.info = info - self.repaint = True - - def on_render(self) -> None: - if self.repaint: - header = self.info.get_header() - - if header is None or self.info.app_icon is None: - return - ui.header(header, ui.ICON_DEFAULT, ui.GREEN, ui.BG, ui.GREEN) - ui.display.image((ui.WIDTH - 64) // 2, 48, self.info.app_icon) - - app_name = self.info.app_name() - account_name = self.info.account_name() - - # Dummy requests usually have some text as both app_name and account_name, - # in that case show the text only once. - if account_name is not None: - if app_name != account_name: - text_center_trim_left(ui.WIDTH // 2, 140, app_name) - text_center_trim_right(ui.WIDTH // 2, 172, account_name) - else: - text_center_trim_right(ui.WIDTH // 2, 156, account_name) - else: - text_center_trim_left(ui.WIDTH // 2, 156, app_name) - - self.repaint = False - - class State: def __init__(self, cid: int, iface: io.HID) -> None: self.cid = cid diff --git a/core/src/apps/webauthn/add_resident_credential.py b/core/src/apps/webauthn/add_resident_credential.py new file mode 100644 index 0000000000..9d9f11ba2c --- /dev/null +++ b/core/src/apps/webauthn/add_resident_credential.py @@ -0,0 +1,54 @@ +from trezor import ui, wire +from trezor.messages.Success import Success +from trezor.messages.WebAuthnAddResidentCredential import WebAuthnAddResidentCredential +from trezor.ui.text import Text + +from apps.common.confirm import require_confirm +from apps.common.storage.webauthn import store_resident_credential +from apps.webauthn.confirm import ConfirmContent, ConfirmInfo +from apps.webauthn.credential import Fido2Credential + +if False: + from typing import Optional + + +class ConfirmAddCredential(ConfirmInfo): + def __init__(self, cred: Fido2Credential): + self._cred = cred + self.load_icon(cred.rp_id_hash) + + def get_header(self) -> str: + return "Import credential" + + def app_name(self) -> str: + return self._cred.app_name() + + def account_name(self) -> Optional[str]: + return self._cred.account_name() + + +async def add_resident_credential( + ctx: wire.Context, msg: WebAuthnAddResidentCredential +) -> Success: + if not msg.credential_id: + raise wire.ProcessError("Missing credential ID parameter.") + + cred = Fido2Credential.from_cred_id(msg.credential_id, None) + if cred is None: + text = Text("Import credential", ui.ICON_WRONG, ui.RED) + text.normal( + "The credential you are", + "trying to import does", + "not belong to this", + "authenticator.", + ) + await require_confirm(ctx, text, confirm=None) + raise wire.ActionCancelled("Cancelled") + + content = ConfirmContent(ConfirmAddCredential(cred)) + await require_confirm(ctx, content) + + if store_resident_credential(cred): + return Success(message="Credential added") + else: + raise wire.ProcessError("Internal credential storage is full.") diff --git a/core/src/apps/webauthn/confirm.py b/core/src/apps/webauthn/confirm.py new file mode 100644 index 0000000000..6cb0ae08a8 --- /dev/null +++ b/core/src/apps/webauthn/confirm.py @@ -0,0 +1,62 @@ +from trezor import log, ui +from trezor.ui.text import text_center_trim_left, text_center_trim_right + +if False: + from typing import Optional + + +class ConfirmInfo: + def __init__(self) -> None: + self.app_icon = None # type: Optional[bytes] + + def get_header(self) -> str: + raise NotImplementedError + + def app_name(self) -> str: + raise NotImplementedError + + def account_name(self) -> Optional[str]: + return None + + def load_icon(self, rp_id_hash: bytes) -> None: + from trezor import res + from apps.webauthn import knownapps + + try: + namepart = knownapps.knownapps[rp_id_hash].lower().replace(" ", "_") + icon = res.load("apps/webauthn/res/icon_%s.toif" % namepart) + except Exception as e: + icon = res.load("apps/webauthn/res/icon_webauthn.toif") + if __debug__: + log.exception(__name__, e) + self.app_icon = icon + + +class ConfirmContent(ui.Component): + def __init__(self, info: ConfirmInfo) -> None: + self.info = info + self.repaint = True + + def on_render(self) -> None: + if self.repaint: + header = self.info.get_header() + ui.header(header, ui.ICON_DEFAULT, ui.GREEN, ui.BG, ui.GREEN) + + if self.info.app_icon is not None: + ui.display.image((ui.WIDTH - 64) // 2, 48, self.info.app_icon) + + app_name = self.info.app_name() + account_name = self.info.account_name() + + # Dummy requests usually have some text as both app_name and account_name, + # in that case show the text only once. + if account_name is not None: + if app_name != account_name: + text_center_trim_left(ui.WIDTH // 2, 140, app_name) + text_center_trim_right(ui.WIDTH // 2, 172, account_name) + else: + text_center_trim_right(ui.WIDTH // 2, 156, account_name) + else: + text_center_trim_left(ui.WIDTH // 2, 156, app_name) + + self.repaint = False diff --git a/core/src/apps/webauthn/credential.py b/core/src/apps/webauthn/credential.py index c1862ab249..dd48b500c3 100644 --- a/core/src/apps/webauthn/credential.py +++ b/core/src/apps/webauthn/credential.py @@ -30,6 +30,7 @@ _U2F_KEY_PATH = const(0x80553246) class Credential: def __init__(self) -> None: + self.index = None # type Optional[int] self.id = b"" # type: bytes self.rp_id = "" # type: str self.rp_id_hash = b"" # type: bytes @@ -104,7 +105,9 @@ class Fido2Credential(Credential): self.id = _CRED_ID_VERSION + iv + ciphertext + tag @staticmethod - def from_cred_id(cred_id: bytes, rp_id_hash: bytes) -> Optional["Fido2Credential"]: + def from_cred_id( + cred_id: bytes, rp_id_hash: Optional[bytes] + ) -> Optional["Fido2Credential"]: if len(cred_id) < _CRED_ID_MIN_LENGTH or cred_id[0:4] != _CRED_ID_VERSION: return None @@ -114,6 +117,16 @@ class Fido2Credential(Credential): iv = cred_id[4:16] ciphertext = cred_id[16:-16] tag = cred_id[-16:] + + if rp_id_hash is None: + ctx = chacha20poly1305(key, iv) + data = ctx.decrypt(ciphertext) + try: + rp_id = cbor.decode(data)[_CRED_ID_RP_ID] + except Exception: + return None + rp_id_hash = hashlib.sha256(rp_id).digest() + ctx = chacha20poly1305(key, iv) ctx.auth(rp_id_hash) data = ctx.decrypt(ciphertext) diff --git a/core/src/apps/webauthn/list_resident_credentials.py b/core/src/apps/webauthn/list_resident_credentials.py new file mode 100644 index 0000000000..4d7b0020a5 --- /dev/null +++ b/core/src/apps/webauthn/list_resident_credentials.py @@ -0,0 +1,38 @@ +from trezor import wire +from trezor.messages.WebAuthnCredential import WebAuthnCredential +from trezor.messages.WebAuthnCredentials import WebAuthnCredentials +from trezor.messages.WebAuthnListResidentCredentials import ( + WebAuthnListResidentCredentials, +) +from trezor.ui.text import Text + +from apps.common.confirm import require_confirm +from apps.common.storage.webauthn import get_resident_credentials + + +async def list_resident_credentials( + ctx: wire.Context, msg: WebAuthnListResidentCredentials +) -> WebAuthnCredentials: + text = Text("List credentials") + text.normal( + "Do you want to export", + "information about the", + "resident credentials", + "stored on this device?", + ) + await require_confirm(ctx, text) + creds = [ + WebAuthnCredential( + index=cred.index, + id=cred.id, + rp_id=cred.rp_id, + rp_name=cred.rp_name, + user_id=cred.user_id, + user_name=cred.user_name, + user_display_name=cred.user_display_name, + creation_time=cred._creation_time, + hmac_secret=cred.hmac_secret, + ) + for cred in get_resident_credentials() + ] + return WebAuthnCredentials(creds) diff --git a/core/src/apps/webauthn/remove_resident_credential.py b/core/src/apps/webauthn/remove_resident_credential.py new file mode 100644 index 0000000000..0e41d6dbe3 --- /dev/null +++ b/core/src/apps/webauthn/remove_resident_credential.py @@ -0,0 +1,48 @@ +from trezor import wire +from trezor.messages.Success import Success +from trezor.messages.WebAuthnRemoveResidentCredential import ( + WebAuthnRemoveResidentCredential, +) + +from apps.common.confirm import require_confirm +from apps.common.storage.webauthn import ( + erase_resident_credential, + get_resident_credential, +) +from apps.webauthn.confirm import ConfirmContent, ConfirmInfo +from apps.webauthn.credential import Fido2Credential + +if False: + from typing import Optional + + +class ConfirmRemoveCredential(ConfirmInfo): + def __init__(self, cred: Fido2Credential): + self._cred = cred + self.load_icon(cred.rp_id_hash) + + def get_header(self) -> str: + return "Remove credential" + + def app_name(self) -> str: + return self._cred.app_name() + + def account_name(self) -> Optional[str]: + return self._cred.account_name() + + +async def remove_resident_credential( + ctx: wire.Context, msg: WebAuthnRemoveResidentCredential +) -> Success: + if msg.index is None: + raise wire.ProcessError("Missing credential index parameter.") + + cred = get_resident_credential(msg.index) + if cred is None: + raise wire.ProcessError("Invalid credential index.") + + content = ConfirmContent(ConfirmRemoveCredential(cred)) + await require_confirm(ctx, content) + + erase_resident_credential(msg.index) + return Success(message="Credential removed")