Skip to content

vesvault/VESlocker

Repository files navigation

          ___       ___
         /   \     /   \    VESvault
         \__ /     \ __/    Encrypt Everything without fear of losing the Key
            \\     //                   https://vesvault.com https://ves.host
             \\   //
     ___      \\_//
    /   \     /   \         VESlocker:        Hardware-grade PIN Security API
    \__ /     \ __/
       \\     //                                        https://veslocker.com
        \\   //
         \\_//
         /   \
         \___/

VESlocker — Hardware-grade PIN Security API

CI client: Apache-2.0 server: PolyForm Perimeter live demo

Give a short PIN the strength of a hardware security module — without the hardware.

A 4-digit PIN has only 10,000 combinations. Anywhere it guards data that an attacker can copy (a browser, a file, a backup), they can brute-force it offline in milliseconds. VESlocker removes the offline attack entirely: the decryption key is split between the user's PIN and a secret held by a small key server, and the server hands the key back only under a strict, exponentially-throttled budget. After a few dozen wrong guesses the entry is locked for good — a true lifetime cap on attempts, the way a TPM or a secure enclave behaves, implemented in ~250 lines of vanilla JS and ~110 lines of PHP.

🔎 Try it: there is a live demo at veslocker.com.


How it works

  client                                            key server
  ──────                                            ──────────
  challenge = SHA-256(seed ‖ PIN)  ───── id, challenge ─────▶  look up per-id secret
                                                               throttle check (2^attempts)
  key = AES-GCM key  ◀──── SHA-256(secret ‖ challenge) ──────  return derived key
  ciphertext = AES-GCM(key, data)
  (ciphertext + seed are stored locally; the key is never stored)
  • The data is encrypted client-side with AES-GCM. The key is derived from the server's per-entry secret and a challenge seeded by the user's PIN.
  • The server never receives the seed, the PIN, the plaintext, or the ciphertext — only an id and a challenge (a hash). It returns the derived key, gated by throttling.
  • Each request to an id increments an attempt counter; the next request is refused until 2^attempts seconds have passed (capped at 32). After ~32 attempts the wait exceeds a century — the entry is effectively sealed.

Quickstart

Client

Install from npm (Node 20+, or any bundler):

npm install veslocker
import VESlocker from "veslocker";

…or drop it straight into a page — same API, no build step:

<script src="https://veslocker.com/pub/VESlocker.js"></script>

Then:

// apiUrl defaults to the public veslocker.com key server, so you can try it
// with zero config. Pass apiUrl to point at your own server/VESlocker.php:
const vl = new VESlocker({
  apiUrl: "https://your-key-server.example/VESlocker.php"
});

// Encrypt a secret behind a PIN and stash it in localStorage under a name:
await vl.store("launch-codes", "1234", "the actual launch codes");

// Read it back with the PIN. fetch() returns the value, and on every read
// re-encrypts under a fresh id + seed (resetting the attempt budget) — so it's
// the right call for routine reads:
const secret = await vl.fetch("launch-codes", "1234");          // "the actual launch codes"

// Supply a new PIN and the *same* call also rotates it: fetch() reads with the
// old PIN and re-encrypts under the new one in one step, still returning the value:
const same = await vl.fetch("launch-codes", "1234", "5678");    // "the actual launch codes", now behind 5678

⚠️ Prefer fetch() over peek() for routine reads. peek() decrypts without rekeying: each call spends one of the entry's ~32 lifetime attempts and leaves the counter climbing toward the permanent seal. Reach for peek() only when you deliberately don't want to rotate the entry.

There's a runnable Node example in example/node.mjs and a browser one in example/browser.html.

Prefer to manage storage yourself? encrypt / decrypt are stateless and return / take a self-contained token:

const token = await vl.encrypt("1234", "the actual launch codes");
const back  = await vl.decrypt("1234", token);   // "the actual launch codes"

Get notified when an entry's key is accessed (an early-warning signal that someone is guessing the PIN):

vl.accessFn = (id, count, at) => {
  if (count > 0) console.warn(`VESlocker '${id}' accessed ${count}× (last: ${at})`);
};

Key server

The server is a single PHP file with a PDO storage layer. It defaults to SQLite — no database server to install, one file on disk — and the schema is created automatically on first request:

  1. Serve server/VESlocker.php behind any PHP-capable HTTP server (PHP 7.4+, with the pdo_sqlite extension — bundled by default).
  2. Point $DBsqlite at a writable path outside the web root (the file holds every entry's secret, so it must never be served).
  3. Point the client's apiUrl at it.

Prefer MySQL/MariaDB (e.g. you already run one, or want multiple app servers sharing a store)? Set $DBdriver = "mysql", fill in the $DB* settings, and load the schema from server/VESlocker.mysql.sql.

Requirements — Server: PHP 7.4+ with PDO (pdo_sqlite, bundled by default; or pdo_mysql). Client: any browser with Web Crypto (all current browsers).

Security model

VESlocker is a key oracle gated by throttling, not a vault. Reason about it honestly before you deploy:

  • The server cannot read your data. It never sees plaintext or ciphertext — only an id and a challenge hash.
  • No offline brute force. An attacker who copies the ciphertext cannot test PINs offline: the key also requires the server's secret, which they don't have.
  • Hard lifetime cap on online guesses. Exponential per-id throttling limits an attacker (and the legitimate user) to roughly 32 attempts, ever.
  • ⚠️ The cap is irreversible — by design. There is no reset: once an entry seals (or you simply forget the PIN), the data is gone for good, the way a lost hardware token is. The same property is a denial-of-service vector if an entry's id ever leaks — whoever holds it can spend the budget and permanently seal that entry. The id is 256 random bits and never guessable, so this reduces to: treat the id as a bearer capability, and keep an independent backup of anything you cannot afford to lose. VESlocker is for data where "lost forever" is an acceptable failure mode, not a system of record.
  • ⚠️ Availability dependency. Decryption requires the key server to be reachable. This is by design — it is what makes the throttle unavoidable.
  • ⚠️ Two-secret compromise. If an attacker obtains both the server-side secret store and the ciphertext, the PIN can be brute-forced offline (bounded only by PIN entropy). Protect the key server's database accordingly, and treat the PIN's entropy as your last line of defense in that scenario.

Found a vulnerability? See SECURITY.md — please report privately.

Operating the key server

Two operational rules fall straight out of the zero-knowledge design — get them right and the server is close to maintenance-free:

  • Never expire entries. The server cannot tell a stored-and-forgotten id from one written once and read back a year later — it has no knowledge of whether your ciphertext still exists. So it must never TTL or garbage-collect rows; doing so would silently destroy live data. Rows are tiny (~100 bytes) and kept for good; storage grows with genuine use, which is fine.
  • Creation is rate-limited for you. With no accounts, anyone can mint a new id, and each new id is an unauthenticated row insert — so VESlocker.php caps entry creation per source address with a built-in leaky bucket (VESlocker_ratelimit), no proxy or CDN config required; retrieval of existing ids is untouched. The defaults allow a burst of 600 creations per source and about one per second after that — tune $RateWindow / $RateCost at the top of VESlocker.php if your clients are busier or sit behind shared NAT. That table is self-cleaning; only the entry store is kept forever.

How it compares

VESlocker isn't a new cryptographic protocol — it's a deliberately minimal take on a known idea: harden a low-entropy secret (a PIN) with the help of a key server, so it can't be brute-forced offline. It's worth saying plainly how it relates to the rigorous prior art, and where it consciously trades rigor for simplicity.

The server is unauthenticated by design — and that is the zero-knowledge property. There are no accounts, no API keys, no per-user auth. The only credential is the entry's id: 32 cryptographically-random bytes (256 bits) minted on the client. You cannot throttle-attack — or even address — an entry whose id you don't hold, and guessing one costs 2^256. The server stores only (id → secret, attempt-counter) and learns nothing that links an id to a person, a PIN, or any ciphertext. The id is a bearer capability, not an identity.

What it's for Trust anchor Hard lifetime guess cap No special hardware Survives a malicious operator
VESlocker Decrypting local data behind a PIN Server operator + availability ✅ exponential → ~century ✅ any PHP host + Web Crypto by design
OPAQUE / aPAKE Login / session key agreement Server operator ❌ needs bolt-on rate limiting partial
Signal SVR PIN-recovering a stored secret Hardware enclave (SGX) + attestation ✅ destroy-after-N needs SGX
Pythia / PHE Hardening + rotatable password keys Server key (rotatable) ❌ focus is rotation partial (oblivious + rotation)

Read the table by its one standout cell: among the systems compared here, VESlocker is the only row that is ✅ in both "hard lifetime guess cap" and "no special hardware." Signal's SVR gets the cap by putting the throttle inside a secure enclave; OPAQUE and Pythia run on commodity servers but have no built-in attempt limit. VESlocker gets both by enforcing the throttle in ordinary application code — the trade-off being the last column: it trusts the operator, where an enclave would not.

  • vs. OPAQUE and PAKEs. OPAQUE proves knowledge of a password and agrees a session key without the server learning the password. It's built for authentication, and online guessing is still possible unless you add your own rate limiting. VESlocker does no handshake (one round trip: id + challenge = H(seed ‖ PIN)H(secret ‖ challenge)) and makes the throttle itself the headline feature — an exponential, lifetime-capped attempt budget — for the narrower job of unlocking local data.
  • vs. Signal's Secure Value Recovery. SVR is the closest cousin: a PIN, a server-side hard attempt limit, and a secret that becomes unrecoverable after too many tries. The difference is the trust anchor. SVR enforces the limit inside an SGX enclave with remote attestation, so it holds even against a malicious operator — at the cost of requiring that hardware and attestation infrastructure. VESlocker enforces the throttle in ordinary code on any commodity host, so it does not defend against the operator — it buys most of SVR's user-facing behavior for roughly none of the operational complexity, by trusting the operator and the network.
  • vs. Pythia / Password-Hardened Encryption. Pythia is a verifiable, oblivious PRF service with server-key rotation, so the server can't dictionary-attack the input even with its own key, and a stolen database can be invalidated by rotating. VESlocker's challenge is a plain hash, not blinded — the server could attack the PIN if it also had the seed, which it never does (the seed is client-only and never sent). VESlocker has no obliviousness and no rotation; in exchange it's SHA-256 and a counter column in ~150 lines of PHP, with nothing to install.

The honest one-liner: VESlocker gives a PIN the behavior of a hardware attempt-limiter — rate-limited, lifetime-capped, no offline attack — by trusting a reachable server instead of a chip. If your threat model includes a malicious server operator, reach for SVR or an OPRF service. If it's "someone copied my browser storage / my backup," VESlocker covers it at a fraction of the cost — and unlike a TPM, the protected data is portable to any device that has the token and the PIN.

License

VESlocker is licensed per component:

Component License
client/ Apache License 2.0 Embed it in your app with no copyleft obligations.
server/ PolyForm Perimeter 1.0.1 Use, modify, and self-host for any purpose — except offering a product that competes with VESlocker.

Everything else in this repository (documentation, examples, tests) is Apache License 2.0 — see LICENSE.

Contributing

Contributions are welcome under the DCO — see CONTRIBUTING.md. In short: sign off your commits with git commit -s.


Part of the VESvault project — Encrypt Everything without fear of losing the Key.

About

Hardware-enclave PIN security without the hardware: the key is split between the user's PIN and a rate-throttled key server, eliminating offline brute-force — wrong guesses hit a lifetime cap, like a TPM. Vanilla JS client + tiny PHP server.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

14 stars

Watchers

1 watching

Forks

Packages

 
 
 

Contributors