nwc.mx raw
1 package main
2
3 import (
4 "git.smesh.lol/nostr/pkg/crypto/nip44"
5 "git.smesh.lol/musiquay/web/common/helpers"
6 "git.smesh.lol/musiquay/web/common/jsbridge/ext"
7 "git.smesh.lol/musiquay/web/common/jsbridge/schnorr"
8 "git.smesh.lol/musiquay/web/common/jsbridge/subtle"
9 "git.smesh.lol/nostr/pkg/core"
10 nosig "git.smesh.lol/nostr/pkg/core/sig"
11 )
12
13 // NWC: Nostr Wallet Connect (NIP-47) connection management.
14 // Connections stored in vault storage alongside signSt.identities.
15 // Each connection has its own keypair (walletPubkey + ephemeral secret).
16
17 type nwcConn struct {
18 Alias string
19 WalletPK string // wallet service pubkey
20 RelayURL string
21 Secret string // our NIP-47 ephemeral secret, hex
22 }
23
24
25 const nwcStorageKey = "musiquay-nwc"
26
27 func loadNWC() {
28 ext.StorageGet(nwcStorageKey, func(data string) {
29 if data != "" {
30 parseNWCConns(data)
31 }
32 })
33 }
34
35 func parseNWCConns(s string) {
36 signSt.walletConns = nil
37 i := 0
38 for i < len(s) && s[i] != '[' {
39 i++
40 }
41 i++
42 for i < len(s) {
43 for i < len(s) && s[i] != '{' && s[i] != ']' {
44 i++
45 }
46 if i >= len(s) || s[i] == ']' {
47 break
48 }
49 end := i + 1
50 depth := 1
51 for end < len(s) && depth > 0 {
52 if s[end] == '{' {
53 depth++
54 } else if s[end] == '}' {
55 depth--
56 } else if s[end] == '"' {
57 end++
58 for end < len(s) && s[end] != '"' {
59 if s[end] == '\\' {
60 end++
61 }
62 end++
63 }
64 }
65 end++
66 }
67 obj := s[i:end]
68 kAlias := string(push([]byte(nil), 'a', 'l', 'i', 'a', 's'))
69 kWalletPK := string(push([]byte(nil), 'w', 'a', 'l', 'l', 'e', 't', 'P', 'K'))
70 kRelay := string(push([]byte(nil), 'r', 'e', 'l', 'a', 'y'))
71 kSecret := string(push([]byte(nil), 's', 'e', 'c', 'r', 'e', 't'))
72 signSt.walletConns = push(signSt.walletConns, nwcConn{
73 Alias: helpers.JsonGetString(obj, kAlias),
74 WalletPK: helpers.JsonGetString(obj, kWalletPK),
75 RelayURL: helpers.JsonGetString(obj, kRelay),
76 Secret: helpers.JsonGetString(obj, kSecret),
77 })
78 i = end
79 }
80 }
81
82 func saveNWC() {
83 b := push([]byte(nil), '[')
84 for i, c := range signSt.walletConns {
85 if i > 0 {
86 b = b | ","
87 }
88 b = b | "{\"alias\":"
89 b = b | helpers.JsonString(c.Alias)
90 b = b | ",\"walletPK\":"
91 b = b | helpers.JsonString(c.WalletPK)
92 b = b | ",\"relay\":"
93 b = b | helpers.JsonString(c.RelayURL)
94 b = b | ",\"secret\":"
95 b = b | helpers.JsonString(c.Secret)
96 b = b | "}"
97 }
98 b = b | "]"
99 ext.StorageSet(nwcStorageKey, string(b))
100 }
101
102 func nwcList() (s string) {
103 b := push([]byte(nil), '{', '"', 'r', 'e', 's', 'u', 'l', 't', '"', ':', '[')
104 for i, c := range signSt.walletConns {
105 if i > 0 {
106 b = b | ","
107 }
108 b = b | "{\"alias\":"
109 b = b | helpers.JsonString(c.Alias)
110 b = b | ",\"walletPK\":"
111 b = b | helpers.JsonString(c.WalletPK)
112 b = b | ",\"relay\":"
113 b = b | helpers.JsonString(c.RelayURL)
114 b = b | "}"
115 }
116 return string(b | "]}")
117 }
118
119 func nwcAdd(paramsJSON string) (s string) {
120 kURI := string(push([]byte(nil), 'u', 'r', 'i'))
121 kAlias := string(push([]byte(nil), 'a', 'l', 'i', 'a', 's'))
122 uri := helpers.JsonGetString(paramsJSON, kURI)
123 alias := helpers.JsonGetString(paramsJSON, kAlias)
124 if uri == "" {
125 return jsonErr(string(push([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'u', 'r', 'i')))
126 }
127 walletPK, relay, secret := parseNWCURI(uri)
128 if walletPK == "" || relay == "" || secret == "" {
129 return jsonErr(string(push([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 'u', 'r', 'i')))
130 }
131 signSt.walletConns = push(signSt.walletConns, nwcConn{Alias: alias, WalletPK: walletPK, RelayURL: relay, Secret: secret})
132 saveNWC()
133 return jsonTrue()
134 }
135
136 func nwcRemove(paramsJSON string) (s string) {
137 kWalletPK := string(push([]byte(nil), 'w', 'a', 'l', 'l', 'e', 't', 'P', 'K'))
138 walletPK := helpers.JsonGetString(paramsJSON, kWalletPK)
139 for i, c := range signSt.walletConns {
140 if c.WalletPK == walletPK {
141 signSt.walletConns = signSt.walletConns[:i] | signSt.walletConns[i+1:]
142 saveNWC()
143 return jsonTrue()
144 }
145 }
146 return jsonFalse()
147 }
148
149 func nwcBuildRequest(paramsJSON string) (s string) {
150 kWalletPKB := string(push([]byte(nil), 'w', 'a', 'l', 'l', 'e', 't', 'P', 'K'))
151 kMethod := string(push([]byte(nil), 'm', 'e', 't', 'h', 'o', 'd'))
152 kParams := string(push([]byte(nil), 'p', 'a', 'r', 'a', 'm', 's'))
153 walletPK := helpers.JsonGetString(paramsJSON, kWalletPKB)
154 method := helpers.JsonGetString(paramsJSON, kMethod)
155 reqParams := helpers.JsonGetValue(paramsJSON, kParams)
156 if walletPK == "" || method == "" {
157 return jsonErr(string(push([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'p', 'a', 'r', 'a', 'm', 's')))
158 }
159 var conn *nwcConn
160 for i := range signSt.walletConns {
161 if signSt.walletConns[i].WalletPK == walletPK {
162 conn = &signSt.walletConns[i]
163 break
164 }
165 }
166 if conn == nil {
167 return jsonErr(string(push([]byte(nil), 'c', 'o', 'n', 'n', 'e', 'c', 't', 'i', 'o', 'n', ' ', 'n', 'o', 't', ' ', 'f', 'o', 'u', 'n', 'd')))
168 }
169 sk32, ok := helpers.HexDecode32(conn.Secret)
170 if !ok {
171 return jsonErr(string(push([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 's', 'e', 'c', 'r', 'e', 't')))
172 }
173 pk32, ok := helpers.HexDecode32(walletPK)
174 if !ok {
175 return jsonErr(string(push([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 'w', 'a', 'l', 'l', 'e', 't', 'P', 'K')))
176 }
177 convKey, ok := nip44.ConversationKey(sk32, pk32)
178 if !ok {
179 return jsonErr(string(push([]byte(nil), 'k', 'e', 'y', ' ', 'd', 'e', 'r', 'i', 'v', 'a', 't', 'i', 'o', 'n', ' ', 'f', 'a', 'i', 'l', 'e', 'd')))
180 }
181 reqID := randomHex(32)
182 reqIDJSON := helpers.JsonString(reqID)
183 methodJSON := helpers.JsonString(method)
184 bodyB := push([]byte(nil), '{', '"', 'i', 'd', '"', ':')
185 bodyB = bodyB | reqIDJSON
186 bodyB = bodyB | ",\"method\":"
187 bodyB = bodyB | methodJSON
188 bodyB = bodyB | ",\"params\":"
189 bodyB = bodyB | reqParams
190 bodyB = bodyB | "}"
191 body := string(bodyB)
192 var nonce [32]byte
193 subtle.RandomBytes(nonce[:])
194 encrypted := nip44.Encrypt(body, convKey, nonce)
195 ourPKBytes, ok := schnorr.PubKeyFromSecKey(sk32[:])
196 if !ok {
197 return jsonErr(string(push([]byte(nil), 'p', 'u', 'b', 'k', 'e', 'y', ' ', 'f', 'a', 'i', 'l', 'e', 'd')))
198 }
199 ev := &nostr.Event{
200 Kind: 23194,
201 Content: encrypted,
202 CreatedAt: 0,
203 Tags: [][]string{[]string{"p", walletPK}},
204 PubKey: helpers.HexEncode(ourPKBytes),
205 }
206 (*nosig.Event)(ev).ComputeID()
207 idBytes := helpers.HexDecode(ev.ID)
208 var aux [32]byte
209 subtle.RandomBytes(aux[:])
210 sig, ok := schnorr.SignSchnorr(sk32[:], idBytes, aux[:])
211 if !ok {
212 return jsonErr(string(push([]byte(nil), 's', 'i', 'g', 'n', ' ', 'f', 'a', 'i', 'l', 'e', 'd')))
213 }
214 ev.Sig = helpers.HexEncode(sig)
215 rb := push([]byte(nil), '{', '"', 'r', 'e', 's', 'u', 'l', 't', '"', ':')
216 rb = rb | ev.ToJSON()
217 rb = rb | ",\"reqID\":"
218 rb = rb | reqIDJSON
219 rb = rb | "}"
220 return string(rb)
221 }
222
223 func nwcParseResponse(paramsJSON string) (s string) {
224 kWalletPKR := string(push([]byte(nil), 'w', 'a', 'l', 'l', 'e', 't', 'P', 'K'))
225 kContent := string(push([]byte(nil), 'c', 'o', 'n', 't', 'e', 'n', 't'))
226 walletPK := helpers.JsonGetString(paramsJSON, kWalletPKR)
227 content := helpers.JsonGetString(paramsJSON, kContent)
228 if walletPK == "" || content == "" {
229 return jsonErr(string(push([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'p', 'a', 'r', 'a', 'm', 's')))
230 }
231 var conn *nwcConn
232 for i := range signSt.walletConns {
233 if signSt.walletConns[i].WalletPK == walletPK {
234 conn = &signSt.walletConns[i]
235 break
236 }
237 }
238 if conn == nil {
239 return jsonErr(string(push([]byte(nil), 'c', 'o', 'n', 'n', 'e', 'c', 't', 'i', 'o', 'n', ' ', 'n', 'o', 't', ' ', 'f', 'o', 'u', 'n', 'd')))
240 }
241 sk32, ok := helpers.HexDecode32(conn.Secret)
242 if !ok {
243 return jsonErr(string(push([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 's', 'e', 'c', 'r', 'e', 't')))
244 }
245 pk32, ok := helpers.HexDecode32(walletPK)
246 if !ok {
247 return jsonErr(string(push([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 'w', 'a', 'l', 'l', 'e', 't', 'P', 'K')))
248 }
249 convKey, ok := nip44.ConversationKey(sk32, pk32)
250 if !ok {
251 return jsonErr(string(push([]byte(nil), 'k', 'e', 'y', ' ', 'd', 'e', 'r', 'i', 'v', 'a', 't', 'i', 'o', 'n', ' ', 'f', 'a', 'i', 'l', 'e', 'd')))
252 }
253 pt, ok := nip44.Decrypt(content, convKey)
254 if !ok {
255 return jsonErr(string(push([]byte(nil), 'd', 'e', 'c', 'r', 'y', 'p', 't', ' ', 'f', 'a', 'i', 'l', 'e', 'd')))
256 }
257 return jsonResult(pt)
258 }
259
260 // parseNWCURI parses nostr+walletconnect://<walletPK>?relay=<url>&secret=<hex>
261 func parseNWCURI(uri string) (walletPK, relay, secret string) {
262 scheme := string(push([]byte(nil), 'n', 'o', 's', 't', 'r', '+', 'w', 'a', 'l', 'l', 'e', 't', 'c', 'o', 'n', 'n', 'e', 'c', 't', ':', '/', '/'))
263 if len(uri) < len(scheme) || uri[:len(scheme)] != scheme {
264 return "", "", ""
265 }
266 rest := uri[len(scheme):]
267 // Find the '?' separating walletPK from query params
268 qIdx := -1
269 for i := 0; i < len(rest); i++ {
270 if rest[i] == '?' {
271 qIdx = i
272 break
273 }
274 }
275 if qIdx < 0 {
276 return "", "", ""
277 }
278 walletPK = rest[:qIdx]
279 query := rest[qIdx+1:]
280 // Parse key=value pairs separated by '&'
281 kRelay := string(push([]byte(nil), 'r', 'e', 'l', 'a', 'y'))
282 kSecret := string(push([]byte(nil), 's', 'e', 'c', 'r', 'e', 't'))
283 start := 0
284 for start <= len(query) {
285 end := start
286 for end < len(query) && query[end] != '&' {
287 end++
288 }
289 pair := query[start:end]
290 eqIdx := -1
291 for j := 0; j < len(pair); j++ {
292 if pair[j] == '=' {
293 eqIdx = j
294 break
295 }
296 }
297 if eqIdx > 0 {
298 k := pair[:eqIdx]
299 v := pair[eqIdx+1:]
300 if k == kRelay {
301 relay = v
302 } else if k == kSecret {
303 secret = v
304 }
305 }
306 start = end + 1
307 }
308 if walletPK == "" || relay == "" || secret == "" {
309 return "", "", ""
310 }
311 return walletPK, relay, secret
312 }
313
314 func randomHex(n int32) (s string) {
315 b := []byte{:n}
316 subtle.RandomBytes(b)
317 return helpers.HexEncode(b)
318 }
319
320