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:
parent
d8ce90306d
commit
3356a370c7
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user