lnd+walletunlocker: implement ChangePassword RPC
This commit is contained in:
parent
fc6505a0ff
commit
58a3419283
22
lnd.go
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")
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user