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