package main import ( "crypto/bip32" "crypto/bip39" "git.smesh.lol/smesh/web/common/helpers" "git.smesh.lol/smesh/web/common/jsbridge/schnorr" "git.smesh.lol/smesh/web/common/jsbridge/subtle" ) // HD keychain wraps moxie stdlib BIP-39 + BIP-32 (crypto/bip39 + crypto/secp256k1) // with smesh's WASM bridge for entropy and JSON handling. var ( hdMnemonic string // decrypted BIP-39 phrase, "" when not an HD vault hdNextAccount int32 // next unused account index ) func mgmtGenerateMnemonic() (s string) { entropy := []byte{:16} subtle.RandomBytes(entropy) return jsonResult(helpers.JsonString(bip39.EntropyToMnemonic(entropy))) } func mgmtValidateMnemonic(paramsJSON string) (s string) { kMnemonic := string(append([]byte(nil), 'm', 'n', 'e', 'm', 'o', 'n', 'i', 'c')) phrase := helpers.JsonGetString(paramsJSON, kMnemonic) if bip39.ValidateMnemonic(phrase) { return jsonTrue() } return jsonFalse() } func mgmtGetMnemonic() (s string) { if hdMnemonic == "" { return jsonResult(string(append([]byte(nil), '"', '"'))) } return jsonResult(helpers.JsonString(hdMnemonic)) } func mgmtIsHD() (s string) { if hdMnemonic != "" { return jsonTrue() } return jsonFalse() } func mgmtCreateHDVault(paramsJSON string) (s string) { kPassword := string(append([]byte(nil), 'p', 'a', 's', 's', 'w', 'o', 'r', 'd')) kMnemonic := string(append([]byte(nil), 'm', 'n', 'e', 'm', 'o', 'n', 'i', 'c')) pw := helpers.JsonGetString(paramsJSON, kPassword) phrase := helpers.JsonGetString(paramsJSON, kMnemonic) if pw == "" { return jsonErr(string(append([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'))) } if phrase == "" { entropy := []byte{:16} subtle.RandomBytes(entropy) phrase = bip39.EntropyToMnemonic(entropy) } else if !bip39.ValidateMnemonic(phrase) { // Reject invalid input. Otherwise PBKDF2-SHA512 happily produces a // deterministic seed from any string, which would silently give every // caller the same key for the same garbage input. return jsonErr(string(append([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 'm', 'n', 'e', 'm', 'o', 'n', 'i', 'c'))) } seed := bip39.MnemonicToSeed(phrase, "") if seed == nil { return jsonErr(string(append([]byte(nil), 's', 'e', 'e', 'd', ' ', 'd', 'e', 'r', 'i', 'v', 'a', 't', 'i', 'o', 'n', ' ', 'f', 'a', 'i', 'l', 'e', 'd'))) } key, _ := bip32.DerivePath(seed, bip32.NostrPath(0)) if key == nil { 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'))) } pkBytes, ok := schnorr.PubKeyFromSecKey(key) if !ok { return jsonErr(string(append([]byte(nil), 'p', 'u', 'b', 'k', 'e', 'y', ' ', 'f', 'a', 'i', 'l', 'e', 'd'))) } if !createVault(pw) { return jsonErr(string(append([]byte(nil), 'v', 'a', 'u', 'l', 't', ' ', 'c', 'r', 'e', 'a', 't', 'i', 'o', 'n', ' ', 'f', 'a', 'i', 'l', 'e', 'd'))) } identities = []identity{{Pubkey: helpers.HexEncode(pkBytes), Seckey: helpers.HexEncode(key), Name: ""}} activeIdx = 0 hdMnemonic = phrase hdNextAccount = 1 saveVault() return jsonTrue() } func mgmtRestoreHDVault(paramsJSON string) (s string) { kPassword := string(append([]byte(nil), 'p', 'a', 's', 's', 'w', 'o', 'r', 'd')) kMnemonic := string(append([]byte(nil), 'm', 'n', 'e', 'm', 'o', 'n', 'i', 'c')) pw := helpers.JsonGetString(paramsJSON, kPassword) phrase := helpers.JsonGetString(paramsJSON, kMnemonic) if pw == "" || phrase == "" { return jsonErr(string(append([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'p', 'a', 'r', 'a', 'm', 's'))) } if !bip39.ValidateMnemonic(phrase) { return jsonErr(string(append([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 'm', 'n', 'e', 'm', 'o', 'n', 'i', 'c'))) } pb := append([]byte(nil), '{', '"', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd', '"', ':') pb = pb | helpers.JsonString(pw) pb = append(pb, ',', '"', 'm', 'n', 'e', 'm', 'o', 'n', 'i', 'c', '"', ':') pb = pb | helpers.JsonString(phrase) pb = append(pb, '}') return mgmtCreateHDVault(string(pb)) } func mgmtDeriveIdentity(paramsJSON string) (s string) { if hdMnemonic == "" { return jsonErr(string(append([]byte(nil), 'n', 'o', 't', ' ', 'a', 'n', ' ', 'H', 'D', ' ', 'v', 'a', 'u', 'l', 't'))) } kAccount := string(append([]byte(nil), 'a', 'c', 'c', 'o', 'u', 'n', 't')) accountRaw := helpers.JsonGetValue(paramsJSON, kAccount) account := 0 for i := 0; i < len(accountRaw); i++ { c := accountRaw[i] if c >= '0' && c <= '9' { account = account*10 + int32(c-'0') } } seed := bip39.MnemonicToSeed(hdMnemonic, "") key, _ := bip32.DerivePath(seed, bip32.NostrPath(account)) if key == nil { return jsonErr(string(append([]byte(nil), 'd', 'e', 'r', 'i', 'v', 'a', 't', 'i', 'o', 'n', ' ', 'f', 'a', 'i', 'l', 'e', 'd'))) } pkBytes, ok := schnorr.PubKeyFromSecKey(key) if !ok { return jsonErr(string(append([]byte(nil), 'p', 'u', 'b', 'k', 'e', 'y', ' ', 'f', 'a', 'i', 'l', 'e', 'd'))) } pk := helpers.HexEncode(pkBytes) identities = append(identities, identity{Pubkey: pk, Seckey: helpers.HexEncode(key)}) if account >= hdNextAccount { hdNextAccount = account + 1 } saveVault() return jsonResult(helpers.JsonString(pk)) } func mgmtProbeAccount(paramsJSON string) (s string) { if hdMnemonic == "" { return jsonErr(string(append([]byte(nil), 'n', 'o', 't', ' ', 'a', 'n', ' ', 'H', 'D', ' ', 'v', 'a', 'u', 'l', 't'))) } kAccounts := string(append([]byte(nil), 'a', 'c', 'c', 'o', 'u', 'n', 't', 's')) accounts := helpers.JsonGetIntArray(paramsJSON, kAccounts) if len(accounts) == 0 { return jsonResult(string(append([]byte(nil), '[', ']'))) } seed := bip39.MnemonicToSeed(hdMnemonic, "") if seed == nil { return jsonErr(string(append([]byte(nil), 's', 'e', 'e', 'd', ' ', 'f', 'a', 'i', 'l', 'e', 'd'))) } pubkeys := []string{:0:len(accounts)} for _, acct := range accounts { key, _ := bip32.DerivePath(seed, bip32.NostrPath(int32(acct))) if key == nil { pubkeys = append(pubkeys, "") continue } pkBytes, ok := schnorr.PubKeyFromSecKey(key) if !ok { pubkeys = append(pubkeys, "") } else { pubkeys = append(pubkeys, helpers.HexEncode(pkBytes)) } } b := append([]byte(nil), '[') for i, pk := range pubkeys { if i > 0 { b = append(b, ',') } b = b | helpers.JsonString(pk) } b = append(b, ']') return jsonResult(string(b)) }