From b7ba2697c8711d306b91926f18e157efc8f10a7c Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 12 Oct 2017 11:13:58 +0200 Subject: [PATCH] walletunlocker: add package walletunlocker The walletunlocker package contains the UnlockerService, which implements the lnrpc.WalletUnlocker interface. This service is used for receiving a password from the user over RPC, and doing simple validity checks like making sure the user is not trying to create a new wallet if one already exists, and that in case the wallet exists, the provided password is correct. The service will the pass the passwords over the CreatePasswords or UnlockPasswords channels, for use within lnd.go. --- walletunlocker/service.go | 132 +++++++++++++++++++++++++++++ walletunlocker/service_test.go | 149 +++++++++++++++++++++++++++++++++ 2 files changed, 281 insertions(+) create mode 100644 walletunlocker/service.go create mode 100644 walletunlocker/service_test.go diff --git a/walletunlocker/service.go b/walletunlocker/service.go new file mode 100644 index 00000000..35eb103c --- /dev/null +++ b/walletunlocker/service.go @@ -0,0 +1,132 @@ +package walletunlocker + +import ( + "fmt" + + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnwallet/btcwallet" + "github.com/lightningnetwork/lnd/macaroons" + "github.com/roasbeef/btcd/chaincfg" + "github.com/roasbeef/btcwallet/wallet" + "golang.org/x/net/context" + "gopkg.in/macaroon-bakery.v1/bakery" +) + +// UnlockerService implements the WalletUnlocker service used to provide lnd +// with a password for wallet encryption at startup. +type UnlockerService struct { + // CreatePasswords is a channel where passwords provided by the rpc + // client to be used to initially create and encrypt a wallet will + // be sent. + CreatePasswords chan []byte + + // UnlockPasswords is a channel where passwords provided by the rpc + // client to be used to unlock and decrypt an existing wallet will + // be sent. + UnlockPasswords chan []byte + + // authSvc is the authentication/authorization service backed by + // macaroons. + authSvc *bakery.Service + + chainDir string + netParams *chaincfg.Params +} + +// New creates and returns a new UnlockerService. +func New(authSvc *bakery.Service, chainDir string, + params *chaincfg.Params) *UnlockerService { + return &UnlockerService{ + CreatePasswords: make(chan []byte, 1), + UnlockPasswords: make(chan []byte, 1), + authSvc: authSvc, + chainDir: chainDir, + netParams: params, + } +} + +// CreateWallet will read the password provided in the CreateWalletRequest +// and send it over the CreatePasswords channel in case no wallet already +// exist in the chain's wallet database directory. +func (u *UnlockerService) CreateWallet(ctx context.Context, + in *lnrpc.CreateWalletRequest) (*lnrpc.CreateWalletResponse, error) { + + // Check macaroon to see if this is allowed. + if u.authSvc != nil { + if err := macaroons.ValidateMacaroon(ctx, "createwallet", + u.authSvc); err != nil { + return nil, err + } + } + + netDir := btcwallet.NetworkDir(u.chainDir, u.netParams) + loader := wallet.NewLoader(u.netParams, netDir) + + // Check if wallet already exists. + walletExists, err := loader.WalletExists() + if err != nil { + return nil, err + } + + if walletExists { + // Cannot create wallet if it already exists! + return nil, fmt.Errorf("wallet already exists") + } + + // We send the password over the CreatePasswords channel, such that it + // can be used by lnd to open or create the wallet. + u.CreatePasswords <- in.Password + + return &lnrpc.CreateWalletResponse{}, nil +} + +// UnlockWallet sends the password provided by the incoming UnlockWalletRequest +// over the UnlockPasswords 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) { + + // Check macaroon to see if this is allowed. + if u.authSvc != nil { + if err := macaroons.ValidateMacaroon(ctx, "unlockwallet", + u.authSvc); err != nil { + return nil, err + } + } + + netDir := btcwallet.NetworkDir(u.chainDir, u.netParams) + loader := wallet.NewLoader(u.netParams, netDir) + + // 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. + _, err = loader.OpenExistingWallet(in.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, but we'll need to unload + // it to make sure lnd can open it later. + if err := loader.UnloadWallet(); err != nil { + // TODO: not return error here? + return nil, err + } + + // At this point we was able to open the existing wallet with the + // provided password. We send the password over the UnlockPasswords + // channel, such that it can be used by lnd to open the wallet. + u.UnlockPasswords <- in.Password + + return &lnrpc.UnlockWalletResponse{}, nil +} diff --git a/walletunlocker/service_test.go b/walletunlocker/service_test.go new file mode 100644 index 00000000..14d7c71d --- /dev/null +++ b/walletunlocker/service_test.go @@ -0,0 +1,149 @@ +package walletunlocker_test + +import ( + "bytes" + "io/ioutil" + "os" + "testing" + "time" + + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnwallet/btcwallet" + "github.com/lightningnetwork/lnd/walletunlocker" + "github.com/roasbeef/btcd/chaincfg" + "github.com/roasbeef/btcwallet/wallet" + "golang.org/x/net/context" +) + +const ( + walletDbName = "wallet.db" +) + +var ( + testPassword = []byte("test-password") + testSeed = []byte("test-seed-123456789") + + testNetParams = &chaincfg.MainNetParams +) + +func createTestWallet(t *testing.T, dir string, netParams *chaincfg.Params) { + netDir := btcwallet.NetworkDir(dir, netParams) + loader := wallet.NewLoader(netParams, netDir) + _, err := loader.CreateNewWallet(testPassword, testPassword, testSeed) + if err != nil { + t.Fatalf("failed creating wallet: %v", err) + } + err = loader.UnloadWallet() + if err != nil { + t.Fatalf("failed unloading wallet: %v", err) + } +} + +// TestCreateWallet checks that CreateWallet correctly returns a password that +// can be used for creating a wallet if no wallet exists from before, and +// returns an error when it already exists. +func TestCreateWallet(t *testing.T) { + t.Parallel() + + // testDir is empty, meaning wallet was not created from before. + testDir, err := ioutil.TempDir("", "testcreate") + if err != nil { + t.Fatalf("unable to create temp directory: %v", err) + } + defer func() { + os.RemoveAll(testDir) + }() + + // Create new UnlockerService. + service := walletunlocker.New(nil, testDir, testNetParams) + + ctx := context.Background() + req := &lnrpc.CreateWalletRequest{ + Password: testPassword, + } + _, err = service.CreateWallet(ctx, req) + if err != nil { + t.Fatalf("CreateWallet call failed: %v", err) + } + + // Password should be sent over the channel. + select { + case pw := <-service.CreatePasswords: + if !bytes.Equal(pw, testPassword) { + t.Fatalf("expected to receive password %x, got %x", + testPassword, pw) + } + case <-time.After(3 * time.Second): + t.Fatalf("password not received") + } + + // Create a wallet in testDir. + createTestWallet(t, testDir, testNetParams) + + // Now calling CreateWallet should fail, since a wallet already exists + // in the directory. + _, err = service.CreateWallet(ctx, req) + if err == nil { + t.Fatalf("CreateWallet did not fail as expected") + } +} + +// TestUnlockWallet checks that trying to unlock non-existing wallet fail, +// that unlocking existing wallet with wrong passphrase fails, and that +// unlocking existing wallet with correct passphrase succeeds. +func TestUnlockWallet(t *testing.T) { + t.Parallel() + + // testDir is empty, meaning wallet was not created from before. + testDir, err := ioutil.TempDir("", "testunlock") + if err != nil { + t.Fatalf("unable to create temp directory: %v", err) + } + defer func() { + os.RemoveAll(testDir) + }() + + // Create new UnlockerService. + service := walletunlocker.New(nil, testDir, testNetParams) + + ctx := context.Background() + req := &lnrpc.UnlockWalletRequest{ + Password: testPassword, + } + + // Should fail to unlock non-existing wallet. + _, err = service.UnlockWallet(ctx, req) + if err == nil { + t.Fatalf("expected call to UnlockWallet to fail") + } + + // Create a wallet we can try to unlock. + createTestWallet(t, testDir, testNetParams) + + // Try unlocking this wallet with the wrong passphrase. + wrongReq := &lnrpc.UnlockWalletRequest{ + Password: []byte("wrong-ofc"), + } + _, err = service.UnlockWallet(ctx, wrongReq) + if err == nil { + t.Fatalf("expected call to UnlockWallet to fail") + } + + // With the correct password, we should be able to unlock the wallet. + _, err = service.UnlockWallet(ctx, req) + if err != nil { + t.Fatalf("unable to unlock wallet: %v", err) + } + + // Password should be sent over the channel. + select { + case pw := <-service.UnlockPasswords: + if !bytes.Equal(pw, testPassword) { + t.Fatalf("expected to receive password %x, got %x", + testPassword, pw) + } + case <-time.After(3 * time.Second): + t.Fatalf("password not received") + } + +}