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") + } + +}