Insight

Fenko Vault: what it is, how it works, and what’s left before 1.0

Fenko Vault stores WebAuthn passkeys and TOTP codes on your own devices, with optional end-to-end encrypted sync over Nostr relays. Past 15,000 installs and featured on the Chrome Web Store, here is why we built it, how the interception and sync actually work, and what has to be true before we call it 1.0.

Fenko Vault: what it is, how it works, and what's left before 1.0

Fenko Vault is past 15,000 installs across Chrome, Firefox and Google Play, and the Chrome Web Store has picked up the listing as Featured. That is more people than we expected to be running a tool we originally wrote for ourselves, and a good moment to write down what it actually does, where it came from, and what still has to happen before the version number stops starting with a zero.

The short version: it stores WebAuthn passkeys and TOTP codes on your own devices, it has no account and no Fenko server holding your credentials, and if you turn on sync your devices talk to each other over public Nostr relays in ciphertext. It’s MIT licensed and the source is at FenkoHQ/passkey-vault.

Why we built it

Two reasons, and the first one is selfish.

We test authentication for a living. Somewhere in most engagements there is a WebAuthn flow, and the moment you want to look at it closely you hit a wall. The passkey lives in a platform authenticator run by Apple, Google or Microsoft. You cannot export it. You cannot read it. You cannot point it at a staging environment, hand it to a colleague, or diff what one relying party sends against another. The whole design goal of a platform authenticator is that the key is sealed away from you, which is correct for your bank and infuriating for a tester. So we wrote something that does the WebAuthn dance properly but keeps the key material where we can inspect it.

The second reason showed up once we started using it daily. Password managers have been quietly absorbing every factor you own. Password, TOTP seed, and now the passkey, all in the same vault behind the same unlock. It’s convenient, and we are not trying to talk anyone out of a password manager. But a second factor is only worth having if it lives somewhere the first one doesn’t, and a single vault breach takes the password and the thing meant to back it up in the same motion. Fenko Vault holds no passwords at all, by design. It is the other basket.

That is also why it is not a password manager and never will be. It does one job.

How it works

Interception

A content script injects into the page and overrides navigator.credentials.create() and navigator.credentials.get() before the browser gets a look at them.

On create(), the background service worker generates an ECDSA P-256 key pair, builds a valid attestation response, and writes the credential to extension storage. Attested credentials carry our own AAGUID, d2717a32-9851-48a8-9961-b264c97a411a, registered in the community authenticator registry so sites that show you which authenticator you used display Fenko Vault instead of a blank box. Consumer builds use none attestation, which is the privacy-preserving default and means no per-install certificate goes to the relying party.

On get(), it signs the challenge with the stored private key, with the CBOR encoding the spec actually asks for rather than the one that happens to work on the site you tested against.

If a site asks for a passkey you don’t have stored, the vault steps out of the way and lets the browser and OS handle it as normal. That passthrough matters more than it sounds. An authenticator that intercepts everything and then fails is worse than no authenticator, because it breaks sign-in on sites you never intended to move. Interception has three modes: off, all sites, or an allowlist you control.

Codes

TOTP and HOTP are RFC 6238 and RFC 4226, computed locally with HMAC-SHA1, SHA256 or SHA512, 6 to 8 digits, custom periods. You add a code by pasting the otpauth:// URI, pasting a QR screenshot straight into the popup, or uploading the image. Decoding happens on-device with jsQR, so there is no camera permission and nothing to send anywhere.

Passkeys and codes share one searchable list rather than living in two apps that don’t know about each other. That was the single most requested thing early on.

At rest

Set an optional 4 to 12 digit master PIN and the vault gets written as an AES-GCM encrypted copy, with a lock screen in front of the popup and an auto-lock timeout.

One limitation to be clear about, because it is the honest reason the version still starts with a zero. On the browser build, a raw copy of the vault is kept alongside the encrypted one so the popup can render without an unlock. That is deliberate for now and we are not happy about it: users with no PIN have nowhere else to keep the data, and an MV3 service worker can be killed at any moment, which would mean an unlock prompt every time the browser felt like recycling it. Treat your browser profile and any exported backup as credential material, because that is exactly what they are.

Sync

Sync is optional, off by default, and has no account behind it.

Turning it on generates a BIP-39 seed phrase. That phrase is the sync chain. Type it into another device and the two are linked; there is no pairing server, no QR handshake with us in the middle, no email address. From the seed, each device derives two things with PBKDF2 at 100k iterations over SHA-256: an AES-256-GCM key for the payloads, and a secp256k1 keypair for signing.

Transport is plain Nostr. Kind 30078 application-data events with a d tag of pksync-<chainId>, signed with BIP340 Schnorr signatures, content is AES-GCM ciphertext. A relay operator, or anyone watching a relay, sees ciphertext, the chain tag and the timing. Everything with meaning in it, the passkey IDs, the relying-party domains, your device names, how many entries you hold, is inside the encrypted blob.

Receiving devices check the Schnorr signature and the event ID hash before they attempt to decrypt anything, drop replayed event IDs, and throw away whatever doesn’t decrypt under the chain key. Merging is additive: a device takes entries it doesn’t have and updates ones with a newer creation time. Sync never deletes local data, which is the boring choice and the right one when the failure mode is losing a credential.

By default your devices publish to a relay we run at wss://vaultsync.fenko.nz (running nosflare, configured to accept only kind 30078 sync events) plus three public ones. You can remove ours entirely and point it wherever you like, including at your own. Two devices stay in sync as long as they share one working relay.

The thing we like about this design is what it does not require. No token, no OAuth provider, no platform account, no company that can be served with a request for your vault, because the only copy anyone else holds is bytes they cannot read. The tradeoff is equally blunt: anyone who obtains your seed phrase joins your chain, and if you lose every device and every backup, nobody can recover it for you. That is stated in the app, twice, on purpose.

What has to happen before 1.0

We are not putting a date on this, because we would rather ship it late than tell you a month and then quietly move it. The list is short and it is public.

The at-rest story on the web build has to be finished, which means removing the plaintext copy without turning every service-worker restart into an unlock prompt. The PBKDF2 iteration count needs to go up, and doing that safely needs a stored-iteration-count migration, otherwise the bump locks every existing vault out of its own data. Android and iOS should be using the platform Keystore and Keychain rather than the shared store.

Sync needs a verified four-way round trip: Chrome, Firefox, Android and iOS on one seed phrase, merging end to end, checked by hand and not just by the test suite. A green test suite is not the same thing as two devices actually reading each other’s events, so we do not consider this proven until someone has watched it happen on real hardware.

The emergency recovery page is still a stub. It exists, the entry point works, the interface behind it is not finished. Test coverage on the sync service, the message handlers and the UI needs to be a lot better than it is.

There is an iOS build in the tree. It uses a native ASCredentialProviderExtension for passkeys with the same shared engine the Android app and the extensions run, so sync interoperates by construction instead of by reimplementation. It builds, it runs, the crypto round-trips. It has not been tested on a real device yet and we are not giving it an ETA.

Verifiable attestation, the packed kind with a real certificate chain that regulated relying parties can check, is not on this list. It needs a per-organisation attestation CA and FIDO functional certification, and it belongs to an enterprise tier rather than to the free consumer build. The consumer default stays none.

Tell us what breaks

The most useful thing you can send us is a site where the vault does the wrong thing. Open an issue with the site, the browser or Android version, what you expected and what actually happened. If it is a sign-in failure, say whether the vault took the request or handed it back to the browser, because those are two different bugs. If it is sync, turn on the debug flag in settings and include the protocol log.

Feature requests are welcome too, with one filter: if it means holding passwords, the answer is no. Everything else is open.

Fenko Vault is on the Chrome Web Store for Chrome, Edge, Brave and Opera, on Firefox Add-ons, and on Google Play. It costs nothing, there is no account to create, and the source is right there if you would rather read it than trust us.