7KEY Privacy

Threat model

What 7key protects, against whom, and where the protection ends. The mechanisms behind each claim are in Cryptography and Teams and CI secrets; the tests that enforce them are listed at the end.

Assets

  1. Vault contents: site names, URLs, usernames, passwords, 2FA secrets, notes, cards and addresses.
  2. The master password, which opens the vault on any device.
  3. The recovery code, which can replace the master password.
  4. Team secrets: team vault items and project secrets shared inside an organization, and the repository keys that let CI read them.
  5. Shared items, while a link is alive.
  6. Metadata: who has an account, how many items, when they sign in, who belongs to which organization.

Parties

PartyAssumed capability
Network attackerReads and alters traffic outside TLS
Server operatorReads D1 and Durable Object storage, changes Worker code and secrets
Database thiefHolds a copy of D1 and of the Durable Objects' storage, nothing else
Malicious websiteRuns script in a page the user visits
TeammateA member of the same organization or team
Link carrierThe chat or email service that transports a share link
Breach-check serviceHave I Been Pwned, asked for password hash prefixes
Icon serviceDuckDuckGo or Google, asked by the Worker for logos
Device thiefHolds a powered-off or locked device
Malware on the deviceRuns code as the user while the vault is unlocked

What each party can learn

PartyVault contentsMaster passwordMetadata
Network attackerNo: TLS, and the payload is ciphertext anywayNoTiming and size of requests
Database thiefNoNo offline guessing without AT_REST_SECRETEmails, item counts, timestamps, organization membership, audit log with IPs
Server operatorNo, as long as the extension is honest (see below)Can guess offline at the account's PBKDF2 cost, at least 600,000 iterationsEverything the database holds, plus what Cloudflare records about each request
Malicious websiteA login the user fills there, only on its own domain; a card or address the user picks from the menu on that pageNoWhether autofill is on
TeammateEverything in the teams they share, whose key every member holdsNoMember list and roles; for admins, the activity log
Link carrierThe shared item, unless a passphrase was set and sent separatelyNoThat something was shared
Breach-check serviceNo: it receives 5-character SHA-1 prefixes, each shared by hundreds of passwords, with padded responsesNoThe user's IP address and that a check ran
Icon serviceNoNoDomain names, from the Worker's address, not the user's
Device thiefWith auto-lock on "Never", yes while the session is valid; otherwise no, the local copy is ciphertextCan guess offline against the cached wrapped keyEmail, item count
Malware on deviceYes, once unlockedYes, if it logs keystrokesYes

What the server learns

The server, and so anyone who operates or breaches it, learns:

Cloudflare sees the IP address of every request, and the Worker's logs, which wrangler.toml turns on, are kept by Cloudflare.

With website icons on (the default), the server also sees the domain of each saved login while it fetches that site's logo. The Worker keeps each logo in Cloudflare's edge cache for up to 7 days, keyed by domain only, not by account; the extension keeps fetched logos in memory only, never on disk. With the option off, the server never learns site names. It never learns URLs, usernames, passwords, 2FA secrets, notes, card numbers, or the contents of a share.

Exported files

An encrypted export is as strong as its passphrase: PBKDF2-SHA-256 with 600,000 iterations and AES-256-GCM, with a fresh salt per file. Anyone holding the file can guess the passphrase offline at that cost, so 7key requires at least 10 characters. A CSV export is plain text and offers no protection once copied.

Signing in

Key derivation. The master password goes through PBKDF2-SHA-256 on the device. 600,000 iterations is the floor: the server refuses fewer when an account is created and when its password is changed or recovered. An older account with fewer iterations still signs in, and the client then re-wraps the vault key at 600,000 iterations without asking.

Master password policy. A new master password must be 12 to 256 characters and must not equal the email address or its local part. On account creation, on a password change and on recovery, the client also looks it up in Have I Been Pwned with the same k-anonymity range query as the health check, and refuses a password that appears in a breach. If the service cannot be reached, that check is skipped rather than blocking the user. The server never sees the password, so it cannot enforce any of this; a modified client can set a weak one.

Guessing. Failed attempts are counted per address and network, the email and the IP together. The first five failures cost nothing; each one after that makes that pair wait twice as long as the last, from 5 seconds up to one hour. Separately, after 20 failures within an hour against one address from anywhere, every attempt on that address is slowed by 250 ms, doubling every five failures up to 5 seconds. There is never a hard lock, so an attacker guessing from one network cannot lock the owner out from another. Recovery codes and two-factor codes are throttled the same way with their own counters; the code asked for at sign-in shares the password's counters. Sign-in, sign-up, recovery and share endpoints are also limited to 30 requests per minute per IP, and sign-up requests to 10.

Which addresses have accounts. prelogin returns stable, plausible KDF parameters for unknown addresses. Login runs the same keyed comparison whether or not the address exists. On a server without email, signing up with a taken address returns a decoy 201 with a token that is not stored anywhere, and changes nothing. On a server with email, sign-up needs a code sent to the address, and an address that already has an account receives a "you already have an account" email instead of a code, so only someone who reads that inbox learns anything. Endpoints that must give the same answer either way (sign-up codes and share access codes) look up the address and send the email after responding, in waitUntil, so their timing does not depend on whether the address exists.

Bots. When Turnstile is configured, account creation, sign-up codes, the first two recovery steps and share access codes need a valid token, checked against the server's own hostnames. Sign-in, share reads and opening a share with a code are not behind it. Without Turnstile configured, nothing changes.

Sessions. Session tokens are 32 random bytes sent as a bearer header, never a cookie; the server stores their SHA-256. A session ends after 14 days without use or 30 days after it was created, whichever comes first, and an account keeps at most 20. Settings lists the active sessions with their device label and last use, and revokes any one of them or all the others. Signing in with a device label the account has not used before sends an email, when the server can send email. Changing the password signs out every session, or every other session when the client asks to keep its own.

Account two-factor authentication

An account can require a TOTP code (SHA-1, 6 digits, 30 seconds, one step of drift either way) at sign-in. Turning it on needs the master password and a working code, and signs out every other session. Ten single-use backup codes come with it.

Limits: there is no "remember this device", so every sign-in asks. 2FA guards sign-in only. PIN and biometric unlock reopen an existing session and never ask for a code. Changing the master password, deleting the account and creating a recovery code need the master password, not a code. Using a recovery code turns 2FA off. 2FA does nothing against a server operator or a database thief: it is not part of any key.

Forgotten master password

Nobody holds a copy of the master password, and the server cannot reset it. The only way back in is a recovery code created beforehand in Settings. Without one, a forgotten master password means the vault is lost.

A recovery code is 160 random bits. The device derives two keys from it with HKDF: one seals the vault key, the other proves to the server that the user holds the code. The server stores the sealed vault key, sealed again under a key derived from AT_REST_SECRET and bound to the account, and an HMAC of the proof. Neither opens the vault, and 160 bits cannot be guessed offline, even by someone who also holds the Worker's secrets.

Resetting needs the code and, when the server can send email, a 6-digit code sent to the account's address, so a stolen recovery code alone is not enough there. A server without email accepts the recovery code alone. A reset sets a new master password and a new recovery code in one step, so the code used stops working; it signs out every session, turns two-factor authentication off and deletes the backup codes, and emails the owner that the account was recovered and, separately, that 2FA was turned off. Recovery attempts are throttled like sign-in and answer an unknown address the same as a wrong code.

A recovery code is a second way in. Whoever holds it, and can read the account's email where the server sends email, owns the vault.

Integrity of stored items

Each personal item is sealed with AES-256-GCM under the vault key, with personal-vault/item/<id> as associated data, so the server cannot move one item's ciphertext into another item's row. The account id is not part of an item's associated data. It binds the server-side layer on the wrapped vault key (personal-vault/wrapped-vault-key/<account>), so wrapped keys cannot be moved between accounts; items of different accounts are under different vault keys in any case.

Items written by early versions carried no associated data. The extension still opens those until it has rewritten them in the bound form; it then records, per server and account, that this device has migrated, and from then on refuses unbound ciphertext, also after signing out and back in. A device that has not yet migrated this account, such as a new one or one whose storage was cleared, accepts unbound items until its first migration, which runs on the first sync after unlock. During that window a malicious server could serve an old unbound ciphertext under another item's id, as long as the item inside names no id or the same id.

Share links

A share link's fragment is its key; the server stores ciphertext only, and the recipient page decrypts locally. Secret fields, the password and hidden custom fields, are shown masked. Pressing reveal places the value in the page, and it is masked again after 30 seconds or as soon as the tab is hidden. Copy works without revealing. The username, website, visible custom fields and, when the sender included it, the note are shown in clear. The note is left out unless the sender ticks it.

The Worker serves the page with its own Content Security Policy (default-src 'none', scripts only from the page's origin and Turnstile, frame-ancestors 'none'), X-Frame-Options: DENY, no-referrer and HSTS. See Share links for lifetimes, view limits and recipient restrictions.

Malicious websites and autofill

Autofill is off until the user grants the extension access to sites. The page script is treated as untrusted; the service worker decides, using the sender details Chrome reports.

Organizations and teams

Each account has an ECDH P-256 key pair whose private key is sealed under the vault key. A team key is wrapped for each member's public key; project keys and team vault items are sealed under the team key, with associated data naming the team, the key generation and the object.

Audit log

Each organization's Durable Object appends an event for every recorded action (who, what, which object, when, and the client's IP) and chains each record to the previous one with SHA-256. Deleting or editing a record breaks every later hash, and the log view reports whether a page verifies.

The chain is not keyed and is stored by the operator. It reveals accidents and an outsider who can change rows but not recompute hashes. An operator, or anyone who controls the Worker, can rewrite the log and recompute the chain. Some events, such as revealing or copying a secret in the console, are reported by the client: they record what an honest client did, not what a member could do with the key they hold.

CI and the GitHub Action

A linked repository has its own key pair. The environment key is wrapped for the repository's public key; the private key (SEVENKEY_KEY) is shown once and kept as a GitHub secret. To read secrets, the CLI or the Action requests a GitHub OIDC token whose audience is the 7key server. The server checks its signature against GitHub's published keys, its issuer, audience and expiry, then the link's rule: the exact repository and the exact branch or GitHub environment, never pull_request_target. Only then does it return the wrapped key and the sealed secrets, and it records ci.read with the repository, ref and IP.

The server learns which repository read which environment, from where and when; it never sees the values. Neither half is enough alone: the repository key without a matching OIDC token gets nothing from the server, and the server's response is useless without the repository key. In the job, the Action masks secret values in the log and writes them to GITHUB_ENV, so every later step of that job can read them. A compromised action in the job is a compromised secret.

The CLI

The 7key CLI signs in with the master password, and a 2FA code when it is on, and keeps the session token and the account's private key, which opens every team key the account holds. It stores them in the macOS Keychain or, on Linux, the Secret Service keyring. Without a system keyring it refuses to keep them, unless the user passes --insecure-file-store or sets SEVENKEY_INSECURE_FILE_STORE=1 or SEVENKEY_KEYRING=file; the file is then written with mode 600 and a warning. Anyone who can read that keyring entry or file as the user can use the session and the team keys until the session ends.

7key agent

7key agent -- <command> runs a command, typically an AI coding agent, with secrets it can use but not read.

Limits: any process that inherits the child's environment can use the proxy, and so can send authenticated requests to the allowed hosts while the command runs. The scrubber covers the encodings above, not every transformation; an allowed host that returns a secret in another form, reversed or encrypted for example, defeats it. Allow only the real API hosts for each secret.

Trash

An item moved to the trash keeps the trash date inside its encrypted record, so the server cannot tell a trashed item from a live one. Thirty days later the extension deletes trashed personal items on its next sync while unlocked; until a device does that, the ciphertext stays on the server. Team vault items are not purged automatically.

Transport and headers

API responses and the share page carry Strict-Transport-Security with a two-year max-age and includeSubDomains, X-Content-Type-Options: nosniff and Referrer-Policy: no-referrer. The API answers cross-origin requests only from extension origins. The web dashboard at /app has a Content Security Policy that allows connections only to its own origin and the breach-check service. /.well-known/security.txt publishes the security contact (security@7key.online unless the operator sets another) and links to SECURITY.md. Public pages are also served as Markdown to clients that ask for text/markdown; the HTML responses keep their security headers, and the Markdown response is plain text with nosniff.

Out of reach, by design

A forgotten master password without a recovery code. Nothing can unwrap the vault key. A recovery code is the one deliberate second way in, open to whoever holds it.

Malware on an unlocked device. Code running as the user can read the extension's memory, the screen and the keyboard. No client-side design survives a compromised endpoint. The same holds for the CLI's keyring entry and for any process running under 7key agent.

The link carrier. A share link's fragment is its key. The service that carries the link can read it like any other text. A passphrase delivered over a different channel closes this, at the cost of a second message.

A malicious server and an honest-looking extension update. The extension's code decides what it sends. An operator who also ships the extension can ship one that sends keys home. Self-hosting from source you have read, or building the extension yourself, is the defence. A Chrome Web Store build is only as trustworthy as its publisher.

A malicious server and the web dashboard. The dashboard at /app is code the server sends on every visit. An operator, or anyone who takes over the server, can send a version that leaks the master password or keys of whoever signs in next. Only organization admins need the dashboard; the extension covers the same tasks without this exposure.

A malicious server and public keys. The server can substitute a public key when someone is added to a team. Only comparing fingerprints out of band catches it.

Teammates. Everyone in a team can read everything in it. Roles limit writes, not reads.

Server-side rollback. A server can return an older copy of an item, or withhold a newer one; the extension cannot tell. Integrity of each item is protected, freshness of the whole vault is not.

Metadata. The list above is accepted for a password manager. If hiding who uses the server, or when, matters to you, this design is not enough.

Known gaps

Specific defences

ThreatDefence
Login request reveals the vault keyAuth key and wrapping key are separate HKDF outputs
Leaked database used to guess passwordsAuth hash and wrapped key keyed by AT_REST_SECRET, which is not in the database
Weak key derivationAt least 600,000 PBKDF2 iterations; older accounts are re-wrapped at their next sign-in
Weak or breached master password12 to 256 characters, not the email address, refused when Have I Been Pwned lists it (skipped when unreachable); enforced by the client
Online password guessingPer address and network: five free failures, then waits doubling from 5 seconds to one hour; per address: a slowdown of up to 5 seconds after 20 failures in an hour; never a hard lock; 30 requests per minute per IP
Stolen master passwordOptional TOTP with replay protection and single-use backup codes; an email on sign-in from a new device label
Finding out which emails have accountsStable decoy prelogin parameters; the same comparison at login for unknown addresses; a decoy 201 for a duplicate sign-up, or a different email when the server sends email; same-answer lookups run after the response
Bots on sign-up, recovery and share codesTurnstile, when configured
Strangers using a personal serverAccount creation needs REGISTRATION_TOKEN unless open registration is chosen or the address holds an invitation
Flooding a shared serverPer-network daily account limit, per-account item and write limits
Server swaps ciphertext between itemsItem id is AES-GCM associated data; a device that has migrated refuses unbound legacy items
Server swaps wrapped keys between accountsAccount id is associated data of the server-side layer on the wrapped vault key and on the recovery copy
Stolen session token replaces the passwordChanging the password, deleting the account, and managing 2FA or the recovery code need the current auth key
Stolen session token survives a password changeChanging the password revokes the other sessions; sessions also end after 14 idle days or 30 days, and can be revoked from Settings
Leaked database yields usable tokensOnly SHA-256 hashes of session, revoke and 2FA challenge tokens are stored
Stolen recovery codeResetting also needs a code emailed to the account when the server sends email; attempts are throttled; the owner is emailed
Two people opening a single-view link at onceThe view is claimed in one conditional UPDATE … RETURNING
Probing a share's recipient listRequesting a code returns the same answer for every address, and the lookup runs after the response
A page impersonating another site to autofillThe service worker uses the URL Chrome reports for the sender's top frame
A page clicking autofill on the user's behalfEvery action requires a trusted user event; the UI is in a closed shadow root
A frame posing as a payment fieldCard data goes only to frames on the payment-provider allowlist, on an https page, addressed by document id
Phishing on a lookalike domainLogins never fill there; a local warning names the saved domain it resembles
A page using another site's passkeyThe RP ID is checked against the top-frame origin Chrome reports, no broader than its registrable domain; clientDataJSON carries the real origin
Copied password left on the clipboardCleared after 30 seconds by the service worker, even after the popup closes
Vault key written to diskOnly sealed under a key the server holds per session, and only with auto-lock on "Never"; deleted on lock, sign-out or a revoked session
PIN guessed on a stolen laptopThe sealed vault key needs both the PIN and a key the server releases only for the right PIN; five wrong tries wipe it, and the session ending, sign-out or a password change ends it too
Biometric unlock replayed or forgedThe server releases the key only for a signature over a fresh one-time challenge from the key pair the platform authenticator created, with user verification; the sealed copy also needs a secret that stays on the device
A copied session token while PIN or biometric unlock is onThe token stays on disk while the vault is locked, so a copy of the browser profile can call the API until the session ends: list, delete or roll back items to older ciphertext from the same disk. It cannot read anything or unlock the vault, and signing out or changing the password ends it
Script injection or framing on the share pageStrict CSP set by the Worker, frame-ancestors 'none'; a revealed secret is masked again after 30 seconds or when the tab is hidden
Removed team member reads new secretsThe team is flagged and writes are refused until an admin re-keys it
CI secrets read by another repository or a forkOIDC token checked for signature, issuer, audience and expiry; exact repository and branch or environment; pull_request_target refused; the values still need the repository key
CLI session left in a readable fileOS keyring only, unless the user opts in to a mode-600 file
An AI agent reading the secrets it uses7key agent gives it placeholders, swaps them only in headers to allowed hosts, and scrubs encoded copies from responses
Supply-chain attack through dependenciesNo dependencies in src/core; QR codes read with the browser's built-in BarcodeDetector

Verifying the claims

The strongest statements above are enforced by tests:

npm test

The project has not had an independent security audit.