Merge pull request #4650 from guggero/lncli-no-macaroons
lncli: fix macaroon usage when unlocking or using --no-macaroons
This commit is contained in:
commit
fd962d322a
@ -117,7 +117,7 @@ func profileAdd(ctx *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a profile struct from all the global options.
|
// Create a profile struct from all the global options.
|
||||||
profile, err := profileFromContext(ctx, true)
|
profile, err := profileFromContext(ctx, true, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not load global options: %v", err)
|
return fmt.Errorf("could not load global options: %v", err)
|
||||||
}
|
}
|
||||||
|
553
cmd/lncli/cmd_walletunlocker.go
Normal file
553
cmd/lncli/cmd_walletunlocker.go
Normal file
@ -0,0 +1,553 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/walletunlocker"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
var createCommand = cli.Command{
|
||||||
|
Name: "create",
|
||||||
|
Category: "Startup",
|
||||||
|
Usage: "Initialize a wallet when starting lnd for the first time.",
|
||||||
|
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.
|
||||||
|
|
||||||
|
Finally, it's also possible to use this command and a set of static
|
||||||
|
channel backups to trigger a recover attempt for the provided Static
|
||||||
|
Channel Backups. Only one of the three parameters will be accepted. See
|
||||||
|
the restorechanbackup command for further details w.r.t the format
|
||||||
|
accepted.
|
||||||
|
`,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "single_backup",
|
||||||
|
Usage: "a hex encoded single channel backup obtained " +
|
||||||
|
"from exportchanbackup",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "multi_backup",
|
||||||
|
Usage: "a hex encoded multi-channel backup obtained " +
|
||||||
|
"from exportchanbackup",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "multi_file",
|
||||||
|
Usage: "the path to a multi-channel back up file",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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 {
|
||||||
|
ctxb := context.Background()
|
||||||
|
client, cleanUp := getWalletUnlockerClient(ctx)
|
||||||
|
defer cleanUp()
|
||||||
|
|
||||||
|
var (
|
||||||
|
chanBackups *lnrpc.ChanBackupSnapshot
|
||||||
|
|
||||||
|
// We use var restoreSCB to track if we will be including an SCB
|
||||||
|
// recovery in the init wallet request.
|
||||||
|
restoreSCB = false
|
||||||
|
)
|
||||||
|
|
||||||
|
backups, err := parseChanBackups(ctx)
|
||||||
|
|
||||||
|
// We'll check to see if the user provided any static channel backups (SCB),
|
||||||
|
// if so, we will warn the user that SCB recovery closes all open channels
|
||||||
|
// and ask them to confirm their intention.
|
||||||
|
// If the user agrees, we'll add the SCB recovery onto the final init wallet
|
||||||
|
// request.
|
||||||
|
switch {
|
||||||
|
// parseChanBackups returns an errMissingBackup error (which we ignore) if
|
||||||
|
// the user did not request a SCB recovery.
|
||||||
|
case err == errMissingChanBackup:
|
||||||
|
|
||||||
|
// Passed an invalid channel backup file.
|
||||||
|
case err != nil:
|
||||||
|
return fmt.Errorf("unable to parse chan backups: %v", err)
|
||||||
|
|
||||||
|
// We have an SCB recovery option with a valid backup file.
|
||||||
|
default:
|
||||||
|
|
||||||
|
warningLoop:
|
||||||
|
for {
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("WARNING: You are attempting to restore from a " +
|
||||||
|
"static channel backup (SCB) file.\nThis action will CLOSE " +
|
||||||
|
"all currently open channels, and you will pay on-chain fees." +
|
||||||
|
"\n\nAre you sure you want to recover funds from a" +
|
||||||
|
" static channel backup? (Enter y/n): ")
|
||||||
|
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
answer, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer = strings.TrimSpace(answer)
|
||||||
|
answer = strings.ToLower(answer)
|
||||||
|
|
||||||
|
switch answer {
|
||||||
|
case "y":
|
||||||
|
restoreSCB = true
|
||||||
|
break warningLoop
|
||||||
|
case "n":
|
||||||
|
fmt.Println("Aborting SCB recovery")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proceed with SCB recovery.
|
||||||
|
if restoreSCB {
|
||||||
|
fmt.Println("Static Channel Backup (SCB) recovery selected!")
|
||||||
|
if backups != nil {
|
||||||
|
switch {
|
||||||
|
case backups.GetChanBackups() != nil:
|
||||||
|
singleBackup := backups.GetChanBackups()
|
||||||
|
chanBackups = &lnrpc.ChanBackupSnapshot{
|
||||||
|
SingleChanBackups: singleBackup,
|
||||||
|
}
|
||||||
|
|
||||||
|
case backups.GetMultiChanBackup() != nil:
|
||||||
|
multiBackup := backups.GetMultiChanBackup()
|
||||||
|
chanBackups = &lnrpc.ChanBackupSnapshot{
|
||||||
|
MultiChanBackup: &lnrpc.MultiChanBackup{
|
||||||
|
MultiChanBackup: multiBackup,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
walletPassword, err := capturePassword(
|
||||||
|
"Input wallet password: ", false, walletunlocker.ValidatePassword,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, we'll see if the user has 24-word mnemonic they want to use to
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
recoveryWindow int32
|
||||||
|
)
|
||||||
|
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()
|
||||||
|
|
||||||
|
if len(cipherSeedMnemonic) != 24 {
|
||||||
|
return fmt.Errorf("wrong cipher seed mnemonic "+
|
||||||
|
"length: got %v words, expecting %v words",
|
||||||
|
len(cipherSeedMnemonic), 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additionally, the user may have a passphrase, that will also
|
||||||
|
// need to be provided so the daemon can properly decipher the
|
||||||
|
// cipher seed.
|
||||||
|
aezeedPass, err = readPassword("Input your cipher seed " +
|
||||||
|
"passphrase (press enter if your seed doesn't have a " +
|
||||||
|
"passphrase): ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("Input an optional address look-ahead "+
|
||||||
|
"used to scan for used keys (default %d): ",
|
||||||
|
defaultRecoveryWindow)
|
||||||
|
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
answer, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
answer = strings.TrimSpace(answer)
|
||||||
|
|
||||||
|
if len(answer) == 0 {
|
||||||
|
recoveryWindow = defaultRecoveryWindow
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
lookAhead, err := strconv.Atoi(answer)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Unable to parse recovery "+
|
||||||
|
"window: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
recoveryWindow = int32(lookAhead)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} 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.")
|
||||||
|
|
||||||
|
instruction := "Input your passphrase if you wish to encrypt it " +
|
||||||
|
"(or press enter to proceed without a cipher seed " +
|
||||||
|
"passphrase): "
|
||||||
|
aezeedPass, err = capturePassword(
|
||||||
|
instruction, true, func(_ []byte) error { return nil },
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Generating fresh cipher seed...")
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
genSeedReq := &lnrpc.GenSeedRequest{
|
||||||
|
AezeedPassphrase: aezeedPass,
|
||||||
|
}
|
||||||
|
seedResp, err := client.GenSeed(ctxb, genSeedReq)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to generate seed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cipherSeedMnemonic = seedResp.CipherSeedMnemonic
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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!!!")
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
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: walletPassword,
|
||||||
|
CipherSeedMnemonic: cipherSeedMnemonic,
|
||||||
|
AezeedPassphrase: aezeedPass,
|
||||||
|
RecoveryWindow: recoveryWindow,
|
||||||
|
ChannelBackups: chanBackups,
|
||||||
|
}
|
||||||
|
if _, err := client.InitWallet(ctxb, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nlnd successfully initialized!")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// capturePassword returns a password value that has been entered twice by the
|
||||||
|
// user, to ensure that the user knows what password they have entered. The user
|
||||||
|
// will be prompted to retry until the passwords match. If the optional param is
|
||||||
|
// true, the function may return an empty byte array if the user opts against
|
||||||
|
// using a password.
|
||||||
|
func capturePassword(instruction string, optional bool,
|
||||||
|
validate func([]byte) error) ([]byte, error) {
|
||||||
|
|
||||||
|
for {
|
||||||
|
password, err := readPassword(instruction)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not require users to repeat password if
|
||||||
|
// it is optional and they are not using one.
|
||||||
|
if len(password) == 0 && optional {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the password provided is not valid, restart
|
||||||
|
// password capture process from the beginning.
|
||||||
|
if err := validate(password); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
fmt.Println()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordConfirmed, err := readPassword("Confirm password: ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.Equal(password, passwordConfirmed) {
|
||||||
|
return password, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Passwords don't match, please try again")
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var unlockCommand = cli.Command{
|
||||||
|
Name: "unlock",
|
||||||
|
Category: "Startup",
|
||||||
|
Usage: "Unlock an encrypted wallet at 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
|
||||||
|
--noseedbackup, then a default passphrase will be used.
|
||||||
|
`,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.IntFlag{
|
||||||
|
Name: "recovery_window",
|
||||||
|
Usage: "address lookahead to resume recovery rescan, " +
|
||||||
|
"value should be non-zero -- To recover all " +
|
||||||
|
"funds, this should be greater than the " +
|
||||||
|
"maximum number of consecutive, unused " +
|
||||||
|
"addresses ever generated by the wallet.",
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "stdin",
|
||||||
|
Usage: "read password from standard input instead of " +
|
||||||
|
"prompting for it. THIS IS CONSIDERED TO " +
|
||||||
|
"BE DANGEROUS if the password is located in " +
|
||||||
|
"a file that can be read by another user. " +
|
||||||
|
"This flag should only be used in " +
|
||||||
|
"combination with some sort of password " +
|
||||||
|
"manager or secrets vault.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: actionDecorator(unlock),
|
||||||
|
}
|
||||||
|
|
||||||
|
func unlock(ctx *cli.Context) error {
|
||||||
|
ctxb := context.Background()
|
||||||
|
client, cleanUp := getWalletUnlockerClient(ctx)
|
||||||
|
defer cleanUp()
|
||||||
|
|
||||||
|
var (
|
||||||
|
pw []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
switch {
|
||||||
|
// Read the password from standard in as if it were a file. This should
|
||||||
|
// only be used if the password is piped into lncli from some sort of
|
||||||
|
// password manager. If the user types the password instead, it will be
|
||||||
|
// echoed in the console.
|
||||||
|
case ctx.IsSet("stdin"):
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
pw, err = reader.ReadBytes('\n')
|
||||||
|
|
||||||
|
// Remove carriage return and newline characters.
|
||||||
|
pw = bytes.Trim(pw, "\r\n")
|
||||||
|
|
||||||
|
// Read the password from a terminal by default. This requires the
|
||||||
|
// terminal to be a real tty and will fail if a string is piped into
|
||||||
|
// lncli.
|
||||||
|
default:
|
||||||
|
pw, err = readPassword("Input wallet password: ")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
args := ctx.Args()
|
||||||
|
|
||||||
|
// Parse the optional recovery window if it is specified. By default,
|
||||||
|
// the recovery window will be 0, indicating no lookahead should be
|
||||||
|
// used.
|
||||||
|
var recoveryWindow int32
|
||||||
|
switch {
|
||||||
|
case ctx.IsSet("recovery_window"):
|
||||||
|
recoveryWindow = int32(ctx.Int64("recovery_window"))
|
||||||
|
case args.Present():
|
||||||
|
window, err := strconv.ParseInt(args.First(), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
recoveryWindow = int32(window)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &lnrpc.UnlockWalletRequest{
|
||||||
|
WalletPassword: pw,
|
||||||
|
RecoveryWindow: recoveryWindow,
|
||||||
|
}
|
||||||
|
_, err = client.UnlockWallet(ctxb, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nlnd successfully unlocked!")
|
||||||
|
|
||||||
|
// TODO(roasbeef): add ability to accept hex single and multi backups
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var changePasswordCommand = cli.Command{
|
||||||
|
Name: "changepassword",
|
||||||
|
Category: "Startup",
|
||||||
|
Usage: "Change an encrypted wallet's password at startup.",
|
||||||
|
Description: `
|
||||||
|
The changepassword command is used to Change lnd's encrypted wallet's
|
||||||
|
password. It will automatically unlock the daemon if the password change
|
||||||
|
is successful.
|
||||||
|
|
||||||
|
If one did not specify a password for their wallet (running lnd with
|
||||||
|
--noseedbackup), one must restart their daemon without
|
||||||
|
--noseedbackup and use this command. The "current password" field
|
||||||
|
should be left empty.
|
||||||
|
`,
|
||||||
|
Action: actionDecorator(changePassword),
|
||||||
|
}
|
||||||
|
|
||||||
|
func changePassword(ctx *cli.Context) error {
|
||||||
|
ctxb := context.Background()
|
||||||
|
client, cleanUp := getWalletUnlockerClient(ctx)
|
||||||
|
defer cleanUp()
|
||||||
|
|
||||||
|
currentPw, err := readPassword("Input current wallet password: ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newPw, err := readPassword("Input new wallet password: ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmPw, err := readPassword("Confirm new wallet password: ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(newPw, confirmPw) {
|
||||||
|
return fmt.Errorf("passwords don't match")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &lnrpc.ChangePasswordRequest{
|
||||||
|
CurrentPassword: currentPw,
|
||||||
|
NewPassword: newPw,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.ChangePassword(ctxb, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -23,7 +23,6 @@ import (
|
|||||||
"github.com/lightninglabs/protobuf-hex-display/proto"
|
"github.com/lightninglabs/protobuf-hex-display/proto"
|
||||||
"github.com/lightningnetwork/lnd/lnrpc"
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
"github.com/lightningnetwork/lnd/routing/route"
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
"github.com/lightningnetwork/lnd/walletunlocker"
|
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
@ -1119,543 +1118,6 @@ func listPeers(ctx *cli.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var createCommand = cli.Command{
|
|
||||||
Name: "create",
|
|
||||||
Category: "Startup",
|
|
||||||
Usage: "Initialize a wallet when starting lnd for the first time.",
|
|
||||||
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.
|
|
||||||
|
|
||||||
Finally, it's also possible to use this command and a set of static
|
|
||||||
channel backups to trigger a recover attempt for the provided Static
|
|
||||||
Channel Backups. Only one of the three parameters will be accepted. See
|
|
||||||
the restorechanbackup command for further details w.r.t the format
|
|
||||||
accepted.
|
|
||||||
`,
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "single_backup",
|
|
||||||
Usage: "a hex encoded single channel backup obtained " +
|
|
||||||
"from exportchanbackup",
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "multi_backup",
|
|
||||||
Usage: "a hex encoded multi-channel backup obtained " +
|
|
||||||
"from exportchanbackup",
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "multi_file",
|
|
||||||
Usage: "the path to a multi-channel back up file",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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 {
|
|
||||||
ctxb := context.Background()
|
|
||||||
client, cleanUp := getWalletUnlockerClient(ctx)
|
|
||||||
defer cleanUp()
|
|
||||||
|
|
||||||
var (
|
|
||||||
chanBackups *lnrpc.ChanBackupSnapshot
|
|
||||||
|
|
||||||
// We use var restoreSCB to track if we will be including an SCB
|
|
||||||
// recovery in the init wallet request.
|
|
||||||
restoreSCB = false
|
|
||||||
)
|
|
||||||
|
|
||||||
backups, err := parseChanBackups(ctx)
|
|
||||||
|
|
||||||
// We'll check to see if the user provided any static channel backups (SCB),
|
|
||||||
// if so, we will warn the user that SCB recovery closes all open channels
|
|
||||||
// and ask them to confirm their intention.
|
|
||||||
// If the user agrees, we'll add the SCB recovery onto the final init wallet
|
|
||||||
// request.
|
|
||||||
switch {
|
|
||||||
// parseChanBackups returns an errMissingBackup error (which we ignore) if
|
|
||||||
// the user did not request a SCB recovery.
|
|
||||||
case err == errMissingChanBackup:
|
|
||||||
|
|
||||||
// Passed an invalid channel backup file.
|
|
||||||
case err != nil:
|
|
||||||
return fmt.Errorf("unable to parse chan backups: %v", err)
|
|
||||||
|
|
||||||
// We have an SCB recovery option with a valid backup file.
|
|
||||||
default:
|
|
||||||
|
|
||||||
warningLoop:
|
|
||||||
for {
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("WARNING: You are attempting to restore from a " +
|
|
||||||
"static channel backup (SCB) file.\nThis action will CLOSE " +
|
|
||||||
"all currently open channels, and you will pay on-chain fees." +
|
|
||||||
"\n\nAre you sure you want to recover funds from a" +
|
|
||||||
" static channel backup? (Enter y/n): ")
|
|
||||||
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
|
||||||
answer, err := reader.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
answer = strings.TrimSpace(answer)
|
|
||||||
answer = strings.ToLower(answer)
|
|
||||||
|
|
||||||
switch answer {
|
|
||||||
case "y":
|
|
||||||
restoreSCB = true
|
|
||||||
break warningLoop
|
|
||||||
case "n":
|
|
||||||
fmt.Println("Aborting SCB recovery")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Proceed with SCB recovery.
|
|
||||||
if restoreSCB {
|
|
||||||
fmt.Println("Static Channel Backup (SCB) recovery selected!")
|
|
||||||
if backups != nil {
|
|
||||||
switch {
|
|
||||||
case backups.GetChanBackups() != nil:
|
|
||||||
singleBackup := backups.GetChanBackups()
|
|
||||||
chanBackups = &lnrpc.ChanBackupSnapshot{
|
|
||||||
SingleChanBackups: singleBackup,
|
|
||||||
}
|
|
||||||
|
|
||||||
case backups.GetMultiChanBackup() != nil:
|
|
||||||
multiBackup := backups.GetMultiChanBackup()
|
|
||||||
chanBackups = &lnrpc.ChanBackupSnapshot{
|
|
||||||
MultiChanBackup: &lnrpc.MultiChanBackup{
|
|
||||||
MultiChanBackup: multiBackup,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
walletPassword, err := capturePassword(
|
|
||||||
"Input wallet password: ", false, walletunlocker.ValidatePassword,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next, we'll see if the user has 24-word mnemonic they want to use to
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
recoveryWindow int32
|
|
||||||
)
|
|
||||||
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()
|
|
||||||
|
|
||||||
if len(cipherSeedMnemonic) != 24 {
|
|
||||||
return fmt.Errorf("wrong cipher seed mnemonic "+
|
|
||||||
"length: got %v words, expecting %v words",
|
|
||||||
len(cipherSeedMnemonic), 24)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additionally, the user may have a passphrase, that will also
|
|
||||||
// need to be provided so the daemon can properly decipher the
|
|
||||||
// cipher seed.
|
|
||||||
aezeedPass, err = readPassword("Input your cipher seed " +
|
|
||||||
"passphrase (press enter if your seed doesn't have a " +
|
|
||||||
"passphrase): ")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Input an optional address look-ahead "+
|
|
||||||
"used to scan for used keys (default %d): ",
|
|
||||||
defaultRecoveryWindow)
|
|
||||||
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
|
||||||
answer, err := reader.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
answer = strings.TrimSpace(answer)
|
|
||||||
|
|
||||||
if len(answer) == 0 {
|
|
||||||
recoveryWindow = defaultRecoveryWindow
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
lookAhead, err := strconv.Atoi(answer)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Unable to parse recovery "+
|
|
||||||
"window: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
recoveryWindow = int32(lookAhead)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} 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.")
|
|
||||||
|
|
||||||
instruction := "Input your passphrase if you wish to encrypt it " +
|
|
||||||
"(or press enter to proceed without a cipher seed " +
|
|
||||||
"passphrase): "
|
|
||||||
aezeedPass, err = capturePassword(
|
|
||||||
instruction, true, func(_ []byte) error { return nil },
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("Generating fresh cipher seed...")
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
genSeedReq := &lnrpc.GenSeedRequest{
|
|
||||||
AezeedPassphrase: aezeedPass,
|
|
||||||
}
|
|
||||||
seedResp, err := client.GenSeed(ctxb, genSeedReq)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to generate seed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cipherSeedMnemonic = seedResp.CipherSeedMnemonic
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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: walletPassword,
|
|
||||||
CipherSeedMnemonic: cipherSeedMnemonic,
|
|
||||||
AezeedPassphrase: aezeedPass,
|
|
||||||
RecoveryWindow: recoveryWindow,
|
|
||||||
ChannelBackups: chanBackups,
|
|
||||||
}
|
|
||||||
if _, err := client.InitWallet(ctxb, req); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("\nlnd successfully initialized!")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// capturePassword returns a password value that has been entered twice by the
|
|
||||||
// user, to ensure that the user knows what password they have entered. The user
|
|
||||||
// will be prompted to retry until the passwords match. If the optional param is
|
|
||||||
// true, the function may return an empty byte array if the user opts against
|
|
||||||
// using a password.
|
|
||||||
func capturePassword(instruction string, optional bool,
|
|
||||||
validate func([]byte) error) ([]byte, error) {
|
|
||||||
|
|
||||||
for {
|
|
||||||
password, err := readPassword(instruction)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not require users to repeat password if
|
|
||||||
// it is optional and they are not using one.
|
|
||||||
if len(password) == 0 && optional {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the password provided is not valid, restart
|
|
||||||
// password capture process from the beginning.
|
|
||||||
if err := validate(password); err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
fmt.Println()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
passwordConfirmed, err := readPassword("Confirm password: ")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if bytes.Equal(password, passwordConfirmed) {
|
|
||||||
return password, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Passwords don't match, please try again")
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var unlockCommand = cli.Command{
|
|
||||||
Name: "unlock",
|
|
||||||
Category: "Startup",
|
|
||||||
Usage: "Unlock an encrypted wallet at 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
|
|
||||||
--noseedbackup, then a default passphrase will be used.
|
|
||||||
`,
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
cli.IntFlag{
|
|
||||||
Name: "recovery_window",
|
|
||||||
Usage: "address lookahead to resume recovery rescan, " +
|
|
||||||
"value should be non-zero -- To recover all " +
|
|
||||||
"funds, this should be greater than the " +
|
|
||||||
"maximum number of consecutive, unused " +
|
|
||||||
"addresses ever generated by the wallet.",
|
|
||||||
},
|
|
||||||
cli.BoolFlag{
|
|
||||||
Name: "stdin",
|
|
||||||
Usage: "read password from standard input instead of " +
|
|
||||||
"prompting for it. THIS IS CONSIDERED TO " +
|
|
||||||
"BE DANGEROUS if the password is located in " +
|
|
||||||
"a file that can be read by another user. " +
|
|
||||||
"This flag should only be used in " +
|
|
||||||
"combination with some sort of password " +
|
|
||||||
"manager or secrets vault.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Action: actionDecorator(unlock),
|
|
||||||
}
|
|
||||||
|
|
||||||
func unlock(ctx *cli.Context) error {
|
|
||||||
ctxb := context.Background()
|
|
||||||
client, cleanUp := getWalletUnlockerClient(ctx)
|
|
||||||
defer cleanUp()
|
|
||||||
|
|
||||||
var (
|
|
||||||
pw []byte
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
switch {
|
|
||||||
// Read the password from standard in as if it were a file. This should
|
|
||||||
// only be used if the password is piped into lncli from some sort of
|
|
||||||
// password manager. If the user types the password instead, it will be
|
|
||||||
// echoed in the console.
|
|
||||||
case ctx.IsSet("stdin"):
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
|
||||||
pw, err = reader.ReadBytes('\n')
|
|
||||||
|
|
||||||
// Remove carriage return and newline characters.
|
|
||||||
pw = bytes.Trim(pw, "\r\n")
|
|
||||||
|
|
||||||
// Read the password from a terminal by default. This requires the
|
|
||||||
// terminal to be a real tty and will fail if a string is piped into
|
|
||||||
// lncli.
|
|
||||||
default:
|
|
||||||
pw, err = readPassword("Input wallet password: ")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
args := ctx.Args()
|
|
||||||
|
|
||||||
// Parse the optional recovery window if it is specified. By default,
|
|
||||||
// the recovery window will be 0, indicating no lookahead should be
|
|
||||||
// used.
|
|
||||||
var recoveryWindow int32
|
|
||||||
switch {
|
|
||||||
case ctx.IsSet("recovery_window"):
|
|
||||||
recoveryWindow = int32(ctx.Int64("recovery_window"))
|
|
||||||
case args.Present():
|
|
||||||
window, err := strconv.ParseInt(args.First(), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
recoveryWindow = int32(window)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := &lnrpc.UnlockWalletRequest{
|
|
||||||
WalletPassword: pw,
|
|
||||||
RecoveryWindow: recoveryWindow,
|
|
||||||
}
|
|
||||||
_, err = client.UnlockWallet(ctxb, req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("\nlnd successfully unlocked!")
|
|
||||||
|
|
||||||
// TODO(roasbeef): add ability to accept hex single and multi backups
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var changePasswordCommand = cli.Command{
|
|
||||||
Name: "changepassword",
|
|
||||||
Category: "Startup",
|
|
||||||
Usage: "Change an encrypted wallet's password at startup.",
|
|
||||||
Description: `
|
|
||||||
The changepassword command is used to Change lnd's encrypted wallet's
|
|
||||||
password. It will automatically unlock the daemon if the password change
|
|
||||||
is successful.
|
|
||||||
|
|
||||||
If one did not specify a password for their wallet (running lnd with
|
|
||||||
--noseedbackup), one must restart their daemon without
|
|
||||||
--noseedbackup and use this command. The "current password" field
|
|
||||||
should be left empty.
|
|
||||||
`,
|
|
||||||
Action: actionDecorator(changePassword),
|
|
||||||
}
|
|
||||||
|
|
||||||
func changePassword(ctx *cli.Context) error {
|
|
||||||
ctxb := context.Background()
|
|
||||||
client, cleanUp := getWalletUnlockerClient(ctx)
|
|
||||||
defer cleanUp()
|
|
||||||
|
|
||||||
currentPw, err := readPassword("Input current wallet password: ")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
newPw, err := readPassword("Input new wallet password: ")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
confirmPw, err := readPassword("Confirm new wallet password: ")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(newPw, confirmPw) {
|
|
||||||
return fmt.Errorf("passwords don't match")
|
|
||||||
}
|
|
||||||
|
|
||||||
req := &lnrpc.ChangePasswordRequest{
|
|
||||||
CurrentPassword: currentPw,
|
|
||||||
NewPassword: newPw,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = client.ChangePassword(ctxb, req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var walletBalanceCommand = cli.Command{
|
var walletBalanceCommand = cli.Command{
|
||||||
Name: "walletbalance",
|
Name: "walletbalance",
|
||||||
Category: "Wallet",
|
Category: "Wallet",
|
||||||
|
@ -70,7 +70,7 @@ func getClient(ctx *cli.Context) (lnrpc.LightningClient, func()) {
|
|||||||
func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn {
|
func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn {
|
||||||
// First, we'll get the selected stored profile or an ephemeral one
|
// First, we'll get the selected stored profile or an ephemeral one
|
||||||
// created from the global options in the CLI context.
|
// created from the global options in the CLI context.
|
||||||
profile, err := getGlobalOptions(ctx)
|
profile, err := getGlobalOptions(ctx, skipMacaroons)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatal(fmt.Errorf("could not load global options: %v", err))
|
fatal(fmt.Errorf("could not load global options: %v", err))
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,9 @@ func (e *profileEntry) cert() (*x509.CertPool, error) {
|
|||||||
// exists, these global options might be read from a predefined profile. If no
|
// exists, these global options might be read from a predefined profile. If no
|
||||||
// profile exists, the global options from the command line are returned as an
|
// profile exists, the global options from the command line are returned as an
|
||||||
// ephemeral profile entry.
|
// ephemeral profile entry.
|
||||||
func getGlobalOptions(ctx *cli.Context) (*profileEntry, error) {
|
func getGlobalOptions(ctx *cli.Context, skipMacaroons bool) (*profileEntry,
|
||||||
|
error) {
|
||||||
|
|
||||||
var profileName string
|
var profileName string
|
||||||
|
|
||||||
// Try to load the default profile file and depending on its existence
|
// Try to load the default profile file and depending on its existence
|
||||||
@ -62,7 +64,7 @@ func getGlobalOptions(ctx *cli.Context) (*profileEntry, error) {
|
|||||||
// The legacy case where no profile file exists and the user also didn't
|
// The legacy case where no profile file exists and the user also didn't
|
||||||
// request to use one. We only consider the global options here.
|
// request to use one. We only consider the global options here.
|
||||||
case err == errNoProfileFile && !ctx.GlobalIsSet("profile"):
|
case err == errNoProfileFile && !ctx.GlobalIsSet("profile"):
|
||||||
return profileFromContext(ctx, false)
|
return profileFromContext(ctx, false, skipMacaroons)
|
||||||
|
|
||||||
// The file doesn't exist but the user specified an explicit profile.
|
// The file doesn't exist but the user specified an explicit profile.
|
||||||
case err == errNoProfileFile && ctx.GlobalIsSet("profile"):
|
case err == errNoProfileFile && ctx.GlobalIsSet("profile"):
|
||||||
@ -78,13 +80,13 @@ func getGlobalOptions(ctx *cli.Context) (*profileEntry, error) {
|
|||||||
// setting the flag to an empty string. We fall back to the default/old
|
// setting the flag to an empty string. We fall back to the default/old
|
||||||
// behavior.
|
// behavior.
|
||||||
case ctx.GlobalIsSet("profile") && ctx.GlobalString("profile") == "":
|
case ctx.GlobalIsSet("profile") && ctx.GlobalString("profile") == "":
|
||||||
return profileFromContext(ctx, false)
|
return profileFromContext(ctx, false, skipMacaroons)
|
||||||
|
|
||||||
// There is a file, but no default profile is specified. The user also
|
// There is a file, but no default profile is specified. The user also
|
||||||
// didn't specify a profile to use so we fall back to the default/old
|
// didn't specify a profile to use so we fall back to the default/old
|
||||||
// behavior.
|
// behavior.
|
||||||
case !ctx.GlobalIsSet("profile") && len(f.Default) == 0:
|
case !ctx.GlobalIsSet("profile") && len(f.Default) == 0:
|
||||||
return profileFromContext(ctx, false)
|
return profileFromContext(ctx, false, skipMacaroons)
|
||||||
|
|
||||||
// The user didn't specify a profile but there is a default one defined.
|
// The user didn't specify a profile but there is a default one defined.
|
||||||
case !ctx.GlobalIsSet("profile") && len(f.Default) > 0:
|
case !ctx.GlobalIsSet("profile") && len(f.Default) > 0:
|
||||||
@ -109,7 +111,9 @@ func getGlobalOptions(ctx *cli.Context) (*profileEntry, error) {
|
|||||||
|
|
||||||
// profileFromContext creates an ephemeral profile entry from the global options
|
// profileFromContext creates an ephemeral profile entry from the global options
|
||||||
// set in the CLI context.
|
// set in the CLI context.
|
||||||
func profileFromContext(ctx *cli.Context, store bool) (*profileEntry, error) {
|
func profileFromContext(ctx *cli.Context, store, skipMacaroons bool) (
|
||||||
|
*profileEntry, error) {
|
||||||
|
|
||||||
// Parse the paths of the cert and macaroon. This will validate the
|
// Parse the paths of the cert and macaroon. This will validate the
|
||||||
// chain and network value as well.
|
// chain and network value as well.
|
||||||
tlsCertPath, macPath, err := extractPathArgs(ctx)
|
tlsCertPath, macPath, err := extractPathArgs(ctx)
|
||||||
@ -129,6 +133,22 @@ func profileFromContext(ctx *cli.Context, store bool) (*profileEntry, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry := &profileEntry{
|
||||||
|
RPCServer: ctx.GlobalString("rpcserver"),
|
||||||
|
LndDir: lncfg.CleanAndExpandPath(ctx.GlobalString("lnddir")),
|
||||||
|
Chain: ctx.GlobalString("chain"),
|
||||||
|
Network: ctx.GlobalString("network"),
|
||||||
|
NoMacaroons: ctx.GlobalBool("no-macaroons"),
|
||||||
|
TLSCert: string(tlsCert),
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we aren't using macaroons in general (flag --no-macaroons) or
|
||||||
|
// don't need macaroons for this command (wallet unlocker), we can now
|
||||||
|
// return already.
|
||||||
|
if skipMacaroons || ctx.GlobalBool("no-macaroons") {
|
||||||
|
return entry, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Now load and possibly encrypt the macaroon file.
|
// Now load and possibly encrypt the macaroon file.
|
||||||
macBytes, err := ioutil.ReadFile(macPath)
|
macBytes, err := ioutil.ReadFile(macPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -166,22 +186,16 @@ func profileFromContext(ctx *cli.Context, store bool) (*profileEntry, error) {
|
|||||||
macEntry.Name = strings.TrimSuffix(macEntry.Name, ".macaroon")
|
macEntry.Name = strings.TrimSuffix(macEntry.Name, ".macaroon")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now that we have the complicated arguments behind us, let's return
|
// Now that we have the macaroon jar as well, let's return the entry
|
||||||
// the new entry with all the values populated.
|
// with all the values populated.
|
||||||
return &profileEntry{
|
entry.Macaroons = &macaroonJar{
|
||||||
RPCServer: ctx.GlobalString("rpcserver"),
|
Default: macEntry.Name,
|
||||||
LndDir: lncfg.CleanAndExpandPath(ctx.GlobalString("lnddir")),
|
Timeout: ctx.GlobalInt64("macaroontimeout"),
|
||||||
Chain: ctx.GlobalString("chain"),
|
IP: ctx.GlobalString("macaroonip"),
|
||||||
Network: ctx.GlobalString("network"),
|
Jar: []*macaroonEntry{macEntry},
|
||||||
NoMacaroons: ctx.GlobalBool("no-macaroons"),
|
}
|
||||||
TLSCert: string(tlsCert),
|
|
||||||
Macaroons: &macaroonJar{
|
return entry, nil
|
||||||
Default: macEntry.Name,
|
|
||||||
Timeout: ctx.GlobalInt64("macaroontimeout"),
|
|
||||||
IP: ctx.GlobalString("macaroonip"),
|
|
||||||
Jar: []*macaroonEntry{macEntry},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadProfileFile tries to load the file specified and JSON deserialize it into
|
// loadProfileFile tries to load the file specified and JSON deserialize it into
|
||||||
|
Loading…
Reference in New Issue
Block a user