From ae71d60715da0fc0770acdd5445a8c15f75074c0 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 6 Oct 2020 17:23:30 +0200 Subject: [PATCH] macaroons: add ability to change the password or create a new root key --- macaroons/service.go | 16 +++- macaroons/store.go | 171 +++++++++++++++++++++++++++++++++++----- macaroons/store_test.go | 152 +++++++++++++++++++++++++++++------ 3 files changed, 290 insertions(+), 49 deletions(-) diff --git a/macaroons/service.go b/macaroons/service.go index 8f3402d0..50a59b0e 100644 --- a/macaroons/service.go +++ b/macaroons/service.go @@ -257,8 +257,8 @@ func (svc *Service) ValidateMacaroon(ctx context.Context, return err } - // Check the method being called against the permitted operation and - // the expiration time and IP address and return the result. + // Check the method being called against the permitted operation, the + // expiration time and IP address and return the result. authChecker := svc.Checker.Auth(macaroon.Slice{mac}) _, err = authChecker.Allow(ctx, requiredPermissions...) @@ -325,3 +325,15 @@ func (svc *Service) DeleteMacaroonID(ctxt context.Context, rootKeyID []byte) ([]byte, error) { return svc.rks.DeleteMacaroonID(ctxt, rootKeyID) } + +// GenerateNewRootKey calls the underlying root key store's GenerateNewRootKey +// and returns the result. +func (svc *Service) GenerateNewRootKey() error { + return svc.rks.GenerateNewRootKey() +} + +// ChangePassword calls the underlying root key store's ChangePassword and +// returns the result. +func (svc *Service) ChangePassword(oldPw, newPw []byte) error { + return svc.rks.ChangePassword(oldPw, newPw) +} diff --git a/macaroons/store.go b/macaroons/store.go index 4bf75824..c6bc12c8 100644 --- a/macaroons/store.go +++ b/macaroons/store.go @@ -11,6 +11,7 @@ import ( "github.com/lightningnetwork/lnd/channeldb/kvdb" "github.com/btcsuite/btcwallet/snacl" + "github.com/btcsuite/btcwallet/walletdb" ) const ( @@ -26,10 +27,10 @@ var ( // just 0, to emulate the memory storage that comes with bakery. DefaultRootKeyID = []byte("0") - // encryptedKeyID is the name of the database key that stores the + // encryptionKeyID is the name of the database key that stores the // encryption key, encrypted with a salted + hashed password. The // format is 32 bytes of salt, and the rest is encrypted key. - encryptedKeyID = []byte("enckey") + encryptionKeyID = []byte("enckey") // ErrAlreadyUnlocked specifies that the store has already been // unlocked. @@ -45,6 +46,15 @@ var ( // ErrKeyValueForbidden is used when the root key ID uses encryptedKeyID as // its value. ErrKeyValueForbidden = fmt.Errorf("root key ID value is not allowed") + + // ErrRootKeyBucketNotFound specifies that there is no macaroon root key + // bucket yet which can/should only happen if the store has been + // corrupted or was initialized incorrectly. + ErrRootKeyBucketNotFound = fmt.Errorf("root key bucket not found") + + // ErrEncKeyNotFound specifies that there was no encryption key found + // even if one was expected to be generated. + ErrEncKeyNotFound = fmt.Errorf("macaroon encryption key not found") ) // RootKeyStorage implements the bakery.RootKeyStorage interface. @@ -89,7 +99,10 @@ func (r *RootKeyStorage) CreateUnlock(password *[]byte) error { return kvdb.Update(r, func(tx kvdb.RwTx) error { bucket := tx.ReadWriteBucket(rootKeyBucketName) - dbKey := bucket.Get(encryptedKeyID) + if bucket == nil { + return ErrRootKeyBucketNotFound + } + dbKey := bucket.Get(encryptionKeyID) if len(dbKey) > 0 { // We've already stored a key, so try to unlock with // the password. @@ -116,7 +129,7 @@ func (r *RootKeyStorage) CreateUnlock(password *[]byte) error { return err } - err = bucket.Put(encryptedKeyID, encKey.Marshal()) + err = bucket.Put(encryptionKeyID, encKey.Marshal()) if err != nil { return err } @@ -126,6 +139,83 @@ func (r *RootKeyStorage) CreateUnlock(password *[]byte) error { }, func() {}) } +// ChangePassword decrypts the macaroon root key with the old password and then +// encrypts it again with the new password. +func (r *RootKeyStorage) ChangePassword(oldPw, newPw []byte) error { + // We need the store to already be unlocked. With this we can make sure + // that there already is a key in the DB. + if r.encKey == nil { + return ErrStoreLocked + } + + // Check if a nil password has been passed; return an error if so. + if oldPw == nil || newPw == nil { + return ErrPasswordRequired + } + + return kvdb.Update(r, func(tx kvdb.RwTx) error { + bucket := tx.ReadWriteBucket(rootKeyBucketName) + if bucket == nil { + return ErrRootKeyBucketNotFound + } + encKeyDb := bucket.Get(encryptionKeyID) + rootKeyDb := bucket.Get(DefaultRootKeyID) + + // Both the encryption key and the root key must be present + // otherwise we are in the wrong state to change the password. + if len(encKeyDb) == 0 || len(rootKeyDb) == 0 { + return ErrEncKeyNotFound + } + + // Unmarshal parameters for old encryption key and derive the + // old key with them. + encKeyOld := &snacl.SecretKey{} + err := encKeyOld.Unmarshal(encKeyDb) + if err != nil { + return err + } + err = encKeyOld.DeriveKey(&oldPw) + if err != nil { + return err + } + + // Create a new encryption key from the new password. + encKeyNew, err := snacl.NewSecretKey( + &newPw, scryptN, scryptR, scryptP, + ) + if err != nil { + return err + } + + // Now try to decrypt the root key with the old encryption key, + // encrypt it with the new one and then store it in the DB. + decryptedKey, err := encKeyOld.Decrypt(rootKeyDb) + if err != nil { + return err + } + rootKey := make([]byte, len(decryptedKey)) + copy(rootKey, decryptedKey) + encryptedKey, err := encKeyNew.Encrypt(rootKey) + if err != nil { + return err + } + err = bucket.Put(DefaultRootKeyID, encryptedKey) + if err != nil { + return err + } + + // Finally, store the new encryption key parameters in the DB + // as well. + err = bucket.Put(encryptionKeyID, encKeyNew.Marshal()) + if err != nil { + return err + } + + r.encKey = encKeyNew + return nil + }, func() {}) +} + // Get implements the Get method for the bakery.RootKeyStorage interface. func (r *RootKeyStorage) Get(_ context.Context, id []byte) ([]byte, error) { r.encKeyMtx.RLock() @@ -136,7 +226,11 @@ func (r *RootKeyStorage) Get(_ context.Context, id []byte) ([]byte, error) { } var rootKey []byte err := kvdb.View(r, func(tx kvdb.RTx) error { - dbKey := tx.ReadBucket(rootKeyBucketName).Get(id) + bucket := tx.ReadBucket(rootKeyBucketName) + if bucket == nil { + return ErrRootKeyBucketNotFound + } + dbKey := bucket.Get(id) if len(dbKey) == 0 { return fmt.Errorf("root key with id %s doesn't exist", string(id)) @@ -178,13 +272,16 @@ func (r *RootKeyStorage) RootKey(ctx context.Context) ([]byte, []byte, error) { return nil, nil, err } - if bytes.Equal(id, encryptedKeyID) { + if bytes.Equal(id, encryptionKeyID) { return nil, nil, ErrKeyValueForbidden } err = kvdb.Update(r, func(tx kvdb.RwTx) error { - ns := tx.ReadWriteBucket(rootKeyBucketName) - dbKey := ns.Get(id) + bucket := tx.ReadWriteBucket(rootKeyBucketName) + if bucket == nil { + return ErrRootKeyBucketNotFound + } + dbKey := bucket.Get(id) // If there's a root key stored in the bucket, decrypt it and // return it. @@ -199,18 +296,11 @@ func (r *RootKeyStorage) RootKey(ctx context.Context) ([]byte, []byte, error) { return nil } - // Otherwise, create a RootKeyLen-byte root key, encrypt it, + // Otherwise, create a new root key, encrypt it, // and store it in the bucket. - rootKey = make([]byte, RootKeyLen) - if _, err := io.ReadFull(rand.Reader, rootKey[:]); err != nil { - return err - } - - encKey, err := r.encKey.Encrypt(rootKey) - if err != nil { - return err - } - return ns.Put(id, encKey) + newKey, err := generateAndStoreNewRootKey(bucket, id, r.encKey) + rootKey = newKey + return err }, func() { rootKey = nil }) @@ -221,6 +311,26 @@ func (r *RootKeyStorage) RootKey(ctx context.Context) ([]byte, []byte, error) { return rootKey, id, nil } +// GenerateNewRootKey generates a new macaroon root key, replacing the previous +// root key if it existed. +func (r *RootKeyStorage) GenerateNewRootKey() error { + // We need the store to already be unlocked. With this we can make sure + // that there already is a key in the DB that can be replaced. + if r.encKey == nil { + return ErrStoreLocked + } + return kvdb.Update(r, func(tx kvdb.RwTx) error { + bucket := tx.ReadWriteBucket(rootKeyBucketName) + if bucket == nil { + return ErrRootKeyBucketNotFound + } + _, err := generateAndStoreNewRootKey( + bucket, DefaultRootKeyID, r.encKey, + ) + return err + }, func() {}) +} + // Close closes the underlying database and zeroes the encryption key stored // in memory. func (r *RootKeyStorage) Close() error { @@ -229,10 +339,29 @@ func (r *RootKeyStorage) Close() error { if r.encKey != nil { r.encKey.Zero() + r.encKey = nil } return r.Backend.Close() } +// generateAndStoreNewRootKey creates a new random RootKeyLen-byte root key, +// encrypts it with the given encryption key and stores it in the bucket. +// Any previously set key will be overwritten. +func generateAndStoreNewRootKey(bucket walletdb.ReadWriteBucket, id []byte, + key *snacl.SecretKey) ([]byte, error) { + + rootKey := make([]byte, RootKeyLen) + if _, err := io.ReadFull(rand.Reader, rootKey); err != nil { + return nil, err + } + + encryptedKey, err := key.Encrypt(rootKey) + if err != nil { + return nil, err + } + return rootKey, bucket.Put(id, encryptedKey) +} + // ListMacaroonIDs returns all the root key ID values except the value of // encryptedKeyID. func (r *RootKeyStorage) ListMacaroonIDs(_ context.Context) ([][]byte, error) { @@ -254,7 +383,7 @@ func (r *RootKeyStorage) ListMacaroonIDs(_ context.Context) ([][]byte, error) { // to rootKeySlice. appendRootKey := func(k, _ []byte) error { // Only append when the key value is not encryptedKeyID. - if !bytes.Equal(k, encryptedKeyID) { + if !bytes.Equal(k, encryptionKeyID) { rootKeySlice = append(rootKeySlice, k) } return nil @@ -290,7 +419,7 @@ func (r *RootKeyStorage) DeleteMacaroonID( } // Deleting encryptedKeyID or DefaultRootKeyID is not allowed. - if bytes.Equal(rootKeyID, encryptedKeyID) || + if bytes.Equal(rootKeyID, encryptionKeyID) || bytes.Equal(rootKeyID, DefaultRootKeyID) { return nil, ErrDeletionForbidden diff --git a/macaroons/store_test.go b/macaroons/store_test.go index 2f381934..b4427eb1 100644 --- a/macaroons/store_test.go +++ b/macaroons/store_test.go @@ -14,12 +14,31 @@ import ( "github.com/stretchr/testify/require" ) -func TestStore(t *testing.T) { +var ( + defaultRootKeyIDContext = macaroons.ContextWithRootKeyID( + context.Background(), macaroons.DefaultRootKeyID, + ) +) + +// newTestStore creates a new bolt DB in a temporary directory and then +// initializes a root key storage for that DB. +func newTestStore(t *testing.T) (string, func(), *macaroons.RootKeyStorage) { tempDir, err := ioutil.TempDir("", "macaroonstore-") require.NoError(t, err) - defer func() { + + cleanup, store := openTestStore(t, tempDir) + cleanup2 := func() { + cleanup() _ = os.RemoveAll(tempDir) - }() + } + + return tempDir, cleanup2, store +} + +// openTestStore opens an existing bolt DB and then initializes a root key +// storage for that DB. +func openTestStore(t *testing.T, tempDir string) (func(), + *macaroons.RootKeyStorage) { db, err := kvdb.Create( kvdb.BoltBackendName, path.Join(tempDir, "weks.db"), true, @@ -31,11 +50,21 @@ func TestStore(t *testing.T) { _ = db.Close() t.Fatalf("Error creating root key store: %v", err) } - defer func() { - _ = store.Close() - }() - _, _, err = store.RootKey(context.TODO()) + cleanup := func() { + _ = store.Close() + } + + return cleanup, store +} + +// TestStore tests the normal use cases of the store like creating, unlocking, +// reading keys and closing it. +func TestStore(t *testing.T) { + tempDir, cleanup, store := newTestStore(t) + defer cleanup() + + _, _, err := store.RootKey(context.TODO()) require.Equal(t, macaroons.ErrStoreLocked, err) _, err = store.Get(context.TODO(), nil) @@ -63,16 +92,13 @@ func TestStore(t *testing.T) { require.Equal(t, macaroons.ErrKeyValueForbidden, err) // Create a context with root key ID value. - ctx := macaroons.ContextWithRootKeyID( - context.TODO(), macaroons.DefaultRootKeyID, - ) - key, id, err := store.RootKey(ctx) + key, id, err := store.RootKey(defaultRootKeyIDContext) require.NoError(t, err) rootID := id require.Equal(t, macaroons.DefaultRootKeyID, rootID) - key2, err := store.Get(ctx, id) + key2, err := store.Get(defaultRootKeyIDContext, id) require.NoError(t, err) require.Equal(t, key, key2) @@ -85,16 +111,7 @@ func TestStore(t *testing.T) { // 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 = kvdb.Create( - kvdb.BoltBackendName, path.Join(tempDir, "weks.db"), true, - ) - require.NoError(t, err) - - store, err = macaroons.NewRootKeyStorage(db) - if err != nil { - _ = db.Close() - t.Fatalf("Error creating root key store: %v", err) - } + _, store = openTestStore(t, tempDir) err = store.CreateUnlock(&badpw) require.Equal(t, snacl.ErrInvalidPassword, err) @@ -102,21 +119,104 @@ func TestStore(t *testing.T) { err = store.CreateUnlock(nil) require.Equal(t, macaroons.ErrPasswordRequired, err) - _, _, err = store.RootKey(ctx) + _, _, err = store.RootKey(defaultRootKeyIDContext) require.Equal(t, macaroons.ErrStoreLocked, err) - _, err = store.Get(ctx, nil) + _, err = store.Get(defaultRootKeyIDContext, nil) require.Equal(t, macaroons.ErrStoreLocked, err) err = store.CreateUnlock(&pw) require.NoError(t, err) - key, err = store.Get(ctx, rootID) + key, err = store.Get(defaultRootKeyIDContext, rootID) require.NoError(t, err) require.Equal(t, key, key2) - key, id, err = store.RootKey(ctx) + key, id, err = store.RootKey(defaultRootKeyIDContext) require.NoError(t, err) require.Equal(t, key, key2) require.Equal(t, rootID, id) } + +// TestStoreGenerateNewRootKey tests that a root key can be replaced with a new +// one in the store without changing the password. +func TestStoreGenerateNewRootKey(t *testing.T) { + _, cleanup, store := newTestStore(t) + defer cleanup() + + // The store must be unlocked to replace the root key. + err := store.GenerateNewRootKey() + require.Equal(t, macaroons.ErrStoreLocked, err) + + // Unlock the store and read the current key. + pw := []byte("weks") + err = store.CreateUnlock(&pw) + require.NoError(t, err) + oldRootKey, _, err := store.RootKey(defaultRootKeyIDContext) + require.NoError(t, err) + + // Replace the root key with a new random key. + err = store.GenerateNewRootKey() + require.NoError(t, err) + + // Finally, read the root key from the DB and compare it to the one + // we got returned earlier. This makes sure that the encryption/ + // decryption of the key in the DB worked as expected too. + newRootKey, _, err := store.RootKey(defaultRootKeyIDContext) + require.NoError(t, err) + require.NotEqual(t, oldRootKey, newRootKey) +} + +// TestStoreChangePassword tests that the password for the store can be changed +// without changing the root key. +func TestStoreChangePassword(t *testing.T) { + tempDir, cleanup, store := newTestStore(t) + defer cleanup() + + // The store must be unlocked to replace the root key. + err := store.ChangePassword(nil, nil) + require.Equal(t, macaroons.ErrStoreLocked, err) + + // Unlock the DB and read the current root key. This will need to stay + // the same after changing the password for the test to succeed. + pw := []byte("weks") + err = store.CreateUnlock(&pw) + require.NoError(t, err) + rootKey, _, err := store.RootKey(defaultRootKeyIDContext) + require.NoError(t, err) + + // Both passwords must be set. + err = store.ChangePassword(nil, nil) + require.Equal(t, macaroons.ErrPasswordRequired, err) + + // Make sure that an error is returned if we try to change the password + // without the correct old password. + wrongPw := []byte("wrong") + newPw := []byte("newpassword") + err = store.ChangePassword(wrongPw, newPw) + require.Equal(t, snacl.ErrInvalidPassword, err) + + // Now really do change the password. + err = store.ChangePassword(pw, newPw) + require.NoError(t, err) + + // Close the store. This will close the underlying DB and we need to + // create a new store instance. Let's make sure we can't use it again + // after closing. + err = store.Close() + require.NoError(t, err) + + err = store.CreateUnlock(&newPw) + require.Error(t, err) + + // Let's open it again and try unlocking with the new password. + _, store = openTestStore(t, tempDir) + err = store.CreateUnlock(&newPw) + require.NoError(t, err) + + // Finally read the root key from the DB using the new password and + // make sure the root key stayed the same. + rootKeyDb, _, err := store.RootKey(defaultRootKeyIDContext) + require.NoError(t, err) + require.Equal(t, rootKey, rootKeyDb) +}