bech32.mx raw
1 package helpers
2
3 import "git.smesh.lol/moxie/pkg/mxutil"
4
5 // Bech32 encoding/decoding for NIP-19.
6 // Implements bech32 (BIP-173) without external deps.
7
8 const bech32Charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
9
10 // Bech32Encode encodes data with the given human-readable part. The output
11 // size is known once the checksum exists, so it is presized and written
12 // through a cursor: `buf | "1"` had a non-empty left side, and concat then
13 // allocated a fresh exact-size buffer, leaving the first value push no room.
14 func Bech32Encode(hrp string, data []byte) (s string) {
15 values := bytesToBase32(data)
16 checksum := bech32Checksum(hrp, values)
17 values = values | checksum
18
19 buf := []byte{:len(hrp) + 1 + len(values)}
20 j := 0
21 for i := 0; i < len(hrp); i++ {
22 buf[j] = hrp[i]
23 j++
24 }
25 buf[j] = '1'
26 j++
27 for _, v := range values {
28 buf[j] = bech32Charset[v]
29 j++
30 }
31 return string(buf)
32 }
33
34 // Bech32Decode decodes a bech32 string. Returns hrp and data bytes.
35 func Bech32Decode(s string) (hrp string, data []byte) {
36 // Find separator.
37 pos := -1
38 for i := len(s) - 1; i >= 0; i-- {
39 if s[i] == '1' {
40 pos = i
41 break
42 }
43 }
44 if pos < 1 || pos+7 > len(s) {
45 return "", nil
46 }
47
48 hrp := s[:pos]
49 dataStr := s[pos+1:]
50
51 values := []byte{:len(dataStr)}
52 for i := 0; i < len(dataStr); i++ {
53 idx := charsetIndex(dataStr[i])
54 if idx < 0 {
55 return "", nil
56 }
57 values[i] = byte(idx)
58 }
59
60 if !bech32Verify(hrp, values) {
61 return "", nil
62 }
63
64 // Strip checksum (last 6 chars).
65 values = values[:len(values)-6]
66 data := base32ToBytes(values)
67 return hrp, data
68 }
69
70 // NIP-19 helpers.
71
72 // EncodeNpub encodes a 32-byte public key as npub.
73 func EncodeNpub(pubkey []byte) (s string) {
74 return Bech32Encode("npub", pubkey)
75 }
76
77 // EncodeNsec encodes a 32-byte secret key as nsec.
78 func EncodeNsec(seckey []byte) (s string) {
79 return Bech32Encode("nsec", seckey)
80 }
81
82 // EncodeNote encodes a 32-byte event ID as note.
83 func EncodeNote(eventID []byte) (s string) {
84 return Bech32Encode("note", eventID)
85 }
86
87 // EncodeNevent encodes an event reference as nevent (NIP-19 TLV).
88 // The TLV length is a function of its parts, so it is presized exactly: push
89 // does not grow, and the `data | idBytes` step returns a full slice.
90 func EncodeNevent(id string, relays []string, author string) (s string) {
91 idBytes := HexDecode(id)
92 ab := HexDecode(author)
93 hasID := len(idBytes) == 32
94 hasAuthor := len(ab) == 32
95 n := 0
96 if hasID {
97 n += 34
98 }
99 for _, r := range relays {
100 n += 2 + len(r)
101 }
102 if hasAuthor {
103 n += 34
104 }
105 data := []byte{:n}
106 j := 0
107 if hasID {
108 data[j] = 0
109 data[j+1] = 32
110 j += 2
111 for _, b := range idBytes {
112 data[j] = b
113 j++
114 }
115 }
116 for _, r := range relays {
117 data[j] = 1
118 data[j+1] = byte(len(r))
119 j += 2
120 for k := 0; k < len(r); k++ {
121 data[j] = r[k]
122 j++
123 }
124 }
125 if hasAuthor {
126 data[j] = 2
127 data[j+1] = 32
128 j += 2
129 for _, b := range ab {
130 data[j] = b
131 j++
132 }
133 }
134 return Bech32Encode("nevent", data[:j])
135 }
136
137 // DecodeNpub decodes an npub string to 32 bytes.
138 func DecodeNpub(s string) (buf []byte) {
139 hrp, data := Bech32Decode(s)
140 if hrp != "npub" || len(data) != 32 {
141 return nil
142 }
143 return data
144 }
145
146 // DecodeNsec decodes an nsec string to 32 bytes.
147 func DecodeNsec(s string) (buf []byte) {
148 hrp, data := Bech32Decode(s)
149 if hrp != "nsec" || len(data) != 32 {
150 return nil
151 }
152 return data
153 }
154
155 // DecodeNote decodes a note string to 32 bytes.
156 func DecodeNote(s string) (buf []byte) {
157 hrp, data := Bech32Decode(s)
158 if hrp != "note" || len(data) != 32 {
159 return nil
160 }
161 return data
162 }
163
164 // Nevent holds decoded nevent TLV data (NIP-19).
165 type Nevent struct {
166 ID string // hex event ID
167 Relays []string // optional relay hints
168 Author string // hex pubkey (optional)
169 }
170
171 // DecodeNevent decodes a nevent1... bech32 string (TLV format).
172 func DecodeNevent(s string) (n *Nevent) {
173 hrp, data := Bech32Decode(s)
174 if hrp != "nevent" {
175 return nil
176 }
177 result := &Nevent{}
178 i := 0
179 for i+2 <= len(data) {
180 t := data[i]
181 l := int32(data[i+1])
182 i += 2
183 if i+l > len(data) {
184 break
185 }
186 v := data[i : i+l]
187 i += l
188 switch t {
189 case 0:
190 if l == 32 {
191 result.ID = HexEncode(v)
192 }
193 case 1:
194 result.Relays = mxutil.Ensure(result.Relays, 1)
195 result.Relays = push(result.Relays, string(v))
196 case 2:
197 if l == 32 {
198 result.Author = HexEncode(v)
199 }
200 }
201 }
202 if result.ID == "" {
203 return nil
204 }
205 return result
206 }
207
208 // Nprofile holds decoded nprofile TLV data (NIP-19).
209 type Nprofile struct {
210 Pubkey string // hex pubkey
211 Relays []string // optional relay hints
212 }
213
214 // DecodeNprofile decodes an nprofile1... bech32 string (TLV format).
215 func DecodeNprofile(s string) (n *Nprofile) {
216 hrp, data := Bech32Decode(s)
217 if hrp != "nprofile" {
218 return nil
219 }
220 result := &Nprofile{}
221 i := 0
222 for i+2 <= len(data) {
223 t := data[i]
224 l := int32(data[i+1])
225 i += 2
226 if i+l > len(data) {
227 break
228 }
229 v := data[i : i+l]
230 i += l
231 switch t {
232 case 0:
233 if l == 32 {
234 result.Pubkey = HexEncode(v)
235 }
236 case 1:
237 result.Relays = mxutil.Ensure(result.Relays, 1)
238 result.Relays = push(result.Relays, string(v))
239 }
240 }
241 if result.Pubkey == "" {
242 return nil
243 }
244 return result
245 }
246
247 // Naddr holds decoded naddr TLV data (NIP-19).
248 type Naddr struct {
249 Kind uint32
250 Pubkey string // hex pubkey
251 D string // d-tag identifier
252 Relays []string // optional relay hints
253 }
254
255 // DecodeNaddr decodes an naddr1... bech32 string (TLV format).
256 func DecodeNaddr(s string) (n *Naddr) {
257 hrp, data := Bech32Decode(s)
258 if hrp != "naddr" {
259 return nil
260 }
261 result := &Naddr{}
262 i := 0
263 for i+2 <= len(data) {
264 t := data[i]
265 l := int32(data[i+1])
266 i += 2
267 if i+l > len(data) {
268 break
269 }
270 v := data[i : i+l]
271 i += l
272 switch t {
273 case 0:
274 result.D = string(v)
275 case 1:
276 result.Relays = mxutil.Ensure(result.Relays, 1)
277 result.Relays = push(result.Relays, string(v))
278 case 2:
279 if l == 32 {
280 result.Pubkey = HexEncode(v)
281 }
282 case 3:
283 if l == 4 {
284 result.Kind = uint32(v[0])<<24 | uint32(v[1])<<16 | uint32(v[2])<<8 | uint32(v[3])
285 }
286 }
287 }
288 if result.Pubkey == "" || result.Kind == 0 {
289 return nil
290 }
291 return result
292 }
293
294 // NaddrCoordKey returns the canonical "kind:pubkey:d" lookup key.
295 func NaddrCoordKey(kind uint32, pubkey, d string) (s string) {
296 k := ""
297 n := kind
298 if n == 0 {
299 k = "0"
300 } else {
301 for n > 0 {
302 k = string([]byte{byte('0' + n%10)}) | k
303 n /= 10
304 }
305 }
306 return k | ":" | pubkey | ":" | d
307 }
308
309 // PubkeyShort returns first 8 chars of hex pubkey.
310 func PubkeyShort(pubkey string) (s string) {
311 if len(pubkey) >= 8 {
312 return pubkey[:8]
313 }
314 return pubkey
315 }
316
317 // Internal bech32 functions.
318
319 // bytesToBase32 converts 8-bit bytes to 5-bit groups. The output length is a
320 // function of the input length, so it is presized and written through a
321 // cursor: push does not grow, and a nil sink stops at four values.
322 func bytesToBase32(data []byte) (buf []byte) {
323 out := []byte{:(len(data)*8 + 4) / 5}
324 j := 0
325 acc := 0
326 bits := 0
327 for _, b := range data {
328 acc = (acc << 8) | int32(b)
329 bits += 8
330 for bits >= 5 {
331 bits -= 5
332 out[j] = byte((acc >> bits) & 0x1f)
333 j++
334 }
335 acc &= (1 << uint32(bits)) - 1
336 }
337 if bits > 0 {
338 out[j] = byte((acc << (5 - bits)) & 0x1f)
339 j++
340 }
341 return out[:j]
342 }
343
344 func base32ToBytes(data []byte) (buf []byte) {
345 out := []byte{:len(data) * 5 / 8}
346 j := 0
347 acc := 0
348 bits := 0
349 for _, v := range data {
350 acc = (acc << 5) | int32(v)
351 bits += 5
352 for bits >= 8 {
353 bits -= 8
354 out[j] = byte((acc >> bits) & 0xff)
355 j++
356 }
357 acc &= (1 << uint32(bits)) - 1
358 }
359 return out[:j]
360 }
361
362 func bech32Polymod(values []byte) (n uint32) {
363 gen := [5]uint32{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}
364 chk := uint32(1)
365 for _, v := range values {
366 b := chk >> 25
367 chk = ((chk & 0x1ffffff) << 5) ^ uint32(v)
368 for i := 0; i < 5; i++ {
369 if (b>>uint32(i))&1 == 1 {
370 chk ^= gen[i]
371 }
372 }
373 }
374 return chk
375 }
376
377 func bech32HRPExpand(hrp string) (buf []byte) {
378 out := []byte{:0:len(hrp)*2+1}
379 for i := 0; i < len(hrp); i++ {
380 out = push(out, byte(hrp[i]>>5))
381 }
382 out = push(out, 0)
383 for i := 0; i < len(hrp); i++ {
384 out = push(out, byte(hrp[i]&0x1f))
385 }
386 return out
387 }
388
389 func bech32Checksum(hrp string, data []byte) (buf []byte) {
390 exp := bech32HRPExpand(hrp)
391 values := []byte{:len(exp) + len(data) + 6}
392 j := 0
393 for _, b := range exp {
394 values[j] = b
395 j++
396 }
397 for _, b := range data {
398 values[j] = b
399 j++
400 }
401 // Six zero bytes pad the checksum input (BIP-173).
402 for i := 0; i < 6; i++ {
403 values[j] = 0
404 j++
405 }
406 polymod := bech32Polymod(values) ^ 1
407 out := []byte{:6}
408 for i := 0; i < 6; i++ {
409 out[i] = byte((polymod >> (5 * (5 - uint32(i)))) & 0x1f)
410 }
411 return out
412 }
413
414 func bech32Verify(hrp string, data []byte) (ok bool) {
415 values := bech32HRPExpand(hrp) | data
416 return bech32Polymod(values) == 1
417 }
418
419 func charsetIndex(c byte) (n int32) {
420 for i := 0; i < len(bech32Charset); i++ {
421 if bech32Charset[i] == c {
422 return i
423 }
424 }
425 return -1
426 }
427