From 2373b6f02ef027a423762d5d6349cd9e0f36090e Mon Sep 17 00:00:00 2001 From: cirakg Date: Mon, 19 Oct 2026 22:28:09 +0200 Subject: test(ui): drive the simulator with a scripted flow and check every screen tools/ui_flow.py clicks buttons and taps from the device files, waits for DIAG shown lines and compares 10 screens per watch with goldens on four test and three spot-check devices. Adds the fixture-wrap generator, UI logic tests and driver tests; the launcher opens apps from their glance. Co-Authored-By: Claude Opus 5.5 --- tools/gen_fixture_wrap.py | 73 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 tools/gen_fixture_wrap.py (limited to 'tools/gen_fixture_wrap.py') diff --git a/tools/gen_fixture_wrap.py b/tools/gen_fixture_wrap.py new file mode 100644 index 0000000..c389fea --- /dev/null +++ b/tools/gen_fixture_wrap.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# Author: cirakg & claude ai +# Summary: Compute the UI-test fixture's PIN wrap (fake PIN 123456) so the watch needn't run PBKDF2 at start-up. +"""Print the fake PIN wrap that source/app/Fixtures.mc stores in the UI-test build. + +The wrap follows docs/SECURITY.md ("PIN wrap, version 1") with fixed, fake +inputs: PIN 123456, salt 10..1f, IV 20..2f, master key 40..5f, 2000 +PBKDF2 iterations. Deriving the PIN key on the watch in one event trips the +watchdog, and the UI tests need the store ready before the first screen, so +the fixture stores this precomputed value instead. The app's own unlock code +then checks it like any other wrap, which also cross-checks the format. + +Usage: gen_fixture_wrap.py (prints FIXTURE_WRAP and FIXTURE_MASTER_KEY in hex) +""" + +from __future__ import annotations + +import hashlib +import hmac +import struct +import subprocess + +PIN = b"123456" +ITERATIONS = 2000 +SALT = bytes(range(0x10, 0x20)) +IV = bytes(range(0x20, 0x30)) +MASTER_KEY = bytes(range(0x40, 0x60)) + + +def hkdf(ikm: bytes, info: bytes, length: int = 32) -> bytes: + """HKDF-SHA256 with an empty salt (RFC 5869), as in source/crypto/Kdf.mc.""" + prk = hmac.new(bytes(32), ikm, hashlib.sha256).digest() + out, block, counter = b"", b"", 1 + while len(out) < length: + block = hmac.new(prk, block + info + bytes([counter]), hashlib.sha256).digest() + out += block + counter += 1 + return out[:length] + + +def aes_cbc_encrypt(key: bytes, iv: bytes, plaintext: bytes) -> bytes: + """AES-256-CBC with PKCS#7 padding, through openssl (not in the standard library).""" + result = subprocess.run( + ["openssl", "enc", "-aes-256-cbc", "-K", key.hex(), "-iv", iv.hex()], + input=plaintext, + capture_output=True, + check=True, + ) + return result.stdout + + +def sealed_box(enc_key: bytes, mac_key: bytes, aad: bytes, plaintext: bytes) -> bytes: + """source/crypto/SealedBox.mc: 0x01 | IV | ciphertext | HMAC tag.""" + body = b"\x01" + IV + aes_cbc_encrypt(enc_key, IV, plaintext) + tag = hmac.new(mac_key, b"OTPW-REC-1" + struct.pack(">I", len(aad)) + aad + body, hashlib.sha256).digest() + return body + tag + + +def fixture_wrap() -> bytes: + pin_key = hashlib.pbkdf2_hmac("sha256", PIN, SALT, ITERATIONS, 32) + enc_key, mac_key = hkdf(pin_key, b"OTPW enc v1"), hkdf(pin_key, b"OTPW mac v1") + count = struct.pack(">I", ITERATIONS) + aad = b"OTPW-WRAP-1" + SALT + count + return SALT + count + sealed_box(enc_key, mac_key, aad, MASTER_KEY) + + +def main() -> None: + print("FIXTURE_WRAP =", fixture_wrap().hex()) + print("FIXTURE_MASTER_KEY =", MASTER_KEY.hex()) + + +if __name__ == "__main__": + main() -- cgit v1.2.3