Merge pull request #1288 from guggero/stateless-init

walletunlocker+lnd: add command line flag to allow passing admin macaroon after wallet creation
This commit is contained in:
Oliver Gugger 2020-11-07 12:39:04 +00:00 committed by GitHub
commit 72cacb9c5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1628 additions and 506 deletions

View File

@ -4,16 +4,31 @@ import (
"bufio"
"bytes"
"context"
"encoding/hex"
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/walletunlocker"
"github.com/urfave/cli"
)
var (
statelessInitFlag = cli.BoolFlag{
Name: "stateless_init",
Usage: "do not create any macaroon files in the file " +
"system of the daemon",
}
saveToFlag = cli.StringFlag{
Name: "save_to",
Usage: "save returned admin macaroon to this file",
}
)
var createCommand = cli.Command{
Name: "create",
Category: "Startup",
@ -37,6 +52,14 @@ var createCommand = cli.Command{
to potentially recover all on-chain funds, and most off-chain funds as
well.
If the --stateless_init flag is set, no macaroon files are created by
the daemon. Instead, the binary serialized admin macaroon is returned
in the answer. This answer MUST be stored somewhere, otherwise all
access to the RPC server will be lost and the wallet must be recreated
to re-gain access.
If the --save_to parameter is set, the macaroon is saved to this file,
otherwise it is printed to standard out.
Finally, it's also possible to use this command and a set of static
channel backups to trigger a recover attempt for the provided Static
Channel Backups. Only one of the three parameters will be accepted. See
@ -58,6 +81,8 @@ var createCommand = cli.Command{
Name: "multi_file",
Usage: "the path to a multi-channel back up file",
},
statelessInitFlag,
saveToFlag,
},
Action: actionDecorator(create),
}
@ -171,7 +196,15 @@ func create(ctx *cli.Context) error {
}
}
}
}
// Should the daemon be initialized stateless? Then we expect an answer
// with the admin macaroon later. Because the --save_to is related to
// stateless init, it doesn't make sense to be set on its own.
statelessInit := ctx.Bool(statelessInitFlag.Name)
if !statelessInit && ctx.IsSet(saveToFlag.Name) {
return fmt.Errorf("cannot set save_to parameter without " +
"stateless_init")
}
walletPassword, err := capturePassword(
@ -349,13 +382,19 @@ mnemonicCheck:
AezeedPassphrase: aezeedPass,
RecoveryWindow: recoveryWindow,
ChannelBackups: chanBackups,
StatelessInit: statelessInit,
}
if _, err := client.InitWallet(ctxb, req); err != nil {
response, err := client.InitWallet(ctxb, req)
if err != nil {
return err
}
fmt.Println("\nlnd successfully initialized!")
if statelessInit {
return storeOrPrintAdminMac(ctx, response.AdminMacaroon)
}
return nil
}
@ -410,6 +449,12 @@ var unlockCommand = cli.Command{
start up. This command MUST be run after booting up lnd before it's
able to carry out its duties. An exception is if a user is running with
--noseedbackup, then a default passphrase will be used.
If the --stateless_init flag is set, no macaroon files are created by
the daemon. This should be set for every unlock if the daemon was
initially initialized stateless. Otherwise the daemon will create
unencrypted macaroon files which could leak information to the system
that the daemon runs on.
`,
Flags: []cli.Flag{
cli.IntFlag{
@ -430,6 +475,7 @@ var unlockCommand = cli.Command{
"combination with some sort of password " +
"manager or secrets vault.",
},
statelessInitFlag,
},
Action: actionDecorator(unlock),
}
@ -485,6 +531,7 @@ func unlock(ctx *cli.Context) error {
req := &lnrpc.UnlockWalletRequest{
WalletPassword: pw,
RecoveryWindow: recoveryWindow,
StatelessInit: ctx.Bool(statelessInitFlag.Name),
}
_, err = client.UnlockWallet(ctxb, req)
if err != nil {
@ -511,7 +558,35 @@ var changePasswordCommand = cli.Command{
--noseedbackup), one must restart their daemon without
--noseedbackup and use this command. The "current password" field
should be left empty.
If the daemon was originally initialized stateless, then the
--stateless_init flag needs to be set for the change password request
as well! Otherwise the daemon will generate unencrypted macaroon files
in its file system again and possibly leak sensitive information.
Changing the password will by default not change the macaroon root key
(just re-encrypt the macaroon database with the new password). So all
macaroons will still be valid.
If one wants to make sure that all previously created macaroons are
invalidated, a new macaroon root key can be generated by using the
--new_mac_root_key flag.
After a successful password change with the --stateless_init flag set,
the current or new admin macaroon is returned binary serialized in the
answer. This answer MUST then be stored somewhere, otherwise
all access to the RPC server will be lost and the wallet must be re-
created to re-gain access. If the --save_to parameter is set, the
macaroon is saved to this file, otherwise it is printed to standard out.
`,
Flags: []cli.Flag{
statelessInitFlag,
saveToFlag,
cli.BoolFlag{
Name: "new_mac_root_key",
Usage: "rotate the macaroon root key resulting in " +
"all previously created macaroons to be " +
"invalidated",
},
},
Action: actionDecorator(changePassword),
}
@ -539,15 +614,53 @@ func changePassword(ctx *cli.Context) error {
return fmt.Errorf("passwords don't match")
}
req := &lnrpc.ChangePasswordRequest{
CurrentPassword: currentPw,
NewPassword: newPw,
// Should the daemon be initialized stateless? Then we expect an answer
// with the admin macaroon later. Because the --save_to is related to
// stateless init, it doesn't make sense to be set on its own.
statelessInit := ctx.Bool(statelessInitFlag.Name)
if !statelessInit && ctx.IsSet(saveToFlag.Name) {
return fmt.Errorf("cannot set save_to parameter without " +
"stateless_init")
}
_, err = client.ChangePassword(ctxb, req)
req := &lnrpc.ChangePasswordRequest{
CurrentPassword: currentPw,
NewPassword: newPw,
StatelessInit: statelessInit,
NewMacaroonRootKey: ctx.Bool("new_mac_root_key"),
}
response, err := client.ChangePassword(ctxb, req)
if err != nil {
return err
}
if statelessInit {
return storeOrPrintAdminMac(ctx, response.AdminMacaroon)
}
return nil
}
// storeOrPrintAdminMac either stores the admin macaroon to a file specified or
// prints it to standard out, depending on the user flags set.
func storeOrPrintAdminMac(ctx *cli.Context, adminMac []byte) error {
// The user specified the optional --save_to parameter. We'll save the
// macaroon to that file.
if ctx.IsSet("save_to") {
macSavePath := lncfg.CleanAndExpandPath(ctx.String("save_to"))
err := ioutil.WriteFile(macSavePath, adminMac, 0644)
if err != nil {
_ = os.Remove(macSavePath)
return err
}
fmt.Printf("Admin macaroon saved to %s\n", macSavePath)
return nil
}
// Otherwise we just print it. The user MUST store this macaroon
// somewhere so we either save it to a provided file path or just print
// it to standard output.
fmt.Printf("Admin macaroon: %s\n", hex.EncodeToString(adminMac))
return nil
}

View File

@ -109,6 +109,49 @@ timeout can be changed with the `--macaroontimeout` option; this can be
increased for making RPC calls between systems whose clocks are more than 60s
apart.
## Stateless initialization
As mentioned above, by default `lnd` creates several macaroon files in its
directory. These are unencrypted and in case of the `admin.macaroon` provide
full access to the daemon. This can be seen as quite a big security risk if
the `lnd` daemon runs in an environment that is not fully trusted.
The macaroon files are the only files with highly sensitive information that
are not encrypted (unlike the wallet file and the macaroon database file that
contains the [root key](../macaroons/README.md), these are always encrypted,
even if no password is used).
To avoid leaking the macaroon information, `lnd` supports the so called
`stateless initialization` mode:
* The three startup commands `create`, `unlock` and `changepassword` of `lncli`
all have a flag called `--stateless_init` that instructs the daemon **not**
to create `*.macaroon` files.
* The two operations `create` and `changepassword` that actually create/update
the macaroon database will return the admin macaroon in the RPC call.
Assuming the daemon and the `lncli` are not used on the same machine, this
will leave no unencrypted information on the machine where `lnd` runs on.
* To be more precise: By default, when using the `changepassword` command, the
macaroon root key in the macaroon DB is just re-encrypted with the new
password. But the key remains the same and therefore the macaroons issued
before the `changepassword` command still remain valid. If a user wants to
invalidate all previously created macaroons, the `--new_mac_root_key` flag
of the `changepassword` command should be used!
* An user of `lncli` will see the returned admin macaroon printed to the screen
or saved to a file if the parameter `--save_to=some_file.macaroon` is used.
* **Important:** By default, `lnd` will create the macaroon files during the
`unlock` phase, if the `--stateless_init` flag is not used. So to avoid
leakage of the macaroon information, use the stateless initialization flag
for all three startup commands of the wallet unlocker service!
Examples:
* Create a new wallet stateless (first run):
* `lncli create --stateless_init --save_to=/safe/location/admin.macaroon`
* Unlock a wallet that has previously been initialized stateless:
* `lncli unlock --stateless_init`
* Use the created macaroon:
* `lncli --macaroonpath=/safe/location/admin.macaroon getinfo`
## Using Macaroons with GRPC clients
When interacting with `lnd` using the GRPC interface, the macaroons are encoded

248
lnd.go
View File

@ -318,6 +318,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error {
var (
walletInitParams WalletUnlockParams
shutdownUnlocker = func() {}
privateWalletPw = lnwallet.DefaultPrivatePassphrase
publicWalletPw = lnwallet.DefaultPublicPassphrase
)
@ -377,7 +378,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error {
// started with the --noseedbackup flag, we use the default password
// for wallet encryption.
if !cfg.NoSeedBackup {
params, err := waitForWalletPassword(
params, shutdown, err := waitForWalletPassword(
cfg, cfg.RESTListeners, serverOpts, restDialOpts,
restProxyDest, tlsCfg, walletUnlockerListeners,
)
@ -389,6 +390,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error {
}
walletInitParams = *params
shutdownUnlocker = shutdown
privateWalletPw = walletInitParams.Password
publicWalletPw = walletInitParams.Password
defer func() {
@ -408,7 +410,8 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error {
if !cfg.NoMacaroons {
// Create the macaroon authentication/authorization service.
macaroonService, err = macaroons.NewService(
cfg.networkDir, "lnd", macaroons.IPLockChecker,
cfg.networkDir, "lnd", walletInitParams.StatelessInit,
macaroons.IPLockChecker,
)
if err != nil {
err := fmt.Errorf("unable to set up macaroon "+
@ -419,17 +422,42 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error {
defer macaroonService.Close()
// 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)
if err != nil {
if err != nil && err != macaroons.ErrAlreadyUnlocked {
err := fmt.Errorf("unable to unlock macaroons: %v", err)
ltndLog.Error(err)
return err
}
// Create macaroon files for lncli to use if they don't exist.
if !fileExists(cfg.AdminMacPath) && !fileExists(cfg.ReadMacPath) &&
// In case we actually needed to unlock the wallet, we now need
// 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) {
// Create macaroon files for lncli to use if they don't
// exist.
err = genMacaroons(
ctx, macaroonService, cfg.AdminMacPath,
cfg.ReadMacPath, cfg.InvoiceMacPath,
@ -441,8 +469,34 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error {
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
// start the main RPC service later.
shutdownUnlocker()
// With the information parsed from the configuration, create valid
// instances of the pertinent interfaces required to operate the
// Lightning Network Daemon.
@ -944,6 +998,21 @@ func fileExists(name string) bool {
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
// invoice access and one read-only. These can also be used to generate more
// granular macaroons.
@ -954,57 +1023,48 @@ func genMacaroons(ctx context.Context, svc *macaroons.Service,
// access invoice related calls. This is useful for merchants and other
// services to allow an isolated instance that can only query and
// modify invoices.
invoiceMac, err := svc.NewMacaroon(
ctx, macaroons.DefaultRootKeyID, invoicePermissions...,
)
if err != nil {
return err
}
invoiceMacBytes, err := invoiceMac.M().MarshalBinary()
invoiceMacBytes, err := bakeMacaroon(ctx, svc, invoicePermissions)
if err != nil {
return err
}
err = ioutil.WriteFile(invoiceFile, invoiceMacBytes, 0644)
if err != nil {
os.Remove(invoiceFile)
_ = os.Remove(invoiceFile)
return err
}
// Generate the read-only macaroon and write it to a file.
roMacaroon, err := svc.NewMacaroon(
ctx, macaroons.DefaultRootKeyID, readPermissions...,
)
if err != nil {
return err
}
roBytes, err := roMacaroon.M().MarshalBinary()
roBytes, err := bakeMacaroon(ctx, svc, readPermissions)
if err != nil {
return err
}
if err = ioutil.WriteFile(roFile, roBytes, 0644); err != nil {
os.Remove(admFile)
_ = os.Remove(roFile)
return err
}
// Generate the admin macaroon and write it to a file.
adminPermissions := append(readPermissions, writePermissions...)
admMacaroon, err := svc.NewMacaroon(
ctx, macaroons.DefaultRootKeyID, adminPermissions...,
)
if err != nil {
return err
}
admBytes, err := admMacaroon.M().MarshalBinary()
admBytes, err := bakeMacaroon(ctx, svc, adminPermissions())
if err != nil {
return err
}
if err = ioutil.WriteFile(admFile, admBytes, 0600); err != nil {
_ = os.Remove(admFile)
return err
}
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
// lnd's wallet after it has already been created.
type WalletUnlockParams struct {
@ -1033,6 +1093,15 @@ type WalletUnlockParams struct {
// UnloadWallet is a function for unloading the wallet, which should
// be called on shutdown.
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
@ -1041,40 +1110,49 @@ type WalletUnlockParams struct {
func waitForWalletPassword(cfg *Config, restEndpoints []net.Addr,
serverOpts []grpc.ServerOption, restDialOpts []grpc.DialOption,
restProxyDest string, tlsConf *tls.Config,
getListeners rpcListeners) (*WalletUnlockParams, error) {
// Start a gRPC server listening for HTTP/2 connections, solely used
// for getting the encryption password from the client.
listeners, cleanup, err := getListeners()
if err != nil {
return nil, err
}
defer cleanup()
// Set up a new PasswordService, which will listen for passwords
// provided over RPC.
grpcServer := grpc.NewServer(serverOpts...)
defer grpcServer.GracefulStop()
getListeners rpcListeners) (*WalletUnlockParams, func(), error) {
chainConfig := cfg.Bitcoin
if cfg.registeredChains.PrimaryChain() == chainreg.LitecoinChain {
chainConfig = cfg.Litecoin
}
// The macaroon files are passed to the wallet unlocker since they are
// also encrypted with the wallet's password. These files will be
// deleted within it and recreated when successfully changing the
// wallet's password.
// The macaroonFiles are passed to the wallet unlocker so they can be
// deleted and recreated in case the root macaroon key is also changed
// during the change password operation.
macaroonFiles := []string{
filepath.Join(cfg.networkDir, macaroons.DBFilename),
cfg.AdminMacPath, cfg.ReadMacPath, cfg.InvoiceMacPath,
}
pwService := walletunlocker.New(
chainConfig.ChainDir, cfg.ActiveNetParams.Params, !cfg.SyncFreelist,
macaroonFiles,
chainConfig.ChainDir, cfg.ActiveNetParams.Params,
!cfg.SyncFreelist, macaroonFiles,
)
// Set up a new PasswordService, which will listen for passwords
// provided over RPC.
grpcServer := grpc.NewServer(serverOpts...)
lnrpc.RegisterWalletUnlockerServer(grpcServer, pwService)
var shutdownFuncs []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 {
shutdownFn()
}
}
shutdownFuncs = append(shutdownFuncs, grpcServer.GracefulStop)
// Start a gRPC server listening for HTTP/2 connections, solely used
// for getting the encryption password from the client.
listeners, cleanup, err := getListeners()
if err != nil {
return nil, shutdown, err
}
shutdownFuncs = append(shutdownFuncs, cleanup)
// Use a WaitGroup so we can be sure the instructions on how to input the
// password is the last thing to be printed to the console.
var wg sync.WaitGroup
@ -1082,21 +1160,21 @@ func waitForWalletPassword(cfg *Config, restEndpoints []net.Addr,
for _, lis := range listeners {
wg.Add(1)
go func(lis *ListenerWithSignal) {
rpcsLog.Infof("password RPC server listening on %s",
rpcsLog.Infof("Password RPC server listening on %s",
lis.Addr())
// Close the ready chan to indicate we are listening.
close(lis.Ready)
wg.Done()
grpcServer.Serve(lis)
_ = grpcServer.Serve(lis)
}(lis)
}
// Start a REST proxy for our gRPC server above.
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
shutdownFuncs = append(shutdownFuncs, cancel)
mux := proxy.NewServeMux()
@ -1104,7 +1182,7 @@ func waitForWalletPassword(cfg *Config, restEndpoints []net.Addr,
ctx, mux, restProxyDest, restDialOpts,
)
if err != nil {
return nil, err
return nil, shutdown, err
}
srv := &http.Server{Handler: allowCORS(mux, cfg.RestCORS)}
@ -1112,22 +1190,24 @@ func waitForWalletPassword(cfg *Config, restEndpoints []net.Addr,
for _, restEndpoint := range restEndpoints {
lis, err := lncfg.TLSListenOnAddress(restEndpoint, tlsConf)
if err != nil {
ltndLog.Errorf(
"password gRPC proxy unable to listen on %s",
restEndpoint,
)
return nil, err
ltndLog.Errorf("Password gRPC proxy unable to listen "+
"on %s", restEndpoint)
return nil, shutdown, err
}
defer lis.Close()
shutdownFuncs = append(shutdownFuncs, func() {
err := lis.Close()
if err != nil {
rpcsLog.Errorf("Error closing listener: %v",
err)
}
})
wg.Add(1)
go func() {
rpcsLog.Infof(
"password gRPC proxy started at %s",
lis.Addr(),
)
rpcsLog.Infof("Password gRPC proxy started at %s",
lis.Addr())
wg.Done()
srv.Serve(lis)
_ = srv.Serve(lis)
}()
}
@ -1158,8 +1238,8 @@ func waitForWalletPassword(cfg *Config, restEndpoints []net.Addr,
// version, then we'll return an error as we don't understand
// this.
if cipherSeed.InternalVersion != keychain.KeyDerivationVersion {
return nil, fmt.Errorf("invalid internal seed version "+
"%v, current version is %v",
return nil, shutdown, fmt.Errorf("invalid internal "+
"seed version %v, current version is %v",
cipherSeed.InternalVersion,
keychain.KeyDerivationVersion)
}
@ -1185,31 +1265,35 @@ func waitForWalletPassword(cfg *Config, restEndpoints []net.Addr,
ltndLog.Errorf("Could not unload new "+
"wallet: %v", err)
}
return nil, err
return nil, shutdown, err
}
return &WalletUnlockParams{
Password: password,
Birthday: birthday,
RecoveryWindow: recoveryWindow,
Wallet: newWallet,
ChansToRestore: initMsg.ChanBackups,
UnloadWallet: loader.UnloadWallet,
}, nil
Password: password,
Birthday: birthday,
RecoveryWindow: recoveryWindow,
Wallet: newWallet,
ChansToRestore: initMsg.ChanBackups,
UnloadWallet: loader.UnloadWallet,
StatelessInit: initMsg.StatelessInit,
MacResponseChan: pwService.MacResponseChan,
}, shutdown, nil
// The wallet has already been created in the past, and is simply being
// unlocked. So we'll just return these passphrases.
case unlockMsg := <-pwService.UnlockMsgs:
return &WalletUnlockParams{
Password: unlockMsg.Passphrase,
RecoveryWindow: unlockMsg.RecoveryWindow,
Wallet: unlockMsg.Wallet,
ChansToRestore: unlockMsg.ChanBackups,
UnloadWallet: unlockMsg.UnloadWallet,
}, nil
Password: unlockMsg.Passphrase,
RecoveryWindow: unlockMsg.RecoveryWindow,
Wallet: unlockMsg.Wallet,
ChansToRestore: unlockMsg.ChanBackups,
UnloadWallet: unlockMsg.UnloadWallet,
StatelessInit: unlockMsg.StatelessInit,
MacResponseChan: pwService.MacResponseChan,
}, shutdown, nil
case <-signal.ShutdownChannel():
return nil, fmt.Errorf("shutting down")
return nil, shutdown, fmt.Errorf("shutting down")
}
}

View File

@ -72,16 +72,6 @@ var (
"still in the process of starting")
)
// fileExists reports whether the named file or directory exists.
func fileExists(name string) bool {
if _, err := os.Stat(name); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}
// Server is a sub-server of the main RPC server: the chain notifier RPC. This
// RPC sub-server allows external callers to access the full chain notifier
// capabilities of lnd. This allows callers to create custom protocols, external
@ -111,9 +101,12 @@ func New(cfg *Config) (*Server, lnrpc.MacaroonPerms, error) {
}
// Now that we know the full path of the chain notifier macaroon, we can
// check to see if we need to create it or not.
// check to see if we need to create it or not. If stateless_init is set
// then we don't write the macaroons.
macFilePath := cfg.ChainNotifierMacPath
if cfg.MacService != nil && !fileExists(macFilePath) {
if cfg.MacService != nil && !cfg.MacService.StatelessInit &&
!lnrpc.FileExists(macFilePath) {
log.Infof("Baking macaroons for ChainNotifier RPC Server at: %v",
macFilePath)
@ -121,8 +114,7 @@ func New(cfg *Config) (*Server, lnrpc.MacaroonPerms, error) {
// doesn't yet, exist, so we need to create it with the help of
// the main macaroon service.
chainNotifierMac, err := cfg.MacService.NewMacaroon(
context.Background(),
macaroons.DefaultRootKeyID,
context.Background(), macaroons.DefaultRootKeyID,
macaroonOps...,
)
if err != nil {
@ -134,7 +126,7 @@ func New(cfg *Config) (*Server, lnrpc.MacaroonPerms, error) {
}
err = ioutil.WriteFile(macFilePath, chainNotifierMacBytes, 0644)
if err != nil {
os.Remove(macFilePath)
_ = os.Remove(macFilePath)
return nil, nil, err
}
}

View File

@ -92,8 +92,11 @@ func New(cfg *Config) (*Server, lnrpc.MacaroonPerms, error) {
)
// Now that we know the full path of the invoices macaroon, we can
// check to see if we need to create it or not.
if !lnrpc.FileExists(macFilePath) && cfg.MacService != nil {
// check to see if we need to create it or not. If stateless_init is set
// then we don't write the macaroons.
if cfg.MacService != nil && !cfg.MacService.StatelessInit &&
!lnrpc.FileExists(macFilePath) {
log.Infof("Baking macaroons for invoices RPC Server at: %v",
macFilePath)
@ -113,7 +116,7 @@ func New(cfg *Config) (*Server, lnrpc.MacaroonPerms, error) {
}
err = ioutil.WriteFile(macFilePath, invoicesMacBytes, 0644)
if err != nil {
os.Remove(macFilePath)
_ = os.Remove(macFilePath)
return nil, nil, err
}
}

View File

@ -131,16 +131,6 @@ type Server struct {
// gRPC service.
var _ RouterServer = (*Server)(nil)
// fileExists reports whether the named file or directory exists.
func fileExists(name string) bool {
if _, err := os.Stat(name); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}
// New creates a new instance of the RouterServer given a configuration struct
// that contains all external dependencies. If the target macaroon exists, and
// we're unable to create it, then an error will be returned. We also return
@ -156,9 +146,12 @@ func New(cfg *Config) (*Server, lnrpc.MacaroonPerms, error) {
}
// Now that we know the full path of the router macaroon, we can check
// to see if we need to create it or not.
// to see if we need to create it or not. If stateless_init is set
// then we don't write the macaroons.
macFilePath := cfg.RouterMacPath
if !fileExists(macFilePath) && cfg.MacService != nil {
if cfg.MacService != nil && !cfg.MacService.StatelessInit &&
!lnrpc.FileExists(macFilePath) {
log.Infof("Making macaroons for Router RPC Server at: %v",
macFilePath)
@ -178,7 +171,7 @@ func New(cfg *Config) (*Server, lnrpc.MacaroonPerms, error) {
}
err = ioutil.WriteFile(macFilePath, routerMacBytes, 0644)
if err != nil {
os.Remove(macFilePath)
_ = os.Remove(macFilePath)
return nil, nil, err
}
}

View File

@ -103,9 +103,12 @@ func New(cfg *Config) (*Server, lnrpc.MacaroonPerms, error) {
}
// Now that we know the full path of the signer macaroon, we can check
// to see if we need to create it or not.
// to see if we need to create it or not. If stateless_init is set
// then we don't write the macaroons.
macFilePath := cfg.SignerMacPath
if cfg.MacService != nil && !lnrpc.FileExists(macFilePath) {
if cfg.MacService != nil && !cfg.MacService.StatelessInit &&
!lnrpc.FileExists(macFilePath) {
log.Infof("Making macaroons for Signer RPC Server at: %v",
macFilePath)
@ -125,7 +128,7 @@ func New(cfg *Config) (*Server, lnrpc.MacaroonPerms, error) {
}
err = ioutil.WriteFile(macFilePath, signerMacBytes, 0644)
if err != nil {
os.Remove(macFilePath)
_ = os.Remove(macFilePath)
return nil, nil, err
}
}

View File

@ -170,9 +170,12 @@ func New(cfg *Config) (*WalletKit, lnrpc.MacaroonPerms, error) {
}
// Now that we know the full path of the wallet kit macaroon, we can
// check to see if we need to create it or not.
// check to see if we need to create it or not. If stateless_init is set
// then we don't write the macaroons.
macFilePath := cfg.WalletKitMacPath
if !lnrpc.FileExists(macFilePath) && cfg.MacService != nil {
if cfg.MacService != nil && !cfg.MacService.StatelessInit &&
!lnrpc.FileExists(macFilePath) {
log.Infof("Baking macaroons for WalletKit RPC Server at: %v",
macFilePath)
@ -180,8 +183,7 @@ func New(cfg *Config) (*WalletKit, lnrpc.MacaroonPerms, error) {
// yet, exist, so we need to create it with the help of the
// main macaroon service.
walletKitMac, err := cfg.MacService.NewMacaroon(
context.Background(),
macaroons.DefaultRootKeyID,
context.Background(), macaroons.DefaultRootKeyID,
macaroonOps...,
)
if err != nil {
@ -193,7 +195,7 @@ func New(cfg *Config) (*WalletKit, lnrpc.MacaroonPerms, error) {
}
err = ioutil.WriteFile(macFilePath, walletKitMacBytes, 0644)
if err != nil {
os.Remove(macFilePath)
_ = os.Remove(macFilePath)
return nil, nil, err
}
}

View File

@ -166,10 +166,16 @@ type InitWalletRequest struct {
//total data loss occurred. If specified, then after on-chain recovery of
//funds, lnd begin to carry out the data loss recovery protocol in order to
//recover the funds in each channel from a remote force closed transaction.
ChannelBackups *ChanBackupSnapshot `protobuf:"bytes,5,opt,name=channel_backups,json=channelBackups,proto3" json:"channel_backups,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
ChannelBackups *ChanBackupSnapshot `protobuf:"bytes,5,opt,name=channel_backups,json=channelBackups,proto3" json:"channel_backups,omitempty"`
//
//stateless_init is an optional argument instructing the daemon NOT to create
//any *.macaroon files in its filesystem. If this parameter is set, then the
//admin macaroon returned in the response MUST be stored by the caller of the
//RPC as otherwise all access to the daemon will be lost!
StatelessInit bool `protobuf:"varint,6,opt,name=stateless_init,json=statelessInit,proto3" json:"stateless_init,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *InitWalletRequest) Reset() { *m = InitWalletRequest{} }
@ -232,7 +238,21 @@ func (m *InitWalletRequest) GetChannelBackups() *ChanBackupSnapshot {
return nil
}
func (m *InitWalletRequest) GetStatelessInit() bool {
if m != nil {
return m.StatelessInit
}
return false
}
type InitWalletResponse struct {
//
//The binary serialized admin macaroon that can be used to access the daemon
//after creating the wallet. If the stateless_init parameter was set to true,
//this is the ONLY copy of the macaroon and MUST be stored safely by the
//caller. Otherwise a copy of this macaroon is also persisted on disk by the
//daemon, together with other macaroon files.
AdminMacaroon []byte `protobuf:"bytes,1,opt,name=admin_macaroon,json=adminMacaroon,proto3" json:"admin_macaroon,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
@ -263,6 +283,13 @@ func (m *InitWalletResponse) XXX_DiscardUnknown() {
var xxx_messageInfo_InitWalletResponse proto.InternalMessageInfo
func (m *InitWalletResponse) GetAdminMacaroon() []byte {
if m != nil {
return m.AdminMacaroon
}
return nil
}
type UnlockWalletRequest struct {
//
//wallet_password should be the current valid passphrase for the daemon. This
@ -283,10 +310,14 @@ type UnlockWalletRequest struct {
//total data loss occurred. If specified, then after on-chain recovery of
//funds, lnd begin to carry out the data loss recovery protocol in order to
//recover the funds in each channel from a remote force closed transaction.
ChannelBackups *ChanBackupSnapshot `protobuf:"bytes,3,opt,name=channel_backups,json=channelBackups,proto3" json:"channel_backups,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
ChannelBackups *ChanBackupSnapshot `protobuf:"bytes,3,opt,name=channel_backups,json=channelBackups,proto3" json:"channel_backups,omitempty"`
//
//stateless_init is an optional argument instructing the daemon NOT to create
//any *.macaroon files in its file system.
StatelessInit bool `protobuf:"varint,4,opt,name=stateless_init,json=statelessInit,proto3" json:"stateless_init,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *UnlockWalletRequest) Reset() { *m = UnlockWalletRequest{} }
@ -335,6 +366,13 @@ func (m *UnlockWalletRequest) GetChannelBackups() *ChanBackupSnapshot {
return nil
}
func (m *UnlockWalletRequest) GetStatelessInit() bool {
if m != nil {
return m.StatelessInit
}
return false
}
type UnlockWalletResponse struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
@ -374,7 +412,18 @@ type ChangePasswordRequest struct {
//
//new_password should be the new passphrase that will be needed to unlock the
//daemon. When using REST, this field must be encoded as base64.
NewPassword []byte `protobuf:"bytes,2,opt,name=new_password,json=newPassword,proto3" json:"new_password,omitempty"`
NewPassword []byte `protobuf:"bytes,2,opt,name=new_password,json=newPassword,proto3" json:"new_password,omitempty"`
//
//stateless_init is an optional argument instructing the daemon NOT to create
//any *.macaroon files in its filesystem. If this parameter is set, then the
//admin macaroon returned in the response MUST be stored by the caller of the
//RPC as otherwise all access to the daemon will be lost!
StatelessInit bool `protobuf:"varint,3,opt,name=stateless_init,json=statelessInit,proto3" json:"stateless_init,omitempty"`
//
//new_macaroon_root_key is an optional argument instructing the daemon to
//rotate the macaroon root key when set to true. This will invalidate all
//previously generated macaroons.
NewMacaroonRootKey bool `protobuf:"varint,4,opt,name=new_macaroon_root_key,json=newMacaroonRootKey,proto3" json:"new_macaroon_root_key,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
@ -419,7 +468,29 @@ func (m *ChangePasswordRequest) GetNewPassword() []byte {
return nil
}
func (m *ChangePasswordRequest) GetStatelessInit() bool {
if m != nil {
return m.StatelessInit
}
return false
}
func (m *ChangePasswordRequest) GetNewMacaroonRootKey() bool {
if m != nil {
return m.NewMacaroonRootKey
}
return false
}
type ChangePasswordResponse struct {
//
//The binary serialized admin macaroon that can be used to access the daemon
//after rotating the macaroon root key. If both the stateless_init and
//new_macaroon_root_key parameter were set to true, this is the ONLY copy of
//the macaroon that was created from the new root key and MUST be stored
//safely by the caller. Otherwise a copy of this macaroon is also persisted on
//disk by the daemon, together with other macaroon files.
AdminMacaroon []byte `protobuf:"bytes,1,opt,name=admin_macaroon,json=adminMacaroon,proto3" json:"admin_macaroon,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
@ -450,6 +521,13 @@ func (m *ChangePasswordResponse) XXX_DiscardUnknown() {
var xxx_messageInfo_ChangePasswordResponse proto.InternalMessageInfo
func (m *ChangePasswordResponse) GetAdminMacaroon() []byte {
if m != nil {
return m.AdminMacaroon
}
return nil
}
func init() {
proto.RegisterType((*GenSeedRequest)(nil), "lnrpc.GenSeedRequest")
proto.RegisterType((*GenSeedResponse)(nil), "lnrpc.GenSeedResponse")
@ -464,39 +542,45 @@ func init() {
func init() { proto.RegisterFile("walletunlocker.proto", fileDescriptor_76e3ed10ed53e4fd) }
var fileDescriptor_76e3ed10ed53e4fd = []byte{
// 510 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x54, 0x4f, 0x6b, 0xdb, 0x4e,
0x10, 0x45, 0xf6, 0xcf, 0xbf, 0x92, 0x89, 0x91, 0x92, 0xad, 0x63, 0x14, 0xb5, 0x05, 0x47, 0x50,
0xec, 0x52, 0xb0, 0x4b, 0x7a, 0xe9, 0xb5, 0x2e, 0x25, 0xf4, 0x10, 0x08, 0x0e, 0x21, 0xd0, 0x8b,
0x2b, 0x4b, 0x83, 0x25, 0x2c, 0xcf, 0x6e, 0x77, 0xe5, 0x8a, 0xf4, 0x13, 0xf4, 0x8b, 0xf4, 0xd4,
0x2f, 0x59, 0xbc, 0xbb, 0xfe, 0x17, 0xcb, 0xd0, 0xf6, 0xfa, 0xde, 0xbc, 0xdd, 0x79, 0x6f, 0x66,
0x17, 0x5a, 0x65, 0x94, 0xe7, 0x58, 0x2c, 0x28, 0xe7, 0xf1, 0x0c, 0x65, 0x5f, 0x48, 0x5e, 0x70,
0xd6, 0xc8, 0x49, 0x8a, 0x38, 0x38, 0x92, 0x22, 0x36, 0x48, 0xf8, 0x05, 0xdc, 0x2b, 0xa4, 0x5b,
0xc4, 0x64, 0x84, 0x5f, 0x17, 0xa8, 0x0a, 0xf6, 0x1a, 0x4e, 0x23, 0xfc, 0x8e, 0x98, 0x8c, 0x45,
0xa4, 0x94, 0x48, 0x65, 0xa4, 0xd0, 0x77, 0x3a, 0x4e, 0xaf, 0x39, 0x3a, 0x31, 0xc4, 0xcd, 0x1a,
0x67, 0x17, 0xd0, 0x54, 0xcb, 0x52, 0xa4, 0x42, 0x72, 0xf1, 0xe0, 0xd7, 0x74, 0xdd, 0xf1, 0x12,
0xfb, 0x68, 0xa0, 0x30, 0x07, 0x6f, 0x7d, 0x83, 0x12, 0x9c, 0x14, 0xb2, 0x37, 0xd0, 0x8a, 0x33,
0x91, 0xa2, 0x1c, 0x6b, 0xf1, 0x9c, 0x70, 0xce, 0x29, 0x8b, 0x7d, 0xa7, 0x53, 0xef, 0x1d, 0x8d,
0x98, 0xe1, 0x96, 0x8a, 0x6b, 0xcb, 0xb0, 0x2e, 0x78, 0x48, 0x06, 0xc7, 0x44, 0xab, 0xec, 0x55,
0xee, 0x06, 0x5e, 0x0a, 0xc2, 0x1f, 0x35, 0x38, 0xfd, 0x44, 0x59, 0x71, 0xaf, 0xed, 0xaf, 0x3c,
0x75, 0xc1, 0x33, 0x79, 0x68, 0x4f, 0x25, 0x97, 0x89, 0x75, 0xe4, 0x1a, 0xf8, 0xc6, 0xa2, 0x07,
0x3b, 0xab, 0x1d, 0xec, 0xac, 0x32, 0xae, 0xfa, 0x81, 0xb8, 0xba, 0xe0, 0x49, 0x8c, 0xf9, 0x37,
0x94, 0x0f, 0xe3, 0x32, 0xa3, 0x84, 0x97, 0xfe, 0x7f, 0x1d, 0xa7, 0xd7, 0x18, 0xb9, 0x2b, 0xf8,
0x5e, 0xa3, 0x6c, 0x08, 0x5e, 0x9c, 0x46, 0x44, 0x98, 0x8f, 0x27, 0x51, 0x3c, 0x5b, 0x08, 0xe5,
0x37, 0x3a, 0x4e, 0xef, 0xf8, 0xf2, 0xbc, 0xaf, 0x47, 0xd8, 0xff, 0x90, 0x46, 0x34, 0xd4, 0xcc,
0x2d, 0x45, 0x42, 0xa5, 0xbc, 0x18, 0xb9, 0x56, 0x61, 0x60, 0x15, 0xb6, 0x80, 0x6d, 0x27, 0x61,
0xb2, 0x0f, 0x7f, 0x39, 0xf0, 0xf4, 0x4e, 0x6f, 0xc5, 0x3f, 0x46, 0x54, 0xe1, 0xa1, 0xf6, 0xa7,
0x1e, 0xea, 0x7f, 0xeb, 0xa1, 0x0d, 0xad, 0xdd, 0x66, 0xad, 0x0b, 0x84, 0xb3, 0xa5, 0x7a, 0x8a,
0xab, 0xb6, 0x56, 0x36, 0x5e, 0xc1, 0x49, 0xbc, 0x90, 0x12, 0x69, 0xcf, 0x87, 0x67, 0xf1, 0xb5,
0x91, 0x0b, 0x68, 0x12, 0x96, 0x9b, 0x32, 0xbb, 0xbb, 0x84, 0xe5, 0xaa, 0x24, 0xf4, 0xa1, 0xfd,
0xf8, 0x1a, 0xd3, 0xc0, 0xe5, 0xcf, 0x1a, 0xb8, 0xa6, 0xa7, 0x3b, 0xfb, 0xc4, 0xd8, 0x3b, 0x78,
0x62, 0x17, 0x9d, 0x9d, 0x59, 0x87, 0xbb, 0x4f, 0x2b, 0x68, 0x3f, 0x86, 0xed, 0x7b, 0x78, 0x0f,
0xb0, 0x99, 0x14, 0xf3, 0x6d, 0xd5, 0xde, 0x1a, 0x07, 0xe7, 0x15, 0x8c, 0x3d, 0xe2, 0x0a, 0x9a,
0xdb, 0x41, 0xb1, 0xc0, 0x96, 0x56, 0x8c, 0x3a, 0x78, 0x56, 0xc9, 0xd9, 0x83, 0xae, 0xc1, 0xdd,
0xb5, 0xcc, 0x9e, 0x6f, 0x8d, 0x6b, 0x2f, 0xf0, 0xe0, 0xc5, 0x01, 0xd6, 0x1c, 0x37, 0xec, 0x7e,
0x7e, 0x39, 0xcd, 0x8a, 0x74, 0x31, 0xe9, 0xc7, 0x7c, 0x3e, 0xc8, 0xb3, 0x69, 0x5a, 0x50, 0x46,
0x53, 0xc2, 0xa2, 0xe4, 0x72, 0x36, 0xc8, 0x29, 0x19, 0x68, 0xfd, 0xe4, 0x7f, 0xfd, 0x1f, 0xbd,
0xfd, 0x1d, 0x00, 0x00, 0xff, 0xff, 0x94, 0x54, 0xe3, 0x28, 0xb9, 0x04, 0x00, 0x00,
// 599 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x94, 0xdd, 0x6a, 0xdb, 0x4c,
0x10, 0x86, 0x91, 0x9d, 0xe4, 0xfb, 0x32, 0x71, 0xe4, 0x64, 0x9b, 0x04, 0xc5, 0x6d, 0xc1, 0x11,
0x04, 0xbb, 0x14, 0x9c, 0x36, 0x3d, 0x29, 0xf4, 0xa0, 0x34, 0xa5, 0x84, 0x52, 0x02, 0x41, 0x21,
0x04, 0x7a, 0xa2, 0x6e, 0xa4, 0xc1, 0x12, 0x96, 0x67, 0xd5, 0xdd, 0x75, 0x85, 0x7b, 0x3f, 0x3d,
0xee, 0x25, 0xf4, 0x1e, 0x7a, 0x45, 0x45, 0xab, 0xb5, 0xf3, 0x63, 0x19, 0xfa, 0x73, 0xfa, 0xcc,
0xcc, 0xee, 0xbc, 0xef, 0xcc, 0x2e, 0xec, 0x14, 0x3c, 0xcb, 0x50, 0x4f, 0x28, 0x13, 0xd1, 0x08,
0xe5, 0x20, 0x97, 0x42, 0x0b, 0xb6, 0x9a, 0x91, 0xcc, 0xa3, 0xce, 0xba, 0xcc, 0xa3, 0x8a, 0xf8,
0x9f, 0xc0, 0x3d, 0x45, 0xba, 0x40, 0x8c, 0x03, 0xfc, 0x3c, 0x41, 0xa5, 0xd9, 0x53, 0xd8, 0xe6,
0xf8, 0x15, 0x31, 0x0e, 0x73, 0xae, 0x54, 0x9e, 0x48, 0xae, 0xd0, 0x73, 0xba, 0x4e, 0xbf, 0x15,
0x6c, 0x55, 0x81, 0xf3, 0x39, 0x67, 0x07, 0xd0, 0x52, 0x65, 0x2a, 0x92, 0x96, 0x22, 0x9f, 0x7a,
0x0d, 0x93, 0xb7, 0x51, 0xb2, 0x77, 0x15, 0xf2, 0x33, 0x68, 0xcf, 0x6f, 0x50, 0xb9, 0x20, 0x85,
0xec, 0x19, 0xec, 0x44, 0x69, 0x9e, 0xa0, 0x0c, 0x4d, 0xf1, 0x98, 0x70, 0x2c, 0x28, 0x8d, 0x3c,
0xa7, 0xdb, 0xec, 0xaf, 0x07, 0xac, 0x8a, 0x95, 0x15, 0x67, 0x36, 0xc2, 0x7a, 0xd0, 0x46, 0xaa,
0x38, 0xc6, 0xa6, 0xca, 0x5e, 0xe5, 0xde, 0xe0, 0xb2, 0xc0, 0xff, 0xde, 0x80, 0xed, 0xf7, 0x94,
0xea, 0x2b, 0x23, 0x7f, 0xa6, 0xa9, 0x07, 0xed, 0xca, 0x0f, 0xa3, 0xa9, 0x10, 0x32, 0xb6, 0x8a,
0xdc, 0x0a, 0x9f, 0x5b, 0xba, 0xb4, 0xb3, 0xc6, 0xd2, 0xce, 0x6a, 0xed, 0x6a, 0x2e, 0xb1, 0xab,
0x07, 0x6d, 0x89, 0x91, 0xf8, 0x82, 0x72, 0x1a, 0x16, 0x29, 0xc5, 0xa2, 0xf0, 0x56, 0xba, 0x4e,
0x7f, 0x35, 0x70, 0x67, 0xf8, 0xca, 0x50, 0x76, 0x02, 0xed, 0x28, 0xe1, 0x44, 0x98, 0x85, 0xd7,
0x3c, 0x1a, 0x4d, 0x72, 0xe5, 0xad, 0x76, 0x9d, 0xfe, 0xc6, 0xf1, 0xfe, 0xc0, 0x8c, 0x70, 0xf0,
0x36, 0xe1, 0x74, 0x62, 0x22, 0x17, 0xc4, 0x73, 0x95, 0x08, 0x1d, 0xb8, 0xb6, 0xa2, 0xc2, 0x8a,
0x1d, 0x82, 0xab, 0x34, 0xd7, 0x98, 0xa1, 0x52, 0x61, 0x4a, 0xa9, 0xf6, 0xd6, 0xba, 0x4e, 0xff,
0xff, 0x60, 0x73, 0x4e, 0x4b, 0xa3, 0xfc, 0x57, 0xc0, 0x6e, 0x1b, 0x66, 0x47, 0x74, 0x08, 0x2e,
0x8f, 0xc7, 0x29, 0x85, 0x63, 0x1e, 0x71, 0x29, 0x04, 0x59, 0xc3, 0x36, 0x0d, 0x3d, 0xb3, 0xd0,
0xff, 0xe9, 0xc0, 0x83, 0x4b, 0xb3, 0x63, 0x7f, 0x69, 0x78, 0x8d, 0x23, 0x8d, 0xdf, 0x75, 0xa4,
0xf9, 0xef, 0x8e, 0xac, 0xd4, 0x39, 0xb2, 0x07, 0x3b, 0x77, 0x35, 0x55, 0x9e, 0xf8, 0x3f, 0x1c,
0xd8, 0x2d, 0x6f, 0x19, 0xe2, 0xac, 0xfd, 0x99, 0xdc, 0x27, 0xb0, 0x15, 0x4d, 0xa4, 0x44, 0x5a,
0xd0, 0xdb, 0xb6, 0x7c, 0x2e, 0xf8, 0x00, 0x5a, 0x84, 0xc5, 0x4d, 0x9a, 0x7d, 0x31, 0x84, 0xc5,
0x3c, 0x65, 0xb1, 0xcd, 0x66, 0x4d, 0x9b, 0xec, 0x39, 0xec, 0x96, 0x27, 0xcd, 0x06, 0x14, 0x4a,
0x21, 0x74, 0x38, 0xc2, 0xa9, 0x15, 0xc5, 0x08, 0x8b, 0xd9, 0x9c, 0x02, 0x21, 0xf4, 0x07, 0x9c,
0xfa, 0xaf, 0x61, 0xef, 0xbe, 0x80, 0x3f, 0x9a, 0xf7, 0xf1, 0xb7, 0x06, 0xb8, 0x95, 0x2b, 0x97,
0xf6, 0x67, 0x61, 0x2f, 0xe1, 0x3f, 0xfb, 0xbe, 0xd9, 0xae, 0x1d, 0xc5, 0xdd, 0x1f, 0xa5, 0xb3,
0x77, 0x1f, 0xdb, 0x3b, 0xdf, 0x00, 0xdc, 0x6c, 0x1e, 0xf3, 0x6c, 0xd6, 0xc2, 0xeb, 0xed, 0xec,
0xd7, 0x44, 0xec, 0x11, 0xa7, 0xd0, 0xba, 0x3d, 0x2a, 0xd6, 0xb1, 0xa9, 0x35, 0x3b, 0xd9, 0x79,
0x58, 0x1b, 0xb3, 0x07, 0x9d, 0x81, 0x7b, 0xd7, 0x19, 0xf6, 0xe8, 0xd6, 0x5e, 0x2d, 0x4c, 0xbc,
0xf3, 0x78, 0x49, 0xb4, 0x3a, 0xee, 0xa4, 0xf7, 0xf1, 0x70, 0x98, 0xea, 0x64, 0x72, 0x3d, 0x88,
0xc4, 0xf8, 0x28, 0x4b, 0x87, 0x89, 0xa6, 0x94, 0x86, 0x84, 0xba, 0x10, 0x72, 0x74, 0x94, 0x51,
0x7c, 0x64, 0xea, 0xaf, 0xd7, 0xcc, 0x37, 0xfc, 0xe2, 0x57, 0x00, 0x00, 0x00, 0xff, 0xff, 0xf8,
0x7a, 0x3b, 0x08, 0xb0, 0x05, 0x00, 0x00,
}
// Reference imports to suppress errors if they are not otherwise used.

View File

@ -141,8 +141,24 @@ message InitWalletRequest {
recover the funds in each channel from a remote force closed transaction.
*/
ChanBackupSnapshot channel_backups = 5;
/*
stateless_init is an optional argument instructing the daemon NOT to create
any *.macaroon files in its filesystem. If this parameter is set, then the
admin macaroon returned in the response MUST be stored by the caller of the
RPC as otherwise all access to the daemon will be lost!
*/
bool stateless_init = 6;
}
message InitWalletResponse {
/*
The binary serialized admin macaroon that can be used to access the daemon
after creating the wallet. If the stateless_init parameter was set to true,
this is the ONLY copy of the macaroon and MUST be stored safely by the
caller. Otherwise a copy of this macaroon is also persisted on disk by the
daemon, together with other macaroon files.
*/
bytes admin_macaroon = 1;
}
message UnlockWalletRequest {
@ -171,6 +187,12 @@ message UnlockWalletRequest {
recover the funds in each channel from a remote force closed transaction.
*/
ChanBackupSnapshot channel_backups = 3;
/*
stateless_init is an optional argument instructing the daemon NOT to create
any *.macaroon files in its file system.
*/
bool stateless_init = 4;
}
message UnlockWalletResponse {
}
@ -187,6 +209,30 @@ message ChangePasswordRequest {
daemon. When using REST, this field must be encoded as base64.
*/
bytes new_password = 2;
/*
stateless_init is an optional argument instructing the daemon NOT to create
any *.macaroon files in its filesystem. If this parameter is set, then the
admin macaroon returned in the response MUST be stored by the caller of the
RPC as otherwise all access to the daemon will be lost!
*/
bool stateless_init = 3;
/*
new_macaroon_root_key is an optional argument instructing the daemon to
rotate the macaroon root key when set to true. This will invalidate all
previously generated macaroons.
*/
bool new_macaroon_root_key = 4;
}
message ChangePasswordResponse {
}
/*
The binary serialized admin macaroon that can be used to access the daemon
after rotating the macaroon root key. If both the stateless_init and
new_macaroon_root_key parameter were set to true, this is the ONLY copy of
the macaroon that was created from the new root key and MUST be stored
safely by the caller. Otherwise a copy of this macaroon is also persisted on
disk by the daemon, together with other macaroon files.
*/
bytes admin_macaroon = 1;
}

View File

@ -180,11 +180,28 @@
"type": "string",
"format": "byte",
"description": "new_password should be the new passphrase that will be needed to unlock the\ndaemon. When using REST, this field must be encoded as base64."
},
"stateless_init": {
"type": "boolean",
"format": "boolean",
"title": "stateless_init is an optional argument instructing the daemon NOT to create\nany *.macaroon files in its filesystem. If this parameter is set, then the\nadmin macaroon returned in the response MUST be stored by the caller of the\nRPC as otherwise all access to the daemon will be lost!"
},
"new_macaroon_root_key": {
"type": "boolean",
"format": "boolean",
"description": "new_macaroon_root_key is an optional argument instructing the daemon to\nrotate the macaroon root key when set to true. This will invalidate all\npreviously generated macaroons."
}
}
},
"lnrpcChangePasswordResponse": {
"type": "object"
"type": "object",
"properties": {
"admin_macaroon": {
"type": "string",
"format": "byte",
"description": "The binary serialized admin macaroon that can be used to access the daemon\nafter rotating the macaroon root key. If both the stateless_init and\nnew_macaroon_root_key parameter were set to true, this is the ONLY copy of\nthe macaroon that was created from the new root key and MUST be stored\nsafely by the caller. Otherwise a copy of this macaroon is also persisted on\ndisk by the daemon, together with other macaroon files."
}
}
},
"lnrpcChannelBackup": {
"type": "object",
@ -276,11 +293,23 @@
"channel_backups": {
"$ref": "#/definitions/lnrpcChanBackupSnapshot",
"description": "channel_backups is an optional argument that allows clients to recover the\nsettled funds within a set of channels. This should be populated if the\nuser was unable to close out all channels and sweep funds before partial or\ntotal data loss occurred. If specified, then after on-chain recovery of\nfunds, lnd begin to carry out the data loss recovery protocol in order to\nrecover the funds in each channel from a remote force closed transaction."
},
"stateless_init": {
"type": "boolean",
"format": "boolean",
"title": "stateless_init is an optional argument instructing the daemon NOT to create\nany *.macaroon files in its filesystem. If this parameter is set, then the\nadmin macaroon returned in the response MUST be stored by the caller of the\nRPC as otherwise all access to the daemon will be lost!"
}
}
},
"lnrpcInitWalletResponse": {
"type": "object"
"type": "object",
"properties": {
"admin_macaroon": {
"type": "string",
"format": "byte",
"description": "The binary serialized admin macaroon that can be used to access the daemon\nafter creating the wallet. If the stateless_init parameter was set to true,\nthis is the ONLY copy of the macaroon and MUST be stored safely by the\ncaller. Otherwise a copy of this macaroon is also persisted on disk by the\ndaemon, together with other macaroon files."
}
}
},
"lnrpcMultiChanBackup": {
"type": "object",
@ -315,6 +344,11 @@
"channel_backups": {
"$ref": "#/definitions/lnrpcChanBackupSnapshot",
"description": "channel_backups is an optional argument that allows clients to recover the\nsettled funds within a set of channels. This should be populated if the\nuser was unable to close out all channels and sweep funds before partial or\ntotal data loss occurred. If specified, then after on-chain recovery of\nfunds, lnd begin to carry out the data loss recovery protocol in order to\nrecover the funds in each channel from a remote force closed transaction."
},
"stateless_init": {
"type": "boolean",
"format": "boolean",
"description": "stateless_init is an optional argument instructing the daemon NOT to create\nany *.macaroon files in its file system."
}
}
},

View File

@ -270,11 +270,12 @@ func (n *NetworkHarness) NewNode(name string, extraArgs []string) (*HarnessNode,
// wallet password. The generated mnemonic is returned along with the
// initialized harness node.
func (n *NetworkHarness) NewNodeWithSeed(name string, extraArgs []string,
password []byte) (*HarnessNode, []string, error) {
password []byte, statelessInit bool) (*HarnessNode, []string, []byte,
error) {
node, err := n.newNode(name, extraArgs, true, password)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
timeout := time.Duration(time.Second * 15)
@ -289,7 +290,7 @@ func (n *NetworkHarness) NewNodeWithSeed(name string, extraArgs []string,
ctxt, _ := context.WithTimeout(ctxb, timeout)
genSeedResp, err := node.GenSeed(ctxt, genSeedReq)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
// With the seed created, construct the init request to the node,
@ -298,20 +299,25 @@ func (n *NetworkHarness) NewNodeWithSeed(name string, extraArgs []string,
WalletPassword: password,
CipherSeedMnemonic: genSeedResp.CipherSeedMnemonic,
AezeedPassphrase: password,
StatelessInit: statelessInit,
}
// Pass the init request via rpc to finish unlocking the node. This will
// also initialize the macaroon-authenticated LightningClient.
err = node.Init(ctxb, initReq)
response, err := node.Init(ctxb, initReq)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
// With the node started, we can now record its public key within the
// global mapping.
n.RegisterNode(node)
return node, genSeedResp.CipherSeedMnemonic, nil
// In stateless initialization mode we get a macaroon back that we have
// to return to the test, otherwise gRPC calls won't be possible since
// there are no macaroon files created in that mode.
// In stateful init the admin macaroon will just be nil.
return node, genSeedResp.CipherSeedMnemonic, response.AdminMacaroon, nil
}
// RestoreNodeWithSeed fully initializes a HarnessNode using a chosen mnemonic,
@ -336,7 +342,7 @@ func (n *NetworkHarness) RestoreNodeWithSeed(name string, extraArgs []string,
ChannelBackups: chanBackups,
}
err = node.Init(context.Background(), initReq)
_, err = node.Init(context.Background(), initReq)
if err != nil {
return nil, err
}
@ -616,17 +622,8 @@ func (n *NetworkHarness) DisconnectNodes(ctx context.Context, a, b *HarnessNode)
func (n *NetworkHarness) RestartNode(node *HarnessNode, callback func() error,
chanBackups ...*lnrpc.ChanBackupSnapshot) error {
if err := node.stop(); err != nil {
return err
}
if callback != nil {
if err := callback(); err != nil {
return err
}
}
if err := node.start(n.lndBinary, n.lndErrorChan); err != nil {
err := n.RestartNodeNoUnlock(node, callback)
if err != nil {
return err
}
@ -649,6 +646,27 @@ func (n *NetworkHarness) RestartNode(node *HarnessNode, callback func() error,
return node.Unlock(context.Background(), unlockReq)
}
// RestartNodeNoUnlock attempts to restart a lightning node by shutting it down
// cleanly, then restarting the process. In case the node was setup with a seed,
// it will be left in the unlocked state. This function is fully blocking. If
// the callback parameter is non-nil, then the function will be executed after
// the node shuts down, but *before* the process has been started up again.
func (n *NetworkHarness) RestartNodeNoUnlock(node *HarnessNode,
callback func() error) error {
if err := node.stop(); err != nil {
return err
}
if callback != nil {
if err := callback(); err != nil {
return err
}
}
return node.start(n.lndBinary, n.lndErrorChan)
}
// SuspendNode stops the given node and returns a callback that can be used to
// start it again.
func (n *NetworkHarness) SuspendNode(node *HarnessNode) (func() error, error) {

View File

@ -797,8 +797,8 @@ func testChanRestoreScenario(t *harnessTest, net *lntest.NetworkHarness,
// First, we'll create a brand new node we'll use within the test. If
// we have a custom backup file specified, then we'll also create that
// for use.
dave, mnemonic, err := net.NewNodeWithSeed(
"dave", nodeArgs, password,
dave, mnemonic, _, err := net.NewNodeWithSeed(
"dave", nodeArgs, password, false,
)
if err != nil {
t.Fatalf("unable to create new node: %v", err)

View File

@ -1,10 +1,13 @@
package itest
import (
"bytes"
"context"
"encoding/hex"
"os"
"sort"
"strconv"
"strings"
"testing"
"github.com/lightningnetwork/lnd/lnrpc"
@ -486,6 +489,105 @@ func testDeleteMacaroonID(net *lntest.NetworkHarness, t *harnessTest) {
require.Contains(t.t, err.Error(), "cannot get macaroon")
}
// testStatelessInit checks that the stateless initialization of the daemon
// does not write any macaroon files to the daemon's file system and returns
// the admin macaroon in the response. It then checks that the password
// change of the wallet can also happen stateless.
func testStatelessInit(net *lntest.NetworkHarness, t *harnessTest) {
var (
initPw = []byte("stateless")
newPw = []byte("stateless-new")
newAddrReq = &lnrpc.NewAddressRequest{
Type: AddrTypeWitnessPubkeyHash,
}
)
// First, create a new node and request it to initialize stateless.
// This should return us the binary serialized admin macaroon that we
// can then use for further calls.
carol, _, macBytes, err := net.NewNodeWithSeed(
"Carol", nil, initPw, true,
)
require.NoError(t.t, err)
if len(macBytes) == 0 {
t.Fatalf("invalid macaroon returned in stateless init")
}
// Now make sure no macaroon files have been created by the node Carol.
_, err = os.Stat(carol.AdminMacPath())
require.Error(t.t, err)
_, err = os.Stat(carol.ReadMacPath())
require.Error(t.t, err)
_, err = os.Stat(carol.InvoiceMacPath())
require.Error(t.t, err)
// Then check that we can unmarshal the binary serialized macaroon.
adminMac := &macaroon.Macaroon{}
err = adminMac.UnmarshalBinary(macBytes)
require.NoError(t.t, err)
// Find out if we can actually use the macaroon that has been returned
// to us for a RPC call.
conn, err := carol.ConnectRPCWithMacaroon(adminMac)
require.NoError(t.t, err)
defer conn.Close()
adminMacClient := lnrpc.NewLightningClient(conn)
ctxt, _ := context.WithTimeout(context.Background(), defaultTimeout)
res, err := adminMacClient.NewAddress(ctxt, newAddrReq)
require.NoError(t.t, err)
if !strings.HasPrefix(res.Address, harnessNetParams.Bech32HRPSegwit) {
t.Fatalf("returned address was not a regtest address")
}
// As a second part, shut down the node and then try to change the
// password when we start it up again.
if err := net.RestartNodeNoUnlock(carol, nil); err != nil {
t.Fatalf("Node restart failed: %v", err)
}
changePwReq := &lnrpc.ChangePasswordRequest{
CurrentPassword: initPw,
NewPassword: newPw,
StatelessInit: true,
}
ctxb := context.Background()
response, err := carol.InitChangePassword(ctxb, changePwReq)
require.NoError(t.t, err)
// Again, make sure no macaroon files have been created by the node
// Carol.
_, err = os.Stat(carol.AdminMacPath())
require.Error(t.t, err)
_, err = os.Stat(carol.ReadMacPath())
require.Error(t.t, err)
_, err = os.Stat(carol.InvoiceMacPath())
require.Error(t.t, err)
// Then check that we can unmarshal the new binary serialized macaroon
// and that it really is a new macaroon.
if err = adminMac.UnmarshalBinary(response.AdminMacaroon); err != nil {
t.Fatalf("unable to unmarshal macaroon: %v", err)
}
if bytes.Equal(response.AdminMacaroon, macBytes) {
t.Fatalf("expected new macaroon to be different")
}
// Finally, find out if we can actually use the new macaroon that has
// been returned to us for a RPC call.
conn2, err := carol.ConnectRPCWithMacaroon(adminMac)
require.NoError(t.t, err)
defer conn2.Close()
adminMacClient = lnrpc.NewLightningClient(conn2)
// Changing the password takes a while, so we use the default timeout
// of 30 seconds to wait for the connection to be ready.
ctxt, _ = context.WithTimeout(context.Background(), defaultTimeout)
res, err = adminMacClient.NewAddress(ctxt, newAddrReq)
require.NoError(t.t, err)
if !strings.HasPrefix(res.Address, harnessNetParams.Bech32HRPSegwit) {
t.Fatalf("returned address was not a regtest address")
}
}
// readMacaroonFromHex loads a macaroon from a hex string.
func readMacaroonFromHex(macHex string) (*macaroon.Macaroon, error) {
macBytes, err := hex.DecodeString(macHex)

View File

@ -774,7 +774,9 @@ func testGetRecoveryInfo(net *lntest.NetworkHarness, t *harnessTest) {
// used for key derivation. This will bring up Carol with an empty
// wallet, and such that she is synced up.
password := []byte("The Magic Words are Squeamish Ossifrage")
carol, mnemonic, err := net.NewNodeWithSeed("Carol", nil, password)
carol, mnemonic, _, err := net.NewNodeWithSeed(
"Carol", nil, password, false,
)
if err != nil {
t.Fatalf("unable to create node with seed; %v", err)
}
@ -875,7 +877,9 @@ func testOnchainFundRecovery(net *lntest.NetworkHarness, t *harnessTest) {
// used for key derivation. This will bring up Carol with an empty
// wallet, and such that she is synced up.
password := []byte("The Magic Words are Squeamish Ossifrage")
carol, mnemonic, err := net.NewNodeWithSeed("Carol", nil, password)
carol, mnemonic, _, err := net.NewNodeWithSeed(
"Carol", nil, password, false,
)
if err != nil {
t.Fatalf("unable to create node with seed; %v", err)
}

View File

@ -282,4 +282,8 @@ var allTestCases = []*testCase{
name: "connection timeout",
test: testNetworkConnectionTimeout,
},
{
name: "stateless init",
test: testStatelessInit,
},
}

View File

@ -613,22 +613,88 @@ func (hn *HarnessNode) initClientWhenReady() error {
}
// Init initializes a harness node by passing the init request via rpc. After
// the request is submitted, this method will block until an
// macaroon-authenticated rpc connection can be established to the harness node.
// the request is submitted, this method will block until a
// macaroon-authenticated RPC connection can be established to the harness node.
// Once established, the new connection is used to initialize the
// LightningClient and subscribes the HarnessNode to topology changes.
func (hn *HarnessNode) Init(ctx context.Context,
initReq *lnrpc.InitWalletRequest) error {
initReq *lnrpc.InitWalletRequest) (*lnrpc.InitWalletResponse, error) {
ctxt, _ := context.WithTimeout(ctx, DefaultTimeout)
_, err := hn.InitWallet(ctxt, initReq)
ctxt, cancel := context.WithTimeout(ctx, DefaultTimeout)
defer cancel()
response, err := hn.InitWallet(ctxt, initReq)
if err != nil {
return err
return nil, err
}
// Wait for the wallet to finish unlocking, such that we can connect to
// it via a macaroon-authenticated rpc connection.
return hn.initClientWhenReady()
var conn *grpc.ClientConn
if err = wait.Predicate(func() bool {
// If the node has been initialized stateless, we need to pass
// the macaroon to the client.
if initReq.StatelessInit {
adminMac := &macaroon.Macaroon{}
err := adminMac.UnmarshalBinary(response.AdminMacaroon)
if err != nil {
return false
}
conn, err = hn.ConnectRPCWithMacaroon(adminMac)
return err == nil
}
// Normal initialization, we expect a macaroon to be in the
// file system.
conn, err = hn.ConnectRPC(true)
return err == nil
}, DefaultTimeout); err != nil {
return nil, err
}
return response, hn.initLightningClient(conn)
}
// InitChangePassword initializes a harness node by passing the change password
// request via RPC. After the request is submitted, this method will block until
// a macaroon-authenticated RPC connection can be established to the harness
// node. Once established, the new connection is used to initialize the
// LightningClient and subscribes the HarnessNode to topology changes.
func (hn *HarnessNode) InitChangePassword(ctx context.Context,
chngPwReq *lnrpc.ChangePasswordRequest) (*lnrpc.ChangePasswordResponse,
error) {
ctxt, cancel := context.WithTimeout(ctx, DefaultTimeout)
defer cancel()
response, err := hn.ChangePassword(ctxt, chngPwReq)
if err != nil {
return nil, err
}
// Wait for the wallet to finish unlocking, such that we can connect to
// it via a macaroon-authenticated rpc connection.
var conn *grpc.ClientConn
if err = wait.Predicate(func() bool {
// If the node has been initialized stateless, we need to pass
// the macaroon to the client.
if chngPwReq.StatelessInit {
adminMac := &macaroon.Macaroon{}
err := adminMac.UnmarshalBinary(response.AdminMacaroon)
if err != nil {
return false
}
conn, err = hn.ConnectRPCWithMacaroon(adminMac)
return err == nil
}
// Normal initialization, we expect a macaroon to be in the
// file system.
conn, err = hn.ConnectRPC(true)
return err == nil
}, DefaultTimeout); err != nil {
return nil, err
}
return response, hn.initLightningClient(conn)
}
// Unlock attempts to unlock the wallet of the target HarnessNode. This method

View File

@ -62,6 +62,10 @@ type Service struct {
// If no external validator for an URI is specified, the service will
// use the internal validator.
externalValidators map[string]MacaroonValidator
// StatelessInit denotes if the service was initialized in the stateless
// mode where no macaroon files should be created on disk.
StatelessInit bool
}
// NewService returns a service backed by the macaroon Bolt DB stored in the
@ -71,7 +75,9 @@ type Service struct {
// listing the same checker more than once is not harmful. Default checkers,
// such as those for `allow`, `time-before`, `declared`, and `error` caveats
// are registered automatically and don't need to be added.
func NewService(dir, location string, checks ...Checker) (*Service, error) {
func NewService(dir, location string, statelessInit bool,
checks ...Checker) (*Service, error) {
// Ensure that the path to the directory exists.
if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, 0700); err != nil {
@ -118,6 +124,7 @@ func NewService(dir, location string, checks ...Checker) (*Service, error) {
Bakery: *svc,
rks: rootKeyStore,
externalValidators: make(map[string]MacaroonValidator),
StatelessInit: statelessInit,
}, nil
}
@ -257,8 +264,8 @@ func (svc *Service) ValidateMacaroon(ctx context.Context,
return err
}
// Check the method being called against the permitted operation and
// the expiration time and IP address and return the result.
// Check the method being called against the permitted operation, the
// expiration time and IP address and return the result.
authChecker := svc.Checker.Auth(macaroon.Slice{mac})
_, err = authChecker.Allow(ctx, requiredPermissions...)
@ -325,3 +332,15 @@ func (svc *Service) DeleteMacaroonID(ctxt context.Context,
rootKeyID []byte) ([]byte, error) {
return svc.rks.DeleteMacaroonID(ctxt, rootKeyID)
}
// GenerateNewRootKey calls the underlying root key store's GenerateNewRootKey
// and returns the result.
func (svc *Service) GenerateNewRootKey() error {
return svc.rks.GenerateNewRootKey()
}
// ChangePassword calls the underlying root key store's ChangePassword and
// returns the result.
func (svc *Service) ChangePassword(oldPw, newPw []byte) error {
return svc.rks.ChangePassword(oldPw, newPw)
}

View File

@ -67,7 +67,7 @@ func TestNewService(t *testing.T) {
// Second, create the new service instance, unlock it and pass in a
// checker that we expect it to add to the bakery.
service, err := macaroons.NewService(
tempDir, "lnd", macaroons.IPLockChecker,
tempDir, "lnd", false, macaroons.IPLockChecker,
)
if err != nil {
t.Fatalf("Error creating new service: %v", err)
@ -118,7 +118,7 @@ func TestValidateMacaroon(t *testing.T) {
tempDir := setupTestRootKeyStorage(t)
defer os.RemoveAll(tempDir)
service, err := macaroons.NewService(
tempDir, "lnd", macaroons.IPLockChecker,
tempDir, "lnd", false, macaroons.IPLockChecker,
)
if err != nil {
t.Fatalf("Error creating new service: %v", err)
@ -178,7 +178,7 @@ func TestListMacaroonIDs(t *testing.T) {
// Second, create the new service instance, unlock it and pass in a
// checker that we expect it to add to the bakery.
service, err := macaroons.NewService(
tempDir, "lnd", macaroons.IPLockChecker,
tempDir, "lnd", false, macaroons.IPLockChecker,
)
require.NoError(t, err, "Error creating new service")
defer service.Close()
@ -210,7 +210,7 @@ func TestDeleteMacaroonID(t *testing.T) {
// Second, create the new service instance, unlock it and pass in a
// checker that we expect it to add to the bakery.
service, err := macaroons.NewService(
tempDir, "lnd", macaroons.IPLockChecker,
tempDir, "lnd", false, macaroons.IPLockChecker,
)
require.NoError(t, err, "Error creating new service")
defer service.Close()

View File

@ -11,6 +11,7 @@ import (
"github.com/lightningnetwork/lnd/channeldb/kvdb"
"github.com/btcsuite/btcwallet/snacl"
"github.com/btcsuite/btcwallet/walletdb"
)
const (
@ -26,10 +27,10 @@ var (
// just 0, to emulate the memory storage that comes with bakery.
DefaultRootKeyID = []byte("0")
// encryptedKeyID is the name of the database key that stores the
// encryptionKeyID is the name of the database key that stores the
// encryption key, encrypted with a salted + hashed password. The
// format is 32 bytes of salt, and the rest is encrypted key.
encryptedKeyID = []byte("enckey")
encryptionKeyID = []byte("enckey")
// ErrAlreadyUnlocked specifies that the store has already been
// unlocked.
@ -45,6 +46,15 @@ var (
// ErrKeyValueForbidden is used when the root key ID uses encryptedKeyID as
// its value.
ErrKeyValueForbidden = fmt.Errorf("root key ID value is not allowed")
// ErrRootKeyBucketNotFound specifies that there is no macaroon root key
// bucket yet which can/should only happen if the store has been
// corrupted or was initialized incorrectly.
ErrRootKeyBucketNotFound = fmt.Errorf("root key bucket not found")
// ErrEncKeyNotFound specifies that there was no encryption key found
// even if one was expected to be generated.
ErrEncKeyNotFound = fmt.Errorf("macaroon encryption key not found")
)
// RootKeyStorage implements the bakery.RootKeyStorage interface.
@ -89,7 +99,10 @@ func (r *RootKeyStorage) CreateUnlock(password *[]byte) error {
return kvdb.Update(r, func(tx kvdb.RwTx) error {
bucket := tx.ReadWriteBucket(rootKeyBucketName)
dbKey := bucket.Get(encryptedKeyID)
if bucket == nil {
return ErrRootKeyBucketNotFound
}
dbKey := bucket.Get(encryptionKeyID)
if len(dbKey) > 0 {
// We've already stored a key, so try to unlock with
// the password.
@ -116,7 +129,7 @@ func (r *RootKeyStorage) CreateUnlock(password *[]byte) error {
return err
}
err = bucket.Put(encryptedKeyID, encKey.Marshal())
err = bucket.Put(encryptionKeyID, encKey.Marshal())
if err != nil {
return err
}
@ -126,6 +139,83 @@ func (r *RootKeyStorage) CreateUnlock(password *[]byte) error {
}, func() {})
}
// ChangePassword decrypts the macaroon root key with the old password and then
// encrypts it again with the new password.
func (r *RootKeyStorage) ChangePassword(oldPw, newPw []byte) error {
// We need the store to already be unlocked. With this we can make sure
// that there already is a key in the DB.
if r.encKey == nil {
return ErrStoreLocked
}
// Check if a nil password has been passed; return an error if so.
if oldPw == nil || newPw == nil {
return ErrPasswordRequired
}
return kvdb.Update(r, func(tx kvdb.RwTx) error {
bucket := tx.ReadWriteBucket(rootKeyBucketName)
if bucket == nil {
return ErrRootKeyBucketNotFound
}
encKeyDb := bucket.Get(encryptionKeyID)
rootKeyDb := bucket.Get(DefaultRootKeyID)
// Both the encryption key and the root key must be present
// otherwise we are in the wrong state to change the password.
if len(encKeyDb) == 0 || len(rootKeyDb) == 0 {
return ErrEncKeyNotFound
}
// Unmarshal parameters for old encryption key and derive the
// old key with them.
encKeyOld := &snacl.SecretKey{}
err := encKeyOld.Unmarshal(encKeyDb)
if err != nil {
return err
}
err = encKeyOld.DeriveKey(&oldPw)
if err != nil {
return err
}
// Create a new encryption key from the new password.
encKeyNew, err := snacl.NewSecretKey(
&newPw, scryptN, scryptR, scryptP,
)
if err != nil {
return err
}
// Now try to decrypt the root key with the old encryption key,
// encrypt it with the new one and then store it in the DB.
decryptedKey, err := encKeyOld.Decrypt(rootKeyDb)
if err != nil {
return err
}
rootKey := make([]byte, len(decryptedKey))
copy(rootKey, decryptedKey)
encryptedKey, err := encKeyNew.Encrypt(rootKey)
if err != nil {
return err
}
err = bucket.Put(DefaultRootKeyID, encryptedKey)
if err != nil {
return err
}
// Finally, store the new encryption key parameters in the DB
// as well.
err = bucket.Put(encryptionKeyID, encKeyNew.Marshal())
if err != nil {
return err
}
r.encKey = encKeyNew
return nil
}, func() {})
}
// Get implements the Get method for the bakery.RootKeyStorage interface.
func (r *RootKeyStorage) Get(_ context.Context, id []byte) ([]byte, error) {
r.encKeyMtx.RLock()
@ -136,7 +226,11 @@ func (r *RootKeyStorage) Get(_ context.Context, id []byte) ([]byte, error) {
}
var rootKey []byte
err := kvdb.View(r, func(tx kvdb.RTx) error {
dbKey := tx.ReadBucket(rootKeyBucketName).Get(id)
bucket := tx.ReadBucket(rootKeyBucketName)
if bucket == nil {
return ErrRootKeyBucketNotFound
}
dbKey := bucket.Get(id)
if len(dbKey) == 0 {
return fmt.Errorf("root key with id %s doesn't exist",
string(id))
@ -178,13 +272,16 @@ func (r *RootKeyStorage) RootKey(ctx context.Context) ([]byte, []byte, error) {
return nil, nil, err
}
if bytes.Equal(id, encryptedKeyID) {
if bytes.Equal(id, encryptionKeyID) {
return nil, nil, ErrKeyValueForbidden
}
err = kvdb.Update(r, func(tx kvdb.RwTx) error {
ns := tx.ReadWriteBucket(rootKeyBucketName)
dbKey := ns.Get(id)
bucket := tx.ReadWriteBucket(rootKeyBucketName)
if bucket == nil {
return ErrRootKeyBucketNotFound
}
dbKey := bucket.Get(id)
// If there's a root key stored in the bucket, decrypt it and
// return it.
@ -199,18 +296,11 @@ func (r *RootKeyStorage) RootKey(ctx context.Context) ([]byte, []byte, error) {
return nil
}
// Otherwise, create a RootKeyLen-byte root key, encrypt it,
// Otherwise, create a new root key, encrypt it,
// and store it in the bucket.
rootKey = make([]byte, RootKeyLen)
if _, err := io.ReadFull(rand.Reader, rootKey[:]); err != nil {
return err
}
encKey, err := r.encKey.Encrypt(rootKey)
if err != nil {
return err
}
return ns.Put(id, encKey)
newKey, err := generateAndStoreNewRootKey(bucket, id, r.encKey)
rootKey = newKey
return err
}, func() {
rootKey = nil
})
@ -221,6 +311,26 @@ func (r *RootKeyStorage) RootKey(ctx context.Context) ([]byte, []byte, error) {
return rootKey, id, nil
}
// GenerateNewRootKey generates a new macaroon root key, replacing the previous
// root key if it existed.
func (r *RootKeyStorage) GenerateNewRootKey() error {
// We need the store to already be unlocked. With this we can make sure
// that there already is a key in the DB that can be replaced.
if r.encKey == nil {
return ErrStoreLocked
}
return kvdb.Update(r, func(tx kvdb.RwTx) error {
bucket := tx.ReadWriteBucket(rootKeyBucketName)
if bucket == nil {
return ErrRootKeyBucketNotFound
}
_, err := generateAndStoreNewRootKey(
bucket, DefaultRootKeyID, r.encKey,
)
return err
}, func() {})
}
// Close closes the underlying database and zeroes the encryption key stored
// in memory.
func (r *RootKeyStorage) Close() error {
@ -229,10 +339,29 @@ func (r *RootKeyStorage) Close() error {
if r.encKey != nil {
r.encKey.Zero()
r.encKey = nil
}
return r.Backend.Close()
}
// generateAndStoreNewRootKey creates a new random RootKeyLen-byte root key,
// encrypts it with the given encryption key and stores it in the bucket.
// Any previously set key will be overwritten.
func generateAndStoreNewRootKey(bucket walletdb.ReadWriteBucket, id []byte,
key *snacl.SecretKey) ([]byte, error) {
rootKey := make([]byte, RootKeyLen)
if _, err := io.ReadFull(rand.Reader, rootKey); err != nil {
return nil, err
}
encryptedKey, err := key.Encrypt(rootKey)
if err != nil {
return nil, err
}
return rootKey, bucket.Put(id, encryptedKey)
}
// ListMacaroonIDs returns all the root key ID values except the value of
// encryptedKeyID.
func (r *RootKeyStorage) ListMacaroonIDs(_ context.Context) ([][]byte, error) {
@ -254,7 +383,7 @@ func (r *RootKeyStorage) ListMacaroonIDs(_ context.Context) ([][]byte, error) {
// to rootKeySlice.
appendRootKey := func(k, _ []byte) error {
// Only append when the key value is not encryptedKeyID.
if !bytes.Equal(k, encryptedKeyID) {
if !bytes.Equal(k, encryptionKeyID) {
rootKeySlice = append(rootKeySlice, k)
}
return nil
@ -290,7 +419,7 @@ func (r *RootKeyStorage) DeleteMacaroonID(
}
// Deleting encryptedKeyID or DefaultRootKeyID is not allowed.
if bytes.Equal(rootKeyID, encryptedKeyID) ||
if bytes.Equal(rootKeyID, encryptionKeyID) ||
bytes.Equal(rootKeyID, DefaultRootKeyID) {
return nil, ErrDeletionForbidden

View File

@ -1,7 +1,6 @@
package macaroons_test
import (
"bytes"
"context"
"io/ioutil"
"os"
@ -12,161 +11,212 @@ import (
"github.com/lightningnetwork/lnd/macaroons"
"github.com/btcsuite/btcwallet/snacl"
"github.com/stretchr/testify/require"
)
func TestStore(t *testing.T) {
var (
defaultRootKeyIDContext = macaroons.ContextWithRootKeyID(
context.Background(), macaroons.DefaultRootKeyID,
)
)
// newTestStore creates a new bolt DB in a temporary directory and then
// initializes a root key storage for that DB.
func newTestStore(t *testing.T) (string, func(), *macaroons.RootKeyStorage) {
tempDir, err := ioutil.TempDir("", "macaroonstore-")
if err != nil {
t.Fatalf("Error creating temp dir: %v", err)
require.NoError(t, err)
cleanup, store := openTestStore(t, tempDir)
cleanup2 := func() {
cleanup()
_ = os.RemoveAll(tempDir)
}
defer os.RemoveAll(tempDir)
return tempDir, cleanup2, store
}
// openTestStore opens an existing bolt DB and then initializes a root key
// storage for that DB.
func openTestStore(t *testing.T, tempDir string) (func(),
*macaroons.RootKeyStorage) {
db, err := kvdb.Create(
kvdb.BoltBackendName, path.Join(tempDir, "weks.db"), true,
)
if err != nil {
t.Fatalf("Error opening store DB: %v", err)
}
require.NoError(t, err)
store, err := macaroons.NewRootKeyStorage(db)
if err != nil {
db.Close()
_ = db.Close()
t.Fatalf("Error creating root key store: %v", err)
}
defer store.Close()
_, _, err = store.RootKey(context.TODO())
if err != macaroons.ErrStoreLocked {
t.Fatalf("Received %v instead of ErrStoreLocked", err)
cleanup := func() {
_ = store.Close()
}
return cleanup, store
}
// TestStore tests the normal use cases of the store like creating, unlocking,
// reading keys and closing it.
func TestStore(t *testing.T) {
tempDir, cleanup, store := newTestStore(t)
defer cleanup()
_, _, err := store.RootKey(context.TODO())
require.Equal(t, macaroons.ErrStoreLocked, err)
_, err = store.Get(context.TODO(), nil)
if err != macaroons.ErrStoreLocked {
t.Fatalf("Received %v instead of ErrStoreLocked", err)
}
require.Equal(t, macaroons.ErrStoreLocked, err)
pw := []byte("weks")
err = store.CreateUnlock(&pw)
if err != nil {
t.Fatalf("Error creating store encryption key: %v", err)
}
require.NoError(t, err)
// Check ErrContextRootKeyID is returned when no root key ID found in
// context.
_, _, err = store.RootKey(context.TODO())
if err != macaroons.ErrContextRootKeyID {
t.Fatalf("Received %v instead of ErrContextRootKeyID", err)
}
require.Equal(t, macaroons.ErrContextRootKeyID, err)
// Check ErrMissingRootKeyID is returned when empty root key ID is used.
emptyKeyID := []byte{}
emptyKeyID := make([]byte, 0)
badCtx := macaroons.ContextWithRootKeyID(context.TODO(), emptyKeyID)
_, _, err = store.RootKey(badCtx)
if err != macaroons.ErrMissingRootKeyID {
t.Fatalf("Received %v instead of ErrMissingRootKeyID", err)
}
require.Equal(t, macaroons.ErrMissingRootKeyID, err)
// Create a context with illegal root key ID value.
encryptedKeyID := []byte("enckey")
badCtx = macaroons.ContextWithRootKeyID(context.TODO(), encryptedKeyID)
_, _, err = store.RootKey(badCtx)
if err != macaroons.ErrKeyValueForbidden {
t.Fatalf("Received %v instead of ErrKeyValueForbidden", err)
}
require.Equal(t, macaroons.ErrKeyValueForbidden, err)
// Create a context with root key ID value.
ctx := macaroons.ContextWithRootKeyID(
context.TODO(), macaroons.DefaultRootKeyID,
)
key, id, err := store.RootKey(ctx)
if err != nil {
t.Fatalf("Error getting root key from store: %v", err)
}
key, id, err := store.RootKey(defaultRootKeyIDContext)
require.NoError(t, err)
rootID := id
if !bytes.Equal(rootID, macaroons.DefaultRootKeyID) {
t.Fatalf("Root key ID doesn't match: expected %v, got %v",
macaroons.DefaultRootKeyID, rootID)
}
require.Equal(t, macaroons.DefaultRootKeyID, rootID)
key2, err := store.Get(ctx, id)
if err != nil {
t.Fatalf("Error getting key with ID %s: %v", string(id), err)
}
if !bytes.Equal(key, key2) {
t.Fatalf("Root key doesn't match: expected %v, got %v",
key, key2)
}
key2, err := store.Get(defaultRootKeyIDContext, id)
require.NoError(t, err)
require.Equal(t, key, key2)
badpw := []byte("badweks")
err = store.CreateUnlock(&badpw)
if err != macaroons.ErrAlreadyUnlocked {
t.Fatalf("Received %v instead of ErrAlreadyUnlocked", err)
}
require.Equal(t, macaroons.ErrAlreadyUnlocked, err)
store.Close()
_ = store.Close()
// Between here and the re-opening of the store, it's possible to get
// a double-close, but that's not such a big deal since the tests will
// fail anyway in that case.
db, err = kvdb.Create(
kvdb.BoltBackendName, path.Join(tempDir, "weks.db"), true,
)
if err != nil {
t.Fatalf("Error opening store DB: %v", err)
}
store, err = macaroons.NewRootKeyStorage(db)
if err != nil {
db.Close()
t.Fatalf("Error creating root key store: %v", err)
}
_, store = openTestStore(t, tempDir)
err = store.CreateUnlock(&badpw)
if err != snacl.ErrInvalidPassword {
t.Fatalf("Received %v instead of ErrInvalidPassword", err)
}
require.Equal(t, snacl.ErrInvalidPassword, err)
err = store.CreateUnlock(nil)
if err != macaroons.ErrPasswordRequired {
t.Fatalf("Received %v instead of ErrPasswordRequired", err)
}
require.Equal(t, macaroons.ErrPasswordRequired, err)
_, _, err = store.RootKey(ctx)
if err != macaroons.ErrStoreLocked {
t.Fatalf("Received %v instead of ErrStoreLocked", err)
}
_, _, err = store.RootKey(defaultRootKeyIDContext)
require.Equal(t, macaroons.ErrStoreLocked, err)
_, err = store.Get(ctx, nil)
if err != macaroons.ErrStoreLocked {
t.Fatalf("Received %v instead of ErrStoreLocked", err)
}
_, err = store.Get(defaultRootKeyIDContext, nil)
require.Equal(t, macaroons.ErrStoreLocked, err)
err = store.CreateUnlock(&pw)
if err != nil {
t.Fatalf("Error unlocking root key store: %v", err)
}
require.NoError(t, err)
key, err = store.Get(ctx, rootID)
if err != nil {
t.Fatalf("Error getting key with ID %s: %v",
string(rootID), err)
}
if !bytes.Equal(key, key2) {
t.Fatalf("Root key doesn't match: expected %v, got %v",
key2, key)
}
key, err = store.Get(defaultRootKeyIDContext, rootID)
require.NoError(t, err)
require.Equal(t, key, key2)
key, id, err = store.RootKey(ctx)
if err != nil {
t.Fatalf("Error getting root key from store: %v", err)
}
if !bytes.Equal(key, key2) {
t.Fatalf("Root key doesn't match: expected %v, got %v",
key2, key)
}
if !bytes.Equal(rootID, id) {
t.Fatalf("Root ID doesn't match: expected %v, got %v",
rootID, id)
}
key, id, err = store.RootKey(defaultRootKeyIDContext)
require.NoError(t, err)
require.Equal(t, key, key2)
require.Equal(t, rootID, id)
}
// TestStoreGenerateNewRootKey tests that a root key can be replaced with a new
// one in the store without changing the password.
func TestStoreGenerateNewRootKey(t *testing.T) {
_, cleanup, store := newTestStore(t)
defer cleanup()
// The store must be unlocked to replace the root key.
err := store.GenerateNewRootKey()
require.Equal(t, macaroons.ErrStoreLocked, err)
// Unlock the store and read the current key.
pw := []byte("weks")
err = store.CreateUnlock(&pw)
require.NoError(t, err)
oldRootKey, _, err := store.RootKey(defaultRootKeyIDContext)
require.NoError(t, err)
// Replace the root key with a new random key.
err = store.GenerateNewRootKey()
require.NoError(t, err)
// Finally, read the root key from the DB and compare it to the one
// we got returned earlier. This makes sure that the encryption/
// decryption of the key in the DB worked as expected too.
newRootKey, _, err := store.RootKey(defaultRootKeyIDContext)
require.NoError(t, err)
require.NotEqual(t, oldRootKey, newRootKey)
}
// TestStoreChangePassword tests that the password for the store can be changed
// without changing the root key.
func TestStoreChangePassword(t *testing.T) {
tempDir, cleanup, store := newTestStore(t)
defer cleanup()
// The store must be unlocked to replace the root key.
err := store.ChangePassword(nil, nil)
require.Equal(t, macaroons.ErrStoreLocked, err)
// Unlock the DB and read the current root key. This will need to stay
// the same after changing the password for the test to succeed.
pw := []byte("weks")
err = store.CreateUnlock(&pw)
require.NoError(t, err)
rootKey, _, err := store.RootKey(defaultRootKeyIDContext)
require.NoError(t, err)
// Both passwords must be set.
err = store.ChangePassword(nil, nil)
require.Equal(t, macaroons.ErrPasswordRequired, err)
// Make sure that an error is returned if we try to change the password
// without the correct old password.
wrongPw := []byte("wrong")
newPw := []byte("newpassword")
err = store.ChangePassword(wrongPw, newPw)
require.Equal(t, snacl.ErrInvalidPassword, err)
// Now really do change the password.
err = store.ChangePassword(pw, newPw)
require.NoError(t, err)
// Close the store. This will close the underlying DB and we need to
// create a new store instance. Let's make sure we can't use it again
// after closing.
err = store.Close()
require.NoError(t, err)
err = store.CreateUnlock(&newPw)
require.Error(t, err)
// Let's open it again and try unlocking with the new password.
_, store = openTestStore(t, tempDir)
err = store.CreateUnlock(&newPw)
require.NoError(t, err)
// Finally read the root key from the DB using the new password and
// make sure the root key stayed the same.
rootKeyDb, _, err := store.RootKey(defaultRootKeyIDContext)
require.NoError(t, err)
require.Equal(t, rootKey, rootKeyDb)
}

View File

@ -16,6 +16,13 @@ import (
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwallet"
"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
@ -54,6 +61,11 @@ type WalletInitMsg struct {
// ChanBackups a set of static channel backups that should be received
// after the wallet has been initialized.
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
@ -85,6 +97,11 @@ type WalletUnlockMsg struct {
// UnloadWallet is a function for unloading the wallet, which should
// be called on shutdown.
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
@ -100,10 +117,18 @@ type UnlockerService struct {
// sent.
UnlockMsgs chan *WalletUnlockMsg
// MacResponseChan is the channel for sending back the admin macaroon to
// the WalletUnlocker service.
MacResponseChan chan []byte
chainDir string
noFreelistSync bool
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.
@ -111,11 +136,15 @@ func New(chainDir string, params *chaincfg.Params, noFreelistSync bool,
macaroonFiles []string) *UnlockerService {
return &UnlockerService{
InitMsgs: make(chan *WalletInitMsg, 1),
UnlockMsgs: make(chan *WalletUnlockMsg, 1),
chainDir: chainDir,
netParams: params,
macaroonFiles: macaroonFiles,
InitMsgs: make(chan *WalletInitMsg, 1),
UnlockMsgs: make(chan *WalletUnlockMsg, 1),
// Make sure we buffer the channel is buffered so the main lnd
// 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
// method should be used to commit the newly generated seed, and create the
// wallet.
func (u *UnlockerService) GenSeed(ctx context.Context,
func (u *UnlockerService) GenSeed(_ context.Context,
in *lnrpc.GenSeedRequest) (*lnrpc.GenSeedResponse, error) {
// 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,
WalletSeed: cipherSeed,
RecoveryWindow: uint32(recoveryWindow),
StatelessInit: in.StatelessInit,
}
// 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
}
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
@ -351,6 +397,7 @@ func (u *UnlockerService) UnlockWallet(ctx context.Context,
RecoveryWindow: recoveryWindow,
Wallet: unlockedWallet,
UnloadWallet: loader.UnloadWallet,
StatelessInit: in.StatelessInit,
}
// 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
}
// 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
// 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
@ -408,18 +468,32 @@ func (u *UnlockerService) ChangePassword(ctx context.Context,
if err != nil {
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
// password, we'll remove all of the macaroon files so that they're
// re-generated at startup using the new password. We'll make sure to do
// this after unlocking the wallet to ensure macaroon files don't get
// deleted with incorrect password attempts.
for _, file := range u.macaroonFiles {
err := os.Remove(file)
if err != nil && !os.IsNotExist(err) {
return nil, err
// Now that we've opened the wallet, we need to close it in case of an
// error. But not if we succeed, then the caller must close it.
orderlyReturn := false
defer func() {
if !orderlyReturn {
_ = loader.UnloadWallet()
}
}()
// 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)
}
// 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
// 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.

View File

@ -3,9 +3,10 @@ package walletunlocker_test
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"os"
"strings"
"path"
"testing"
"time"
@ -14,15 +15,20 @@ import (
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/wallet"
"github.com/lightningnetwork/lnd/aezeed"
"github.com/lightningnetwork/lnd/channeldb/kvdb"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
"github.com/lightningnetwork/lnd/macaroons"
"github.com/lightningnetwork/lnd/walletunlocker"
"github.com/stretchr/testify/require"
)
var (
testPassword = []byte("test-password")
testSeed = []byte("test-seed-123456789")
testMac = []byte("fakemacaroon")
testEntropy = [aezeed.EntropySize]byte{
0x81, 0xb6, 0x37, 0xd8,
@ -34,9 +40,21 @@ var (
testNetParams = &chaincfg.MainNetParams
testRecoveryWindow uint32 = 150
defaultTestTimeout = 3 * time.Second
defaultRootKeyIDContext = macaroons.ContextWithRootKeyID(
context.Background(), macaroons.DefaultRootKeyID,
)
)
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
// creating new wallet encryption keys.
fastScrypt := waddrmgr.FastScryptOptions
@ -53,15 +71,64 @@ func createTestWallet(t *testing.T, dir string, netParams *chaincfg.Params) {
netDir := btcwallet.NetworkDir(dir, netParams)
loader := wallet.NewLoader(netParams, netDir, true, 0)
_, err := loader.CreateNewWallet(
testPassword, testPassword, testSeed, time.Time{},
pubPw, privPw, testSeed, time.Time{},
)
require.NoError(t, err)
err = loader.UnloadWallet()
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 {
t.Fatalf("failed creating wallet: %v", err)
return nil, err
}
err = loader.UnloadWallet()
store, err := macaroons.NewRootKeyStorage(db)
if err != nil {
t.Fatalf("failed unloading wallet: %v", err)
_ = 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
@ -72,10 +139,10 @@ func TestGenSeed(t *testing.T) {
// 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 os.RemoveAll(testDir)
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(testDir)
}()
service := walletunlocker.New(testDir, testNetParams, true, nil)
@ -89,18 +156,14 @@ func TestGenSeed(t *testing.T) {
ctx := context.Background()
seedResp, err := service.GenSeed(ctx, genSeedReq)
if err != nil {
t.Fatalf("unable to generate seed: %v", err)
}
require.NoError(t, 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)
}
require.NoError(t, err)
}
// TestGenSeedInvalidEntropy tests that the gen seed method generates a valid
@ -112,11 +175,9 @@ func TestGenSeedGenerateEntropy(t *testing.T) {
// 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)
}
require.NoError(t, err)
defer func() {
os.RemoveAll(testDir)
_ = os.RemoveAll(testDir)
}()
service := walletunlocker.New(testDir, testNetParams, true, nil)
@ -129,18 +190,14 @@ func TestGenSeedGenerateEntropy(t *testing.T) {
ctx := context.Background()
seedResp, err := service.GenSeed(ctx, genSeedReq)
if err != nil {
t.Fatalf("unable to generate seed: %v", err)
}
require.NoError(t, 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)
}
require.NoError(t, err)
}
// TestGenSeedInvalidEntropy tests that if a user attempt to create a seed with
@ -152,11 +209,9 @@ func TestGenSeedInvalidEntropy(t *testing.T) {
// 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)
}
require.NoError(t, err)
defer func() {
os.RemoveAll(testDir)
_ = os.RemoveAll(testDir)
}()
service := walletunlocker.New(testDir, testNetParams, true, nil)
@ -172,13 +227,8 @@ func TestGenSeedInvalidEntropy(t *testing.T) {
// 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")
}
require.Error(t, err)
require.Contains(t, err.Error(), "incorrect entropy length")
}
// TestInitWallet tests that the user is able to properly initialize the wallet
@ -188,32 +238,18 @@ func TestInitWallet(t *testing.T) {
// 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)
}
require.NoError(t, err)
defer func() {
os.RemoveAll(testDir)
_ = os.RemoveAll(testDir)
}()
// Create new UnlockerService.
service := walletunlocker.New(testDir, testNetParams, true, nil)
// Once we have the unlocker service created, we'll now instantiate a
// new cipher seed instance.
cipherSeed, err := aezeed.New(
keychain.KeyDerivationVersion, &testEntropy, time.Now(),
)
if err != nil {
t.Fatalf("unable to create seed: %v", err)
}
// With the new seed created, we'll convert it into a mnemonic phrase
// that we'll send over to initialize the wallet.
// new cipher seed and its mnemonic.
pass := []byte("test")
mnemonic, err := cipherSeed.ToMnemonic(pass)
if err != nil {
t.Fatalf("unable to create mnemonic: %v", err)
}
cipherSeed, mnemonic := createSeedAndMnemonic(t, pass)
// 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
@ -222,45 +258,48 @@ func TestInitWallet(t *testing.T) {
ctx := context.Background()
req := &lnrpc.InitWalletRequest{
WalletPassword: testPassword,
CipherSeedMnemonic: []string(mnemonic[:]),
CipherSeedMnemonic: mnemonic[:],
AezeedPassphrase: pass,
RecoveryWindow: int32(testRecoveryWindow),
StatelessInit: true,
}
_, err = service.InitWallet(ctx, req)
if err != nil {
t.Fatalf("InitWallet call failed: %v", err)
}
errChan := make(chan error, 1)
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
// should be sent over and match exactly.
select {
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[:])
}
if msg.RecoveryWindow != testRecoveryWindow {
t.Fatalf("mismatched recovery window: expected %v,"+
"got %v", testRecoveryWindow,
msg.RecoveryWindow)
}
case err := <-errChan:
t.Fatalf("InitWallet call failed: %v", err)
case <-time.After(3 * time.Second):
case msg := <-service.InitMsgs:
msgSeed := msg.WalletSeed
require.Equal(t, testPassword, msg.Passphrase)
require.Equal(
t, cipherSeed.InternalVersion, msgSeed.InternalVersion,
)
require.Equal(t, cipherSeed.Birthday, msgSeed.Birthday)
require.Equal(t, cipherSeed.Entropy, msgSeed.Entropy)
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):
t.Fatalf("password not received")
}
@ -270,16 +309,12 @@ func TestInitWallet(t *testing.T) {
// Now calling InitWallet should fail, since a wallet already exists in
// the directory.
_, err = service.InitWallet(ctx, req)
if err == nil {
t.Fatalf("InitWallet did not fail as expected")
}
require.Error(t, err)
// 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")
}
require.Error(t, err)
}
// TestInitWalletInvalidCipherSeed tests that if we attempt to create a wallet
@ -289,11 +324,9 @@ func TestCreateWalletInvalidEntropy(t *testing.T) {
// 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)
}
require.NoError(t, err)
defer func() {
os.RemoveAll(testDir)
_ = os.RemoveAll(testDir)
}()
// Create new UnlockerService.
@ -309,9 +342,7 @@ func TestCreateWalletInvalidEntropy(t *testing.T) {
ctx := context.Background()
_, err = service.InitWallet(ctx, req)
if err == nil {
t.Fatalf("wallet creation should have failed")
}
require.Error(t, err)
}
// TestUnlockWallet checks that trying to unlock non-existing wallet fail, that
@ -322,11 +353,9 @@ func TestUnlockWallet(t *testing.T) {
// testDir is empty, meaning wallet was not created from before.
testDir, err := ioutil.TempDir("", "testunlock")
if err != nil {
t.Fatalf("unable to create temp directory: %v", err)
}
require.NoError(t, err)
defer func() {
os.RemoveAll(testDir)
_ = os.RemoveAll(testDir)
}()
// Create new UnlockerService.
@ -336,13 +365,12 @@ func TestUnlockWallet(t *testing.T) {
req := &lnrpc.UnlockWalletRequest{
WalletPassword: testPassword,
RecoveryWindow: int32(testRecoveryWindow),
StatelessInit: true,
}
// Should fail to unlock non-existing wallet.
_, err = service.UnlockWallet(ctx, req)
if err == nil {
t.Fatalf("expected call to UnlockWallet to fail")
}
require.Error(t, err)
// Create a wallet we can try to unlock.
createTestWallet(t, testDir, testNetParams)
@ -352,47 +380,63 @@ func TestUnlockWallet(t *testing.T) {
WalletPassword: []byte("wrong-ofc"),
}
_, err = service.UnlockWallet(ctx, wrongReq)
if err == nil {
t.Fatalf("expected call to UnlockWallet to fail")
}
require.Error(t, err)
// With the correct password, we should be able to unlock the wallet.
_, err = service.UnlockWallet(ctx, req)
if err != nil {
t.Fatalf("unable to unlock wallet: %v", err)
}
errChan := make(chan error, 1)
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.
select {
case err := <-errChan:
t.Fatalf("UnlockWallet call failed: %v", err)
case unlockMsg := <-service.UnlockMsgs:
if !bytes.Equal(unlockMsg.Passphrase, testPassword) {
t.Fatalf("expected to receive password %x, got %x",
testPassword, unlockMsg.Passphrase)
}
if unlockMsg.RecoveryWindow != testRecoveryWindow {
t.Fatalf("expected to receive recovery window %d, "+
"got %d", testRecoveryWindow,
unlockMsg.RecoveryWindow)
}
case <-time.After(3 * time.Second):
require.Equal(t, testPassword, unlockMsg.Passphrase)
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):
t.Fatalf("password not received")
}
}
// TestChangeWalletPassword tests that we can successfully change the wallet's
// password needed to unlock it.
func TestChangeWalletPassword(t *testing.T) {
// TestChangeWalletPasswordNewRootkey tests that we can successfully change the
// wallet's password needed to unlock it and rotate the root key for the
// macaroons in the same process.
func TestChangeWalletPasswordNewRootkey(t *testing.T) {
t.Parallel()
// testDir is empty, meaning wallet was not created from before.
testDir, err := ioutil.TempDir("", "testchangepassword")
if err != nil {
t.Fatalf("unable to create temp directory: %v", err)
}
defer os.RemoveAll(testDir)
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, &testPassword, testNetParams,
)
require.NoError(t, err)
require.NoError(t, store.Close())
// 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
for i := 0; i < 3; i++ {
file, err := ioutil.TempFile(testDir, "")
@ -400,7 +444,7 @@ func TestChangeWalletPassword(t *testing.T) {
t.Fatalf("unable to create temp file: %v", err)
}
tempFiles = append(tempFiles, file.Name())
file.Close()
require.NoError(t, file.Close())
}
// Create a new UnlockerService with our temp files.
@ -410,15 +454,14 @@ func TestChangeWalletPassword(t *testing.T) {
newPassword := []byte("hunter2???")
req := &lnrpc.ChangePasswordRequest{
CurrentPassword: testPassword,
NewPassword: newPassword,
CurrentPassword: testPassword,
NewPassword: newPassword,
NewMacaroonRootKey: true,
}
// Changing the password to a non-existing wallet should fail.
_, err = service.ChangePassword(ctx, req)
if err == nil {
t.Fatal("expected call to ChangePassword to fail")
}
require.Error(t, err)
// Create a wallet to test changing the password.
createTestWallet(t, testDir, testNetParams)
@ -430,9 +473,7 @@ func TestChangeWalletPassword(t *testing.T) {
NewPassword: newPassword,
}
_, err = service.ChangePassword(ctx, wrongReq)
if err == nil {
t.Fatal("expected call to ChangePassword to fail")
}
require.Error(t, err)
// The files should still exist after an unsuccessful attempt to change
// the wallet's password.
@ -446,16 +487,28 @@ func TestChangeWalletPassword(t *testing.T) {
// new password should fail.
wrongReq.NewPassword = []byte("8")
_, err = service.ChangePassword(ctx, wrongReq)
if err == nil {
t.Fatal("expected call to ChangePassword to fail")
}
require.Error(t, err)
// When providing the correct wallet's current password and a new
// password that meets the length requirement, the password change
// should succeed.
_, err = service.ChangePassword(ctx, req)
if err != nil {
t.Fatalf("unable to change wallet's password: %v", err)
errChan := make(chan error, 1)
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.
@ -464,15 +517,146 @@ func TestChangeWalletPassword(t *testing.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 {
case err := <-errChan:
t.Fatalf("ChangePassword call failed: %v", err)
case unlockMsg := <-service.UnlockMsgs:
if !bytes.Equal(unlockMsg.Passphrase, newPassword) {
t.Fatalf("expected to receive password %x, got %x",
testPassword, unlockMsg.Passphrase)
}
case <-time.After(3 * time.Second):
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):
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
}
}