Merge pull request #719 from Roasbeef/rpc-seed

lnrpc+walletunlocker: extend wallet creation to allow user generated entropy + entropy restore (BIP 39)
This commit is contained in:
Olaoluwa Osuntokun 2018-03-05 15:56:41 -05:00 committed by GitHub
commit 61a7556931
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1529 additions and 578 deletions

View File

@ -1,6 +1,7 @@
package main
import (
"bufio"
"bytes"
"encoding/hex"
"encoding/json"
@ -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 {
@ -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),
}
@ -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
}

45
lnd.go
View File

@ -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 (
@ -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")
}

File diff suppressed because it is too large Load Diff

View File

@ -28,15 +28,32 @@ var _ status.Status
var _ = runtime.String
var _ = utilities.NewDoubleArray
func request_WalletUnlocker_CreateWallet_0(ctx context.Context, marshaler runtime.Marshaler, client WalletUnlockerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq CreateWalletRequest
var (
filter_WalletUnlocker_GenSeed_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
func request_WalletUnlocker_GenSeed_0(ctx context.Context, marshaler runtime.Marshaler, client WalletUnlockerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GenSeedRequest
var metadata runtime.ServerMetadata
if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_WalletUnlocker_GenSeed_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.GenSeed(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func request_WalletUnlocker_InitWallet_0(ctx context.Context, marshaler runtime.Marshaler, client WalletUnlockerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq InitWalletRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.CreateWallet(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
msg, err := client.InitWallet(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
@ -564,7 +581,7 @@ func RegisterWalletUnlockerHandlerFromEndpoint(ctx context.Context, mux *runtime
func RegisterWalletUnlockerHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
client := NewWalletUnlockerClient(conn)
mux.Handle("POST", pattern_WalletUnlocker_CreateWallet_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("GET", pattern_WalletUnlocker_GenSeed_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
if cn, ok := w.(http.CloseNotifier); ok {
@ -582,14 +599,43 @@ func RegisterWalletUnlockerHandler(ctx context.Context, mux *runtime.ServeMux, c
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_WalletUnlocker_CreateWallet_0(rctx, inboundMarshaler, client, req, pathParams)
resp, md, err := request_WalletUnlocker_GenSeed_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_WalletUnlocker_CreateWallet_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_WalletUnlocker_GenSeed_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_WalletUnlocker_InitWallet_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
if cn, ok := w.(http.CloseNotifier); ok {
go func(done <-chan struct{}, closed <-chan bool) {
select {
case <-done:
case <-closed:
cancel()
}
}(ctx.Done(), cn.CloseNotify())
}
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_WalletUnlocker_InitWallet_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_WalletUnlocker_InitWallet_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
@ -626,13 +672,17 @@ func RegisterWalletUnlockerHandler(ctx context.Context, mux *runtime.ServeMux, c
}
var (
pattern_WalletUnlocker_CreateWallet_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "createwallet"}, ""))
pattern_WalletUnlocker_GenSeed_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "genseed"}, ""))
pattern_WalletUnlocker_InitWallet_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "initwallet"}, ""))
pattern_WalletUnlocker_UnlockWallet_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "unlockwallet"}, ""))
)
var (
forward_WalletUnlocker_CreateWallet_0 = runtime.ForwardResponseMessage
forward_WalletUnlocker_GenSeed_0 = runtime.ForwardResponseMessage
forward_WalletUnlocker_InitWallet_0 = runtime.ForwardResponseMessage
forward_WalletUnlocker_UnlockWallet_0 = runtime.ForwardResponseMessage
)

View File

@ -28,13 +28,39 @@ package lnrpc;
// The WalletUnlocker service is used to set up a wallet password for
// lnd at first startup, and unlock a previously set up wallet.
service WalletUnlocker {
/** lncli: `create`
CreateWallet is used at lnd startup to set the encryption password for
the wallet database.
/**
GenSeed is the first method that should be used to instantiate a new lnd
instance. This method allows a caller to generate a new aezeed cipher seed
given an optional passphrase. If provided, the passphrase will be necessary
to decrypt the cipherseed to expose the internal wallet seed.
Once the cipherseed is obtained and verified by the user, the InitWallet
method should be used to commit the newly generated seed, and create the
wallet.
*/
rpc CreateWallet(CreateWalletRequest) returns (CreateWalletResponse) {
rpc GenSeed(GenSeedRequest) returns (GenSeedResponse) {
option (google.api.http) = {
post: "/v1/createwallet"
get: "/v1/genseed"
};
}
/** lncli: `init`
InitWallet is used when lnd is starting up for the first time to fully
initialize the daemon and its internal wallet. At the very least a wallet
password must be provided. This will be used to encrypt sensitive material
on disk.
In the case of a recovery scenario, the user can also specify their aezeed
mnemonic and passphrase. If set, then the daemon will use this prior state
to initialize its internal wallet.
Alternatively, this can be used along with the GenSeed RPC to obtain a
seed, then present it to the user. Once it has been verified by the user,
the seed can be fed into this RPC in order to commit the new wallet.
*/
rpc InitWallet(InitWalletRequest) returns (InitWalletResponse) {
option (google.api.http) = {
post: "/v1/initwallet"
body: "*"
};
}
@ -51,20 +77,74 @@ service WalletUnlocker {
}
}
message CreateWalletRequest {
bytes password = 1;
}
message CreateWalletResponse {}
message GenSeedRequest {
/**
aezeed_passphrase is an optional user provided passphrase that will be used
to encrypt the generated aezeed cipher seed.
*/
bytes aezeed_passphrase = 1;
/**
seed_entropy is an optional 16-bytes generated via CSPRNG. If not
specified, then a fresh set of randomness will be used to create the seed.
*/
bytes seed_entropy = 2;
}
message GenSeedResponse {
/**
cipher_seed_mnemonic is a 24-word mnemonic that encodes a prior aezeed
cipher seed obtained by the user. This field is optional, as if not
provided, then the daemon will generate a new cipher seed for the user.
Otherwise, then the daemon will attempt to recover the wallet state linked
to this cipher seed.
*/
repeated string cipher_seed_mnemonic = 1;
/**
enciphered_seed are the raw aezeed cipher seed bytes. This is the raw
cipher text before run through our mnemonic encoding scheme.
*/
bytes enciphered_seed = 2;
}
message InitWalletRequest {
/**
wallet_password is the passphrase that should be used to encrypt the
wallet. This MUST be at least 8 chars in length. After creation, this
password is required to unlock the daemon.
*/
bytes wallet_password = 1;
/**
cipher_seed_mnemonic is a 24-word mnemonic that encodes a prior aezeed
cipher seed obtained by the user. This may have been generated by the
GenSeed method, or be an existing seed.
*/
repeated string cipher_seed_mnemonic = 2;
/**
aezeed_passphrase is an optional user provided passphrase that will be used
to encrypt the generated aezeed cipher seed.
*/
bytes aezeed_passphrase = 3;
}
message InitWalletResponse {
}
message UnlockWalletRequest {
bytes password = 1;
/**
wallet_password should be the current valid passphrase for the daemon. This
will be required to decrypt on-disk material that the daemon requires to
function properly.
*/
bytes wallet_password = 1;
}
message UnlockWalletResponse {}
service Lightning {
/** lncli: `walletbalance`
WalletBalance returns total unspent outputs(confirmed and unconfirmed), all confirmed unspent outputs and all unconfirmed unspent outputs under control
WalletBalance returns total unspent outputs(confirmed and unconfirmed), all
confirmed unspent outputs and all unconfirmed unspent outputs under control
by the wallet. This method can be modified by having the request specify
only witness outputs should be factored into the final output sum.
*/

View File

@ -17,7 +17,7 @@
"paths": {
"/v1/balance/blockchain": {
"get": {
"summary": "* lncli: `walletbalance`\nWalletBalance returns total unspent outputs(confirmed and unconfirmed), all confirmed unspent outputs and all unconfirmed unspent outputs under control\nby the wallet. This method can be modified by having the request specify\nonly witness outputs should be factored into the final output sum.",
"summary": "* lncli: `walletbalance`\nWalletBalance returns total unspent outputs(confirmed and unconfirmed), all\nconfirmed unspent outputs and all unconfirmed unspent outputs under control\nby the wallet. This method can be modified by having the request specify\nonly witness outputs should be factored into the final output sum.",
"operationId": "WalletBalance",
"responses": {
"200": {
@ -204,33 +204,6 @@
]
}
},
"/v1/createwallet": {
"post": {
"summary": "* lncli: `create`\nCreateWallet is used at lnd startup to set the encryption password for\nthe wallet database.",
"operationId": "CreateWallet",
"responses": {
"200": {
"description": "",
"schema": {
"$ref": "#/definitions/lnrpcCreateWalletResponse"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/lnrpcCreateWalletRequest"
}
}
],
"tags": [
"WalletUnlocker"
]
}
},
"/v1/fees": {
"get": {
"summary": "* lncli: `feereport`\nFeeReport allows the caller to obtain a report detailing the current fee\nschedule enforced by the node globally for each channel.",
@ -248,6 +221,42 @@
]
}
},
"/v1/genseed": {
"get": {
"summary": "*\nGenSeed is the first method that should be used to instantiate a new lnd\ninstance. This method allows a caller to generate a new aezeed cipher seed\ngiven an optional passphrase. If provided, the passphrase will be necessary\nto decrypt the cipherseed to expose the internal wallet seed.",
"description": "Once the cipherseed is obtained and verified by the user, the InitWallet\nmethod should be used to commit the newly generated seed, and create the\nwallet.",
"operationId": "GenSeed",
"responses": {
"200": {
"description": "",
"schema": {
"$ref": "#/definitions/lnrpcGenSeedResponse"
}
}
},
"parameters": [
{
"name": "aezeed_passphrase",
"description": "*\naezeed_passphrase is an optional user provided passphrase that will be used\nto encrypt the generated aezeed cipher seed.",
"in": "query",
"required": false,
"type": "string",
"format": "byte"
},
{
"name": "seed_entropy",
"description": "*\nseed_entropy is an optional 16-bytes generated via CSPRNG. If not\nspecified, then a fresh set of randomness will be used to create the seed.",
"in": "query",
"required": false,
"type": "string",
"format": "byte"
}
],
"tags": [
"WalletUnlocker"
]
}
},
"/v1/getinfo": {
"get": {
"summary": "* lncli: `getinfo`\nGetInfo returns general information concerning the lightning node including\nit's identity pubkey, alias, the chains it is connected to, and information\nconcerning the number of open+pending channels.",
@ -390,6 +399,34 @@
]
}
},
"/v1/initwallet": {
"post": {
"summary": "* lncli: `init`\nInitWallet is used when lnd is starting up for the first time to fully\ninitialize the daemon and its internal wallet. At the very least a wallet\npassword must be provided. This will be used to encrypt sensitive material\non disk.",
"description": "In the case of a recovery scenario, the user can also specify their aezeed\nmnemonic and passphrase. If set, then the daemon will use this prior state\nto initialize its internal wallet.\n\nAlternatively, this can be used along with the GenSeed RPC to obtain a\nseed, then present it to the user. Once it has been verified by the user,\nthe seed can be fed into this RPC in order to commit the new wallet.",
"operationId": "InitWallet",
"responses": {
"200": {
"description": "",
"schema": {
"$ref": "#/definitions/lnrpcInitWalletResponse"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/lnrpcInitWalletRequest"
}
}
],
"tags": [
"WalletUnlocker"
]
}
},
"/v1/invoice/{r_hash_str}": {
"get": {
"summary": "* lncli: `lookupinvoice`\nLookupInvoice attempts to look up an invoice according to its payment hash.\nThe passed payment hash *must* be exactly 32 bytes, if not, an error is\nreturned.",
@ -1129,18 +1166,6 @@
"lnrpcConnectPeerResponse": {
"type": "object"
},
"lnrpcCreateWalletRequest": {
"type": "object",
"properties": {
"password": {
"type": "string",
"format": "byte"
}
}
},
"lnrpcCreateWalletResponse": {
"type": "object"
},
"lnrpcDebugLevelResponse": {
"type": "object",
"properties": {
@ -1167,6 +1192,23 @@
}
}
},
"lnrpcGenSeedResponse": {
"type": "object",
"properties": {
"cipher_seed_mnemonic": {
"type": "array",
"items": {
"type": "string"
},
"description": "*\ncipher_seed_mnemonic is a 24-word mnemonic that encodes a prior aezeed\ncipher seed obtained by the user. This field is optional, as if not\nprovided, then the daemon will generate a new cipher seed for the user.\nOtherwise, then the daemon will attempt to recover the wallet state linked\nto this cipher seed."
},
"enciphered_seed": {
"type": "string",
"format": "byte",
"description": "*\nenciphered_seed are the raw aezeed cipher seed bytes. This is the raw\ncipher text before run through our mnemonic encoding scheme."
}
}
},
"lnrpcGetInfoResponse": {
"type": "object",
"properties": {
@ -1303,6 +1345,31 @@
}
}
},
"lnrpcInitWalletRequest": {
"type": "object",
"properties": {
"wallet_password": {
"type": "string",
"format": "byte",
"description": "*\nwallet_password is the passphrase that should be used to encrypt the\nwallet. This MUST be at least 8 chars in length. After creation, this\npassword is required to unlock the daemon."
},
"cipher_seed_mnemonic": {
"type": "array",
"items": {
"type": "string"
},
"description": "*\ncipher_seed_mnemonic is a 24-word mnemonic that encodes a prior aezeed\ncipher seed obtained by the user. This may have been generated by the\nGenSeed method, or be an existing seed."
},
"aezeed_passphrase": {
"type": "string",
"format": "byte",
"description": "*\naezeed_passphrase is an optional user provided passphrase that will be used\nto encrypt the generated aezeed cipher seed."
}
}
},
"lnrpcInitWalletResponse": {
"type": "object"
},
"lnrpcInvoice": {
"type": "object",
"properties": {
@ -2062,9 +2129,10 @@
"lnrpcUnlockWalletRequest": {
"type": "object",
"properties": {
"password": {
"wallet_password": {
"type": "string",
"format": "byte"
"format": "byte",
"description": "*\nwallet_password should be the current valid passphrase for the daemon. This\nwill be required to decrypt on-disk material that the daemon requires to\nfunction properly."
}
}
},

View File

@ -80,21 +80,33 @@ func New(cfg Config) (*BtcWallet, error) {
var wallet *base.Wallet
if !walletExists {
// Wallet has never been created, perform initial set up.
wallet, err = loader.CreateNewWallet(pubPass, cfg.PrivatePass,
cfg.HdSeed)
if err != nil {
wallet, err = loader.CreateNewWallet(
pubPass, cfg.PrivatePass, cfg.HdSeed,
)
switch {
// If the wallet already exists, then we'll ignore this error
// and proceed directly to opening the wallet.
case err == base.ErrExists:
// Otherwise, there's a greater error here, and we'll return
// early.
case err != nil:
return nil, err
}
} else {
// Wallet has been created and been initialized at this point,
// open it along with all the required DB namespaces, and the
// DB itself.
wallet, err = loader.OpenExistingWallet(pubPass, false)
if err != nil {
if err := loader.UnloadWallet(); err != nil {
return nil, err
}
}
// Wallet has been created and been initialized at this point, open it
// along with all the required DB namepsaces, and the DB itself.
wallet, err = loader.OpenExistingWallet(pubPass, false)
if err != nil {
return nil, err
}
// Create a bucket within the wallet's database dedicated to storing
// our LN specific data.
db := wallet.Database()

View File

@ -1,8 +1,11 @@
package walletunlocker
import (
"crypto/rand"
"fmt"
"time"
"github.com/lightningnetwork/lnd/aezeed"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
"github.com/lightningnetwork/lnd/macaroons"
@ -11,13 +14,28 @@ import (
"golang.org/x/net/context"
)
// WalletInitMsg is a message sent to the UnlockerService when a user wishes to
// set up the internal wallet for the first time. The user MUST provide a
// passphrase, but is also able to provide their own source of entropy. If
// provided, then this source of entropy will be used to generate the wallet's
// HD seed. Otherwise, the wallet will generate one itself.
type WalletInitMsg struct {
// Passphrase is the passphrase that will be used to encrypt the wallet
// itself. This MUST be at least 8 characters.
Passphrase []byte
// WalletSeed is the deciphered cipher seed that the wallet should use
// to initialize itself.
WalletSeed *aezeed.CipherSeed
}
// UnlockerService implements the WalletUnlocker service used to provide lnd
// with a password for wallet encryption at startup.
// with a password for wallet encryption at startup. Additionally, during
// initial setup, users can provide their own source of entropy which will be
// used to generate the seed that's ultimately used within the wallet.
type UnlockerService struct {
// CreatePasswords is a channel where passwords provided by the rpc
// client to be used to initially create and encrypt a wallet will be
// sent.
CreatePasswords chan []byte
// InitMsgs is a channel that carries all wallet init messages.
InitMsgs chan *WalletInitMsg
// UnlockPasswords is a channel where passwords provided by the rpc
// client to be used to unlock and decrypt an existing wallet will be
@ -32,42 +50,141 @@ type UnlockerService struct {
// New creates and returns a new UnlockerService.
func New(authSvc *macaroons.Service, chainDir string,
params *chaincfg.Params) *UnlockerService {
return &UnlockerService{
CreatePasswords: make(chan []byte, 1),
InitMsgs: make(chan *WalletInitMsg, 1),
UnlockPasswords: make(chan []byte, 1),
chainDir: chainDir,
netParams: params,
}
}
// CreateWallet will read the password provided in the CreateWalletRequest and
// send it over the CreatePasswords channel in case no wallet already exist in
// the chain's wallet database directory.
func (u *UnlockerService) CreateWallet(ctx context.Context,
in *lnrpc.CreateWalletRequest) (*lnrpc.CreateWalletResponse, error) {
// GenSeed is the first method that should be used to instantiate a new lnd
// instance. This method allows a caller to generate a new aezeed cipher seed
// given an optional passphrase. If provided, the passphrase will be necessary
// to decrypt the cipherseed to expose the internal wallet seed.
//
// Once the cipherseed is obtained and verified by the user, the InitWallet
// method should be used to commit the newly generated seed, and create the
// wallet.
func (u *UnlockerService) GenSeed(ctx context.Context,
in *lnrpc.GenSeedRequest) (*lnrpc.GenSeedResponse, error) {
// Require the provided password to have a length of at
// least 8 characters.
password := in.Password
// Before we start, we'll ensure that the wallet hasn't already created
// so we don't show a *new* seed to the user if one already exists.
netDir := btcwallet.NetworkDir(u.chainDir, u.netParams)
loader := wallet.NewLoader(u.netParams, netDir)
walletExists, err := loader.WalletExists()
if err != nil {
return nil, err
}
if walletExists {
return nil, fmt.Errorf("wallet already exists")
}
var entropy [aezeed.EntropySize]byte
switch {
// If the user provided any entropy, then we'll make sure it's sized
// properly.
case len(in.SeedEntropy) != 0 && len(in.SeedEntropy) != aezeed.EntropySize:
return nil, fmt.Errorf("incorrect entropy length: expected "+
"16 bytes, instead got %v bytes", len(in.SeedEntropy))
// If the user provided the correct number of bytes, then we'll copy it
// over into our buffer for usage.
case len(in.SeedEntropy) == aezeed.EntropySize:
copy(entropy[:], in.SeedEntropy[:])
// Otherwise, we'll generate a fresh new set of bytes to use as entropy
// to generate the seed.
default:
if _, err := rand.Read(entropy[:]); err != nil {
return nil, err
}
}
// Now that we have our set of entropy, we'll create a new cipher seed
// instance.
//
// TODO(roasbeef): should use current keychain version here
cipherSeed, err := aezeed.New(0, &entropy, time.Now())
if err != nil {
return nil, err
}
// With our raw cipher seed obtained, we'll convert it into an encoded
// mnemonic using the user specified pass phrase.
mnemonic, err := cipherSeed.ToMnemonic(in.AezeedPassphrase)
if err != nil {
return nil, err
}
// Additionally, we'll also obtain the raw enciphered cipher seed as
// well to return to the user.
encipheredSeed, err := cipherSeed.Encipher(in.AezeedPassphrase)
if err != nil {
return nil, err
}
return &lnrpc.GenSeedResponse{
CipherSeedMnemonic: []string(mnemonic[:]),
EncipheredSeed: encipheredSeed[:],
}, nil
}
// InitWallet is used when lnd is starting up for the first time to fully
// initialize the daemon and its internal wallet. At the very least a wallet
// password must be provided. This will be used to encrypt sensitive material
// on disk.
//
// In the case of a recovery scenario, the user can also specify their aezeed
// mnemonic and passphrase. If set, then the daemon will use this prior state
// to initialize its internal wallet.
//
// Alternatively, this can be used along with the GenSeed RPC to obtain a
// seed, then present it to the user. Once it has been verified by the user,
// the seed can be fed into this RPC in order to commit the new wallet.
func (u *UnlockerService) InitWallet(ctx context.Context,
in *lnrpc.InitWalletRequest) (*lnrpc.InitWalletResponse, error) {
// Require the provided password to have a length of at least 8
// characters.
password := in.WalletPassword
if len(password) < 8 {
return nil, fmt.Errorf("password must have " +
"at least 8 characters")
}
// We'll then open up the directory that will be used to store the
// wallet's files so we can check if the wallet already exists.
netDir := btcwallet.NetworkDir(u.chainDir, u.netParams)
loader := wallet.NewLoader(u.netParams, netDir)
// Check if wallet already exists.
walletExists, err := loader.WalletExists()
if err != nil {
return nil, err
}
// If the wallet already exists, then we'll exit early as we can't
// create the wallet if it already exists!
if walletExists {
// Cannot create wallet if it already exists!
return nil, fmt.Errorf("wallet already exists")
}
// At this point, we know that the wallet doesn't already exist. So
// we'll map the user provided aezeed and passphrase into a decoded
// cipher seed instance.
var mnemonic aezeed.Mnemonic
copy(mnemonic[:], in.CipherSeedMnemonic[:])
// If we're unable to map it back into the ciphertext, then either the
// mnemonic is wrong, or the passphrase is wrong.
cipherSeed, err := mnemonic.ToCipherSeed(in.AezeedPassphrase)
if err != nil {
return nil, err
}
// Attempt to create a password for the macaroon service.
if u.authSvc != nil {
err = u.authSvc.CreateUnlock(&password)
@ -77,11 +194,17 @@ func (u *UnlockerService) CreateWallet(ctx context.Context,
}
}
// We send the password over the CreatePasswords channel, such that it
// can be used by lnd to open or create the wallet.
u.CreatePasswords <- password
// With the cipher seed deciphered, and the auth service created, we'll
// now send over the wallet password and the seed. This will allow the
// daemon to initialize itself and startup.
initMsg := &WalletInitMsg{
Passphrase: password,
WalletSeed: cipherSeed,
}
return &lnrpc.CreateWalletResponse{}, nil
u.InitMsgs <- initMsg
return &lnrpc.InitWalletResponse{}, nil
}
// UnlockWallet sends the password provided by the incoming UnlockWalletRequest
@ -105,15 +228,15 @@ func (u *UnlockerService) UnlockWallet(ctx context.Context,
}
// Try opening the existing wallet with the provided password.
_, err = loader.OpenExistingWallet(in.Password, false)
_, err = loader.OpenExistingWallet(in.WalletPassword, false)
if err != nil {
// Could not open wallet, most likely this means that
// provided password was incorrect.
// Could not open wallet, most likely this means that provided
// password was incorrect.
return nil, err
}
// We successfully opened the wallet, but we'll need to unload
// it to make sure lnd can open it later.
// We successfully opened the wallet, but we'll need to unload it to
// make sure lnd can open it later.
if err := loader.UnloadWallet(); err != nil {
// TODO: not return error here?
return nil, err
@ -121,7 +244,7 @@ func (u *UnlockerService) UnlockWallet(ctx context.Context,
// Attempt to create a password for the macaroon service.
if u.authSvc != nil {
err = u.authSvc.CreateUnlock(&in.Password)
err = u.authSvc.CreateUnlock(&in.WalletPassword)
if err != nil {
return nil, fmt.Errorf("unable to create/unlock "+
"macaroon store: %v", err)
@ -131,7 +254,7 @@ func (u *UnlockerService) UnlockWallet(ctx context.Context,
// At this point we was able to open the existing wallet with the
// provided password. We send the password over the UnlockPasswords
// channel, such that it can be used by lnd to open the wallet.
u.UnlockPasswords <- in.Password
u.UnlockPasswords <- in.WalletPassword
return &lnrpc.UnlockWalletResponse{}, nil
}

View File

@ -4,9 +4,11 @@ import (
"bytes"
"io/ioutil"
"os"
"strings"
"testing"
"time"
"github.com/lightningnetwork/lnd/aezeed"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
"github.com/lightningnetwork/lnd/walletunlocker"
@ -23,6 +25,13 @@ var (
testPassword = []byte("test-password")
testSeed = []byte("test-seed-123456789")
testEntropy = [aezeed.EntropySize]byte{
0x81, 0xb6, 0x37, 0xd8,
0x63, 0x59, 0xe6, 0x96,
0x0d, 0xe7, 0x95, 0xe4,
0x1e, 0x0b, 0x4c, 0xfd,
}
testNetParams = &chaincfg.MainNetParams
)
@ -39,10 +48,127 @@ func createTestWallet(t *testing.T, dir string, netParams *chaincfg.Params) {
}
}
// TestCreateWallet checks that CreateWallet correctly returns a password that
// can be used for creating a wallet if no wallet exists from before, and
// returns an error when it already exists.
func TestCreateWallet(t *testing.T) {
// TestGenSeedUserEntropy tests that the gen seed method generates a valid
// cipher seed mnemonic phrase and user provided source of entropy.
func TestGenSeed(t *testing.T) {
t.Parallel()
// First, we'll create a new test directory and unlocker service for
// that directory.
testDir, err := ioutil.TempDir("", "testcreate")
if err != nil {
t.Fatalf("unable to create temp directory: %v", err)
}
defer func() {
os.RemoveAll(testDir)
}()
service := walletunlocker.New(nil, testDir, testNetParams)
// Now that the service has been created, we'll ask it to generate a
// new seed for us given a test passphrase.
aezeedPass := []byte("kek")
genSeedReq := &lnrpc.GenSeedRequest{
AezeedPassphrase: aezeedPass,
SeedEntropy: testEntropy[:],
}
ctx := context.Background()
seedResp, err := service.GenSeed(ctx, genSeedReq)
if err != nil {
t.Fatalf("unable to generate seed: %v", err)
}
// We should then be able to take the generated mnemonic, and properly
// decipher both it.
var mnemonic aezeed.Mnemonic
copy(mnemonic[:], seedResp.CipherSeedMnemonic[:])
_, err = mnemonic.ToCipherSeed(aezeedPass)
if err != nil {
t.Fatalf("unable to decipher cipher seed: %v", err)
}
}
// TestGenSeedInvalidEntropy tests that the gen seed method generates a valid
// cipher seed mnemonic pass phrase even when the user doesn't provide its own
// source of entropy.
func TestGenSeedGenerateEntropy(t *testing.T) {
t.Parallel()
// First, we'll create a new test directory and unlocker service for
// that directory.
testDir, err := ioutil.TempDir("", "testcreate")
if err != nil {
t.Fatalf("unable to create temp directory: %v", err)
}
defer func() {
os.RemoveAll(testDir)
}()
service := walletunlocker.New(nil, testDir, testNetParams)
// Now that the service has been created, we'll ask it to generate a
// new seed for us given a test passphrase. Note that we don't actually
aezeedPass := []byte("kek")
genSeedReq := &lnrpc.GenSeedRequest{
AezeedPassphrase: aezeedPass,
}
ctx := context.Background()
seedResp, err := service.GenSeed(ctx, genSeedReq)
if err != nil {
t.Fatalf("unable to generate seed: %v", err)
}
// We should then be able to take the generated mnemonic, and properly
// decipher both it.
var mnemonic aezeed.Mnemonic
copy(mnemonic[:], seedResp.CipherSeedMnemonic[:])
_, err = mnemonic.ToCipherSeed(aezeedPass)
if err != nil {
t.Fatalf("unable to decipher cipher seed: %v", err)
}
}
// TestGenSeedInvalidEntropy tests that if a user attempt to create a seed with
// the wrong number of bytes for the initial entropy, then the proper error is
// returned.
func TestGenSeedInvalidEntropy(t *testing.T) {
t.Parallel()
// First, we'll create a new test directory and unlocker service for
// that directory.
testDir, err := ioutil.TempDir("", "testcreate")
if err != nil {
t.Fatalf("unable to create temp directory: %v", err)
}
defer func() {
os.RemoveAll(testDir)
}()
service := walletunlocker.New(nil, testDir, testNetParams)
// Now that the service has been created, we'll ask it to generate a
// new seed for us given a test passphrase. However, we'll be using an
// invalid set of entropy that's 55 bytes, instead of 15 bytes.
aezeedPass := []byte("kek")
genSeedReq := &lnrpc.GenSeedRequest{
AezeedPassphrase: aezeedPass,
SeedEntropy: bytes.Repeat([]byte("a"), 55),
}
// We should get an error now since the entropy source was invalid.
ctx := context.Background()
_, err = service.GenSeed(ctx, genSeedReq)
if err == nil {
t.Fatalf("seed creation should've failed")
}
if !strings.Contains(err.Error(), "incorrect entropy length") {
t.Fatalf("wrong error, expected incorrect entropy length")
}
}
// TestInitWallet tests that the user is able to properly initialize the wallet
// given an existing cipher seed passphrase.
func TestInitWallet(t *testing.T) {
t.Parallel()
// testDir is empty, meaning wallet was not created from before.
@ -57,22 +183,60 @@ func TestCreateWallet(t *testing.T) {
// Create new UnlockerService.
service := walletunlocker.New(nil, testDir, testNetParams)
ctx := context.Background()
req := &lnrpc.CreateWalletRequest{
Password: testPassword,
}
_, err = service.CreateWallet(ctx, req)
// Once we have the unlocker service created, we'll now instantiate a
// new cipher seed instance.
cipherSeed, err := aezeed.New(0, &testEntropy, time.Now())
if err != nil {
t.Fatalf("CreateWallet call failed: %v", err)
t.Fatalf("unable to create seed: %v", err)
}
// Password should be sent over the channel.
// With the new seed created, we'll convert it into a mnemonic phrase
// that we'll send over to initialize the wallet.
pass := []byte("test")
mnemonic, err := cipherSeed.ToMnemonic(pass)
if err != nil {
t.Fatalf("unable to create mnemonic: %v", err)
}
// Now that we have all the necessary items, we'll now issue the Init
// command to the wallet. This should check the validity of the cipher
// seed, then send over the initialization information over the init
// channel.
ctx := context.Background()
req := &lnrpc.InitWalletRequest{
WalletPassword: testPassword,
CipherSeedMnemonic: []string(mnemonic[:]),
AezeedPassphrase: pass,
}
_, err = service.InitWallet(ctx, req)
if err != nil {
t.Fatalf("InitWallet call failed: %v", err)
}
// The same user passphrase, and also the plaintext cipher seed
// should be sent over and match exactly.
select {
case pw := <-service.CreatePasswords:
if !bytes.Equal(pw, testPassword) {
t.Fatalf("expected to receive password %x, got %x",
testPassword, pw)
case msg := <-service.InitMsgs:
if !bytes.Equal(msg.Passphrase, testPassword) {
t.Fatalf("expected to receive password %x, "+
"got %x", testPassword, msg.Passphrase)
}
if msg.WalletSeed.InternalVersion != cipherSeed.InternalVersion {
t.Fatalf("mismatched versions: expected %v, "+
"got %v", cipherSeed.InternalVersion,
msg.WalletSeed.InternalVersion)
}
if msg.WalletSeed.Birthday != cipherSeed.Birthday {
t.Fatalf("mismatched birthday: expected %v, "+
"got %v", cipherSeed.Birthday,
msg.WalletSeed.Birthday)
}
if msg.WalletSeed.Entropy != cipherSeed.Entropy {
t.Fatalf("mismatched versions: expected %x, "+
"got %x", cipherSeed.Entropy[:],
msg.WalletSeed.Entropy[:])
}
case <-time.After(3 * time.Second):
t.Fatalf("password not received")
}
@ -80,11 +244,50 @@ func TestCreateWallet(t *testing.T) {
// Create a wallet in testDir.
createTestWallet(t, testDir, testNetParams)
// Now calling CreateWallet should fail, since a wallet already exists
// in the directory.
_, err = service.CreateWallet(ctx, req)
// Now calling InitWallet should fail, since a wallet already exists in
// the directory.
_, err = service.InitWallet(ctx, req)
if err == nil {
t.Fatalf("CreateWallet did not fail as expected")
t.Fatalf("InitWallet did not fail as expected")
}
// Similarly, if we try to do GenSeed again, we should get an error as
// the wallet already exists.
_, err = service.GenSeed(ctx, &lnrpc.GenSeedRequest{})
if err == nil {
t.Fatalf("seed generation should have failed")
}
}
// TestInitWalletInvalidCipherSeed tests that if we attempt to create a wallet
// with an invalid cipher seed, then we'll receive an error.
func TestCreateWalletInvalidEntropy(t *testing.T) {
t.Parallel()
// testDir is empty, meaning wallet was not created from before.
testDir, err := ioutil.TempDir("", "testcreate")
if err != nil {
t.Fatalf("unable to create temp directory: %v", err)
}
defer func() {
os.RemoveAll(testDir)
}()
// Create new UnlockerService.
service := walletunlocker.New(nil, testDir, testNetParams)
// We'll attempt to init the wallet with an invalid cipher seed and
// passphrase.
req := &lnrpc.InitWalletRequest{
WalletPassword: testPassword,
CipherSeedMnemonic: []string{"invalid", "seed"},
AezeedPassphrase: []byte("fake pass"),
}
ctx := context.Background()
_, err = service.InitWallet(ctx, req)
if err == nil {
t.Fatalf("wallet creation should have failed")
}
}
@ -108,7 +311,7 @@ func TestUnlockWallet(t *testing.T) {
ctx := context.Background()
req := &lnrpc.UnlockWalletRequest{
Password: testPassword,
WalletPassword: testPassword,
}
// Should fail to unlock non-existing wallet.
@ -122,7 +325,7 @@ func TestUnlockWallet(t *testing.T) {
// Try unlocking this wallet with the wrong passphrase.
wrongReq := &lnrpc.UnlockWalletRequest{
Password: []byte("wrong-ofc"),
WalletPassword: []byte("wrong-ofc"),
}
_, err = service.UnlockWallet(ctx, wrongReq)
if err == nil {
@ -145,5 +348,4 @@ func TestUnlockWallet(t *testing.T) {
case <-time.After(3 * time.Second):
t.Fatalf("password not received")
}
}