From 244a56d1bb050be61c3e1a850fd7f5a2a726aea9 Mon Sep 17 00:00:00 2001 From: Rostislav Dugin Date: Wed, 19 Nov 2025 18:53:58 +0300 Subject: [PATCH] FEATURE (secrets): Move secrets to the secret.key file instead of DB --- backend/cmd/main.go | 7 ++ backend/internal/config/config.go | 6 +- .../internal/features/backups/backups/di.go | 7 +- .../features/backups/backups/service.go | 13 ++-- .../features/backups/backups/service_test.go | 15 ++-- .../usecases/postgresql/create_backup_uc.go | 11 ++- .../backups/backups/usecases/postgresql/di.go | 4 +- .../features/encryption/secrets/di.go | 9 +++ .../features/encryption/secrets/model.go | 1 + .../features/encryption/secrets/service.go | 73 +++++++++++++++++++ .../restores/usecases/postgresql/di.go | 4 +- .../usecases/postgresql/restore_backup_uc.go | 8 +- .../features/users/repositories/di.go | 5 -- .../repositories/secret_key_repository.go | 34 --------- .../internal/features/users/services/di.go | 7 +- .../features/users/services/user_services.go | 13 ++-- backend/internal/util/encryption/di.go | 4 +- .../encryption/secret_key_field_encryptor.go | 9 +-- 18 files changed, 144 insertions(+), 86 deletions(-) create mode 100644 backend/internal/features/encryption/secrets/di.go create mode 100644 backend/internal/features/encryption/secrets/model.go create mode 100644 backend/internal/features/encryption/secrets/service.go delete mode 100644 backend/internal/features/users/repositories/secret_key_repository.go diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 99c4a29..a5396a1 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -18,6 +18,7 @@ import ( backups_config "postgresus-backend/internal/features/backups/config" "postgresus-backend/internal/features/databases" "postgresus-backend/internal/features/disk" + "postgresus-backend/internal/features/encryption/secrets" healthcheck_attempt "postgresus-backend/internal/features/healthcheck/attempt" healthcheck_config "postgresus-backend/internal/features/healthcheck/config" "postgresus-backend/internal/features/notifiers" @@ -64,6 +65,12 @@ func main() { os.Exit(1) } + err = secrets.GetSecretKeyService().MigrateKeyFromDbToFileIfExist() + if err != nil { + log.Error("Failed to migrate secret key from database to file", "error", err) + os.Exit(1) + } + err = users_services.GetUserService().CreateInitialAdmin() if err != nil { log.Error("Failed to create initial admin", "error", err) diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 10f6a01..d629473 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -26,8 +26,9 @@ type EnvVariables struct { EnvMode env_utils.EnvMode `env:"ENV_MODE" required:"true"` PostgresesInstallDir string `env:"POSTGRES_INSTALL_DIR"` - DataFolder string - TempFolder string + DataFolder string + TempFolder string + SecretKeyPath string TestGoogleDriveClientID string `env:"TEST_GOOGLE_DRIVE_CLIENT_ID"` TestGoogleDriveClientSecret string `env:"TEST_GOOGLE_DRIVE_CLIENT_SECRET"` @@ -146,6 +147,7 @@ func loadEnvVariables() { // (projectRoot/postgresus-data -> /postgresus-data) env.DataFolder = filepath.Join(filepath.Dir(backendRoot), "postgresus-data", "backups") env.TempFolder = filepath.Join(filepath.Dir(backendRoot), "postgresus-data", "temp") + env.SecretKeyPath = filepath.Join(filepath.Dir(backendRoot), "postgresus-data", "secret.key") if env.IsTesting { if env.TestPostgres12Port == "" { diff --git a/backend/internal/features/backups/backups/di.go b/backend/internal/features/backups/backups/di.go index 7cc965d..769c580 100644 --- a/backend/internal/features/backups/backups/di.go +++ b/backend/internal/features/backups/backups/di.go @@ -1,17 +1,18 @@ package backups import ( + "time" + audit_logs "postgresus-backend/internal/features/audit_logs" "postgresus-backend/internal/features/backups/backups/usecases" backups_config "postgresus-backend/internal/features/backups/config" "postgresus-backend/internal/features/databases" + encryption_secrets "postgresus-backend/internal/features/encryption/secrets" "postgresus-backend/internal/features/notifiers" "postgresus-backend/internal/features/storages" - users_repositories "postgresus-backend/internal/features/users/repositories" workspaces_services "postgresus-backend/internal/features/workspaces/services" "postgresus-backend/internal/util/encryption" "postgresus-backend/internal/util/logger" - "time" ) var backupRepository = &BackupRepository{} @@ -25,7 +26,7 @@ var backupService = &BackupService{ notifiers.GetNotifierService(), notifiers.GetNotifierService(), backups_config.GetBackupConfigService(), - users_repositories.GetSecretKeyRepository(), + encryption_secrets.GetSecretKeyService(), encryption.GetFieldEncryptor(), usecases.GetCreateBackupUsecase(), logger.GetLogger(), diff --git a/backend/internal/features/backups/backups/service.go b/backend/internal/features/backups/backups/service.go index d44eff1..76d9c72 100644 --- a/backend/internal/features/backups/backups/service.go +++ b/backend/internal/features/backups/backups/service.go @@ -7,19 +7,20 @@ import ( "fmt" "io" "log/slog" + "slices" + "strings" + "time" + audit_logs "postgresus-backend/internal/features/audit_logs" "postgresus-backend/internal/features/backups/backups/encryption" backups_config "postgresus-backend/internal/features/backups/config" "postgresus-backend/internal/features/databases" + encryption_secrets "postgresus-backend/internal/features/encryption/secrets" "postgresus-backend/internal/features/notifiers" "postgresus-backend/internal/features/storages" users_models "postgresus-backend/internal/features/users/models" - users_repositories "postgresus-backend/internal/features/users/repositories" workspaces_services "postgresus-backend/internal/features/workspaces/services" util_encryption "postgresus-backend/internal/util/encryption" - "slices" - "strings" - "time" "github.com/google/uuid" ) @@ -31,7 +32,7 @@ type BackupService struct { notifierService *notifiers.NotifierService notificationSender NotificationSender backupConfigService *backups_config.BackupConfigService - secretKeyRepo *users_repositories.SecretKeyRepository + secretKeyService *encryption_secrets.SecretKeyService fieldEncryptor util_encryption.FieldEncryptor createBackupUseCase CreateBackupUsecase @@ -628,7 +629,7 @@ func (s *BackupService) getBackupReader(backupID uuid.UUID) (io.ReadCloser, erro } // Get master key - masterKey, err := s.secretKeyRepo.GetSecretKey() + masterKey, err := s.secretKeyService.GetSecretKey() if err != nil { if closeErr := fileReader.Close(); closeErr != nil { s.logger.Error("Failed to close file reader", "error", closeErr) diff --git a/backend/internal/features/backups/backups/service_test.go b/backend/internal/features/backups/backups/service_test.go index b283ee6..56117c4 100644 --- a/backend/internal/features/backups/backups/service_test.go +++ b/backend/internal/features/backups/backups/service_test.go @@ -3,21 +3,22 @@ package backups import ( "context" "errors" + "strings" + "testing" + "time" + usecases_postgresql "postgresus-backend/internal/features/backups/backups/usecases/postgresql" backups_config "postgresus-backend/internal/features/backups/config" "postgresus-backend/internal/features/databases" + encryption_secrets "postgresus-backend/internal/features/encryption/secrets" "postgresus-backend/internal/features/notifiers" "postgresus-backend/internal/features/storages" users_enums "postgresus-backend/internal/features/users/enums" - users_repositories "postgresus-backend/internal/features/users/repositories" users_testing "postgresus-backend/internal/features/users/testing" workspaces_services "postgresus-backend/internal/features/workspaces/services" workspaces_testing "postgresus-backend/internal/features/workspaces/testing" "postgresus-backend/internal/util/encryption" "postgresus-backend/internal/util/logger" - "strings" - "testing" - "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -56,7 +57,7 @@ func Test_BackupExecuted_NotificationSent(t *testing.T) { notifiers.GetNotifierService(), mockNotificationSender, backups_config.GetBackupConfigService(), - users_repositories.GetSecretKeyRepository(), + encryption_secrets.GetSecretKeyService(), encryption.GetFieldEncryptor(), &CreateFailedBackupUsecase{}, logger.GetLogger(), @@ -104,7 +105,7 @@ func Test_BackupExecuted_NotificationSent(t *testing.T) { notifiers.GetNotifierService(), mockNotificationSender, backups_config.GetBackupConfigService(), - users_repositories.GetSecretKeyRepository(), + encryption_secrets.GetSecretKeyService(), encryption.GetFieldEncryptor(), &CreateSuccessBackupUsecase{}, logger.GetLogger(), @@ -129,7 +130,7 @@ func Test_BackupExecuted_NotificationSent(t *testing.T) { notifiers.GetNotifierService(), mockNotificationSender, backups_config.GetBackupConfigService(), - users_repositories.GetSecretKeyRepository(), + encryption_secrets.GetSecretKeyService(), encryption.GetFieldEncryptor(), &CreateSuccessBackupUsecase{}, logger.GetLogger(), diff --git a/backend/internal/features/backups/backups/usecases/postgresql/create_backup_uc.go b/backend/internal/features/backups/backups/usecases/postgresql/create_backup_uc.go index eed7a4d..f52c902 100644 --- a/backend/internal/features/backups/backups/usecases/postgresql/create_backup_uc.go +++ b/backend/internal/features/backups/backups/usecases/postgresql/create_backup_uc.go @@ -19,8 +19,8 @@ import ( backups_config "postgresus-backend/internal/features/backups/config" "postgresus-backend/internal/features/databases" pgtypes "postgresus-backend/internal/features/databases/databases/postgresql" + encryption_secrets "postgresus-backend/internal/features/encryption/secrets" "postgresus-backend/internal/features/storages" - users_repositories "postgresus-backend/internal/features/users/repositories" "postgresus-backend/internal/util/encryption" "postgresus-backend/internal/util/tools" @@ -34,16 +34,15 @@ const ( progressReportIntervalMB = 1.0 pgConnectTimeout = 30 compressionLevel = 5 - defaultBackupLimit = 1000 exitCodeAccessViolation = -1073741819 exitCodeGenericError = 1 exitCodeConnectionError = 2 ) type CreatePostgresqlBackupUsecase struct { - logger *slog.Logger - secretKeyRepo *users_repositories.SecretKeyRepository - fieldEncryptor encryption.FieldEncryptor + logger *slog.Logger + secretKeyService *encryption_secrets.SecretKeyService + fieldEncryptor encryption.FieldEncryptor } // Execute creates a backup of the database @@ -466,7 +465,7 @@ func (uc *CreatePostgresqlBackupUsecase) setupBackupEncryption( return nil, nil, metadata, fmt.Errorf("failed to generate nonce: %w", err) } - masterKey, err := uc.secretKeyRepo.GetSecretKey() + masterKey, err := uc.secretKeyService.GetSecretKey() if err != nil { return nil, nil, metadata, fmt.Errorf("failed to get master key: %w", err) } diff --git a/backend/internal/features/backups/backups/usecases/postgresql/di.go b/backend/internal/features/backups/backups/usecases/postgresql/di.go index dd2c955..1086f15 100644 --- a/backend/internal/features/backups/backups/usecases/postgresql/di.go +++ b/backend/internal/features/backups/backups/usecases/postgresql/di.go @@ -1,14 +1,14 @@ package usecases_postgresql import ( - users_repositories "postgresus-backend/internal/features/users/repositories" + "postgresus-backend/internal/features/encryption/secrets" "postgresus-backend/internal/util/encryption" "postgresus-backend/internal/util/logger" ) var createPostgresqlBackupUsecase = &CreatePostgresqlBackupUsecase{ logger.GetLogger(), - users_repositories.GetSecretKeyRepository(), + secrets.GetSecretKeyService(), encryption.GetFieldEncryptor(), } diff --git a/backend/internal/features/encryption/secrets/di.go b/backend/internal/features/encryption/secrets/di.go new file mode 100644 index 0000000..c44e68b --- /dev/null +++ b/backend/internal/features/encryption/secrets/di.go @@ -0,0 +1,9 @@ +package secrets + +var secretKeyService = &SecretKeyService{ + nil, +} + +func GetSecretKeyService() *SecretKeyService { + return secretKeyService +} diff --git a/backend/internal/features/encryption/secrets/model.go b/backend/internal/features/encryption/secrets/model.go new file mode 100644 index 0000000..b2b97d5 --- /dev/null +++ b/backend/internal/features/encryption/secrets/model.go @@ -0,0 +1 @@ +package secrets diff --git a/backend/internal/features/encryption/secrets/service.go b/backend/internal/features/encryption/secrets/service.go new file mode 100644 index 0000000..4e390e4 --- /dev/null +++ b/backend/internal/features/encryption/secrets/service.go @@ -0,0 +1,73 @@ +package secrets + +import ( + "errors" + "fmt" + "os" + + "postgresus-backend/internal/config" + user_models "postgresus-backend/internal/features/users/models" + "postgresus-backend/internal/storage" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type SecretKeyService struct { + cachedKey *string +} + +func (s *SecretKeyService) MigrateKeyFromDbToFileIfExist() error { + var secretKey user_models.SecretKey + + err := storage.GetDb().First(&secretKey).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return fmt.Errorf("failed to check for secret key in database: %w", err) + } + + if secretKey.Secret == "" { + return nil + } + + secretKeyPath := config.GetEnv().SecretKeyPath + if err := os.WriteFile(secretKeyPath, []byte(secretKey.Secret), 0600); err != nil { + return fmt.Errorf("failed to write secret key to file: %w", err) + } + + if err := storage.GetDb().Exec("DELETE FROM secret_keys").Error; err != nil { + return fmt.Errorf("failed to delete secret key from database: %w", err) + } + + return nil +} + +func (s *SecretKeyService) GetSecretKey() (string, error) { + if s.cachedKey != nil { + return *s.cachedKey, nil + } + + secretKeyPath := config.GetEnv().SecretKeyPath + data, err := os.ReadFile(secretKeyPath) + if err != nil { + if os.IsNotExist(err) { + newKey := s.generateNewSecretKey() + if err := os.WriteFile(secretKeyPath, []byte(newKey), 0600); err != nil { + return "", fmt.Errorf("failed to write new secret key: %w", err) + } + s.cachedKey = &newKey + return newKey, nil + } + return "", fmt.Errorf("failed to read secret key file: %w", err) + } + + key := string(data) + s.cachedKey = &key + return key, nil +} + +func (s *SecretKeyService) generateNewSecretKey() string { + return uuid.New().String() + uuid.New().String() +} diff --git a/backend/internal/features/restores/usecases/postgresql/di.go b/backend/internal/features/restores/usecases/postgresql/di.go index b099f40..9f5e6df 100644 --- a/backend/internal/features/restores/usecases/postgresql/di.go +++ b/backend/internal/features/restores/usecases/postgresql/di.go @@ -1,13 +1,13 @@ package usecases_postgresql import ( - users_repositories "postgresus-backend/internal/features/users/repositories" + "postgresus-backend/internal/features/encryption/secrets" "postgresus-backend/internal/util/logger" ) var restorePostgresqlBackupUsecase = &RestorePostgresqlBackupUsecase{ logger.GetLogger(), - users_repositories.GetSecretKeyRepository(), + secrets.GetSecretKeyService(), } func GetRestorePostgresqlBackupUsecase() *RestorePostgresqlBackupUsecase { diff --git a/backend/internal/features/restores/usecases/postgresql/restore_backup_uc.go b/backend/internal/features/restores/usecases/postgresql/restore_backup_uc.go index 8b828e6..8a7b593 100644 --- a/backend/internal/features/restores/usecases/postgresql/restore_backup_uc.go +++ b/backend/internal/features/restores/usecases/postgresql/restore_backup_uc.go @@ -20,9 +20,9 @@ import ( backups_config "postgresus-backend/internal/features/backups/config" "postgresus-backend/internal/features/databases" pgtypes "postgresus-backend/internal/features/databases/databases/postgresql" + encryption_secrets "postgresus-backend/internal/features/encryption/secrets" "postgresus-backend/internal/features/restores/models" "postgresus-backend/internal/features/storages" - users_repositories "postgresus-backend/internal/features/users/repositories" util_encryption "postgresus-backend/internal/util/encryption" files_utils "postgresus-backend/internal/util/files" "postgresus-backend/internal/util/tools" @@ -31,8 +31,8 @@ import ( ) type RestorePostgresqlBackupUsecase struct { - logger *slog.Logger - secretKeyRepo *users_repositories.SecretKeyRepository + logger *slog.Logger + secretKeyService *encryption_secrets.SecretKeyService } func (uc *RestorePostgresqlBackupUsecase) Execute( @@ -232,7 +232,7 @@ func (uc *RestorePostgresqlBackupUsecase) downloadBackupToTempFile( } // Get master key - masterKey, err := uc.secretKeyRepo.GetSecretKey() + masterKey, err := uc.secretKeyService.GetSecretKey() if err != nil { cleanupFunc() return "", nil, fmt.Errorf("failed to get master key for decryption: %w", err) diff --git a/backend/internal/features/users/repositories/di.go b/backend/internal/features/users/repositories/di.go index 3e5f324..386d9da 100644 --- a/backend/internal/features/users/repositories/di.go +++ b/backend/internal/features/users/repositories/di.go @@ -1,13 +1,8 @@ package users_repositories -var secretKeyRepository = &SecretKeyRepository{} var userRepository = &UserRepository{} var usersSettingsRepository = &UsersSettingsRepository{} -func GetSecretKeyRepository() *SecretKeyRepository { - return secretKeyRepository -} - func GetUserRepository() *UserRepository { return userRepository } diff --git a/backend/internal/features/users/repositories/secret_key_repository.go b/backend/internal/features/users/repositories/secret_key_repository.go deleted file mode 100644 index 23fe726..0000000 --- a/backend/internal/features/users/repositories/secret_key_repository.go +++ /dev/null @@ -1,34 +0,0 @@ -package users_repositories - -import ( - "errors" - user_models "postgresus-backend/internal/features/users/models" - "postgresus-backend/internal/storage" - - "github.com/google/uuid" - "gorm.io/gorm" -) - -type SecretKeyRepository struct{} - -func (r *SecretKeyRepository) GetSecretKey() (string, error) { - var secretKey user_models.SecretKey - - if err := storage.GetDb().First(&secretKey).Error; err != nil { - // create a new secret key if not found - if errors.Is(err, gorm.ErrRecordNotFound) { - newSecretKey := user_models.SecretKey{ - Secret: uuid.New().String() + uuid.New().String(), - } - if err := storage.GetDb().Create(&newSecretKey).Error; err != nil { - return "", errors.New("failed to create new secret key") - } - - return newSecretKey.Secret, nil - } - - return "", err - } - - return secretKey.Secret, nil -} diff --git a/backend/internal/features/users/services/di.go b/backend/internal/features/users/services/di.go index 858e3a5..0bcabef 100644 --- a/backend/internal/features/users/services/di.go +++ b/backend/internal/features/users/services/di.go @@ -1,10 +1,13 @@ package users_services -import users_repositories "postgresus-backend/internal/features/users/repositories" +import ( + "postgresus-backend/internal/features/encryption/secrets" + users_repositories "postgresus-backend/internal/features/users/repositories" +) var userService = &UserService{ users_repositories.GetUserRepository(), - users_repositories.GetSecretKeyRepository(), + secrets.GetSecretKeyService(), settingsService, nil, } diff --git a/backend/internal/features/users/services/user_services.go b/backend/internal/features/users/services/user_services.go index 9e7a60f..2170622 100644 --- a/backend/internal/features/users/services/user_services.go +++ b/backend/internal/features/users/services/user_services.go @@ -17,6 +17,7 @@ import ( "golang.org/x/oauth2/google" "postgresus-backend/internal/config" + "postgresus-backend/internal/features/encryption/secrets" users_dto "postgresus-backend/internal/features/users/dto" users_enums "postgresus-backend/internal/features/users/enums" users_interfaces "postgresus-backend/internal/features/users/interfaces" @@ -25,10 +26,10 @@ import ( ) type UserService struct { - userRepository *users_repositories.UserRepository - secretKeyRepository *users_repositories.SecretKeyRepository - settingsService *SettingsService - auditLogWriter users_interfaces.AuditLogWriter + userRepository *users_repositories.UserRepository + secretKeyService *secrets.SecretKeyService + settingsService *SettingsService + auditLogWriter users_interfaces.AuditLogWriter } func (s *UserService) SetAuditLogWriter(writer users_interfaces.AuditLogWriter) { @@ -162,7 +163,7 @@ func (s *UserService) SignIn( } func (s *UserService) GetUserFromToken(token string) (*users_models.User, error) { - secretKey, err := s.secretKeyRepository.GetSecretKey() + secretKey, err := s.secretKeyService.GetSecretKey() if err != nil { return nil, fmt.Errorf("failed to get secret key: %w", err) } @@ -221,7 +222,7 @@ func (s *UserService) GetUserFromToken(token string) (*users_models.User, error) func (s *UserService) GenerateAccessToken( user *users_models.User, ) (*users_dto.SignInResponseDTO, error) { - secretKey, err := s.secretKeyRepository.GetSecretKey() + secretKey, err := s.secretKeyService.GetSecretKey() if err != nil { return nil, fmt.Errorf("failed to get secret key: %w", err) } diff --git a/backend/internal/util/encryption/di.go b/backend/internal/util/encryption/di.go index a6791e5..4e6893e 100644 --- a/backend/internal/util/encryption/di.go +++ b/backend/internal/util/encryption/di.go @@ -1,9 +1,9 @@ package encryption -import users_repositories "postgresus-backend/internal/features/users/repositories" +import "postgresus-backend/internal/features/encryption/secrets" var fieldEncryptor = &SecretKeyFieldEncryptor{ - users_repositories.GetSecretKeyRepository(), + secrets.GetSecretKeyService(), } func GetFieldEncryptor() FieldEncryptor { diff --git a/backend/internal/util/encryption/secret_key_field_encryptor.go b/backend/internal/util/encryption/secret_key_field_encryptor.go index 0cceb94..eb097c2 100644 --- a/backend/internal/util/encryption/secret_key_field_encryptor.go +++ b/backend/internal/util/encryption/secret_key_field_encryptor.go @@ -8,17 +8,16 @@ import ( "encoding/base64" "errors" "fmt" + "postgresus-backend/internal/features/encryption/secrets" "strings" - users_repositories "postgresus-backend/internal/features/users/repositories" - "github.com/google/uuid" ) const encryptedPrefix = "enc:" type SecretKeyFieldEncryptor struct { - secretKeyRepository *users_repositories.SecretKeyRepository + secretKeyService *secrets.SecretKeyService } func (e *SecretKeyFieldEncryptor) Encrypt(itemID uuid.UUID, plaintext string) (string, error) { @@ -30,7 +29,7 @@ func (e *SecretKeyFieldEncryptor) Encrypt(itemID uuid.UUID, plaintext string) (s return plaintext, nil } - masterKey, err := e.secretKeyRepository.GetSecretKey() + masterKey, err := e.secretKeyService.GetSecretKey() if err != nil { return "", fmt.Errorf("failed to get master key: %w", err) } @@ -82,7 +81,7 @@ func (e *SecretKeyFieldEncryptor) Decrypt(itemID uuid.UUID, ciphertext string) ( return "", fmt.Errorf("failed to decode ciphertext: %w", err) } - masterKey, err := e.secretKeyRepository.GetSecretKey() + masterKey, err := e.secretKeyService.GetSecretKey() if err != nil { return "", fmt.Errorf("failed to get master key: %w", err) }