From 3213f4ac37829b67a4164e8d4ace91095a83e30f Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Fri, 24 Mar 2023 19:23:48 +0100 Subject: [PATCH] [core] Add validation and reset of encrypted values in order to detect change of encryption pass Signed-off-by: Knut Ahlers --- internal/service/access/access.go | 7 ++++ main.go | 17 ++++++++++ pkg/database/coreKV.go | 54 +++++++++++++++++++++++++++++++ pkg/database/database.go | 2 ++ 4 files changed, 80 insertions(+) diff --git a/internal/service/access/access.go b/internal/service/access/access.go index 18d624d..ff7114f 100644 --- a/internal/service/access/access.go +++ b/internal/service/access/access.go @@ -209,6 +209,13 @@ func (s Service) ListPermittedChannels() ([]string, error) { return out, nil } +func (s Service) RemoveAllExtendedTwitchCredentials() error { + return errors.Wrap( + s.db.DB().Delete(&extendedPermission{}, "1 = 1").Error, + "deleting data from table", + ) +} + func (s Service) RemoveExendedTwitchCredentials(channel string) error { return errors.Wrap( s.db.DB().Delete(&extendedPermission{}, "channel = ?", strings.TrimLeft(channel, "#")).Error, diff --git a/main.go b/main.go index 4f4ee5e..de7509e 100644 --- a/main.go +++ b/main.go @@ -198,6 +198,7 @@ func handleSubCommand(args []string) { fmt.Println(" actor-docs Generate markdown documentation for available actors") fmt.Println(" api-token Generate an api-token to be entered into the config") fmt.Println(" migrate-v2 Migrate old (*.json.gz) storage file into new database") + fmt.Println(" reset-secrets Remove encrypted data to reset encryption passphrase") fmt.Println(" validate-config Try to load configuration file and report errors if any") fmt.Println(" help Prints this help message") @@ -217,6 +218,18 @@ func handleSubCommand(args []string) { log.Info("v2 storage file was migrated") + case "reset-secrets": + // Nuke permission table entries + if err := accessService.RemoveAllExtendedTwitchCredentials(); err != nil { + log.WithError(err).Fatal("resetting Twitch credentials") + } + log.Info("removed stored Twitch credentials") + + if err := db.ResetEncryptedCoreMeta(); err != nil { + log.WithError(err).Fatal("resetting encrypted meta entries") + } + log.Info("removed encrypted meta entries") + case "validate-config": if err := loadConfig(cfg.Config); err != nil { log.WithError(err).Fatal("loading config") @@ -303,6 +316,10 @@ func main() { return } + if err = db.ValidateEncryption(); err != nil { + log.WithError(err).Fatal("validation of database encryption failed, fix encryption passphrase or use 'twitch-bot reset-secrets' to wipe encrypted data") + } + if err = loadConfig(cfg.Config); err != nil { if os.IsNotExist(errors.Cause(err)) { if err = writeDefaultConfigFile(cfg.Config); err != nil { diff --git a/pkg/database/coreKV.go b/pkg/database/coreKV.go index f244c88..94e9018 100644 --- a/pkg/database/coreKV.go +++ b/pkg/database/coreKV.go @@ -2,12 +2,23 @@ package database import ( "bytes" + "crypto/sha512" "encoding/json" + "fmt" "strings" + "time" "github.com/pkg/errors" "gorm.io/gorm" "gorm.io/gorm/clause" + + "github.com/Luzifer/go_helpers/v2/backoff" +) + +const ( + encryptionValidationKey = "encryption-validation" + encryptionValidationMinBackoff = 500 * time.Millisecond + encryptionValidationTries = 5 ) type ( @@ -47,12 +58,55 @@ func (c connector) ReadEncryptedCoreMeta(key string, value any) error { return c.readCoreMeta(key, value, c.DecryptField) } +// ResetEncryptedCoreMeta removes all CoreKV entries from the database +func (c connector) ResetEncryptedCoreMeta() error { + return errors.Wrap( + c.db.Delete(&coreKV{}, "value LIKE ?", "U2FsdGVkX1%").Error, + "removing encrypted meta entries", + ) +} + // StoreEncryptedCoreMeta works like StoreCoreMeta but encrypts the // marshalled value before storing it func (c connector) StoreEncryptedCoreMeta(key string, value any) error { return c.storeCoreMeta(key, value, c.EncryptField) } +func (c connector) ValidateEncryption() error { + validationHasher := sha512.New() + fmt.Fprint(validationHasher, c.encryptionSecret) + + var ( + storedHash string + validationHash = fmt.Sprintf("%x", validationHasher.Sum(nil)) + ) + + err := backoff.NewBackoff(). + WithMaxIterations(encryptionValidationTries). + WithMinIterationTime(encryptionValidationMinBackoff). + Retry(func() error { + return c.ReadEncryptedCoreMeta(encryptionValidationKey, &storedHash) + }) + + switch { + case err == nil: + if storedHash != validationHash { + // Shouldn't happen: When decryption is possible it should match + return errors.New("mismatch between expected and stored hash") + } + return nil + + case errors.Is(err, ErrCoreMetaNotFound): + return errors.Wrap( + c.StoreEncryptedCoreMeta(encryptionValidationKey, validationHash), + "initializing encryption validation", + ) + + default: + return errors.Wrap(err, "reading encryption-validation") + } +} + func (c connector) readCoreMeta(key string, value any, processor func(string) (string, error)) (err error) { var data coreKV diff --git a/pkg/database/database.go b/pkg/database/database.go index 0cfe0ea..7694605 100644 --- a/pkg/database/database.go +++ b/pkg/database/database.go @@ -16,8 +16,10 @@ type ( ReadCoreMeta(key string, value any) error StoreCoreMeta(key string, value any) error ReadEncryptedCoreMeta(key string, value any) error + ResetEncryptedCoreMeta() error StoreEncryptedCoreMeta(key string, value any) error DecryptField(string) (string, error) EncryptField(string) (string, error) + ValidateEncryption() error } )