Guide

How locallock.app stays on this device

locallock.app is a local vault. Encryption happens in the browser with AES-256-GCM. There is no account, and documents are not uploaded unless you send a sealed copy yourself.

What is stored

A random vault key encrypts each document, including its name. Your password, PIN, and emergency code are never saved. Each one only wraps that key, using PBKDF2-HMAC-SHA-256 with 250,000 iterations. Closing the lock drops the key from memory. A portable .llock file is separate: its password is inside that file’s own seal, so you can open it on another computer.

Trial and lifetime membership

A new vault includes Pro for 14 days: folders, batch, backup, PIN, and files up to 400 MB. When that ends, Free stays — 8 documents, 20 MB each, one at a time. Pro returns after a $29 lifetime membership. One payment. It does not renew. The clock and the membership flag live in this browser. FULLFORCE LTD cannot open the vault from the registered office.

Stripe

When a secret key named STRIPE_SECRET_KEY is present on the server (test keys start with sk_test_), Buy lifetime opens Stripe Checkout for a single $29 payment. After payment, locallock.app checks the Checkout session and stores lifetime membership on this device. No card data is handled by locallock.app. Use Stripe’s test card 4242 4242 4242 4242 in test mode. If the key is missing, checkout cannot start and the trial still ends on time.

A webhook is optional. Storing membership from the success page is enough, because the record is local. Use a Worker only if you later want a server-side receipt.

Encrypted backup and R2

Backup is off until you add an https address in Settings. locallock.app seals the file with a password you type, then PUTs only those bytes. It refuses any address that is not https. Point the address at a Cloudflare Worker that writes the body into an R2 bucket. The bucket should not be public.

export default {
  async fetch(request, env) {
    if (request.method !== "PUT") return new Response("no", { status: 405 });
    if (request.headers.get("authorization") !== `Bearer ${env.BACKUP_TOKEN}`) {
      return new Response("no", { status: 401 });
    }
    const key = decodeURIComponent(new URL(request.url).pathname.slice(1));
    await env.VAULT_BUCKET.put(key, request.body);
    return new Response("ok");
  },
};

Allow this site in the Worker’s CORS rules for PUT and OPTIONS, including the authorization and content-type headers. The browser will not send a plaintext document through this path.

Cloudflare Pages and a custom domain

The marketing pages are static content and can sit on Cloudflare Pages. The vault itself needs the app host, because sealing runs here and Pro checkout calls Stripe from the server. To put a hostname on either one:

  1. Add the site in Cloudflare and import the project into Pages, or attach the hostname to the app host.
  2. In DNS, create the record Cloudflare shows — usually a CNAME — and leave the proxy on if you want Cloudflare’s certificate.
  3. Set SSL/TLS to Full. Wait until the certificate is active before sharing the hostname.
  4. Do not point a public bucket at unencrypted files. R2, if you use it, receives only .llock objects.

Device unlock

Settings can register this device’s authenticator. On Windows that is Windows Hello in Edge or Chrome, when the browser exposes a platform passkey with the PRF extension. If it does not, use the password. A PIN is optional and weaker than the password — it is a shortcut, not a second copy of a strong key.