Security Architecture

How your API keys are encrypted, what our servers hold, what they can never read, and — just as important — the things this design does not protect you from.

What “zero-knowledge” means here

Your API key values are encrypted on your own device, with a key derived from a master password we never receive. What reaches our servers is ciphertext. We cannot decrypt it, we cannot reset your master password, and a subpoena or a breach of our database yields encrypted blobs rather than credentials.

That claim is narrow on purpose, so here is its exact shape:

  • It applies to key values — the secret string itself, and the contents of imported .env templates.
  • It does not apply to all metadata. The name you give a key, the service it belongs to and its timestamps are stored unencrypted so they can be indexed and listed. See what is visible below — we would rather you read it here than discover it later.
  • It is a statement about our servers, not about your machine. Once a key is decrypted on your device — into your clipboard, a .env file, or an editor buffer — it is plaintext under your operating system’s protection, not ours.

The cryptography, with real parameters

Both clients — the browser extension and the VS Code extension — use the same parameters and the same envelope format, so a vault written by one is readable by the other. All primitives come from the platform’s Web Crypto implementation; none of it is hand-rolled.

Encryption

AES-256-GCM

Authenticated encryption: 256-bit key, 12-byte IV, 128-bit authentication tag. Tampering with stored ciphertext makes decryption fail rather than return corrupted output.

Key derivation

PBKDF2-HMAC-SHA-256, 600,000 iterations

Your master password is stretched with a 32-byte random salt. 600,000 iterations matches the OWASP recommendation for PBKDF2-SHA-256. The derived key is marked non-extractable, so browser and Node code cannot read its bytes back out.

Per-item randomness

Fresh salt and IV every time

Every encryption generates a new 32-byte salt and a new 12-byte IV. Two keys with the same value produce unrelated ciphertext, and no IV is ever reused under a given derived key.

Stored envelope

salt ‖ IV ‖ ciphertext+tag

One base64 string of 32 bytes of salt, 12 bytes of IV, then the ciphertext with its 16-byte tag appended. That is the entire record — there is no separate key store to compromise.

A master password must be at least 12 characters. Its strength is the load-bearing part of this design; the iteration count only sets the cost of each guess.


What the server stores

Your encrypted vault backup lives in Google Cloud Firestore, under your user document. It is reachable only through our API: the Firestore security rules deny client access to these collections outright, so no browser SDK — ours or anyone else’s — can read them directly.

What the server holds and whether it can be read
Stored itemWhat it isReadable by us?
Encrypted key valuesAES-256-GCM ciphertext under your master passwordNo
Project metadata blobProject names, URLs, icons, colours and descriptions — encrypted client-side into one opaque string before uploadNo
Sharing keypair backupYour ECDH private key, encrypted with your master password, plus the matching public key (public by design)Private half: no
Shared-project keysKey values encrypted with a per-project data key (DEK)No
Wrapped DEKsOne copy of the project DEK per member, wrapped to that member’s public keyNo
Key and project identifiers, names, timestampsListing and sync metadataYes — see below
Account and billing recordsEmail, plan, subscription status, team membershipYes

Your master password, the key derived from it, and your recovery key are never sent anywhere. The password lives in the browser’s session storage — memory only, cleared when the browser closes — or, in VS Code, in a process buffer that is zeroed on lock. The local password check is a PBKDF2 verifier stored on your own device and never uploaded.


The metadata that is visible to us

Most security pages stop at “we can’t read your secrets”. Here is the rest of it. When your vault is backed up, each key record carries these fields unencrypted:

  • The key’s random identifier, and the identifier of the project it belongs to.
  • The name you gave the key (for example “Stripe production”) — so avoid putting anything confidential in a key name.
  • The service it is for and its category (for example openai, “AI & ML”), plus that service’s sensitivity label.
  • Creation time, last-used time, and whether it is marked as the project default.
  • How many keys you have, and when the vault was last synced.

For shared projects, the same key metadata is visible, along with which account added each key and who the project members are. Project names are the exception: they travel inside the encrypted blob, so a project reads as an opaque identifier to us.

What this means in practice. Someone with our database could tell that you hold a production Stripe key and when you last used it. They could not tell you what the key is. If that inference alone is unacceptable in your threat model, name your keys generically — the name is never used for anything but display.

In the browser extension, encrypted keys and this metadata also sync through your browser’s own account sync so they reach your other profiles. That copy is subject to your browser vendor’s protections, not ours.


How a key gets from the browser to VS Code

  1. You add a key in the browser extension.The value is encrypted in the page with your master password before it is written anywhere. Storage never receives the plaintext.
  2. The vault backs itself up.The already-encrypted records are uploaded to your account. Project names are encrypted separately into a single blob first. Nothing is decrypted for transport.
  3. VS Code authorises a device.Sign-in happens in your browser and returns tokens to a local loopback server bound to127.0.0.1. Tokens are stored in VS Code’s SecretStorage, which uses the operating system keychain.
  4. VS Code downloads the same ciphertext.It fetches the encrypted backup with its bearer token. The server does not decrypt anything; it hands back the blobs it was given.
  5. You unlock locally.VS Code prompts for your master password, derives the key with the same parameters, and decrypts in-process. The password is not stored, not written to settings, and not sent to any server.
  6. You use the key.Insert it at the cursor, copy it (the clipboard is cleared after 45 seconds), or generate a .env file. From this point the value is plaintext on your machine.

At no point in that sequence does a plaintext key value, or anything that would let us derive one, cross the network.


Account security

Encryption protects the key values. These controls protect the account that holds the encrypted backup — a different job, and worth being precise about.

Two-factor authentication

Optional TOTP (the six-digit-code kind, from any authenticator app). Enrolment only completes after a code from your app verifies, so a misconfigured authenticator cannot lock you out, and the QR code is generated inside your browser rather than fetched from a third-party image service. The stored secret is itself encrypted at rest, and you get ten single-use recovery codes which we keep only as hashes.

Where it is enforced: the admin panel, and authorising a device your account has never used before. Routine token refresh on a device you already approved does not prompt. If you have not enabled it, nothing changes for you.

Devices and sessions

  • Every client token is bound to a device identifier and to a session record you can revoke. A token with no device binding is refused.
  • Access tokens last 24 hours; the device session is a sliding 30-day window that extends on each refresh, so an idle device eventually has to sign in again.
  • Revoking a device invalidates its token on the next request. Verdicts are cached briefly for performance, so revocation can take up to a minute to take effect on an already-warm server instance — we would rather state that than imply it is instant.

Transport

The API is served over HTTPS. The browser extension is additionally pinned by its own manifest: its content security policy permits network connections only to this origin, so a compromised extension page cannot exfiltrate to an arbitrary host. Tokens are sent in request headers. The one exception is the VS Code sign-in handoff, where tokens arrive as parameters on a 127.0.0.1 URL — that hop never leaves your machine, but it does mean the tokens pass through your browser’s history.


Team sharing

Sharing cannot use your master password — your teammates do not have it. Instead each shared project gets its own data encryption key, and that key is delivered to each member under their own public key. The server relays wrapped material and never holds an unwrapped project key.

Step 1

Per-project data key

A fresh 256-bit AES-GCM key is generated on the owner’s device, and the project’s key values are encrypted with it.

Step 2

ECDH P-256 + HKDF-SHA-256

For each member, an ephemeral keypair and that member’s public key produce a shared secret, which HKDF turns into a one-off wrapping key with its own 32-byte salt.

Step 3

AES-KW (RFC 3394)

The data key is wrapped with that key. The server receives only the wrapped bytes, the ephemeral public key and the salt — enough for the recipient to unwrap, and nothing more.

Step 4

Rotation on removal

Removing a member generates a new data key and re-encrypts the project for everyone who remains. Read the caveat below — this is the part most often overstated.

What rotation can and cannot undo. Removing someone blocks their access immediately and rotation stops them decrypting anything future. It cannot retract what they already decrypted or copied while they were a member — no cryptography can. If rotation itself fails (it is retried, and the app tells you plainly if it could not finish), the removed member could still read the old ciphertext they had until you retry it. Treat removal as a reason to rotate the underlying credentials at the provider, exactly as you would after any offboarding.


What the browser extension can reach

An extension that manages credentials should ask for as little as it can, and you should be able to check that from the store listing. Ours is Manifest V3 and requests storage, windows, sidePanel, activeTab and scripting, with host access to this site only.

  • No content scripts. The manifest declares none, so the extension is not running on the pages you browse. There is no background code watching your tabs.
  • activeTab + scripting instead of broad host access. Code runs in a page only when you click a button in the extension, only in that one tab, and only in its main frame — never in cross-origin iframes.
  • It reads site details, not credentials. That injection scans a page for things like a company name or contact details, or fills those back in. The fill policy explicitly refuses any field that looks like a password, secret, token, API key, one-time code, payment detail or government identifier.
  • API keys are never typed into a web page for you. They reach you by clipboard or by generated file, both of which you trigger.
  • Network egress is restricted by the manifest to this origin, and only this site may message the extension.

The VS Code extension

  • Decryption is local. The extension downloads ciphertext and decrypts in-process with the same parameters as the browser. It never asks a server to decrypt.
  • The master password is prompted, never stored. It is not written to SecretStorage, to workspace or global state, or to a settings file, and it is not sent over the network. It is held in a buffer that is zeroed when the vault locks.
  • Tokens live in SecretStorage, VS Code’s OS-keychain-backed store — not in settings.json, where they would be committable.
  • Auto-lock defaults to 15 minutes of no vault activity. Locking zeroes the password buffer and drops every cached derived key and decrypted project name. Ciphertext is kept; it is not secret.
  • Key values stay out of the interface. The tree shows names and services only, and the extension writes no key value to its log. Values appear only where you put them: the editor, the clipboard, or a generated file.

Generated .env files and the clipboard

These are the two places where a key deliberately becomes plaintext, so they deserve to be spelled out rather than buried.

Clipboard

Copying a key from the vault schedules the clipboard to be wiped 45 seconds later, in both the browser and VS Code, and the countdown is shown when you copy. The wipe is skipped if you have since copied something else, so it never destroys work. While it sits there, any application on your machine can read it — that is how clipboards work on every operating system.

Generated .env files

A .env file is plaintext by definition; that is the point of generating one. Once written it is an ordinary file with your project’s default permissions, outside anything we control, and it is not added to your .gitignore automatically. The file carries a “do not commit” header, but the responsibility for keeping it out of source control, backups and container images is yours. Prefer inserting a single key where you need it over generating a whole file you then have to remember to delete.


If you forget your master password

We cannot reset it. There is no support path, no override, and no administrator anywhere who can decrypt your vault — that is the direct consequence of never having your password in the first place. Any product that can reset a master password can also read your data.

The way back in is a recovery key, generated when you set up your vault: a 25-character code with 125 bits of entropy, written in an alphabet that avoids confusable characters so it survives being copied by hand. It encrypts your master password into a blob that is useless to anyone without the key itself.

Two things to know about it. The recovery blob is stored on the device where you created it, so a recovery key alone will not rescue a vault on a machine you no longer have — keep a copy of the key somewhere durable and separate, such as a password manager or paper in a safe. And losing both the master password and the recovery key means the data is unrecoverable, by us or by anyone.


What this does not protect against

Every one of the following is a real limit of this design. If a security page does not have a section like this, it is describing an aspiration rather than a system.

A weak master password

600,000 PBKDF2 iterations set the price of one guess; they do not set the number of guesses required. Anyone who obtained your encrypted backup could attack it offline, at their own pace, with no rate limit we can impose. Against a short or reused password that is a matter of time. Use a long, unique passphrase — this single choice does more for your security than anything else on this page.

A compromised device

Malware, an attacker at your unlocked machine, or a hostile process able to read another process’s memory can capture your master password as you type it, or read decrypted values while the vault is unlocked. Client-side encryption assumes the client is trustworthy. If it is not, nothing here holds.

Other software on your machine

A malicious browser extension or a malicious VS Code extension runs with its own permissions alongside ours. Anything on the clipboard is readable by every application. A generated .env is readable by anything that can read your project directory. We cannot sandbox the rest of your system.

A key you have already used somewhere else

Once you paste a key into a terminal, a CI dashboard, a chat message or a screenshot, it lives there too. We protect the copy in your vault; we cannot protect the copies you make.

What your metadata reveals

As set out above, key names, services, counts and timestamps are stored unencrypted. That is enough to profile which providers you depend on. Only the values themselves are protected.

Someone who steals an active session

A valid token downloads your encrypted backup — that is what it is for. It does not decrypt it: without your master password the attacker holds the same ciphertext we do. Two-factor authentication is what stops a stolen website password from turning into a new authorised device, which is why we recommend turning it on.

A team member who has already seen a secret

Removal and key rotation stop future access. They cannot un-see a value someone already decrypted. Rotate the credential at the provider when someone leaves.

Us, if we shipped malicious client code

The encryption runs in code we publish. A future release could, in principle, be built to exfiltrate your password — that is true of every client-side-encrypted product, including the ones that do not mention it. What limits it is that the clients are inspectable and the extension’s manifest restricts where it may send data at all.


Reporting a vulnerability

If you find something that contradicts this page, we want to hear it before anyone else does. Write to gauthier.bros@gmail.com with enough detail to reproduce it. Please do not include real API keys or customer data in a report — use test credentials and redacted values.

This page describes the system as implemented in the current clients and backend. It is maintained alongside the code rather than written once for marketing, and it is meant to be checkable: the cryptographic parameters, the permission model and the stored fields described here are all observable in the published extensions and in network traffic from your own machine.