1 package prompt
2 3 import (
4 "bufio"
5 "bytes"
6 "encoding/hex"
7 "fmt"
8 "os"
9 "strings"
10 11 "github.com/btcsuite/golangcrypto/ssh/terminal"
12 13 "github.com/p9c/p9/pkg/util/hdkeychain"
14 "github.com/p9c/p9/pkg/util/legacy/keystore"
15 )
16 17 // ProvideSeed is used to prompt for the wallet seed which maybe required during upgrades.
18 func ProvideSeed() ([]byte, error) {
19 reader := bufio.NewReader(os.Stdin)
20 for {
21 fmt.Print("Enter existing wallet seed: ")
22 seedStr, e := reader.ReadString('\n')
23 if e != nil {
24 return nil, e
25 }
26 seedStr = strings.TrimSpace(strings.ToLower(seedStr))
27 seed, e := hex.DecodeString(seedStr)
28 if e != nil || len(seed) < hdkeychain.MinSeedBytes ||
29 len(seed) > hdkeychain.MaxSeedBytes {
30 E.F(
31 "Invalid seed specified. "+
32 "Must be a hexadecimal value that is at least %d bits and at"+
33 " most %d bits", hdkeychain.MinSeedBytes*8,
34 hdkeychain.MaxSeedBytes*8,
35 )
36 continue
37 }
38 return seed, nil
39 }
40 }
41 42 // ProvidePrivPassphrase is used to prompt for the private passphrase which maybe required during upgrades.
43 func ProvidePrivPassphrase() ([]byte, error) {
44 prompt := "enter the private passphrase for your wallet: "
45 for {
46 fmt.Print(prompt)
47 pass, e := terminal.ReadPassword(int(os.Stdin.Fd()))
48 if e != nil {
49 return nil, e
50 }
51 fmt.Print("\n")
52 pass = bytes.TrimSpace(pass)
53 if len(pass) == 0 {
54 continue
55 }
56 return pass, nil
57 }
58 }
59 60 // promptList prompts the user with the given prefix, list of valid responses, and default list entry to use. The
61 // function will repeat the prompt to the user until they enter a valid response.
62 func promptList(reader *bufio.Reader, prefix string, validResponses []string, defaultEntry string) (string, error) {
63 // Setup the prompt according to the parameters.
64 validStrings := strings.Join(validResponses, "/")
65 var prompt string
66 if defaultEntry != "" {
67 prompt = fmt.Sprintf(
68 "%s (%s) [%s]: ", prefix, validStrings,
69 defaultEntry,
70 )
71 } else {
72 prompt = fmt.Sprintf("%s (%s): ", prefix, validStrings)
73 }
74 // Prompt the user until one of the valid responses is given.
75 for {
76 fmt.Print(prompt)
77 reply, e := reader.ReadString('\n')
78 if e != nil {
79 return "", e
80 }
81 reply = strings.TrimSpace(strings.ToLower(reply))
82 if reply == "" {
83 reply = defaultEntry
84 }
85 for _, validResponse := range validResponses {
86 if reply == validResponse {
87 return reply, nil
88 }
89 }
90 }
91 }
92 93 // promptListBool prompts the user for a boolean (yes/no) with the given prefix. The function will repeat the prompt to
94 // the user until they enter a valid reponse.
95 func promptListBool(reader *bufio.Reader, prefix string, defaultEntry string) (bool, error) {
96 // Setup the valid responses.
97 valid := []string{"n", "no", "y", "yes"}
98 response, e := promptList(reader, prefix, valid, defaultEntry)
99 if e != nil {
100 return false, e
101 }
102 return response == "yes" || response == "y", nil
103 }
104 105 // promptPass prompts the user for a passphrase with the given prefix. The function will ask the user to confirm the
106 // passphrase and will repeat the prompts until they enter a matching response.
107 func promptPass(reader *bufio.Reader, prefix string, confirm bool) ([]byte, error) {
108 // Prompt the user until they enter a passphrase.
109 prompt := fmt.Sprintf("%s: ", prefix)
110 for {
111 fmt.Print(prompt)
112 pass, e := terminal.ReadPassword(int(os.Stdin.Fd()))
113 if e != nil {
114 return nil, e
115 }
116 fmt.Print("\n")
117 pass = bytes.TrimSpace(pass)
118 if len(pass) == 0 {
119 continue
120 }
121 if !confirm {
122 return pass, nil
123 }
124 fmt.Print("Confirm passphrase: ")
125 confirm, e := terminal.ReadPassword(int(os.Stdin.Fd()))
126 if e != nil {
127 return nil, e
128 }
129 fmt.Print("\n")
130 confirm = bytes.TrimSpace(confirm)
131 if !bytes.Equal(pass, confirm) {
132 E.Ln("The entered passphrases do not match")
133 continue
134 }
135 return pass, nil
136 }
137 }
138 139 // PrivatePass prompts the user for a private passphrase with varying behavior depending on whether the passed legacy
140 // keystore exists. When it does, the user is prompted for the existing passphrase which is then used to unlock it.
141 //
142 // On the other hand, when the legacy keystore is nil, the user is prompted for a new private passphrase. All prompts
143 // are repeated until the user enters a valid response.
144 func PrivatePass(reader *bufio.Reader, legacyKeyStore *keystore.Store) ([]byte, error) {
145 // When there is not an existing legacy wallet, simply prompt the user for a new private passphase and return it.
146 if legacyKeyStore == nil {
147 return promptPass(
148 reader,
149 "Creating new wallet\n\nEnter the private passphrase for your new wallet", true,
150 )
151 }
152 // At this point, there is an existing legacy wallet, so prompt the user for the existing private passphrase and
153 // ensure it properly unlocks the legacy wallet so all of the addresses can later be imported.
154 W.Ln(
155 "You have an existing legacy wallet. " +
156 "All addresses from your existing legacy wallet will be imported into" +
157 " the new wallet format.",
158 )
159 for {
160 privPass, e := promptPass(reader, "Enter the private passphrase for your existing wallet", false)
161 if e != nil {
162 return nil, e
163 }
164 // Keep prompting the user until the passphrase is correct.
165 if e := legacyKeyStore.Unlock(privPass); E.Chk(e) {
166 if e == keystore.ErrWrongPassphrase {
167 continue
168 }
169 return nil, e
170 }
171 return privPass, nil
172 }
173 }
174 175 // PublicPass prompts the user whether they want to add an additional layer of encryption to the wallet. When the user
176 // answers yes and there is already a public passphrase provided via the passed config, it prompts them whether or not
177 // to use that configured passphrase.
178 //
179 // It will also detect when the same passphrase is used for the private and public passphrase and prompt the user if
180 // they are sure they want to use the same passphrase for both. Finally, all prompts are repeated until the user enters
181 // a valid response.
182 func PublicPass(
183 reader *bufio.Reader, privPass []byte,
184 defaultPubPassphrase, configPubPassphrase []byte,
185 ) ([]byte, error) {
186 pubPass := defaultPubPassphrase
187 usePubPass, e := promptListBool(
188 reader, "Do you want "+
189 "to add an additional layer of encryption for public "+
190 "data?", "no",
191 )
192 if e != nil {
193 return nil, e
194 }
195 if !usePubPass {
196 return pubPass, nil
197 }
198 if !bytes.Equal(configPubPassphrase, pubPass) {
199 var useExisting bool
200 useExisting, e = promptListBool(
201 reader, "Use the "+
202 "existing configured public passphrase for encryption "+
203 "of public data?", "no",
204 )
205 if e != nil {
206 return nil, e
207 }
208 if useExisting {
209 return configPubPassphrase, nil
210 }
211 }
212 for {
213 pubPass, e = promptPass(
214 reader, "Enter the public "+
215 "passphrase for your new wallet", true,
216 )
217 if e != nil {
218 return nil, e
219 }
220 if bytes.Equal(pubPass, privPass) {
221 useSamePass, e := promptListBool(
222 reader,
223 "Are you sure want to use the same passphrase "+
224 "for public and private data?", "no",
225 )
226 if e != nil {
227 return nil, e
228 }
229 if useSamePass {
230 break
231 }
232 continue
233 }
234 break
235 }
236 fmt.Println(
237 "NOTE: Use the --walletpass opt to configure your " +
238 "public passphrase.",
239 )
240 return pubPass, nil
241 }
242 243 // Seed prompts the user whether they want to use an existing wallet generation seed.
244 //
245 // When the user answers no, a seed will be generated and displayed to the user along with prompting them for
246 // confirmation.
247 //
248 // When the user answers yes, a the user is prompted for it.
249 //
250 // All prompts are repeated until the user enters a valid response.
251 func Seed(reader *bufio.Reader) ([]byte, error) {
252 // Ascertain the wallet generation seed.
253 useUserSeed, e := promptListBool(
254 reader, "Do you have an "+
255 "existing wallet seed you want to use?", "no",
256 )
257 if e != nil {
258 return nil, e
259 }
260 if !useUserSeed {
261 seed, e := hdkeychain.GenerateSeed(hdkeychain.RecommendedSeedLen)
262 if e != nil {
263 return nil, e
264 }
265 fmt.Println("\nYour wallet generation seed is:")
266 fmt.Printf("\n%x\n\n", seed)
267 fmt.Print(
268 "IMPORTANT: Keep the seed in a safe place as you will NOT be" +
269 " able to restore your wallet without it.\n\n",
270 )
271 fmt.Print(
272 "Please keep in mind that anyone who has access to the seed" +
273 " can also restore your wallet thereby giving them access to all your funds, so it is imperative that you keep it in a secure location.\n\n",
274 )
275 for {
276 fmt.Print(
277 `Once you have stored the seed in a safe ` +
278 `and secure location, enter "OK" to continue: `,
279 )
280 confirmSeed, e := reader.ReadString('\n')
281 if e != nil {
282 return nil, e
283 }
284 confirmSeed = strings.TrimSpace(confirmSeed)
285 confirmSeed = strings.Trim(confirmSeed, `"`)
286 if confirmSeed == "OK" {
287 break
288 }
289 }
290 return seed, nil
291 }
292 for {
293 fmt.Print("Enter existing wallet seed: ")
294 seedStr, e := reader.ReadString('\n')
295 if e != nil {
296 return nil, e
297 }
298 seedStr = strings.TrimSpace(strings.ToLower(seedStr))
299 seed, e := hex.DecodeString(seedStr)
300 if e != nil || len(seed) < hdkeychain.MinSeedBytes ||
301 len(seed) > hdkeychain.MaxSeedBytes {
302 E.F(
303 "Invalid seed specified. Must be a "+
304 "hexadecimal value that is at least %d bits and "+
305 "at most %d bits\n", hdkeychain.MinSeedBytes*8,
306 hdkeychain.MaxSeedBytes*8,
307 )
308 continue
309 }
310 return seed, nil
311 }
312 }
313