package walletunlocker import ( "context" "crypto/rand" "errors" "fmt" "os" "time" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcwallet/wallet" "github.com/lightningnetwork/lnd/aezeed" "github.com/lightningnetwork/lnd/chanbackup" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/btcwallet" ) // 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. WalletSeed *aezeed.CipherSeed // 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 } // 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 } // 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 { // 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 chainDir string netParams *chaincfg.Params macaroonFiles []string } // New creates and returns a new UnlockerService. func New(chainDir string, params *chaincfg.Params, macaroonFiles []string) *UnlockerService { return &UnlockerService{ InitMsgs: make(chan *WalletInitMsg, 1), UnlockMsgs: make(chan *WalletUnlockMsg, 1), chainDir: chainDir, netParams: params, macaroonFiles: macaroonFiles, } } // 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(ctx 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. netDir := btcwallet.NetworkDir(u.chainDir, u.netParams) loader := wallet.NewLoader(u.netParams, netDir, 0) 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.KeyDerivationVersion, &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: []string(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 = chanbackup.PackedMulti( 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) } // 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. netDir := btcwallet.NetworkDir(u.chainDir, u.netParams) loader := wallet.NewLoader(u.netParams, netDir, uint32(recoveryWindow)) 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 that the wallet doesn't already exist. So // 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 } // With the cipher seed deciphered, and the auth service created, we'll // now send over the wallet password and the seed. This will allow the // daemon to initialize itself and startup. initMsg := &WalletInitMsg{ Passphrase: password, WalletSeed: cipherSeed, RecoveryWindow: uint32(recoveryWindow), } // 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 } u.InitMsgs <- initMsg return &lnrpc.InitWalletResponse{}, 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) netDir := btcwallet.NetworkDir(u.chainDir, u.netParams) loader := wallet.NewLoader(u.netParams, netDir, recoveryWindow) // Check if wallet already exists. walletExists, err := loader.WalletExists() if err != nil { return nil, err } if !walletExists { // Cannot unlock a wallet that does not exist! return 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, 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, } // 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 was 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 return &lnrpc.UnlockWalletResponse{}, nil } // 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) { netDir := btcwallet.NetworkDir(u.chainDir, u.netParams) loader := wallet.NewLoader(u.netParams, netDir, 0) // 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 } // 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 } } // 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) } // Finally, send the new password across the UnlockPasswords channel to // automatically unlock the wallet. u.UnlockMsgs <- &WalletUnlockMsg{Passphrase: in.NewPassword} return &lnrpc.ChangePasswordResponse{}, nil } // 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 }