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