package main import ( "git.smesh.lol/musiquay/web/common/helpers" "git.smesh.lol/musiquay/web/common/jsbridge/ext" "git.smesh.lol/musiquay/web/common/jsbridge/schnorr" "git.smesh.lol/musiquay/web/common/jsbridge/subtle" ) // Management API: vault lifecycle, identity CRUD, export/import. func mgmtGetVaultStatus() (s string) { if !signSt.vaultExists { return jsonResult(string(push([]byte(nil), '"', 'n', 'o', 'n', 'e', '"'))) } if signSt.vaultOpen { return jsonResult(string(push([]byte(nil), '"', 'u', 'n', 'l', 'o', 'c', 'k', 'e', 'd', '"'))) } return jsonResult(string(push([]byte(nil), '"', 'l', 'o', 'c', 'k', 'e', 'd', '"'))) } func mgmtUnlockVault(paramsJSON string) (s string) { kPassword := string(push([]byte(nil), 'p', 'a', 's', 's', 'w', 'o', 'r', 'd')) pw := helpers.JsonGetString(paramsJSON, kPassword) if pw == "" { return jsonErr(string(push([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'))) } if unlockVault(pw) { return jsonTrue() } if signSt.lastUnlockErr != "" { return jsonErr(signSt.lastUnlockErr) } return jsonFalse() } func mgmtCreateVault(paramsJSON string) (s string) { kPassword := string(push([]byte(nil), 'p', 'a', 's', 's', 'w', 'o', 'r', 'd')) pw := helpers.JsonGetString(paramsJSON, kPassword) if pw == "" { return jsonErr(string(push([]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 !signSt.vaultOpen { return jsonErr(string(push([]byte(nil), 'v', 'a', 'u', 'l', 't', ' ', 'l', 'o', 'c', 'k', 'e', 'd'))) } b := push([]byte(nil), '{', '"', 'r', 'e', 's', 'u', 'l', 't', '"', ':', '[') for i, id := range signSt.identities { if i > 0 { b = b | "," } b = b | "{\"pubkey\":" b = b | helpers.JsonString(id.Pubkey) b = b | ",\"name\":" b = b | helpers.JsonString(id.Name) b = b | "}" } return string(b | "]}") } func mgmtSwitchIdentity(paramsJSON string) (s string) { if !signSt.vaultOpen { return jsonErr(string(push([]byte(nil), 'v', 'a', 'u', 'l', 't', ' ', 'l', 'o', 'c', 'k', 'e', 'd'))) } kPubkey := string(push([]byte(nil), 'p', 'u', 'b', 'k', 'e', 'y')) pk := helpers.JsonGetString(paramsJSON, kPubkey) for i, id := range signSt.identities { if id.Pubkey == pk { signSt.activeIdx = i cacheSession() return jsonTrue() } } return jsonFalse() } func mgmtAddIdentity(paramsJSON string) (s string) { if !signSt.vaultOpen { return jsonErr(string(push([]byte(nil), 'v', 'a', 'u', 'l', 't', ' ', 'l', 'o', 'c', 'k', 'e', 'd'))) } kNsec := string(push([]byte(nil), 'n', 's', 'e', 'c')) kSeckey := string(push([]byte(nil), 's', 'e', 'c', 'k', 'e', 'y')) kName := string(push([]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, nsecOK := decodeNsec(nsec) if !nsecOK { return jsonErr(string(push([]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(push([]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(push([]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 signSt.identities { if id.Pubkey == pk { return jsonErr(string(push([]byte(nil), 'i', 'd', 'e', 'n', 't', 'i', 't', 'y', ' ', 'e', 'x', 'i', 's', 't', 's'))) } } signSt.identities = push(signSt.identities, identity{Pubkey: pk, Seckey: sk, Name: name}) if signSt.activeIdx < 0 { signSt.activeIdx = len(signSt.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) (v2 [32]byte, ok bool) { var zero [32]byte prefix := string(push([]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, charOK := bech32CharVal(data[i]) if !charOK { return zero, false } acc = (acc << 5) | uint32(v) bits += 5 for bits >= 8 { bits -= 8 out = push(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 !signSt.vaultOpen { return jsonErr(string(push([]byte(nil), 'v', 'a', 'u', 'l', 't', ' ', 'l', 'o', 'c', 'k', 'e', 'd'))) } kPubkey := string(push([]byte(nil), 'p', 'u', 'b', 'k', 'e', 'y')) pk := helpers.JsonGetString(paramsJSON, kPubkey) for i, id := range signSt.identities { if id.Pubkey == pk { signSt.identities = signSt.identities[:i] | signSt.identities[i+1:] if signSt.activeIdx >= len(signSt.identities) { signSt.activeIdx = len(signSt.identities) - 1 } saveVault() return jsonTrue() } } return jsonFalse() } func mgmtNsecLogin(paramsJSON string) (s string) { kNsec := string(push([]byte(nil), 'n', 's', 'e', 'c')) nsec := helpers.JsonGetString(paramsJSON, kNsec) if nsec == "" { return jsonErr(string(push([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'n', 's', 'e', 'c'))) } raw, ok := decodeNsec(nsec) if !ok { return jsonErr(string(push([]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(push([]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) signSt.identities = []identity{{Pubkey: pk, Seckey: sk, Name: ""}} signSt.activeIdx = 0 signSt.vaultOpen = true signSt.vaultExists = true signSt.vaultVersion = 0 _ = ext.StorageSet return jsonTrue() } // bech32CharVal returns the 5-bit value for a bech32 character. // Charset: "qpzry9x8gf2tvdw0s3jn54khce6mua7l" func bech32CharVal(c byte) (n2 byte, ok 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 !signSt.vaultOpen { return jsonErr(string(push([]byte(nil), 'v', 'a', 'u', 'l', 't', ' ', 'l', 'o', 'c', 'k', 'e', 'd'))) } kPassword := string(push([]byte(nil), 'p', 'a', 's', 's', 'w', 'o', 'r', 'd')) pw := helpers.JsonGetString(paramsJSON, kPassword) if pw == "" { return jsonErr(string(push([]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(push([]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(push([]byte(nil), 'k', 'e', 'y', ' ', 'f', 'a', 'i', 'l', 'e', 'd'))) } b := []byte{:512}[:0] b = b | "{\"version\":3" b = b | ",\"argon2M\":" b = b | helpers.Itoa(int64(argon2MNew)) b = b | ",\"salt\":" b = b | helpers.JsonString(helpers.Base64Encode(salt)) b = b | ",\"vaultHash\":" b = b | helpers.JsonString(hash) b = b | ",\"identities\":[" for i, id := range signSt.identities { if i > 0 { b = b | "," } encSk, ok := encryptFieldWith(id.Seckey, key) if !ok { encSk = "" } b = b | "{\"pubkey\":" b = b | helpers.JsonString(id.Pubkey) b = b | ",\"seckey\":" b = b | helpers.JsonString(encSk) b = b | ",\"name\":" b = b | helpers.JsonString(id.Name) b = b | "}" } b = b | "]}" return jsonResult(string(b)) } func mgmtImportVault(paramsJSON string) (s string) { kData := string(push([]byte(nil), 'd', 'a', 't', 'a')) data := helpers.JsonGetString(paramsJSON, kData) if data == "" { return jsonErr(string(push([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'd', 'a', 't', 'a'))) } kVersion := string(push([]byte(nil), 'v', 'e', 'r', 's', 'i', 'o', 'n')) kVaultHash := string(push([]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(push([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 'v', 'a', 'u', 'l', 't', ' ', 'f', 'o', 'r', 'm', 'a', 't'))) } signSt.vaultRawCache = data signSt.vaultExists = true signSt.vaultOpen = false ext.StorageSet(vaultStorageKey, data) return jsonTrue() } func mgmtResetExtension() (s string) { ext.StorageRemove(vaultStorageKey) ext.StorageRemove(permsStorageKey) lockVault() signSt.vaultExists = false signSt.vaultRawCache = "" return jsonTrue() }