1
0
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:
Pavol Rusnak 2019-09-13 11:20:48 +02:00 committed by GitHub
commit 2711ce2a3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1819 additions and 246 deletions

View File

@ -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);

View File

@ -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);

View File

@ -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) {

View File

@ -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:

View File

@ -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"

View 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)

View 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

View 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

View File

@ -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

Binary file not shown.

Binary file not shown.

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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:

View File

@ -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"

View File

@ -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)

View 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()