aboutsummaryrefslogtreecommitdiffstats
path: root/bech32.go
diff options
context:
space:
mode:
authorClawd <ai@clawd.bot>2026-02-19 21:24:07 -0800
committerClawd <ai@clawd.bot>2026-02-19 21:24:07 -0800
commit5778c288e1f61af9dee23967c62871c4c31b0feb (patch)
tree5838473fc3be94d64176a1b6303def7da933b03e /bech32.go
parent6f3902d09504838e4e486825f073df971c412595 (diff)
Add bech32 encoding (npub/nsec for Nostr)
Diffstat (limited to 'bech32.go')
-rw-r--r--bech32.go212
1 files changed, 212 insertions, 0 deletions
diff --git a/bech32.go b/bech32.go
new file mode 100644
index 0000000..835156d
--- /dev/null
+++ b/bech32.go
@@ -0,0 +1,212 @@
1package secp256k1
2
3import (
4 "fmt"
5 "math/big"
6 "strings"
7)
8
9// Bech32 alphabet (no 1, b, i, o to avoid confusion)
10const bech32Alphabet = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
11
12// bech32Polymod computes the Bech32 checksum
13func bech32Polymod(values []int) int {
14 gen := []int{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}
15 chk := 1
16 for _, v := range values {
17 top := chk >> 25
18 chk = (chk&0x1ffffff)<<5 ^ v
19 for i := 0; i < 5; i++ {
20 if (top>>i)&1 == 1 {
21 chk ^= gen[i]
22 }
23 }
24 }
25 return chk
26}
27
28// bech32HRPExpand expands the human-readable part for checksum
29func bech32HRPExpand(hrp string) []int {
30 result := make([]int, len(hrp)*2+1)
31 for i, c := range hrp {
32 result[i] = int(c) >> 5
33 }
34 result[len(hrp)] = 0
35 for i, c := range hrp {
36 result[len(hrp)+1+i] = int(c) & 31
37 }
38 return result
39}
40
41// bech32CreateChecksum creates a 6-character checksum
42func bech32CreateChecksum(hrp string, data []int) []int {
43 values := append(bech32HRPExpand(hrp), data...)
44 values = append(values, []int{0, 0, 0, 0, 0, 0}...)
45 polymod := bech32Polymod(values) ^ 1
46 checksum := make([]int, 6)
47 for i := 0; i < 6; i++ {
48 checksum[i] = (polymod >> (5 * (5 - i))) & 31
49 }
50 return checksum
51}
52
53// bech32VerifyChecksum verifies a bech32 checksum
54func bech32VerifyChecksum(hrp string, data []int) bool {
55 return bech32Polymod(append(bech32HRPExpand(hrp), data...)) == 1
56}
57
58// convertBits converts between bit groups
59func convertBits(data []byte, fromBits, toBits int, pad bool) ([]int, error) {
60 acc := 0
61 bits := 0
62 result := []int{}
63 maxv := (1 << toBits) - 1
64
65 for _, value := range data {
66 acc = (acc << fromBits) | int(value)
67 bits += fromBits
68 for bits >= toBits {
69 bits -= toBits
70 result = append(result, (acc>>bits)&maxv)
71 }
72 }
73
74 if pad {
75 if bits > 0 {
76 result = append(result, (acc<<(toBits-bits))&maxv)
77 }
78 } else if bits >= fromBits || ((acc<<(toBits-bits))&maxv) != 0 {
79 return nil, fmt.Errorf("invalid padding")
80 }
81
82 return result, nil
83}
84
85// Bech32Encode encodes data with a human-readable prefix
86func Bech32Encode(hrp string, data []byte) (string, error) {
87 // Convert 8-bit bytes to 5-bit groups
88 values, err := convertBits(data, 8, 5, true)
89 if err != nil {
90 return "", err
91 }
92
93 // Add checksum
94 checksum := bech32CreateChecksum(hrp, values)
95 combined := append(values, checksum...)
96
97 // Build result string
98 var result strings.Builder
99 result.WriteString(hrp)
100 result.WriteByte('1')
101 for _, v := range combined {
102 result.WriteByte(bech32Alphabet[v])
103 }
104
105 return result.String(), nil
106}
107
108// Bech32Decode decodes a bech32 string
109func Bech32Decode(s string) (string, []byte, error) {
110 // Find separator
111 pos := strings.LastIndex(s, "1")
112 if pos < 1 || pos+7 > len(s) {
113 return "", nil, fmt.Errorf("invalid bech32: separator position")
114 }
115
116 // Split HRP and data
117 hrp := strings.ToLower(s[:pos])
118 dataStr := strings.ToLower(s[pos+1:])
119
120 // Decode data characters
121 data := make([]int, len(dataStr))
122 for i, c := range dataStr {
123 idx := strings.IndexRune(bech32Alphabet, c)
124 if idx < 0 {
125 return "", nil, fmt.Errorf("invalid bech32: character %c", c)
126 }
127 data[i] = idx
128 }
129
130 // Verify checksum
131 if !bech32VerifyChecksum(hrp, data) {
132 return "", nil, fmt.Errorf("invalid bech32: checksum")
133 }
134
135 // Remove checksum and convert back to 8-bit
136 values := data[:len(data)-6]
137 bytes, err := convertBitsToBytes(values)
138 if err != nil {
139 return "", nil, err
140 }
141
142 return hrp, bytes, nil
143}
144
145// convertBitsToBytes converts 5-bit groups back to bytes
146func convertBitsToBytes(data []int) ([]byte, error) {
147 acc := 0
148 bits := 0
149 result := []byte{}
150
151 for _, value := range data {
152 acc = (acc << 5) | value
153 bits += 5
154 for bits >= 8 {
155 bits -= 8
156 result = append(result, byte((acc>>bits)&0xff))
157 }
158 }
159
160 return result, nil
161}
162
163// === Nostr-specific helpers ===
164
165// Nsec returns the private key as a Nostr nsec string
166func (priv *PrivateKey) Nsec() string {
167 encoded, _ := Bech32Encode("nsec", priv.Bytes())
168 return encoded
169}
170
171// Npub returns the public key as a Nostr npub string (x-only)
172func (pub *PublicKey) Npub() string {
173 encoded, _ := Bech32Encode("npub", pub.XOnlyBytes())
174 return encoded
175}
176
177// PrivateKeyFromNsec parses an nsec string
178func PrivateKeyFromNsec(nsec string) (*PrivateKey, error) {
179 hrp, data, err := Bech32Decode(nsec)
180 if err != nil {
181 return nil, fmt.Errorf("invalid nsec: %w", err)
182 }
183 if hrp != "nsec" {
184 return nil, fmt.Errorf("invalid nsec: wrong prefix %q", hrp)
185 }
186 if len(data) != 32 {
187 return nil, fmt.Errorf("invalid nsec: wrong length %d", len(data))
188 }
189 return NewPrivateKeyFromBytes(data)
190}
191
192// PublicKeyFromNpub parses an npub string
193func PublicKeyFromNpub(npub string) (*PublicKey, error) {
194 hrp, data, err := Bech32Decode(npub)
195 if err != nil {
196 return nil, fmt.Errorf("invalid npub: %w", err)
197 }
198 if hrp != "npub" {
199 return nil, fmt.Errorf("invalid npub: wrong prefix %q", hrp)
200 }
201 if len(data) != 32 {
202 return nil, fmt.Errorf("invalid npub: wrong length %d", len(data))
203 }
204
205 // Lift x-coordinate to full point
206 point, err := liftX(new(big.Int).SetBytes(data))
207 if err != nil {
208 return nil, fmt.Errorf("invalid npub: %w", err)
209 }
210
211 return &PublicKey{Point: point}, nil
212}