diff --git a/config.go b/config.go index 0a23839c..b8fd7862 100644 --- a/config.go +++ b/config.go @@ -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."` @@ -687,6 +688,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 @@ -1278,6 +1282,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 diff --git a/lnd.go b/lnd.go index 7c7c80c4..81831ffd 100644 --- a/lnd.go +++ b/lnd.go @@ -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(), diff --git a/sample-lnd.conf b/sample-lnd.conf index 2d63c619..5224675b 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -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