NIP-K1 Protocol

Passkey login
for Nostr

Encrypt your nsec with a passkey. Log in to any Nostr client with your fingerprint. No seed phrases. No clipboard. No extensions.

Clipboard exposure

Copy-pasting nsec between devices leaks your identity to every app that reads the clipboard.

🔒

No sync

Browser extensions don't follow you across devices. Hardware signers add complexity.

One mistake, forever

Leak your nsec once and your Nostr identity is compromised permanently. There's no rotation.

keytr fixes this. Your nsec gets encrypted with a passkey and published to Nostr relays. On any new device, authenticate with your fingerprint to decrypt. That's it. Inspired by the passkey vault approach pioneered by BitTasker.

How it works

Six steps from nsec chaos to passkey-protected login.

1

Generate or import

Create a new nsec or bring your existing one into any keytr-compatible client.

2

Register a passkey

Your device creates a WebAuthn credential bound to a gateway (e.g. keytr.org) or the client's own domain.

3

Encrypt with PRF

The passkey's PRF extension produces a deterministic secret, which is run through HKDF-SHA256 to derive an AES-256 encryption key. Your nsec is encrypted and your public key is stored in the passkey's user.id field for discoverable login.

4

Publish to relays

The encrypted blob is published as a kind:30079 parameterized replaceable event to your Nostr relays.

5

Open any client

On a new device, open any keytr-compatible client and tap "Login with Passkey".

6

Authenticate & decrypt

Your passkey syncs via iCloud / Google / Windows — authenticate with your fingerprint or face to decrypt your nsec instantly.

Architecture

No servers to trust. No passwords to remember. Just math and hardware.

Your device
Passkey + biometric
PRF → HKDF
Deterministic key derivation
AES-256-GCM
Encrypt nsec → 93-byte blob
Nostr relays
kind:30079 event
All cryptography happens client-side. Relays are dumb stores — they never see your nsec or encryption key.

Security

Designed to make the right thing easy and the wrong thing impossible.

Encryption scheme

  • Key source PRF extension → HKDF-SHA256 with random salt
  • user.id 32-byte Nostr public key (enables discoverable login)
  • Cipher AES-256-GCM with 12-byte random IV
  • AAD Credential ID + version byte (prevents substitution & downgrade)

What attackers can't do

  • Phish the key Passkey is origin-bound to the rpId
  • Brute-force Random 256-bit key, not a password
  • Swap ciphertexts AAD binds blob to credential ID
  • Compromise relays End-to-end encrypted, relay sees nothing

The encryption key is derived from the passkey's PRF output — a hardware-bound secret that never leaves the authenticator. HKDF-SHA256 with a random salt turns it into a unique AES key for each encryption. Even if the encrypted event is public on relays, it is useless without your passkey.

Two gateways, zero single points of failure

Your passkey-encrypted keys live on Nostr relays. The gateways just provide the WebAuthn rpId — and now there are two.

Cloudflare
keytr.org
Primary gateway operated by sovIT. Hosted on Cloudflare for global edge performance.
  • Hosting Cloudflare Pages
  • Operator sovIT
  • Status Active
Why does this matter? WebAuthn passkeys are bound to the domain (rpId) they were created on. If keytr.org goes down or Cloudflare has an outage, passkeys registered against it can't authenticate. By registering passkeys against both gateways, you maintain access even if one provider fails. Each gateway produces a separate kind:30079 event on your relays.

Federated cross-client login

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:30079 event for each. keytr.org runs on Cloudflare, nostkey.org runs on GitHub Pages — different providers, different infrastructure, zero single points of failure.

keytr.org
Primary gateway on Cloudflare (sovIT)
your-domain.com
Run your own — anyone can be a gateway
Cross-client WebAuthn
// Store 32-byte pubkey as user.id for discoverable login
const pubkeyBytes = hexToBytes(pubkeyHex)  // 32 bytes

// Register passkey with PRF extension + gateway rpId
navigator.credentials.create({
  publicKey: {
    rp: { id: "keytr.org", name: "keytr" },
    user: { id: pubkeyBytes, name: npub, displayName: npub },
    authenticatorSelection: {
      residentKey: "required",
      userVerification: "required"
    },
    extensions: {
      prf: { eval: { first: "keytr-v1" } }
    }
  }
})

// PRF output → HKDF-SHA256 → AES key → encrypt nsec
// On login, authenticator returns pubkey via userHandle
// + PRF output for key derivation → decrypt nsec

Run your own gateway

Decentralize the protocol further. All you need is a domain and one file.

/.well-known/webauthn
{
  "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.

The event — NIP-K1

Encrypted keys are stored as kind:30079 parameterized replaceable events.

kind:30079
{
  "kind": 30079,
  "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. transports and client tags are optional. Lose one passkey, your others still work.

For client developers

Integrate keytr into your Nostr client in minutes.

Install
npm install @sovit.xyz/keytr
Usage
import { setupKeytr, addBackupGateway, loginWithKeytr, fetchKeytrEvents } from '@sovit.xyz/keytr'

// Setup: register passkey + encrypt nsec
const { credential, encryptedBlob, eventTemplate, nsecBytes, npub }
  = await setupKeytr({ userName: 'alice', userDisplayName: 'Alice', rpId: 'keytr.org' })

// Sign & publish the kind:30079 event to relays

// Login on new device: one biometric tap
const events = await fetchKeytrEvents(pubkey, relays)
const { nsecBytes, npub } = await loginWithKeytr(events)

To have your origin authorized for cross-client login, add your domain to the origins list via PR.

Start building with keytr

Open protocol. Open source. Open to everyone.