walletunlocker+lnd: implement stateless wallet initialization
This commit is contained in:
parent
4b0b20c98b
commit
e7aa9256ab
157
lnd.go
157
lnd.go
@ -410,7 +410,8 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error {
|
|||||||
if !cfg.NoMacaroons {
|
if !cfg.NoMacaroons {
|
||||||
// Create the macaroon authentication/authorization service.
|
// Create the macaroon authentication/authorization service.
|
||||||
macaroonService, err = macaroons.NewService(
|
macaroonService, err = macaroons.NewService(
|
||||||
cfg.networkDir, "lnd", false, macaroons.IPLockChecker,
|
cfg.networkDir, "lnd", walletInitParams.StatelessInit,
|
||||||
|
macaroons.IPLockChecker,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := fmt.Errorf("unable to set up macaroon "+
|
err := fmt.Errorf("unable to set up macaroon "+
|
||||||
@ -421,17 +422,42 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error {
|
|||||||
defer macaroonService.Close()
|
defer macaroonService.Close()
|
||||||
|
|
||||||
// Try to unlock the macaroon store with the private password.
|
// Try to unlock the macaroon store with the private password.
|
||||||
|
// Ignore ErrAlreadyUnlocked since it could be unlocked by the
|
||||||
|
// wallet unlocker.
|
||||||
err = macaroonService.CreateUnlock(&privateWalletPw)
|
err = macaroonService.CreateUnlock(&privateWalletPw)
|
||||||
if err != nil {
|
if err != nil && err != macaroons.ErrAlreadyUnlocked {
|
||||||
err := fmt.Errorf("unable to unlock macaroons: %v", err)
|
err := fmt.Errorf("unable to unlock macaroons: %v", err)
|
||||||
ltndLog.Error(err)
|
ltndLog.Error(err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create macaroon files for lncli to use if they don't exist.
|
// In case we actually needed to unlock the wallet, we now need
|
||||||
if !fileExists(cfg.AdminMacPath) && !fileExists(cfg.ReadMacPath) &&
|
// to create an instance of the admin macaroon and send it to
|
||||||
|
// the unlocker so it can forward it to the user. In no seed
|
||||||
|
// backup mode, there's nobody listening on the channel and we'd
|
||||||
|
// block here forever.
|
||||||
|
if !cfg.NoSeedBackup {
|
||||||
|
adminMacBytes, err := bakeMacaroon(
|
||||||
|
ctx, macaroonService, adminPermissions(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// The channel is buffered by one element so writing
|
||||||
|
// should not block here.
|
||||||
|
walletInitParams.MacResponseChan <- adminMacBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user requested a stateless initialization, no macaroon
|
||||||
|
// files should be created.
|
||||||
|
if !walletInitParams.StatelessInit &&
|
||||||
|
!fileExists(cfg.AdminMacPath) &&
|
||||||
|
!fileExists(cfg.ReadMacPath) &&
|
||||||
!fileExists(cfg.InvoiceMacPath) {
|
!fileExists(cfg.InvoiceMacPath) {
|
||||||
|
|
||||||
|
// Create macaroon files for lncli to use if they don't
|
||||||
|
// exist.
|
||||||
err = genMacaroons(
|
err = genMacaroons(
|
||||||
ctx, macaroonService, cfg.AdminMacPath,
|
ctx, macaroonService, cfg.AdminMacPath,
|
||||||
cfg.ReadMacPath, cfg.InvoiceMacPath,
|
cfg.ReadMacPath, cfg.InvoiceMacPath,
|
||||||
@ -443,6 +469,28 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// As a security service to the user, if they requested
|
||||||
|
// stateless initialization and there are macaroon files on disk
|
||||||
|
// we log a warning.
|
||||||
|
if walletInitParams.StatelessInit {
|
||||||
|
msg := "Found %s macaroon on disk (%s) even though " +
|
||||||
|
"--stateless_init was requested. Unencrypted " +
|
||||||
|
"state is accessible by the host system. You " +
|
||||||
|
"should change the password and use " +
|
||||||
|
"--new_mac_root_key with --stateless_init to " +
|
||||||
|
"clean up and invalidate old macaroons."
|
||||||
|
|
||||||
|
if fileExists(cfg.AdminMacPath) {
|
||||||
|
ltndLog.Warnf(msg, "admin", cfg.AdminMacPath)
|
||||||
|
}
|
||||||
|
if fileExists(cfg.ReadMacPath) {
|
||||||
|
ltndLog.Warnf(msg, "readonly", cfg.ReadMacPath)
|
||||||
|
}
|
||||||
|
if fileExists(cfg.InvoiceMacPath) {
|
||||||
|
ltndLog.Warnf(msg, "invoice", cfg.InvoiceMacPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now we're definitely done with the unlocker, shut it down so we can
|
// Now we're definitely done with the unlocker, shut it down so we can
|
||||||
@ -950,6 +998,21 @@ func fileExists(name string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bakeMacaroon creates a new macaroon with newest version and the given
|
||||||
|
// permissions then returns it binary serialized.
|
||||||
|
func bakeMacaroon(ctx context.Context, svc *macaroons.Service,
|
||||||
|
permissions []bakery.Op) ([]byte, error) {
|
||||||
|
|
||||||
|
mac, err := svc.NewMacaroon(
|
||||||
|
ctx, macaroons.DefaultRootKeyID, permissions...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mac.M().MarshalBinary()
|
||||||
|
}
|
||||||
|
|
||||||
// genMacaroons generates three macaroon files; one admin-level, one for
|
// genMacaroons generates three macaroon files; one admin-level, one for
|
||||||
// invoice access and one read-only. These can also be used to generate more
|
// invoice access and one read-only. These can also be used to generate more
|
||||||
// granular macaroons.
|
// granular macaroons.
|
||||||
@ -960,57 +1023,48 @@ func genMacaroons(ctx context.Context, svc *macaroons.Service,
|
|||||||
// access invoice related calls. This is useful for merchants and other
|
// access invoice related calls. This is useful for merchants and other
|
||||||
// services to allow an isolated instance that can only query and
|
// services to allow an isolated instance that can only query and
|
||||||
// modify invoices.
|
// modify invoices.
|
||||||
invoiceMac, err := svc.NewMacaroon(
|
invoiceMacBytes, err := bakeMacaroon(ctx, svc, invoicePermissions)
|
||||||
ctx, macaroons.DefaultRootKeyID, invoicePermissions...,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
invoiceMacBytes, err := invoiceMac.M().MarshalBinary()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = ioutil.WriteFile(invoiceFile, invoiceMacBytes, 0644)
|
err = ioutil.WriteFile(invoiceFile, invoiceMacBytes, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
os.Remove(invoiceFile)
|
_ = os.Remove(invoiceFile)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate the read-only macaroon and write it to a file.
|
// Generate the read-only macaroon and write it to a file.
|
||||||
roMacaroon, err := svc.NewMacaroon(
|
roBytes, err := bakeMacaroon(ctx, svc, readPermissions)
|
||||||
ctx, macaroons.DefaultRootKeyID, readPermissions...,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
roBytes, err := roMacaroon.M().MarshalBinary()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = ioutil.WriteFile(roFile, roBytes, 0644); err != nil {
|
if err = ioutil.WriteFile(roFile, roBytes, 0644); err != nil {
|
||||||
os.Remove(admFile)
|
_ = os.Remove(roFile)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate the admin macaroon and write it to a file.
|
// Generate the admin macaroon and write it to a file.
|
||||||
adminPermissions := append(readPermissions, writePermissions...)
|
admBytes, err := bakeMacaroon(ctx, svc, adminPermissions())
|
||||||
admMacaroon, err := svc.NewMacaroon(
|
|
||||||
ctx, macaroons.DefaultRootKeyID, adminPermissions...,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
admBytes, err := admMacaroon.M().MarshalBinary()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = ioutil.WriteFile(admFile, admBytes, 0600); err != nil {
|
if err = ioutil.WriteFile(admFile, admBytes, 0600); err != nil {
|
||||||
|
_ = os.Remove(admFile)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// adminPermissions returns a list of all permissions in a safe way that doesn't
|
||||||
|
// modify any of the source lists.
|
||||||
|
func adminPermissions() []bakery.Op {
|
||||||
|
admin := make([]bakery.Op, len(readPermissions)+len(writePermissions))
|
||||||
|
copy(admin[:len(readPermissions)], readPermissions)
|
||||||
|
copy(admin[len(readPermissions):], writePermissions)
|
||||||
|
return admin
|
||||||
|
}
|
||||||
|
|
||||||
// WalletUnlockParams holds the variables used to parameterize the unlocking of
|
// WalletUnlockParams holds the variables used to parameterize the unlocking of
|
||||||
// lnd's wallet after it has already been created.
|
// lnd's wallet after it has already been created.
|
||||||
type WalletUnlockParams struct {
|
type WalletUnlockParams struct {
|
||||||
@ -1039,6 +1093,15 @@ type WalletUnlockParams struct {
|
|||||||
// UnloadWallet is a function for unloading the wallet, which should
|
// UnloadWallet is a function for unloading the wallet, which should
|
||||||
// be called on shutdown.
|
// be called on shutdown.
|
||||||
UnloadWallet func() error
|
UnloadWallet func() error
|
||||||
|
|
||||||
|
// StatelessInit signals that the user requested the daemon to be
|
||||||
|
// initialized stateless, which means no unencrypted macaroons should be
|
||||||
|
// written to disk.
|
||||||
|
StatelessInit bool
|
||||||
|
|
||||||
|
// MacResponseChan is the channel for sending back the admin macaroon to
|
||||||
|
// the WalletUnlocker service.
|
||||||
|
MacResponseChan chan []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// waitForWalletPassword will spin up gRPC and REST endpoints for the
|
// waitForWalletPassword will spin up gRPC and REST endpoints for the
|
||||||
@ -1054,12 +1117,10 @@ func waitForWalletPassword(cfg *Config, restEndpoints []net.Addr,
|
|||||||
chainConfig = cfg.Litecoin
|
chainConfig = cfg.Litecoin
|
||||||
}
|
}
|
||||||
|
|
||||||
// The macaroon files are passed to the wallet unlocker since they are
|
// The macaroonFiles are passed to the wallet unlocker so they can be
|
||||||
// also encrypted with the wallet's password. These files will be
|
// deleted and recreated in case the root macaroon key is also changed
|
||||||
// deleted within it and recreated when successfully changing the
|
// during the change password operation.
|
||||||
// wallet's password.
|
|
||||||
macaroonFiles := []string{
|
macaroonFiles := []string{
|
||||||
filepath.Join(cfg.networkDir, macaroons.DBFilename),
|
|
||||||
cfg.AdminMacPath, cfg.ReadMacPath, cfg.InvoiceMacPath,
|
cfg.AdminMacPath, cfg.ReadMacPath, cfg.InvoiceMacPath,
|
||||||
}
|
}
|
||||||
pwService := walletunlocker.New(
|
pwService := walletunlocker.New(
|
||||||
@ -1074,6 +1135,10 @@ func waitForWalletPassword(cfg *Config, restEndpoints []net.Addr,
|
|||||||
|
|
||||||
var shutdownFuncs []func()
|
var shutdownFuncs []func()
|
||||||
shutdown := func() {
|
shutdown := func() {
|
||||||
|
// Make sure nothing blocks on reading on the macaroon channel,
|
||||||
|
// otherwise the GracefulStop below will never return.
|
||||||
|
close(pwService.MacResponseChan)
|
||||||
|
|
||||||
for _, shutdownFn := range shutdownFuncs {
|
for _, shutdownFn := range shutdownFuncs {
|
||||||
shutdownFn()
|
shutdownFn()
|
||||||
}
|
}
|
||||||
@ -1204,23 +1269,27 @@ func waitForWalletPassword(cfg *Config, restEndpoints []net.Addr,
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &WalletUnlockParams{
|
return &WalletUnlockParams{
|
||||||
Password: password,
|
Password: password,
|
||||||
Birthday: birthday,
|
Birthday: birthday,
|
||||||
RecoveryWindow: recoveryWindow,
|
RecoveryWindow: recoveryWindow,
|
||||||
Wallet: newWallet,
|
Wallet: newWallet,
|
||||||
ChansToRestore: initMsg.ChanBackups,
|
ChansToRestore: initMsg.ChanBackups,
|
||||||
UnloadWallet: loader.UnloadWallet,
|
UnloadWallet: loader.UnloadWallet,
|
||||||
|
StatelessInit: initMsg.StatelessInit,
|
||||||
|
MacResponseChan: pwService.MacResponseChan,
|
||||||
}, shutdown, nil
|
}, shutdown, nil
|
||||||
|
|
||||||
// The wallet has already been created in the past, and is simply being
|
// The wallet has already been created in the past, and is simply being
|
||||||
// unlocked. So we'll just return these passphrases.
|
// unlocked. So we'll just return these passphrases.
|
||||||
case unlockMsg := <-pwService.UnlockMsgs:
|
case unlockMsg := <-pwService.UnlockMsgs:
|
||||||
return &WalletUnlockParams{
|
return &WalletUnlockParams{
|
||||||
Password: unlockMsg.Passphrase,
|
Password: unlockMsg.Passphrase,
|
||||||
RecoveryWindow: unlockMsg.RecoveryWindow,
|
RecoveryWindow: unlockMsg.RecoveryWindow,
|
||||||
Wallet: unlockMsg.Wallet,
|
Wallet: unlockMsg.Wallet,
|
||||||
ChansToRestore: unlockMsg.ChanBackups,
|
ChansToRestore: unlockMsg.ChanBackups,
|
||||||
UnloadWallet: unlockMsg.UnloadWallet,
|
UnloadWallet: unlockMsg.UnloadWallet,
|
||||||
|
StatelessInit: unlockMsg.StatelessInit,
|
||||||
|
MacResponseChan: pwService.MacResponseChan,
|
||||||
}, shutdown, nil
|
}, shutdown, nil
|
||||||
|
|
||||||
case <-signal.ShutdownChannel():
|
case <-signal.ShutdownChannel():
|
||||||
|
@ -16,6 +16,13 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lnrpc"
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
||||||
|
"github.com/lightningnetwork/lnd/macaroons"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrUnlockTimeout signals that we did not get the expected unlock
|
||||||
|
// message before the timeout occurred.
|
||||||
|
ErrUnlockTimeout = errors.New("got no unlock message before timeout")
|
||||||
)
|
)
|
||||||
|
|
||||||
// ChannelsToRecover wraps any set of packed (serialized+encrypted) channel
|
// ChannelsToRecover wraps any set of packed (serialized+encrypted) channel
|
||||||
@ -54,6 +61,11 @@ type WalletInitMsg struct {
|
|||||||
// ChanBackups a set of static channel backups that should be received
|
// ChanBackups a set of static channel backups that should be received
|
||||||
// after the wallet has been initialized.
|
// after the wallet has been initialized.
|
||||||
ChanBackups ChannelsToRecover
|
ChanBackups ChannelsToRecover
|
||||||
|
|
||||||
|
// StatelessInit signals that the user requested the daemon to be
|
||||||
|
// initialized stateless, which means no unencrypted macaroons should be
|
||||||
|
// written to disk.
|
||||||
|
StatelessInit bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// WalletUnlockMsg is a message sent by the UnlockerService when a user wishes
|
// WalletUnlockMsg is a message sent by the UnlockerService when a user wishes
|
||||||
@ -85,6 +97,11 @@ type WalletUnlockMsg struct {
|
|||||||
// UnloadWallet is a function for unloading the wallet, which should
|
// UnloadWallet is a function for unloading the wallet, which should
|
||||||
// be called on shutdown.
|
// be called on shutdown.
|
||||||
UnloadWallet func() error
|
UnloadWallet func() error
|
||||||
|
|
||||||
|
// StatelessInit signals that the user requested the daemon to be
|
||||||
|
// initialized stateless, which means no unencrypted macaroons should be
|
||||||
|
// written to disk.
|
||||||
|
StatelessInit bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnlockerService implements the WalletUnlocker service used to provide lnd
|
// UnlockerService implements the WalletUnlocker service used to provide lnd
|
||||||
@ -100,10 +117,18 @@ type UnlockerService struct {
|
|||||||
// sent.
|
// sent.
|
||||||
UnlockMsgs chan *WalletUnlockMsg
|
UnlockMsgs chan *WalletUnlockMsg
|
||||||
|
|
||||||
|
// MacResponseChan is the channel for sending back the admin macaroon to
|
||||||
|
// the WalletUnlocker service.
|
||||||
|
MacResponseChan chan []byte
|
||||||
|
|
||||||
chainDir string
|
chainDir string
|
||||||
noFreelistSync bool
|
noFreelistSync bool
|
||||||
netParams *chaincfg.Params
|
netParams *chaincfg.Params
|
||||||
macaroonFiles []string
|
|
||||||
|
// macaroonFiles is the path to the three generated macaroons with
|
||||||
|
// different access permissions. These might not exist in a stateless
|
||||||
|
// initialization of lnd.
|
||||||
|
macaroonFiles []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates and returns a new UnlockerService.
|
// New creates and returns a new UnlockerService.
|
||||||
@ -111,11 +136,15 @@ func New(chainDir string, params *chaincfg.Params, noFreelistSync bool,
|
|||||||
macaroonFiles []string) *UnlockerService {
|
macaroonFiles []string) *UnlockerService {
|
||||||
|
|
||||||
return &UnlockerService{
|
return &UnlockerService{
|
||||||
InitMsgs: make(chan *WalletInitMsg, 1),
|
InitMsgs: make(chan *WalletInitMsg, 1),
|
||||||
UnlockMsgs: make(chan *WalletUnlockMsg, 1),
|
UnlockMsgs: make(chan *WalletUnlockMsg, 1),
|
||||||
chainDir: chainDir,
|
|
||||||
netParams: params,
|
// Make sure we buffer the channel is buffered so the main lnd
|
||||||
macaroonFiles: macaroonFiles,
|
// goroutine isn't blocking on writing to it.
|
||||||
|
MacResponseChan: make(chan []byte, 1),
|
||||||
|
chainDir: chainDir,
|
||||||
|
netParams: params,
|
||||||
|
macaroonFiles: macaroonFiles,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,7 +156,7 @@ func New(chainDir string, params *chaincfg.Params, noFreelistSync bool,
|
|||||||
// Once the cipherseed is obtained and verified by the user, the InitWallet
|
// 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
|
// method should be used to commit the newly generated seed, and create the
|
||||||
// wallet.
|
// wallet.
|
||||||
func (u *UnlockerService) GenSeed(ctx context.Context,
|
func (u *UnlockerService) GenSeed(_ context.Context,
|
||||||
in *lnrpc.GenSeedRequest) (*lnrpc.GenSeedResponse, error) {
|
in *lnrpc.GenSeedRequest) (*lnrpc.GenSeedResponse, error) {
|
||||||
|
|
||||||
// Before we start, we'll ensure that the wallet hasn't already created
|
// Before we start, we'll ensure that the wallet hasn't already created
|
||||||
@ -297,6 +326,7 @@ func (u *UnlockerService) InitWallet(ctx context.Context,
|
|||||||
Passphrase: password,
|
Passphrase: password,
|
||||||
WalletSeed: cipherSeed,
|
WalletSeed: cipherSeed,
|
||||||
RecoveryWindow: uint32(recoveryWindow),
|
RecoveryWindow: uint32(recoveryWindow),
|
||||||
|
StatelessInit: in.StatelessInit,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Before we return the unlock payload, we'll check if we can extract
|
// Before we return the unlock payload, we'll check if we can extract
|
||||||
@ -306,9 +336,25 @@ func (u *UnlockerService) InitWallet(ctx context.Context,
|
|||||||
initMsg.ChanBackups = *chansToRestore
|
initMsg.ChanBackups = *chansToRestore
|
||||||
}
|
}
|
||||||
|
|
||||||
u.InitMsgs <- initMsg
|
// Deliver the initialization message back to the main daemon.
|
||||||
|
select {
|
||||||
|
case u.InitMsgs <- initMsg:
|
||||||
|
// We need to read from the channel to let the daemon continue
|
||||||
|
// its work and to get the admin macaroon. Once the response
|
||||||
|
// arrives, we directly forward it to the client.
|
||||||
|
select {
|
||||||
|
case adminMac := <-u.MacResponseChan:
|
||||||
|
return &lnrpc.InitWalletResponse{
|
||||||
|
AdminMacaroon: adminMac,
|
||||||
|
}, nil
|
||||||
|
|
||||||
return &lnrpc.InitWalletResponse{}, nil
|
case <-ctx.Done():
|
||||||
|
return nil, ErrUnlockTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ErrUnlockTimeout
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnlockWallet sends the password provided by the incoming UnlockWalletRequest
|
// UnlockWallet sends the password provided by the incoming UnlockWalletRequest
|
||||||
@ -351,6 +397,7 @@ func (u *UnlockerService) UnlockWallet(ctx context.Context,
|
|||||||
RecoveryWindow: recoveryWindow,
|
RecoveryWindow: recoveryWindow,
|
||||||
Wallet: unlockedWallet,
|
Wallet: unlockedWallet,
|
||||||
UnloadWallet: loader.UnloadWallet,
|
UnloadWallet: loader.UnloadWallet,
|
||||||
|
StatelessInit: in.StatelessInit,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Before we return the unlock payload, we'll check if we can extract
|
// Before we return the unlock payload, we'll check if we can extract
|
||||||
@ -360,12 +407,25 @@ func (u *UnlockerService) UnlockWallet(ctx context.Context,
|
|||||||
walletUnlockMsg.ChanBackups = *chansToRestore
|
walletUnlockMsg.ChanBackups = *chansToRestore
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this point we was able to open the existing wallet with the
|
// At this point we were able to open the existing wallet with the
|
||||||
// provided password. We send the password over the UnlockMsgs
|
// provided password. We send the password over the UnlockMsgs
|
||||||
// channel, such that it can be used by lnd to open the wallet.
|
// channel, such that it can be used by lnd to open the wallet.
|
||||||
u.UnlockMsgs <- walletUnlockMsg
|
select {
|
||||||
|
case u.UnlockMsgs <- walletUnlockMsg:
|
||||||
|
// We need to read from the channel to let the daemon continue
|
||||||
|
// its work. But we don't need the returned macaroon for this
|
||||||
|
// operation, so we read it but then discard it.
|
||||||
|
select {
|
||||||
|
case <-u.MacResponseChan:
|
||||||
|
return &lnrpc.UnlockWalletResponse{}, nil
|
||||||
|
|
||||||
return &lnrpc.UnlockWalletResponse{}, nil
|
case <-ctx.Done():
|
||||||
|
return nil, ErrUnlockTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ErrUnlockTimeout
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChangePassword changes the password of the wallet and sends the new password
|
// ChangePassword changes the password of the wallet and sends the new password
|
||||||
@ -408,18 +468,32 @@ func (u *UnlockerService) ChangePassword(ctx context.Context,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Unload the wallet to allow lnd to open it later on.
|
|
||||||
defer loader.UnloadWallet()
|
|
||||||
|
|
||||||
// Since the macaroon database is also encrypted with the wallet's
|
// Now that we've opened the wallet, we need to close it in case of an
|
||||||
// password, we'll remove all of the macaroon files so that they're
|
// error. But not if we succeed, then the caller must close it.
|
||||||
// re-generated at startup using the new password. We'll make sure to do
|
orderlyReturn := false
|
||||||
// this after unlocking the wallet to ensure macaroon files don't get
|
defer func() {
|
||||||
// deleted with incorrect password attempts.
|
if !orderlyReturn {
|
||||||
for _, file := range u.macaroonFiles {
|
_ = loader.UnloadWallet()
|
||||||
err := os.Remove(file)
|
}
|
||||||
if err != nil && !os.IsNotExist(err) {
|
}()
|
||||||
return nil, err
|
|
||||||
|
// Before we actually change the password, we need to check if all flags
|
||||||
|
// were set correctly. The content of the previously generated macaroon
|
||||||
|
// files will become invalid after we generate a new root key. So we try
|
||||||
|
// to delete them here and they will be recreated during normal startup
|
||||||
|
// later. If they are missing, this is only an error if the
|
||||||
|
// stateless_init flag was not set.
|
||||||
|
if in.NewMacaroonRootKey || in.StatelessInit {
|
||||||
|
for _, file := range u.macaroonFiles {
|
||||||
|
err := os.Remove(file)
|
||||||
|
if err != nil && !in.StatelessInit {
|
||||||
|
return nil, fmt.Errorf("could not remove "+
|
||||||
|
"macaroon file: %v. if the wallet "+
|
||||||
|
"was initialized stateless please "+
|
||||||
|
"add the --stateless_init "+
|
||||||
|
"flag", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -434,11 +508,86 @@ func (u *UnlockerService) ChangePassword(ctx context.Context,
|
|||||||
"%v", err)
|
"%v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The next step is to load the macaroon database, change the password
|
||||||
|
// then close it again.
|
||||||
|
// Attempt to open the macaroon DB, unlock it and then change
|
||||||
|
// the passphrase.
|
||||||
|
macaroonService, err := macaroons.NewService(
|
||||||
|
netDir, "lnd", in.StatelessInit,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = macaroonService.CreateUnlock(&privatePw)
|
||||||
|
if err != nil {
|
||||||
|
closeErr := macaroonService.Close()
|
||||||
|
if closeErr != nil {
|
||||||
|
return nil, fmt.Errorf("could not create unlock: %v "+
|
||||||
|
"--> follow-up error when closing: %v", err,
|
||||||
|
closeErr)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = macaroonService.ChangePassword(privatePw, in.NewPassword)
|
||||||
|
if err != nil {
|
||||||
|
closeErr := macaroonService.Close()
|
||||||
|
if closeErr != nil {
|
||||||
|
return nil, fmt.Errorf("could not change password: %v "+
|
||||||
|
"--> follow-up error when closing: %v", err,
|
||||||
|
closeErr)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If requested by the user, attempt to replace the existing
|
||||||
|
// macaroon root key with a new one.
|
||||||
|
if in.NewMacaroonRootKey {
|
||||||
|
err = macaroonService.GenerateNewRootKey()
|
||||||
|
if err != nil {
|
||||||
|
closeErr := macaroonService.Close()
|
||||||
|
if closeErr != nil {
|
||||||
|
return nil, fmt.Errorf("could not generate "+
|
||||||
|
"new root key: %v --> follow-up error "+
|
||||||
|
"when closing: %v", err, closeErr)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = macaroonService.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not close macaroon service: %v",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
|
||||||
// Finally, send the new password across the UnlockPasswords channel to
|
// Finally, send the new password across the UnlockPasswords channel to
|
||||||
// automatically unlock the wallet.
|
// automatically unlock the wallet.
|
||||||
u.UnlockMsgs <- &WalletUnlockMsg{Passphrase: in.NewPassword}
|
walletUnlockMsg := &WalletUnlockMsg{
|
||||||
|
Passphrase: in.NewPassword,
|
||||||
|
Wallet: w,
|
||||||
|
StatelessInit: in.StatelessInit,
|
||||||
|
UnloadWallet: loader.UnloadWallet,
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case u.UnlockMsgs <- walletUnlockMsg:
|
||||||
|
// We need to read from the channel to let the daemon continue
|
||||||
|
// its work and to get the admin macaroon. Once the response
|
||||||
|
// arrives, we directly forward it to the client.
|
||||||
|
orderlyReturn = true
|
||||||
|
select {
|
||||||
|
case adminMac := <-u.MacResponseChan:
|
||||||
|
return &lnrpc.ChangePasswordResponse{
|
||||||
|
AdminMacaroon: adminMac,
|
||||||
|
}, nil
|
||||||
|
|
||||||
return &lnrpc.ChangePasswordResponse{}, nil
|
case <-ctx.Done():
|
||||||
|
return nil, ErrUnlockTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ErrUnlockTimeout
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidatePassword assures the password meets all of our constraints.
|
// ValidatePassword assures the password meets all of our constraints.
|
||||||
|
@ -3,8 +3,10 @@ package walletunlocker_test
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -13,8 +15,10 @@ import (
|
|||||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||||
"github.com/btcsuite/btcwallet/wallet"
|
"github.com/btcsuite/btcwallet/wallet"
|
||||||
"github.com/lightningnetwork/lnd/aezeed"
|
"github.com/lightningnetwork/lnd/aezeed"
|
||||||
|
"github.com/lightningnetwork/lnd/channeldb/kvdb"
|
||||||
"github.com/lightningnetwork/lnd/keychain"
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
"github.com/lightningnetwork/lnd/lnrpc"
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
||||||
"github.com/lightningnetwork/lnd/macaroons"
|
"github.com/lightningnetwork/lnd/macaroons"
|
||||||
"github.com/lightningnetwork/lnd/walletunlocker"
|
"github.com/lightningnetwork/lnd/walletunlocker"
|
||||||
@ -24,6 +28,7 @@ import (
|
|||||||
var (
|
var (
|
||||||
testPassword = []byte("test-password")
|
testPassword = []byte("test-password")
|
||||||
testSeed = []byte("test-seed-123456789")
|
testSeed = []byte("test-seed-123456789")
|
||||||
|
testMac = []byte("fakemacaroon")
|
||||||
|
|
||||||
testEntropy = [aezeed.EntropySize]byte{
|
testEntropy = [aezeed.EntropySize]byte{
|
||||||
0x81, 0xb6, 0x37, 0xd8,
|
0x81, 0xb6, 0x37, 0xd8,
|
||||||
@ -44,6 +49,12 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func createTestWallet(t *testing.T, dir string, netParams *chaincfg.Params) {
|
func createTestWallet(t *testing.T, dir string, netParams *chaincfg.Params) {
|
||||||
|
createTestWalletWithPw(t, testPassword, testPassword, dir, netParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTestWalletWithPw(t *testing.T, pubPw, privPw []byte, dir string,
|
||||||
|
netParams *chaincfg.Params) {
|
||||||
|
|
||||||
// Instruct waddrmgr to use the cranked down scrypt parameters when
|
// Instruct waddrmgr to use the cranked down scrypt parameters when
|
||||||
// creating new wallet encryption keys.
|
// creating new wallet encryption keys.
|
||||||
fastScrypt := waddrmgr.FastScryptOptions
|
fastScrypt := waddrmgr.FastScryptOptions
|
||||||
@ -60,13 +71,66 @@ func createTestWallet(t *testing.T, dir string, netParams *chaincfg.Params) {
|
|||||||
netDir := btcwallet.NetworkDir(dir, netParams)
|
netDir := btcwallet.NetworkDir(dir, netParams)
|
||||||
loader := wallet.NewLoader(netParams, netDir, true, 0)
|
loader := wallet.NewLoader(netParams, netDir, true, 0)
|
||||||
_, err := loader.CreateNewWallet(
|
_, err := loader.CreateNewWallet(
|
||||||
testPassword, testPassword, testSeed, time.Time{},
|
pubPw, privPw, testSeed, time.Time{},
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = loader.UnloadWallet()
|
err = loader.UnloadWallet()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createSeedAndMnemonic(t *testing.T,
|
||||||
|
pass []byte) (*aezeed.CipherSeed, aezeed.Mnemonic) {
|
||||||
|
cipherSeed, err := aezeed.New(
|
||||||
|
keychain.KeyDerivationVersion, &testEntropy, time.Now(),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// With the new seed created, we'll convert it into a mnemonic phrase
|
||||||
|
// that we'll send over to initialize the wallet.
|
||||||
|
mnemonic, err := cipherSeed.ToMnemonic(pass)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return cipherSeed, mnemonic
|
||||||
|
}
|
||||||
|
|
||||||
|
// openOrCreateTestMacStore opens or creates a bbolt DB and then initializes a
|
||||||
|
// root key storage for that DB and then unlocks it, creating a root key in the
|
||||||
|
// process.
|
||||||
|
func openOrCreateTestMacStore(tempDir string, pw *[]byte,
|
||||||
|
netParams *chaincfg.Params) (*macaroons.RootKeyStorage, error) {
|
||||||
|
|
||||||
|
netDir := btcwallet.NetworkDir(tempDir, netParams)
|
||||||
|
err := os.MkdirAll(netDir, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
db, err := kvdb.Create(
|
||||||
|
kvdb.BoltBackendName, path.Join(netDir, macaroons.DBFilename),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
store, err := macaroons.NewRootKeyStorage(db)
|
||||||
|
if err != nil {
|
||||||
|
_ = db.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.CreateUnlock(pw)
|
||||||
|
if err != nil {
|
||||||
|
_ = store.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, _, err = store.RootKey(defaultRootKeyIDContext)
|
||||||
|
if err != nil {
|
||||||
|
_ = store.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return store, nil
|
||||||
|
}
|
||||||
|
|
||||||
// TestGenSeedUserEntropy tests that the gen seed method generates a valid
|
// TestGenSeedUserEntropy tests that the gen seed method generates a valid
|
||||||
// cipher seed mnemonic phrase and user provided source of entropy.
|
// cipher seed mnemonic phrase and user provided source of entropy.
|
||||||
func TestGenSeed(t *testing.T) {
|
func TestGenSeed(t *testing.T) {
|
||||||
@ -183,17 +247,9 @@ func TestInitWallet(t *testing.T) {
|
|||||||
service := walletunlocker.New(testDir, testNetParams, true, nil)
|
service := walletunlocker.New(testDir, testNetParams, true, nil)
|
||||||
|
|
||||||
// Once we have the unlocker service created, we'll now instantiate a
|
// Once we have the unlocker service created, we'll now instantiate a
|
||||||
// new cipher seed instance.
|
// new cipher seed and its mnemonic.
|
||||||
cipherSeed, err := aezeed.New(
|
|
||||||
keychain.KeyDerivationVersion, &testEntropy, time.Now(),
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// 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")
|
pass := []byte("test")
|
||||||
mnemonic, err := cipherSeed.ToMnemonic(pass)
|
cipherSeed, mnemonic := createSeedAndMnemonic(t, pass)
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Now that we have all the necessary items, we'll now issue the Init
|
// 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
|
// command to the wallet. This should check the validity of the cipher
|
||||||
@ -205,13 +261,29 @@ func TestInitWallet(t *testing.T) {
|
|||||||
CipherSeedMnemonic: mnemonic[:],
|
CipherSeedMnemonic: mnemonic[:],
|
||||||
AezeedPassphrase: pass,
|
AezeedPassphrase: pass,
|
||||||
RecoveryWindow: int32(testRecoveryWindow),
|
RecoveryWindow: int32(testRecoveryWindow),
|
||||||
|
StatelessInit: true,
|
||||||
}
|
}
|
||||||
_, err = service.InitWallet(ctx, req)
|
errChan := make(chan error, 1)
|
||||||
require.NoError(t, err)
|
go func() {
|
||||||
|
response, err := service.InitWallet(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
errChan <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(response.AdminMacaroon, testMac) {
|
||||||
|
errChan <- fmt.Errorf("mismatched macaroon: "+
|
||||||
|
"expected %x, got %x", testMac,
|
||||||
|
response.AdminMacaroon)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// The same user passphrase, and also the plaintext cipher seed
|
// The same user passphrase, and also the plaintext cipher seed
|
||||||
// should be sent over and match exactly.
|
// should be sent over and match exactly.
|
||||||
select {
|
select {
|
||||||
|
case err := <-errChan:
|
||||||
|
t.Fatalf("InitWallet call failed: %v", err)
|
||||||
|
|
||||||
case msg := <-service.InitMsgs:
|
case msg := <-service.InitMsgs:
|
||||||
msgSeed := msg.WalletSeed
|
msgSeed := msg.WalletSeed
|
||||||
require.Equal(t, testPassword, msg.Passphrase)
|
require.Equal(t, testPassword, msg.Passphrase)
|
||||||
@ -221,6 +293,11 @@ func TestInitWallet(t *testing.T) {
|
|||||||
require.Equal(t, cipherSeed.Birthday, msgSeed.Birthday)
|
require.Equal(t, cipherSeed.Birthday, msgSeed.Birthday)
|
||||||
require.Equal(t, cipherSeed.Entropy, msgSeed.Entropy)
|
require.Equal(t, cipherSeed.Entropy, msgSeed.Entropy)
|
||||||
require.Equal(t, testRecoveryWindow, msg.RecoveryWindow)
|
require.Equal(t, testRecoveryWindow, msg.RecoveryWindow)
|
||||||
|
require.Equal(t, true, msg.StatelessInit)
|
||||||
|
|
||||||
|
// Send a fake macaroon that should be returned in the response
|
||||||
|
// in the async code above.
|
||||||
|
service.MacResponseChan <- testMac
|
||||||
|
|
||||||
case <-time.After(defaultTestTimeout):
|
case <-time.After(defaultTestTimeout):
|
||||||
t.Fatalf("password not received")
|
t.Fatalf("password not received")
|
||||||
@ -288,6 +365,7 @@ func TestUnlockWallet(t *testing.T) {
|
|||||||
req := &lnrpc.UnlockWalletRequest{
|
req := &lnrpc.UnlockWalletRequest{
|
||||||
WalletPassword: testPassword,
|
WalletPassword: testPassword,
|
||||||
RecoveryWindow: int32(testRecoveryWindow),
|
RecoveryWindow: int32(testRecoveryWindow),
|
||||||
|
StatelessInit: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should fail to unlock non-existing wallet.
|
// Should fail to unlock non-existing wallet.
|
||||||
@ -305,23 +383,39 @@ func TestUnlockWallet(t *testing.T) {
|
|||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
// With the correct password, we should be able to unlock the wallet.
|
// With the correct password, we should be able to unlock the wallet.
|
||||||
_, err = service.UnlockWallet(ctx, req)
|
errChan := make(chan error, 1)
|
||||||
require.NoError(t, err)
|
go func() {
|
||||||
|
// With the correct password, we should be able to unlock the
|
||||||
|
// wallet.
|
||||||
|
_, err := service.UnlockWallet(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
errChan <- err
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Password and recovery window should be sent over the channel.
|
// Password and recovery window should be sent over the channel.
|
||||||
select {
|
select {
|
||||||
|
case err := <-errChan:
|
||||||
|
t.Fatalf("UnlockWallet call failed: %v", err)
|
||||||
|
|
||||||
case unlockMsg := <-service.UnlockMsgs:
|
case unlockMsg := <-service.UnlockMsgs:
|
||||||
require.Equal(t, testPassword, unlockMsg.Passphrase)
|
require.Equal(t, testPassword, unlockMsg.Passphrase)
|
||||||
require.Equal(t, testRecoveryWindow, unlockMsg.RecoveryWindow)
|
require.Equal(t, testRecoveryWindow, unlockMsg.RecoveryWindow)
|
||||||
|
require.Equal(t, true, unlockMsg.StatelessInit)
|
||||||
|
|
||||||
|
// Send a fake macaroon that should be returned in the response
|
||||||
|
// in the async code above.
|
||||||
|
service.MacResponseChan <- testMac
|
||||||
|
|
||||||
case <-time.After(defaultTestTimeout):
|
case <-time.After(defaultTestTimeout):
|
||||||
t.Fatalf("password not received")
|
t.Fatalf("password not received")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestChangeWalletPassword tests that we can successfully change the wallet's
|
// TestChangeWalletPasswordNewRootkey tests that we can successfully change the
|
||||||
// password needed to unlock it.
|
// wallet's password needed to unlock it and rotate the root key for the
|
||||||
func TestChangeWalletPassword(t *testing.T) {
|
// macaroons in the same process.
|
||||||
|
func TestChangeWalletPasswordNewRootkey(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// testDir is empty, meaning wallet was not created from before.
|
// testDir is empty, meaning wallet was not created from before.
|
||||||
@ -331,8 +425,18 @@ func TestChangeWalletPassword(t *testing.T) {
|
|||||||
_ = os.RemoveAll(testDir)
|
_ = os.RemoveAll(testDir)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Changing the password of the wallet will also try to change the
|
||||||
|
// password of the macaroon DB. We create a default DB here but close it
|
||||||
|
// immediately so the service does not fail when trying to open it.
|
||||||
|
store, err := openOrCreateTestMacStore(
|
||||||
|
testDir, &testPassword, testNetParams,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, store.Close())
|
||||||
|
|
||||||
// Create some files that will act as macaroon files that should be
|
// Create some files that will act as macaroon files that should be
|
||||||
// deleted after a password change is successful.
|
// deleted after a password change is successful with a new root key
|
||||||
|
// requested.
|
||||||
var tempFiles []string
|
var tempFiles []string
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
file, err := ioutil.TempFile(testDir, "")
|
file, err := ioutil.TempFile(testDir, "")
|
||||||
@ -350,8 +454,9 @@ func TestChangeWalletPassword(t *testing.T) {
|
|||||||
newPassword := []byte("hunter2???")
|
newPassword := []byte("hunter2???")
|
||||||
|
|
||||||
req := &lnrpc.ChangePasswordRequest{
|
req := &lnrpc.ChangePasswordRequest{
|
||||||
CurrentPassword: testPassword,
|
CurrentPassword: testPassword,
|
||||||
NewPassword: newPassword,
|
NewPassword: newPassword,
|
||||||
|
NewMacaroonRootKey: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Changing the password to a non-existing wallet should fail.
|
// Changing the password to a non-existing wallet should fail.
|
||||||
@ -387,8 +492,24 @@ func TestChangeWalletPassword(t *testing.T) {
|
|||||||
// When providing the correct wallet's current password and a new
|
// When providing the correct wallet's current password and a new
|
||||||
// password that meets the length requirement, the password change
|
// password that meets the length requirement, the password change
|
||||||
// should succeed.
|
// should succeed.
|
||||||
_, err = service.ChangePassword(ctx, req)
|
errChan := make(chan error, 1)
|
||||||
require.NoError(t, err)
|
go doChangePassword(service, testDir, req, errChan)
|
||||||
|
|
||||||
|
// The new password should be sent over the channel.
|
||||||
|
select {
|
||||||
|
case err := <-errChan:
|
||||||
|
t.Fatalf("ChangePassword call failed: %v", err)
|
||||||
|
|
||||||
|
case unlockMsg := <-service.UnlockMsgs:
|
||||||
|
require.Equal(t, newPassword, unlockMsg.Passphrase)
|
||||||
|
|
||||||
|
// Send a fake macaroon that should be returned in the response
|
||||||
|
// in the async code above.
|
||||||
|
service.MacResponseChan <- testMac
|
||||||
|
|
||||||
|
case <-time.After(defaultTestTimeout):
|
||||||
|
t.Fatalf("password not received")
|
||||||
|
}
|
||||||
|
|
||||||
// The files should no longer exist.
|
// The files should no longer exist.
|
||||||
for _, tempFile := range tempFiles {
|
for _, tempFile := range tempFiles {
|
||||||
@ -396,13 +517,146 @@ func TestChangeWalletPassword(t *testing.T) {
|
|||||||
t.Fatal("file exists but it shouldn't")
|
t.Fatal("file exists but it shouldn't")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The new password should be sent over the channel.
|
// TestChangeWalletPasswordStateless checks that trying to change the password
|
||||||
|
// of an existing wallet that was initialized stateless works when when the
|
||||||
|
// --stateless_init flat is set. Also checks that if no password is given,
|
||||||
|
// the default password is used.
|
||||||
|
func TestChangeWalletPasswordStateless(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// testDir is empty, meaning wallet was not created from before.
|
||||||
|
testDir, err := ioutil.TempDir("", "testchangepasswordstateless")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() {
|
||||||
|
_ = os.RemoveAll(testDir)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Changing the password of the wallet will also try to change the
|
||||||
|
// password of the macaroon DB. We create a default DB here but close it
|
||||||
|
// immediately so the service does not fail when trying to open it.
|
||||||
|
store, err := openOrCreateTestMacStore(
|
||||||
|
testDir, &lnwallet.DefaultPrivatePassphrase, testNetParams,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, store.Close())
|
||||||
|
|
||||||
|
// Create a temp file that will act as the macaroon DB file that will
|
||||||
|
// be deleted by changing the password.
|
||||||
|
tmpFile, err := ioutil.TempFile(testDir, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
tempMacFile := tmpFile.Name()
|
||||||
|
err = tmpFile.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a file name that does not exist that will be used as a
|
||||||
|
// macaroon file reference. The fact that the file does not exist should
|
||||||
|
// not throw an error when --stateless_init is used.
|
||||||
|
nonExistingFile := path.Join(testDir, "does-not-exist")
|
||||||
|
|
||||||
|
// Create a new UnlockerService with our temp files.
|
||||||
|
service := walletunlocker.New(testDir, testNetParams, true, []string{
|
||||||
|
tempMacFile, nonExistingFile,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a wallet we can try to unlock. We use the default password
|
||||||
|
// so we can check that the unlocker service defaults to this when
|
||||||
|
// we give it an empty CurrentPassword to indicate we come from a
|
||||||
|
// --noencryptwallet state.
|
||||||
|
createTestWalletWithPw(
|
||||||
|
t, lnwallet.DefaultPublicPassphrase,
|
||||||
|
lnwallet.DefaultPrivatePassphrase, testDir, testNetParams,
|
||||||
|
)
|
||||||
|
|
||||||
|
// We make sure that we get a proper error message if we forget to
|
||||||
|
// add the --stateless_init flag but the macaroon files don't exist.
|
||||||
|
badReq := &lnrpc.ChangePasswordRequest{
|
||||||
|
NewPassword: testPassword,
|
||||||
|
NewMacaroonRootKey: true,
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
_, err = service.ChangePassword(ctx, badReq)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// Prepare the correct request we are going to send to the unlocker
|
||||||
|
// service. We don't provide a current password to indicate there
|
||||||
|
// was none set before.
|
||||||
|
req := &lnrpc.ChangePasswordRequest{
|
||||||
|
NewPassword: testPassword,
|
||||||
|
StatelessInit: true,
|
||||||
|
NewMacaroonRootKey: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since we indicated the wallet was initialized stateless, the service
|
||||||
|
// will block until it receives the macaroon through the channel
|
||||||
|
// provided in the message in UnlockMsgs. So we need to call the service
|
||||||
|
// async and then wait for the unlock message to arrive so we can send
|
||||||
|
// back a fake macaroon.
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
go doChangePassword(service, testDir, req, errChan)
|
||||||
|
|
||||||
|
// Password and recovery window should be sent over the channel.
|
||||||
select {
|
select {
|
||||||
|
case err := <-errChan:
|
||||||
|
t.Fatalf("ChangePassword call failed: %v", err)
|
||||||
|
|
||||||
case unlockMsg := <-service.UnlockMsgs:
|
case unlockMsg := <-service.UnlockMsgs:
|
||||||
require.Equal(t, newPassword, unlockMsg.Passphrase)
|
require.Equal(t, testPassword, unlockMsg.Passphrase)
|
||||||
|
|
||||||
|
// Send a fake macaroon that should be returned in the response
|
||||||
|
// in the async code above.
|
||||||
|
service.MacResponseChan <- testMac
|
||||||
|
|
||||||
case <-time.After(defaultTestTimeout):
|
case <-time.After(defaultTestTimeout):
|
||||||
t.Fatalf("password not received")
|
t.Fatalf("password not received")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func doChangePassword(service *walletunlocker.UnlockerService, testDir string,
|
||||||
|
req *lnrpc.ChangePasswordRequest, errChan chan error) {
|
||||||
|
|
||||||
|
// When providing the correct wallet's current password and a
|
||||||
|
// new password that meets the length requirement, the password
|
||||||
|
// change should succeed.
|
||||||
|
ctx := context.Background()
|
||||||
|
response, err := service.ChangePassword(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
errChan <- fmt.Errorf("could not change password: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(response.AdminMacaroon, testMac) {
|
||||||
|
errChan <- fmt.Errorf("mismatched macaroon: expected "+
|
||||||
|
"%x, got %x", testMac, response.AdminMacaroon)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the macaroon DB and try to open it and read the root
|
||||||
|
// key with the new password.
|
||||||
|
store, err := openOrCreateTestMacStore(
|
||||||
|
testDir, &testPassword, testNetParams,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
errChan <- fmt.Errorf("could not create test store: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _, err = store.RootKey(defaultRootKeyIDContext)
|
||||||
|
if err != nil {
|
||||||
|
errChan <- fmt.Errorf("could not get root key: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do cleanup now. Since we are in a go func, the defer at the
|
||||||
|
// top of the outer would not work, because it would delete
|
||||||
|
// the directory before we could check the content in here.
|
||||||
|
err = store.Close()
|
||||||
|
if err != nil {
|
||||||
|
errChan <- fmt.Errorf("could not close store: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = os.RemoveAll(testDir)
|
||||||
|
if err != nil {
|
||||||
|
errChan <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user