246 lines
9.7 KiB
Markdown
246 lines
9.7 KiB
Markdown
# Mobile Companion App
|
|
|
|
The companion is the credential store and the mobile half of the LOCQR run.
|
|
It holds all user credentials at rest, performs the mobile side of the
|
|
cryptographic key exchange, authenticates the user via biometric or device PIN
|
|
before each delivery, and sends the encrypted credential to the backend relay.
|
|
|
|
The companion exists in two implementations: a **web stub** used during
|
|
development, and the **native app** that is the production target.
|
|
|
|
---
|
|
|
|
## Web stub (development tool)
|
|
|
|
The web stub is a single-page web application that implements the full
|
|
cryptographic protocol without the security properties of the native app. Its
|
|
purpose is to allow end-to-end development and testing of the extension without
|
|
requiring a native build.
|
|
|
|
**What it does:**
|
|
- Scans QR codes via the browser `MediaDevices` camera API
|
|
- Performs all crypto operations in full (X25519, ML-KEM-768, HKDF, AES-256-GCM)
|
|
using WebCrypto and a WASM ML-KEM library
|
|
- Derives and displays the PIN
|
|
- Delivers a credential — either a hardcoded payload or any credential entered
|
|
or received during the current browser session
|
|
- Communicates with the backend relay over HTTPS
|
|
|
|
**What it deliberately omits:**
|
|
- No persistent credential storage
|
|
- No biometric or device PIN gate (replaced by a simple button)
|
|
- No hardware-backed key storage
|
|
- No platform security guarantees
|
|
|
|
The stub is a first-class development artifact. It should be maintained
|
|
alongside the extension so that the full run can always be exercised
|
|
in a browser.
|
|
|
|
The visual design specified in `gui.md` does not apply to the stub. Adhere to
|
|
it only where doing so costs nothing — the stub's purpose is functional
|
|
correctness, not UI fidelity.
|
|
|
|
---
|
|
|
|
## Native app
|
|
|
|
Visual design, screen layouts, and interaction details for the native app are
|
|
specified in `gui.md`.
|
|
|
|
### Platform
|
|
|
|
Android-first, written in Kotlin. iOS is deferred.
|
|
|
|
| Concern | Android |
|
|
|---|---|
|
|
| Credential encryption key | Android Keystore (hardware-backed where available) |
|
|
| Biometric auth | `BiometricPrompt` API |
|
|
| QR scanning | ML Kit Barcode Scanning |
|
|
| Compile-time constants | `BuildConfig` fields set at build time |
|
|
|
|
### Compile-time constants
|
|
|
|
| Constant | Algorithm | Purpose |
|
|
|---|---|---|
|
|
| `ED25519_PUBKEY` | Ed25519 | Verifies signed tokens from backend |
|
|
|
|
Pinned at build time. Any signed token that does not verify against this key
|
|
is rejected before any other processing occurs.
|
|
|
|
---
|
|
|
|
## State model
|
|
|
|
The companion's run flow is linear. There is at most one active run at a time.
|
|
|
|
The companion's home screen is freely accessible without authentication, following
|
|
the pattern of transaction-gated apps (e.g. PayPal). Biometric or device PIN is
|
|
required at the point the user initiates a run — when they tap "Scan QR" —
|
|
before the camera opens. The vault remains unlocked for the duration of the run.
|
|
Lock policy after run completion (immediate re-lock vs. timeout window) is a
|
|
deferred UX decision.
|
|
|
|
### States
|
|
|
|
- `idle` — home screen; freely accessible; no active run.
|
|
- `authenticating` — biometric or device PIN prompt active; triggered by tapping
|
|
"Scan QR."
|
|
- `scanning` — authentication passed; vault unlocked; camera active.
|
|
- `verifying` — QR parsed; running: Ed25519 signature check, TTL check, backend
|
|
bundle fetch, alpha_hash recomputation.
|
|
- `key_exchange` — verification passed; performing X25519 key agreement and
|
|
ML-KEM-768 encapsulation; sending `kem_ciphertext` to backend relay.
|
|
- `pin_display` — run key derived; PIN displayed to user. User reads
|
|
the PIN, confirms the match on the extension, then taps Continue on the
|
|
companion to advance. The companion does not receive a signal from the
|
|
extension; the two devices proceed independently from this point.
|
|
- `credential_select` — credentials for the current site presented; vault
|
|
already unlocked from biometric at session initiation. If exactly one
|
|
credential is stored for the site it may be pre-selected. If multiple are
|
|
stored the user must choose.
|
|
- `delivering` — credential selected; encrypted with AES-256-GCM under the
|
|
run key; ciphertext sent to backend relay.
|
|
- `complete` — ciphertext sent; run key discarded; brief confirmation shown;
|
|
returns to `idle`.
|
|
- `error` — run failed at any phase; reason and class held for display.
|
|
|
|
### Transitions
|
|
|
|
```
|
|
idle
|
|
→ authenticating (user taps "Scan QR")
|
|
|
|
authenticating
|
|
→ scanning (biometric or PIN accepted; vault unlocked)
|
|
→ idle (auth failed or user cancels)
|
|
|
|
scanning
|
|
→ verifying (QR code parsed)
|
|
→ idle (user cancels)
|
|
|
|
verifying
|
|
→ key_exchange (all checks pass)
|
|
→ error/ttl_expired (TTL in QR token has passed)
|
|
→ error/signature_invalid (security: Ed25519 signature fails)
|
|
→ error/alpha_hash_mismatch (security: key bundle does not match QR commitment)
|
|
→ error/bundle_consumed (security: backend reports bundle already fetched)
|
|
→ error/backend_unreachable (network: cannot fetch key bundle)
|
|
→ idle (user cancels)
|
|
|
|
key_exchange
|
|
→ pin_display (kem_ciphertext sent; run key derived)
|
|
→ error/relay_error (network: cannot send kem_ciphertext)
|
|
|
|
pin_display
|
|
→ credential_select (user taps Continue)
|
|
→ idle (user cancels; run key discarded)
|
|
|
|
credential_select
|
|
→ delivering (user selects credential)
|
|
→ idle (user cancels; run key discarded)
|
|
|
|
delivering
|
|
→ complete (ciphertext sent to relay)
|
|
→ error/relay_error (network: relay unreachable or error)
|
|
|
|
complete
|
|
→ idle (automatic after brief display)
|
|
|
|
error
|
|
→ authenticating (user retries; security-class errors do not offer retry)
|
|
→ idle (user dismisses)
|
|
```
|
|
|
|
### Error classes
|
|
|
|
*Security-class* (companion notifies backend; user sees explicit security
|
|
warning; no retry offered):
|
|
- `signature_invalid` — Ed25519 signature on the QR token does not verify
|
|
against the pinned `ED25519_PUBKEY`.
|
|
- `alpha_hash_mismatch` — key bundle fetched from backend does not match the
|
|
commitment in the QR. Indicates tampering between the extension and backend.
|
|
- `bundle_consumed` — backend reports the bundle was already fetched before the
|
|
legitimate companion reached it. Possible QR hijack.
|
|
|
|
*Timeout / user class* (expected; retry offered):
|
|
- `ttl_expired` — the TTL in the signed token had already passed when the
|
|
companion scanned.
|
|
- `user_abort` — user cancelled at any phase.
|
|
|
|
*Network class* (transient; retry offered):
|
|
- `backend_unreachable` — key bundle fetch failed.
|
|
- `relay_error` — could not send `kem_ciphertext` or credential ciphertext.
|
|
|
|
---
|
|
|
|
## Credential storage
|
|
|
|
Credentials are the companion's primary data. The companion is the only entity
|
|
in the system that holds them; there is no server-side copy.
|
|
|
|
### Structure
|
|
|
|
Credentials are stored per domain. Each domain maps to an ordered list of
|
|
credential entries. A credential entry holds at minimum a label, a username,
|
|
and a password. TOTP secrets are a planned addition (deferred; see
|
|
`../flows.md`).
|
|
|
|
### Encryption at rest
|
|
|
|
The credential store is encrypted under a key held in the platform secure
|
|
enclave (Android Keystore). The key never leaves the enclave; all encrypt and
|
|
decrypt operations are performed in hardware. The encrypted store can be backed
|
|
up to user-controlled storage without exposing credentials — the backup is
|
|
useless without the enclave key, which does not travel with it.
|
|
|
|
Backup and restore, device-to-device transfer, and multiple-device sync are
|
|
deferred. See `../flows.md` — *Companion loss, replacement, and recovery*.
|
|
|
|
---
|
|
|
|
## Cryptographic responsibilities
|
|
|
|
- **QR verification:** Ed25519 signature check against pinned `ED25519_PUBKEY`;
|
|
TTL check; recomputation of `alpha_hash` from the fetched key bundle.
|
|
- **Key exchange:** X25519 key agreement against the extension's public key;
|
|
ML-KEM-768 encapsulation against the extension's ML-KEM-768 public key →
|
|
`kem_ciphertext` and `kem_shared_secret`.
|
|
- **Session key derivation:**
|
|
`HKDF-SHA256(x25519_shared || kem_shared_secret, salt=runId_utf8, info="locqr-run-key-v1", length=32)` —
|
|
classical value precedes post-quantum per the normative ordering rule in `../crypto.md`.
|
|
- **PIN derivation:** `HKDF-SHA256(run_key, salt=[], info="locqr-pin-v1",
|
|
length=4)`, displayed as a zero-padded six-digit decimal.
|
|
- **Payload encryption:** AES-256-GCM with a fresh nonce; exactly one
|
|
encryption per run key.
|
|
|
|
All algorithm parameters and normative ordering rules are in `../crypto.md`.
|
|
|
|
---
|
|
|
|
## Backend API surface
|
|
|
|
- Fetch key bundle by run ID (at-most-once; bundle destroyed on fetch).
|
|
- Send `kem_ciphertext` to relay addressed to run ID (Phase 2).
|
|
- Send encrypted credential ciphertext to relay addressed to run ID (Phase 4).
|
|
- Report security-class errors to `/security/report`.
|
|
|
|
Request/response schemas are in `../interfaces.md`. Endpoint behaviour is in
|
|
`../server/claude.md`.
|
|
|
|
---
|
|
|
|
## Security invariants
|
|
|
|
- The run key is never written to persistent storage. It is discarded
|
|
immediately on `complete` or on any transition to `idle` or `error`.
|
|
- Biometric or device PIN authentication is required to initiate a run
|
|
(before the camera opens). The vault remains unlocked for the run duration.
|
|
Authentication is not re-requested mid-run.
|
|
- Any signed token that does not verify against the pinned `ED25519_PUBKEY` is
|
|
rejected before any network call is made.
|
|
- The companion does not proceed past `verifying` if the alpha_hash recomputed
|
|
from the fetched key bundle does not match the value in the QR token.
|
|
- Security-class errors are reported to the backend. No retry is offered.
|
|
- The companion operates anonymously with respect to the backend. It holds no
|
|
registered identity and requires no user account.
|