Client-side encryption, explained

Your prompts are encrypted in your browser before they reach our servers. We store ciphertext, and a breach of our database alone exposes nothing readable. This page explains exactly how that works, where the trust boundaries are—including the instant-unlock recovery key—and what it does and does not protect against.

The short version

Every prompt you store in Prompt Cellar is encrypted with AES-256-GCM using a random 96-bit IV. The encryption key lives in your browser's memory. It is derived from your passkey using the WebAuthn PRF extension and HKDF-SHA-256. The key is never transmitted to our servers.

Our database contains encrypted blobs. Without the key that decrypts them, those blobs are indistinguishable from random bytes, and that key is never written to the database—so a breach of the database by itself yields nothing readable.

There is one important caveat, and we want it stated up front rather than buried: instant unlock. Read on.

One important detail: instant unlock. Tapping a passkey every time you open the app is friction, so Prompt Cellar also stores an encrypted recovery copy of your master key on the server so you can unlock instantly and across devices. We want to be upfront about the trade-off: with this recovery copy, our servers are technically able to decrypt your prompts. We access your content only as needed to operate the Service. That recovery copy is itself encrypted with a key held in our application configuration—never written to the database—so a breach of the database alone still exposes nothing readable. The sections below describe the client-side encryption in detail; read them alongside this caveat.

Key derivation

How your encryption key is created in your browser.

Step 1: Passkey authentication with PRF

When you authenticate, your passkey provider executes the WebAuthn PRF (Pseudo-Random Function) extension. This produces a deterministic 32-byte secret derived from a salt unique to your vault. The PRF output never leaves the authenticator boundary in plaintext during the WebAuthn ceremony—it goes directly to the browser's WebCrypto context.

Step 2: HKDF-SHA-256 key derivation

The PRF output is fed into HKDF (HMAC-based Key Derivation Function) with SHA-256. The info string is weldedanvil-vault-wrap-v1. This produces a 256-bit wrapping key. The wrapping key is non-exportable—it exists only inside the WebCrypto context and cannot be extracted by JavaScript.

Step 3: Master key unwrap

Your vault has a randomly generated AES-256-GCM master key. This master key is wrapped (encrypted) with the wrapping key from Step 2 and stored on the server. At unlock time, the wrapped master key is downloaded and unwrapped locally. The plaintext master key exists only in your browser's memory.

What the server stores: the wrapped (encrypted) master key, a 96-bit wrap IV, and the PRF salt. None of these are useful to a database-only attacker without the passkey that produced the PRF output. The passkey-derived wrapping key never leaves your browser. If you use instant unlock, the server additionally stores an encrypted recovery copy of the master key, protected by a key held in our application configuration (not in the database).

Per-prompt encryption

Every prompt gets its own IV. Reusing an IV with AES-GCM would be catastrophic, so we don't.

AES-256-GCM with random 96-bit IV

Each prompt is encrypted individually using AES-256-GCM. A fresh 96-bit IV is generated via crypto.getRandomValues() for every encryption operation. The IV is stored alongside the ciphertext. GCM provides both confidentiality and integrity—any tampering with the ciphertext causes decryption to fail.

Encryption happens in the browser

The WebCrypto API does the actual encryption. The plaintext prompt is UTF-8 encoded and passed to crypto.subtle.encrypt(). The resulting ciphertext is base64url-encoded and sent to the server. The server never receives the plaintext prompt. (Your master key reaches the server only as the encrypted instant-unlock recovery copy described above, never in the clear.)

Base64url encoding, not hex

Ciphertext and IVs are encoded with base64url (RFC 4648) for compact storage. This is a wire format choice, not a security property. The ciphertext blob stored on our servers is the GCM ciphertext plus the 128-bit authentication tag appended by the WebCrypto API.

Encrypted search index

Search works without the server knowing what you searched for.

Index built client-side

When a prompt is captured, search tokens are extracted from the plaintext in your browser. These tokens (along with metadata like repo name, branch, and tool) are encrypted as a JSON blob using the same AES-256-GCM master key and a fresh IV. The server stores the encrypted token blob.

Query execution is local

When you search, the encrypted index entries are fetched in bulk and decrypted in your browser in parallel batches. The search query runs against the decrypted tokens in memory. The server sees that you requested the index. It does not see what you searched for or which results matched.

Tradeoff: index must fit in memory

Client-side search means the entire decrypted index lives in browser memory. This works well for thousands of prompts. It would not scale to millions. We chose this tradeoff deliberately: the alternative is server-side search, which requires the server to see your search tokens. We picked privacy over scale.

What the server stores, and what a breach exposes

A concrete breakdown. The right-hand column reflects an attacker who obtains the database but not our application's key material. With instant unlock enabled, the server itself holds a recovery key and is therefore able to decrypt your content; it does so only as needed to operate the Service.

What the server stores

  • That you have an account
  • How many encrypted prompts you have stored
  • When prompts were created (timestamps)
  • The size of each encrypted blob
  • Your wrapped (encrypted) master key
  • That you performed a search (not the query)
  • An encrypted recovery copy of your master key, if instant unlock is enabled

What a database-only breach cannot expose

  • × The content of any prompt
  • × Your search queries or results
  • × Your passkey-derived wrapping key, or a usable master key
  • × Which AI tool a prompt was sent to
  • × Repository names, branch names, or project paths
  • × Search tokens or index content

Threat model: what happens in a breach

Every encryption scheme has limits. Here is where ours are.

Scenario: full database dump

An attacker gets a complete copy of our database. They have every encrypted prompt, every wrapped master key, every IV and PRF salt.

Result: They have ciphertext. To decrypt any prompt, they need the corresponding master key. The wrapped master key requires your passkey-derived wrapping key; the instant-unlock recovery copy requires key material we keep outside the database. Neither of those is present in a database dump, so against the database alone the data is useless — every key it contains is itself encrypted with something the dump doesn't hold.

Scenario: compromised server code

An attacker modifies the server to log data or inject malicious responses.

Result: The server never receives plaintext prompts or your passkey-derived keys during normal operation, so passive logging reveals nothing. A fully compromised server is more serious, though: it holds both the database and the instant-unlock key material, so it can decrypt stored content directly. It also serves the JavaScript that runs in your browser, so it could serve modified JavaScript that exfiltrates your key or plaintext. This is the primary trust boundary: you trust that we operate our servers honestly and that the JavaScript we serve is the JavaScript we wrote.

Scenario: malicious browser extension

A browser extension with page access reads DOM content or intercepts WebCrypto calls.

Result: Game over. A browser extension with sufficient permissions can read decrypted prompts from the DOM, extract keys from memory, or intercept API calls. Client-side encryption does not protect against a compromised browser environment. This is true of every client-side-encryption web application.

Scenario: stolen passkey

An attacker obtains your passkey (e.g., compromised password manager, cloned hardware key).

Result: With the passkey and a database dump, the attacker can derive the wrapping key, unwrap the master key, and decrypt all prompts. Passkey security is the foundation. Use a hardware authenticator or a reputable password manager with PRF support.

How this compares to "encryption at rest"

Most SaaS products encrypt your data at rest. That protects against a very specific threat. It does not protect against the provider.

Encryption at rest (what most services do)

The provider encrypts your data when it is written to disk. The provider holds the encryption key. The data is decrypted when the provider's application reads it.

Protects against: someone stealing a hard drive from the data center. That's it. The provider, their employees, a subpoena, a compromised admin account—all of these can read your data because the provider holds the key.

Client-side encryption (what Prompt Cellar does)

You encrypt your data in your browser before it reaches the server, and we store ciphertext. The key that decrypts it is never written to our database.

Protects against: a breach of the database alone, which yields only ciphertext. The limit: to offer instant unlock, the server stores a recovery copy of your key, encrypted with key material we keep outside the database. So a party holding both the database and that key material—us, or someone who fully compromises our servers—can decrypt. We access your content only as needed to operate the Service.

Threat At Rest Prompt Cellar
Stolen hard drive
Database dump
Rogue employee
Subpoena for stored data
Compromised admin account
Malicious browser extension
Compromised JS delivery

What this does not protect against

No encryption scheme covers everything. Here is what falls outside our threat model.

×

Compromised browser environment

Malicious extensions, browser-level malware, or a compromised OS can read decrypted data from memory or the DOM. Client-side encryption operates above the browser trust boundary. If your browser is compromised, the encryption is irrelevant.

×

Malicious JavaScript served by us

If an attacker compromises our deployment pipeline and serves modified JavaScript, that code could exfiltrate your key before encryption happens. This is the fundamental limitation of web-based client-side-encryption systems. Native applications with pinned binaries do not have this problem. We mitigate this with integrity checks and deployment controls, but we cannot eliminate it.

×

Traffic analysis

An observer can see that you are communicating with Prompt Cellar, how often, and the approximate size of encrypted payloads. The content is hidden, but the pattern of usage is not. We do not pad ciphertext to a uniform size.

×

Lost passkey

If you lose access to all of your registered passkeys and have no backup, you lose access to your account, because passkeys are the only way to sign in to the Service. Register more than one passkey so a single lost or broken device does not lock you out. There is no password reset.

Implementation specifics

For the auditors and the curious.

Encryption algorithm AES-256-GCM
IV length 96 bits (12 bytes)
Auth tag length 128 bits (WebCrypto default)
IV generation crypto.getRandomValues()
Key derivation HKDF-SHA-256
KDF input WebAuthn PRF output
HKDF info string weldedanvil-vault-wrap-v1
Master key size 256 bits
PRF salt length 256 bits (32 bytes)
Ciphertext encoding Base64url (RFC 4648)
Crypto API Web Crypto (SubtleCrypto)

References and standards

Every primitive on this page is a published standard, not a proprietary scheme. Read the primary sources.

Your prompts. Your keys. Your data.

First 100 prompts are free. No credit card required.

Get Started