feat: add backup encryption using public key and decryption using private key

This commit is contained in:
Jonas Kaninda
2024-10-08 11:04:46 +02:00
parent 815aae28f8
commit 35c4a5475e
9 changed files with 339 additions and 69 deletions

View File

@@ -166,9 +166,10 @@ func localBackup(db *dbConfig, config *BackupConfig) {
BackupDatabase(db, config.backupFileName, disableCompression)
finalFileName := config.backupFileName
if config.encryption {
encryptBackup(config.backupFileName, config.passphrase)
encryptBackup(config)
finalFileName = fmt.Sprintf("%s.%s", config.backupFileName, gpgExtension)
}
utils.Info("Backup name is %s", finalFileName)
moveToBackup(finalFileName, storagePath)
//Send notification
@@ -189,7 +190,7 @@ func s3Backup(db *dbConfig, config *BackupConfig) {
BackupDatabase(db, config.backupFileName, disableCompression)
finalFileName := config.backupFileName
if config.encryption {
encryptBackup(config.backupFileName, config.passphrase)
encryptBackup(config)
finalFileName = fmt.Sprintf("%s.%s", config.backupFileName, "gpg")
}
utils.Info("Uploading backup archive to remote storage S3 ... ")
@@ -226,7 +227,7 @@ func sshBackup(db *dbConfig, config *BackupConfig) {
BackupDatabase(db, config.backupFileName, disableCompression)
finalFileName := config.backupFileName
if config.encryption {
encryptBackup(config.backupFileName, config.passphrase)
encryptBackup(config)
finalFileName = fmt.Sprintf("%s.%s", config.backupFileName, "gpg")
}
utils.Info("Uploading backup archive to remote storage ... ")
@@ -261,7 +262,7 @@ func ftpBackup(db *dbConfig, config *BackupConfig) {
BackupDatabase(db, config.backupFileName, disableCompression)
finalFileName := config.backupFileName
if config.encryption {
encryptBackup(config.backupFileName, config.passphrase)
encryptBackup(config)
finalFileName = fmt.Sprintf("%s.%s", config.backupFileName, "gpg")
}
utils.Info("Uploading backup archive to the remote FTP server ... ")
@@ -291,10 +292,18 @@ func ftpBackup(db *dbConfig, config *BackupConfig) {
deleteTemp()
}
func encryptBackup(backupFileName, gpqPassphrase string) {
err := Encrypt(filepath.Join(tmpPath, backupFileName), gpqPassphrase)
if err != nil {
utils.Fatal("Error during encrypting backup %v", err)
func encryptBackup(config *BackupConfig) {
if config.usingKey {
err := encrypt(filepath.Join(tmpPath, config.backupFileName), config.publicKey)
if err != nil {
utils.Fatal("Error during encrypting backup %v", err)
}
} else if config.passphrase != "" {
err := Encrypt(filepath.Join(tmpPath, config.backupFileName), config.passphrase)
if err != nil {
utils.Fatal("Error during encrypting backup %v", err)
}
}
}

View File

@@ -40,9 +40,11 @@ type BackupConfig struct {
backupRetention int
disableCompression bool
prune bool
encryption bool
remotePath string
encryption bool
usingKey bool
passphrase string
publicKey string
storage string
cronExpression string
}
@@ -163,10 +165,14 @@ func initBackupConfig(cmd *cobra.Command) *BackupConfig {
_ = utils.GetEnv(cmd, "path", "AWS_S3_PATH")
cronExpression := os.Getenv("BACKUP_CRON_EXPRESSION")
if passphrase != "" {
publicKeyFile, err := checkPubKeyFile(os.Getenv("GPG_PUBLIC_KEY"))
if err == nil {
encryption = true
usingKey = true
} else if passphrase != "" {
encryption = true
usingKey = false
}
//Initialize backup configs
config := BackupConfig{}
config.backupRetention = backupRetention
@@ -176,17 +182,21 @@ func initBackupConfig(cmd *cobra.Command) *BackupConfig {
config.encryption = encryption
config.remotePath = remotePath
config.passphrase = passphrase
config.publicKey = publicKeyFile
config.usingKey = usingKey
config.cronExpression = cronExpression
return &config
}
type RestoreConfig struct {
s3Path string
remotePath string
storage string
file string
bucket string
gpqPassphrase string
s3Path string
remotePath string
storage string
file string
bucket string
usingKey bool
passphrase string
privateKey string
}
func initRestoreConfig(cmd *cobra.Command) *RestoreConfig {
@@ -199,7 +209,14 @@ func initRestoreConfig(cmd *cobra.Command) *RestoreConfig {
storage = utils.GetEnv(cmd, "storage", "STORAGE")
file = utils.GetEnv(cmd, "file", "FILE_NAME")
bucket := utils.GetEnvVariable("AWS_S3_BUCKET_NAME", "BUCKET_NAME")
gpqPassphrase := os.Getenv("GPG_PASSPHRASE")
passphrase := os.Getenv("GPG_PASSPHRASE")
privateKeyFile, err := checkPrKeyFile(os.Getenv("GPG_PRIVATE_KEY"))
if err == nil {
usingKey = true
} else if passphrase != "" {
usingKey = false
}
//Initialize restore configs
rConfig := RestoreConfig{}
rConfig.s3Path = s3Path
@@ -208,7 +225,9 @@ func initRestoreConfig(cmd *cobra.Command) *RestoreConfig {
rConfig.bucket = bucket
rConfig.file = file
rConfig.storage = storage
rConfig.gpqPassphrase = gpqPassphrase
rConfig.passphrase = passphrase
rConfig.usingKey = usingKey
rConfig.privateKey = privateKeyFile
return &rConfig
}
func initTargetDbConfig() *targetDbConfig {

View File

@@ -7,14 +7,19 @@
package pkg
import (
"errors"
"fmt"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/jkaninda/pg-bkup/utils"
"os"
"os/exec"
"strings"
)
// Decrypt decrypts backup file using a passphrase
func Decrypt(inputFile string, passphrase string) error {
utils.Info("Decrypting backup file: %s...", inputFile)
utils.Info("Decrypting backup using passphrase...")
//Create gpg home dir
err := utils.MakeDirAll(gpgHome)
if err != nil {
@@ -29,13 +34,15 @@ func Decrypt(inputFile string, passphrase string) error {
if err != nil {
return err
}
utils.Info("Decrypting backup using passphrase...done")
utils.Info("Backup file decrypted successful!")
return nil
}
// Encrypt encrypts backup using a passphrase
func Encrypt(inputFile string, passphrase string) error {
utils.Info("Encrypting backup...")
utils.Info("Encrypting backup using passphrase...")
//Create gpg home dir
err := utils.MakeDirAll(gpgHome)
if err != nil {
@@ -50,11 +57,114 @@ func Encrypt(inputFile string, passphrase string) error {
if err != nil {
return err
}
utils.Info("Encrypting backup using passphrase...done")
utils.Info("Backup file encrypted successful!")
return nil
}
// encrypt encrypts backup using a public key
func encrypt(inputFile string, publicKey string) error {
utils.Info("Encrypting backup using public key...")
// Read the public key
pubKeyBytes, err := os.ReadFile(publicKey)
if err != nil {
return errors.New(fmt.Sprintf("Error reading public key: %s", err))
}
// Create a new keyring with the public key
publicKeyObj, err := crypto.NewKeyFromArmored(string(pubKeyBytes))
if err != nil {
return errors.New(fmt.Sprintf("Error parsing public key: %s", err))
}
keyRing, err := crypto.NewKeyRing(publicKeyObj)
if err != nil {
return errors.New(fmt.Sprintf("Error creating key ring: %v", err))
}
// Read the file to encrypt
fileContent, err := os.ReadFile(inputFile)
if err != nil {
return errors.New(fmt.Sprintf("Error reading file: %v", err))
}
// Encrypt the file
message := crypto.NewPlainMessage(fileContent)
encMessage, err := keyRing.Encrypt(message, nil)
if err != nil {
return errors.New(fmt.Sprintf("Error encrypting file: %v", err))
}
// Save the encrypted file
err = os.WriteFile(fmt.Sprintf("%s.%s", inputFile, gpgExtension), encMessage.GetBinary(), 0644)
if err != nil {
return errors.New(fmt.Sprintf("Error saving encrypted file: %v", err))
}
utils.Info("Encrypting backup using public key...done")
utils.Info("Backup file encrypted successful!")
return nil
}
// decrypt decrypts backup file using a private key and passphrase.
// privateKey GPG private key
// passphrase GPG passphrase
func decrypt(inputFile, privateKey, passphrase string) error {
utils.Info("Encrypting backup using private key...")
// Read the private key
priKeyBytes, err := os.ReadFile(privateKey)
if err != nil {
return errors.New(fmt.Sprintf("Error reading private key: %s", err))
}
// Read the password for the private key (if its password-protected)
password := []byte(passphrase)
// Create a key object from the armored private key
privateKeyObj, err := crypto.NewKeyFromArmored(string(priKeyBytes))
if err != nil {
return errors.New(fmt.Sprintf("Error parsing private key: %s", err))
}
// Unlock the private key with the password
if passphrase != "" {
// Unlock the private key with the password
_, err = privateKeyObj.Unlock(password)
if err != nil {
return errors.New(fmt.Sprintf("Error unlocking private key: %s", err))
}
}
// Create a new keyring with the private key
keyRing, err := crypto.NewKeyRing(privateKeyObj)
if err != nil {
return errors.New(fmt.Sprintf("Error creating key ring: %v", err))
}
// Read the encrypted file
encFileContent, err := os.ReadFile(inputFile)
if err != nil {
return errors.New(fmt.Sprintf("Error reading encrypted file: %s", err))
}
// Decrypt the file
encryptedMessage := crypto.NewPGPMessage(encFileContent)
message, err := keyRing.Decrypt(encryptedMessage, nil, 0)
if err != nil {
return errors.New(fmt.Sprintf("Error decrypting file: %s", err))
}
// Save the decrypted file
err = os.WriteFile(RemoveLastExtension(inputFile), message.GetBinary(), 0644)
if err != nil {
return errors.New(fmt.Sprintf("Error saving decrypted file: %s", err))
}
utils.Info("Encrypting backup using public key...done")
fmt.Println("File successfully decrypted!")
return nil
}
func RemoveLastExtension(filename string) string {
if idx := strings.LastIndex(filename, "."); idx != -1 {
return filename[:idx]

View File

@@ -8,13 +8,16 @@ package pkg
import (
"bytes"
"fmt"
"github.com/jkaninda/pg-bkup/utils"
"gopkg.in/yaml.v3"
"os"
"os/exec"
"path/filepath"
"time"
)
// copyToTmp copy file to temporary directory
func copyToTmp(sourcePath string, backupFileName string) {
//Copy backup from storage to /tmp
err := utils.CopyFile(filepath.Join(sourcePath, backupFileName), filepath.Join(tmpPath, backupFileName))
@@ -137,3 +140,64 @@ func testDatabaseConnection(db *dbConfig) {
utils.Info("Successfully connected to %s database", db.dbName)
}
func readConf(filename string) (*Config, error) {
configFile := filepath.Join("", filename)
if utils.FileExists(configFile) {
buf, err := os.ReadFile(configFile)
if err != nil {
return nil, err
}
c := &Config{}
err = yaml.Unmarshal(buf, c)
if err != nil {
return nil, fmt.Errorf("in file %q: %w", filename, err)
}
return c, err
}
return nil, fmt.Errorf("config file %q not found", filename)
}
func checkPubKeyFile(pubKey string) (string, error) {
utils.Info("Checking file %s ...", pubKey)
// Define possible key file names
keyFiles := []string{filepath.Join(gpgHome, "public_key.asc"), filepath.Join(gpgHome, "public_key.gpg"), pubKey}
// Loop through key file names and check if they exist
for _, keyFile := range keyFiles {
if _, err := os.Stat(keyFile); err == nil {
// File exists
return keyFile, nil
} else if os.IsNotExist(err) {
// File does not exist, continue to the next one
continue
} else {
// An unexpected error occurred
return "", err
}
}
// Return an error if neither file exists
return "", fmt.Errorf("no public key file found")
}
func checkPrKeyFile(prKey string) (string, error) {
// Define possible key file names
keyFiles := []string{filepath.Join(gpgHome, "private_key.asc"), filepath.Join(gpgHome, "private_key.gpg"), prKey}
// Loop through key file names and check if they exist
for _, keyFile := range keyFiles {
if _, err := os.Stat(keyFile); err == nil {
// File exists
return keyFile, nil
} else if os.IsNotExist(err) {
// File does not exist, continue to the next one
continue
} else {
// An unexpected error occurred
return "", err
}
}
// Return an error if neither file exists
return "", fmt.Errorf("no public key file found")
}

View File

@@ -30,11 +30,13 @@ func StartMigration(cmd *cobra.Command) {
//Generate file name
backupFileName := fmt.Sprintf("%s_%s.sql", dbConf.dbName, time.Now().Format("20060102_150405"))
conf := &RestoreConfig{}
conf.file = backupFileName
//Backup source Database
BackupDatabase(dbConf, backupFileName, true)
//Restore source database into target database
utils.Info("Restoring [%s] database into [%s] database...", dbConf.dbName, targetDbConf.targetDbName)
RestoreDatabase(&newDbConfig, backupFileName)
RestoreDatabase(&newDbConfig, conf)
utils.Info("[%s] database has been restored into [%s] database", dbConf.dbName, targetDbConf.targetDbName)
utils.Info("Database migration completed.")
}

View File

@@ -7,7 +7,6 @@
package pkg
import (
"fmt"
"github.com/jkaninda/pg-bkup/utils"
"github.com/spf13/cobra"
"os"
@@ -24,75 +23,76 @@ func StartRestore(cmd *cobra.Command) {
case "local":
utils.Info("Restore database from local")
copyToTmp(storagePath, restoreConf.file)
RestoreDatabase(dbConf, restoreConf.file)
RestoreDatabase(dbConf, restoreConf)
case "s3", "S3":
restoreFromS3(dbConf, restoreConf.file, restoreConf.bucket, restoreConf.s3Path)
restoreFromS3(dbConf, restoreConf)
case "ssh", "SSH", "remote":
restoreFromRemote(dbConf, restoreConf.file, restoreConf.remotePath)
restoreFromRemote(dbConf, restoreConf)
case "ftp", "FTP":
restoreFromFTP(dbConf, restoreConf.file, restoreConf.remotePath)
restoreFromFTP(dbConf, restoreConf)
default:
utils.Info("Restore database from local")
copyToTmp(storagePath, restoreConf.file)
RestoreDatabase(dbConf, restoreConf.file)
RestoreDatabase(dbConf, restoreConf)
}
}
func restoreFromS3(db *dbConfig, file, bucket, s3Path string) {
func restoreFromS3(db *dbConfig, conf *RestoreConfig) {
utils.Info("Restore database from s3")
err := DownloadFile(tmpPath, file, bucket, s3Path)
err := DownloadFile(tmpPath, conf.file, conf.bucket, conf.s3Path)
if err != nil {
utils.Fatal("Error download file from s3 %s %v ", file, err)
utils.Fatal("Error download file from s3 %s %v ", conf.file, err)
}
RestoreDatabase(db, file)
RestoreDatabase(db, conf)
}
func restoreFromRemote(db *dbConfig, file, remotePath string) {
func restoreFromRemote(db *dbConfig, conf *RestoreConfig) {
utils.Info("Restore database from remote server")
err := CopyFromRemote(file, remotePath)
err := CopyFromRemote(conf.file, conf.remotePath)
if err != nil {
utils.Fatal("Error download file from remote server: %s %v", filepath.Join(remotePath, file), err)
utils.Fatal("Error download file from remote server: %s %v", filepath.Join(conf.remotePath, conf.file), err)
}
RestoreDatabase(db, file)
RestoreDatabase(db, conf)
}
func restoreFromFTP(db *dbConfig, file, remotePath string) {
func restoreFromFTP(db *dbConfig, conf *RestoreConfig) {
utils.Info("Restore database from FTP server")
err := CopyFromFTP(file, remotePath)
err := CopyFromFTP(conf.file, conf.remotePath)
if err != nil {
utils.Fatal("Error download file from FTP server: %s %v", filepath.Join(remotePath, file), err)
utils.Fatal("Error download file from FTP server: %s %v", filepath.Join(conf.remotePath, conf.file), err)
}
RestoreDatabase(db, file)
RestoreDatabase(db, conf)
}
// RestoreDatabase restore database
func RestoreDatabase(db *dbConfig, file string) {
gpgPassphrase := os.Getenv("GPG_PASSPHRASE")
if file == "" {
func RestoreDatabase(db *dbConfig, conf *RestoreConfig) {
if conf.file == "" {
utils.Fatal("Error, file required")
}
extension := filepath.Ext(fmt.Sprintf("%s/%s", tmpPath, file))
extension := filepath.Ext(filepath.Join(tmpPath, conf.file))
if extension == ".gpg" {
if gpgPassphrase == "" {
utils.Fatal("Error: GPG passphrase is required, your file seems to be a GPG file.\nYou need to provide GPG keys. GPG_PASSPHRASE environment variable is required.")
} else {
//Decrypt file
err := Decrypt(filepath.Join(tmpPath, file), gpgPassphrase)
if conf.usingKey {
err := decrypt(filepath.Join(tmpPath, conf.file), conf.privateKey, conf.passphrase)
if err != nil {
utils.Fatal("Error decrypting file %s %v", file, err)
utils.Fatal("Error during decrypting backup %v", err)
}
} else {
if conf.passphrase == "" {
utils.Error("Error, passphrase or private key required")
utils.Fatal("Your file seems to be a GPG file.\nYou need to provide GPG keys. GPG_PASSPHRASE or GPG_PRIVATE_KEY environment variable is required.")
} else {
//Decrypt file
err := Decrypt(filepath.Join(tmpPath, conf.file), conf.passphrase)
if err != nil {
utils.Fatal("Error decrypting file %s %v", file, err)
}
//Update file name
file = RemoveLastExtension(file)
}
//Update file name
file = RemoveLastExtension(file)
}
}
err := utils.CheckEnvVars(dbHVars)
if err != nil {
utils.Error("Please make sure all required environment variables for database are set")
utils.Fatal("Error checking environment variables: %s", err)
}
if utils.FileExists(fmt.Sprintf("%s/%s", tmpPath, file)) {
if utils.FileExists(filepath.Join(tmpPath, conf.file)) {
err := os.Setenv("PGPASSWORD", db.dbPassword)
if err != nil {
@@ -101,10 +101,10 @@ func RestoreDatabase(db *dbConfig, file string) {
testDatabaseConnection(db)
utils.Info("Restoring database...")
extension := filepath.Ext(file)
extension := filepath.Ext(conf.file)
// Restore from compressed file / .sql.gz
if extension == ".gz" {
str := "zcat " + filepath.Join(tmpPath, file) + " | psql -h " + db.dbHost + " -p " + db.dbPort + " -U " + db.dbUserName + " -v -d " + db.dbName
str := "zcat " + filepath.Join(tmpPath, conf.file) + " | psql -h " + db.dbHost + " -p " + db.dbPort + " -U " + db.dbUserName + " -v -d " + db.dbName
_, err := exec.Command("sh", "-c", str).Output()
if err != nil {
utils.Fatal("Error, in restoring the database %v", err)
@@ -130,6 +130,6 @@ func RestoreDatabase(db *dbConfig, file string) {
}
} else {
utils.Fatal("File not found in %s", fmt.Sprintf("%s/%s", tmpPath, file))
utils.Fatal("File not found in %s", filepath.Join(tmpPath, conf.file))
}
}

View File

@@ -18,6 +18,7 @@ var (
storagePath = "/backup"
disableCompression = false
encryption = false
usingKey = false
)
// dbHVars Required environment variables for database