From cc7637b833bb66cd8715d466e615a23ec0f05c92 Mon Sep 17 00:00:00 2001 From: Clawd Date: Sun, 8 Mar 2026 16:32:53 -0700 Subject: feat: NIP-04 encrypt/decrypt using internal secp256k1 --- nip04.go | 46 ++++++++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 26 deletions(-) (limited to 'nip04.go') diff --git a/nip04.go b/nip04.go index 1f1c245..f979bcd 100644 --- a/nip04.go +++ b/nip04.go @@ -10,13 +10,13 @@ import ( "fmt" "strings" - "github.com/btcsuite/btcd/btcec/v2" + "code.northwest.io/nostr/internal/secp256k1" ) -// NIP04Encrypt encrypts a plaintext message for a recipient using NIP-04 -// (ECDH shared secret + AES-256-CBC). The sender's private key and -// recipient's public key (64-char hex) are required. -// Returns the ciphertext in NIP-04 format: "?iv=" +// NIP04Encrypt encrypts plaintext for a recipient using NIP-04 +// (ECDH shared secret + AES-256-CBC). +// recipientPubHex is the recipient's x-only public key as 64-char hex. +// Returns ciphertext in NIP-04 format: "?iv=" func (k *Key) NIP04Encrypt(recipientPubHex, plaintext string) (string, error) { if k.priv == nil { return "", fmt.Errorf("cannot encrypt: public-only key") @@ -45,7 +45,7 @@ func (k *Key) NIP04Encrypt(recipientPubHex, plaintext string) (string, error) { } // NIP04Decrypt decrypts a NIP-04 ciphertext using the receiver's private key -// and sender's public key (64-char hex). +// and the sender's x-only public key as 64-char hex. func (k *Key) NIP04Decrypt(senderPubHex, ciphertext string) (string, error) { if k.priv == nil { return "", fmt.Errorf("cannot decrypt: public-only key") @@ -91,37 +91,31 @@ func (k *Key) NIP04Decrypt(senderPubHex, ciphertext string) (string, error) { return string(unpadded), nil } -// nip04SharedSecret computes the ECDH shared secret per NIP-04: -// SHA256(privKey * recipientPubKey).x -func nip04SharedSecret(priv *btcec.PrivateKey, recipientPubHex string) ([]byte, error) { +// nip04SharedSecret computes the NIP-04 ECDH shared secret: +// SHA256( (privKey * recipientPubKey).x ) +func nip04SharedSecret(priv *secp256k1.PrivateKey, recipientPubHex string) ([]byte, error) { pubBytes, err := hex.DecodeString(recipientPubHex) if err != nil { return nil, fmt.Errorf("decode pubkey: %w", err) } - // NIP-04 pubkeys are x-only (32 bytes, Schnorr). Prefix with 0x02 for - // compressed SEC1 parsing. - if len(pubBytes) == 32 { - pubBytes = append([]byte{0x02}, pubBytes...) - } - - pub, err := btcec.ParsePubKey(pubBytes) + // Expect 32-byte x-only key + pub, err := secp256k1.ParsePublicKeyXOnly(pubBytes) if err != nil { return nil, fmt.Errorf("parse pubkey: %w", err) } - // ECDH: scalar multiply - var point btcec.JacobianPoint - pub.AsJacobian(&point) - priv.Key.SetByteSlice(priv.Serialize()) - btcec.ScalarMultNonConst(&priv.Key, &point, &point) - point.ToAffine() + // ECDH: scalar multiply — shared point = priv.D * recipientPub + shared := pub.Point.ScalarMul(priv.D) - // Shared secret is SHA256 of the x-coordinate - xBytes := point.X.Bytes() - shared := sha256.Sum256(xBytes[:]) + // Shared secret = SHA256(x-coordinate, big-endian, 32 bytes) + xBytes := shared.XBytes() + if xBytes == nil { + return nil, fmt.Errorf("ECDH result is point at infinity") + } - return shared[:], nil + h := sha256.Sum256(xBytes) + return h[:], nil } func pkcs7Pad(data []byte, blockSize int) []byte { -- cgit v1.2.3