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.
This commit is contained in:
Johan T. Halseth 2017-10-12 11:13:58 +02:00 committed by Olaoluwa Osuntokun
parent 1f34bd815d
commit b7ba2697c8
2 changed files with 281 additions and 0 deletions

132
walletunlocker/service.go Normal file

@ -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
}

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