diff options
| author | Clawd <ai@clawd.bot> | 2026-03-08 16:32:53 -0700 |
|---|---|---|
| committer | Clawd <ai@clawd.bot> | 2026-03-08 16:32:53 -0700 |
| commit | cc7637b833bb66cd8715d466e615a23ec0f05c92 (patch) | |
| tree | 7c242a3c3be0ad122f5d74f8a8c04f80a85c4c98 /nip04.go | |
| parent | 0dcf191e7f63f35efe85afb60bcb57f4934a0acb (diff) | |
Diffstat (limited to 'nip04.go')
| -rw-r--r-- | nip04.go | 46 |
1 files changed, 20 insertions, 26 deletions
| @@ -10,13 +10,13 @@ import ( | |||
| 10 | "fmt" | 10 | "fmt" |
| 11 | "strings" | 11 | "strings" |
| 12 | 12 | ||
| 13 | "github.com/btcsuite/btcd/btcec/v2" | 13 | "code.northwest.io/nostr/internal/secp256k1" |
| 14 | ) | 14 | ) |
| 15 | 15 | ||
| 16 | // NIP04Encrypt encrypts a plaintext message for a recipient using NIP-04 | 16 | // NIP04Encrypt encrypts plaintext for a recipient using NIP-04 |
| 17 | // (ECDH shared secret + AES-256-CBC). The sender's private key and | 17 | // (ECDH shared secret + AES-256-CBC). |
| 18 | // recipient's public key (64-char hex) are required. | 18 | // recipientPubHex is the recipient's x-only public key as 64-char hex. |
| 19 | // Returns the ciphertext in NIP-04 format: "<base64>?iv=<base64>" | 19 | // Returns ciphertext in NIP-04 format: "<base64>?iv=<base64>" |
| 20 | func (k *Key) NIP04Encrypt(recipientPubHex, plaintext string) (string, error) { | 20 | func (k *Key) NIP04Encrypt(recipientPubHex, plaintext string) (string, error) { |
| 21 | if k.priv == nil { | 21 | if k.priv == nil { |
| 22 | return "", fmt.Errorf("cannot encrypt: public-only key") | 22 | return "", fmt.Errorf("cannot encrypt: public-only key") |
| @@ -45,7 +45,7 @@ func (k *Key) NIP04Encrypt(recipientPubHex, plaintext string) (string, error) { | |||
| 45 | } | 45 | } |
| 46 | 46 | ||
| 47 | // NIP04Decrypt decrypts a NIP-04 ciphertext using the receiver's private key | 47 | // NIP04Decrypt decrypts a NIP-04 ciphertext using the receiver's private key |
| 48 | // and sender's public key (64-char hex). | 48 | // and the sender's x-only public key as 64-char hex. |
| 49 | func (k *Key) NIP04Decrypt(senderPubHex, ciphertext string) (string, error) { | 49 | func (k *Key) NIP04Decrypt(senderPubHex, ciphertext string) (string, error) { |
| 50 | if k.priv == nil { | 50 | if k.priv == nil { |
| 51 | return "", fmt.Errorf("cannot decrypt: public-only key") | 51 | return "", fmt.Errorf("cannot decrypt: public-only key") |
| @@ -91,37 +91,31 @@ func (k *Key) NIP04Decrypt(senderPubHex, ciphertext string) (string, error) { | |||
| 91 | return string(unpadded), nil | 91 | return string(unpadded), nil |
| 92 | } | 92 | } |
| 93 | 93 | ||
| 94 | // nip04SharedSecret computes the ECDH shared secret per NIP-04: | 94 | // nip04SharedSecret computes the NIP-04 ECDH shared secret: |
| 95 | // SHA256(privKey * recipientPubKey).x | 95 | // SHA256( (privKey * recipientPubKey).x ) |
| 96 | func nip04SharedSecret(priv *btcec.PrivateKey, recipientPubHex string) ([]byte, error) { | 96 | func nip04SharedSecret(priv *secp256k1.PrivateKey, recipientPubHex string) ([]byte, error) { |
| 97 | pubBytes, err := hex.DecodeString(recipientPubHex) | 97 | pubBytes, err := hex.DecodeString(recipientPubHex) |
| 98 | if err != nil { | 98 | if err != nil { |
| 99 | return nil, fmt.Errorf("decode pubkey: %w", err) | 99 | return nil, fmt.Errorf("decode pubkey: %w", err) |
| 100 | } | 100 | } |
| 101 | 101 | ||
| 102 | // NIP-04 pubkeys are x-only (32 bytes, Schnorr). Prefix with 0x02 for | 102 | // Expect 32-byte x-only key |
| 103 | // compressed SEC1 parsing. | 103 | pub, err := secp256k1.ParsePublicKeyXOnly(pubBytes) |
| 104 | if len(pubBytes) == 32 { | ||
| 105 | pubBytes = append([]byte{0x02}, pubBytes...) | ||
| 106 | } | ||
| 107 | |||
| 108 | pub, err := btcec.ParsePubKey(pubBytes) | ||
| 109 | if err != nil { | 104 | if err != nil { |
| 110 | return nil, fmt.Errorf("parse pubkey: %w", err) | 105 | return nil, fmt.Errorf("parse pubkey: %w", err) |
| 111 | } | 106 | } |
| 112 | 107 | ||
| 113 | // ECDH: scalar multiply | 108 | // ECDH: scalar multiply — shared point = priv.D * recipientPub |
| 114 | var point btcec.JacobianPoint | 109 | shared := pub.Point.ScalarMul(priv.D) |
| 115 | pub.AsJacobian(&point) | ||
| 116 | priv.Key.SetByteSlice(priv.Serialize()) | ||
| 117 | btcec.ScalarMultNonConst(&priv.Key, &point, &point) | ||
| 118 | point.ToAffine() | ||
| 119 | 110 | ||
| 120 | // Shared secret is SHA256 of the x-coordinate | 111 | // Shared secret = SHA256(x-coordinate, big-endian, 32 bytes) |
| 121 | xBytes := point.X.Bytes() | 112 | xBytes := shared.XBytes() |
| 122 | shared := sha256.Sum256(xBytes[:]) | 113 | if xBytes == nil { |
| 114 | return nil, fmt.Errorf("ECDH result is point at infinity") | ||
| 115 | } | ||
| 123 | 116 | ||
| 124 | return shared[:], nil | 117 | h := sha256.Sum256(xBytes) |
| 118 | return h[:], nil | ||
| 125 | } | 119 | } |
| 126 | 120 | ||
| 127 | func pkcs7Pad(data []byte, blockSize int) []byte { | 121 | func pkcs7Pad(data []byte, blockSize int) []byte { |
