Skip to content

smyrnakis/friend-lock

Repository files navigation

friend-lock 🔒

Lock a file for a friend using their password — without ever knowing it, and without being able to reopen the file yourself.

You have a file. A friend has a password only they know. friend-lock lets you encrypt the file so that only your friend can ever open it — not you, not anyone the file passes through. The locked file even looks like an ordinary photo.

It's a single HTML file that runs entirely in your browser. No install, no server, no account, nothing uploaded anywhere.


Why the obvious idea (a "password hash") doesn't work

The natural first thought is: my friend hashes their password, gives me the hash, I encrypt with it. But with any symmetric scheme, whatever locks also unlocks — if you can encrypt with it, you can decrypt with it too. That defeats the whole point.

The primitive that actually separates "can lock" from "can unlock" is public-key cryptography, and that's what friend-lock uses.

How it works

  1. Your friend creates a lock code. They pick a password. friend-lock stretches it with Argon2id into a seed and deterministically derives an X25519 key pair. You receive the public key (bundled as a short "lock code"). The private key is never stored — it only ever exists for a moment while your friend re-types the password to unlock.

  2. You lock the file. friend-lock zips your file(s) and encrypts them to your friend's public key using libsodium's sealed box (crypto_box_seal). A sealed box uses a throwaway ephemeral key that is immediately discarded — so even you, right after locking, cannot open it again. Only your friend can.

  3. Your friend unlocks. They open the locked file, type their password, friend-lock re-derives the same private key, and the original file(s) come back.

friend  ──(password)──▶  Argon2id ──▶ X25519 key pair
                                          │  public key
                                          ▼
you  ──(file + public key)──▶ sealed box ──▶ locked .jpg   ✗ you can't reopen
                                                  │
friend ──(locked .jpg + password)──────────────▶ original file  ✓

The "it's just a photo" trick

The locked output is a real, viewable .jpg: a blueprint-style card with a notice and a QR code pointing back here. The encrypted data is appended after the image's end marker, which every normal photo viewer ignores. Double-click it and you see a picture; drop it into friend-lock and it decrypts.

⚠️ Send it as a file, not as an in-chat photo. WhatsApp, Telegram, iMessage, Instagram, etc. re-compress images and would strip the hidden data. Send the .jpg as an email/Drive/USB attachment, or use the "Download raw .flock" option for those channels — both unlock identically.

Usage

Open dist/friend-lock.html in any modern browser (Chrome, Edge, Firefox, Safari). That one file is everything. It works offline straight from disk, and later it can be hosted as a static page (e.g. GitHub Pages) with no changes.

  • Create lock code — your friend does this once and sends you the code.
  • Lock a file — you paste the code, add file(s) or a folder, and download the locked .jpg.
  • Unlock — your friend drops the file in, types their password, gets the original back.

Security notes

This tool optimizes for convenience, not maximum security. Be aware:

  • Everything rests on the password. The private key is derived from it, so a stolen locked file is open to offline password guessing. Argon2id makes each guess slow, but a weak password is still weak. Choose a strong passphrase.
  • There is no password reset. Forget the password → the file is gone forever. That's the design.
  • The lock code is safe to share — it contains only a public key and a (non-secret) salt.
  • Confidentiality + tamper-detection come from the sealed box, but the sender is anonymous: your friend can't cryptographically prove you were the locker.
  • Running the single file offline is the most private option — nothing can leave your machine.

Cryptography

Purpose Primitive
Password stretch Argon2id (crypto_pwhash, interactive params)
Key pair X25519, deterministically seeded from the password
Encryption Sealed box (crypto_box_seal, X25519 + XSalsa20-Poly1305)
Packaging ZIP (preserves names, folders, multiple files)

All via libsodium (the -sumo build).

File formats

lock code    = base64url( u8 version | u32 ops | u32 mem | 16B salt | 32B pubkey )
.flock blob  = "FLK1" | u8 version | u32 ops | u32 mem | 16B salt | sealed ciphertext
.jpg carrier = <valid JPEG cover> | .flock blob | "FLCKPLD1" | u64 payloadLength

The salt and KDF parameters travel inside the file so your friend only needs to remember the password.

Development

npm install          # dev deps (libs to vendor, Playwright for tests)
npm run vendor       # copy library files into vendor/
npm run build        # inline everything into dist/friend-lock.html
npm test             # end-to-end round-trip test in headless Chromium

Source layout:

  • friendlock.js — the crypto/format core (no UI); the part worth auditing.
  • index.html — the four-tab UI (loads vendor/*.js + friendlock.js).
  • technology.html — technical explainer page (how it works, disclaimer).
  • privacy.html — plain-language privacy note.
  • 404.html — friendly not-found page.
  • build.mjs — inlines all local scripts into the single dist/friend-lock.html and rewrites the sibling-page links to absolute SITE_URL links.
  • vendor/ — pinned copies of libsodium, JSZip, and the QR generator.

Hosting

The whole thing is static, so it hosts anywhere for free and never runs code on a server (which keeps it cheap, spike-tolerant, and GDPR-friendly).

GitHub Pages (simplest): the repo is ready — index.html at the root loads vendor/ and friendlock.js via relative paths, and a .nojekyll file keeps Pages from touching anything.

  1. Repo Settings → Pages.
  2. Source: Deploy from a branch → main / root, Save.
  3. Site goes live at https://smyrnakis.github.io/friend-lock/.

Cloudflare Pages (recommended long-term): connect the repo, framework preset None, build command empty, output directory /. You get a global CDN and can later add Turnstile / rate limiting. Neither host meters bandwidth on the free tier, so a traffic spike is absorbed by the CDN and can never generate a bill.

When you move to a custom domain, update SITE_URL in build.mjs so the standalone single-file's links point at the right place.

Roadmap

  • Point the cover QR at the live site once the domain is settled.
  • Optional hardening: Cloudflare Turnstile, user-selectable passphrase strength, IP rate limiting.

License

MIT

About

A tool allowing a trusted person to lock your resources without revealing the password to you.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors