From 581ceecbf046f99b39885c74e2780a5320e5b15e Mon Sep 17 00:00:00 2001 From: bndw Date: Fri, 13 Feb 2026 17:35:32 -0800 Subject: feat: add Nostr protocol implementation (internal/nostr, internal/websocket) --- internal/nostr/keys.go | 217 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 internal/nostr/keys.go (limited to 'internal/nostr/keys.go') diff --git a/internal/nostr/keys.go b/internal/nostr/keys.go new file mode 100644 index 0000000..3a3fb9c --- /dev/null +++ b/internal/nostr/keys.go @@ -0,0 +1,217 @@ +package nostr + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "strings" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" +) + +// Key represents a Nostr key, which may be a full private key or public-only. +// Use GenerateKey or ParseKey for private keys, ParsePublicKey for public-only. +type Key struct { + priv *btcec.PrivateKey // nil for public-only keys + pub *btcec.PublicKey // always set +} + +// GenerateKey generates a new random private key. +func GenerateKey() (*Key, error) { + var keyBytes [32]byte + if _, err := rand.Read(keyBytes[:]); err != nil { + return nil, fmt.Errorf("failed to generate random bytes: %w", err) + } + + priv, _ := btcec.PrivKeyFromBytes(keyBytes[:]) + return &Key{ + priv: priv, + pub: priv.PubKey(), + }, nil +} + +// ParseKey parses a private key from hex or nsec (bech32) format. +func ParseKey(s string) (*Key, error) { + var privBytes []byte + + if strings.HasPrefix(s, "nsec1") { + hrp, data, err := Bech32Decode(s) + if err != nil { + return nil, fmt.Errorf("invalid nsec: %w", err) + } + if hrp != "nsec" { + return nil, fmt.Errorf("invalid prefix: expected nsec, got %s", hrp) + } + if len(data) != 32 { + return nil, fmt.Errorf("invalid nsec data length: %d", len(data)) + } + privBytes = data + } else { + var err error + privBytes, err = hex.DecodeString(s) + if err != nil { + return nil, fmt.Errorf("invalid hex: %w", err) + } + } + + if len(privBytes) != 32 { + return nil, fmt.Errorf("private key must be 32 bytes, got %d", len(privBytes)) + } + + priv, _ := btcec.PrivKeyFromBytes(privBytes) + return &Key{ + priv: priv, + pub: priv.PubKey(), + }, nil +} + +// ParsePublicKey parses a public key from hex or npub (bech32) format. +// The returned Key can only verify, not sign. +func ParsePublicKey(s string) (*Key, error) { + var pubBytes []byte + + if strings.HasPrefix(s, "npub1") { + hrp, data, err := Bech32Decode(s) + if err != nil { + return nil, fmt.Errorf("invalid npub: %w", err) + } + if hrp != "npub" { + return nil, fmt.Errorf("invalid prefix: expected npub, got %s", hrp) + } + if len(data) != 32 { + return nil, fmt.Errorf("invalid npub data length: %d", len(data)) + } + pubBytes = data + } else { + var err error + pubBytes, err = hex.DecodeString(s) + if err != nil { + return nil, fmt.Errorf("invalid hex: %w", err) + } + } + + if len(pubBytes) != 32 { + return nil, fmt.Errorf("public key must be 32 bytes, got %d", len(pubBytes)) + } + + pub, err := schnorr.ParsePubKey(pubBytes) + if err != nil { + return nil, fmt.Errorf("invalid public key: %w", err) + } + + return &Key{ + priv: nil, + pub: pub, + }, nil +} + +// CanSign returns true if this key can sign events (has private key). +func (k *Key) CanSign() bool { + return k.priv != nil +} + +// Public returns the public key as a 64-character hex string. +func (k *Key) Public() string { + return hex.EncodeToString(schnorr.SerializePubKey(k.pub)) +} + +// Private returns the private key as a 64-character hex string. +// Returns empty string if this is a public-only key. +func (k *Key) Private() string { + if k.priv == nil { + return "" + } + return hex.EncodeToString(k.priv.Serialize()) +} + +// Npub returns the public key in bech32 npub format. +func (k *Key) Npub() string { + pubBytes := schnorr.SerializePubKey(k.pub) + npub, _ := Bech32Encode("npub", pubBytes) + return npub +} + +// Nsec returns the private key in bech32 nsec format. +// Returns empty string if this is a public-only key. +func (k *Key) Nsec() string { + if k.priv == nil { + return "" + } + nsec, _ := Bech32Encode("nsec", k.priv.Serialize()) + return nsec +} + +// Sign signs the event with this key. +// Sets the PubKey, ID, and Sig fields on the event. +// Returns an error if this is a public-only key. +func (k *Key) Sign(event *Event) error { + if k.priv == nil { + return fmt.Errorf("cannot sign: public-only key") + } + + // Set public key + event.PubKey = k.Public() + + if event.CreatedAt == 0 { + event.CreatedAt = time.Now().Unix() + } + + // Compute ID + event.SetID() + + // Hash the ID for signing + idBytes, err := hex.DecodeString(event.ID) + if err != nil { + return fmt.Errorf("failed to decode event ID: %w", err) + } + + // Sign with Schnorr + sig, err := schnorr.Sign(k.priv, idBytes) + if err != nil { + return fmt.Errorf("failed to sign event: %w", err) + } + + event.Sig = hex.EncodeToString(sig.Serialize()) + return nil +} + +// Verify verifies the event signature. +// Returns true if the signature is valid, false otherwise. +func (e *Event) Verify() bool { + // Verify ID first + if !e.CheckID() { + return false + } + + // Decode public key + pubKeyBytes, err := hex.DecodeString(e.PubKey) + if err != nil || len(pubKeyBytes) != 32 { + return false + } + + pubKey, err := schnorr.ParsePubKey(pubKeyBytes) + if err != nil { + return false + } + + // Decode signature + sigBytes, err := hex.DecodeString(e.Sig) + if err != nil { + return false + } + + sig, err := schnorr.ParseSignature(sigBytes) + if err != nil { + return false + } + + // Decode ID (message hash) + idBytes, err := hex.DecodeString(e.ID) + if err != nil { + return false + } + + return sig.Verify(idBytes, pubKey) +} -- cgit v1.2.3