mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-24 15:28:10 +00:00
Merge pull request #394 from trezor/andrewkozlik/fido2-squashed
Add FIDO2 support
This commit is contained in:
commit
2711ce2a3d
@ -128,10 +128,29 @@ STATIC mp_obj_t mod_trezorio_HID_write(mp_obj_t self, mp_obj_t msg) {
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorio_HID_write_obj,
|
||||
mod_trezorio_HID_write);
|
||||
|
||||
/// def write_blocking(self, msg: bytes, timeout_ms: int) -> int:
|
||||
/// """
|
||||
/// Sends message using USB HID (device) or UDP (emulator).
|
||||
/// """
|
||||
STATIC mp_obj_t mod_trezorio_HID_write_blocking(mp_obj_t self, mp_obj_t msg,
|
||||
mp_obj_t timeout_ms) {
|
||||
mp_obj_HID_t *o = MP_OBJ_TO_PTR(self);
|
||||
mp_buffer_info_t buf;
|
||||
mp_get_buffer_raise(msg, &buf, MP_BUFFER_READ);
|
||||
uint32_t timeout = trezor_obj_get_uint(timeout_ms);
|
||||
ssize_t r =
|
||||
usb_hid_write_blocking(o->info.iface_num, buf.buf, buf.len, timeout);
|
||||
return MP_OBJ_NEW_SMALL_INT(r);
|
||||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_3(mod_trezorio_HID_write_blocking_obj,
|
||||
mod_trezorio_HID_write_blocking);
|
||||
|
||||
STATIC const mp_rom_map_elem_t mod_trezorio_HID_locals_dict_table[] = {
|
||||
{MP_ROM_QSTR(MP_QSTR_iface_num),
|
||||
MP_ROM_PTR(&mod_trezorio_HID_iface_num_obj)},
|
||||
{MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&mod_trezorio_HID_write_obj)},
|
||||
{MP_ROM_QSTR(MP_QSTR_write_blocking),
|
||||
MP_ROM_PTR(&mod_trezorio_HID_write_blocking_obj)},
|
||||
};
|
||||
STATIC MP_DEFINE_CONST_DICT(mod_trezorio_HID_locals_dict,
|
||||
mod_trezorio_HID_locals_dict_table);
|
||||
|
@ -184,6 +184,10 @@ int usb_hid_write(uint8_t iface_num, const uint8_t *buf, uint32_t len) {
|
||||
}
|
||||
usb_hid_state_t *state = &iface->hid;
|
||||
|
||||
if (state->ep_in_is_idle == 0) {
|
||||
return 0; // Last transmission is not over yet
|
||||
}
|
||||
|
||||
state->ep_in_is_idle = 0;
|
||||
USBD_LL_Transmit(&usb_dev_handle, state->ep_in, UNCONST(buf), (uint16_t)len);
|
||||
|
||||
|
@ -24,6 +24,7 @@
|
||||
#include <string.h>
|
||||
#include <sys/poll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "touch.h"
|
||||
#include "usb.h"
|
||||
@ -227,6 +228,18 @@ int usb_hid_write(uint8_t iface_num, const uint8_t *buf, uint32_t len) {
|
||||
return usb_emulated_write(iface_num, buf, len);
|
||||
}
|
||||
|
||||
int usb_hid_write_blocking(uint8_t iface_num, const uint8_t *buf, uint32_t len,
|
||||
int timeout) {
|
||||
const uint32_t start = clock();
|
||||
while (sectrue != usb_hid_can_write(iface_num)) {
|
||||
if (timeout >= 0 &&
|
||||
(1000 * (clock() - start)) / CLOCKS_PER_SEC >= timeout) {
|
||||
return 0; // Timeout
|
||||
}
|
||||
}
|
||||
return usb_hid_write(iface_num, buf, len);
|
||||
}
|
||||
|
||||
int usb_webusb_write(uint8_t iface_num, const uint8_t *buf, uint32_t len) {
|
||||
if (iface_num >= USBD_MAX_NUM_INTERFACES ||
|
||||
usb_ifaces[iface_num].type != USB_IFACE_TYPE_WEBUSB) {
|
||||
|
@ -171,6 +171,11 @@ class HID:
|
||||
Sends message using USB HID (device) or UDP (emulator).
|
||||
"""
|
||||
|
||||
def write_blocking(self, msg: bytes, timeout_ms: int) -> int:
|
||||
"""
|
||||
Sends message using USB HID (device) or UDP (emulator).
|
||||
"""
|
||||
|
||||
|
||||
# extmod/modtrezorio/modtrezorio-poll.h
|
||||
def poll(ifaces: Iterable[int], list_ref: List, timeout_us: int) -> bool:
|
||||
|
@ -9,6 +9,7 @@ if False:
|
||||
_APP_DEVICE = 0x01
|
||||
_APP_RECOVERY = 0x02
|
||||
_APP_RECOVERY_SHARES = 0x03
|
||||
_APP_FIDO2 = 0x04
|
||||
# fmt: on
|
||||
|
||||
_FALSE_BYTE = b"\x00"
|
||||
|
78
core/src/apps/common/storage/webauthn.py
Normal file
78
core/src/apps/common/storage/webauthn.py
Normal file
@ -0,0 +1,78 @@
|
||||
from micropython import const
|
||||
|
||||
from apps.common.storage import common
|
||||
from apps.webauthn.credential import Credential, Fido2Credential
|
||||
|
||||
if False:
|
||||
from typing import List, Optional
|
||||
|
||||
_RESIDENT_CREDENTIAL_START_KEY = const(1)
|
||||
_MAX_RESIDENT_CREDENTIALS = const(16)
|
||||
|
||||
|
||||
def get_resident_credentials(rp_id_hash: Optional[bytes]) -> 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)
|
||||
|
||||
return creds
|
||||
|
||||
|
||||
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)
|
||||
if stored_cred_data is None:
|
||||
if slot is None:
|
||||
slot = i
|
||||
continue
|
||||
|
||||
stored_rp_id_hash = stored_cred_data[:32]
|
||||
stored_cred_id = stored_cred_data[32:]
|
||||
|
||||
if cred.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 None:
|
||||
# Stored credential is not for this RP ID.
|
||||
continue
|
||||
|
||||
# If a credential for the same RP ID and user ID already exists, then overwrite it.
|
||||
if stored_cred.user_id == cred.user_id:
|
||||
slot = i
|
||||
break
|
||||
|
||||
if slot is None:
|
||||
return False
|
||||
|
||||
common._set(common._APP_FIDO2, slot, 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)
|
39
core/src/apps/webauthn/README.md
Normal file
39
core/src/apps/webauthn/README.md
Normal file
@ -0,0 +1,39 @@
|
||||
# WebAuthn
|
||||
|
||||
MAINTAINER = Andrew R. Kozlik <andrew.kozlik@satoshilabs.com>
|
||||
|
||||
AUTHOR = Andrew R. Kozlik <andrew.kozlik@satoshilabs.com>
|
||||
|
||||
REVIEWER = Jan Pochyla <jan.pochyla@satoshilabs.com>, Ondrej Vejpustek <ondrej.vejpustek@satoshilabs.com>
|
||||
|
||||
-----
|
||||
|
||||
This app implements WebAuthn authenticator functionality in accordance with the following specifications:
|
||||
|
||||
* [Web Authentication](https://www.w3.org/TR/webauthn/): An API for accessing Public Key Credentials Level 1, W3C Recommendation, 4 March 2019
|
||||
* [FIDO Client to Authenticator Protocol (CTAP) v2.0](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#sctn-hmac-secret-extension), Proposed Standard, January 30, 2019
|
||||
* [SLIP-0022](https://github.com/satoshilabs/slips/blob/master/slip-0022.md): FIDO2 credential ID format for HD wallets
|
||||
|
||||
## Supported features and algorithms
|
||||
|
||||
This implementation supports client-side credential storage on the device and user verification by PIN entry, making the Trezor T a first-factor roaming authenticator usable for passwordless login.
|
||||
|
||||
### User verification
|
||||
|
||||
The device is capable of verifying the user within itself by direct PIN entry via the touchscreen. Client PIN is not supported, because it is less secure than direct PIN verification. The authenticatorClientPIN command is therefore implemented only to the extent required by the hmac-secret extension. Namely, only the getKeyAgreement subcommand is supported.
|
||||
|
||||
### Credential selection
|
||||
|
||||
Credential selection is supported directly on the device. The authenticatorGetNextAssertion command is therefore not implemented.
|
||||
|
||||
### Public key credential algorithms
|
||||
|
||||
* COSE algorithm ES256 (-7): ECDSA using the NIST P-256 curve with SHA-256.
|
||||
|
||||
### Extenstions
|
||||
|
||||
* hmac-secret extension.
|
||||
|
||||
### Attestation types
|
||||
|
||||
* Self attestation.
|
File diff suppressed because it is too large
Load Diff
299
core/src/apps/webauthn/credential.py
Normal file
299
core/src/apps/webauthn/credential.py
Normal file
@ -0,0 +1,299 @@
|
||||
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)
|
||||
_KEY_HANDLE_LENGTH = const(64)
|
||||
|
||||
# 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)
|
||||
|
||||
# Key paths
|
||||
_U2F_KEY_PATH = const(0x80553246)
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
class U2fCredential(Credential):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.node = None # type: Optional[bip32.HDNode]
|
||||
|
||||
def __lt__(self, other: "Credential") -> bool:
|
||||
# Sort U2F credentials after FIDO2 credentials.
|
||||
if isinstance(other, Fido2Credential):
|
||||
return False
|
||||
|
||||
# Sort U2F credentials lexicographically amongst each other.
|
||||
return self.id < other.id
|
||||
|
||||
def private_key(self) -> bytes:
|
||||
if self.node is None:
|
||||
return b""
|
||||
return self.node.private_key()
|
||||
|
||||
def generate_key_handle(self) -> None:
|
||||
# derivation path is m/U2F'/r'/r'/r'/r'/r'/r'/r'/r'
|
||||
path = [HARDENED | random.uniform(0x80000000) for _ in range(0, 8)]
|
||||
nodepath = [_U2F_KEY_PATH] + path
|
||||
|
||||
# prepare signing key from random path, compute decompressed public key
|
||||
self.node = seed.derive_node_without_passphrase(nodepath, "nist256p1")
|
||||
|
||||
# first half of keyhandle is keypath
|
||||
keypath = ustruct.pack("<8L", *path)
|
||||
|
||||
# second half of keyhandle is a hmac of rp_id_hash and keypath
|
||||
mac = hmac.Hmac(self.node.private_key(), self.rp_id_hash, hashlib.sha256)
|
||||
mac.update(keypath)
|
||||
|
||||
self.id = keypath + mac.digest()
|
||||
|
||||
def app_name(self) -> str:
|
||||
from apps.webauthn import knownapps
|
||||
|
||||
app_name = knownapps.knownapps.get(self.rp_id_hash, None)
|
||||
if app_name is None:
|
||||
app_name = "%s...%s" % (
|
||||
hexlify(self.rp_id_hash[:4]).decode(),
|
||||
hexlify(self.rp_id_hash[-4:]).decode(),
|
||||
)
|
||||
return app_name
|
||||
|
||||
@staticmethod
|
||||
def from_key_handle(
|
||||
key_handle: bytes, rp_id_hash: bytes
|
||||
) -> Optional["U2fCredential"]:
|
||||
# check the keyHandle and generate the signing key
|
||||
node = U2fCredential._node_from_key_handle(rp_id_hash, key_handle, "<8L")
|
||||
if node is None:
|
||||
# prior to firmware version 2.0.8, keypath was serialized in a
|
||||
# big-endian manner, instead of little endian, like in trezor-mcu.
|
||||
# try to parse it as big-endian now and check the HMAC.
|
||||
node = U2fCredential._node_from_key_handle(rp_id_hash, key_handle, ">8L")
|
||||
if node is None:
|
||||
# specific error logged in msg_authenticate_genkey
|
||||
return None
|
||||
|
||||
cred = U2fCredential()
|
||||
cred.id = key_handle
|
||||
cred.rp_id_hash = rp_id_hash
|
||||
cred.node = node
|
||||
return cred
|
||||
|
||||
@staticmethod
|
||||
def _node_from_key_handle(
|
||||
rp_id_hash: bytes, keyhandle: bytes, pathformat: str
|
||||
) -> Optional[bip32.HDNode]:
|
||||
# unpack the keypath from the first half of keyhandle
|
||||
keypath = keyhandle[:32]
|
||||
path = ustruct.unpack(pathformat, keypath)
|
||||
|
||||
# check high bit for hardened keys
|
||||
for i in path:
|
||||
if not i & HARDENED:
|
||||
if __debug__:
|
||||
log.warning(__name__, "invalid key path")
|
||||
return None
|
||||
|
||||
# derive the signing key
|
||||
nodepath = [_U2F_KEY_PATH] + list(path)
|
||||
node = seed.derive_node_without_passphrase(nodepath, "nist256p1")
|
||||
|
||||
# second half of keyhandle is a hmac of rp_id_hash and keypath
|
||||
mac = hmac.Hmac(node.private_key(), rp_id_hash, hashlib.sha256)
|
||||
mac.update(keypath)
|
||||
|
||||
# verify the hmac
|
||||
if not utils.consteq(mac.digest(), keyhandle[32:]):
|
||||
if __debug__:
|
||||
log.warning(__name__, "invalid key handle")
|
||||
return None
|
||||
|
||||
return node
|
@ -1,49 +1,47 @@
|
||||
from trezor import ui
|
||||
|
||||
if False:
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
def pin_to_int(pin: str) -> int:
|
||||
return int("1" + pin)
|
||||
|
||||
|
||||
_previous_progress = None
|
||||
_previous_progress = None # type: Optional[int]
|
||||
_previous_seconds = None # type: Optional[int]
|
||||
keepalive_callback = None # type: Any
|
||||
|
||||
|
||||
def show_pin_timeout(seconds: int, progress: int, message: str) -> bool:
|
||||
global _previous_progress
|
||||
global _previous_seconds
|
||||
|
||||
if callable(keepalive_callback):
|
||||
keepalive_callback()
|
||||
|
||||
if progress == 0:
|
||||
if progress != _previous_progress:
|
||||
# avoid overdraw in case of repeated progress calls
|
||||
ui.display.clear()
|
||||
_previous_seconds = None
|
||||
ui.display.text_center(
|
||||
ui.WIDTH // 2, 37, message, ui.BOLD, ui.FG, ui.BG, ui.WIDTH
|
||||
)
|
||||
ui.display.loader(progress, False, 0, ui.FG, ui.BG)
|
||||
if seconds == 0:
|
||||
ui.display.text_center(
|
||||
ui.WIDTH // 2, ui.HEIGHT - 22, "Done", ui.BOLD, ui.FG, ui.BG, ui.WIDTH
|
||||
)
|
||||
elif seconds == 1:
|
||||
ui.display.text_center(
|
||||
ui.WIDTH // 2,
|
||||
ui.HEIGHT - 22,
|
||||
"1 second left",
|
||||
ui.BOLD,
|
||||
ui.FG,
|
||||
ui.BG,
|
||||
ui.WIDTH,
|
||||
)
|
||||
else:
|
||||
ui.display.text_center(
|
||||
ui.WIDTH // 2,
|
||||
ui.HEIGHT - 22,
|
||||
"%d seconds left" % seconds,
|
||||
ui.BOLD,
|
||||
ui.FG,
|
||||
ui.BG,
|
||||
ui.WIDTH,
|
||||
)
|
||||
ui.display.refresh()
|
||||
|
||||
if seconds != _previous_seconds:
|
||||
if seconds == 0:
|
||||
remaining = "Done"
|
||||
elif seconds == 1:
|
||||
remaining = "1 second left"
|
||||
else:
|
||||
remaining = "%d seconds left" % seconds
|
||||
ui.display.text_center(
|
||||
ui.WIDTH // 2, ui.HEIGHT - 22, remaining, ui.BOLD, ui.FG, ui.BG, ui.WIDTH
|
||||
)
|
||||
_previous_seconds = seconds
|
||||
|
||||
ui.display.refresh()
|
||||
_previous_progress = progress
|
||||
return False
|
||||
|
BIN
core/src/trezor/res/swipe_left.toif
Normal file
BIN
core/src/trezor/res/swipe_left.toif
Normal file
Binary file not shown.
BIN
core/src/trezor/res/swipe_right.toif
Normal file
BIN
core/src/trezor/res/swipe_right.toif
Normal file
Binary file not shown.
@ -6,7 +6,7 @@ from trezorui import Display
|
||||
from trezor import io, loop, res, utils
|
||||
|
||||
if False:
|
||||
from typing import Any, Generator, Iterable, Tuple, TypeVar
|
||||
from typing import Any, Generator, Tuple, TypeVar
|
||||
|
||||
Pos = Tuple[int, int]
|
||||
Area = Tuple[int, int, int, int]
|
||||
@ -70,9 +70,22 @@ from trezor.ui import style # isort:skip
|
||||
from trezor.ui.style import * # isort:skip # noqa: F401,F403
|
||||
|
||||
|
||||
def pulse(coef: int) -> float:
|
||||
def pulse(period: int, offset: int = 0) -> float:
|
||||
# normalize sin from interval -1:1 to 0:1
|
||||
return 0.5 + 0.5 * math.sin(utime.ticks_us() / coef)
|
||||
return 0.5 + 0.5 * math.sin(2 * math.pi * (utime.ticks_us() + offset) / period)
|
||||
|
||||
|
||||
async def alert(count: int = 3) -> None:
|
||||
short_sleep = loop.sleep(20000)
|
||||
long_sleep = loop.sleep(80000)
|
||||
for i in range(count * 2):
|
||||
if i % 2 == 0:
|
||||
display.backlight(style.BACKLIGHT_MAX)
|
||||
await short_sleep
|
||||
else:
|
||||
display.backlight(style.BACKLIGHT_DIM)
|
||||
await long_sleep
|
||||
display.backlight(style.BACKLIGHT_NORMAL)
|
||||
|
||||
|
||||
async def click() -> Pos:
|
||||
@ -276,7 +289,7 @@ class Layout(Component):
|
||||
def __await__(self) -> Generator[Any, Any, ResultValue]:
|
||||
return self.__iter__() # type: ignore
|
||||
|
||||
def create_tasks(self) -> Iterable[loop.Task]:
|
||||
def create_tasks(self) -> Tuple[loop.Task, ...]:
|
||||
"""
|
||||
Called from `__iter__`. Creates and returns a sequence of tasks that
|
||||
run this layout. Tasks are executed in parallel. When one of them
|
||||
|
@ -1,9 +1,11 @@
|
||||
from trezor import res, ui
|
||||
from micropython import const
|
||||
|
||||
from trezor import loop, res, ui
|
||||
from trezor.ui.button import Button, ButtonCancel, ButtonConfirm
|
||||
from trezor.ui.loader import Loader, LoaderDefault
|
||||
|
||||
if False:
|
||||
from typing import Optional
|
||||
from typing import Any, Optional, Tuple
|
||||
from trezor.ui.button import ButtonContent, ButtonStyleType
|
||||
from trezor.ui.loader import LoaderStyleType
|
||||
|
||||
@ -55,6 +57,7 @@ class Confirm(ui.Layout):
|
||||
self.cancel = None
|
||||
|
||||
def dispatch(self, event: int, x: int, y: int) -> None:
|
||||
super().dispatch(event, x, y)
|
||||
self.content.dispatch(event, x, y)
|
||||
if self.confirm is not None:
|
||||
self.confirm.dispatch(event, x, y)
|
||||
@ -68,6 +71,82 @@ class Confirm(ui.Layout):
|
||||
raise ui.Result(CANCELLED)
|
||||
|
||||
|
||||
class Pageable:
|
||||
def __init__(self) -> None:
|
||||
self._page = 0
|
||||
|
||||
def page(self) -> int:
|
||||
return self._page
|
||||
|
||||
def page_count(self) -> int:
|
||||
raise NotImplementedError
|
||||
|
||||
def is_first(self) -> bool:
|
||||
return self._page == 0
|
||||
|
||||
def is_last(self) -> bool:
|
||||
return self._page == self.page_count() - 1
|
||||
|
||||
def next(self) -> None:
|
||||
self._page = min(self._page + 1, self.page_count() - 1)
|
||||
|
||||
def prev(self) -> None:
|
||||
self._page = max(self._page - 1, 0)
|
||||
|
||||
|
||||
class ConfirmPageable(Confirm):
|
||||
def __init__(self, pageable: Pageable, *args: Any, **kwargs: Any):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.pageable = pageable
|
||||
|
||||
async def handle_paging(self) -> None:
|
||||
from trezor.ui.swipe import SWIPE_HORIZONTAL, SWIPE_LEFT, SWIPE_RIGHT, Swipe
|
||||
|
||||
if self.pageable.is_first():
|
||||
directions = SWIPE_LEFT
|
||||
elif self.pageable.is_last():
|
||||
directions = SWIPE_RIGHT
|
||||
else:
|
||||
directions = SWIPE_HORIZONTAL
|
||||
|
||||
swipe = await Swipe(directions)
|
||||
|
||||
if swipe == SWIPE_LEFT:
|
||||
self.pageable.next()
|
||||
else:
|
||||
self.pageable.prev()
|
||||
|
||||
self.content.repaint = True
|
||||
if self.confirm is not None:
|
||||
self.confirm.repaint = True
|
||||
if self.cancel is not None:
|
||||
self.cancel.repaint = True
|
||||
|
||||
def create_tasks(self) -> Tuple[loop.Task, ...]:
|
||||
tasks = super().create_tasks()
|
||||
if self.pageable.page_count() > 1:
|
||||
return tasks + (self.handle_paging(),)
|
||||
else:
|
||||
return tasks
|
||||
|
||||
def on_render(self) -> None:
|
||||
PULSE_PERIOD = const(1200000)
|
||||
|
||||
super().on_render()
|
||||
|
||||
if not self.pageable.is_first():
|
||||
t = ui.pulse(PULSE_PERIOD)
|
||||
c = ui.blend(ui.GREY, ui.DARK_GREY, t)
|
||||
icon = res.load(ui.ICON_SWIPE_RIGHT)
|
||||
ui.display.icon(18, 68, icon, c, ui.BG)
|
||||
|
||||
if not self.pageable.is_last():
|
||||
t = ui.pulse(PULSE_PERIOD, PULSE_PERIOD // 2)
|
||||
c = ui.blend(ui.GREY, ui.DARK_GREY, t)
|
||||
icon = res.load(ui.ICON_SWIPE_LEFT)
|
||||
ui.display.icon(205, 68, icon, c, ui.BG)
|
||||
|
||||
|
||||
class HoldToConfirm(ui.Layout):
|
||||
DEFAULT_CONFIRM = "Hold To Confirm"
|
||||
DEFAULT_CONFIRM_STYLE = ButtonConfirm
|
||||
|
@ -7,7 +7,7 @@ from trezor.ui.button import Button, ButtonClear, ButtonConfirm
|
||||
from trezor.ui.swipe import SWIPE_HORIZONTAL, SWIPE_LEFT, Swipe
|
||||
|
||||
if False:
|
||||
from typing import List, Iterable, Optional
|
||||
from typing import Iterable, List, Optional, Tuple
|
||||
from trezor.ui.button import ButtonContent, ButtonStyleStateType
|
||||
|
||||
SPACE = res.load(ui.ICON_SPACE)
|
||||
@ -244,7 +244,7 @@ class PassphraseKeyboard(ui.Layout):
|
||||
def on_confirm(self) -> None:
|
||||
raise ui.Result(self.input.text)
|
||||
|
||||
def create_tasks(self) -> Iterable[loop.Task]:
|
||||
def create_tasks(self) -> Tuple[loop.Task, ...]:
|
||||
return self.handle_input(), self.handle_rendering(), self.handle_paging()
|
||||
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
from trezor import loop, ui
|
||||
|
||||
if False:
|
||||
from typing import Iterable
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
class Popup(ui.Layout):
|
||||
@ -12,7 +12,7 @@ class Popup(ui.Layout):
|
||||
def dispatch(self, event: int, x: int, y: int) -> None:
|
||||
self.content.dispatch(event, x, y)
|
||||
|
||||
def create_tasks(self) -> Iterable[loop.Task]:
|
||||
def create_tasks(self) -> Tuple[loop.Task, ...]:
|
||||
return self.handle_input(), self.handle_rendering(), self.handle_timeout()
|
||||
|
||||
def handle_timeout(self) -> loop.Task: # type: ignore
|
||||
|
@ -9,7 +9,7 @@ if __debug__:
|
||||
from apps.debug import swipe_signal
|
||||
|
||||
if False:
|
||||
from typing import Iterable, Sequence
|
||||
from typing import Tuple, Sequence
|
||||
|
||||
|
||||
def render_scrollbar(pages: int, page: int) -> None:
|
||||
@ -32,10 +32,10 @@ def render_scrollbar(pages: int, page: int) -> None:
|
||||
|
||||
|
||||
def render_swipe_icon() -> None:
|
||||
DRAW_DELAY = const(200000)
|
||||
PULSE_PERIOD = const(1200000)
|
||||
|
||||
icon = res.load(ui.ICON_SWIPE)
|
||||
t = ui.pulse(DRAW_DELAY)
|
||||
t = ui.pulse(PULSE_PERIOD)
|
||||
c = ui.blend(ui.GREY, ui.DARK_GREY, t)
|
||||
ui.display.icon(70, 205, icon, c, ui.BG)
|
||||
|
||||
@ -91,7 +91,7 @@ class Paginated(ui.Layout):
|
||||
|
||||
self.on_change()
|
||||
|
||||
def create_tasks(self) -> Iterable[loop.Task]:
|
||||
def create_tasks(self) -> Tuple[loop.Task, ...]:
|
||||
return self.handle_input(), self.handle_rendering(), self.handle_paging()
|
||||
|
||||
def on_change(self) -> None:
|
||||
|
@ -64,5 +64,7 @@ ICON_LOCK = "trezor/res/lock.toif"
|
||||
ICON_CLICK = "trezor/res/click.toif"
|
||||
ICON_BACK = "trezor/res/left.toif"
|
||||
ICON_SWIPE = "trezor/res/swipe.toif"
|
||||
ICON_SWIPE_LEFT = "trezor/res/swipe_left.toif"
|
||||
ICON_SWIPE_RIGHT = "trezor/res/swipe_right.toif"
|
||||
ICON_CHECK = "trezor/res/check.toif"
|
||||
ICON_SPACE = "trezor/res/space.toif"
|
||||
|
@ -208,3 +208,51 @@ class Label(ui.Component):
|
||||
tx, ty, self.content, self.style, ui.FG, ui.BG, aw
|
||||
)
|
||||
self.repaint = False
|
||||
|
||||
|
||||
def text_center_trim_left(
|
||||
x: int, y: int, text: str, font: int = ui.NORMAL, width: int = ui.WIDTH - 16
|
||||
) -> None:
|
||||
if ui.display.text_width(text, font) <= width:
|
||||
ui.display.text_center(x, y, text, font, ui.FG, ui.BG)
|
||||
return
|
||||
|
||||
ELLIPSIS_WIDTH = ui.display.text_width("...", ui.BOLD)
|
||||
if width < ELLIPSIS_WIDTH:
|
||||
return
|
||||
|
||||
text_length = 0
|
||||
for i in range(1, len(text)):
|
||||
if ui.display.text_width(text[-i:], font) + ELLIPSIS_WIDTH > width:
|
||||
text_length = i - 1
|
||||
break
|
||||
|
||||
text_width = ui.display.text_width(text[-text_length:], font)
|
||||
x -= (text_width + ELLIPSIS_WIDTH) // 2
|
||||
ui.display.text(x, y, "...", ui.BOLD, ui.GREY, ui.BG)
|
||||
x += ELLIPSIS_WIDTH
|
||||
ui.display.text(x, y, text[-text_length:], font, ui.FG, ui.BG)
|
||||
|
||||
|
||||
def text_center_trim_right(
|
||||
x: int, y: int, text: str, font: int = ui.NORMAL, width: int = ui.WIDTH - 16
|
||||
) -> None:
|
||||
if ui.display.text_width(text, font) <= width:
|
||||
ui.display.text_center(x, y, text, font, ui.FG, ui.BG)
|
||||
return
|
||||
|
||||
ELLIPSIS_WIDTH = ui.display.text_width("...", ui.BOLD)
|
||||
if width < ELLIPSIS_WIDTH:
|
||||
return
|
||||
|
||||
text_length = 0
|
||||
for i in range(1, len(text)):
|
||||
if ui.display.text_width(text[:i], font) + ELLIPSIS_WIDTH > width:
|
||||
text_length = i - 1
|
||||
break
|
||||
|
||||
text_width = ui.display.text_width(text[:text_length], font)
|
||||
x -= (text_width + ELLIPSIS_WIDTH) // 2
|
||||
ui.display.text(x, y, text[:text_length], font, ui.FG, ui.BG)
|
||||
x += text_width
|
||||
ui.display.text(x, y, "...", ui.BOLD, ui.GREY, ui.BG)
|
||||
|
64
core/tests/test_apps.webauthn.credential.py
Normal file
64
core/tests/test_apps.webauthn.credential.py
Normal file
@ -0,0 +1,64 @@
|
||||
from common import *
|
||||
from apps.common import mnemonic, storage
|
||||
from apps.webauthn.credential import Fido2Credential
|
||||
from trezor.crypto.curve import nist256p1
|
||||
from trezor.crypto.hashlib import sha256
|
||||
|
||||
|
||||
class TestCredential(unittest.TestCase):
|
||||
def test_fido2_credential_decode(self):
|
||||
mnemonic_secret = b"all all all all all all all all all all all all"
|
||||
mnemonic.get = lambda: (mnemonic_secret, mnemonic.TYPE_BIP39)
|
||||
storage.is_initialized = lambda: True
|
||||
|
||||
cred_id = (
|
||||
b"f1d0020013e65c865634ad8abddf7a66df56ae7d8c3afd356f76426801508b2e"
|
||||
b"579bcb3496fe6396a6002e3cd6d80f6359dfa9961e24c544bfc2f26acec1b8d8"
|
||||
b"78ba56727e1f6a7b5176c607552aea63a5abe5d826d69fab3063edfa0201d9a5"
|
||||
b"1013d69eddb2eff37acdd5963f"
|
||||
)
|
||||
|
||||
rp_id = "example.com"
|
||||
rp_id_hash = sha256(rp_id).digest()
|
||||
|
||||
user_id = (
|
||||
b"3082019330820138a0030201023082019330820138a003020102308201933082"
|
||||
)
|
||||
|
||||
user_name = "johnpsmith@example.com"
|
||||
|
||||
creation_time = 2
|
||||
|
||||
public_key = (
|
||||
b"0451f0d4c307bc737c90ac605c6279f7d01e451798aa7b74df550fdb43a7760c"
|
||||
b"7c02b5107fef42094d00f52a9b1e90afb90e1b9decbf15a6f13d4f882de857e2"
|
||||
b"f4"
|
||||
)
|
||||
|
||||
cred_random = (
|
||||
b"36a9b5d71c13ed54594474b54073af1fb03ea91cd056588909dae43ae2f35dbf"
|
||||
)
|
||||
|
||||
# Load credential.
|
||||
cred = Fido2Credential.from_cred_id(unhexlify(cred_id), rp_id_hash)
|
||||
self.assertIsNotNone(cred)
|
||||
|
||||
# Check credential data.
|
||||
self.assertEqual(hexlify(cred.id), cred_id)
|
||||
self.assertEqual(cred.rp_id, rp_id)
|
||||
self.assertEqual(cred.rp_id_hash, rp_id_hash)
|
||||
self.assertEqual(hexlify(cred.user_id), user_id)
|
||||
self.assertEqual(cred.user_name, user_name)
|
||||
self.assertEqual(cred._creation_time, 2)
|
||||
self.assertTrue(cred.hmac_secret)
|
||||
self.assertIsNone(cred.rp_name)
|
||||
self.assertIsNone(cred.user_display_name)
|
||||
|
||||
# Check credential keys.
|
||||
self.assertEqual(hexlify(cred.hmac_secret_key()), cred_random)
|
||||
|
||||
cred_public_key = nist256p1.publickey(cred.private_key(), False)
|
||||
self.assertEqual(hexlify(cred_public_key), public_key)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Loading…
Reference in New Issue
Block a user