vault.mx raw
1 package main
2
3 import (
4 "git.smesh.lol/nostr/pkg/crypto/aes256gcm"
5 "crypto/sha256"
6 "git.smesh.lol/musiquay/web/common/helpers"
7 "git.smesh.lol/musiquay/web/common/jsbridge/ext"
8 "git.smesh.lol/musiquay/web/common/jsbridge/schnorr"
9 "git.smesh.lol/musiquay/web/common/jsbridge/subtle"
10 )
11
12 // Vault: Argon2id (v2) or PBKDF2 (v1, auto-migrated) + AES-256-GCM field
13 // encryption. Compatible with the Plebeian/Smesh signer format.
14 // Stored in browser.storage.local under vaultStorageKey.
15
16 const (
17 vaultStorageKey = "musiquay-vault"
18 permsStorageKey = "musiquay-permissions"
19 pbkdf2Salt = "3e7cdebd-3b4c-4125-a18c-05750cad8ec3"
20 pbkdf2Iters = 1000
21 argon2T = 3
22 // argon2MLegacy = original 64 MB. Existing vaults use this; new vaults use
23 // argon2MNew. 64 MB allocates a 8MB block that can trip TinyGo GC stack
24 // overflow on mobile WASM (limited linear memory, narrow GC stack).
25 argon2MLegacy = 65536
26 argon2MNew = 4096 // 4 MB - mobile-safe, still ~4 GPU-min/guess
27 argon2P = 1
28 argon2DKLen = 32
29 )
30
31 type identity struct {
32 Pubkey string // hex
33 Seckey string // hex, plaintext in memory while unlocked
34 Name string
35 }
36
37
38 func loadVault() {
39 ext.StorageGet(vaultStorageKey, func(data string) {
40 signSt.vaultRawCache = data
41 signSt.vaultExists = data != ""
42 if data == "" {
43 ext.SignalReady()
44 return
45 }
46 // Use heap-built key to avoid literal address relocation bug in wasm32.
47 kVersion := string(push([]byte(nil), 'v', 'e', 'r', 's', 'i', 'o', 'n'))
48 ver := helpers.JsonGetValue(data, kVersion)
49 if len(ver) == 1 && ver[0] == '0' {
50 restorePlaintextVault(data)
51 ext.SignalReady()
52 return
53 }
54 if ext.IsInPage() {
55 tryRestoreSession()
56 }
57 ext.SignalReady()
58 })
59 }
60
61 func activeIdentity() (p *identity) {
62 if !signSt.vaultOpen || signSt.activeIdx < 0 || signSt.activeIdx >= len(signSt.identities) {
63 return nil
64 }
65 return &signSt.identities[signSt.activeIdx]
66 }
67
68 func restoreActiveIdx(pk string) {
69 if pk == "" {
70 return
71 }
72 for i, id := range signSt.identities {
73 if id.Pubkey == pk {
74 signSt.activeIdx = i
75 return
76 }
77 }
78 }
79
80 func createVault(password string) (ok bool) {
81 hash := passwordHash(password)
82 if hash == "" {
83 return false
84 }
85 salt := []byte{:32}
86 subtle.RandomBytes(salt)
87 key := subtle.Argon2idDeriveKey(password, salt, argon2T, argon2MNew, argon2P, argon2DKLen)
88 if len(key) == 0 {
89 return false
90 }
91 signSt.vaultKey = key
92 signSt.vaultSalt = salt
93 signSt.vaultHash = hash
94 signSt.vaultVersion = 3
95 signSt.vaultArgon2M = argon2MNew
96 signSt.vaultOpen = true
97 signSt.identities = nil
98 signSt.activeIdx = -1
99 signSt.vaultExists = true
100 saveVault()
101 return true
102 }
103
104 func unlockVault(password string) (ok bool) {
105 signSt.lastUnlockErr = ""
106 data := signSt.vaultRawCache
107 if data == "" {
108 signSt.lastUnlockErr = "no-vault"
109 return false
110 }
111 if len(data) > 0 && data[0] != '{' {
112 return unlockLegacy(data, password)
113 }
114 kVaultHash := string(push([]byte(nil), 'v', 'a', 'u', 'l', 't', 'H', 'a', 's', 'h'))
115 kIV := string(push([]byte(nil), 'i', 'v'))
116 kSalt := string(push([]byte(nil), 's', 'a', 'l', 't'))
117 kVersion := string(push([]byte(nil), 'v', 'e', 'r', 's', 'i', 'o', 'n'))
118 kArgon2M := string(push([]byte(nil), 'a', 'r', 'g', 'o', 'n', '2', 'M'))
119 storedHash := helpers.JsonGetString(data, kVaultHash)
120 if storedHash == "" {
121 signSt.lastUnlockErr = "no-hash"
122 return false
123 }
124 computed := passwordHash(password)
125 signSt.vaultHash = storedHash
126 if computed != storedHash {
127 // Include hash prefixes so the diagnostic distinguishes:
128 // - typo (different prefixes, expected for a real typo)
129 // - corrupted save (computed stable across attempts, stored differs)
130 // - non-deterministic hash on WASM (computed differs across attempts)
131 cp := computed
132 if len(cp) > 12 {
133 cp = cp[:12]
134 }
135 sp := storedHash
136 if len(sp) > 12 {
137 sp = sp[:12]
138 }
139 signSt.lastUnlockErr = "wrong-password computed=" | cp | " stored=" | sp | " pwlen=" | helpers.Itoa(int64(len(password)))
140 return false
141 }
142 ver := helpers.JsonGetValue(data, kVersion)
143 storedVersion := 2
144 if len(ver) == 1 && ver[0] == '3' {
145 storedVersion = 3
146 }
147 // v2 vaults have a shared IV; v3 has per-field IVs (no "iv" in JSON).
148 var legacyIV []byte
149 if storedVersion <= 2 {
150 ivB64 := helpers.JsonGetString(data, kIV)
151 legacyIV = helpers.Base64Decode(ivB64)
152 if len(legacyIV) != 12 {
153 signSt.lastUnlockErr = "bad-iv"
154 return false
155 }
156 }
157 // Read argon2M param. Missing field = legacy vault (m=65536).
158 argMRaw := helpers.JsonGetValue(data, kArgon2M)
159 argM := argon2MLegacy
160 if argMRaw != "" {
161 v := 0
162 for i := 0; i < len(argMRaw); i++ {
163 c := argMRaw[i]
164 if c >= '0' && c <= '9' {
165 v = v*10 + int32(c-'0')
166 } else {
167 v = 0
168 break
169 }
170 }
171 if v > 0 {
172 argM = v
173 }
174 }
175 saltB64 := helpers.JsonGetString(data, kSalt)
176 if saltB64 != "" {
177 salt := helpers.Base64Decode(saltB64)
178 argonKey := subtle.Argon2idDeriveKey(password, salt, argon2T, argM, argon2P, argon2DKLen)
179 if len(argonKey) == 0 {
180 signSt.lastUnlockErr = "kdf-failed"
181 return false
182 }
183 if !finishUnlock(data, argonKey, legacyIV, salt, storedVersion) {
184 signSt.lastUnlockErr = "decrypt-failed"
185 return false
186 }
187 signSt.vaultArgon2M = argM
188 return true
189 }
190 key := subtle.PBKDF2DeriveKey(password, []byte(pbkdf2Salt), pbkdf2Iters)
191 if len(key) == 0 {
192 signSt.lastUnlockErr = "kdf-failed"
193 return false
194 }
195 if !finishUnlock(data, key, legacyIV, nil, 1) {
196 signSt.lastUnlockErr = "decrypt-failed"
197 return false
198 }
199 return true
200 }
201
202 func finishUnlock(data string, key, legacyIV, salt []byte, version int32) (ok bool) {
203 signSt.vaultKey = key
204 signSt.vaultSalt = salt
205 signSt.vaultVersion = version
206 signSt.v2IV = legacyIV
207 signSt.vaultOpen = true
208 // Restore signSt.vaultHash and signSt.vaultArgon2M from stored JSON so a subsequent
209 // saveVault doesn't see len(signSt.vaultHash)==0 and wipe storage with "{}".
210 // On a fresh page load, globals reset to zero and only data fields are
211 // rehydrated by callers - these two were missed before.
212 kVaultHashF := string(push([]byte(nil), 'v', 'a', 'u', 'l', 't', 'H', 'a', 's', 'h'))
213 if h := helpers.JsonGetString(data, kVaultHashF); h != "" {
214 signSt.vaultHash = h
215 }
216 kArgon2MF := string(push([]byte(nil), 'a', 'r', 'g', 'o', 'n', '2', 'M'))
217 if signSt.vaultArgon2M == 0 {
218 raw := helpers.JsonGetValue(data, kArgon2MF)
219 v := 0
220 for i := 0; i < len(raw); i++ {
221 c := raw[i]
222 if c < '0' || c > '9' { v = 0; break }
223 v = v*10 + int32(c-'0')
224 }
225 if v > 0 {
226 signSt.vaultArgon2M = v
227 } else {
228 signSt.vaultArgon2M = argon2MLegacy
229 }
230 }
231 kIdentities := string(push([]byte(nil), 'i', 'd', 'e', 'n', 't', 'i', 't', 'i', 'e', 's'))
232 idsRaw := helpers.JsonGetValue(data, kIdentities)
233 signSt.identities = nil
234 signSt.activeIdx = -1
235 if idsRaw != "" {
236 kPubkey := string(push([]byte(nil), 'p', 'u', 'b', 'k', 'e', 'y'))
237 kSeckey := string(push([]byte(nil), 's', 'e', 'c', 'k', 'e', 'y'))
238 kName := string(push([]byte(nil), 'n', 'a', 'm', 'e'))
239 i := 0
240 for i < len(idsRaw) && idsRaw[i] != '[' {
241 i++
242 }
243 i++
244 for i < len(idsRaw) {
245 for i < len(idsRaw) && idsRaw[i] != '{' && idsRaw[i] != ']' {
246 i++
247 }
248 if i >= len(idsRaw) || idsRaw[i] == ']' {
249 break
250 }
251 end := i + 1
252 depth := 1
253 for end < len(idsRaw) && depth > 0 {
254 if idsRaw[end] == '{' {
255 depth++
256 } else if idsRaw[end] == '}' {
257 depth--
258 } else if idsRaw[end] == '"' {
259 end++
260 for end < len(idsRaw) && idsRaw[end] != '"' {
261 if idsRaw[end] == '\\' {
262 end++
263 }
264 end++
265 }
266 }
267 end++
268 }
269 obj := idsRaw[i:end]
270 pk := helpers.JsonGetString(obj, kPubkey)
271 encSk := helpers.JsonGetString(obj, kSeckey)
272 name := helpers.JsonGetString(obj, kName)
273 if pk != "" {
274 sk := encSk
275 decryptOK := true
276 if version >= 2 && encSk != "" {
277 if plain, ok2 := decryptField(encSk); ok2 {
278 sk = plain
279 } else {
280 decryptOK = false
281 }
282 }
283 if !decryptOK {
284 // Bail. Caller surfaces this as decrypt-failed.
285 lockVault()
286 return false
287 }
288 signSt.identities = push(signSt.identities, identity{Pubkey: pk, Seckey: sk, Name: name})
289 }
290 i = end
291 }
292 }
293 if len(signSt.identities) > 0 {
294 signSt.activeIdx = 0
295 }
296 // Migrate v2 -> v3: re-encrypt with per-field IVs and save.
297 if version <= 2 && len(signSt.identities) > 0 {
298 signSt.vaultVersion = 3
299 signSt.v2IV = nil
300 saveVault()
301 }
302 return true
303 }
304
305 func lockVault() {
306 for i := range signSt.vaultKey {
307 signSt.vaultKey[i] = 0
308 }
309 signSt.vaultKey = nil
310 signSt.vaultSalt = nil
311 signSt.v2IV = nil
312 signSt.vaultOpen = false
313 signSt.identities = nil
314 }
315
316 func saveVault() {
317 // Refuse to save if state is incomplete. Writing "{}" here would destroy
318 // the on-disk vault when called from a refresh-then-modify flow where
319 // signSt.vaultHash hadn't been rehydrated yet.
320 if !signSt.vaultExists || len(signSt.vaultHash) == 0 {
321 return
322 }
323 // Persist argon2M used at create time, so unlock uses the right param even
324 // if defaults change later. Legacy vaults (no field) default to argon2MLegacy.
325 mField := signSt.vaultArgon2M
326 if mField == 0 {
327 mField = argon2MLegacy
328 }
329 b := []byte{:512}[:0]
330 b = b | "{\"version\":"
331 b = b | helpers.Itoa(int64(signSt.vaultVersion))
332 b = b | ",\"argon2M\":"
333 b = b | helpers.Itoa(int64(mField))
334 b = b | ",\"vaultHash\":"
335 b = b | helpers.JsonString(signSt.vaultHash)
336 b = b | ",\"salt\":"
337 b = b | helpers.JsonString(helpers.Base64Encode(signSt.vaultSalt))
338 b = b | ",\"identities\":["
339 for i, id := range signSt.identities {
340 if i > 0 {
341 b = b | ","
342 }
343 encSk, ok := encryptField(id.Seckey)
344 if !ok {
345 encSk = id.Seckey
346 }
347 b = b | "{\"pubkey\":"
348 b = b | helpers.JsonString(id.Pubkey)
349 b = b | ",\"seckey\":"
350 b = b | helpers.JsonString(encSk)
351 b = b | ",\"name\":"
352 b = b | helpers.JsonString(id.Name)
353 b = b | "}"
354 }
355 b = b | "]}"
356 signSt.vaultRawCache = string(b)
357 ext.StorageSet(vaultStorageKey, signSt.vaultRawCache)
358 if signSt.vaultOpen && ext.IsInPage() {
359 cacheSession()
360 }
361 }
362
363 func restorePlaintextVault(data string) {
364 kIdentities := string(push([]byte(nil), 'i', 'd', 'e', 'n', 't', 'i', 't', 'i', 'e', 's'))
365 ids := helpers.JsonGetValue(data, kIdentities)
366 if ids == "" {
367 return
368 }
369 kPubkey := string(push([]byte(nil), 'p', 'u', 'b', 'k', 'e', 'y'))
370 kSeckey := string(push([]byte(nil), 's', 'e', 'c', 'k', 'e', 'y'))
371 pk := helpers.JsonGetString(ids, kPubkey)
372 sk := helpers.JsonGetString(ids, kSeckey)
373 if pk == "" || sk == "" {
374 return
375 }
376 signSt.vaultOpen = true
377 signSt.vaultExists = true
378 signSt.vaultVersion = 0
379 signSt.identities = []identity{{Pubkey: pk, Seckey: sk, Name: ""}}
380 signSt.activeIdx = 0
381 }
382
383 const sessionCacheKey = "vault-session"
384
385 func cacheSession() {
386 if !ext.IsInPage() || !signSt.vaultOpen || len(signSt.vaultKey) == 0 {
387 return
388 }
389 b := []byte(nil) | helpers.HexEncode(signSt.vaultKey)
390 b = b | ":"
391 b = b | helpers.HexEncode(signSt.vaultSalt)
392 b = b | ":"
393 b = b | helpers.Itoa(int64(signSt.vaultVersion))
394 b = b | ":"
395 aid := activeIdentity()
396 if aid != nil {
397 b = b | aid.Pubkey
398 }
399 kSession := string(push([]byte(nil), 'v', 'a', 'u', 'l', 't', '-', 's', 'e', 's', 's', 'i', 'o', 'n'))
400 ext.SessionSet(kSession, string(b))
401 }
402
403 func clearSessionCache() {
404 kSession := string(push([]byte(nil), 'v', 'a', 'u', 'l', 't', '-', 's', 'e', 's', 's', 'i', 'o', 'n'))
405 ext.SessionSet(kSession, "")
406 }
407
408 func tryRestoreSession() {
409 kSession := string(push([]byte(nil), 'v', 'a', 'u', 'l', 't', '-', 's', 'e', 's', 's', 'i', 'o', 'n'))
410 ext.SessionGet(kSession, func(val string) {
411 if val == "" {
412 return
413 }
414 var parts [6]string
415 n := 0
416 start := 0
417 for i := 0; i < len(val) && n < 5; i++ {
418 if val[i] == ':' {
419 parts[n] = val[start:i]
420 n++
421 start = i + 1
422 }
423 }
424 parts[n] = val[start:]
425 n++
426 data := signSt.vaultRawCache
427 if data == "" {
428 clearSessionCache()
429 return
430 }
431 // Detect format by part count and whether parts[2] is a 1-char
432 // version digit (v3) or a long hex salt (v2).
433 // v3: key:salt:version[:activePubkey] - parts[2] is 1 char
434 // v2: key:iv:salt:version[:activePubkey] - parts[2] is hex salt
435 isV3 := n >= 3 && len(parts[2]) == 1
436 if isV3 {
437 key := helpers.HexDecode(parts[0])
438 salt := helpers.HexDecode(parts[1])
439 if len(key) != 32 {
440 clearSessionCache()
441 return
442 }
443 ver := parseSessionVer(parts[2])
444 if !finishUnlock(data, key, nil, salt, ver) {
445 clearSessionCache()
446 return
447 }
448 if n >= 4 {
449 restoreActiveIdx(parts[3])
450 }
451 } else if n >= 4 {
452 key := helpers.HexDecode(parts[0])
453 iv := helpers.HexDecode(parts[1])
454 salt := helpers.HexDecode(parts[2])
455 if len(key) != 32 || len(iv) != 12 {
456 clearSessionCache()
457 return
458 }
459 ver := parseSessionVer(parts[3])
460 if !finishUnlock(data, key, iv, salt, ver) {
461 clearSessionCache()
462 return
463 }
464 if n >= 5 {
465 restoreActiveIdx(parts[4])
466 }
467 } else {
468 clearSessionCache()
469 }
470 })
471 }
472
473 func parseSessionVer(s string) (n int32) {
474 if len(s) == 1 {
475 if s[0] == '1' {
476 return 1
477 }
478 if s[0] == '2' {
479 return 2
480 }
481 }
482 return 3
483 }
484
485 // encryptFieldWith encrypts using explicit key (for export with different password).
486 // Generates a fresh 12-byte random IV per call, prepended to ciphertext.
487 // Output: base64(iv[12] || ciphertext || tag[16]).
488 func encryptFieldWith(plain string, key []byte) (s string, ok bool) {
489 if len(key) == 0 {
490 return "", false
491 }
492 iv := []byte{:12}
493 subtle.RandomBytes(iv)
494 ct := aes256gcm.Seal(key, iv, []byte(plain), nil)
495 if ct == nil {
496 return "", false
497 }
498 out := []byte{:0:12 + len(ct)}
499 out = out | iv
500 out = out | ct
501 return helpers.Base64Encode(out), true
502 }
503
504 // encryptField encrypts a plaintext string using signSt.vaultKey via AES-256-GCM.
505 // Fresh random IV per call - no nonce reuse.
506 func encryptField(plain string) (s string, ok bool) {
507 if !signSt.vaultOpen || len(signSt.vaultKey) == 0 {
508 return "", false
509 }
510 return encryptFieldWith(plain, signSt.vaultKey)
511 }
512
513 // decryptField decrypts a base64-encoded AES-256-GCM ciphertext.
514 // v3 format: base64(iv[12] || ciphertext || tag[16]).
515 // v2 compat: if signSt.v2IV is set, tries old format (no prepended IV) as fallback.
516 func decryptField(enc string) (s string, ok2 bool) {
517 if !signSt.vaultOpen || len(signSt.vaultKey) == 0 {
518 return "", false
519 }
520 raw := helpers.Base64Decode(enc)
521 if raw == nil {
522 return "", false
523 }
524 // v3: first 12 bytes are per-field IV.
525 if len(raw) >= 12+16 {
526 iv := raw[:12]
527 ct := raw[12:]
528 pt, ok := aes256gcm.Open(signSt.vaultKey, iv, ct, nil)
529 if ok {
530 return string(pt), true
531 }
532 }
533 // v2 fallback: shared IV from vault JSON.
534 if len(signSt.v2IV) == 12 {
535 pt, ok := aes256gcm.Open(signSt.vaultKey, signSt.v2IV, raw, nil)
536 if ok {
537 return string(pt), true
538 }
539 }
540 return "", false
541 }
542
543 func passwordHash(pw string) (s string) {
544 return subtle.SHA256Hex([]byte(pw))
545 }
546
547 func unlockLegacy(data, password string) (ok bool) {
548 sepIdx := -1
549 for i := 0; i < len(data); i++ {
550 if data[i] == ':' {
551 sepIdx = i
552 break
553 }
554 }
555 if sepIdx < 1 {
556 return false
557 }
558 iv := helpers.HexDecode(data[:sepIdx])
559 ct := helpers.HexDecode(data[sepIdx+1:])
560 if iv == nil || ct == nil {
561 return false
562 }
563 key := legacyDeriveKey(password)
564 pt := subtle.AESCBCDecrypt(key[:], iv, ct)
565 if len(pt) == 0 {
566 return false
567 }
568 parseLegacyIdentities(string(pt))
569 newSalt := []byte{:32}
570 subtle.RandomBytes(newSalt)
571 hash := passwordHash(password)
572 if hash == "" {
573 return true
574 }
575 newKey := subtle.Argon2idDeriveKey(password, newSalt, argon2T, argon2MNew, argon2P, argon2DKLen)
576 if len(newKey) == 0 {
577 return true
578 }
579 signSt.vaultKey = newKey
580 signSt.vaultSalt = newSalt
581 signSt.vaultHash = hash
582 signSt.vaultVersion = 3
583 signSt.vaultArgon2M = argon2MNew
584 signSt.vaultOpen = true
585 signSt.vaultExists = true
586 saveVault()
587 return true
588 }
589
590 // legacyDeriveKey derives a 32-byte key via 100000 rounds of SHA-256 over
591 // "smesh-vault-salt:" | password. The prefix is the one the original signer
592 // used and must not be renamed: it is the interop contract for importing a
593 // legacy (v1) vault.
594 func legacyDeriveKey(password string) (v [32]byte) {
595 saltPrefix := push([]byte(nil), 's', 'm', 'e', 's', 'h', '-', 'v', 'a', 'u', 'l', 't', '-', 's', 'a', 'l', 't', ':')
596 saltPrefix = saltPrefix | password
597 h := sha256.Sum(saltPrefix)
598 for i := 0; i < 100000; i++ {
599 h = sha256.Sum(h[:])
600 }
601 return h
602 }
603
604 // parseLegacyIdentities parses plaintext JSON array of signSt.identities from old format.
605 func parseLegacyIdentities(s string) {
606 signSt.identities = nil
607 signSt.activeIdx = -1
608 i := 0
609 for i < len(s) && s[i] != '[' {
610 i++
611 }
612 i++
613 for i < len(s) {
614 for i < len(s) && s[i] != '{' && s[i] != ']' {
615 i++
616 }
617 if i >= len(s) || s[i] == ']' {
618 break
619 }
620 end := i + 1
621 depth := 1
622 for end < len(s) && depth > 0 {
623 if s[end] == '{' {
624 depth++
625 } else if s[end] == '}' {
626 depth--
627 } else if s[end] == '"' {
628 end++
629 for end < len(s) && s[end] != '"' {
630 if s[end] == '\\' {
631 end++
632 }
633 end++
634 }
635 }
636 end++
637 }
638 obj := s[i:end]
639 kPubkey := string(push([]byte(nil), 'p', 'u', 'b', 'k', 'e', 'y'))
640 kPublicKey := string(push([]byte(nil), 'p', 'u', 'b', 'l', 'i', 'c', 'K', 'e', 'y'))
641 kPub := string(push([]byte(nil), 'p', 'u', 'b'))
642 kSeckey := string(push([]byte(nil), 's', 'e', 'c', 'k', 'e', 'y'))
643 kPrivkey := string(push([]byte(nil), 'p', 'r', 'i', 'v', 'k', 'e', 'y'))
644 kSecretKey := string(push([]byte(nil), 's', 'e', 'c', 'r', 'e', 't', 'K', 'e', 'y'))
645 kSec := string(push([]byte(nil), 's', 'e', 'c'))
646 kKey := string(push([]byte(nil), 'k', 'e', 'y'))
647 kName := string(push([]byte(nil), 'n', 'a', 'm', 'e'))
648 kNick := string(push([]byte(nil), 'n', 'i', 'c', 'k'))
649 kLabel := string(push([]byte(nil), 'l', 'a', 'b', 'e', 'l'))
650 pk := helpers.JsonGetString(obj, kPubkey)
651 if pk == "" {
652 pk = helpers.JsonGetString(obj, kPublicKey)
653 }
654 if pk == "" {
655 pk = helpers.JsonGetString(obj, kPub)
656 }
657 sk := helpers.JsonGetString(obj, kSeckey)
658 if sk == "" {
659 sk = helpers.JsonGetString(obj, kPrivkey)
660 }
661 if sk == "" {
662 sk = helpers.JsonGetString(obj, kSecretKey)
663 }
664 if sk == "" {
665 sk = helpers.JsonGetString(obj, kSec)
666 }
667 if sk == "" {
668 sk = helpers.JsonGetString(obj, kKey)
669 }
670 nm := helpers.JsonGetString(obj, kName)
671 if nm == "" {
672 nm = helpers.JsonGetString(obj, kNick)
673 }
674 if nm == "" {
675 nm = helpers.JsonGetString(obj, kLabel)
676 }
677 // nsec decode
678 kNsec1 := string(push([]byte(nil), 'n', 's', 'e', 'c', '1'))
679 if len(sk) >= 5 && sk[:5] == kNsec1 {
680 skBytes := helpers.DecodeNsec(sk)
681 if skBytes != nil {
682 sk = helpers.HexEncode(skBytes)
683 }
684 }
685 // derive pubkey if absent
686 if pk == "" && sk != "" {
687 skBytes := helpers.HexDecode(sk)
688 if skBytes != nil {
689 dpk, ok := schnorr.PubKeyFromSecKey(skBytes)
690 if ok {
691 pk = helpers.HexEncode(dpk)
692 }
693 }
694 }
695 if pk != "" && sk != "" {
696 signSt.identities = push(signSt.identities, identity{Pubkey: pk, Seckey: sk, Name: nm})
697 }
698 i = end
699 }
700 if len(signSt.identities) > 0 {
701 signSt.activeIdx = 0
702 }
703 }
704