decred.org/dcrwallet/v3@v3.1.0/internal/prompt/prompt.go (about) 1 // Copyright (c) 2015-2016 The btcsuite developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package prompt 6 7 import ( 8 "bufio" 9 "bytes" 10 "encoding/hex" 11 "fmt" 12 "io" 13 "os" 14 "strings" 15 "unicode" 16 17 "decred.org/dcrwallet/v3/errors" 18 "decred.org/dcrwallet/v3/walletseed" 19 "github.com/decred/dcrd/hdkeychain/v3" 20 "golang.org/x/term" 21 ) 22 23 // ProvideSeed is used to prompt for the wallet seed which maybe required during 24 // upgrades. 25 func ProvideSeed() ([]byte, error) { 26 reader := bufio.NewReader(os.Stdin) 27 for { 28 fmt.Print("Enter existing wallet seed: ") 29 seedStr, err := reader.ReadString('\n') 30 if err != nil { 31 return nil, err 32 } 33 seedStr = strings.TrimSpace(strings.ToLower(seedStr)) 34 35 seed, err := hex.DecodeString(seedStr) 36 if err != nil || len(seed) < hdkeychain.MinSeedBytes || 37 len(seed) > hdkeychain.MaxSeedBytes { 38 39 fmt.Printf("Invalid seed specified. Must be a "+ 40 "hexadecimal value that is at least %d bits and "+ 41 "at most %d bits\n", hdkeychain.MinSeedBytes*8, 42 hdkeychain.MaxSeedBytes*8) 43 continue 44 } 45 46 return seed, nil 47 } 48 } 49 50 // ProvidePrivPassphrase is used to prompt for the private passphrase which 51 // maybe required during upgrades. 52 func ProvidePrivPassphrase() ([]byte, error) { 53 prompt := "Enter the private passphrase of your wallet: " 54 for { 55 fmt.Print(prompt) 56 pass, err := term.ReadPassword(int(os.Stdin.Fd())) 57 if err != nil { 58 return nil, err 59 } 60 fmt.Print("\n") 61 pass = bytes.TrimSpace(pass) 62 if len(pass) == 0 { 63 continue 64 } 65 66 return pass, nil 67 } 68 } 69 70 // promptList prompts the user with the given prefix, list of valid responses, 71 // and default list entry to use. The function will repeat the prompt to the 72 // user until they enter a valid response. 73 func promptList(reader *bufio.Reader, prefix string, validResponses []string, defaultEntry string) (string, error) { 74 // Setup the prompt according to the parameters. 75 validStrings := strings.Join(validResponses, "/") 76 var prompt string 77 if defaultEntry != "" { 78 prompt = fmt.Sprintf("%s (%s) [%s]: ", prefix, validStrings, 79 defaultEntry) 80 } else { 81 prompt = fmt.Sprintf("%s (%s): ", prefix, validStrings) 82 } 83 84 // Prompt the user until one of the valid responses is given. 85 for { 86 fmt.Print(prompt) 87 reply, err := reader.ReadString('\n') 88 if err != nil { 89 return "", err 90 } 91 reply = strings.TrimSpace(strings.ToLower(reply)) 92 if reply == "" { 93 reply = defaultEntry 94 } 95 96 for _, validResponse := range validResponses { 97 if reply == validResponse { 98 return reply, nil 99 } 100 } 101 } 102 } 103 104 // promptListBool prompts the user for a boolean (yes/no) with the given prefix. 105 // The function will repeat the prompt to the user until they enter a valid 106 // response. 107 func promptListBool(reader *bufio.Reader, prefix string, defaultEntry string) (bool, error) { 108 // Setup the valid responses. 109 valid := []string{"n", "no", "y", "yes"} 110 response, err := promptList(reader, prefix, valid, defaultEntry) 111 if err != nil { 112 return false, err 113 } 114 return response == "yes" || response == "y", nil 115 } 116 117 // PassPrompt prompts the user for a passphrase with the given prefix. The 118 // function will ask the user to confirm the passphrase and will repeat the 119 // prompts until they enter a matching response. 120 func PassPrompt(reader *bufio.Reader, prefix string, confirm bool) ([]byte, error) { 121 // Prompt the user until they enter a passphrase. 122 prompt := fmt.Sprintf("%s: ", prefix) 123 for { 124 fmt.Print(prompt) 125 var pass []byte 126 var err error 127 fd := int(os.Stdin.Fd()) 128 if term.IsTerminal(fd) { 129 pass, err = term.ReadPassword(fd) 130 } else { 131 pass, err = reader.ReadBytes('\n') 132 if errors.Is(err, io.EOF) { 133 err = nil 134 } 135 } 136 if err != nil { 137 return nil, err 138 } 139 fmt.Print("\n") 140 pass = bytes.TrimSpace(pass) 141 if len(pass) == 0 { 142 continue 143 } 144 145 if !confirm { 146 return pass, nil 147 } 148 149 fmt.Print("Confirm passphrase: ") 150 var confirm []byte 151 if term.IsTerminal(fd) { 152 confirm, err = term.ReadPassword(fd) 153 } else { 154 confirm, err = reader.ReadBytes('\n') 155 if errors.Is(err, io.EOF) { 156 err = nil 157 } 158 } 159 if err != nil { 160 return nil, err 161 } 162 fmt.Print("\n") 163 confirm = bytes.TrimSpace(confirm) 164 if !bytes.Equal(pass, confirm) { 165 fmt.Println("The entered passphrases do not match") 166 continue 167 } 168 169 return pass, nil 170 } 171 } 172 173 // PrivatePass prompts the user for a private passphrase. All prompts are 174 // repeated until the user enters a valid response. 175 func PrivatePass(reader *bufio.Reader, configPass []byte) ([]byte, error) { 176 if len(configPass) > 0 { 177 useExisting, err := promptListBool(reader, "Use the "+ 178 "existing configured private passphrase for "+ 179 "wallet encryption?", "no") 180 if err != nil { 181 return nil, err 182 } 183 if useExisting { 184 return configPass, nil 185 } 186 } 187 return PassPrompt(reader, "Enter the private passphrase for your new wallet", true) 188 } 189 190 // PublicPass prompts the user whether they want to add an additional layer of 191 // encryption to the wallet. When the user answers yes and there is already a 192 // public passphrase provided via the passed config, it prompts them whether or 193 // not to use that configured passphrase. It will also detect when the same 194 // passphrase is used for the private and public passphrase and prompt the user 195 // if they are sure they want to use the same passphrase for both. Finally, all 196 // prompts are repeated until the user enters a valid response. 197 func PublicPass(reader *bufio.Reader, privPass []byte, 198 defaultPubPassphrase, configPubPass []byte) ([]byte, error) { 199 200 pubPass := defaultPubPassphrase 201 usePubPass, err := promptListBool(reader, "Do you want "+ 202 "to add an additional layer of encryption for public "+ 203 "data?", "no") 204 if err != nil { 205 return nil, err 206 } 207 208 if !usePubPass { 209 return pubPass, nil 210 } 211 212 if len(configPubPass) != 0 && !bytes.Equal(configPubPass, pubPass) { 213 useExisting, err := promptListBool(reader, "Use the "+ 214 "existing configured public passphrase for encryption "+ 215 "of public data?", "no") 216 if err != nil { 217 return nil, err 218 } 219 220 if useExisting { 221 return configPubPass, nil 222 } 223 } 224 225 for { 226 pubPass, err = PassPrompt(reader, "Enter the public "+ 227 "passphrase for your new wallet", true) 228 if err != nil { 229 return nil, err 230 } 231 232 if bytes.Equal(pubPass, privPass) { 233 useSamePass, err := promptListBool(reader, 234 "Are you sure want to use the same passphrase "+ 235 "for public and private data?", "no") 236 if err != nil { 237 return nil, err 238 } 239 240 if useSamePass { 241 break 242 } 243 244 continue 245 } 246 247 break 248 } 249 250 fmt.Println("NOTE: Use the --walletpass option to configure your " + 251 "public passphrase.") 252 return pubPass, nil 253 } 254 255 // Seed prompts the user whether they want to use an existing wallet generation 256 // seed. When the user answers no, a seed will be generated and displayed to 257 // the user along with prompting them for confirmation. When the user answers 258 // yes, a the user is prompted for it. All prompts are repeated until the user 259 // enters a valid response. The bool returned indicates if the wallet was 260 // restored from a given seed or not. 261 func Seed(reader *bufio.Reader) (seed []byte, imported bool, err error) { 262 // Ascertain the wallet generation seed. 263 useUserSeed, err := promptListBool(reader, "Do you have an "+ 264 "existing wallet seed you want to use?", "no") 265 if err != nil { 266 return nil, false, err 267 } 268 if !useUserSeed { 269 seed, err := hdkeychain.GenerateSeed(hdkeychain.RecommendedSeedLen) 270 if err != nil { 271 return nil, false, err 272 } 273 274 seedStrSplit := walletseed.EncodeMnemonicSlice(seed) 275 276 fmt.Println("Your wallet generation seed is:") 277 for i := 0; i < hdkeychain.RecommendedSeedLen+1; i++ { 278 fmt.Printf("%v ", seedStrSplit[i]) 279 280 if (i+1)%6 == 0 { 281 fmt.Printf("\n") 282 } 283 } 284 285 fmt.Printf("\n\nHex: %x\n", seed) 286 fmt.Println("IMPORTANT: Keep the seed in a safe place as you\n" + 287 "will NOT be able to restore your wallet without it.") 288 fmt.Println("Please keep in mind that anyone who has access\n" + 289 "to the seed can also restore your wallet thereby\n" + 290 "giving them access to all your funds, so it is\n" + 291 "imperative that you keep it in a secure location.") 292 293 for { 294 fmt.Print(`Once you have stored the seed in a safe ` + 295 `and secure location, enter "OK" to continue: `) 296 confirmSeed, err := reader.ReadString('\n') 297 if err != nil { 298 return nil, false, err 299 } 300 confirmSeed = strings.TrimSpace(confirmSeed) 301 confirmSeed = strings.Trim(confirmSeed, `"`) 302 if strings.EqualFold("OK", confirmSeed) { 303 break 304 } 305 } 306 307 return seed, false, nil 308 } 309 310 for { 311 fmt.Print("Enter existing wallet seed " + 312 "(follow seed words with additional blank line): ") 313 314 // Use scanner instead of buffio.Reader so we can choose choose 315 // more complicated ending condition rather than just a single 316 // newline. 317 var seedStr string 318 scanner := bufio.NewScanner(reader) 319 for firstline := true; scanner.Scan(); { 320 line := scanner.Text() 321 if line == "" { 322 break 323 } 324 if firstline { 325 _, err := hex.DecodeString(line) 326 if err == nil { 327 seedStr = line 328 break 329 } 330 firstline = false 331 } 332 seedStr += " " + line 333 } 334 seedStrTrimmed := strings.TrimSpace(seedStr) 335 seedStrTrimmed = collapseSpace(seedStrTrimmed) 336 wordCount := strings.Count(seedStrTrimmed, " ") + 1 337 338 var seed []byte 339 if wordCount == 1 { 340 if len(seedStrTrimmed)%2 != 0 { 341 seedStrTrimmed = "0" + seedStrTrimmed 342 } 343 seed, err = hex.DecodeString(seedStrTrimmed) 344 if err != nil { 345 fmt.Printf("Input error: %v\n", err.Error()) 346 } 347 } else { 348 seed, err = walletseed.DecodeUserInput(seedStrTrimmed) 349 if err != nil { 350 fmt.Printf("Input error: %v\n", err.Error()) 351 } 352 } 353 if err != nil || len(seed) < hdkeychain.MinSeedBytes || 354 len(seed) > hdkeychain.MaxSeedBytes { 355 fmt.Printf("Invalid seed specified. Must be a "+ 356 "word seed (usually 33 words) using the PGP wordlist or "+ 357 "hexadecimal value that is at least %d bits and "+ 358 "at most %d bits\n", hdkeychain.MinSeedBytes*8, 359 hdkeychain.MaxSeedBytes*8) 360 continue 361 } 362 363 fmt.Printf("\nSeed input successful. \nHex: %x\n", seed) 364 365 return seed, true, nil 366 } 367 } 368 369 // collapseSpace takes a string and replaces any repeated areas of whitespace 370 // with a single space character. 371 func collapseSpace(in string) string { 372 whiteSpace := false 373 out := "" 374 for _, c := range in { 375 if unicode.IsSpace(c) { 376 if !whiteSpace { 377 out = out + " " 378 } 379 whiteSpace = true 380 } else { 381 out = out + string(c) 382 whiteSpace = false 383 } 384 } 385 return out 386 }