All posts
Security Encryption AES-256-GCM

Zero-Knowledge Encryption in Practice: How PermitCore Keeps Your License Keys Secret

PC
PermitCore
June 15, 2026 · 8 min read
PLAINTEXT KEY PERMIT-7K9M-3XBP BROWSER ONLY AES-256-GCM encrypt SHA-256 hash PBKDF2 derive key SERVER STORES ciphertext (AES) HMAC hash only ✗ never sees plaintext customer's key your browser

When we say PermitCore uses "zero-knowledge encryption," we're not using the term as a marketing buzzword, but it's worth being precise about what it does and doesn't mean: the server never stores a plaintext key, and generation/encryption happens entirely in your browser before anything is sent for storage. It's not a claim that the server is blind to plaintext in every context — when your customer's app calls the validation endpoint, it does send the plaintext key over the wire, because that's how a stateless validation API works. What the server never does is persist it. Here's exactly how that works.

The Problem with Traditional License Key Storage

Most license management systems store keys in one of two ways: plaintext in a database column, or with server-side encryption using a key the server itself holds. Both approaches have the same fundamental weakness — if an attacker gains access to your database and your server, they have everything they need to extract every license key you've ever issued.

The more subtle problem: even without a breach, your license management provider can read your keys. That means they could, in principle, issue duplicate keys, see which customers have which licenses, or share that data with third parties. For enterprise customers with compliance requirements, this is a non-starter.

The PermitCore Approach: Client-Side Encryption

PermitCore generates and encrypts license keys entirely within your browser, before anything is sent to our servers. The encryption chain works like this:

Step 1 — Key generation. The browser generates a random license key in the format PERMIT-XXXX-XXXX-XXXX-XXXX, using a character set that avoids visually ambiguous characters (no O/0, I/1, etc.).

Step 2 — Key derivation. Your passphrase is run through PBKDF2-SHA256 with 600,000 iterations and your tenant's unique salt to derive a 256-bit AES key. This runs entirely in the browser using the Web Crypto API's SubtleCrypto interface.

Step 3 — Encryption. The plaintext key is encrypted with AES-256-GCM, producing ciphertext and a random 12-byte IV. Both are base64-encoded and sent to the server. The server stores these two values and nothing else.

Step 4 — Hashing for validation. Before sending the key to the server, the browser computes SHA-256 of the plaintext. The server then applies an HMAC-SHA256 over that hash using a secret key it holds. The result is stored as the validation lookup index. Neither value reveals the plaintext.

Validation Without a Plaintext Round-Trip to Storage

When a customer presents a license key to your application, the SDK sends the key to POST /api/v1/validate. The plaintext key does reach the server at this point — over TLS, in the request body, transiently in memory — because that's what the server needs to compute SHA-256 of it. It applies HMAC to that hash and looks up the result in the database. If the HMAC hash matches a stored record, the key is valid. The server never stores, logs, or persists the plaintext anywhere — it's hashed and discarded within the same request, which is the actual zero-knowledge guarantee: not that the server is blind to plaintext, but that a stolen database can never expose it.

This is analogous to how password storage works: you never store the password, you store a hash of it. The innovation here is that we've applied the same principle to license key validation at scale, with the additional requirement that the key must remain recoverable (for display and backup purposes) without server involvement.

What the Server Actually Stores

-- What's in the database for each license key:
encrypted_key:      "u2vMc7Xq8..."      -- AES-256-GCM ciphertext (base64)
iv:                 "A3mK9pX2..."      -- Random 12-byte GCM nonce (base64)
plaintext_key_hash: "h:7f8a2b..."      -- h: + base64(HMAC-SHA256(sha256_bytes))

-- What's NOT in the database:
plaintext_key:      never stored (sent to the server transiently during validate/activate calls, never persisted)
passphrase:         never leaves the browser
aes_key:            never leaves the browser

The HMAC Layer: Why SHA-256 Alone Isn't Enough

You might wonder why we don't just store the SHA-256 hash of the license key directly. The reason: SHA-256 is deterministic and public. An attacker who obtains your database could mount an offline dictionary attack — generate candidate keys in the PERMIT-XXXX format, hash each one, and compare against stored hashes. Given that our key space, while large, is enumerable, this is a real threat.

By applying HMAC-SHA256 with a server-side secret, we convert the stored value into something that cannot be verified without knowing the secret. Even if an attacker gets a complete database dump, they cannot verify whether a candidate key matches any stored value without also knowing the HMAC secret. The secret never leaves the server.

Key Display and Recovery

One practical consequence of client-side encryption: if a user loses their passphrase, the license key is unrecoverable from the server. We handle this by encouraging users to export keys at creation time and by providing a passphrase-derived backup mechanism. The tradeoff — slightly more friction during setup — is worth it for the security guarantees it provides.

The validation and activation flow, on the other hand, requires no passphrase at all. Your customers validate keys directly against the HMAC hash, with no encryption involved at that point. This keeps the public API simple and fast — a hash lookup with no auth handshake — while maintaining full zero-knowledge semantics for key storage.

Inspect It Yourself

Because the cryptographic operations run in your browser, you can verify them. Open DevTools on any PermitCore page, examine the permitCryptoInterop JavaScript object, and trace the createEncryptedLicense flow. The Web Crypto API calls are visible in the Sources panel. We deliberately chose browser-native cryptography over a third-party library so there's no additional trust surface to audit.

Zero-knowledge encryption isn't magic — it's a specific, verifiable set of engineering decisions that constrain what any single party can know. We think that's the right constraint to build a license management system on.

All posts Next: License Distribution Models