aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--internal/secp256k1/point.go12
-rw-r--r--nip04.go46
2 files changed, 32 insertions, 26 deletions
diff --git a/internal/secp256k1/point.go b/internal/secp256k1/point.go
index 1def176..5215590 100644
--- a/internal/secp256k1/point.go
+++ b/internal/secp256k1/point.go
@@ -162,6 +162,18 @@ func (p *Point) Negate() *Point {
162 return &Point{x: p.x.Clone(), y: negY, infinity: false} 162 return &Point{x: p.x.Clone(), y: negY, infinity: false}
163} 163}
164 164
165// XBytes returns the x-coordinate as a 32-byte big-endian slice.
166// Returns nil for the point at infinity.
167func (p *Point) XBytes() []byte {
168 if p.infinity {
169 return nil
170 }
171 b := p.x.value.Bytes()
172 out := make([]byte, 32)
173 copy(out[32-len(b):], b)
174 return out
175}
176
165// String returns a readable representation 177// String returns a readable representation
166func (p *Point) String() string { 178func (p *Point) String() string {
167 if p.infinity { 179 if p.infinity {
diff --git a/nip04.go b/nip04.go
index 1f1c245..f979bcd 100644
--- a/nip04.go
+++ b/nip04.go
@@ -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>"
20func (k *Key) NIP04Encrypt(recipientPubHex, plaintext string) (string, error) { 20func (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.
49func (k *Key) NIP04Decrypt(senderPubHex, ciphertext string) (string, error) { 49func (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 )
96func nip04SharedSecret(priv *btcec.PrivateKey, recipientPubHex string) ([]byte, error) { 96func 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
127func pkcs7Pad(data []byte, blockSize int) []byte { 121func pkcs7Pad(data []byte, blockSize int) []byte {