mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-23 06:48:16 +00:00
core: Implement credential management.
This commit is contained in:
parent
3e2ae5e469
commit
b89a9dc590
@ -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
|
||||
|
@ -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
|
||||
|
54
core/src/apps/webauthn/add_resident_credential.py
Normal file
54
core/src/apps/webauthn/add_resident_credential.py
Normal file
@ -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.")
|
62
core/src/apps/webauthn/confirm.py
Normal file
62
core/src/apps/webauthn/confirm.py
Normal file
@ -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
|
@ -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)
|
||||
|
38
core/src/apps/webauthn/list_resident_credentials.py
Normal file
38
core/src/apps/webauthn/list_resident_credentials.py
Normal file
@ -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)
|
48
core/src/apps/webauthn/remove_resident_credential.py
Normal file
48
core/src/apps/webauthn/remove_resident_credential.py
Normal file
@ -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")
|
Loading…
Reference in New Issue
Block a user