# Plan: Keys ## Topic Summary Introduce nostr cryptographic identity via `PublicKey` and `SecretKey` — two type-safe Rust wrappers around `secp256k1` that support hex and NIP-19 bech32 (`npub` / `nsec`) encoding, random generation, validation, and `FromStr` auto-detection. The chapter should make it hard to accidentally leak secret material: `SecretKey` has a redacted `Debug`, no `Display`, and requires an explicit getter to expose its bytes. ## Chapter Outline 1. **Identity as a keypair.** One paragraph framing: in nostr, identity *is* a secp256k1 keypair you generate yourself. The public key is your name; the secret key is your ability to speak as that name. 2. **secp256k1, briefly.** Two or three sentences on why this curve and why x-only / Schnorr keys (BIP-340). No cryptography lecture. 3. **The `PublicKey` type.** Newtype around `secp256k1::XOnlyPublicKey`, with hex and npub encoding. `Display` shows hex. `FromStr` auto-detects. 4. **The `SecretKey` type.** Newtype around `secp256k1::SecretKey`. Redacted `Debug`, no `Display`, explicit `.to_hex()` / `.to_nsec()`. `FromStr` auto-detects. Random generation. 5. **Deriving a public key from a secret key.** `SecretKey::public_key()`. 6. **NIP-19: the `npub` / `nsec` bech32 envelope.** Short explanation of what bech32 is doing for us and why the user-facing encoding is different from the wire format. 7. **Tests.** Round-trip coverage for hex, bech32, `FromStr` auto-detection, prefix validation, and debug redaction. ## API Design In `coracle-lib/src/keys.rs`: ```rust pub struct PublicKey(secp256k1::XOnlyPublicKey); impl PublicKey { pub fn from_hex(s: &str) -> Result; pub fn to_hex(&self) -> String; pub fn from_npub(s: &str) -> Result; pub fn to_npub(&self) -> String; pub fn as_bytes(&self) -> [u8; 32]; } impl fmt::Display for PublicKey { /* hex */ } impl FromStr for PublicKey { /* npub1 → from_npub, else from_hex */ } pub struct SecretKey(secp256k1::SecretKey); impl SecretKey { pub fn generate() -> Self; pub fn from_hex(s: &str) -> Result; pub fn to_hex(&self) -> String; // explicit opt-in pub fn from_nsec(s: &str) -> Result; pub fn to_nsec(&self) -> String; // explicit opt-in pub fn public_key(&self) -> PublicKey; } impl fmt::Debug for SecretKey { /* "SecretKey()" */ } // NO Display impl — callers must choose to_hex() or to_nsec() impl FromStr for SecretKey { /* nsec1 → from_nsec, else from_hex */ } pub enum KeyError { InvalidHex, InvalidBech32, WrongBech32Prefix { expected: &'static str, found: String }, InvalidKey, // from secp256k1 } ``` ## Code Organization - All code lives in `coracle-lib/src/keys.rs`. - `coracle-lib/src/lib.rs` re-exports the public types: `pub mod keys;` and `pub use keys::{PublicKey, SecretKey, KeyError};`. - No cross-crate coupling. Signing (which consumes `SecretKey`) is the next chapter and will live in `coracle-signer`. ## Dependencies In `coracle-lib/Cargo.toml`: - `secp256k1 = { version = "0.29", features = ["global-context", "rand", "serde"] }` — curve operations and a process-wide randomized context. - `hex = "0.4"` — already present; used for hex encoding. - `bech32 = "0.11"` — new; for NIP-19 envelopes. Use the `Bech32` variant (NIP-19 uses the original bech32, not bech32m). - `rand = "0.8"` — for `SecretKey::generate()`. secp256k1's `rand` feature pulls this in. ## Narrative Notes - Open with the philosophy: generating a keypair is the only step required to "sign up" for nostr. No server, no email, no approval. - Explain the split between hex (wire format, inside event JSON) and bech32 (human-facing, for display and copy-paste). Both encode the same 32 bytes. - Justify why `SecretKey` has no `Display`: it's easy to accidentally `println!("{}", key)` and leak credentials into logs. Making the caller write `.to_hex()` or `.to_nsec()` forces a conscious decision. - Justify the redacted `Debug`: `#[derive(Debug)]` on a struct containing a `SecretKey` is common, and we don't want that to dump the key. - Briefly mention that `secp256k1::SecretKey` zeroes its memory on drop so we get that for free. - The chapter should not discuss HD wallets, mnemonics, or encrypted key storage — keep it about the protocol primitive. ## Design Decisions - **Wrap `secp256k1` directly, don't depend on `bitcoin`.** Matches rust-nostr and keeps the dependency graph small. - **x-only public keys.** BIP-340 / Schnorr is the nostr standard; the underlying type is `XOnlyPublicKey`. - **No `Copy` on `SecretKey`.** Following rust-nostr's safety posture. `PublicKey` is `Copy` (it's just 32 public bytes). - **No `Display` on `SecretKey`, custom `Debug`.** The research flagged that rust-nostr derives `Debug` on `SecretKey` and relies on the composite `Keys` type to redact — which is a footgun. We'll redact at the type where the material lives. - **Errors as one enum.** A teaching resource benefits from a single `KeyError` users can match on. We won't split into per-operation error types. - **Auto-detecting `FromStr`.** Matches every reference implementation and makes round-tripping user input painless. - **`bech32 = 0.11`.** Current version; rust-nostr uses the same. ## Open Questions - None blocking. If the `bech32` 0.11 API turns out to be awkward for a teaching context, fall back to writing a small helper and explain it.