# VisualEyes — Architecture *Prototype R1 — July 2026. Derived from the VisualEyes Design Specification SD R1 (25 FEB 21), updated to the 2026 positioning: VisualEyes is a **human-memory authentication factor** — device-free recovery, friendly second factor, and primary login for low/medium-value accounts — rather than a head-on universal password replacement.* ## Positioning Users authenticate by recognizing their own photographs among decoys. The secret lives in human recognition memory: nothing to remember, nothing to carry, works from any screen in any language. Three products, one engine: 1. **Recovery factor** — "lost my passkey/device" flow for sites that use passkeys as primary. 2. **Second factor** — a friendlier step-two than TOTP codes or SMS. 3. **Primary login** for low/medium-value accounts (memberships, communities, loyalty) where the pitch is UX + zero credential liability for the site. ## Components ``` ┌────────────┐ 1. create challenge (HMAC-signed, server→server) ┌──────────────────┐ │ Client site │ ───────────────────────────────────────────────────▶ │ VisualEyes server │ │ ("Freddie's │ ◀─────────────── challenge_url ──────────────────── │ │ │ Food Shop") │ │ challenge engine │ └─────┬──────┘ 2. browser redirected to challenge_url │ image store │ │ ┌───────────────────────────────────────┐ │ (encrypted) │ ▼ │ Challenge UI (VisualEyes-hosted page) │──────────▶│ policy / rate │ user's │ pie-chart rounds, catch rounds, skips │ │ limiting / audit │ browser ────▶ │ selections POSTed per round │ └──────────────────┘ └───────────────────────────────────────┘ ▲ 3. redirect back to client callback with single-use result_token │ └── 4. client site verifies result_token server→server (HMAC-signed) → signed verdict ``` The client site never sees the user's images and never holds any secret about the user. The browser never learns which image was correct — scoring is server-side only, revealed only at the end, and the authoritative verdict is only available via the server-to-server verify call (the browser redirect is UX, not proof). ## Challenge engine - A challenge = N scored rounds + K **catch rounds** (rounds containing none of the user's images; the correct answer is the center "not here" button). Catch rounds defeat the "always pick the most personal-looking photo" heuristic and detect bots. - Each round shows M images (default 6, range 4–8) in an **n×n square grid** with a "Not here" tile occupying a random cell (the pie-chart layout from the patent filing is an alternative embodiment; the grid is the shipping presentation). Image→cell assignment and the keyboard numbers on each image are independently randomized per round, so neither click positions nor typed digits are replayable (keylogger / mouse-tracker resistance, per the original design spec). - **Skips**: the center button in a scored round consumes a skip allowance and appends a replacement round ("my image isn't recognizable right now" / deliberate omission). - **Misses**: a wrong pick consumes a miss allowance and appends a replacement round (the spec's "tag along" feature). Exceeding either allowance fails the challenge. - **No per-round oracle**: the answer response never says correct/incorrect; the user learns only pass/fail at the end. Prevents an attacker from binary-searching the user's images. - **No-repeat policy**: target images are chosen least-shown-first, so an observer of one session learns as little as possible about the next (intersection-attack mitigation). Enrollment should build a pool ≥ 3× the rounds-per-challenge; larger pools = more security. - Guessing odds: 6 images × 4 scored rounds ≈ 1/1,296 per attempt at default policy; 8 × 6 ≈ 1/262,144 at high policy. These odds are meaningful **only** together with the rate limits below — the lockout policy is a first-class security control, not a nicety. ## Security controls (implemented in prototype) | Threat (from design review) | Control | |--------------------------------------|---------| | Brute force / trial-and-error | Per-user lockout: 3 failed challenges → exponential lockout; per-IP throttle on session APIs | | Replay of challenge or result | Challenge tokens: 256-bit random, single session, 5-min TTL. Result tokens single-use, verified server-to-server only | | Client-site impersonation | Client API calls HMAC-signed (client_id + shared secret + timestamp, ±120 s window) | | Image enumeration / scraping | Image URLs are per-challenge signed tokens scoped to the challenge and its TTL; image ids never exposed; no user id in any browser-visible URL | | Keylogger / mouse tracker | Randomized sector positions and randomized keypad numbers per round | | Bot picks "most personal" image | Catch rounds with no correct image | | Per-round correctness oracle | Scoring hidden until challenge completes | | Intersection attack (repeat viewing) | Least-shown-first target selection; times_shown/last_shown tracked per image | | Server image-store breach | Image blobs encrypted at rest with per-user derived keys (prototype: HMAC-CTR stream cipher + encrypt-then-MAC; production: AES-256-GCM via KMS-held master key) | | Challenge left open | Server-side TTL; client-side round countdown auto-skips | ## Known limits (honest, by design — drive the roadmap) - **Live phishing relay** is not defeated by the mechanism itself. Roadmap: origin-bound challenge URLs + client-callback allow-listing (prototype pins callbacks to the registered client prefix), and Push-to-Phone completion (spec's dual-channel idea) for high-assurance. - **AI image matching vs. public photos**: enrollment guidance requires never-shared images, enforced by enrollment-time reverse-image screening (TinEye-backed, pluggable — `server/screening.py`; offline mode marks images "unscreened"). Personal photos are never sent to third parties for decoy sourcing — that would leak the secret image set; decoys come from category-matched public pools. - Security ceiling means VisualEyes is sold as a *factor* (VE1 assurance), not a sole guardian of high-value accounts. RLV1/RLV2 real-world-verification flags from the spec layer on top as separate assurance signals. ## Data model (SQLite in prototype; Postgres in production) `users` (email + separate system key so an email can be safely reused after deletion, per spec) · `images` (kind personal/decoy, category, encrypted blob, times_shown, last_shown) · `clients` (client_id, secret, callback prefix, default policy) · `challenges` (token, state, policy snapshot, miss/skip counters, result_token, verified flag, TTL) · `rounds` (seq, kind scored/catch, layout incl. randomized numbers, correct_slot, selection) · `lockouts` · `audit` (append-only event log). ## API surface **Client (server-to-server, HMAC-signed):** - `POST /api/v1/challenges` `{user, callback_url, policy?}` → `{enrolled, challenge_url, expires_at}` - `POST /api/v1/verify` `{result_token}` → `{passed, user, stats, assurance:"VE1"}` (single use) - `POST /api/v1/enroll` `{email, images:[{category, data_b64}] | generate:{count}}` **Browser (bearer = challenge token):** - `GET /c/` challenge UI · `GET /api/session/` current round · `POST /api/session//answer` `{selection}` · `GET /img/` ## Production deltas (not in prototype) TLS everywhere · Postgres + object store · AES-GCM with KMS · WebAuthn-style origin binding for challenge pages · Push-to-Phone (iOS/Android apps, also the image-enrollment on-ramp per spec) · category-matched decoy sourcing at scale · GDPR data-deletion flows (spec §Deleting accounts) · structured billing (subscription + per-transaction, per the business plan; token economics deliberately decoupled).