# Research: Keys ## Topic Summary The Keys chapter covers nostr cryptographic identity: `PublicKey` and `SecretKey` as type-safe Rust wrappers around secp256k1. Includes hex encoding, NIP-19 bech32 encoding (`npub` / `nsec`), key generation, validation, `FromStr` auto-detection of encoding variants, and safety measures that reduce accidental leakage of secret material (redacted `Debug`, no `Display`, zeroization on drop). ## Philosophy From `ref/building-nostr`: - **Self-sovereignty via cryptography.** Nostr inverts platform-mediated identity: users unilaterally generate a secp256k1 keypair and become the authority over their own identity. No custodian, no issuer. - **Identity is subjective and relational.** Keys enable users to *act*; reputation and context are layered on top via follow graphs, NIP-05, kind-0 profiles. - **Publicity, not privacy.** Nostr is "publicity technology" — signed, verifiable, permanent. Keys enable authenticity, not confidentiality. - **Key handling is a big assumption.** Storage is deferred to signers (NIP-07 browser, NIP-46 remote, NIP-55 Android). A library should make it hard to accidentally leak secret material. - **Key rotation is unsolved.** Losing a secret key means losing social identity permanently; this is a philosophical reason to take secret-key handling seriously at the type-system level. - **Zooko's triangle.** Hex/bech32 keys are secure and decentralized but not human-meaningful. The raw-key layer should be correct and minimal; naming is someone else's job. Design implication: the `SecretKey` type should nudge users toward safety by default (no `Display`, redacted `Debug`, explicit method to extract material). ## Reference Implementation Analysis ### applesauce (TypeScript) - Secret keys: raw `Uint8Array` (32 bytes). Public keys: 64-char lowercase hex. - No wrapper types — thin helpers (`normalizeToSecretKey`, `normalizeToPubkey`) accept hex, nsec, or NIP-19 pointer and return primitives. - Depends on `nostr-tools` for NIP-19, `@noble/secp256k1` underneath. - No debug redaction; no memory zeroing. Locking a signer just nulls its private field. - `isHexKey()` validates format (length + charset), not curve membership. ### ndk (TypeScript) - `Hexpubkey` and `Npub` are branded string type aliases; secret keys are `Uint8Array` stored privately inside `NDKPrivateKeySigner`. - Auto-detects `nsec1` prefix vs. 64-char hex on signer construction, throws on invalid input. - Delegates all NIP-19 work to `nostr-tools/nip19`; uses `@noble/hashes` for hex↔bytes and `nostr-tools/nip49` for password-encrypted keys (`ncryptsec`). - Keys live in private fields with controlled accessors; no accidental logging. - Character-code loop for hex validation (avoids regex). ### nostr-gadgets (TypeScript) - No dedicated key types at all. `pubkey` is `string` (hex), secrets are passed as `Uint8Array` at call sites and not stored. - `isHex32()` validates via `charCodeAt` against ASCII ranges. - `bareNostrUser()` normalizes npub / nprofile / hex into both hex and npub fields on demand. - Zero key material coupling — cryptography is entirely outsourced to `@nostr/tools`. ### nostr-tools (TypeScript) - Minimal, explicit: `generateSecretKey() -> Uint8Array`, `getPublicKey(sk) -> hex`. - `NPub`/`NSec` branded string types for bech32 forms. - NIP-19 TLV encoding/decoding via `@scure/base` for nprofile/nevent/naddr. - Dependencies: `@noble/curves` (schnorr), `@noble/hashes`, `@scure/base`. Pure-JS, auditable, no OS bindings. - Type guards via regex. `validateEvent()` rejects pubkeys that don't match `^[a-f0-9]{64}$`. - No zeroization — JS has no safe wipe primitive. ### rust-nostr (Rust — most directly relevant) - `PublicKey`: 32-byte wrapper around `secp256k1::XOnlyPublicKey`. Implements `Copy`. - `SecretKey`: newtype around `secp256k1::SecretKey`, **not** `Copy`, implements `Drop` via `non_secure_erase()` from secp256k1-rs. - `Keys`: composite holding both plus `secp256k1::Keypair`. Custom `Debug` impl that only shows the public key. - `SecretKey` derives `Debug` directly, which the authors note is a footgun — `Keys` compensates by overriding. - `parse()` method on both types auto-detects hex vs. bech32 (and NIP-21 `nostr:` URIs on `PublicKey`). `FromStr` delegates to `parse()`. - Dependencies: `secp256k1 = "0.29"`, `bech32 = "0.11"`, `faster-hex = "0.10"`, `bitcoin_hashes`. - Global `SECP256K1: LazyLock>` context, randomized via OsRng at first access (blinding countermeasure). - NIP-19 handled in a separate `nip19.rs` module; serde serializes `PublicKey` as hex string. ### welshman (TypeScript) - No wrapper types; keys are hex strings. `makeSecret()` / `getPubkey()` as pure functions. - `Pubkey` class wraps a pubkey hex with `.toNpub()` and `Pubkey.from(entity)` that decodes npub/nprofile and validates with regex. - `Nip01Signer` stores the secret in a private `#secret` field, exposing only async signing operations. - Depends on `nostr-tools` + `@noble/curves`. ## Common Patterns - **secp256k1 everywhere.** All references use secp256k1 with Schnorr-style x-only public keys (BIP-340). This is fixed by the protocol. - **Dual encoding.** Hex for internal storage and wire format (event JSON); bech32 (NIP-19 `npub`/`nsec`) for user-facing display and copy-paste. - **Auto-detect on parse.** Most libraries accept either form and figure out which is which by prefix or length. - **Secret material isolation.** The TypeScript libraries hide secrets in private fields of a signer class. rust-nostr uses the type system: no `Copy`, explicit `to_secret_hex()` getter, redacted `Debug` on the composite, memory erase on `Drop`. - **No curve-membership validation up front.** Most validate format only and let the underlying crypto library reject invalid points lazily. rust-nostr is stricter because `secp256k1::SecretKey::from_slice` rejects out-of-range values on construction. - **NIP-19 is a separate concern.** Every library has a distinct NIP-19 module; the key types depend on it for bech32 but the bech32 logic is not inside the key type itself. ## Considerations for Our Implementation **Crate choice.** The `secp256k1` crate (already in `coracle-lib/Cargo.toml` per commit history) is the natural fit — widely audited, used by Bitcoin Core bindings, same choice as rust-nostr. Its `SecretKey::from_slice` enforces curve constraints. For bech32 we'll pull in `bech32 = "0.11"`. Hex can come from the existing `hex` crate (already a dependency). **Types.** - `PublicKey` — newtype around `secp256k1::XOnlyPublicKey`. Implements `Copy`, `Debug`, `Display` (hex), `FromStr`, `Serialize`, `Deserialize`. - `SecretKey` — newtype around `secp256k1::SecretKey`. Does *not* implement `Copy` or `Display`. Custom `Debug` that prints `SecretKey()`. `FromStr` that auto-detects hex vs. `nsec1…`. Explicit `to_hex()` and `to_nsec()` methods that opt into exposing the material. **Encoding.** - `PublicKey::to_hex()` / `to_npub()` and their `from_` counterparts. - `SecretKey::to_hex()` / `to_nsec()` / `from_*`. - `FromStr` auto-detects by checking `npub1`/`nsec1` prefix first, then falling back to 64-char hex. **Safety posture.** Match rust-nostr's conventions where they're sensible in a teaching context, but don't hide secret-material extraction behind unsafe or complicated APIs — the chapter is pedagogical. Redact `Debug`, omit `Display`, require an explicit `.to_hex()` / `.to_nsec()` call. **Out of scope for this chapter.** - Signing and verification (next chapter). - Key derivation, BIP-32, mnemonics (not part of the nostr protocol). - NIP-49 encrypted key storage (`ncryptsec`). - Remote / browser / Android signers (later chapters). - `nprofile`, `nevent`, `naddr` TLV pointers (chapter on entities/content). Keep the chapter narrow: identity as a keypair, how to encode it, how to avoid leaking it.