management.mx raw
1 package main
2
3 import (
4 "git.smesh.lol/smesh/web/common/helpers"
5 "git.smesh.lol/smesh/web/common/jsbridge/ext"
6 "git.smesh.lol/smesh/web/common/jsbridge/schnorr"
7 "git.smesh.lol/smesh/web/common/jsbridge/subtle"
8 )
9
10 // Management API: vault lifecycle, identity CRUD, export/import.
11
12 func mgmtGetVaultStatus() (s string) {
13 if !vaultExists {
14 return jsonResult(string(append([]byte(nil), '"', 'n', 'o', 'n', 'e', '"')))
15 }
16 if vaultOpen {
17 return jsonResult(string(append([]byte(nil), '"', 'u', 'n', 'l', 'o', 'c', 'k', 'e', 'd', '"')))
18 }
19 return jsonResult(string(append([]byte(nil), '"', 'l', 'o', 'c', 'k', 'e', 'd', '"')))
20 }
21
22 func mgmtUnlockVault(paramsJSON string) (s string) {
23 kPassword := string(append([]byte(nil), 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'))
24 pw := helpers.JsonGetString(paramsJSON, kPassword)
25 if pw == "" {
26 return jsonErr(string(append([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd')))
27 }
28 if unlockVault(pw) {
29 return jsonTrue()
30 }
31 if lastUnlockErr != "" {
32 return jsonErr(lastUnlockErr)
33 }
34 return jsonFalse()
35 }
36
37 func mgmtCreateVault(paramsJSON string) (s string) {
38 kPassword := string(append([]byte(nil), 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'))
39 pw := helpers.JsonGetString(paramsJSON, kPassword)
40 if pw == "" {
41 return jsonErr(string(append([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd')))
42 }
43 if createVault(pw) {
44 return jsonTrue()
45 }
46 return jsonFalse()
47 }
48
49 func mgmtLockVault() (s string) {
50 lockVault()
51 return jsonTrue()
52 }
53
54 func mgmtListIdentities() (s string) {
55 if !vaultOpen {
56 return jsonErr(string(append([]byte(nil), 'v', 'a', 'u', 'l', 't', ' ', 'l', 'o', 'c', 'k', 'e', 'd')))
57 }
58 b := append([]byte(nil), '{', '"', 'r', 'e', 's', 'u', 'l', 't', '"', ':', '[')
59 for i, id := range identities {
60 if i > 0 {
61 b = append(b, ',')
62 }
63 b = append(b, '{', '"', 'p', 'u', 'b', 'k', 'e', 'y', '"', ':')
64 b = b | helpers.JsonString(id.Pubkey)
65 b = append(b, ',', '"', 'n', 'a', 'm', 'e', '"', ':')
66 b = b | helpers.JsonString(id.Name)
67 b = append(b, '}')
68 }
69 return string(append(b, ']', '}'))
70 }
71
72 func mgmtSwitchIdentity(paramsJSON string) (s string) {
73 if !vaultOpen {
74 return jsonErr(string(append([]byte(nil), 'v', 'a', 'u', 'l', 't', ' ', 'l', 'o', 'c', 'k', 'e', 'd')))
75 }
76 kPubkey := string(append([]byte(nil), 'p', 'u', 'b', 'k', 'e', 'y'))
77 pk := helpers.JsonGetString(paramsJSON, kPubkey)
78 for i, id := range identities {
79 if id.Pubkey == pk {
80 activeIdx = i
81 cacheSession()
82 return jsonTrue()
83 }
84 }
85 return jsonFalse()
86 }
87
88 func mgmtAddIdentity(paramsJSON string) (s string) {
89 if !vaultOpen {
90 return jsonErr(string(append([]byte(nil), 'v', 'a', 'u', 'l', 't', ' ', 'l', 'o', 'c', 'k', 'e', 'd')))
91 }
92 kNsec := string(append([]byte(nil), 'n', 's', 'e', 'c'))
93 kSeckey := string(append([]byte(nil), 's', 'e', 'c', 'k', 'e', 'y'))
94 kName := string(append([]byte(nil), 'n', 'a', 'm', 'e'))
95 nsec := helpers.JsonGetString(paramsJSON, kNsec)
96 sk := helpers.JsonGetString(paramsJSON, kSeckey)
97 name := helpers.JsonGetString(paramsJSON, kName)
98 var skBytes []byte
99 if nsec != "" {
100 // Page-side API passes bech32 nsec.
101 raw, ok := decodeNsec(nsec)
102 if !ok {
103 return jsonErr(string(append([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 'n', 's', 'e', 'c')))
104 }
105 skBytes = raw[:]
106 sk = helpers.HexEncode(skBytes)
107 } else if sk != "" {
108 // Backward-compat: hex seckey.
109 skBytes = helpers.HexDecode(sk)
110 if skBytes == nil {
111 return jsonErr(string(append([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 's', 'e', 'c', 'k', 'e', 'y')))
112 }
113 } else {
114 // Neither nsec nor seckey provided: generate a fresh random identity.
115 var raw [32]byte
116 subtle.RandomBytes(raw[:])
117 skBytes = raw[:]
118 sk = helpers.HexEncode(skBytes)
119 }
120 pkBytes, ok := schnorr.PubKeyFromSecKey(skBytes)
121 if !ok {
122 return jsonErr(string(append([]byte(nil), 'k', 'e', 'y', ' ', 'd', 'e', 'r', 'i', 'v', 'a', 't', 'i', 'o', 'n', ' ', 'f', 'a', 'i', 'l', 'e', 'd')))
123 }
124 pk := helpers.HexEncode(pkBytes)
125 for _, id := range identities {
126 if id.Pubkey == pk {
127 return jsonErr(string(append([]byte(nil), 'i', 'd', 'e', 'n', 't', 'i', 't', 'y', ' ', 'e', 'x', 'i', 's', 't', 's')))
128 }
129 }
130 identities = append(identities, identity{Pubkey: pk, Seckey: sk, Name: name})
131 if activeIdx < 0 {
132 activeIdx = len(identities) - 1
133 }
134 saveVault()
135 return jsonResult(helpers.JsonString(pk))
136 }
137
138 // decodeNsec decodes a bech32-encoded nsec1... string into 32 raw bytes.
139 // Returns ([32]byte{}, false) on invalid format/length/charset.
140 func decodeNsec(nsec string) ([32]byte, bool) {
141 var zero [32]byte
142 prefix := string(append([]byte(nil), 'n', 's', 'e', 'c', '1'))
143 if len(nsec) < 5 || nsec[:5] != prefix {
144 return zero, false
145 }
146 sep := -1
147 for i := len(nsec) - 1; i >= 0; i-- {
148 if nsec[i] == '1' {
149 sep = i
150 break
151 }
152 }
153 if sep < 0 || len(nsec)-sep-1 <= 6 {
154 return zero, false
155 }
156 data := nsec[sep+1 : len(nsec)-6]
157 out := []byte{:32}[:0]
158 acc := uint32(0)
159 bits := uint32(0)
160 for i := 0; i < len(data); i++ {
161 v, ok := bech32CharVal(data[i])
162 if !ok {
163 return zero, false
164 }
165 acc = (acc << 5) | uint32(v)
166 bits += 5
167 for bits >= 8 {
168 bits -= 8
169 out = append(out, byte(acc>>bits))
170 }
171 }
172 if len(out) != 32 {
173 return zero, false
174 }
175 var result [32]byte
176 for i := 0; i < 32; i++ {
177 result[i] = out[i]
178 }
179 return result, true
180 }
181
182 func mgmtRemoveIdentity(paramsJSON string) (s string) {
183 if !vaultOpen {
184 return jsonErr(string(append([]byte(nil), 'v', 'a', 'u', 'l', 't', ' ', 'l', 'o', 'c', 'k', 'e', 'd')))
185 }
186 kPubkey := string(append([]byte(nil), 'p', 'u', 'b', 'k', 'e', 'y'))
187 pk := helpers.JsonGetString(paramsJSON, kPubkey)
188 for i, id := range identities {
189 if id.Pubkey == pk {
190 identities = identities[:i] | identities[i+1:]
191 if activeIdx >= len(identities) {
192 activeIdx = len(identities) - 1
193 }
194 saveVault()
195 return jsonTrue()
196 }
197 }
198 return jsonFalse()
199 }
200
201 func mgmtNsecLogin(paramsJSON string) (s string) {
202 kNsec := string(append([]byte(nil), 'n', 's', 'e', 'c'))
203 nsec := helpers.JsonGetString(paramsJSON, kNsec)
204 if nsec == "" {
205 return jsonErr(string(append([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'n', 's', 'e', 'c')))
206 }
207 raw, ok := decodeNsec(nsec)
208 if !ok {
209 return jsonErr(string(append([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 'n', 's', 'e', 'c')))
210 }
211 skBytes := raw[:]
212 sk := helpers.HexEncode(skBytes)
213 pkBytes, ok := schnorr.PubKeyFromSecKey(skBytes)
214 if !ok {
215 return jsonErr(string(append([]byte(nil), 'k', 'e', 'y', ' ', 'd', 'e', 'r', 'i', 'v', 'a', 't', 'i', 'o', 'n', ' ', 'f', 'a', 'i', 'l', 'e', 'd')))
216 }
217 pk := helpers.HexEncode(pkBytes)
218 identities = []identity{{Pubkey: pk, Seckey: sk, Name: ""}}
219 activeIdx = 0
220 vaultOpen = true
221 vaultExists = true
222 vaultVersion = 0
223 _ = ext.StorageSet
224 return jsonTrue()
225 }
226
227 // bech32CharVal returns the 5-bit value for a bech32 character.
228 // Charset: "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
229 func bech32CharVal(c byte) (byte, bool) {
230 switch c {
231 case 'q':
232 return 0, true
233 case 'p':
234 return 1, true
235 case 'z':
236 return 2, true
237 case 'r':
238 return 3, true
239 case 'y':
240 return 4, true
241 case '9':
242 return 5, true
243 case 'x':
244 return 6, true
245 case '8':
246 return 7, true
247 case 'g':
248 return 8, true
249 case 'f':
250 return 9, true
251 case '2':
252 return 10, true
253 case 't':
254 return 11, true
255 case 'v':
256 return 12, true
257 case 'd':
258 return 13, true
259 case 'w':
260 return 14, true
261 case '0':
262 return 15, true
263 case 's':
264 return 16, true
265 case '3':
266 return 17, true
267 case 'j':
268 return 18, true
269 case 'n':
270 return 19, true
271 case '5':
272 return 20, true
273 case '4':
274 return 21, true
275 case 'k':
276 return 22, true
277 case 'h':
278 return 23, true
279 case 'c':
280 return 24, true
281 case 'e':
282 return 25, true
283 case '6':
284 return 26, true
285 case 'm':
286 return 27, true
287 case 'u':
288 return 28, true
289 case 'a':
290 return 29, true
291 case '7':
292 return 30, true
293 case 'l':
294 return 31, true
295 }
296 return 0, false
297 }
298
299 func mgmtExportVault(paramsJSON string) (s string) {
300 if !vaultOpen {
301 return jsonErr(string(append([]byte(nil), 'v', 'a', 'u', 'l', 't', ' ', 'l', 'o', 'c', 'k', 'e', 'd')))
302 }
303 kPassword := string(append([]byte(nil), 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'))
304 pw := helpers.JsonGetString(paramsJSON, kPassword)
305 if pw == "" {
306 return jsonErr(string(append([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd')))
307 }
308 salt := []byte{:32}
309 subtle.RandomBytes(salt)
310 hash := passwordHash(pw)
311 if hash == "" {
312 return jsonErr(string(append([]byte(nil), 'h', 'a', 's', 'h', ' ', 'f', 'a', 'i', 'l', 'e', 'd')))
313 }
314 key := subtle.Argon2idDeriveKey(pw, salt, argon2T, argon2MNew, argon2P, argon2DKLen)
315 if len(key) == 0 {
316 return jsonErr(string(append([]byte(nil), 'k', 'e', 'y', ' ', 'f', 'a', 'i', 'l', 'e', 'd')))
317 }
318 b := []byte{:512}[:0]
319 b = append(b, '{', '"', 'v', 'e', 'r', 's', 'i', 'o', 'n', '"', ':', '3')
320 b = append(b, ',', '"', 'a', 'r', 'g', 'o', 'n', '2', 'M', '"', ':')
321 b = b | helpers.Itoa(int64(argon2MNew))
322 b = append(b, ',', '"', 's', 'a', 'l', 't', '"', ':')
323 b = b | helpers.JsonString(helpers.Base64Encode(salt))
324 b = append(b, ',', '"', 'v', 'a', 'u', 'l', 't', 'H', 'a', 's', 'h', '"', ':')
325 b = b | helpers.JsonString(hash)
326 b = append(b, ',', '"', 'i', 'd', 'e', 'n', 't', 'i', 't', 'i', 'e', 's', '"', ':', '[')
327 for i, id := range identities {
328 if i > 0 {
329 b = append(b, ',')
330 }
331 encSk, ok := encryptFieldWith(id.Seckey, key)
332 if !ok {
333 encSk = ""
334 }
335 b = append(b, '{', '"', 'p', 'u', 'b', 'k', 'e', 'y', '"', ':')
336 b = b | helpers.JsonString(id.Pubkey)
337 b = append(b, ',', '"', 's', 'e', 'c', 'k', 'e', 'y', '"', ':')
338 b = b | helpers.JsonString(encSk)
339 b = append(b, ',', '"', 'n', 'a', 'm', 'e', '"', ':')
340 b = b | helpers.JsonString(id.Name)
341 b = append(b, '}')
342 }
343 b = append(b, ']', '}')
344 return jsonResult(string(b))
345 }
346
347 func mgmtImportVault(paramsJSON string) (s string) {
348 kData := string(append([]byte(nil), 'd', 'a', 't', 'a'))
349 data := helpers.JsonGetString(paramsJSON, kData)
350 if data == "" {
351 return jsonErr(string(append([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'd', 'a', 't', 'a')))
352 }
353 kVersion := string(append([]byte(nil), 'v', 'e', 'r', 's', 'i', 'o', 'n'))
354 kVaultHash := string(append([]byte(nil), 'v', 'a', 'u', 'l', 't', 'H', 'a', 's', 'h'))
355 ver := helpers.JsonGetValue(data, kVersion)
356 vh := helpers.JsonGetString(data, kVaultHash)
357 if ver == "" || vh == "" {
358 return jsonErr(string(append([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 'v', 'a', 'u', 'l', 't', ' ', 'f', 'o', 'r', 'm', 'a', 't')))
359 }
360 vaultRawCache = data
361 vaultExists = true
362 vaultOpen = false
363 kVault := string(append([]byte(nil), 's', 'm', 'e', 's', 'h', '-', 'v', 'a', 'u', 'l', 't'))
364 ext.StorageSet(kVault, data)
365 return jsonTrue()
366 }
367
368 func mgmtResetExtension() (s string) {
369 ext.StorageRemove(vaultStorageKey)
370 ext.StorageRemove(string(append([]byte(nil), 's', 'm', 'e', 's', 'h', '-', 'p', 'e', 'r', 'm', 'i', 's', 's', 'i', 'o', 'n', 's')))
371 lockVault()
372 vaultExists = false
373 vaultRawCache = ""
374 return jsonTrue()
375 }
376