Security & privacy
The page where we tell you what doesn’t work.
Every security product has a boundary. Most of them make you find it yourself. This page is the boundary: what CircleKey protects, what it deliberately does not, what our servers can see, and what we are not yet in a position to claim.
How the protection works
A group vault has a version counter, a current key, a member list and a governance policy. Every membership change — a join, a removal, a promotion, a policy edit, a device being linked — produces a new version with a new key, generated on the device that made the change and sealed individually to every current member’s every device.
Those changes form a chain: each one names the cryptographic hash of the one before it, and carries a signature. Before a client accepts a change it checks, on its own, that the version is exactly one greater than the one it already has, that the hash link matches, that the signer held the authority to make this particular change before the change took effect, that the signature verifies, and that the governance policy still holds afterwards. Anything that fails is rejected and surfaced as a typed error — never silently dropped.
That entire structure — the change, its member list, its policy, its signature — is then sealed: encrypted to the vault key of the version it creates, padded to a fixed size, and only then handed to the backend. So the checks above run on something the backend never got to read. A client fetches the sealed blobs, unwraps its own key envelope, works backwards to recover the earlier keys it is entitled to, and only then replays the chain forwards and applies the checklist.
Being able to open a change proves the author was a member. It does not make the change legitimate, and the implementation treats those as separate questions on purpose: the full checklist decides, every time, after decryption succeeds. Anything else would let any member forge any change.
This is why the backend’s honesty is not load-bearing. It relays. It cannot manufacture a membership change without a member’s private signing key, cannot rewrite history without breaking the hash chain, and cannot roll a vault backwards without every client noticing simultaneously — and now it cannot read what it is relaying either.
Two further properties fall out of the design rather than from configuration. Device management is self-scoped: a signer may only change their own device list, so a manager cannot quietly attach a device to somebody else’s account. And recovery is mandatory: a device cannot finish joining any vault until a recovery credential — 128 bits from a cryptographically secure generator, shown to the user once, never transmitted — has been created and confirmed.
What our servers see
Zero-knowledge here is scoped precisely: it covers content, key material and group composition — but not the existence of a vault or the rhythm of its use. Anyone telling you their server “knows nothing” is describing a system that could not route a message. Here is the real split.
◐ Visible in plaintext
- The vault identifier and its version counter
- The vault’s capacity profile — an upper bound on size, never the membership
- That a change occurred, and when
- Derived record identifiers, and how many records exist
- Each record’s size bucket
- Recovery-backup salt and key-derivation parameters, under a blinded handle
- Request metadata any host sees: IP address, user agent, timing
● Never transmitted at all
- Plaintext of any record
- The member list — user identifiers, device public keys, manager flags
- The kind of any change — join, removal, promotion, policy edit
- Whether a removal occurred at all
- The governance policy
- Which device signed a change, and the signature itself
- Any vault key, per-record key, device private key or recovery credential
- The contents of any sealed envelope, transition body or backup blob
Three details make that hold up rather than merely sound good. Key envelopes carry no recipient address and every vault ships a fixed number of them, padded out with decoys that are byte-identical to the real ones — so counting envelopes does not count members. Every sealed body is padded to one fixed size, so a bulky change looks like a trivial one. And a removal notice rides on every single change, always the same length, whether or not anyone was removed — which is what stops its presence from being the signal.
Identifiers never carry your words. Both kinds are produced on the device and neither is something your application chooses: vault identifiers are generated at random when the vault is created, and record names are hashed into opaque identifiers before they are sent. An application keeps its own labels — “the Acme matter”,
q3-layoffs.docx — in its own storage, and we receive neither.
This is deliberate: an identifier the caller could name is an identifier the caller
would eventually name badly, and it appears in every single request.
The practical consequence: an attacker who fully compromises our infrastructure learns that a vault exists, roughly how large it may be, how often it changes and roughly how much it holds. They do not learn who is in it, what happened to it, or anything about the data — and they cannot add a reader. The one thing they can do is stop serving you.
What it protects against — and what it doesn’t
Protects against
- A curious or malicious administrator at the hosting provider
- A database leak or a stolen storage bucket
- Cloud-provider compromise
- Network interception in transit
- A compromised backend attempting to forge a membership change
- A compromised backend attempting to roll back or fork vault history
- A legal demand served on us for content we do not hold
- A hosting provider profiling who works with whom, or when a team changed shape
Does not protect against
- Malware on a user’s device, or a compromised browser
- Cross-site scripting in your application
- A user screenshotting, copying or forwarding what they can legitimately read
- A removed member keeping data they downloaded before removal
- A future adversary with a cryptographically relevant quantum computer
- A backend refusing to serve you — availability is not a cryptographic property
- Traffic analysis: we still see your IP, your timing, and which vault you touched
What we are not claiming
These are the six things a careful reader would eventually work out. We would rather you read them here than discover them later.
- Not independently audited.
- No third party has reviewed the library or the hosted service. It is complete and tested against the specification, but that is our assessment of our own work. When an audit is done we will publish it in full, findings included, and change this line.
- Not forward secrecy.
- Re-keying protects the future, not the past. Because each vault version seals the previous key so that new members can read history, anyone holding one version’s key can walk backwards and recover the earlier ones. That is a deliberate trade for a shared workspace — a colleague who joins in month nine is supposed to read month one — and it is written down in the specification rather than glossed over. If you need past traffic to become unreadable to a future key holder, this is not the right tool.
- Removal is immediate, but not retroactive.
- A removed member is locked out of everything written after their removal. Data they had already downloaded is on their device, and no protocol running on your servers can reach into a machine you do not control and delete it. Anyone claiming otherwise is selling you a feeling.
- Not post-quantum.
- Key agreement uses X25519, which a sufficiently capable quantum adversary would break. A hybrid post-quantum suite is a candidate for a future version of the protocol, not a shipped feature. If your threat model includes store-now-decrypt-later against a state-level adversary, factor that in.
- Not anonymity, and not traffic-analysis resistance.
- We removed the metadata we could remove by cryptography. What is left is the metadata inherent in being a network service: your IP address, the times you connect, which vault identifier you asked for, and how often it changes. Someone watching our logs could still infer that a particular office is unusually busy on a particular matter. Padding and unaddressed envelopes do not fix that, and we are not going to imply they do — mitigating it is a deployment question (a VPN, batching, your own backend), not a property of the protocol.
- Who may create a vault is your application’s decision.
- Cryptography guarantees that nothing changes undetectably after a vault exists. It cannot tell you whether the account that created it was really who they said they were — that is anchored in your own authentication layer, and it is the one link in the chain we cannot supply.
The cryptography
Standard primitives, no invention. The suite is identified by a version string carried inside every envelope, record and backup blob, and anything carrying an unrecognised identifier is rejected rather than guessed at.
Key agreement
X25519, per device rather than per user.
Signatures
Ed25519, over a canonical encoding so that a signature means exactly one thing.
Key derivation
HKDF-SHA256, with domain-separated labels drawn from a closed set — never free-form strings.
Encryption
AES-256-GCM with 96-bit nonces from a CSPRNG, fresh per operation, with per-key usage counters that refuse to encrypt past a safe bound.
Recovery credentials
Argon2id, with PBKDF2-HMAC-SHA256 as a fallback. Parameters travel with each backup, so raising them later never strands an old one.
Implementation
The platform’s own WebCrypto where it provides the primitive, and established open-source implementations otherwise. Nothing hand-rolled, and only two runtime dependencies, both confined to the cryptography adapter.
Reporting a vulnerability
Mail security@circlekey.io. Tell us what you found and how to reproduce it. We will acknowledge within three business days and keep you updated until it is closed.
Please do not test against other people’s data or against the production service in a way that degrades it for other customers — the library runs entirely offline against an in-memory mock backend, which is a far better place to break things. We will not pursue legal action over good-faith research conducted that way, and we will credit you unless you would rather we didn’t.
Disagreements about the design itself are welcome too. The specification is public precisely so that people can argue with it.