diff --git a/Makefile b/Makefile index 24f776043f..798015226d 100644 --- a/Makefile +++ b/Makefile @@ -65,14 +65,20 @@ templates: ## rebuild coin lists from definitions in common templates_check: ## check that coin lists are up to date ./core/tools/build_templates --check +icons: ## generate FIDO service icons + python3 core/tools/build_icons.py + +icons_check: ## generate FIDO service icons + python3 core/tools/build_icons.py --check + protobuf: ## generate python protobuf headers ./tools/build_protobuf protobuf_check: ## check that generated protobuf headers are up to date ./tools/build_protobuf --check -gen: mocks templates protobuf ## regeneate auto-generated files from sources +gen: mocks templates protobuf icons ## regeneate auto-generated files from sources make -C python coins_json -gen_check: mocks_check templates_check protobuf_check ## check validity of auto-generated files +gen_check: mocks_check templates_check protobuf_check icons_check ## check validity of auto-generated files make -C python coins_json_check diff --git a/core/src/apps/webauthn/res/icon_binance.toif b/core/src/apps/webauthn/res/icon_binance.toif index 8d4d22936d..914792da1f 100644 Binary files a/core/src/apps/webauthn/res/icon_binance.toif and b/core/src/apps/webauthn/res/icon_binance.toif differ diff --git a/core/src/apps/webauthn/res/icon_bitbucket.toif b/core/src/apps/webauthn/res/icon_bitbucket.toif index c4cce921b4..0a49f76615 100644 Binary files a/core/src/apps/webauthn/res/icon_bitbucket.toif and b/core/src/apps/webauthn/res/icon_bitbucket.toif differ diff --git a/core/src/apps/webauthn/res/icon_bitfinex.toif b/core/src/apps/webauthn/res/icon_bitfinex.toif index d86c16dc00..96d9e3f06d 100644 Binary files a/core/src/apps/webauthn/res/icon_bitfinex.toif and b/core/src/apps/webauthn/res/icon_bitfinex.toif differ diff --git a/core/src/apps/webauthn/res/icon_bitwarden.toif b/core/src/apps/webauthn/res/icon_bitwarden.toif new file mode 100644 index 0000000000..c8cb4f71e6 Binary files /dev/null and b/core/src/apps/webauthn/res/icon_bitwarden.toif differ diff --git a/core/src/apps/webauthn/res/icon_dashlane.toif b/core/src/apps/webauthn/res/icon_dashlane.toif index cdace4d4a1..aa4cfb0504 100644 Binary files a/core/src/apps/webauthn/res/icon_dashlane.toif and b/core/src/apps/webauthn/res/icon_dashlane.toif differ diff --git a/core/src/apps/webauthn/res/icon_dropbox.toif b/core/src/apps/webauthn/res/icon_dropbox.toif index a284d1b58e..bbc921ba8e 100644 Binary files a/core/src/apps/webauthn/res/icon_dropbox.toif and b/core/src/apps/webauthn/res/icon_dropbox.toif differ diff --git a/core/src/apps/webauthn/res/icon_duo.toif b/core/src/apps/webauthn/res/icon_duo.toif index 613a16e9e6..447d62df81 100644 Binary files a/core/src/apps/webauthn/res/icon_duo.toif and b/core/src/apps/webauthn/res/icon_duo.toif differ diff --git a/core/src/apps/webauthn/res/icon_fastmail.toif b/core/src/apps/webauthn/res/icon_fastmail.toif index 6cf90f0037..e4e14dc683 100644 Binary files a/core/src/apps/webauthn/res/icon_fastmail.toif and b/core/src/apps/webauthn/res/icon_fastmail.toif differ diff --git a/core/src/apps/webauthn/res/icon_fedora.toif b/core/src/apps/webauthn/res/icon_fedora.toif index 4982615f2d..316cd66ccd 100644 Binary files a/core/src/apps/webauthn/res/icon_fedora.toif and b/core/src/apps/webauthn/res/icon_fedora.toif differ diff --git a/core/src/apps/webauthn/res/icon_gandi.toif b/core/src/apps/webauthn/res/icon_gandi.toif index 070bb1e78f..b840c107cc 100644 Binary files a/core/src/apps/webauthn/res/icon_gandi.toif and b/core/src/apps/webauthn/res/icon_gandi.toif differ diff --git a/core/src/apps/webauthn/res/icon_github.toif b/core/src/apps/webauthn/res/icon_github.toif index 8f82062600..d56fee3fb6 100644 Binary files a/core/src/apps/webauthn/res/icon_github.toif and b/core/src/apps/webauthn/res/icon_github.toif differ diff --git a/core/src/apps/webauthn/res/icon_gitlab.toif b/core/src/apps/webauthn/res/icon_gitlab.toif index 4c2c412107..2dd96830d5 100644 Binary files a/core/src/apps/webauthn/res/icon_gitlab.toif and b/core/src/apps/webauthn/res/icon_gitlab.toif differ diff --git a/core/src/apps/webauthn/res/icon_google.toif b/core/src/apps/webauthn/res/icon_google.toif index 4b34f971d6..aa528f80e6 100644 Binary files a/core/src/apps/webauthn/res/icon_google.toif and b/core/src/apps/webauthn/res/icon_google.toif differ diff --git a/core/src/apps/webauthn/res/icon_keeper.toif b/core/src/apps/webauthn/res/icon_keeper.toif index 8c3e80303c..d749a44900 100644 Binary files a/core/src/apps/webauthn/res/icon_keeper.toif and b/core/src/apps/webauthn/res/icon_keeper.toif differ diff --git a/core/src/apps/webauthn/res/icon_lastpass.toif b/core/src/apps/webauthn/res/icon_lastpass.toif index c3cde22396..6482fe8056 100644 Binary files a/core/src/apps/webauthn/res/icon_lastpass.toif and b/core/src/apps/webauthn/res/icon_lastpass.toif differ diff --git a/core/src/apps/webauthn/res/icon_login.gov.toif b/core/src/apps/webauthn/res/icon_login.gov.toif index a87cd90782..5eef2dae35 100644 Binary files a/core/src/apps/webauthn/res/icon_login.gov.toif and b/core/src/apps/webauthn/res/icon_login.gov.toif differ diff --git a/core/src/apps/webauthn/res/icon_microsoft.toif b/core/src/apps/webauthn/res/icon_microsoft.toif index 8457156c95..3a91cc3c2f 100644 Binary files a/core/src/apps/webauthn/res/icon_microsoft.toif and b/core/src/apps/webauthn/res/icon_microsoft.toif differ diff --git a/core/src/apps/webauthn/res/icon_mojeid.toif b/core/src/apps/webauthn/res/icon_mojeid.toif index caea946380..2b57d47296 100644 Binary files a/core/src/apps/webauthn/res/icon_mojeid.toif and b/core/src/apps/webauthn/res/icon_mojeid.toif differ diff --git a/core/src/apps/webauthn/res/icon_slush_pool.toif b/core/src/apps/webauthn/res/icon_slush_pool.toif deleted file mode 100644 index c3e5fc0c6c..0000000000 Binary files a/core/src/apps/webauthn/res/icon_slush_pool.toif and /dev/null differ diff --git a/core/src/apps/webauthn/res/icon_slushpool.toif b/core/src/apps/webauthn/res/icon_slushpool.toif new file mode 100644 index 0000000000..f99d008f26 Binary files /dev/null and b/core/src/apps/webauthn/res/icon_slushpool.toif differ diff --git a/core/src/apps/webauthn/res/icon_stripe.toif b/core/src/apps/webauthn/res/icon_stripe.toif index b1f7808734..299935dfee 100644 Binary files a/core/src/apps/webauthn/res/icon_stripe.toif and b/core/src/apps/webauthn/res/icon_stripe.toif differ diff --git a/core/tools/build_icons.py b/core/tools/build_icons.py new file mode 100755 index 0000000000..d20041e2bd --- /dev/null +++ b/core/tools/build_icons.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 + +from pathlib import Path +import sys + +import click +from PIL import Image + +from trezorlib._internal import toif + +HERE = Path(__file__).parent +ROOT = HERE.parent.parent + +ICON_SIZE = (64, 64) +DESTINATION = ROOT / "core" / "src" / "apps" / "webauthn" / "res" +EXCLUDE = {"icon_webauthn"} + +# insert ../../common/tools to sys.path, so that we can import coin_info +# XXX this is hacky, but we want to keep coin_info in the common/ subdir for the purpose +# of exporting it to e.g. Connect +# And making a special python package out of it seems needless + +COMMON_TOOLS_PATH = ROOT / "common" / "tools" +sys.path.insert(0, str(COMMON_TOOLS_PATH)) + +import coin_info + + +@click.command() +@click.option("-c", "--check", is_flag=True, help="Do not write, only check.") +@click.option("-r", "--remove", is_flag=True, help="Remove unrecognized files.") +def build_icons(check, remove): + """Build FIDO app icons in the source tree.""" + + checks_ok = True + apps = coin_info.fido_info() + + total_size = 0 + + for app in apps: + if app["icon"] is None: + if not app.get("demo"): + raise click.ClickException(f"Icon not found for: {app['key']}") + else: + continue + + im = Image.open(app["icon"]) + resized = im.resize(ICON_SIZE, Image.BOX) + toi = toif.from_image(resized) + dest_path = DESTINATION / f"icon_{app['key']}.toif" + + total_size += len(toi.to_bytes()) + + if not check: + toi.save(dest_path) + else: + if not dest_path.exists(): + print(f"Missing TOIF: {dest_path}") + checks_ok = False + continue + data = dest_path.read_bytes() + if data != toi.to_bytes(): + print(f"Icon different from source: {dest_path}") + checks_ok = False + + print(f"Total icon size: {total_size} bytes") + + keys = EXCLUDE | {"icon_" + app["key"] for app in apps} + unrecognized_files = False + for icon_file in DESTINATION.glob("*.toif"): + name = icon_file.stem + if name not in keys: + unrecognized_files = True + if remove: + print(f"Removing unrecognized file: {icon_file}") + icon_file.unlink() + else: + print(f"Unrecognized file: {icon_file}") + checks_ok = False + + if not remove and unrecognized_files: + raise click.ClickException( + "Unrecognized files found in icon directory.\n" + "Use 'build_icons.py -r' to remove them automatically." + ) + if not checks_ok: + raise click.ClickException("Some checks have failed.") + + +if __name__ == "__main__": + build_icons() diff --git a/python/src/trezorlib/_internal/toif.py b/python/src/trezorlib/_internal/toif.py new file mode 100644 index 0000000000..0d4436c54e --- /dev/null +++ b/python/src/trezorlib/_internal/toif.py @@ -0,0 +1,128 @@ +import struct +import zlib +from typing import Sequence, Tuple + +import attr +from PIL import Image + +from .. import firmware + +RGBPixel = Tuple[int, int, int] + + +def _compress(data: bytes) -> bytes: + z = zlib.compressobj(level=9, wbits=-10) + return z.compress(data) + z.flush() + + +def _decompress(data: bytes) -> bytes: + return zlib.decompress(data, wbits=-10) + + +def _from_pil_rgb(pixels: Sequence[RGBPixel]) -> bytes: + data = bytearray() + for r, g, b in pixels: + c = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3) + data += struct.pack(">H", c) + return bytes(data) + + +def _to_rgb(data: bytes) -> bytes: + res = bytearray() + for i in range(0, len(data), 2): + (c,) = struct.unpack(">H", data[i : i + 2]) + r = (c & 0xF800) >> 8 + g = (c & 0x07C0) >> 3 + b = (c & 0x001F) << 3 + res += bytes((r, g, b)) + return bytes(res) + + +def _from_pil_grayscale(pixels: Sequence[int]) -> bytes: + data = bytearray() + for i in range(0, len(pixels), 2): + left, right = pixels[i], pixels[i + 1] + c = (left & 0xF0) | ((right & 0xF0) >> 4) + data += struct.pack(">B", c) + return bytes(data) + + +def _to_grayscale(data: bytes) -> bytes: + res = bytearray() + for pixel in data: + left = pixel & 0xF0 + right = (pixel & 0x0F) << 4 + res += bytes((left, right)) + return bytes(res) + + +@attr.s +class Toif: + mode = attr.ib() # type: firmware.ToifMode + size = attr.ib() # type: Tuple[int, int] + data = attr.ib() # type: bytes + + def _expected_data_length(self) -> int: + width, height = self.size + if self.mode is firmware.ToifMode.grayscale: + return width * height // 2 + else: + return width * height * 2 + + def to_image(self) -> Image: + uncompressed = _decompress(self.data) + expected_size = self._expected_data_length() + if len(uncompressed) != expected_size: + raise ValueError( + "Uncompressed data is {} bytes, expected {}".format( + len(uncompressed), expected_size + ) + ) + + if self.mode is firmware.ToifMode.grayscale: + pil_mode = "L" + raw_data = _to_grayscale(uncompressed) + else: + pil_mode = "RGB" + raw_data = _to_rgb(uncompressed) + + return Image.frombuffer(pil_mode, self.size, raw_data, "raw", pil_mode, 0, 1) + + def to_bytes(self) -> bytes: + width, height = self.size + return firmware.Toif.build( + dict(format=self.mode, width=width, height=height, data=self.data) + ) + + def save(self, filename: str) -> None: + with open(filename, "wb") as out: + out.write(self.to_bytes()) + + +def from_bytes(data: bytes) -> Toif: + parsed = firmware.Toif.parse(data) + return Toif(parsed.format, (parsed.width, parsed.height), parsed.data) + + +def load(filename: str) -> Toif: + with open(filename, "rb") as f: + return from_bytes(f.read()) + + +def from_image(image: Image, background=(0, 0, 0, 255)) -> Toif: + if image.mode == "RGBA": + background = Image.new("RGBA", image.size, background) + blend = Image.alpha_composite(background, image) + image = blend.convert("RGB") + + if image.mode == "L": + toif_mode = firmware.ToifMode.grayscale + toif_data = _from_pil_grayscale(image.getdata()) + elif image.mode == "RGB": + toif_mode = firmware.ToifMode.full_color + toif_data = _from_pil_rgb(image.getdata()) + else: + raise ValueError("Unsupported image mode: {}".format(image.mode)) + + data = _compress(toif_data) + return Toif(toif_mode, image.size, data) diff --git a/python/src/trezorlib/firmware.py b/python/src/trezorlib/firmware.py index a2ed7f0421..36ed6be399 100644 --- a/python/src/trezorlib/firmware.py +++ b/python/src/trezorlib/firmware.py @@ -75,10 +75,30 @@ class Unsigned(FirmwareIntegrityError): pass +class ToifMode(Enum): + full_color = b"f" + grayscale = b"g" + + +class EnumAdapter(c.Adapter): + def __init__(self, subcon, enum): + self.enum = enum + super().__init__(subcon) + + def _encode(self, obj, ctx, path): + return obj.value + + def _decode(self, obj, ctx, path): + try: + return self.enum(obj) + except ValueError: + return obj + + # fmt: off Toif = c.Struct( "magic" / c.Const(b"TOI"), - "format" / c.Enum(c.Byte, full_color=b"f", grayscale=b"g"), + "format" / EnumAdapter(c.Bytes(1), ToifMode), "width" / c.Int16ul, "height" / c.Int16ul, "data" / c.Prefixed(c.Int32ul, c.GreedyBytes),