walletunlocker: modify service to implement new 2-step wallet creation

In this commit, we extend the UnlockerService to account for the new
changes in the lnrpc definition. Setting up the daemon for the first
time is now two step process: first the user will generate a new seed
via the GenSeed method, then the user will present this new seed (and
optional pass) to the InitWallet method which then will finalize the
wallet creation.

This two step process ensures that we don't commit the wallet changes
in the case that the user doesn't actually "ACK" the new seed.

In the case that the user already has an existing seed, they can
re-enter it and skip straight to the InitWallet step.

We also update the tests to account for the new API changes.
This commit is contained in:
Olaoluwa Osuntokun 2018-02-01 20:44:40 -08:00
parent 7f953bf0b9
commit 428693cb6e
No known key found for this signature in database
GPG Key ID: 964EA263DD637C21
2 changed files with 374 additions and 49 deletions

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