summaryrefslogtreecommitdiffstats
path: root/keys.go
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-02-07 15:20:57 -0800
committerbndw <ben@bdw.to>2026-02-07 15:20:57 -0800
commitd4fd2467d691a69a0ba75348086424b9fb33a297 (patch)
tree51bae6f1579e3248843a01053ccdea336f2730b2 /keys.go
wip
Diffstat (limited to 'keys.go')
-rw-r--r--keys.go217
1 files changed, 217 insertions, 0 deletions
diff --git a/keys.go b/keys.go
new file mode 100644
index 0000000..3a3fb9c
--- /dev/null
+++ b/keys.go
@@ -0,0 +1,217 @@
1package nostr
2
3import (
4 "crypto/rand"
5 "encoding/hex"
6 "fmt"
7 "strings"
8 "time"
9
10 "github.com/btcsuite/btcd/btcec/v2"
11 "github.com/btcsuite/btcd/btcec/v2/schnorr"
12)
13
14// Key represents a Nostr key, which may be a full private key or public-only.
15// Use GenerateKey or ParseKey for private keys, ParsePublicKey for public-only.
16type Key struct {
17 priv *btcec.PrivateKey // nil for public-only keys
18 pub *btcec.PublicKey // always set
19}
20
21// GenerateKey generates a new random private key.
22func GenerateKey() (*Key, error) {
23 var keyBytes [32]byte
24 if _, err := rand.Read(keyBytes[:]); err != nil {
25 return nil, fmt.Errorf("failed to generate random bytes: %w", err)
26 }
27
28 priv, _ := btcec.PrivKeyFromBytes(keyBytes[:])
29 return &Key{
30 priv: priv,
31 pub: priv.PubKey(),
32 }, nil
33}
34
35// ParseKey parses a private key from hex or nsec (bech32) format.
36func ParseKey(s string) (*Key, error) {
37 var privBytes []byte
38
39 if strings.HasPrefix(s, "nsec1") {
40 hrp, data, err := Bech32Decode(s)
41 if err != nil {
42 return nil, fmt.Errorf("invalid nsec: %w", err)
43 }
44 if hrp != "nsec" {
45 return nil, fmt.Errorf("invalid prefix: expected nsec, got %s", hrp)
46 }
47 if len(data) != 32 {
48 return nil, fmt.Errorf("invalid nsec data length: %d", len(data))
49 }
50 privBytes = data
51 } else {
52 var err error
53 privBytes, err = hex.DecodeString(s)
54 if err != nil {
55 return nil, fmt.Errorf("invalid hex: %w", err)
56 }
57 }
58
59 if len(privBytes) != 32 {
60 return nil, fmt.Errorf("private key must be 32 bytes, got %d", len(privBytes))
61 }
62
63 priv, _ := btcec.PrivKeyFromBytes(privBytes)
64 return &Key{
65 priv: priv,
66 pub: priv.PubKey(),
67 }, nil
68}
69
70// ParsePublicKey parses a public key from hex or npub (bech32) format.
71// The returned Key can only verify, not sign.
72func ParsePublicKey(s string) (*Key, error) {
73 var pubBytes []byte
74
75 if strings.HasPrefix(s, "npub1") {
76 hrp, data, err := Bech32Decode(s)
77 if err != nil {
78 return nil, fmt.Errorf("invalid npub: %w", err)
79 }
80 if hrp != "npub" {
81 return nil, fmt.Errorf("invalid prefix: expected npub, got %s", hrp)
82 }
83 if len(data) != 32 {
84 return nil, fmt.Errorf("invalid npub data length: %d", len(data))
85 }
86 pubBytes = data
87 } else {
88 var err error
89 pubBytes, err = hex.DecodeString(s)
90 if err != nil {
91 return nil, fmt.Errorf("invalid hex: %w", err)
92 }
93 }
94
95 if len(pubBytes) != 32 {
96 return nil, fmt.Errorf("public key must be 32 bytes, got %d", len(pubBytes))
97 }
98
99 pub, err := schnorr.ParsePubKey(pubBytes)
100 if err != nil {
101 return nil, fmt.Errorf("invalid public key: %w", err)
102 }
103
104 return &Key{
105 priv: nil,
106 pub: pub,
107 }, nil
108}
109
110// CanSign returns true if this key can sign events (has private key).
111func (k *Key) CanSign() bool {
112 return k.priv != nil
113}
114
115// Public returns the public key as a 64-character hex string.
116func (k *Key) Public() string {
117 return hex.EncodeToString(schnorr.SerializePubKey(k.pub))
118}
119
120// Private returns the private key as a 64-character hex string.
121// Returns empty string if this is a public-only key.
122func (k *Key) Private() string {
123 if k.priv == nil {
124 return ""
125 }
126 return hex.EncodeToString(k.priv.Serialize())
127}
128
129// Npub returns the public key in bech32 npub format.
130func (k *Key) Npub() string {
131 pubBytes := schnorr.SerializePubKey(k.pub)
132 npub, _ := Bech32Encode("npub", pubBytes)
133 return npub
134}
135
136// Nsec returns the private key in bech32 nsec format.
137// Returns empty string if this is a public-only key.
138func (k *Key) Nsec() string {
139 if k.priv == nil {
140 return ""
141 }
142 nsec, _ := Bech32Encode("nsec", k.priv.Serialize())
143 return nsec
144}
145
146// Sign signs the event with this key.
147// Sets the PubKey, ID, and Sig fields on the event.
148// Returns an error if this is a public-only key.
149func (k *Key) Sign(event *Event) error {
150 if k.priv == nil {
151 return fmt.Errorf("cannot sign: public-only key")
152 }
153
154 // Set public key
155 event.PubKey = k.Public()
156
157 if event.CreatedAt == 0 {
158 event.CreatedAt = time.Now().Unix()
159 }
160
161 // Compute ID
162 event.SetID()
163
164 // Hash the ID for signing
165 idBytes, err := hex.DecodeString(event.ID)
166 if err != nil {
167 return fmt.Errorf("failed to decode event ID: %w", err)
168 }
169
170 // Sign with Schnorr
171 sig, err := schnorr.Sign(k.priv, idBytes)
172 if err != nil {
173 return fmt.Errorf("failed to sign event: %w", err)
174 }
175
176 event.Sig = hex.EncodeToString(sig.Serialize())
177 return nil
178}
179
180// Verify verifies the event signature.
181// Returns true if the signature is valid, false otherwise.
182func (e *Event) Verify() bool {
183 // Verify ID first
184 if !e.CheckID() {
185 return false
186 }
187
188 // Decode public key
189 pubKeyBytes, err := hex.DecodeString(e.PubKey)
190 if err != nil || len(pubKeyBytes) != 32 {
191 return false
192 }
193
194 pubKey, err := schnorr.ParsePubKey(pubKeyBytes)
195 if err != nil {
196 return false
197 }
198
199 // Decode signature
200 sigBytes, err := hex.DecodeString(e.Sig)
201 if err != nil {
202 return false
203 }
204
205 sig, err := schnorr.ParseSignature(sigBytes)
206 if err != nil {
207 return false
208 }
209
210 // Decode ID (message hash)
211 idBytes, err := hex.DecodeString(e.ID)
212 if err != nil {
213 return false
214 }
215
216 return sig.Verify(idBytes, pubKey)
217}