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.
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.
Byte-level deltas
Pipeline visualization
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.
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.
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.
- 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.
- 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) needstandardorloose. - 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.
- Pick a unique
storageKeyper gate. The gate persists an unlock token insessionStorageso users don't re-paste on every navigation within the tab. Bumping the key invalidates existing unlocks (use this when rotating reference images). - Optionally pass
debugwhile developing — the failure panel will show distance and threshold per hash so you can see how close a near-miss came.
- 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)
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.
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-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/.