lnd+walletunlocker: implement ChangePassword RPC

This commit is contained in:
Wilmer Paulino 2018-04-20 03:14:41 -04:00
parent fc6505a0ff
commit 58a3419283
No known key found for this signature in database
GPG Key ID: 6DF57B9F9514972F
3 changed files with 221 additions and 27 deletions

22
lnd.go

@ -879,16 +879,25 @@ func waitForWalletPassword(grpcEndpoints, restEndpoints []string,
serverOpts []grpc.ServerOption, proxyOpts []grpc.DialOption,
tlsConf *tls.Config) (*WalletUnlockParams, error) {
// Set up a new PasswordService, which will listen
// for passwords provided over RPC.
// Set up a new PasswordService, which will listen for passwords
// provided over RPC.
grpcServer := grpc.NewServer(serverOpts...)
chainConfig := cfg.Bitcoin
if registeredChains.PrimaryChain() == 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.
macaroonFiles := []string{
filepath.Join(macaroonDatabaseDir, macaroons.DBFilename),
cfg.AdminMacPath, cfg.ReadMacPath, cfg.InvoiceMacPath,
}
pwService := walletunlocker.New(
chainConfig.ChainDir, activeNetParams.Params,
chainConfig.ChainDir, activeNetParams.Params, macaroonFiles,
)
lnrpc.RegisterWalletUnlockerServer(grpcServer, pwService)
@ -951,9 +960,10 @@ func waitForWalletPassword(grpcEndpoints, restEndpoints []string,
wg.Wait()
// Wait for user to provide the password.
ltndLog.Infof("Waiting for wallet encryption password. " +
"Use `lncli create` to create wallet, or " +
"`lncli unlock` to unlock already created wallet.")
ltndLog.Infof("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.")
// We currently don't distinguish between getting a password to be used
// for creation or unlocking, as a new wallet db will be created if

@ -2,11 +2,14 @@ package walletunlocker
import (
"crypto/rand"
"errors"
"fmt"
"os"
"time"
"github.com/lightningnetwork/lnd/aezeed"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
"github.com/roasbeef/btcd/chaincfg"
"github.com/roasbeef/btcwallet/wallet"
@ -66,15 +69,19 @@ type UnlockerService struct {
chainDir string
netParams *chaincfg.Params
macaroonFiles []string
}
// New creates and returns a new UnlockerService.
func New(chainDir string, params *chaincfg.Params) *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,
}
}
@ -168,12 +175,10 @@ func (u *UnlockerService) GenSeed(ctx context.Context,
func (u *UnlockerService) InitWallet(ctx context.Context,
in *lnrpc.InitWalletRequest) (*lnrpc.InitWalletResponse, error) {
// Require the provided password to have a length of at least 8
// characters.
// Make sure the password meets our constraints.
password := in.WalletPassword
if len(password) < 8 {
return nil, fmt.Errorf("password must have " +
"at least 8 characters")
if err := validatePassword(password); err != nil {
return nil, err
}
// Require that the recovery window be non-negative.
@ -276,3 +281,85 @@ func (u *UnlockerService) UnlockWallet(ctx context.Context,
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 --noencryptwallet 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 {
if err := os.Remove(file); err != nil {
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
}

@ -64,10 +64,9 @@ func TestGenSeed(t *testing.T) {
if err != nil {
t.Fatalf("unable to create temp directory: %v", err)
}
defer func() {
os.RemoveAll(testDir)
}()
service := walletunlocker.New(testDir, testNetParams)
defer os.RemoveAll(testDir)
service := walletunlocker.New(testDir, testNetParams, nil)
// Now that the service has been created, we'll ask it to generate a
// new seed for us given a test passphrase.
@ -108,7 +107,7 @@ func TestGenSeedGenerateEntropy(t *testing.T) {
defer func() {
os.RemoveAll(testDir)
}()
service := walletunlocker.New(testDir, testNetParams)
service := walletunlocker.New(testDir, testNetParams, nil)
// Now that the service has been created, we'll ask it to generate a
// new seed for us given a test passphrase. Note that we don't actually
@ -148,7 +147,7 @@ func TestGenSeedInvalidEntropy(t *testing.T) {
defer func() {
os.RemoveAll(testDir)
}()
service := walletunlocker.New(testDir, testNetParams)
service := walletunlocker.New(testDir, testNetParams, nil)
// Now that the service has been created, we'll ask it to generate a
// new seed for us given a test passphrase. However, we'll be using an
@ -186,7 +185,7 @@ func TestInitWallet(t *testing.T) {
}()
// Create new UnlockerService.
service := walletunlocker.New(testDir, testNetParams)
service := walletunlocker.New(testDir, testNetParams, nil)
// Once we have the unlocker service created, we'll now instantiate a
// new cipher seed instance.
@ -287,7 +286,7 @@ func TestCreateWalletInvalidEntropy(t *testing.T) {
}()
// Create new UnlockerService.
service := walletunlocker.New(testDir, testNetParams)
service := walletunlocker.New(testDir, testNetParams, nil)
// We'll attempt to init the wallet with an invalid cipher seed and
// passphrase.
@ -320,7 +319,7 @@ func TestUnlockWallet(t *testing.T) {
}()
// Create new UnlockerService.
service := walletunlocker.New(testDir, testNetParams)
service := walletunlocker.New(testDir, testNetParams, nil)
ctx := context.Background()
req := &lnrpc.UnlockWalletRequest{
@ -368,3 +367,101 @@ func TestUnlockWallet(t *testing.T) {
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) {
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)
// Create some files that will act as macaroon files that should be
// deleted after a password change is successful.
var tempFiles []string
for i := 0; i < 3; i++ {
file, err := ioutil.TempFile(testDir, "")
if err != nil {
t.Fatalf("unable to create temp file: %v", err)
}
tempFiles = append(tempFiles, file.Name())
file.Close()
}
// Create a new UnlockerService with our temp files.
service := walletunlocker.New(testDir, testNetParams, tempFiles)
ctx := context.Background()
newPassword := []byte("hunter2???")
req := &lnrpc.ChangePasswordRequest{
CurrentPassword: testPassword,
NewPassword: newPassword,
}
// 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")
}
// Create a wallet to test changing the password.
createTestWallet(t, testDir, testNetParams)
// Attempting to change the wallet's password using an incorrect
// current password should fail.
wrongReq := &lnrpc.ChangePasswordRequest{
CurrentPassword: []byte("wrong-ofc"),
NewPassword: newPassword,
}
_, err = service.ChangePassword(ctx, wrongReq)
if err == nil {
t.Fatal("expected call to ChangePassword to fail")
}
// The files should still exist after an unsuccessful attempt to change
// the wallet's password.
for _, tempFile := range tempFiles {
if _, err := os.Stat(tempFile); os.IsNotExist(err) {
t.Fatal("file does not exist but it should")
}
}
// Attempting to change the wallet's password using an invalid
// new password should fail.
wrongReq.NewPassword = []byte("8")
_, err = service.ChangePassword(ctx, wrongReq)
if err == nil {
t.Fatal("expected call to ChangePassword to fail")
}
// 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)
}
// The files should no longer exist.
for _, tempFile := range tempFiles {
if _, err := os.Open(tempFile); err == nil {
t.Fatal("file exists but it shouldn't")
}
}
// The new password should be sent over the channel.
select {
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):
t.Fatalf("password not received")
}
}