This project has not been audited by anyone outside the project. What follows is a plain account of what it protects, what it does not, and what is known to be broken.
Report problems by opening a public issue. There are no users to endanger yet, and public discussion is more useful than a private process nobody staffs.
| Adversary | What they get | What they do not get |
|---|---|---|
| Someone holding a backup file | That it is a YOURVAULT backup; the random accountId; seq and key epoch; the blob's 64 KiB size band; how many passkeys exist, when each was enrolled, their credential IDs, and any device label you typed | The contents. Modification without detection. The exact item count (up to ~100 ordinary logins all produce identical file sizes) |
| A network observer, now or with a quantum computer later | Nothing — the product makes no network requests at all | — |
| Whoever holds your passkey provider (iCloud / Google account taken, or a FIDO CXF export) | Your PRF output — it is not hardware-bound | The vault. A device-local secret is required as well |
| Malware on your machine while the vault is unlocked | Everything on screen | — This is not defended against. Locking wipes the key from memory |
| Whoever controls how you got the extension | Everything | — This is why the install path is "build it yourself", not the Chrome Web Store |
| Any code running in the same browser profile | Use of the device secret (it cannot be read, but it can be used) | — A browser extension cannot do better |
Verified mechanically, and re-checked on every change:
- No network calls. Zero matches for
fetch/XMLHttpRequest/WebSocket/sendBeacon/EventSourceacross the sources, the built bundle, and the bundled dependencies. The manifest declares nohost_permissions, nocontent_scripts, noexternally_connectable. - No content script exists, so autofill cannot render UI into a page. Selection happens only in extension-owned surfaces, and the injected function refuses to run if the page's origin is not the exact origin the item was saved for.
- One cipher suite. XChaCha20-Poly1305 only; the suite byte is checked, so there is no algorithm to downgrade to.
This is the most important thing on this page.
Rotation re-wraps every remaining unlock slot using a KEM public key stored under the old vault key, and nothing binds that stored public key to the private half its owner holds. Someone who reached the vault key once — a device compromised while unlocked — has two ways to persist:
- Substitute their own public key into a surviving slot. The next rotation encapsulates the new vault key to them, and silently kills that slot for its rightful owner.
- Add a slot of their own by calling the normal slot-creation path. Both halves are consistent by construction, so nothing cryptographic can tell it apart from a real one.
Rotation is deliberately possible without touching your authenticator — that is what makes revocation practical at all — so no secret is available at rotation time to check against. This cannot be prevented. The root cause is that the keyset has no authenticity anchor that a vault-key holder cannot themselves produce.
What is done instead:
- Unlocking a slot verifies that the stored KEM public key is the one its own seed produces. A substitution is caught the next time that slot's owner unlocks — which for a daily-use passkey is soon, and before the rotation that would hand over the new key.
- A slot whose key is correct but whose envelope will not open now raises a distinct "tampered" error instead of a bare decryption failure, so it does not read as "your recovery key is wrong".
- Importing a backup that contains unlock paths this device does not know about requires explicit confirmation. That is the only delivery route for the add variant that does not already require device compromise.
- Revoking prompts once about whether the device may have been compromised while unlocked.
If a device was compromised while the vault was unlocked, do a full recovery — which mints an entirely new key set and recovery kit — rather than revoking that device's passkey. Full recovery drops every old slot without reading any stored public key, so it does evict the attacker. This is verified by test.
No shipping authenticator supports ML-DSA passkeys. This is a signature, not a secret, so "harvest now, decrypt later" does not apply: forging an assertion still does not produce the PRF output, so the vault stays closed. Everything protecting the contents is post-quantum (see the table in the README).
It is protected by a non-extractable CryptoKey, which resists reading the profile
directory, but not arbitrary code running in the same browser profile.
The algorithms follow their specs (ML-KEM-768 per FIPS 203, ML-DSA-65 per FIPS 204, Ed25519 per FIPS 186-5, X-Wing per the IETF CFRG draft). The implementation is @noble and carries no CMVP certificate. Naming a spec is not a certification claim.
There is no server. The recovery key opens the envelopes; the file is the envelopes and the contents. Keep the printed key and the file in different places, and the file in at least two.
Claims here are meant to be checkable, not taken on faith.
npm test # every property below has a test
node scripts/verify-guards.mjs # proves each ★ test fails without its fix
node scripts/inventory.mjs --check # every declared invariant has a test★ marks a regression guard. verify-guards.mjs re-runs each one against the code as it
was before the fix that introduced it, and fails if it still passes — because a test
that passes on the broken code proves nothing. Invariants are declared next to the code
that enforces them (@invariant / @test) and inventory.mjs --check fails if any of
them has no test.
Several findings on this page came from adversarial review rounds that also tried to refute their own findings; the revocation limitation above survived that and got worse, which is why it is documented rather than claimed fixed.