{"id":"2484dfffa5261fc8d5667b212c22a2d4a678f3ca258ea4171cce711dd799b3bf","pubkey":"61bf790b2094afb03495c9e136acf615be0fccc2cb95b5acfb5f6ccefe18b062","created_at":1783433250,"kind":30817,"tags":[["d","nip-2f-serverless-two-factor-key-login-via-nip-05-and-blossom"],["title","NIP-2F | Serverless Two-Factor Key Login via NIP-05 and Blossom"],["alt","Nostr Implementation Possibility: NIP-2F | Serverless Two-Factor Key Login via NIP-05 and Blossom"],["client","NostrHub"]],"content":"NIP-2F\n======\n\nServerless Two-Factor Key Login via NIP-05 and Blossom\n------------------------------------------------------\n\n`draft` `optional`\n\nThis NIP defines a serverless, self-custodial login flow with an email-and-password style user experience. A user's [NIP-49](49.md) encrypted secret key (`ncryptsec`) is stored as a public blob on one or more [Blossom](https://github.com/hzrd149/blossom) servers and discovered via the user's [NIP-05](05.md) identifier. The blob is encrypted under a passphrase derived from **two factors**: a user-chosen password and a high-entropy shared secret held in an RFC 6238 authenticator app or backup QR code. No server participates in authentication, no party ever custodies the key, and all decryption happens client-side.\n\n## Discovery\n\nThe `/.well-known/nostr.json` response defined in [NIP-05](05.md) is extended with an OPTIONAL `ncryptsec` field mapping public keys to blob locations:\n\n```jsonc\n{\n  \"names\": {\n    \"alice\": \"b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9\"\n  },\n  \"relays\": {\n    \"b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9\": [ \"wss://relay.example.com\" ]\n  },\n  \"ncryptsec\": {\n    \"b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9\": {\n      \"sha256\": \"5d2899290e0e69bcd809949ee516a4a1597205390878f780c098707a7f18e3df\",\n      \"servers\": [\n        \"https://blossom.example.com\",\n        \"https://cdn.mirror.example.net\"\n      ]\n    }\n  }\n}\n```\n\n- `sha256` is the hash of the blob content, used as the Blossom blob identifier.\n- `servers` is an ordered list of Blossom servers hosting the blob.\n\nThe blob content MUST be the UTF-8 encoded bech32 `ncryptsec1...` string as defined in [NIP-49](49.md). Because the blob content is deterministic, every mirror serves a byte-identical blob with the same hash, and clients can verify integrity by hashing what they receive.\n\n## Key Derivation\n\nTwo factors are combined into the NIP-49 passphrase:\n\n- `password` — the user's chosen password (any Unicode string, normalized per NIP-49).\n- `S` — a randomly generated shared secret of at least 20 bytes (160 bits), encoded in RFC 4648 base32 without padding. This is the same format used by RFC 6238 authenticator apps.\n\n```\npassphrase = password || \":\" || base32(S)\n```\n\nThe resulting passphrase is passed to NIP-49, which applies its own scrypt key derivation. Clients MUST use a NIP-49 `log_n` of at least 16.\n\nThe secret `S` serves double duty:\n\n1. It is a true second factor with enough entropy (≥160 bits) to make offline brute force of the public blob computationally infeasible regardless of password strength.\n2. It can be enrolled into any RFC 6238 authenticator app via a standard `otpauth://` QR code, letting the same secret also generate ordinary TOTP codes for the user's other services or for future companion protocols. Apps that support secret export (e.g. Aegis) can reproduce `S` on demand; clients SHOULD additionally offer a printable backup QR code containing `S` at setup time.\n\n## Setup Flow\n\n1. Client generates a Nostr keypair (`nsec`/`npub`).\n2. Client generates the shared secret `S` and displays it as an `otpauth://` QR code and as a printable backup QR/text string. The user MUST confirm they have stored it before proceeding.\n3. User chooses a password.\n4. Client derives the passphrase as above and encrypts the `nsec` into an `ncryptsec` per NIP-49.\n5. Client uploads the blob to one or more Blossom servers.\n6. Client (or the user's NIP-05 provider) publishes the `ncryptsec` entry in the domain's `nostr.json`.\n\nThe user retains three things: their NIP-05 address, their password, and their secret `S` (in an authenticator app and/or backup QR).\n\n## Login Flow\n\n1. User enters their NIP-05 identifier (e.g. `alice@example.com`).\n2. Client fetches `/.well-known/nostr.json?name=alice`, obtaining the pubkey, blob hash, and server list.\n3. Client fetches the blob from each listed Blossom server in order until one responds, then verifies the content hashes to the published `sha256`.\n4. User enters their password and provides `S` (by scanning their backup QR, pasting the base32 string, or exporting from their authenticator app).\n5. Client derives the passphrase and decrypts the `ncryptsec` locally per NIP-49.\n6. The `nsec` is available in memory; clients SHOULD immediately re-secure it (e.g. device keychain, NIP-46 bunker) rather than persisting it in plain form.\n\nNo server receives the password, the secret, or the derived passphrase at any point.\n\n## Redundancy\n\nBecause the blob is deterministic and content-addressed, any number of Blossom servers can mirror it with zero additional trust assumptions. Clients SHOULD attempt every listed server before failing. Users SHOULD keep an offline copy of their `ncryptsec` string as a final fallback, since it is fully public data.\n\n## Security Considerations\n\n- **Offline brute force.** The public blob is protected by the combined entropy of the password and `S`. Since `S` alone contributes ≥160 bits, offline attacks against the blob are infeasible even with a weak password. The password's role is to protect against theft of `S` (e.g. a compromised authenticator backup), preserving true two-factor security.\n- **Why not per-login TOTP codes.** A static ciphertext cannot be bound to a rotating code: whatever code was used at encryption time is fixed in the ciphertext forever, so a rotating window neither enables later decryption (the user's app shows a *different* current code) nor prevents offline cracking (the attacker targets the one baked-in code, which adds only ~20 bits). This NIP therefore uses the shared secret itself as the second factor. Serverless designs cannot achieve online-only verification; they can only maximize offline cost, which this construction does.\n- **Server compromise.** Blossom servers and NIP-05 hosts hold only public data. A malicious NIP-05 host could serve a wrong hash or server list, causing denial of service but never key disclosure. Clients MAY pin the blob hash after first login.\n- **Loss of factors.** Losing both the password and `S` makes the key unrecoverable. Losing either one alone is also unrecoverable by design; clients MUST make the backup step in setup explicit and mandatory.\n- **Phishing.** A malicious client can capture both factors at login. This matches the threat model of every existing password-based login and is out of scope.\n\n## Compatibility\n\nNo changes are required to relay software, Blossom server implementations, or the NIP-49 encryption format. NIP-05 servers need only serve one additional JSON field. All new behavior is client-side.\n\n## References\n\n- [NIP-05: Mapping Nostr keys to DNS-based internet identifiers](https://github.com/nostr-protocol/nips/blob/master/05.md)\n- [NIP-49: Private Key Encryption](https://nostrhub.io/nip/49)\n- [Blossom: Blobs Stored Simply on Mediaservers](https://github.com/hzrd149/blossom)\n- [RFC 6238: TOTP — Time-Based One-Time Password Algorithm](https://datatracker.ietf.org/doc/html/rfc6238)\n- [RFC 4648: Base32 Encoding](https://datatracker.ietf.org/doc/html/rfc4648)","sig":"6d7f4fddc150bbd9d72d2355b7a6cca91f11f53ef039d11f389078b8fe9c75d353157bf66f8c493f03013addb2a7329e707d1e2d4ee4da9b6e9e3f8908b74ee2"}