Skip to content

Commit

Permalink
Merge pull request #719 from Roasbeef/rpc-seed
Browse files Browse the repository at this point in the history
lnrpc+walletunlocker: extend wallet creation to allow user generated entropy + entropy restore (BIP 39)
  • Loading branch information
Roasbeef committed Mar 5, 2018
2 parents 294447f + 3356a37 commit 61a7556
Show file tree
Hide file tree
Showing 9 changed files with 1,520 additions and 569 deletions.
220 changes: 211 additions & 9 deletions cmd/lncli/commands.go
@@ -1,6 +1,7 @@
package main

import (
"bufio"
"bytes"
"encoding/hex"
"encoding/json"
Expand Down Expand Up @@ -776,16 +777,63 @@ func listPeers(ctx *cli.Context) error {
}

var createCommand = cli.Command{
Name: "create",
Usage: "Used to set the wallet password at lnd startup",
Name: "create",
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),
}

// 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()

// First, we'll prompt the user for their passphrase twice to ensure
// both attempts match up properly.
fmt.Printf("Input wallet password: ")
pw1, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil {
Expand All @@ -800,24 +848,176 @@ func create(ctx *cli.Context) error {
}
fmt.Println()

// If the passwords don't match, then we'll return an error.
if !bytes.Equal(pw1, pw2) {
return fmt.Errorf("passwords don't match")
}

req := &lnrpc.CreateWalletRequest{
Password: pw1,
// 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
}
}
_, 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
}

fmt.Println("\nlnd successfully initialized!")

return nil
}

var unlockCommand = cli.Command{
Name: "unlock",
Usage: "Unlock encrypted wallet at lnd startup",
Name: "unlock",
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),
}

Expand All @@ -834,13 +1034,15 @@ func unlock(ctx *cli.Context) error {
fmt.Println()

req := &lnrpc.UnlockWalletRequest{
Password: pw,
WalletPassword: pw,
}
_, err = client.UnlockWallet(ctxb, req)
if err != nil {
return err
}

fmt.Println("\nlnd successfully unlocked!")

return nil
}

Expand Down
45 changes: 40 additions & 5 deletions lnd.go
Expand Up @@ -39,12 +39,14 @@ import (
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/macaroons"
"github.com/lightningnetwork/lnd/walletunlocker"
"github.com/roasbeef/btcd/btcec"
"github.com/roasbeef/btcd/wire"
"github.com/roasbeef/btcutil"
"github.com/roasbeef/btcwallet/wallet"
)

const (
Expand Down Expand Up @@ -820,14 +822,47 @@ func waitForWalletPassword(grpcEndpoints, restEndpoints []string,
"Use `lncli create` to create wallet, or " +
"`lncli unlock` to unlock already created wallet.")

// We currently don't distinguish between getting a password to
// be used for creation or unlocking, as a new wallet db will be
// created if none exists when creating the chain control.
// We currently don't distinguish between getting a password to be used
// for creation or unlocking, as a new wallet db will be created if
// none exists when creating the chain control.
select {
case walletPw := <-pwService.CreatePasswords:
return walletPw, walletPw, nil

// The wallet is being created for the first time, we'll check to see
// if the user provided any entropy for seed creation. If so, then
// we'll create the wallet early to load the seed.
case initMsg := <-pwService.InitMsgs:
password := initMsg.Passphrase
cipherSeed := initMsg.WalletSeed

netDir := btcwallet.NetworkDir(
chainConfig.ChainDir, activeNetParams.Params,
)
loader := wallet.NewLoader(activeNetParams.Params, netDir)

// With the seed, we can now use the wallet loader to create
// the wallet, then unload it so it can be opened shortly
// after.
//
// TODO(roasbeef): extend loader to also accept birthday
// * also check with keychain version
_, err = loader.CreateNewWallet(
password, password, cipherSeed.Entropy[:],
)
if err != nil {
return nil, nil, err
}

if err := loader.UnloadWallet(); err != nil {
return nil, nil, err
}

return password, password, nil

// The wallet has already been created in the past, and is simply being
// unlocked. So we'll just return these passphrases.
case walletPw := <-pwService.UnlockPasswords:
return walletPw, walletPw, nil

case <-shutdownChannel:
return nil, nil, fmt.Errorf("shutting down")
}
Expand Down

0 comments on commit 61a7556

Please sign in to comment.