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,
|
serverOpts []grpc.ServerOption, proxyOpts []grpc.DialOption,
|
||||||
tlsConf *tls.Config) (*WalletUnlockParams, error) {
|
tlsConf *tls.Config) (*WalletUnlockParams, error) {
|
||||||
|
|
||||||
// Set up a new PasswordService, which will listen
|
// Set up a new PasswordService, which will listen for passwords
|
||||||
// for passwords provided over RPC.
|
// provided over RPC.
|
||||||
grpcServer := grpc.NewServer(serverOpts...)
|
grpcServer := grpc.NewServer(serverOpts...)
|
||||||
|
|
||||||
chainConfig := cfg.Bitcoin
|
chainConfig := cfg.Bitcoin
|
||||||
if registeredChains.PrimaryChain() == litecoinChain {
|
if registeredChains.PrimaryChain() == litecoinChain {
|
||||||
chainConfig = cfg.Litecoin
|
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(
|
pwService := walletunlocker.New(
|
||||||
chainConfig.ChainDir, activeNetParams.Params,
|
chainConfig.ChainDir, activeNetParams.Params, macaroonFiles,
|
||||||
)
|
)
|
||||||
lnrpc.RegisterWalletUnlockerServer(grpcServer, pwService)
|
lnrpc.RegisterWalletUnlockerServer(grpcServer, pwService)
|
||||||
|
|
||||||
@ -951,9 +960,10 @@ func waitForWalletPassword(grpcEndpoints, restEndpoints []string,
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
// Wait for user to provide the password.
|
// Wait for user to provide the password.
|
||||||
ltndLog.Infof("Waiting for wallet encryption password. " +
|
ltndLog.Infof("Waiting for wallet encryption password. Use `lncli " +
|
||||||
"Use `lncli create` to create wallet, or " +
|
"create` to create a wallet, `lncli unlock` to unlock an " +
|
||||||
"`lncli unlock` to unlock already created wallet.")
|
"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
|
// 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
|
// for creation or unlocking, as a new wallet db will be created if
|
||||||
|
@ -2,11 +2,14 @@ package walletunlocker
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/lightningnetwork/lnd/aezeed"
|
"github.com/lightningnetwork/lnd/aezeed"
|
||||||
"github.com/lightningnetwork/lnd/lnrpc"
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
||||||
"github.com/roasbeef/btcd/chaincfg"
|
"github.com/roasbeef/btcd/chaincfg"
|
||||||
"github.com/roasbeef/btcwallet/wallet"
|
"github.com/roasbeef/btcwallet/wallet"
|
||||||
@ -64,17 +67,21 @@ type UnlockerService struct {
|
|||||||
// sent.
|
// sent.
|
||||||
UnlockMsgs chan *WalletUnlockMsg
|
UnlockMsgs chan *WalletUnlockMsg
|
||||||
|
|
||||||
chainDir string
|
chainDir string
|
||||||
netParams *chaincfg.Params
|
netParams *chaincfg.Params
|
||||||
|
macaroonFiles []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates and returns a new UnlockerService.
|
// 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{
|
return &UnlockerService{
|
||||||
InitMsgs: make(chan *WalletInitMsg, 1),
|
InitMsgs: make(chan *WalletInitMsg, 1),
|
||||||
UnlockMsgs: make(chan *WalletUnlockMsg, 1),
|
UnlockMsgs: make(chan *WalletUnlockMsg, 1),
|
||||||
chainDir: chainDir,
|
chainDir: chainDir,
|
||||||
netParams: params,
|
netParams: params,
|
||||||
|
macaroonFiles: macaroonFiles,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,12 +175,10 @@ func (u *UnlockerService) GenSeed(ctx context.Context,
|
|||||||
func (u *UnlockerService) InitWallet(ctx context.Context,
|
func (u *UnlockerService) InitWallet(ctx context.Context,
|
||||||
in *lnrpc.InitWalletRequest) (*lnrpc.InitWalletResponse, error) {
|
in *lnrpc.InitWalletRequest) (*lnrpc.InitWalletResponse, error) {
|
||||||
|
|
||||||
// Require the provided password to have a length of at least 8
|
// Make sure the password meets our constraints.
|
||||||
// characters.
|
|
||||||
password := in.WalletPassword
|
password := in.WalletPassword
|
||||||
if len(password) < 8 {
|
if err := validatePassword(password); err != nil {
|
||||||
return nil, fmt.Errorf("password must have " +
|
return nil, err
|
||||||
"at least 8 characters")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Require that the recovery window be non-negative.
|
// Require that the recovery window be non-negative.
|
||||||
@ -276,3 +281,85 @@ func (u *UnlockerService) UnlockWallet(ctx context.Context,
|
|||||||
|
|
||||||
return &lnrpc.UnlockWalletResponse{}, nil
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("unable to create temp directory: %v", err)
|
t.Fatalf("unable to create temp directory: %v", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer os.RemoveAll(testDir)
|
||||||
os.RemoveAll(testDir)
|
|
||||||
}()
|
service := walletunlocker.New(testDir, testNetParams, nil)
|
||||||
service := walletunlocker.New(testDir, testNetParams)
|
|
||||||
|
|
||||||
// Now that the service has been created, we'll ask it to generate a
|
// Now that the service has been created, we'll ask it to generate a
|
||||||
// new seed for us given a test passphrase.
|
// new seed for us given a test passphrase.
|
||||||
@ -108,7 +107,7 @@ func TestGenSeedGenerateEntropy(t *testing.T) {
|
|||||||
defer func() {
|
defer func() {
|
||||||
os.RemoveAll(testDir)
|
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
|
// 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
|
// 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() {
|
defer func() {
|
||||||
os.RemoveAll(testDir)
|
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
|
// 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
|
// 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.
|
// 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
|
// Once we have the unlocker service created, we'll now instantiate a
|
||||||
// new cipher seed instance.
|
// new cipher seed instance.
|
||||||
@ -287,7 +286,7 @@ func TestCreateWalletInvalidEntropy(t *testing.T) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Create new UnlockerService.
|
// 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
|
// We'll attempt to init the wallet with an invalid cipher seed and
|
||||||
// passphrase.
|
// passphrase.
|
||||||
@ -320,7 +319,7 @@ func TestUnlockWallet(t *testing.T) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Create new UnlockerService.
|
// Create new UnlockerService.
|
||||||
service := walletunlocker.New(testDir, testNetParams)
|
service := walletunlocker.New(testDir, testNetParams, nil)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
req := &lnrpc.UnlockWalletRequest{
|
req := &lnrpc.UnlockWalletRequest{
|
||||||
@ -368,3 +367,101 @@ func TestUnlockWallet(t *testing.T) {
|
|||||||
t.Fatalf("password not received")
|
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