mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-05-29 12:18:51 +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)
|
_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]
|
creds = [] # type: List[Credential]
|
||||||
for i in range(
|
for i in range(_MAX_RESIDENT_CREDENTIALS):
|
||||||
_RESIDENT_CREDENTIAL_START_KEY,
|
cred = get_resident_credential(i, rp_id_hash)
|
||||||
_RESIDENT_CREDENTIAL_START_KEY + _MAX_RESIDENT_CREDENTIALS,
|
if cred is not None:
|
||||||
):
|
creds.append(cred)
|
||||||
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)
|
|
||||||
|
|
||||||
return creds
|
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:
|
def store_resident_credential(cred: Fido2Credential) -> bool:
|
||||||
slot = None
|
slot = None
|
||||||
for i in range(
|
for i in range(_MAX_RESIDENT_CREDENTIALS):
|
||||||
_RESIDENT_CREDENTIAL_START_KEY,
|
stored_cred_data = common.get(
|
||||||
_RESIDENT_CREDENTIAL_START_KEY + _MAX_RESIDENT_CREDENTIALS,
|
common.APP_FIDO2, i + _RESIDENT_CREDENTIAL_START_KEY
|
||||||
):
|
)
|
||||||
stored_cred_data = common._get(common._APP_FIDO2, i)
|
|
||||||
if stored_cred_data is None:
|
if stored_cred_data is None:
|
||||||
if slot is None:
|
if slot is None:
|
||||||
slot = i
|
slot = i
|
||||||
@ -66,13 +77,21 @@ def store_resident_credential(cred: Fido2Credential) -> bool:
|
|||||||
if slot is None:
|
if slot is None:
|
||||||
return False
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
def erase_resident_credentials() -> None:
|
def erase_resident_credentials() -> None:
|
||||||
for i in range(
|
for i in range(_MAX_RESIDENT_CREDENTIALS):
|
||||||
_RESIDENT_CREDENTIAL_START_KEY,
|
common.delete(common.APP_FIDO2, i + _RESIDENT_CREDENTIAL_START_KEY)
|
||||||
_RESIDENT_CREDENTIAL_START_KEY + _MAX_RESIDENT_CREDENTIALS,
|
|
||||||
):
|
|
||||||
common._delete(common._APP_FIDO2, i)
|
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
|
import utime
|
||||||
from micropython import const
|
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 import aes, der, hashlib, hmac, random
|
||||||
from trezor.crypto.curve import nist256p1
|
from trezor.crypto.curve import nist256p1
|
||||||
|
from trezor.messages import MessageType
|
||||||
from trezor.ui.confirm import CONFIRMED, Confirm, ConfirmPageable, Pageable
|
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 import cbor, storage
|
||||||
from apps.common.storage.webauthn import (
|
from apps.common.storage.webauthn import (
|
||||||
@ -15,6 +16,7 @@ from apps.common.storage.webauthn import (
|
|||||||
get_resident_credentials,
|
get_resident_credentials,
|
||||||
store_resident_credential,
|
store_resident_credential,
|
||||||
)
|
)
|
||||||
|
from apps.webauthn.confirm import ConfirmContent, ConfirmInfo
|
||||||
from apps.webauthn.credential import Credential, Fido2Credential, U2fCredential
|
from apps.webauthn.credential import Credential, Fido2Credential, U2fCredential
|
||||||
|
|
||||||
if __debug__:
|
if __debug__:
|
||||||
@ -464,6 +466,19 @@ def send_cmd_sync(cmd: Cmd, iface: io.HID) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def boot(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))
|
loop.schedule(handle_reports(iface))
|
||||||
|
|
||||||
|
|
||||||
@ -526,64 +541,6 @@ async def confirm(*args: Any, **kwargs: Any) -> bool:
|
|||||||
return await dialog is CONFIRMED
|
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:
|
class State:
|
||||||
def __init__(self, cid: int, iface: io.HID) -> None:
|
def __init__(self, cid: int, iface: io.HID) -> None:
|
||||||
self.cid = cid
|
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:
|
class Credential:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
self.index = None # type Optional[int]
|
||||||
self.id = b"" # type: bytes
|
self.id = b"" # type: bytes
|
||||||
self.rp_id = "" # type: str
|
self.rp_id = "" # type: str
|
||||||
self.rp_id_hash = b"" # type: bytes
|
self.rp_id_hash = b"" # type: bytes
|
||||||
@ -104,7 +105,9 @@ class Fido2Credential(Credential):
|
|||||||
self.id = _CRED_ID_VERSION + iv + ciphertext + tag
|
self.id = _CRED_ID_VERSION + iv + ciphertext + tag
|
||||||
|
|
||||||
@staticmethod
|
@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:
|
if len(cred_id) < _CRED_ID_MIN_LENGTH or cred_id[0:4] != _CRED_ID_VERSION:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -114,6 +117,16 @@ class Fido2Credential(Credential):
|
|||||||
iv = cred_id[4:16]
|
iv = cred_id[4:16]
|
||||||
ciphertext = cred_id[16:-16]
|
ciphertext = cred_id[16:-16]
|
||||||
tag = cred_id[-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 = chacha20poly1305(key, iv)
|
||||||
ctx.auth(rp_id_hash)
|
ctx.auth(rp_id_hash)
|
||||||
data = ctx.decrypt(ciphertext)
|
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