mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 18:10:34 +01:00
ad0dbb1c15
Because the wallet loader sets its internal database reference to nil when unloading a wallet, it cannot be loaded again in case of a remote DB such as etcd or postgres. To avoid running into a nil pointer panic we just re-create the loader as well before opening the wallet.
927 lines
31 KiB
Go
927 lines
31 KiB
Go
package walletunlocker
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
|
"github.com/btcsuite/btcd/chaincfg"
|
|
"github.com/btcsuite/btcwallet/waddrmgr"
|
|
"github.com/btcsuite/btcwallet/wallet"
|
|
"github.com/lightningnetwork/lnd/aezeed"
|
|
"github.com/lightningnetwork/lnd/chanbackup"
|
|
"github.com/lightningnetwork/lnd/keychain"
|
|
"github.com/lightningnetwork/lnd/kvdb"
|
|
"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")
|
|
)
|
|
|
|
// WalletUnlockParams holds the variables used to parameterize the unlocking of
|
|
// lnd's wallet after it has already been created.
|
|
type WalletUnlockParams struct {
|
|
// Password is the public and private wallet passphrase.
|
|
Password []byte
|
|
|
|
// Birthday specifies the approximate time that this wallet was created.
|
|
// This is used to bound any rescans on startup.
|
|
Birthday time.Time
|
|
|
|
// RecoveryWindow specifies the address lookahead when entering recovery
|
|
// mode. A recovery will be attempted if this value is non-zero.
|
|
RecoveryWindow uint32
|
|
|
|
// Wallet is the loaded and unlocked Wallet. This is returned
|
|
// from the unlocker service to avoid it being unlocked twice (once in
|
|
// the unlocker service to check if the password is correct and again
|
|
// later when lnd actually uses it). Because unlocking involves scrypt
|
|
// which is resource intensive, we want to avoid doing it twice.
|
|
Wallet *wallet.Wallet
|
|
|
|
// ChansToRestore a set of static channel backups that should be
|
|
// restored before the main server instance starts up.
|
|
ChansToRestore ChannelsToRecover
|
|
|
|
// 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
|
|
|
|
// MacRootKey is the 32 byte macaroon root key specified by the user
|
|
// during wallet initialization.
|
|
MacRootKey []byte
|
|
}
|
|
|
|
// ChannelsToRecover wraps any set of packed (serialized+encrypted) channel
|
|
// back ups together. These can be passed in when unlocking the wallet, or
|
|
// creating a new wallet for the first time with an existing seed.
|
|
type ChannelsToRecover struct {
|
|
// PackedMultiChanBackup is an encrypted and serialized multi-channel
|
|
// backup.
|
|
PackedMultiChanBackup chanbackup.PackedMulti
|
|
|
|
// PackedSingleChanBackups is a series of encrypted and serialized
|
|
// single-channel backup for one or more channels.
|
|
PackedSingleChanBackups chanbackup.PackedSingles
|
|
}
|
|
|
|
// WalletInitMsg is a message sent by the UnlockerService when a user wishes to
|
|
// set up the internal wallet for the first time. The user MUST provide a
|
|
// passphrase, but is also able to provide their own source of entropy. If
|
|
// provided, then this source of entropy will be used to generate the wallet's
|
|
// HD seed. Otherwise, the wallet will generate one itself.
|
|
type WalletInitMsg struct {
|
|
// Passphrase is the passphrase that will be used to encrypt the wallet
|
|
// itself. This MUST be at least 8 characters.
|
|
Passphrase []byte
|
|
|
|
// WalletSeed is the deciphered cipher seed that the wallet should use
|
|
// to initialize itself. The seed might be nil if the wallet should be
|
|
// created from an extended master root key instead.
|
|
WalletSeed *aezeed.CipherSeed
|
|
|
|
// WalletExtendedKey is the wallet's extended master root key that
|
|
// should be used instead of the seed, if non-nil. The extended key is
|
|
// mutually exclusive to the wallet seed, but one of both is always set.
|
|
WalletExtendedKey *hdkeychain.ExtendedKey
|
|
|
|
// ExtendedKeyBirthday is the birthday of a wallet that's being restored
|
|
// through an extended key instead of an aezeed.
|
|
ExtendedKeyBirthday time.Time
|
|
|
|
// WatchOnlyAccounts is a map of scoped account extended public keys
|
|
// that should be imported to create a watch-only wallet.
|
|
WatchOnlyAccounts map[waddrmgr.ScopedIndex]*hdkeychain.ExtendedKey
|
|
|
|
// WatchOnlyBirthday is the birthday of the master root key the above
|
|
// watch-only account xpubs were derived from.
|
|
WatchOnlyBirthday time.Time
|
|
|
|
// WatchOnlyMasterFingerprint is the fingerprint of the master root key
|
|
// the above watch-only account xpubs were derived from.
|
|
WatchOnlyMasterFingerprint uint32
|
|
|
|
// RecoveryWindow is the address look-ahead used when restoring a seed
|
|
// with existing funds. A recovery window zero indicates that no
|
|
// recovery should be attempted, such as after the wallet's initial
|
|
// creation.
|
|
RecoveryWindow uint32
|
|
|
|
// 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
|
|
|
|
// MacRootKey is the 32 byte macaroon root key specified by the user
|
|
// during wallet initialization.
|
|
MacRootKey []byte
|
|
}
|
|
|
|
// WalletUnlockMsg is a message sent by the UnlockerService when a user wishes
|
|
// to unlock the internal wallet after initial setup. The user can optionally
|
|
// specify a recovery window, which will resume an interrupted rescan for used
|
|
// addresses.
|
|
type WalletUnlockMsg struct {
|
|
// Passphrase is the passphrase that will be used to encrypt the wallet
|
|
// itself. This MUST be at least 8 characters.
|
|
Passphrase []byte
|
|
|
|
// RecoveryWindow is the address look-ahead used when restoring a seed
|
|
// with existing funds. A recovery window zero indicates that no
|
|
// recovery should be attempted, such as after the wallet's initial
|
|
// creation, but before any addresses have been created.
|
|
RecoveryWindow uint32
|
|
|
|
// Wallet is the loaded and unlocked Wallet. This is returned through
|
|
// the channel to avoid it being unlocked twice (once to check if the
|
|
// password is correct, here in the WalletUnlocker and again later when
|
|
// lnd actually uses it). Because unlocking involves scrypt which is
|
|
// resource intensive, we want to avoid doing it twice.
|
|
Wallet *wallet.Wallet
|
|
|
|
// ChanBackups a set of static channel backups that should be received
|
|
// after the wallet has been unlocked.
|
|
ChanBackups ChannelsToRecover
|
|
|
|
// 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
|
|
// with a password for wallet encryption at startup. Additionally, during
|
|
// initial setup, users can provide their own source of entropy which will be
|
|
// used to generate the seed that's ultimately used within the wallet.
|
|
type UnlockerService struct {
|
|
// Required by the grpc-gateway/v2 library for forward compatibility.
|
|
lnrpc.UnimplementedWalletUnlockerServer
|
|
|
|
// InitMsgs is a channel that carries all wallet init messages.
|
|
InitMsgs chan *WalletInitMsg
|
|
|
|
// UnlockMsgs is a channel where unlock parameters provided by the rpc
|
|
// client to be used to unlock and decrypt an existing wallet will be
|
|
// sent.
|
|
UnlockMsgs chan *WalletUnlockMsg
|
|
|
|
// MacResponseChan is the channel for sending back the admin macaroon to
|
|
// the WalletUnlocker service.
|
|
MacResponseChan chan []byte
|
|
|
|
netParams *chaincfg.Params
|
|
|
|
// 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
|
|
|
|
// resetWalletTransactions indicates that the wallet state should be
|
|
// reset on unlock to force a full chain rescan.
|
|
resetWalletTransactions bool
|
|
|
|
// LoaderOpts holds the functional options for the wallet loader.
|
|
loaderOpts []btcwallet.LoaderOption
|
|
|
|
// macaroonDB is an instance of a database backend that stores all
|
|
// macaroon root keys. This will be nil on initialization and must be
|
|
// set using the SetMacaroonDB method as soon as it's available.
|
|
macaroonDB kvdb.Backend
|
|
}
|
|
|
|
// New creates and returns a new UnlockerService.
|
|
func New(params *chaincfg.Params, macaroonFiles []string,
|
|
resetWalletTransactions bool,
|
|
loaderOpts []btcwallet.LoaderOption) *UnlockerService {
|
|
|
|
return &UnlockerService{
|
|
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),
|
|
netParams: params,
|
|
macaroonFiles: macaroonFiles,
|
|
resetWalletTransactions: resetWalletTransactions,
|
|
loaderOpts: loaderOpts,
|
|
}
|
|
}
|
|
|
|
// SetLoaderOpts can be used to inject wallet loader options after the unlocker
|
|
// service has been hooked to the main RPC server.
|
|
func (u *UnlockerService) SetLoaderOpts(loaderOpts []btcwallet.LoaderOption) {
|
|
u.loaderOpts = loaderOpts
|
|
}
|
|
|
|
// SetMacaroonDB can be used to inject the macaroon database after the unlocker
|
|
// service has been hooked to the main RPC server.
|
|
func (u *UnlockerService) SetMacaroonDB(macaroonDB kvdb.Backend) {
|
|
u.macaroonDB = macaroonDB
|
|
}
|
|
|
|
func (u *UnlockerService) newLoader(recoveryWindow uint32) (*wallet.Loader,
|
|
error) {
|
|
|
|
return btcwallet.NewWalletLoader(
|
|
u.netParams, recoveryWindow, u.loaderOpts...,
|
|
)
|
|
}
|
|
|
|
// WalletExists returns whether a wallet exists on the file path the
|
|
// UnlockerService is using.
|
|
func (u *UnlockerService) WalletExists() (bool, error) {
|
|
loader, err := u.newLoader(0)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return loader.WalletExists()
|
|
}
|
|
|
|
// GenSeed is the first method that should be used to instantiate a new lnd
|
|
// instance. This method allows a caller to generate a new aezeed cipher seed
|
|
// given an optional passphrase. If provided, the passphrase will be necessary
|
|
// to decrypt the cipherseed to expose the internal wallet seed.
|
|
//
|
|
// Once the cipherseed is obtained and verified by the user, the InitWallet
|
|
// method should be used to commit the newly generated seed, and create the
|
|
// wallet.
|
|
func (u *UnlockerService) GenSeed(_ context.Context,
|
|
in *lnrpc.GenSeedRequest) (*lnrpc.GenSeedResponse, error) {
|
|
|
|
// Before we start, we'll ensure that the wallet hasn't already created
|
|
// so we don't show a *new* seed to the user if one already exists.
|
|
loader, err := u.newLoader(0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
walletExists, err := loader.WalletExists()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if walletExists {
|
|
return nil, fmt.Errorf("wallet already exists")
|
|
}
|
|
|
|
var entropy [aezeed.EntropySize]byte
|
|
|
|
switch {
|
|
// If the user provided any entropy, then we'll make sure it's sized
|
|
// properly.
|
|
case len(in.SeedEntropy) != 0 && len(in.SeedEntropy) != aezeed.EntropySize:
|
|
return nil, fmt.Errorf("incorrect entropy length: expected "+
|
|
"16 bytes, instead got %v bytes", len(in.SeedEntropy))
|
|
|
|
// If the user provided the correct number of bytes, then we'll copy it
|
|
// over into our buffer for usage.
|
|
case len(in.SeedEntropy) == aezeed.EntropySize:
|
|
copy(entropy[:], in.SeedEntropy[:])
|
|
|
|
// Otherwise, we'll generate a fresh new set of bytes to use as entropy
|
|
// to generate the seed.
|
|
default:
|
|
if _, err := rand.Read(entropy[:]); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Now that we have our set of entropy, we'll create a new cipher seed
|
|
// instance.
|
|
//
|
|
cipherSeed, err := aezeed.New(
|
|
keychain.CurrentKeyDerivationVersion, &entropy, time.Now(),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// With our raw cipher seed obtained, we'll convert it into an encoded
|
|
// mnemonic using the user specified pass phrase.
|
|
mnemonic, err := cipherSeed.ToMnemonic(in.AezeedPassphrase)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Additionally, we'll also obtain the raw enciphered cipher seed as
|
|
// well to return to the user.
|
|
encipheredSeed, err := cipherSeed.Encipher(in.AezeedPassphrase)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &lnrpc.GenSeedResponse{
|
|
CipherSeedMnemonic: mnemonic[:],
|
|
EncipheredSeed: encipheredSeed[:],
|
|
}, nil
|
|
}
|
|
|
|
// extractChanBackups is a helper function that extracts the set of channel
|
|
// backups from the proto into a format that we'll pass to higher level
|
|
// sub-systems.
|
|
func extractChanBackups(chanBackups *lnrpc.ChanBackupSnapshot) *ChannelsToRecover {
|
|
// If there aren't any populated channel backups, then we can exit
|
|
// early as there's nothing to extract.
|
|
if chanBackups == nil || (chanBackups.SingleChanBackups == nil &&
|
|
chanBackups.MultiChanBackup == nil) {
|
|
return nil
|
|
}
|
|
|
|
// Now that we know there's at least a single back up populated, we'll
|
|
// extract the multi-chan backup (if it's there).
|
|
var backups ChannelsToRecover
|
|
if chanBackups.MultiChanBackup != nil {
|
|
multiBackup := chanBackups.MultiChanBackup
|
|
backups.PackedMultiChanBackup = multiBackup.MultiChanBackup
|
|
}
|
|
|
|
if chanBackups.SingleChanBackups == nil {
|
|
return &backups
|
|
}
|
|
|
|
// Finally, we can extract all the single chan backups as well.
|
|
for _, backup := range chanBackups.SingleChanBackups.ChanBackups {
|
|
singleChanBackup := backup.ChanBackup
|
|
|
|
backups.PackedSingleChanBackups = append(
|
|
backups.PackedSingleChanBackups, singleChanBackup,
|
|
)
|
|
}
|
|
|
|
return &backups
|
|
}
|
|
|
|
// InitWallet is used when lnd is starting up for the first time to fully
|
|
// initialize the daemon and its internal wallet. At the very least a wallet
|
|
// password must be provided. This will be used to encrypt sensitive material
|
|
// on disk.
|
|
//
|
|
// In the case of a recovery scenario, the user can also specify their aezeed
|
|
// mnemonic and passphrase. If set, then the daemon will use this prior state
|
|
// to initialize its internal wallet.
|
|
//
|
|
// Alternatively, this can be used along with the GenSeed RPC to obtain a
|
|
// seed, then present it to the user. Once it has been verified by the user,
|
|
// the seed can be fed into this RPC in order to commit the new wallet.
|
|
func (u *UnlockerService) InitWallet(ctx context.Context,
|
|
in *lnrpc.InitWalletRequest) (*lnrpc.InitWalletResponse, error) {
|
|
|
|
// Make sure the password meets our constraints.
|
|
password := in.WalletPassword
|
|
if err := ValidatePassword(password); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Require that the recovery window be non-negative.
|
|
recoveryWindow := in.RecoveryWindow
|
|
if recoveryWindow < 0 {
|
|
return nil, fmt.Errorf("recovery window %d must be "+
|
|
"non-negative", recoveryWindow)
|
|
}
|
|
|
|
// Ensure that the macaroon root key is *exactly* 32-bytes.
|
|
macaroonRootKey := in.MacaroonRootKey
|
|
if len(macaroonRootKey) > 0 &&
|
|
len(macaroonRootKey) != macaroons.RootKeyLen {
|
|
|
|
return nil, fmt.Errorf("macaroon root key must be exactly "+
|
|
"%v bytes, is instead %v",
|
|
macaroons.RootKeyLen, len(macaroonRootKey),
|
|
)
|
|
}
|
|
|
|
// We'll then open up the directory that will be used to store the
|
|
// wallet's files so we can check if the wallet already exists.
|
|
loader, err := u.newLoader(uint32(recoveryWindow))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
walletExists, err := loader.WalletExists()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If the wallet already exists, then we'll exit early as we can't
|
|
// create the wallet if it already exists!
|
|
if walletExists {
|
|
return nil, fmt.Errorf("wallet already exists")
|
|
}
|
|
|
|
// At this point, we know the wallet doesn't already exist so we can
|
|
// prepare the message that we'll send over the channel later.
|
|
initMsg := &WalletInitMsg{
|
|
Passphrase: password,
|
|
RecoveryWindow: uint32(recoveryWindow),
|
|
StatelessInit: in.StatelessInit,
|
|
MacRootKey: macaroonRootKey,
|
|
}
|
|
|
|
// There are two supported ways to initialize the wallet. Either from
|
|
// the aezeed or the final extended master key directly.
|
|
switch {
|
|
// Don't allow the user to specify both as that would be ambiguous.
|
|
case len(in.CipherSeedMnemonic) > 0 && len(in.ExtendedMasterKey) > 0:
|
|
return nil, fmt.Errorf("cannot specify both the cipher " +
|
|
"seed mnemonic and the extended master key")
|
|
|
|
// The aezeed is the preferred and default way of initializing a wallet.
|
|
case len(in.CipherSeedMnemonic) > 0:
|
|
// We'll map the user provided aezeed and passphrase into a
|
|
// decoded cipher seed instance.
|
|
var mnemonic aezeed.Mnemonic
|
|
copy(mnemonic[:], in.CipherSeedMnemonic)
|
|
|
|
// If we're unable to map it back into the ciphertext, then
|
|
// either the mnemonic is wrong, or the passphrase is wrong.
|
|
cipherSeed, err := mnemonic.ToCipherSeed(in.AezeedPassphrase)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
initMsg.WalletSeed = cipherSeed
|
|
|
|
// To support restoring a wallet where the seed isn't known or a wallet
|
|
// created externally to lnd, we also allow the extended master key
|
|
// (xprv) to be imported directly. This is what'll be stored in the
|
|
// btcwallet database anyway.
|
|
case len(in.ExtendedMasterKey) > 0:
|
|
extendedKey, err := hdkeychain.NewKeyFromString(
|
|
in.ExtendedMasterKey,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// The on-chain wallet of lnd is going to derive keys based on
|
|
// the BIP49/84 key derivation paths from this root key. To make
|
|
// sure we use default derivation paths, we want to avoid
|
|
// deriving keys from something other than the master key (at
|
|
// depth 0, denoted with "m/" in BIP32 notation).
|
|
if extendedKey.Depth() != 0 {
|
|
return nil, fmt.Errorf("extended master key must " +
|
|
"be at depth 0 not a child key")
|
|
}
|
|
|
|
// Because we need the master key (at depth 0), it must be an
|
|
// extended private key as the first levels of BIP49/84
|
|
// derivation paths are hardened, which isn't possible with
|
|
// extended public keys.
|
|
if !extendedKey.IsPrivate() {
|
|
return nil, fmt.Errorf("extended master key must " +
|
|
"contain private keys")
|
|
}
|
|
|
|
// To avoid using the wrong master key, we check that it was
|
|
// issued for the correct network. This will cause problems if
|
|
// someone tries to import a "new" BIP84 zprv key because with
|
|
// this we only support the "legacy" zprv prefix. But it is
|
|
// trivial to convert between those formats, as long as the user
|
|
// knows what they're doing.
|
|
if !extendedKey.IsForNet(u.netParams) {
|
|
return nil, fmt.Errorf("extended master key must be "+
|
|
"for network %s", u.netParams.Name)
|
|
}
|
|
|
|
// When importing a wallet from its extended private key we
|
|
// don't know the birthday as that information is not encoded in
|
|
// that format. We therefore must set an arbitrary date to start
|
|
// rescanning at if the user doesn't provide an explicit value
|
|
// for it. Since lnd only uses SegWit addresses, we pick the
|
|
// date of the first block that contained SegWit transactions
|
|
// (481824).
|
|
initMsg.ExtendedKeyBirthday = time.Date(
|
|
2017, time.August, 24, 1, 57, 37, 0, time.UTC,
|
|
)
|
|
if in.ExtendedMasterKeyBirthdayTimestamp != 0 {
|
|
initMsg.ExtendedKeyBirthday = time.Unix(
|
|
int64(in.ExtendedMasterKeyBirthdayTimestamp), 0,
|
|
)
|
|
}
|
|
|
|
initMsg.WalletExtendedKey = extendedKey
|
|
|
|
// The third option for creating a wallet is the watch-only mode:
|
|
// Instead of providing the master root key directly, each individual
|
|
// account is passed as an extended public key only. Because of the
|
|
// hardened derivation path up to the account (depth 3), it is not
|
|
// possible to create a master root extended _public_ key. Therefore, an
|
|
// xpub must be derived and passed into the unlocker for _every_ account
|
|
// lnd expects.
|
|
case in.WatchOnly != nil && len(in.WatchOnly.Accounts) > 0:
|
|
initMsg.WatchOnlyAccounts = make(
|
|
map[waddrmgr.ScopedIndex]*hdkeychain.ExtendedKey,
|
|
len(in.WatchOnly.Accounts),
|
|
)
|
|
|
|
for _, acct := range in.WatchOnly.Accounts {
|
|
scopedIndex := waddrmgr.ScopedIndex{
|
|
Scope: waddrmgr.KeyScope{
|
|
Purpose: acct.Purpose,
|
|
Coin: acct.CoinType,
|
|
},
|
|
Index: acct.Account,
|
|
}
|
|
acctKey, err := hdkeychain.NewKeyFromString(acct.Xpub)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing xpub "+
|
|
"%v: %v", acct.Xpub, err)
|
|
}
|
|
|
|
// Just to make sure the user is doing the right thing,
|
|
// we expect the public key to be at derivation depth
|
|
// three (which is the account level) and the key not to
|
|
// contain any private key material.
|
|
if acctKey.Depth() != 3 {
|
|
return nil, fmt.Errorf("xpub must be at " +
|
|
"depth 3")
|
|
}
|
|
if acctKey.IsPrivate() {
|
|
return nil, fmt.Errorf("xpub is not really " +
|
|
"an xpub, contains private key")
|
|
}
|
|
|
|
initMsg.WatchOnlyAccounts[scopedIndex] = acctKey
|
|
}
|
|
|
|
// When importing a wallet from its extended public keys we
|
|
// don't know the birthday as that information is not encoded in
|
|
// that format. We therefore must set an arbitrary date to start
|
|
// rescanning at if the user doesn't provide an explicit value
|
|
// for it. Since lnd only uses SegWit addresses, we pick the
|
|
// date of the first block that contained SegWit transactions
|
|
// (481824).
|
|
initMsg.WatchOnlyBirthday = time.Date(
|
|
2017, time.August, 24, 1, 57, 37, 0, time.UTC,
|
|
)
|
|
if in.WatchOnly.MasterKeyBirthdayTimestamp != 0 {
|
|
initMsg.WatchOnlyBirthday = time.Unix(
|
|
int64(in.WatchOnly.MasterKeyBirthdayTimestamp),
|
|
0,
|
|
)
|
|
}
|
|
|
|
// No key material was set, no wallet can be created.
|
|
default:
|
|
return nil, fmt.Errorf("must either specify cipher seed " +
|
|
"mnemonic or the extended master key")
|
|
}
|
|
|
|
// Before we return the unlock payload, we'll check if we can extract
|
|
// any channel backups to pass up to the higher level sub-system.
|
|
chansToRestore := extractChanBackups(in.ChannelBackups)
|
|
if chansToRestore != nil {
|
|
initMsg.ChanBackups = *chansToRestore
|
|
}
|
|
|
|
// 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
|
|
|
|
case <-ctx.Done():
|
|
return nil, ErrUnlockTimeout
|
|
}
|
|
|
|
case <-ctx.Done():
|
|
return nil, ErrUnlockTimeout
|
|
}
|
|
}
|
|
|
|
// 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, nil, err
|
|
}
|
|
|
|
// Check if wallet already exists.
|
|
walletExists, err := loader.WalletExists()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if !walletExists {
|
|
// Cannot unlock a wallet that does not exist!
|
|
return nil, nil, fmt.Errorf("wallet not found")
|
|
}
|
|
|
|
// Try opening the existing wallet with the provided password.
|
|
unlockedWallet, err := loader.OpenExistingWallet(password, false)
|
|
if err != nil {
|
|
// Could not open wallet, most likely this means that provided
|
|
// password was incorrect.
|
|
return nil, nil, err
|
|
}
|
|
|
|
// The user requested to drop their whole wallet transaction state to
|
|
// force a full chain rescan for wallet addresses. Dropping the state
|
|
// only properly takes effect after opening the wallet. That's why we
|
|
// start, drop, stop and start again.
|
|
if u.resetWalletTransactions {
|
|
dropErr := wallet.DropTransactionHistory(
|
|
unlockedWallet.Database(), true,
|
|
)
|
|
|
|
// Even if dropping the history fails, we'll want to unload the
|
|
// 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, nil, fmt.Errorf("could not unload "+
|
|
"wallet (tx history drop err: %v): %v", dropErr,
|
|
err)
|
|
}
|
|
|
|
// If dropping failed but unloading didn't, we'll still abort
|
|
// and inform the user.
|
|
if dropErr != nil {
|
|
return nil, nil, dropErr
|
|
}
|
|
|
|
// All looks good, let's now open the wallet again. The loader
|
|
// was unloaded and might have removed its remote DB connection,
|
|
// so let's re-create it as well.
|
|
loader, err = u.newLoader(recoveryWindow)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
unlockedWallet, err = loader.OpenExistingWallet(password, false)
|
|
if err != nil {
|
|
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: unloadFn,
|
|
StatelessInit: in.StatelessInit,
|
|
}
|
|
|
|
// Before we return the unlock payload, we'll check if we can extract
|
|
// any channel backups to pass up to the higher level sub-system.
|
|
chansToRestore := extractChanBackups(in.ChannelBackups)
|
|
if chansToRestore != nil {
|
|
walletUnlockMsg.ChanBackups = *chansToRestore
|
|
}
|
|
|
|
// 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.
|
|
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
|
|
|
|
case <-ctx.Done():
|
|
return nil, ErrUnlockTimeout
|
|
}
|
|
|
|
case <-ctx.Done():
|
|
return nil, ErrUnlockTimeout
|
|
}
|
|
}
|
|
|
|
// ChangePassword changes the password of the wallet and sends the new password
|
|
// across the UnlockPasswords channel to automatically unlock the wallet if
|
|
// successful.
|
|
func (u *UnlockerService) ChangePassword(ctx context.Context,
|
|
in *lnrpc.ChangePasswordRequest) (*lnrpc.ChangePasswordResponse, error) {
|
|
|
|
loader, err := u.newLoader(0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// First, we'll make sure the wallet exists for the specific chain and
|
|
// network.
|
|
walletExists, err := loader.WalletExists()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !walletExists {
|
|
return nil, errors.New("wallet not found")
|
|
}
|
|
|
|
publicPw := in.CurrentPassword
|
|
privatePw := in.CurrentPassword
|
|
|
|
// If the current password is blank, we'll assume the user is coming
|
|
// from a --noseedbackup state, so we'll use the default passwords.
|
|
if len(in.CurrentPassword) == 0 {
|
|
publicPw = lnwallet.DefaultPublicPassphrase
|
|
privatePw = lnwallet.DefaultPrivatePassphrase
|
|
}
|
|
|
|
// Make sure the new password meets our constraints.
|
|
if err := ValidatePassword(in.NewPassword); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Load the existing wallet in order to proceed with the password change.
|
|
w, err := loader.OpenExistingWallet(publicPw, false)
|
|
if err != nil {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Attempt to change both the public and private passphrases for the
|
|
// wallet. This will be done atomically in order to prevent one
|
|
// passphrase change from being successful and not the other.
|
|
err = w.ChangePassphrases(
|
|
publicPw, in.NewPassword, privatePw, in.NewPassword,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to change wallet passphrase: "+
|
|
"%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.
|
|
rootKeyStore, err := macaroons.NewRootKeyStorage(u.macaroonDB)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
macaroonService, err := macaroons.NewService(
|
|
rootKeyStore, "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.
|
|
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
|
|
|
|
case <-ctx.Done():
|
|
return nil, ErrUnlockTimeout
|
|
}
|
|
|
|
case <-ctx.Done():
|
|
return nil, ErrUnlockTimeout
|
|
}
|
|
}
|
|
|
|
// ValidatePassword assures the password meets all of our constraints.
|
|
func ValidatePassword(password []byte) error {
|
|
// Passwords should have a length of at least 8 characters.
|
|
if len(password) < 8 {
|
|
return errors.New("password must have at least 8 characters")
|
|
}
|
|
|
|
return nil
|
|
}
|