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 & vault the key

A random encryption key is generated, used to encrypt your nsec, then packed alongside your npub into the passkey's user.id field — creating a split-knowledge vault that only your biometric can unlock.

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
Export key
Random 32-byte key in user.id
AES-256-GCM
Encrypt nsec → 61-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 Random 256-bit export key in WebAuthn user.id
  • Split knowledge Relay has ciphertext, passkey has key
  • Cipher AES-256-GCM with 12-byte random IV
  • AAD Credential ID (prevents substitution)

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 embedded inside your passkey's user.id field — it only leaves the authenticator after biometric/PIN verification. 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
// Pack npub + export key into user.id (64 bytes)
const userHandle = new Uint8Array(64)
userHandle.set(npubBytes, 0)        // bytes 0–31: public key
userHandle.set(exportKey, 32)       // bytes 32–63: encryption key

// Register passkey with gateway rpId
navigator.credentials.create({
  publicKey: {
    rp: { id: "keytr.org", name: "keytr" },
    user: { id: userHandle, name: npub, displayName: npub },
    authenticatorSelection: {
      residentKey: "required",
      userVerification: "required"
    }
  }
})

// On login, authenticator returns userHandle
// → extract npub + export key → 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 nsec blob>",
  "tags": [
    ["d", "<credential-id>"],
    ["rp", "keytr.org"],
    ["algo", "aes-256-gcm"],
    ["scheme", "passkey-vault"],
    ["v", "1"]
  ]
}

Multiple passkeys can be registered — each produces a separate event with a different d tag. Lose one passkey, your others still work.

For client developers

Integrate keytr into your Nostr client in minutes.

Install
npm install keytr
Usage
import { setupKeytr, loginWithKeytr, fetchKeytrEvents } from 'keytr'

// Setup: register passkey + encrypt nsec
const { credential, encryptedBlob, eventTemplate, nsecBytes, npub }
  = await setupKeytr({ userName: '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[0])

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.