multi: macaroon root key encryption
This commit is contained in:
parent
4b1cc98808
commit
de6efbd1a1
@ -39,7 +39,7 @@ func fatal(err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getWalletUnlockerClient(ctx *cli.Context) (lnrpc.WalletUnlockerClient, func()) {
|
func getWalletUnlockerClient(ctx *cli.Context) (lnrpc.WalletUnlockerClient, func()) {
|
||||||
conn := getClientConn(ctx)
|
conn := getClientConn(ctx, true)
|
||||||
|
|
||||||
cleanUp := func() {
|
cleanUp := func() {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
@ -49,7 +49,7 @@ func getWalletUnlockerClient(ctx *cli.Context) (lnrpc.WalletUnlockerClient, func
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getClient(ctx *cli.Context) (lnrpc.LightningClient, func()) {
|
func getClient(ctx *cli.Context) (lnrpc.LightningClient, func()) {
|
||||||
conn := getClientConn(ctx)
|
conn := getClientConn(ctx, false)
|
||||||
|
|
||||||
cleanUp := func() {
|
cleanUp := func() {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
@ -58,7 +58,7 @@ func getClient(ctx *cli.Context) (lnrpc.LightningClient, func()) {
|
|||||||
return lnrpc.NewLightningClient(conn), cleanUp
|
return lnrpc.NewLightningClient(conn), cleanUp
|
||||||
}
|
}
|
||||||
|
|
||||||
func getClientConn(ctx *cli.Context) *grpc.ClientConn {
|
func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn {
|
||||||
// Load the specified TLS certificate and build transport credentials
|
// Load the specified TLS certificate and build transport credentials
|
||||||
// with it.
|
// with it.
|
||||||
tlsCertPath := cleanAndExpandPath(ctx.GlobalString("tlscertpath"))
|
tlsCertPath := cleanAndExpandPath(ctx.GlobalString("tlscertpath"))
|
||||||
@ -72,8 +72,9 @@ func getClientConn(ctx *cli.Context) *grpc.ClientConn {
|
|||||||
grpc.WithTransportCredentials(creds),
|
grpc.WithTransportCredentials(creds),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only process macaroon credentials if --no-macaroons isn't set.
|
// Only process macaroon credentials if --no-macaroons isn't set and
|
||||||
if !ctx.GlobalBool("no-macaroons") {
|
// if we're not skipping macaroon processing.
|
||||||
|
if !ctx.GlobalBool("no-macaroons") && !skipMacaroons {
|
||||||
// Load the specified macaroon file.
|
// Load the specified macaroon file.
|
||||||
macPath := cleanAndExpandPath(ctx.GlobalString("macaroonpath"))
|
macPath := cleanAndExpandPath(ctx.GlobalString("macaroonpath"))
|
||||||
macBytes, err := ioutil.ReadFile(macPath)
|
macBytes, err := ioutil.ReadFile(macPath)
|
||||||
|
67
lnd.go
67
lnd.go
@ -154,28 +154,6 @@ func lndMain() error {
|
|||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
var macaroonService *bakery.Bakery
|
|
||||||
if !cfg.NoMacaroons {
|
|
||||||
// Create the macaroon authentication/authorization service.
|
|
||||||
macaroonService, err = macaroons.NewService(macaroonDatabaseDir,
|
|
||||||
macaroons.IPLockChecker)
|
|
||||||
if err != nil {
|
|
||||||
srvrLog.Errorf("unable to create macaroon service: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create macaroon files for lncli to use if they don't exist.
|
|
||||||
if !fileExists(cfg.AdminMacPath) && !fileExists(cfg.ReadMacPath) {
|
|
||||||
err = genMacaroons(ctx, macaroonService,
|
|
||||||
cfg.AdminMacPath, cfg.ReadMacPath)
|
|
||||||
if err != nil {
|
|
||||||
ltndLog.Errorf("unable to create macaroon "+
|
|
||||||
"files: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure we create TLS key and certificate if they don't exist
|
// Ensure we create TLS key and certificate if they don't exist
|
||||||
if !fileExists(cfg.TLSCertPath) && !fileExists(cfg.TLSKeyPath) {
|
if !fileExists(cfg.TLSCertPath) && !fileExists(cfg.TLSKeyPath) {
|
||||||
if err := genCertPair(cfg.TLSCertPath, cfg.TLSKeyPath); err != nil {
|
if err := genCertPair(cfg.TLSCertPath, cfg.TLSKeyPath); err != nil {
|
||||||
@ -200,6 +178,18 @@ func lndMain() error {
|
|||||||
}
|
}
|
||||||
proxyOpts := []grpc.DialOption{grpc.WithTransportCredentials(cCreds)}
|
proxyOpts := []grpc.DialOption{grpc.WithTransportCredentials(cCreds)}
|
||||||
|
|
||||||
|
var macaroonService *macaroons.Service
|
||||||
|
if !cfg.NoMacaroons {
|
||||||
|
// Create the macaroon authentication/authorization service.
|
||||||
|
macaroonService, err = macaroons.NewService(macaroonDatabaseDir,
|
||||||
|
macaroons.IPLockChecker)
|
||||||
|
if err != nil {
|
||||||
|
srvrLog.Errorf("unable to create macaroon service: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer macaroonService.Close()
|
||||||
|
}
|
||||||
|
|
||||||
// We wait until the user provides a password over RPC. In case lnd is
|
// We wait until the user provides a password over RPC. In case lnd is
|
||||||
// started with the --noencryptwallet flag, we use the default password
|
// started with the --noencryptwallet flag, we use the default password
|
||||||
// "hello" for wallet encryption.
|
// "hello" for wallet encryption.
|
||||||
@ -215,6 +205,27 @@ func lndMain() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !cfg.NoMacaroons {
|
||||||
|
// Try to unlock the macaroon store with the private password.
|
||||||
|
// Ignore ErrAlreadyUnlocked since it could be unlocked by the
|
||||||
|
// wallet unlocker.
|
||||||
|
err = macaroonService.CreateUnlock(&privateWalletPw)
|
||||||
|
if err != nil && err != macaroons.ErrAlreadyUnlocked {
|
||||||
|
srvrLog.Error(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Create macaroon files for lncli to use if they don't exist.
|
||||||
|
if !fileExists(cfg.AdminMacPath) && !fileExists(cfg.ReadMacPath) {
|
||||||
|
err = genMacaroons(ctx, macaroonService,
|
||||||
|
cfg.AdminMacPath, cfg.ReadMacPath)
|
||||||
|
if err != nil {
|
||||||
|
ltndLog.Errorf("unable to create macaroon "+
|
||||||
|
"files: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// With the information parsed from the configuration, create valid
|
// With the information parsed from the configuration, create valid
|
||||||
// instances of the pertinent interfaces required to operate the
|
// instances of the pertinent interfaces required to operate the
|
||||||
// Lightning Network Daemon.
|
// Lightning Network Daemon.
|
||||||
@ -389,10 +400,10 @@ func lndMain() error {
|
|||||||
// Check macaroon authentication if macaroons aren't disabled.
|
// Check macaroon authentication if macaroons aren't disabled.
|
||||||
if macaroonService != nil {
|
if macaroonService != nil {
|
||||||
serverOpts = append(serverOpts,
|
serverOpts = append(serverOpts,
|
||||||
grpc.UnaryInterceptor(macaroons.UnaryServerInterceptor(
|
grpc.UnaryInterceptor(macaroonService.
|
||||||
macaroonService, permissions)),
|
UnaryServerInterceptor(permissions)),
|
||||||
grpc.StreamInterceptor(macaroons.StreamServerInterceptor(
|
grpc.StreamInterceptor(macaroonService.
|
||||||
macaroonService, permissions)),
|
StreamServerInterceptor(permissions)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -672,7 +683,7 @@ func genCertPair(certFile, keyFile string) error {
|
|||||||
|
|
||||||
// genMacaroons generates a pair of macaroon files; one admin-level and one
|
// genMacaroons generates a pair of macaroon files; one admin-level and one
|
||||||
// read-only. These can also be used to generate more granular macaroons.
|
// read-only. These can also be used to generate more granular macaroons.
|
||||||
func genMacaroons(ctx context.Context, svc *bakery.Bakery, admFile,
|
func genMacaroons(ctx context.Context, svc *macaroons.Service, admFile,
|
||||||
roFile string) error {
|
roFile string) error {
|
||||||
|
|
||||||
// Generate the read-only macaroon and write it to a file.
|
// Generate the read-only macaroon and write it to a file.
|
||||||
@ -712,7 +723,7 @@ func genMacaroons(ctx context.Context, svc *bakery.Bakery, admFile,
|
|||||||
// the user to this RPC server.
|
// the user to this RPC server.
|
||||||
func waitForWalletPassword(grpcEndpoints, restEndpoints []string,
|
func waitForWalletPassword(grpcEndpoints, restEndpoints []string,
|
||||||
serverOpts []grpc.ServerOption, proxyOpts []grpc.DialOption,
|
serverOpts []grpc.ServerOption, proxyOpts []grpc.DialOption,
|
||||||
tlsConf *tls.Config, macaroonService *bakery.Bakery) ([]byte, []byte, error) {
|
tlsConf *tls.Config, macaroonService *macaroons.Service) ([]byte, []byte, error) {
|
||||||
|
|
||||||
// Set up a new PasswordService, which will listen
|
// Set up a new PasswordService, which will listen
|
||||||
// for passwords provided over RPC.
|
// for passwords provided over RPC.
|
||||||
|
@ -2,13 +2,9 @@ package macaroons
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"google.golang.org/grpc/metadata"
|
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"gopkg.in/macaroon-bakery.v2/bakery"
|
|
||||||
macaroon "gopkg.in/macaroon.v2"
|
macaroon "gopkg.in/macaroon.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -47,40 +43,3 @@ func NewMacaroonCredential(m *macaroon.Macaroon) MacaroonCredential {
|
|||||||
ms.Macaroon = m.Clone()
|
ms.Macaroon = m.Clone()
|
||||||
return ms
|
return ms
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateMacaroon validates the capabilities of a given request given a
|
|
||||||
// bakery service, context, and uri. Within the passed context.Context, we
|
|
||||||
// expect a macaroon to be encoded as request metadata using the key
|
|
||||||
// "macaroon".
|
|
||||||
func ValidateMacaroon(ctx context.Context, requiredPermissions []bakery.Op,
|
|
||||||
svc *bakery.Bakery) error {
|
|
||||||
|
|
||||||
// Get macaroon bytes from context and unmarshal into macaroon.
|
|
||||||
md, ok := metadata.FromIncomingContext(ctx)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("unable to get metadata from context")
|
|
||||||
}
|
|
||||||
if len(md["macaroon"]) != 1 {
|
|
||||||
return fmt.Errorf("expected 1 macaroon, got %d",
|
|
||||||
len(md["macaroon"]))
|
|
||||||
}
|
|
||||||
|
|
||||||
// With the macaroon obtained, we'll now decode the hex-string
|
|
||||||
// encoding, then unmarshal it from binary into its concrete struct
|
|
||||||
// representation.
|
|
||||||
macBytes, err := hex.DecodeString(md["macaroon"][0])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
mac := &macaroon.Macaroon{}
|
|
||||||
err = mac.UnmarshalBinary(macBytes)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the method being called against the permitted operation and
|
|
||||||
// the expiration time and IP address and return the result.
|
|
||||||
authChecker := svc.Checker.Auth(macaroon.Slice{mac})
|
|
||||||
_, err = authChecker.Allow(ctx, requiredPermissions...)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
package macaroons
|
package macaroons
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
|
||||||
"gopkg.in/macaroon-bakery.v2/bakery"
|
"gopkg.in/macaroon-bakery.v2/bakery"
|
||||||
"gopkg.in/macaroon-bakery.v2/bakery/checkers"
|
"gopkg.in/macaroon-bakery.v2/bakery/checkers"
|
||||||
|
macaroon "gopkg.in/macaroon.v2"
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
@ -20,6 +23,15 @@ var (
|
|||||||
dbFilename = "macaroons.db"
|
dbFilename = "macaroons.db"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Service encapsulates bakery.Bakery and adds a Close() method that zeroes the
|
||||||
|
// root key service encryption keys, as well as utility methods to validate a
|
||||||
|
// macaroon against the bakery and gRPC middleware for macaroon-based auth.
|
||||||
|
type Service struct {
|
||||||
|
bakery.Bakery
|
||||||
|
|
||||||
|
rks *RootKeyStorage
|
||||||
|
}
|
||||||
|
|
||||||
// NewService returns a service backed by the macaroon Bolt DB stored in the
|
// NewService returns a service backed by the macaroon Bolt DB stored in the
|
||||||
// passed directory. The `checks` argument can be any of the `Checker` type
|
// passed directory. The `checks` argument can be any of the `Checker` type
|
||||||
// functions defined in this package, or a custom checker if desired. This
|
// functions defined in this package, or a custom checker if desired. This
|
||||||
@ -27,7 +39,7 @@ var (
|
|||||||
// listing the same checker more than once is not harmful. Default checkers,
|
// listing the same checker more than once is not harmful. Default checkers,
|
||||||
// such as those for `allow`, `time-before`, `declared`, and `error` caveats
|
// such as those for `allow`, `time-before`, `declared`, and `error` caveats
|
||||||
// are registered automatically and don't need to be added.
|
// are registered automatically and don't need to be added.
|
||||||
func NewService(dir string, checks ...Checker) (*bakery.Bakery, error) {
|
func NewService(dir string, checks ...Checker) (*Service, error) {
|
||||||
// Open the database that we'll use to store the primary macaroon key,
|
// Open the database that we'll use to store the primary macaroon key,
|
||||||
// and all generated macaroons+caveats.
|
// and all generated macaroons+caveats.
|
||||||
macaroonDB, err := bolt.Open(path.Join(dir, dbFilename), 0600,
|
macaroonDB, err := bolt.Open(path.Join(dir, dbFilename), 0600,
|
||||||
@ -62,7 +74,7 @@ func NewService(dir string, checks ...Checker) (*bakery.Bakery, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return svc, nil
|
return &Service{*svc, rootKeyStore}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isRegistered checks to see if the required checker has already been
|
// isRegistered checks to see if the required checker has already been
|
||||||
@ -83,7 +95,7 @@ func isRegistered(c *checkers.Checker, name string) bool {
|
|||||||
|
|
||||||
// UnaryServerInterceptor is a GRPC interceptor that checks whether the
|
// UnaryServerInterceptor is a GRPC interceptor that checks whether the
|
||||||
// request is authorized by the included macaroons.
|
// request is authorized by the included macaroons.
|
||||||
func UnaryServerInterceptor(svc *bakery.Bakery,
|
func (svc *Service) UnaryServerInterceptor(
|
||||||
permissionMap map[string][]bakery.Op) grpc.UnaryServerInterceptor {
|
permissionMap map[string][]bakery.Op) grpc.UnaryServerInterceptor {
|
||||||
|
|
||||||
return func(ctx context.Context, req interface{},
|
return func(ctx context.Context, req interface{},
|
||||||
@ -95,8 +107,7 @@ func UnaryServerInterceptor(svc *bakery.Bakery,
|
|||||||
"required for method", info.FullMethod)
|
"required for method", info.FullMethod)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := ValidateMacaroon(ctx, permissionMap[info.FullMethod],
|
err := svc.ValidateMacaroon(ctx, permissionMap[info.FullMethod])
|
||||||
svc)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -107,7 +118,7 @@ func UnaryServerInterceptor(svc *bakery.Bakery,
|
|||||||
|
|
||||||
// StreamServerInterceptor is a GRPC interceptor that checks whether the
|
// StreamServerInterceptor is a GRPC interceptor that checks whether the
|
||||||
// request is authorized by the included macaroons.
|
// request is authorized by the included macaroons.
|
||||||
func StreamServerInterceptor(svc *bakery.Bakery,
|
func (svc *Service) StreamServerInterceptor(
|
||||||
permissionMap map[string][]bakery.Op) grpc.StreamServerInterceptor {
|
permissionMap map[string][]bakery.Op) grpc.StreamServerInterceptor {
|
||||||
|
|
||||||
return func(srv interface{}, ss grpc.ServerStream,
|
return func(srv interface{}, ss grpc.ServerStream,
|
||||||
@ -118,8 +129,8 @@ func StreamServerInterceptor(svc *bakery.Bakery,
|
|||||||
"for method", info.FullMethod)
|
"for method", info.FullMethod)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := ValidateMacaroon(ss.Context(),
|
err := svc.ValidateMacaroon(ss.Context(),
|
||||||
permissionMap[info.FullMethod], svc)
|
permissionMap[info.FullMethod])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -127,3 +138,52 @@ func StreamServerInterceptor(svc *bakery.Bakery,
|
|||||||
return handler(srv, ss)
|
return handler(srv, ss)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateMacaroon validates the capabilities of a given request given a
|
||||||
|
// bakery service, context, and uri. Within the passed context.Context, we
|
||||||
|
// expect a macaroon to be encoded as request metadata using the key
|
||||||
|
// "macaroon".
|
||||||
|
func (svc *Service) ValidateMacaroon(ctx context.Context,
|
||||||
|
requiredPermissions []bakery.Op) error {
|
||||||
|
|
||||||
|
// Get macaroon bytes from context and unmarshal into macaroon.
|
||||||
|
md, ok := metadata.FromIncomingContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unable to get metadata from context")
|
||||||
|
}
|
||||||
|
if len(md["macaroon"]) != 1 {
|
||||||
|
return fmt.Errorf("expected 1 macaroon, got %d",
|
||||||
|
len(md["macaroon"]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the macaroon obtained, we'll now decode the hex-string
|
||||||
|
// encoding, then unmarshal it from binary into its concrete struct
|
||||||
|
// representation.
|
||||||
|
macBytes, err := hex.DecodeString(md["macaroon"][0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mac := &macaroon.Macaroon{}
|
||||||
|
err = mac.UnmarshalBinary(macBytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the method being called against the permitted operation and
|
||||||
|
// the expiration time and IP address and return the result.
|
||||||
|
authChecker := svc.Checker.Auth(macaroon.Slice{mac})
|
||||||
|
_, err = authChecker.Allow(ctx, requiredPermissions...)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the database that underlies the RootKeyStore and zeroes the
|
||||||
|
// encryption keys.
|
||||||
|
func (svc *Service) Close() error {
|
||||||
|
return svc.rks.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUnlock calls the underlying root key store's CreateUnlock and returns
|
||||||
|
// the result.
|
||||||
|
func (svc *Service) CreateUnlock(password *[]byte) error {
|
||||||
|
return svc.rks.CreateUnlock(password)
|
||||||
|
}
|
||||||
|
@ -8,6 +8,8 @@ import (
|
|||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"github.com/boltdb/bolt"
|
"github.com/boltdb/bolt"
|
||||||
|
|
||||||
|
"github.com/roasbeef/btcwallet/snacl"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -25,13 +27,28 @@ var (
|
|||||||
// TODO(aakselrod): Add support for key rotation.
|
// TODO(aakselrod): Add support for key rotation.
|
||||||
defaultRootKeyID = []byte("0")
|
defaultRootKeyID = []byte("0")
|
||||||
|
|
||||||
// macaroonBucketName is the name of the macaroon store bucket.
|
// encryptedKeyID is the name of the database key that stores the
|
||||||
macaroonBucketName = []byte("macaroons")
|
// encryption key, encrypted with a salted + hashed password. The
|
||||||
|
// format is 32 bytes of salt, and the rest is encrypted key.
|
||||||
|
encryptedKeyID = []byte("enckey")
|
||||||
|
|
||||||
|
// ErrAlreadyUnlocked specifies that the store has already been
|
||||||
|
// unlocked.
|
||||||
|
ErrAlreadyUnlocked = fmt.Errorf("macaroon store already unlocked")
|
||||||
|
|
||||||
|
// ErrStoreLocked specifies that the store needs to be unlocked with
|
||||||
|
// a password.
|
||||||
|
ErrStoreLocked = fmt.Errorf("macaroon store is locked")
|
||||||
|
|
||||||
|
// ErrPasswordRequired specifies that a nil password has been passed.
|
||||||
|
ErrPasswordRequired = fmt.Errorf("a non-nil password is required")
|
||||||
)
|
)
|
||||||
|
|
||||||
// RootKeyStorage implements the bakery.RootKeyStorage interface.
|
// RootKeyStorage implements the bakery.RootKeyStorage interface.
|
||||||
type RootKeyStorage struct {
|
type RootKeyStorage struct {
|
||||||
*bolt.DB
|
*bolt.DB
|
||||||
|
|
||||||
|
encKey *snacl.SecretKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRootKeyStorage creates a RootKeyStorage instance.
|
// NewRootKeyStorage creates a RootKeyStorage instance.
|
||||||
@ -47,11 +64,65 @@ func NewRootKeyStorage(db *bolt.DB) (*RootKeyStorage, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return the DB wrapped in a RootKeyStorage object.
|
// Return the DB wrapped in a RootKeyStorage object.
|
||||||
return &RootKeyStorage{db}, nil
|
return &RootKeyStorage{db, nil}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUnlock sets an encryption key if one is not already set, otherwise it
|
||||||
|
// checks if the password is correct for the stored encryption key.
|
||||||
|
func (r *RootKeyStorage) CreateUnlock(password *[]byte) error {
|
||||||
|
// Check if we've already unlocked the store; return an error if so.
|
||||||
|
if r.encKey != nil {
|
||||||
|
return ErrAlreadyUnlocked
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a nil password has been passed; return an error if so.
|
||||||
|
if password == nil {
|
||||||
|
return ErrPasswordRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Update(func(tx *bolt.Tx) error {
|
||||||
|
bucket := tx.Bucket(rootKeyBucketName)
|
||||||
|
dbKey := bucket.Get(encryptedKeyID)
|
||||||
|
if len(dbKey) > 0 {
|
||||||
|
// We've already stored a key, so try to unlock with
|
||||||
|
// the password.
|
||||||
|
encKey := &snacl.SecretKey{}
|
||||||
|
err := encKey.Unmarshal(dbKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = encKey.DeriveKey(password)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.encKey = encKey
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We haven't yet stored a key, so create a new one.
|
||||||
|
encKey, err := snacl.NewSecretKey(password, snacl.DefaultN,
|
||||||
|
snacl.DefaultR, snacl.DefaultP)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bucket.Put(encryptedKeyID, encKey.Marshal())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.encKey = encKey
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get implements the Get method for the bakery.RootKeyStorage interface.
|
// Get implements the Get method for the bakery.RootKeyStorage interface.
|
||||||
func (r *RootKeyStorage) Get(_ context.Context, id []byte) ([]byte, error) {
|
func (r *RootKeyStorage) Get(_ context.Context, id []byte) ([]byte, error) {
|
||||||
|
if r.encKey == nil {
|
||||||
|
return nil, ErrStoreLocked
|
||||||
|
}
|
||||||
var rootKey []byte
|
var rootKey []byte
|
||||||
err := r.View(func(tx *bolt.Tx) error {
|
err := r.View(func(tx *bolt.Tx) error {
|
||||||
dbKey := tx.Bucket(rootKeyBucketName).Get(id)
|
dbKey := tx.Bucket(rootKeyBucketName).Get(id)
|
||||||
@ -60,8 +131,13 @@ func (r *RootKeyStorage) Get(_ context.Context, id []byte) ([]byte, error) {
|
|||||||
string(id))
|
string(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
rootKey = make([]byte, len(dbKey))
|
decKey, err := r.encKey.Decrypt(dbKey)
|
||||||
copy(rootKey[:], dbKey)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rootKey = make([]byte, len(decKey))
|
||||||
|
copy(rootKey[:], decKey)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -75,23 +151,40 @@ func (r *RootKeyStorage) Get(_ context.Context, id []byte) ([]byte, error) {
|
|||||||
// interface.
|
// interface.
|
||||||
// TODO(aakselrod): Add support for key rotation.
|
// TODO(aakselrod): Add support for key rotation.
|
||||||
func (r *RootKeyStorage) RootKey(_ context.Context) ([]byte, []byte, error) {
|
func (r *RootKeyStorage) RootKey(_ context.Context) ([]byte, []byte, error) {
|
||||||
|
if r.encKey == nil {
|
||||||
|
return nil, nil, ErrStoreLocked
|
||||||
|
}
|
||||||
var rootKey []byte
|
var rootKey []byte
|
||||||
id := defaultRootKeyID
|
id := defaultRootKeyID
|
||||||
err := r.Update(func(tx *bolt.Tx) error {
|
err := r.Update(func(tx *bolt.Tx) error {
|
||||||
ns := tx.Bucket(rootKeyBucketName)
|
ns := tx.Bucket(rootKeyBucketName)
|
||||||
rootKey = ns.Get(id)
|
dbKey := ns.Get(id)
|
||||||
|
|
||||||
// If there's no root key stored in the bucket yet, create one.
|
// If there's a root key stored in the bucket, decrypt it and
|
||||||
if len(rootKey) != 0 {
|
// return it.
|
||||||
|
if len(dbKey) != 0 {
|
||||||
|
decKey, err := r.encKey.Decrypt(dbKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rootKey = make([]byte, len(decKey))
|
||||||
|
copy(rootKey[:], decKey[:])
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a RootKeyLen-byte root key.
|
// Otherwise, create a RootKeyLen-byte root key, encrypt it,
|
||||||
|
// and store it in the bucket.
|
||||||
rootKey = make([]byte, RootKeyLen)
|
rootKey = make([]byte, RootKeyLen)
|
||||||
if _, err := io.ReadFull(rand.Reader, rootKey[:]); err != nil {
|
if _, err := io.ReadFull(rand.Reader, rootKey[:]); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return ns.Put(id, rootKey)
|
|
||||||
|
encKey, err := r.encKey.Encrypt(rootKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ns.Put(id, encKey)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@ -99,3 +192,10 @@ func (r *RootKeyStorage) RootKey(_ context.Context) ([]byte, []byte, error) {
|
|||||||
|
|
||||||
return rootKey, id, nil
|
return rootKey, id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close closes the underlying database and zeroes the encryption key stored
|
||||||
|
// in memory.
|
||||||
|
func (r *RootKeyStorage) Close() error {
|
||||||
|
r.encKey.Zero()
|
||||||
|
return r.DB.Close()
|
||||||
|
}
|
||||||
|
137
macaroons/store_test.go
Normal file
137
macaroons/store_test.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
package macaroons_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
|
|
||||||
|
"github.com/lightningnetwork/lnd/macaroons"
|
||||||
|
|
||||||
|
"github.com/roasbeef/btcwallet/snacl"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStore(t *testing.T) {
|
||||||
|
tempDir, err := ioutil.TempDir("", "macaroonstore-")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error creating temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
db, err := bolt.Open(path.Join(tempDir, "weks.db"), 0600,
|
||||||
|
bolt.DefaultOptions)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error opening store DB: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
store, err := macaroons.NewRootKeyStorage(db)
|
||||||
|
if err != nil {
|
||||||
|
db.Close()
|
||||||
|
t.Fatalf("Error creating root key store: %v", err)
|
||||||
|
}
|
||||||
|
defer store.Close()
|
||||||
|
|
||||||
|
key, id, err := store.RootKey(nil)
|
||||||
|
if err != macaroons.ErrStoreLocked {
|
||||||
|
t.Fatalf("Received %v instead of ErrStoreLocked", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err = store.Get(nil, nil)
|
||||||
|
if err != macaroons.ErrStoreLocked {
|
||||||
|
t.Fatalf("Received %v instead of ErrStoreLocked", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pw := []byte("weks")
|
||||||
|
err = store.CreateUnlock(&pw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error creating store encryption key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, id, err = store.RootKey(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error getting root key from store: %v", err)
|
||||||
|
}
|
||||||
|
rootID := id
|
||||||
|
|
||||||
|
key2, err := store.Get(nil, id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error getting key with ID %s: %v", string(id), err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(key, key2) {
|
||||||
|
t.Fatalf("Root key doesn't match: expected %v, got %v",
|
||||||
|
key, key2)
|
||||||
|
}
|
||||||
|
|
||||||
|
badpw := []byte("badweks")
|
||||||
|
err = store.CreateUnlock(&badpw)
|
||||||
|
if err != macaroons.ErrAlreadyUnlocked {
|
||||||
|
t.Fatalf("Received %v instead of ErrAlreadyUnlocked", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
store.Close()
|
||||||
|
// Between here and the re-opening of the store, it's possible to get
|
||||||
|
// a double-close, but that's not such a big deal since the tests will
|
||||||
|
// fail anyway in that case.
|
||||||
|
db, err = bolt.Open(path.Join(tempDir, "weks.db"), 0600,
|
||||||
|
bolt.DefaultOptions)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error opening store DB: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
store, err = macaroons.NewRootKeyStorage(db)
|
||||||
|
if err != nil {
|
||||||
|
db.Close()
|
||||||
|
t.Fatalf("Error creating root key store: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.CreateUnlock(&badpw)
|
||||||
|
if err != snacl.ErrInvalidPassword {
|
||||||
|
t.Fatalf("Received %v instead of ErrInvalidPassword", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.CreateUnlock(nil)
|
||||||
|
if err != macaroons.ErrPasswordRequired {
|
||||||
|
t.Fatalf("Received %v instead of ErrPasswordRequired", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, id, err = store.RootKey(nil)
|
||||||
|
if err != macaroons.ErrStoreLocked {
|
||||||
|
t.Fatalf("Received %v instead of ErrStoreLocked", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err = store.Get(nil, nil)
|
||||||
|
if err != macaroons.ErrStoreLocked {
|
||||||
|
t.Fatalf("Received %v instead of ErrStoreLocked", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.CreateUnlock(&pw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error unlocking root key store: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err = store.Get(nil, rootID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error getting key with ID %s: %v",
|
||||||
|
string(rootID), err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(key, key2) {
|
||||||
|
t.Fatalf("Root key doesn't match: expected %v, got %v",
|
||||||
|
key2, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, id, err = store.RootKey(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error getting root key from store: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(key, key2) {
|
||||||
|
t.Fatalf("Root key doesn't match: expected %v, got %v",
|
||||||
|
key2, key)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(rootID, id) {
|
||||||
|
t.Fatalf("Root ID doesn't match: expected %v, got %v",
|
||||||
|
rootID, id)
|
||||||
|
}
|
||||||
|
}
|
@ -5,10 +5,10 @@ import (
|
|||||||
|
|
||||||
"github.com/lightningnetwork/lnd/lnrpc"
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
||||||
|
"github.com/lightningnetwork/lnd/macaroons"
|
||||||
"github.com/roasbeef/btcd/chaincfg"
|
"github.com/roasbeef/btcd/chaincfg"
|
||||||
"github.com/roasbeef/btcwallet/wallet"
|
"github.com/roasbeef/btcwallet/wallet"
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
"gopkg.in/macaroon-bakery.v2/bakery"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// UnlockerService implements the WalletUnlocker service used to provide lnd
|
// UnlockerService implements the WalletUnlocker service used to provide lnd
|
||||||
@ -26,10 +26,11 @@ type UnlockerService struct {
|
|||||||
|
|
||||||
chainDir string
|
chainDir string
|
||||||
netParams *chaincfg.Params
|
netParams *chaincfg.Params
|
||||||
|
authSvc *macaroons.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates and returns a new UnlockerService.
|
// New creates and returns a new UnlockerService.
|
||||||
func New(authSvc *bakery.Bakery, chainDir string,
|
func New(authSvc *macaroons.Service, chainDir string,
|
||||||
params *chaincfg.Params) *UnlockerService {
|
params *chaincfg.Params) *UnlockerService {
|
||||||
return &UnlockerService{
|
return &UnlockerService{
|
||||||
CreatePasswords: make(chan []byte, 1),
|
CreatePasswords: make(chan []byte, 1),
|
||||||
@ -67,6 +68,15 @@ func (u *UnlockerService) CreateWallet(ctx context.Context,
|
|||||||
return nil, fmt.Errorf("wallet already exists")
|
return nil, fmt.Errorf("wallet already exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attempt to create a password for the macaroon service.
|
||||||
|
if u.authSvc != nil {
|
||||||
|
err = u.authSvc.CreateUnlock(&password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to create/unlock "+
|
||||||
|
"macaroon store: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// We send the password over the CreatePasswords channel, such that it
|
// We send the password over the CreatePasswords channel, such that it
|
||||||
// can be used by lnd to open or create the wallet.
|
// can be used by lnd to open or create the wallet.
|
||||||
u.CreatePasswords <- password
|
u.CreatePasswords <- password
|
||||||
@ -109,6 +119,15 @@ func (u *UnlockerService) UnlockWallet(ctx context.Context,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attempt to create a password for the macaroon service.
|
||||||
|
if u.authSvc != nil {
|
||||||
|
err = u.authSvc.CreateUnlock(&in.Password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to create/unlock "+
|
||||||
|
"macaroon store: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// At this point we was able to open the existing wallet with the
|
// At this point we was able to open the existing wallet with the
|
||||||
// provided password. We send the password over the UnlockPasswords
|
// provided password. We send the password over the UnlockPasswords
|
||||||
// channel, such that it can be used by lnd to open the wallet.
|
// channel, such that it can be used by lnd to open the wallet.
|
||||||
|
Loading…
Reference in New Issue
Block a user