summaryrefslogtreecommitdiff
path: root/tools/gen_fixture_wrap.py
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-19 22:28:09 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-19 22:28:09 +0200
commit2373b6f02ef027a423762d5d6349cd9e0f36090e (patch)
tree7509b0b1905d7c593074bba8ed4c94e30e437604 /tools/gen_fixture_wrap.py
parent35042ff04ffc50e55ff59a385849442be4429d6b (diff)
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 <noreply@anthropic.com>
Diffstat (limited to 'tools/gen_fixture_wrap.py')
-rw-r--r--tools/gen_fixture_wrap.py73
1 files changed, 73 insertions, 0 deletions
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()