Merge pull request #5256 from guggero/wallet-unlock-file

Auto-unlock wallet from password file
This commit is contained in:
Olaoluwa Osuntokun 2021-05-12 13:41:30 -07:00 committed by GitHub
commit 93730088c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 344 additions and 48 deletions

View File

@ -292,7 +292,8 @@ type Config struct {
NoNetBootstrap bool `long:"nobootstrap" description:"If true, then automatic network bootstrapping will not be attempted."`
NoSeedBackup bool `long:"noseedbackup" description:"If true, NO SEED WILL BE EXPOSED -- EVER, AND THE WALLET WILL BE ENCRYPTED USING THE DEFAULT PASSPHRASE. THIS FLAG IS ONLY FOR TESTING AND SHOULD NEVER BE USED ON MAINNET."`
NoSeedBackup bool `long:"noseedbackup" description:"If true, NO SEED WILL BE EXPOSED -- EVER, AND THE WALLET WILL BE ENCRYPTED USING THE DEFAULT PASSPHRASE. THIS FLAG IS ONLY FOR TESTING AND SHOULD NEVER BE USED ON MAINNET."`
WalletUnlockPasswordFile string `long:"wallet-unlock-password-file" description:"The full path to a file (or pipe/device) that contains the password for unlocking the wallet; if set, no unlocking through RPC is possible and lnd will exit if no wallet exists or the password is incorrect"`
ResetWalletTransactions bool `long:"reset-wallet-transactions" description:"Removes all transaction history from the on-chain wallet on startup, forcing a full chain rescan starting at the wallet's birthday. Implements the same functionality as btcwallet's dropwtxmgr command. Should be set to false after successful execution to avoid rescanning on every restart of lnd."`
@ -689,6 +690,9 @@ func ValidateConfig(cfg Config, usageMessage string,
cfg.Tor.WatchtowerKeyPath = CleanAndExpandPath(cfg.Tor.WatchtowerKeyPath)
cfg.Watchtower.TowerDir = CleanAndExpandPath(cfg.Watchtower.TowerDir)
cfg.BackupFilePath = CleanAndExpandPath(cfg.BackupFilePath)
cfg.WalletUnlockPasswordFile = CleanAndExpandPath(
cfg.WalletUnlockPasswordFile,
)
// Create the lnd directory and all other sub directories if they don't
// already exist. This makes sure that directory trees are also created
@ -1280,6 +1284,20 @@ func ValidateConfig(cfg Config, usageMessage string,
return nil, err
}
switch {
// The no seed backup and auto unlock are mutually exclusive.
case cfg.NoSeedBackup && cfg.WalletUnlockPasswordFile != "":
return nil, fmt.Errorf("cannot set noseedbackup and " +
"wallet-unlock-password-file at the same time")
// If a password file was specified, we need it to exist.
case cfg.WalletUnlockPasswordFile != "" &&
!lnrpc.FileExists(cfg.WalletUnlockPasswordFile):
return nil, fmt.Errorf("wallet unlock password file %s does "+
"not exist", cfg.WalletUnlockPasswordFile)
}
// For each of the RPC listeners (REST+gRPC), we'll ensure that users
// have specified a safe combo for authentication. If not, we'll bail
// out with an error. Since we don't allow disabling TLS for gRPC

View File

@ -226,16 +226,16 @@ On FreeBSD, use gmake instead of make.
Alternatively, if one doesn't wish to use `make`, then the `go` commands can be
used directly:
```shell
cd $GOPATH/src/github.com/lightningnetwork/lnd
git pull
GO111MODULE=on go install -v ./...
cd $GOPATH/src/github.com/lightningnetwork/lnd
git pull
GO111MODULE=on go install -v ./...
```
**Tests**
To check that `lnd` was installed properly run the following command:
```
make check
```shell
make check
```
This command requires `bitcoind` (almost any version should do) to be available
@ -259,7 +259,7 @@ wallet, and the age of the earliest channels (which were created around March
The set of arguments for each of the backend modes is as follows:
## btcd Options
```
```text
btcd:
--btcd.dir= The base directory that contains the node's data, logs, configuration file, etc. (default: /Users/roasbeef/Library/Application Support/Btcd)
--btcd.rpchost= The daemon's rpc listening address. If a port is omitted, then the default port for the selected chain parameters will be used. (default: localhost)
@ -270,7 +270,7 @@ btcd:
```
## Neutrino Options
```
```text
neutrino:
-a, --neutrino.addpeer= Add a peer to connect with at startup
--neutrino.connect= Connect only to the specified peers at startup
@ -282,7 +282,7 @@ neutrino:
```
## Bitcoind Options
```
```text
bitcoind:
--bitcoind.dir= The base directory that contains the node's data, logs, configuration file, etc. (default: /Users/roasbeef/Library/Application Support/Bitcoin)
--bitcoind.rpchost= The daemon's rpc listening address. If a port is omitted, then the default port for the selected chain parameters will be used. (default: localhost)
@ -302,8 +302,8 @@ On FreeBSD, use gmake instead of make.
To install btcd, run the following commands:
Install **btcd**:
```
make btcd
```shell
make btcd
```
Alternatively, you can install [`btcd` directly from its
@ -313,8 +313,8 @@ repo](https://github.com/btcsuite/btcd).
Running the following command will create `rpc.cert` and default `btcd.conf`.
```
btcd --testnet --rpcuser=REPLACEME --rpcpass=REPLACEME
```shell
btcd --testnet --rpcuser=REPLACEME --rpcpass=REPLACEME
```
If you want to use `lnd` on testnet, `btcd` needs to first fully sync the
testnet blockchain. Depending on your hardware, this may take up to a few
@ -326,8 +326,8 @@ directly, rather than scanning blocks or BIP 158 filters for relevant items.
While `btcd` is syncing you can check on its progress using btcd's `getinfo`
RPC command:
```
btcctl --testnet --rpcuser=REPLACEME --rpcpass=REPLACEME getinfo
```shell
btcctl --testnet --rpcuser=REPLACEME --rpcpass=REPLACEME getinfo
{
"version": 120000,
"protocolversion": 70002,
@ -346,8 +346,8 @@ Additionally, you can monitor btcd's logs to track its syncing progress in real
time.
You can test your `btcd` node's connectivity using the `getpeerinfo` command:
```
btcctl --testnet --rpcuser=REPLACEME --rpcpass=REPLACEME getpeerinfo | more
```shell
btcctl --testnet --rpcuser=REPLACEME --rpcpass=REPLACEME getpeerinfo | more
```
### Running lnd using the btcd backend
@ -356,8 +356,9 @@ If you are on testnet, run this command after `btcd` has finished syncing.
Otherwise, replace `--bitcoin.testnet` with `--bitcoin.simnet`. If you are
installing `lnd` in preparation for the
[tutorial](https://dev.lightning.community/tutorial), you may skip this step.
```
lnd --bitcoin.active --bitcoin.testnet --debuglevel=debug --btcd.rpcuser=kek --btcd.rpcpass=kek --externalip=X.X.X.X
```shell
⛰ lnd --bitcoin.active --bitcoin.testnet --debuglevel=debug \
--btcd.rpcuser=kek --btcd.rpcpass=kek --externalip=X.X.X.X
```
## Using Neutrino
@ -371,8 +372,9 @@ mode. A public instance of such a node can be found at
To run lnd in neutrino mode, run `lnd` with the following arguments, (swapping
in `--bitcoin.simnet` if needed), and also your own `btcd` node if available:
```
lnd --bitcoin.active --bitcoin.testnet --debuglevel=debug --bitcoin.node=neutrino --neutrino.connect=faucet.lightning.community
```shell
⛰ lnd --bitcoin.active --bitcoin.testnet --debuglevel=debug \
--bitcoin.node=neutrino --neutrino.connect=faucet.lightning.community
```
@ -407,7 +409,7 @@ the following:
the testnet chain (alternatively, use `--bitcoind.regtest` instead).
Here's a sample `bitcoin.conf` for use with lnd:
```
```text
testnet=1
server=1
daemon=1
@ -421,8 +423,13 @@ updated with the latest blocks on testnet, run the command below to launch
`lnd.conf` to save these options, more info on that is described further
below):
```
lnd --bitcoin.active --bitcoin.testnet --debuglevel=debug --bitcoin.node=bitcoind --bitcoind.rpcuser=REPLACEME --bitcoind.rpcpass=REPLACEME --bitcoind.zmqpubrawblock=tcp://127.0.0.1:28332 --bitcoind.zmqpubrawtx=tcp://127.0.0.1:28333 --externalip=X.X.X.X
```shell
⛰ lnd --bitcoin.active --bitcoin.testnet --debuglevel=debug \
--bitcoin.node=bitcoind --bitcoind.rpcuser=REPLACEME \
--bitcoind.rpcpass=REPLACEME \
--bitcoind.zmqpubrawblock=tcp://127.0.0.1:28332 \
--bitcoind.zmqpubrawtx=tcp://127.0.0.1:28333 \
--externalip=X.X.X.X
```
*NOTE:*
@ -457,8 +464,8 @@ lnd --bitcoin.active --bitcoin.testnet --debuglevel=debug --bitcoin.node=bitcoin
# Creating a wallet
If `lnd` is being run for the first time, create a new wallet with:
```
lncli create
```shell
lncli create
```
This will prompt for a wallet password, and optionally a cipher seed
passphrase.
@ -467,6 +474,8 @@ passphrase.
recover the wallet in case of data loss. The user should write this down and
keep in a safe place.
More [information about managing wallets can be found in the wallet management
document](wallet.md).
# Macaroons
@ -528,7 +537,7 @@ at the command line, you can create an `lnd.conf`.
`~/.lnd/lnd.conf`
Here's a sample `lnd.conf` for `btcd` to get you started:
```
```text
[Application Options]
debuglevel=trace
maxpendingchannels=10

191
docs/wallet.md Normal file
View File

@ -0,0 +1,191 @@
# Wallet management
The wallet in the context of `lnd` is a database file (located in the data
directory, for example `~/.lnd/data/chain/bitcoin/mainnet/wallet.db` on Linux)
that contains all addresses and private keys for the on-chain **and** off-chain
(LN) funds.
The wallet is independent of the chain backend that is used (`bitcoind`, `btcd`
or `neutrino`) and must therefore be created as the first step after starting
up a fresh `lnd` node.
To protect the sensitive content of the wallet, the database is encrypted with
a password chosen by the user when creating the wallet (simply called "wallet
password"). `lnd` will not store that password anywhere by itself (as that would
defeat the purpose of the password) so every time `lnd` is restarted, its wallet
needs to be unlocked with that password. This can either be done [manually
through the command line](#unlocking-a-wallet) or (starting with `lnd` version
`v0.13.0-beta`) [automatically from a file](#auto-unlocking-a-wallet).
## Creating a wallet
If `lnd` is being run for the first time, create a new wallet with:
```shell
⛰ lncli create
```
This will prompt for a wallet password, and optionally a cipher seed
passphrase.
`lnd` will then print a 24 word cipher seed mnemonic, which can be used to
recover the wallet in case of data loss. The user should write this down and
keep in a safe place.
In case a node needs to be recovered from an existing seed, this can also be
done through the `create` command. Please refer to the
[recovery guide](recovery.md) for more information about recovering a node.
## Unlocking a wallet
Every time `lnd` starts up fresh (e.g. after a system restart or a version
upgrade) the user-chosen wallet password needs to be entered to unlock (decrypt)
the wallet database.
This will be indicated in `lnd`'s log with a message like this:
```text
2021-05-06 11:36:11.445 [INF] LTND: Waiting for wallet encryption password. Use `lncli create` to create a wallet, `lncli unlock` to unlock an existing wallet, or `lncli changepassword` to change the password of an existing wallet and unlock it.
```
Unlocking the password manually is as simple as running the command
```shell
⛰ lncli unlock
```
and then typing the wallet password.
## Auto-unlocking a wallet
In some situations (for example automated, cluster based setups) it can be
impractical to manually unlock the wallet every time `lnd` is restarted.
In `lnd` version `v0.13.0-beta` and later there is a configuration option to
tell the wallet to auto-unlock itself by reading the password from a file. This
can only be activated _after_ the wallet was created manually.
### Very basic example (not very secure)
This example only tries to give a basic, minimal example on how to use the
auto-unlock feature. Storing a password in a file on the same disk as the wallet
database is not in itself more secure than leaving the database unencrypted in
the first place. This example might be useful in a containerized environment
though where the secrets are mounted to a file anyway.
- Start `lnd` without the flag:
```shell
⛰ lnd --bitcoin.active --bitcoin.xxxx .....
```
- Create the wallet and write down the seed in a safe place:
```shell
⛰ lncli create
```
- Stop `lnd` again:
```shell
⛰ lncli stop
```
- Write the password to a file:
```shell
⛰ echo 'my-$up3r-Secret-Passw0rd' > /some/safe/location/password.txt
```
- Make sure the password file can only be read by our user:
```shell
⛰ chmod 0400 /some/safe/location/password.txt
```
- Start `lnd` with the auto-unlock flag:
```shell
⛰ lnd --bitcoin.active --bitcoin.xxxx ..... \
--wallet-unlock-password-file=/some/safe/location/password.txt
```
As with every command line flag, the `wallet-unlock-password-file` option can
also be added to `lnd`'s configuration file, for example:
```text
[Application Options]
debuglevel=debug
wallet-unlock-password-file=/some/safe/location/password.txt
[Bitcoin]
bitcoin.active=1
...
```
### More secure example with password manager and using a named pipe
This example is a bit more involved and requires the use of a password manager
of some sort. It will also only work on Unix like file systems that support
named pipes.
We will use the password manager [`pass`](https://www.passwordstore.org/) as an
example here but it should work similarly with other password managers.
- Start `lnd` without the flag:
```shell
⛰ lnd --bitcoin.active --bitcoin.xxxx .....
```
- Create the wallet and write down the seed in a safe place:
```shell
⛰ lncli create
```
- Stop `lnd` again:
```shell
⛰ lncli stop
```
- Store the password in `pass`:
```shell
⛰ pass insert lnd/my-wallet-password
```
- Create a startup script for starting `lnd`, for example `run-lnd.sh`:
```shell
#!/bin/bash
# Create a named pipe. As the name suggests, this is a FIFO (first in first
# out) pipe. Everything sent in can be read out again without the content
# actually being written to a disk.
mkfifo /tmp/wallet-password-pipe
# Read the password from the manager and attempt to write it to the pipe. Any
# write to a pipe will only be accepted once there is a process that reads
# from the pipe at the same time. That's why we need to run this process in
# the background (the ampersand & at the end) because it would block our
# script from continuing otherwise.
pass lnd/my-wallet-password > /tmp/wallet-password-pipe &
# Now we can start lnd.
lnd --bitcoin.active --bitcoin.xxxx ..... \
--wallet-unlock-password-file=/tmp/wallet-password-pipe
```
- Run the startup script instead of running `lnd` directly.
```shell
⛰ ./run-lnd.sh
```
## Changing the password
Changing the wallet password is possible but only while the wallet is locked.
So after restarting `lnd`, instead of using the `unlock` command, the
`changepassword` command can be used:
```shell
⛰ lncli changepassword
```
This will ask for the old/existing password and a new one. If successful, the
database is re-encrypted with the new password and then the wallet is also
unlocked in the process.
## DO NOT USE --noseedbackup on mainnet
There is a way to get rid of the need to unlock the wallet password: The
`--noseedbackup` flag.
Using that flag with **real funds (mainnet) is extremely risky for two reasons**:
1. On first startup a wallet is created automatically. The seed phrase (the 24
words needed to restore a wallet) is never shown to the user. Therefore if
the worst thing happens and the hard disk crashes or the wallet file is
deleted by accident, **THERE IS NO WAY OF GETTING THE FUNDS BACK**.
2. In addition to the seed not being known to the user, the wallet database is
also not protected. A well-known default password is chosen for the
encryption. Any user (or malware) with access to the wallet database can
steal the funds if they copy the file.
The `--noseedbackup` flag should only ever be used in a test setup, for example
on Bitcoin testnet, regtest or simnet.

66
lnd.go
View File

@ -5,6 +5,7 @@
package lnd
import (
"bytes"
"context"
"crypto/tls"
"fmt"
@ -278,7 +279,9 @@ func Main(cfg *Config, lisCfg ListenerCfg, interceptor signal.Interceptor) error
var (
walletInitParams = WalletUnlockParams{
MacResponseChan: make(chan []byte),
// In case we do auto-unlock, we need to be able to send
// into the channel without blocking so we buffer it.
MacResponseChan: make(chan []byte, 1),
}
privateWalletPw = lnwallet.DefaultPrivatePassphrase
publicWalletPw = lnwallet.DefaultPublicPassphrase
@ -475,10 +478,63 @@ func Main(cfg *Config, lisCfg ListenerCfg, interceptor signal.Interceptor) error
interceptorChain.SetWalletLocked()
}
// We wait until the user provides a password over RPC. In case lnd is
// started with the --noseedbackup flag, we use the default password
// for wallet encryption.
if !cfg.NoSeedBackup {
// If we've started in auto unlock mode, then a wallet _must_ already
// exist because we never want to enable the RPC unlocker in that case.
if cfg.WalletUnlockPasswordFile != "" && !walletExists {
return fmt.Errorf("wallet unlock password file was specified " +
"but wallet does not exist; initialize the wallet " +
"before using auto unlocking")
}
// What wallet mode are we running in? We've already made sure the no
// seed backup and auto unlock aren't both set during config parsing.
switch {
// No seed backup means we're also using the default password.
case cfg.NoSeedBackup:
// We continue normally, the default password has already been
// set above.
// A password for unlocking is provided in a file.
case cfg.WalletUnlockPasswordFile != "":
ltndLog.Infof("Attempting automatic wallet unlock with " +
"password provided in file")
pwBytes, err := ioutil.ReadFile(cfg.WalletUnlockPasswordFile)
if err != nil {
return fmt.Errorf("error reading password from file "+
"%s: %v", cfg.WalletUnlockPasswordFile, err)
}
// Remove any newlines at the end of the file. The lndinit tool
// won't ever write a newline but maybe the file was provisioned
// by another process or user.
pwBytes = bytes.TrimRight(pwBytes, "\r\n")
// We have the password now, we can ask the unlocker service to
// do the unlock for us.
unlockedWallet, unloadWalletFn, err := pwService.LoadAndUnlock(
pwBytes, 0,
)
if err != nil {
return fmt.Errorf("error unlocking wallet with "+
"password from file: %v", err)
}
defer func() {
if err := unloadWalletFn(); err != nil {
ltndLog.Errorf("Could not unload wallet: %v",
err)
}
}()
privateWalletPw = pwBytes
publicWalletPw = pwBytes
walletInitParams.Wallet = unlockedWallet
walletInitParams.UnloadWallet = unloadWalletFn
// If none of the automatic startup options are selected, we fall back
// to the default behavior of waiting for the wallet creation/unlocking
// over RPC.
default:
params, err := waitForWalletPassword(
cfg, pwService, []btcwallet.LoaderOption{loaderOpt},
interceptor.ShutdownChannel(),

View File

@ -254,6 +254,11 @@
; BE USED ON MAINNET.
; noseedbackup=true
; The full path to a file (or pipe/device) that contains the password for
; unlocking the wallet; if set, no unlocking through RPC is possible and lnd
; will exit if no wallet exists or the password is incorrect
; wallet-unlock-password-file=/tmp/example.password
; Removes all transaction history from the on-chain wallet on startup, forcing a
; full chain rescan starting at the wallet's birthday. Implements the same
; functionality as btcwallet's dropwtxmgr command. Should be set to false after

View File

@ -401,29 +401,27 @@ func (u *UnlockerService) InitWallet(ctx context.Context,
}
}
// UnlockWallet sends the password provided by the incoming UnlockWalletRequest
// over the UnlockMsgs channel in case it successfully decrypts an existing
// wallet found in the chain's wallet database directory.
func (u *UnlockerService) UnlockWallet(ctx context.Context,
in *lnrpc.UnlockWalletRequest) (*lnrpc.UnlockWalletResponse, error) {
password := in.WalletPassword
recoveryWindow := uint32(in.RecoveryWindow)
// LoadAndUnlock creates a loader for the wallet and tries to unlock the wallet
// with the given password and recovery window. If the drop wallet transactions
// flag is set, the history state drop is performed before unlocking the wallet
// yet again.
func (u *UnlockerService) LoadAndUnlock(password []byte,
recoveryWindow uint32) (*wallet.Wallet, func() error, error) {
loader, err := u.newLoader(recoveryWindow)
if err != nil {
return nil, err
return nil, nil, err
}
// Check if wallet already exists.
walletExists, err := loader.WalletExists()
if err != nil {
return nil, err
return nil, nil, err
}
if !walletExists {
// Cannot unlock a wallet that does not exist!
return nil, fmt.Errorf("wallet not found")
return nil, nil, fmt.Errorf("wallet not found")
}
// Try opening the existing wallet with the provided password.
@ -431,7 +429,7 @@ func (u *UnlockerService) UnlockWallet(ctx context.Context,
if err != nil {
// Could not open wallet, most likely this means that provided
// password was incorrect.
return nil, err
return nil, nil, err
}
// The user requested to drop their whole wallet transaction state to
@ -447,7 +445,7 @@ func (u *UnlockerService) UnlockWallet(ctx context.Context,
// wallet. If unloading fails, that error is probably more
// important to be returned to the user anyway.
if err := loader.UnloadWallet(); err != nil {
return nil, fmt.Errorf("could not unload "+
return nil, nil, fmt.Errorf("could not unload "+
"wallet (tx history drop err: %v): %v", dropErr,
err)
}
@ -455,23 +453,42 @@ func (u *UnlockerService) UnlockWallet(ctx context.Context,
// If dropping failed but unloading didn't, we'll still abort
// and inform the user.
if dropErr != nil {
return nil, dropErr
return nil, nil, dropErr
}
// All looks good, let's now open the wallet again.
unlockedWallet, err = loader.OpenExistingWallet(password, false)
if err != nil {
return nil, err
return nil, nil, err
}
}
return unlockedWallet, loader.UnloadWallet, nil
}
// UnlockWallet sends the password provided by the incoming UnlockWalletRequest
// over the UnlockMsgs channel in case it successfully decrypts an existing
// wallet found in the chain's wallet database directory.
func (u *UnlockerService) UnlockWallet(ctx context.Context,
in *lnrpc.UnlockWalletRequest) (*lnrpc.UnlockWalletResponse, error) {
password := in.WalletPassword
recoveryWindow := uint32(in.RecoveryWindow)
unlockedWallet, unloadFn, err := u.LoadAndUnlock(
password, recoveryWindow,
)
if err != nil {
return nil, err
}
// We successfully opened the wallet and pass the instance back to
// avoid it needing to be unlocked again.
walletUnlockMsg := &WalletUnlockMsg{
Passphrase: password,
RecoveryWindow: recoveryWindow,
Wallet: unlockedWallet,
UnloadWallet: loader.UnloadWallet,
UnloadWallet: unloadFn,
StatelessInit: in.StatelessInit,
}