Encrypt your nsec with a passkey. Log in to any Nostr client with your fingerprint. No seed phrases. No clipboard. No extensions. Works with password managers too.
Copy-pasting nsec between devices leaks your identity to every app that reads the clipboard.
Browser extensions don't follow you across devices. Hardware signers add complexity.
Leak your nsec once and your Nostr identity is compromised permanently. There's no rotation.
Six steps from nsec chaos to passkey-protected login.
Create a new nsec or bring your existing one into any keytr-compatible client.
Your device creates a WebAuthn credential bound to a gateway (e.g. keytr.org) or the client's own domain.
keytr uses the passkey's PRF extension: the authenticator derives a secret that never leaves the hardware, which keytr runs through HKDF-SHA256 to derive an AES-256 encryption key. Authenticators without PRF support aren't used — there is no weaker fallback.
The encrypted blob is published as a kind:31777 parameterized replaceable event to your Nostr relays.
On a new device, open any keytr-compatible client and tap "Login with Passkey".
Your passkey syncs via iCloud / Google / Windows — authenticate with your fingerprint or face to decrypt your nsec instantly.
No servers to trust. No passwords to remember. Just math and hardware.
Designed to make the right thing easy and the wrong thing impossible.
The encryption key is hardware-bound and never leaves the authenticator, so page scripts can't read it. HKDF-SHA256 with a random salt produces a unique AES key per encryption, so even though the encrypted event is public on relays, it is useless without your passkey.
Why not Key-in-Handle? keytr briefly experimented with a Key-in-Handle (KiH) mode that stored the encryption key inside the passkey, but withdrew it as too risky — that key was exposed to page scripts. PRF keeps the key inside the authenticator, where page code can't read it.
Your passkey-encrypted keys live on Nostr relays. The gateways just provide the WebAuthn rpId — and now there are two.
kind:31777 event on your relays.
One passkey works across every Nostr client. No single domain controls the protocol.
WebAuthn binds passkey outputs to the domain (rpId) they were created on. To enable cross-client login, keytr uses a federated gateway model built on the W3C Related Origin Requests spec.
Any domain can become a passkey gateway by hosting a /.well-known/webauthn file listing authorized client origins. Register passkeys against multiple gateways, producing a separate kind:31777 event for each. keytr.org runs on Cloudflare, nostkey.org runs on Hostinger — different providers, different infrastructure, zero single points of failure.
import { setup, discover, publishKeytrEvent } from '@sovit.xyz/keytr'
import { finalizeEvent } from 'nostr-tools/pure'
// Setup: PRF only — throws PrfNotSupportedError if unsupported
const { credential, encryptedBlob, eventTemplate, nsecBytes, npub } = await setup({
rpId: 'keytr.org', rpName: 'keytr',
userName: 'alice', userDisplayName: 'Alice'
})
// Sign & publish
const signed = finalizeEvent(eventTemplate, nsecBytes)
await publishKeytrEvent(signed, relays)
// Discoverable login — one biometric prompt
const { nsecBytes, npub, pubkey } = await discover(relays, { rpId: 'keytr.org' })
Decentralize the protocol further. All you need is a domain and one file.
{
"origins": [
"https://your-gateway.example",
"https://client-a.com",
"https://client-b.com"
]
}
Clients listed in your origins can register passkeys under your domain's rpId. The browser verifies authorization automatically via the Related Origin Requests spec.
Encrypted keys are stored as kind:31777 parameterized replaceable events.
{
"kind": 31777,
"content": "<base64 encrypted 93-byte blob>",
"tags": [
["d", "<credential-id-base64url>"],
["rp", "keytr.org"],
["algo", "aes-256-gcm"],
["kdf", "hkdf-sha256"],
["v", "1"],
["transports", "internal", "hybrid"],
["client", "<client-name>"]
]
}
Multiple passkeys can be registered — each produces a separate event with a different d tag. The v tag is 1 (PRF) for all current events; v=3 is a reserved legacy value (the withdrawn KiH mode). transports and client tags are optional. Lose one passkey, your others still work.
Integrate keytr into your Nostr client in minutes.
npm install @sovit.xyz/keytr
import { setup, discover, publishKeytrEvent } from '@sovit.xyz/keytr'
import { finalizeEvent } from 'nostr-tools/pure'
// Setup: PRF only (throws PrfNotSupportedError if unsupported)
const { credential, encryptedBlob, eventTemplate, nsecBytes, npub }
= await setup({ rpId: 'keytr.org', rpName: 'keytr', userName: 'alice', userDisplayName: 'Alice' })
// Sign & publish the kind:31777 event to relays
const signed = finalizeEvent(eventTemplate, nsecBytes)
await publishKeytrEvent(signed, relays)
// Discoverable login: one biometric tap
const { nsecBytes, npub, pubkey } = await discover(relays, { rpId: 'keytr.org' })
To have your origin authorized for cross-client login, add your domain to the origins list via PR.
Open protocol. Open source. Open to everyone.