1 package main
2
3 import (
4 "crypto/bip32"
5 "crypto/bip39"
6
7 "git.smesh.lol/smesh/web/common/helpers"
8 "git.smesh.lol/smesh/web/common/jsbridge/schnorr"
9 "git.smesh.lol/smesh/web/common/jsbridge/subtle"
10 )
11
12 // HD keychain wraps moxie stdlib BIP-39 + BIP-32 (crypto/bip39 + crypto/secp256k1)
13 // with smesh's WASM bridge for entropy and JSON handling.
14
15 var (
16 hdMnemonic string // decrypted BIP-39 phrase, "" when not an HD vault
17 hdNextAccount int32 // next unused account index
18 )
19
20 func mgmtGenerateMnemonic() (s string) {
21 entropy := []byte{:16}
22 subtle.RandomBytes(entropy)
23 return jsonResult(helpers.JsonString(bip39.EntropyToMnemonic(entropy)))
24 }
25
26 func mgmtValidateMnemonic(paramsJSON string) (s string) {
27 kMnemonic := string(append([]byte(nil), 'm', 'n', 'e', 'm', 'o', 'n', 'i', 'c'))
28 phrase := helpers.JsonGetString(paramsJSON, kMnemonic)
29 if bip39.ValidateMnemonic(phrase) {
30 return jsonTrue()
31 }
32 return jsonFalse()
33 }
34
35 func mgmtGetMnemonic() (s string) {
36 if hdMnemonic == "" {
37 return jsonResult(string(append([]byte(nil), '"', '"')))
38 }
39 return jsonResult(helpers.JsonString(hdMnemonic))
40 }
41
42 func mgmtIsHD() (s string) {
43 if hdMnemonic != "" {
44 return jsonTrue()
45 }
46 return jsonFalse()
47 }
48
49 func mgmtCreateHDVault(paramsJSON string) (s string) {
50 kPassword := string(append([]byte(nil), 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'))
51 kMnemonic := string(append([]byte(nil), 'm', 'n', 'e', 'm', 'o', 'n', 'i', 'c'))
52 pw := helpers.JsonGetString(paramsJSON, kPassword)
53 phrase := helpers.JsonGetString(paramsJSON, kMnemonic)
54 if pw == "" {
55 return jsonErr(string(append([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd')))
56 }
57 if phrase == "" {
58 entropy := []byte{:16}
59 subtle.RandomBytes(entropy)
60 phrase = bip39.EntropyToMnemonic(entropy)
61 } else if !bip39.ValidateMnemonic(phrase) {
62 // Reject invalid input. Otherwise PBKDF2-SHA512 happily produces a
63 // deterministic seed from any string, which would silently give every
64 // caller the same key for the same garbage input.
65 return jsonErr(string(append([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 'm', 'n', 'e', 'm', 'o', 'n', 'i', 'c')))
66 }
67 seed := bip39.MnemonicToSeed(phrase, "")
68 if seed == nil {
69 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')))
70 }
71 key, _ := bip32.DerivePath(seed, bip32.NostrPath(0))
72 if key == nil {
73 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')))
74 }
75 pkBytes, ok := schnorr.PubKeyFromSecKey(key)
76 if !ok {
77 return jsonErr(string(append([]byte(nil), 'p', 'u', 'b', 'k', 'e', 'y', ' ', 'f', 'a', 'i', 'l', 'e', 'd')))
78 }
79 if !createVault(pw) {
80 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')))
81 }
82 identities = []identity{{Pubkey: helpers.HexEncode(pkBytes), Seckey: helpers.HexEncode(key), Name: ""}}
83 activeIdx = 0
84 hdMnemonic = phrase
85 hdNextAccount = 1
86 saveVault()
87 return jsonTrue()
88 }
89
90 func mgmtRestoreHDVault(paramsJSON string) (s string) {
91 kPassword := string(append([]byte(nil), 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'))
92 kMnemonic := string(append([]byte(nil), 'm', 'n', 'e', 'm', 'o', 'n', 'i', 'c'))
93 pw := helpers.JsonGetString(paramsJSON, kPassword)
94 phrase := helpers.JsonGetString(paramsJSON, kMnemonic)
95 if pw == "" || phrase == "" {
96 return jsonErr(string(append([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'p', 'a', 'r', 'a', 'm', 's')))
97 }
98 if !bip39.ValidateMnemonic(phrase) {
99 return jsonErr(string(append([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 'm', 'n', 'e', 'm', 'o', 'n', 'i', 'c')))
100 }
101 pb := append([]byte(nil), '{', '"', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd', '"', ':')
102 pb = pb | helpers.JsonString(pw)
103 pb = append(pb, ',', '"', 'm', 'n', 'e', 'm', 'o', 'n', 'i', 'c', '"', ':')
104 pb = pb | helpers.JsonString(phrase)
105 pb = append(pb, '}')
106 return mgmtCreateHDVault(string(pb))
107 }
108
109 func mgmtDeriveIdentity(paramsJSON string) (s string) {
110 if hdMnemonic == "" {
111 return jsonErr(string(append([]byte(nil), 'n', 'o', 't', ' ', 'a', 'n', ' ', 'H', 'D', ' ', 'v', 'a', 'u', 'l', 't')))
112 }
113 kAccount := string(append([]byte(nil), 'a', 'c', 'c', 'o', 'u', 'n', 't'))
114 accountRaw := helpers.JsonGetValue(paramsJSON, kAccount)
115 account := 0
116 for i := 0; i < len(accountRaw); i++ {
117 c := accountRaw[i]
118 if c >= '0' && c <= '9' {
119 account = account*10 + int32(c-'0')
120 }
121 }
122 seed := bip39.MnemonicToSeed(hdMnemonic, "")
123 key, _ := bip32.DerivePath(seed, bip32.NostrPath(account))
124 if key == nil {
125 return jsonErr(string(append([]byte(nil), 'd', 'e', 'r', 'i', 'v', 'a', 't', 'i', 'o', 'n', ' ', 'f', 'a', 'i', 'l', 'e', 'd')))
126 }
127 pkBytes, ok := schnorr.PubKeyFromSecKey(key)
128 if !ok {
129 return jsonErr(string(append([]byte(nil), 'p', 'u', 'b', 'k', 'e', 'y', ' ', 'f', 'a', 'i', 'l', 'e', 'd')))
130 }
131 pk := helpers.HexEncode(pkBytes)
132 identities = append(identities, identity{Pubkey: pk, Seckey: helpers.HexEncode(key)})
133 if account >= hdNextAccount {
134 hdNextAccount = account + 1
135 }
136 saveVault()
137 return jsonResult(helpers.JsonString(pk))
138 }
139
140 func mgmtProbeAccount(paramsJSON string) (s string) {
141 if hdMnemonic == "" {
142 return jsonErr(string(append([]byte(nil), 'n', 'o', 't', ' ', 'a', 'n', ' ', 'H', 'D', ' ', 'v', 'a', 'u', 'l', 't')))
143 }
144 kAccounts := string(append([]byte(nil), 'a', 'c', 'c', 'o', 'u', 'n', 't', 's'))
145 accounts := helpers.JsonGetIntArray(paramsJSON, kAccounts)
146 if len(accounts) == 0 {
147 return jsonResult(string(append([]byte(nil), '[', ']')))
148 }
149 seed := bip39.MnemonicToSeed(hdMnemonic, "")
150 if seed == nil {
151 return jsonErr(string(append([]byte(nil), 's', 'e', 'e', 'd', ' ', 'f', 'a', 'i', 'l', 'e', 'd')))
152 }
153 pubkeys := []string{:0:len(accounts)}
154 for _, acct := range accounts {
155 key, _ := bip32.DerivePath(seed, bip32.NostrPath(int32(acct)))
156 if key == nil {
157 pubkeys = append(pubkeys, "")
158 continue
159 }
160 pkBytes, ok := schnorr.PubKeyFromSecKey(key)
161 if !ok {
162 pubkeys = append(pubkeys, "")
163 } else {
164 pubkeys = append(pubkeys, helpers.HexEncode(pkBytes))
165 }
166 }
167 b := append([]byte(nil), '[')
168 for i, pk := range pubkeys {
169 if i > 0 {
170 b = append(b, ',')
171 }
172 b = b | helpers.JsonString(pk)
173 }
174 b = append(b, ']')
175 return jsonResult(string(b))
176 }
177