Dev.to · 6 min read

I ship an Ed25519 public key in my JavaScript bundle and call it a licence

I ship an Ed25519 public key in my JavaScript bundle and call it a licence

I build a Bates-numbering tool for litigation PDFs. Bates numbers are the unique page identifiers stamped across a document production — DEF-000123, DEF-000124, running continuously — so a court or an opposing party can point at one exact page. It runs entirely in the browser. The reason it runs entirely in the browser is the part people don't guess, and it's the part that forced every interesting decision downstream, including the licence check. The constraint: the documents can't leave the device The PDFs this tool processes are privileged. Under a protective order, the court can literally forbid the material from leaving the machine it was produced on — "highly confidential — attorneys' eyes only" is a phrase with teeth. Uploading to a server is not a "trusted" option, it's a non-option. A hosted SaaS that asks you to "securely upload your privileged documents for processing" is asking the wrong question. So the tool parses and rewrites PDFs locally with pdf-lib, in the tab, and writes the finished files through the File System Access API to a folder you pick. Your originals are never touched; output goes somewhere else, because stamping a PDF can't be undone. You can verify that claim instead of trusting it. The only network requests the page ever makes are six counters — page view, batch finished, check finished, checkout opened, licence verified, error type. Each is a ~30-byte JSON object, no cookie, no identifier, no filename, no page count: // src/lib/analytics.ts const ENDPOINT = 'https://api.bateskit.com/e' export type Event = 'view' | 'run' | 'check' | 'buy' | 'unlock' | 'error' function send(body: string): void { const blob = new Blob([body], { type: 'text/plain;charset=UTF-8' }) if (navigator.sendBeacon?.(ENDPOINT, blob)) return void fetch(ENDPOINT, { method: 'POST', body, keepalive: true, mode: 'no-cors' }) } Block the endpoint and the tool is identical. Or load the page once, disconnect from the network, and run a batch — it works. That's the property a hosted tool can't offer, and it's the whole reason to use it. The consequence: there is no server to ask "did they pay?" Here's the cascade. The tool can't have a server, because the documents can't go to one. And if there's no server, there's no licence server. Every SaaS licence check works by asking the server whether a key is valid, because the server is the trusted party. Mine can't have one — and the person doing production QC might be on a machine that has never been on the network. Air-gapped is not a hypothetical for this audience. So the licence check has to run offline. That means the verification logic ships in the page, and the verification key ships in the page. The key format is BK1.., both halves base64url, and the verification is a single WebCrypto call — no crypto library at all: // src/engine/license.ts export async function verifyLicense(key: string): Promise { const parts = key.trim().replaceAll(/\s+/g, '').split('.') if (parts.length !== 3 || parts[0] !== 'BK1') return { ok: false } const payload = fromB64Url(parts[1]) const sig = fromB64Url(parts[2]) const pub = await crypto.subtle.importKey('raw', fromB64Url(PUBLIC_KEY_B64), 'Ed25519', false, ['verify']) const ok = await crypto.subtle.verify('Ed25519', pub, sig, payload) return ok ? { ok, order: new TextDecoder().decode(payload) } : { ok: false } } The payload is just the order reference. The public key is a 32-byte constant sitting in the bundle: const PUBLIC_KEY_B64 = 'RzWyRhHf12xzr32jodAg0CiconSeTLDz6_E6n_xpr0M' The signing half of the pair never leaves the signing machine. crypto.subtle.verify is available in every browser, so this has zero dependencies and needs no network at any point. The part I had to accept: the wall doesn't exist Now the honest part. I can't actually stop anyone from pirating this, and neither can anyone building the same thing. The public key is public by construction — you cannot hide the verifying half of a signature, because the verifier is the whole point of the mechanism. Anyone can open the bundle, extract the key, and sign their own payloads. Or simply read the verification code and patch the result. There is no technical wall here. There is no wall that could exist, offline, in shipped JavaScript. The model I landed on works by accepting that and choosing which failure mode I don't want. A licence that phones home to a server can't be cracked, but it can lock out a paying customer the day the server goes away — or the day I get bored and stop paying for the domain. An offline-verified licence can be extracted, but it keeps working forever, air-gapped, with no account, no telemetry, and no one to go out of business. For a $19 one-time purchase used four times a year, "keeps working if the maker vanishes" is worth more than "hard to pirate." So the free tier is one file up to 25 pages with every feature enabled — genuinely useful, enough to validate the output on your own document — and the paid tier is a signed key that unlocks unlimited use. The licence isn't a fortress; it's a convenience unlock for people who'd rather pay $19 than extract a 32-byte key. That's the entire anti-piracy strategy, and it's a deliberate one. Why I'm telling you this Tools like this usually get dismissed as "just a wrapper" or — worse for a tool handling privileged documents — assumed to be secretly phoning home. The offline licence is my answer to both, but it only works as an answer because the verification code is readable. Security through obscurity would be a contradiction here: the whole model depends on the user being able to look at the bundle and confirm the only cryptography is a signature check against a published key. If you're building a client-side tool that needs to charge for it, you'll hit the same wall. The instinct is to bolt on a server just for the licence. Before you do: ask whether the thing you lose — a customer who can never be locked out — is worth the piracy you think you're preventing. For a small one-time price on a tool your users run on machines that may never see the network, it probably isn't. BatesKit is at bateskit.com. It numbers a folder of PDFs, checks redaction and metadata and privilege markers, reconciles against your index, and writes a Summation-style load.dat — all locally, all read-only on the checks, $19 once.

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More Programming & Dev News