package main import ( "git.smesh.lol/smesh/web/common/helpers" "git.smesh.lol/smesh/web/common/jsbridge/ext" "git.smesh.lol/smesh/web/common/jsbridge/schnorr" "git.smesh.lol/smesh/web/common/jsbridge/subtle" ) // Management API: vault lifecycle, identity CRUD, export/import. func mgmtGetVaultStatus() (s string) { if !vaultExists { return jsonResult(string(append([]byte(nil), '"', 'n', 'o', 'n', 'e', '"'))) } if vaultOpen { return jsonResult(string(append([]byte(nil), '"', 'u', 'n', 'l', 'o', 'c', 'k', 'e', 'd', '"'))) } return jsonResult(string(append([]byte(nil), '"', 'l', 'o', 'c', 'k', 'e', 'd', '"'))) } func mgmtUnlockVault(paramsJSON string) (s string) { kPassword := string(append([]byte(nil), 'p', 'a', 's', 's', 'w', 'o', 'r', 'd')) pw := helpers.JsonGetString(paramsJSON, kPassword) if pw == "" { return jsonErr(string(append([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'))) } if unlockVault(pw) { return jsonTrue() } if lastUnlockErr != "" { return jsonErr(lastUnlockErr) } return jsonFalse() } func mgmtCreateVault(paramsJSON string) (s string) { kPassword := string(append([]byte(nil), 'p', 'a', 's', 's', 'w', 'o', 'r', 'd')) pw := helpers.JsonGetString(paramsJSON, kPassword) if pw == "" { return jsonErr(string(append([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'))) } if createVault(pw) { return jsonTrue() } return jsonFalse() } func mgmtLockVault() (s string) { lockVault() return jsonTrue() } func mgmtListIdentities() (s string) { if !vaultOpen { return jsonErr(string(append([]byte(nil), 'v', 'a', 'u', 'l', 't', ' ', 'l', 'o', 'c', 'k', 'e', 'd'))) } b := append([]byte(nil), '{', '"', 'r', 'e', 's', 'u', 'l', 't', '"', ':', '[') for i, id := range identities { if i > 0 { b = append(b, ',') } b = append(b, '{', '"', 'p', 'u', 'b', 'k', 'e', 'y', '"', ':') b = b | helpers.JsonString(id.Pubkey) b = append(b, ',', '"', 'n', 'a', 'm', 'e', '"', ':') b = b | helpers.JsonString(id.Name) b = append(b, '}') } return string(append(b, ']', '}')) } func mgmtSwitchIdentity(paramsJSON string) (s string) { if !vaultOpen { return jsonErr(string(append([]byte(nil), 'v', 'a', 'u', 'l', 't', ' ', 'l', 'o', 'c', 'k', 'e', 'd'))) } kPubkey := string(append([]byte(nil), 'p', 'u', 'b', 'k', 'e', 'y')) pk := helpers.JsonGetString(paramsJSON, kPubkey) for i, id := range identities { if id.Pubkey == pk { activeIdx = i cacheSession() return jsonTrue() } } return jsonFalse() } func mgmtAddIdentity(paramsJSON string) (s string) { if !vaultOpen { return jsonErr(string(append([]byte(nil), 'v', 'a', 'u', 'l', 't', ' ', 'l', 'o', 'c', 'k', 'e', 'd'))) } kNsec := string(append([]byte(nil), 'n', 's', 'e', 'c')) kSeckey := string(append([]byte(nil), 's', 'e', 'c', 'k', 'e', 'y')) kName := string(append([]byte(nil), 'n', 'a', 'm', 'e')) nsec := helpers.JsonGetString(paramsJSON, kNsec) sk := helpers.JsonGetString(paramsJSON, kSeckey) name := helpers.JsonGetString(paramsJSON, kName) var skBytes []byte if nsec != "" { // Page-side API passes bech32 nsec. raw, ok := decodeNsec(nsec) if !ok { return jsonErr(string(append([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 'n', 's', 'e', 'c'))) } skBytes = raw[:] sk = helpers.HexEncode(skBytes) } else if sk != "" { // Backward-compat: hex seckey. skBytes = helpers.HexDecode(sk) if skBytes == nil { return jsonErr(string(append([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 's', 'e', 'c', 'k', 'e', 'y'))) } } else { // Neither nsec nor seckey provided: generate a fresh random identity. var raw [32]byte subtle.RandomBytes(raw[:]) skBytes = raw[:] sk = helpers.HexEncode(skBytes) } pkBytes, ok := schnorr.PubKeyFromSecKey(skBytes) if !ok { 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'))) } pk := helpers.HexEncode(pkBytes) for _, id := range identities { if id.Pubkey == pk { return jsonErr(string(append([]byte(nil), 'i', 'd', 'e', 'n', 't', 'i', 't', 'y', ' ', 'e', 'x', 'i', 's', 't', 's'))) } } identities = append(identities, identity{Pubkey: pk, Seckey: sk, Name: name}) if activeIdx < 0 { activeIdx = len(identities) - 1 } saveVault() return jsonResult(helpers.JsonString(pk)) } // decodeNsec decodes a bech32-encoded nsec1... string into 32 raw bytes. // Returns ([32]byte{}, false) on invalid format/length/charset. func decodeNsec(nsec string) ([32]byte, bool) { var zero [32]byte prefix := string(append([]byte(nil), 'n', 's', 'e', 'c', '1')) if len(nsec) < 5 || nsec[:5] != prefix { return zero, false } sep := -1 for i := len(nsec) - 1; i >= 0; i-- { if nsec[i] == '1' { sep = i break } } if sep < 0 || len(nsec)-sep-1 <= 6 { return zero, false } data := nsec[sep+1 : len(nsec)-6] out := []byte{:32}[:0] acc := uint32(0) bits := uint32(0) for i := 0; i < len(data); i++ { v, ok := bech32CharVal(data[i]) if !ok { return zero, false } acc = (acc << 5) | uint32(v) bits += 5 for bits >= 8 { bits -= 8 out = append(out, byte(acc>>bits)) } } if len(out) != 32 { return zero, false } var result [32]byte for i := 0; i < 32; i++ { result[i] = out[i] } return result, true } func mgmtRemoveIdentity(paramsJSON string) (s string) { if !vaultOpen { return jsonErr(string(append([]byte(nil), 'v', 'a', 'u', 'l', 't', ' ', 'l', 'o', 'c', 'k', 'e', 'd'))) } kPubkey := string(append([]byte(nil), 'p', 'u', 'b', 'k', 'e', 'y')) pk := helpers.JsonGetString(paramsJSON, kPubkey) for i, id := range identities { if id.Pubkey == pk { identities = identities[:i] | identities[i+1:] if activeIdx >= len(identities) { activeIdx = len(identities) - 1 } saveVault() return jsonTrue() } } return jsonFalse() } func mgmtNsecLogin(paramsJSON string) (s string) { kNsec := string(append([]byte(nil), 'n', 's', 'e', 'c')) nsec := helpers.JsonGetString(paramsJSON, kNsec) if nsec == "" { return jsonErr(string(append([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'n', 's', 'e', 'c'))) } raw, ok := decodeNsec(nsec) if !ok { return jsonErr(string(append([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 'n', 's', 'e', 'c'))) } skBytes := raw[:] sk := helpers.HexEncode(skBytes) pkBytes, ok := schnorr.PubKeyFromSecKey(skBytes) if !ok { 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'))) } pk := helpers.HexEncode(pkBytes) identities = []identity{{Pubkey: pk, Seckey: sk, Name: ""}} activeIdx = 0 vaultOpen = true vaultExists = true vaultVersion = 0 _ = ext.StorageSet return jsonTrue() } // bech32CharVal returns the 5-bit value for a bech32 character. // Charset: "qpzry9x8gf2tvdw0s3jn54khce6mua7l" func bech32CharVal(c byte) (byte, bool) { switch c { case 'q': return 0, true case 'p': return 1, true case 'z': return 2, true case 'r': return 3, true case 'y': return 4, true case '9': return 5, true case 'x': return 6, true case '8': return 7, true case 'g': return 8, true case 'f': return 9, true case '2': return 10, true case 't': return 11, true case 'v': return 12, true case 'd': return 13, true case 'w': return 14, true case '0': return 15, true case 's': return 16, true case '3': return 17, true case 'j': return 18, true case 'n': return 19, true case '5': return 20, true case '4': return 21, true case 'k': return 22, true case 'h': return 23, true case 'c': return 24, true case 'e': return 25, true case '6': return 26, true case 'm': return 27, true case 'u': return 28, true case 'a': return 29, true case '7': return 30, true case 'l': return 31, true } return 0, false } func mgmtExportVault(paramsJSON string) (s string) { if !vaultOpen { return jsonErr(string(append([]byte(nil), 'v', 'a', 'u', 'l', 't', ' ', 'l', 'o', 'c', 'k', 'e', 'd'))) } kPassword := string(append([]byte(nil), 'p', 'a', 's', 's', 'w', 'o', 'r', 'd')) pw := helpers.JsonGetString(paramsJSON, kPassword) if pw == "" { return jsonErr(string(append([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'))) } salt := []byte{:32} subtle.RandomBytes(salt) hash := passwordHash(pw) if hash == "" { return jsonErr(string(append([]byte(nil), 'h', 'a', 's', 'h', ' ', 'f', 'a', 'i', 'l', 'e', 'd'))) } key := subtle.Argon2idDeriveKey(pw, salt, argon2T, argon2MNew, argon2P, argon2DKLen) if len(key) == 0 { return jsonErr(string(append([]byte(nil), 'k', 'e', 'y', ' ', 'f', 'a', 'i', 'l', 'e', 'd'))) } b := []byte{:512}[:0] b = append(b, '{', '"', 'v', 'e', 'r', 's', 'i', 'o', 'n', '"', ':', '3') b = append(b, ',', '"', 'a', 'r', 'g', 'o', 'n', '2', 'M', '"', ':') b = b | helpers.Itoa(int64(argon2MNew)) b = append(b, ',', '"', 's', 'a', 'l', 't', '"', ':') b = b | helpers.JsonString(helpers.Base64Encode(salt)) b = append(b, ',', '"', 'v', 'a', 'u', 'l', 't', 'H', 'a', 's', 'h', '"', ':') b = b | helpers.JsonString(hash) b = append(b, ',', '"', 'i', 'd', 'e', 'n', 't', 'i', 't', 'i', 'e', 's', '"', ':', '[') for i, id := range identities { if i > 0 { b = append(b, ',') } encSk, ok := encryptFieldWith(id.Seckey, key) if !ok { encSk = "" } b = append(b, '{', '"', 'p', 'u', 'b', 'k', 'e', 'y', '"', ':') b = b | helpers.JsonString(id.Pubkey) b = append(b, ',', '"', 's', 'e', 'c', 'k', 'e', 'y', '"', ':') b = b | helpers.JsonString(encSk) b = append(b, ',', '"', 'n', 'a', 'm', 'e', '"', ':') b = b | helpers.JsonString(id.Name) b = append(b, '}') } b = append(b, ']', '}') return jsonResult(string(b)) } func mgmtImportVault(paramsJSON string) (s string) { kData := string(append([]byte(nil), 'd', 'a', 't', 'a')) data := helpers.JsonGetString(paramsJSON, kData) if data == "" { return jsonErr(string(append([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'd', 'a', 't', 'a'))) } kVersion := string(append([]byte(nil), 'v', 'e', 'r', 's', 'i', 'o', 'n')) kVaultHash := string(append([]byte(nil), 'v', 'a', 'u', 'l', 't', 'H', 'a', 's', 'h')) ver := helpers.JsonGetValue(data, kVersion) vh := helpers.JsonGetString(data, kVaultHash) if ver == "" || vh == "" { return jsonErr(string(append([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 'v', 'a', 'u', 'l', 't', ' ', 'f', 'o', 'r', 'm', 'a', 't'))) } vaultRawCache = data vaultExists = true vaultOpen = false kVault := string(append([]byte(nil), 's', 'm', 'e', 's', 'h', '-', 'v', 'a', 'u', 'l', 't')) ext.StorageSet(kVault, data) return jsonTrue() } func mgmtResetExtension() (s string) { ext.StorageRemove(vaultStorageKey) ext.StorageRemove(string(append([]byte(nil), 's', 'm', 'e', 's', 'h', '-', 'p', 'e', 'r', 'm', 'i', 's', 's', 'i', 'o', 'n', 's'))) lockVault() vaultExists = false vaultRawCache = "" return jsonTrue() }