Exploration · 2026-04-30

Image as password

A canonicalization-and-perceptual-hash pipeline for using images as authentication signals. The interesting question is how much platform-induced byte variation a hash can survive while still rejecting unrelated images. This page lets you paste two images and inspect every stage — bitmap, blur, hash, byte deltas — then enroll a reference image and drop the resulting <ImagePasswordGate /> into a real page.

Step 1

Explore the pipeline

Two-slot comparison with byte-level deltas. Paste, draw, or snap two images to see what survives the canonicalization steps.

image-as-password — pipeline explorer

Load two images via paste, file upload, freehand drawing, or webcam capture. Each runs through the canonicalization pipeline (grayscale → square crop → blur → resize) and yields three perceptual hashes. The comparison panel shows hamming distance, per-hash thresholds, and a combined match verdict.

Image A
Paste · Drop · Click
Click first to focus, then Cmd+V. Or drop a file.
Image B
Paste · Drop · Click
Click first to focus, then Cmd+V. Or drop a file.
0.80
Load images into both slots to see the match verdict and confidence.

Byte-level deltas

Load both slots to inspect byte-level deltas.

Pipeline visualization

no image yet
no image yet
Step 2

Enroll a reference image

Run an image through the same pipeline to produce its perceptual hashes. Copy the snippet into a page that should be gated.

Reference image
Paste · Drop · Click
Click first to focus, then Cmd+V. Or drop a file.
Step 3

Try the gate

ImagePasswordGate is a design-system component. Anything inside its children only renders after the user presents a matching image. This demo enrolls and gates in the same view.

Enroll a reference image. Once you do, this section becomes a real ImagePasswordGate — paste the same (or a re-encoded) image to unlock the hidden content.

Step 1 — enroll
Paste · Drop · Click
Click first to focus, then Cmd+V. Or drop a file.
The gate will appear here after enrollment.
Reference

Using ImagePasswordGate in a real page

Drop the snippet from Step 2 into any client component. The gate handles input, hashing, comparison, and sessionStorage-based unlock persistence.

  1. Pick the image that will act as the password. Anything works (logo, photo, screenshot) but textures with mid-frequency content (text, icons, distinct shapes) hash more reliably than gradients or flat color fields.
  2. Use Step 2 to enroll it. Take note of which kind of capture you expect users to provide — original PNG download, Slack-shared JPEG, phone photo of a printed copy, screenshot? Tighter capture paths can run tolerance="strict"; messy paths (camera, social pipelines) need standard or loose.
  3. Copy the generated snippet into a client component. The expected hashes are not a secret in any cryptographic sense — they're short hex strings that will end up in your bundle. Anyone with the original image can derive them.
  4. Pick a unique storageKey per gate. The gate persists an unlock token in sessionStorage so users don't re-paste on every navigation within the tab. Bumping the key invalidates existing unlocks (use this when rotating reference images).
  5. Optionally pass debug while developing — the failure panel will show distance and threshold per hash so you can see how close a near-miss came.
Tolerance reference
  • strict — only near-identical captures (same digital file, same canonical resize)
  • standard — tolerates JPEG re-encode, web-platform recompression, light cropping
  • loose — accepts visually similar variants (camera capture of printed image, heavy filtering)
Threat model

ImagePasswordGate is a UX gate — it keeps casual visitors out of a page until they present the right image. It is not a security control. The expected hashes ship in your client bundle, and the matching logic runs in the browser. Don't put real secrets behind it.

For AI agents

Skill prompt

A self-contained how-to-use-this-discovery designed for other agents working in this codebase. Copy verbatim into a skill file or system prompt.

skill: image-as-password5x5 / explorations
# Skill: image-as-password gate

PURPOSE
Render a client-side gate that unlocks page content only when the user
pastes / drops / uploads an image whose perceptual hash matches an
enrolled reference. Robust to JPEG recompression, light cropping, and
platform image-handling (Slack, iMessage, web upload pipelines).

WHEN TO USE
- Soft gate behind party invites, easter eggs, group experiences.
- You have a known reference image and want anyone holding a copy to pass.
- You want privacy: matching is fully client-side, no server round-trip.

WHEN NOT TO USE
- Security-critical content. Expected hashes ship in the client bundle —
  anyone with the source image can derive them.
- Inputs where the image will be heavily edited (filters, cropping past
  ~30%, rotation). Use OCR / a real classifier instead.

CORE API
```tsx
import ImagePasswordGate from "@/components/ui/ImagePasswordGate";

<ImagePasswordGate
  storageKey="some-unique-key"     // bumping invalidates existing unlocks
  tolerance="standard"              // "strict" | "standard" | "loose"
  expected={{
    p: "70979f580f30f2c2",          // pHash hex (recommended)
    d: "090235c3c0404040",          // dHash hex (recommended)
    a: "0e1fe742c0c0c080",          // aHash hex (optional)
  }}
  hint="Paste your invite"          // optional
  debug                             // optional, surfaces distances on miss
  onUnlock={() => {/* ... */}}      // optional
  fallback={<>...</>}                // optional, shown below the gate UI
>
  <ProtectedContent />
</ImagePasswordGate>
```

ENROLLMENT (one-time, manual)
1. Visit /explorations/image-as-password
2. In Step 2 (Enroller), paste/drop/upload the reference image
3. Click "Copy" on the snippet panel — paste it into your page

PROGRAMMATIC VERIFICATION (no JSX)
```ts
import { verifyBlob } from "@/lib/explorations/image-as-password";
const result = await verifyBlob(blob, expected, { tolerance: "standard" });
if (result.passed) { /* unlocked */ }
// result also exposes perHash distances, matchedCount, required
```

MULTI-MATCH / DIRECTORY USE CASE
For "many enrolled images, route to whichever matches" (link-shortener-
style), see /explorations/image-directory and the helpers in
src/lib/explorations/image-as-password/directory.ts. The events registry
at src/lib/events.ts is the canonical EventEntry list — ImageDirectoryGate
takes the same shape.

EVENTS REGISTRY
src/lib/events.ts exports EVENTS: EventEntry[]. Each entry maps an image
password to an external destination (and optional reveal/mask images).
EventEntry: { id, name, externalUrl, hashes: {p,d,a}, revealImageUrl?, maskImageUrl? }.
Use the Enroller's "Copy event entry" button to produce a paste-ready
entry block.

TOLERANCE TUNING
- strict   — only near-identical captures (same digital file, same canonical resize)
- standard — JPEG re-encode, web-platform recompression, light cropping (default)
- loose    — camera capture of printed image, heavy filtering

TROUBLESHOOTING
- false rejects → switch to "loose"; turn debug=true to see per-hash distances
- false accepts → switch to "strict"; require all 3 hashes
- screenshots vs originals → enroll from the screenshot path users will actually take

THREAT MODEL
UX gate, not a security control. Hashes ship in the bundle. Anyone with
the original image can derive them and unlock. Acceptable for
party-style use; never put real secrets behind it.

PIPELINE INTERNALS
Decode → EXIF orient → Y-luminance grayscale → center square crop →
Gaussian blur (σ default 0.8) → resize to 16/32/64/128 → 32×32 DCT for
pHash, 9×8 row deltas for dHash, 8×8 mean threshold for aHash.
2-of-3 voting decides match. See src/lib/explorations/image-as-password/.