cmd/lncli: extend initial wallet creation with aezeed seed support

In this commit, we extend the `lncli create` command to allow users to
specify their own side (if they want). In the case that the user
*doesn’t* specify their own seed, we’ll return the entropy generated by
the wallet in a 24-word mnemonic format for easy backup.

With this change, it’s now possible for users to restore an existing lnd
wallet seed.
This commit is contained in:
Olaoluwa Osuntokun 2018-02-01 20:52:40 -08:00
parent d8ce90306d
commit 3356a370c7
No known key found for this signature in database
GPG Key ID: 964EA263DD637C21

@ -1,6 +1,7 @@
package main package main
import ( import (
"bufio"
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
@ -776,16 +777,63 @@ func listPeers(ctx *cli.Context) error {
} }
var createCommand = cli.Command{ var createCommand = cli.Command{
Name: "create", Name: "create",
Usage: "Used to set the wallet password at lnd startup", Description: `
The create command is used to initialize an lnd wallet from scratch for
the very first time. This is interactive command with one required
argument (the password), and one optional argument (the mnemonic
passphrase).
The first argument (the password) is required and MUST be greater than
8 characters. This will be used to encrypt the wallet within lnd. This
MUST be remembered as it will be required to fully start up the daemon.
The second argument is an optional 24-word mnemonic derived from BIP
39. If provided, then the internal wallet will use the seed derived
from this mnemonic to generate all keys.
This command returns a 24-word seed in the scenario that NO mnemonic
was provided by the user. This should be written down as it can be used
to potentially recover all on-chain funds, and most off-chain funds as
well.
`,
Action: actionDecorator(create), Action: actionDecorator(create),
} }
// monowidthColumns takes a set of words, and the number of desired columns,
// and returns a new set of words that have had white space appended to the
// word in order to create a mono-width column.
func monowidthColumns(words []string, ncols int) []string {
// Determine max size of words in each column.
colWidths := make([]int, ncols)
for i, word := range words {
col := i % ncols
curWidth := colWidths[col]
if len(word) > curWidth {
colWidths[col] = len(word)
}
}
// Append whitespace to each word to make columns mono-width.
finalWords := make([]string, len(words))
for i, word := range words {
col := i % ncols
width := colWidths[col]
diff := width - len(word)
finalWords[i] = word + strings.Repeat(" ", diff)
}
return finalWords
}
func create(ctx *cli.Context) error { func create(ctx *cli.Context) error {
ctxb := context.Background() ctxb := context.Background()
client, cleanUp := getWalletUnlockerClient(ctx) client, cleanUp := getWalletUnlockerClient(ctx)
defer cleanUp() defer cleanUp()
// First, we'll prompt the user for their passphrase twice to ensure
// both attempts match up properly.
fmt.Printf("Input wallet password: ") fmt.Printf("Input wallet password: ")
pw1, err := terminal.ReadPassword(int(syscall.Stdin)) pw1, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil { if err != nil {
@ -800,24 +848,176 @@ func create(ctx *cli.Context) error {
} }
fmt.Println() fmt.Println()
// If the passwords don't match, then we'll return an error.
if !bytes.Equal(pw1, pw2) { if !bytes.Equal(pw1, pw2) {
return fmt.Errorf("passwords don't match") return fmt.Errorf("passwords don't match")
} }
req := &lnrpc.CreateWalletRequest{ // Next, we'll see if the user has 24-word mnemonic they want to use to
Password: pw1, // derive a seed within the wallet.
var (
hasMnemonic bool
)
mnemonicCheck:
for {
fmt.Println()
fmt.Printf("Do you have an existing cipher seed " +
"mnemonic you want to use? (Enter y/n): ")
reader := bufio.NewReader(os.Stdin)
answer, err := reader.ReadString('\n')
if err != nil {
return err
}
fmt.Println()
answer = strings.TrimSpace(answer)
answer = strings.ToLower(answer)
switch answer {
case "y":
hasMnemonic = true
break mnemonicCheck
case "n":
hasMnemonic = false
break mnemonicCheck
}
} }
_, err = client.CreateWallet(ctxb, req)
if err != nil { // If the user *does* have an existing seed they want to use, then
// we'll read that in directly from the terminal.
var (
cipherSeedMnemonic []string
aezeedPass []byte
)
if hasMnemonic {
// We'll now prompt the user to enter in their 24-word
// mnemonic.
fmt.Printf("Input your 24-word mnemonic separated by spaces: ")
reader := bufio.NewReader(os.Stdin)
mnemonic, err := reader.ReadString('\n')
if err != nil {
return err
}
// We'll trim off extra spaces, and ensure the mnemonic is all
// lower case, then populate our request.
mnemonic = strings.TrimSpace(mnemonic)
mnemonic = strings.ToLower(mnemonic)
cipherSeedMnemonic = strings.Split(mnemonic, " ")
fmt.Println()
// Additionally, the user may have a passphrase, that will also
// need to be provided so the daemon can properly decipher the
// cipher seed.
fmt.Printf("Input your cipher seed passphrase (press enter if " +
"your seed doesn't have a passphrase): ")
passphrase, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil {
return err
}
aezeedPass = []byte(passphrase)
fmt.Println()
} else {
// Otherwise, if the user doesn't have a mnemonic that they
// want to use, we'll generate a fresh one with the GenSeed
// command.
fmt.Println("Your cipher seed can optionally be encrypted.")
fmt.Printf("Input your passphrase you wish to encrypt it " +
"(or press enter to proceed without a cipher seed " +
"passphrase): ")
aezeedPass1, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil {
return err
}
fmt.Println()
if len(aezeedPass1) != 0 {
fmt.Printf("Confirm cipher seed passphrase: ")
aezeedPass2, err := terminal.ReadPassword(
int(syscall.Stdin),
)
if err != nil {
return err
}
fmt.Println()
// If the passwords don't match, then we'll return an
// error.
if !bytes.Equal(aezeedPass1, aezeedPass2) {
return fmt.Errorf("cipher seed pass phrases " +
"don't match")
}
}
fmt.Println()
fmt.Println("Generating fresh cipher seed...")
fmt.Println()
genSeedReq := &lnrpc.GenSeedRequest{
AezeedPassphrase: aezeedPass1,
}
seedResp, err := client.GenSeed(ctxb, genSeedReq)
if err != nil {
return fmt.Errorf("unable to generate seed: %v", err)
}
cipherSeedMnemonic = seedResp.CipherSeedMnemonic
aezeedPass = aezeedPass1
}
// Before we initialize the wallet, we'll display the cipher seed to
// the user so they can write it down.
mnemonicWords := cipherSeedMnemonic
fmt.Println("!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " +
"RESTORE THE WALLET!!!\n")
fmt.Println("---------------BEGIN LND CIPHER SEED---------------")
numCols := 4
colWords := monowidthColumns(mnemonicWords, numCols)
for i := 0; i < len(colWords); i += numCols {
fmt.Printf("%2d. %3s %2d. %3s %2d. %3s %2d. %3s\n",
i+1, colWords[i], i+2, colWords[i+1], i+3,
colWords[i+2], i+4, colWords[i+3])
}
fmt.Println("---------------END LND CIPHER SEED-----------------")
fmt.Println("\n!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " +
"RESTORE THE WALLET!!!")
// With either the user's prior cipher seed, or a newly generated one,
// we'll go ahead and initialize the wallet.
req := &lnrpc.InitWalletRequest{
WalletPassword: pw1,
CipherSeedMnemonic: cipherSeedMnemonic,
AezeedPassphrase: aezeedPass,
}
if _, err := client.InitWallet(ctxb, req); err != nil {
return err return err
} }
fmt.Println("\nlnd successfully initialized!")
return nil return nil
} }
var unlockCommand = cli.Command{ var unlockCommand = cli.Command{
Name: "unlock", Name: "unlock",
Usage: "Unlock encrypted wallet at lnd startup", Description: `
The unlock command is used to decrypt lnd's wallet state in order to
start up. This command MUST be run after booting up lnd before it's
able to carry out its duties. An exception is if a user is running with
--noencryptwallet, then a default passphrase will be used.
`,
Action: actionDecorator(unlock), Action: actionDecorator(unlock),
} }
@ -834,13 +1034,15 @@ func unlock(ctx *cli.Context) error {
fmt.Println() fmt.Println()
req := &lnrpc.UnlockWalletRequest{ req := &lnrpc.UnlockWalletRequest{
Password: pw, WalletPassword: pw,
} }
_, err = client.UnlockWallet(ctxb, req) _, err = client.UnlockWallet(ctxb, req)
if err != nil { if err != nil {
return err return err
} }
fmt.Println("\nlnd successfully unlocked!")
return nil return nil
} }