mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2026-06-19 07:36:59 +00:00
feat(backup): enhance backup and restore functionality with crypto secret handling
- Added tests to verify backup and restore processes when the crypto secret changes, ensuring hash verification passes. - Updated `writeManifestFiles` and `verifyBackupManifest` functions to accept an AES key for improved security. - Implemented fallback mechanism for verifying manifest signatures using both AES-derived and legacy signing keys. - Enhanced the overall robustness of the backup and restore system to handle legacy signatures and different crypto secrets.
This commit is contained in:
@@ -145,7 +145,7 @@ func Backup() (Result, error) {
|
||||
},
|
||||
})
|
||||
|
||||
if err := writeManifestFiles(tempDir, manifest); err != nil {
|
||||
if err := writeManifestFiles(tempDir, manifest, key); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
|
||||
|
||||
@@ -540,6 +540,143 @@ func TestRestoreRejectsTamperedManifestSignature(t *testing.T) {
|
||||
assert.ErrorContains(t, err, ErrInvalidManifestSig.Error())
|
||||
}
|
||||
|
||||
func TestRestoreSucceedsWhenCryptoSecretChanges(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "changed-secret-backup-test-*")
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
configPath := filepath.Join(tempDir, "config.ini")
|
||||
err = os.WriteFile(configPath, []byte("[app]\nName = Nginx UI Test\n"), 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
dbName := settings.DatabaseSettings.GetName()
|
||||
dbPath := filepath.Join(tempDir, dbName+".db")
|
||||
err = os.WriteFile(dbPath, []byte("CREATE TABLE users (id INT, name TEXT);"), 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
nginxConfigDir := filepath.Join(tempDir, "nginx")
|
||||
err = os.MkdirAll(nginxConfigDir, 0755)
|
||||
assert.NoError(t, err)
|
||||
err = os.WriteFile(filepath.Join(nginxConfigDir, "nginx.conf"), []byte("events {}\nhttp {}\n"), 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
originalConfPath := cosysettings.ConfPath
|
||||
originalNginxConfigDir := settings.NginxSettings.ConfigDir
|
||||
originalCryptoSecret := settings.CryptoSettings.Secret
|
||||
cosysettings.ConfPath = configPath
|
||||
settings.NginxSettings.ConfigDir = nginxConfigDir
|
||||
settings.CryptoSettings.Secret = "backup-created-with-original-secret"
|
||||
defer func() {
|
||||
cosysettings.ConfPath = originalConfPath
|
||||
settings.NginxSettings.ConfigDir = originalNginxConfigDir
|
||||
settings.CryptoSettings.Secret = originalCryptoSecret
|
||||
}()
|
||||
|
||||
backupResult, err := Backup()
|
||||
assert.NoError(t, err)
|
||||
|
||||
backupPath := filepath.Join(tempDir, backupResult.BackupName)
|
||||
err = os.WriteFile(backupPath, backupResult.BackupContent, 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
key, err := DecodeFromBase64(backupResult.AESKey)
|
||||
assert.NoError(t, err)
|
||||
iv, err := DecodeFromBase64(backupResult.AESIv)
|
||||
assert.NoError(t, err)
|
||||
|
||||
settings.CryptoSettings.Secret = "backup-restored-with-different-secret"
|
||||
|
||||
restoreDir := filepath.Join(tempDir, "restore")
|
||||
result, err := Restore(RestoreOptions{
|
||||
BackupPath: backupPath,
|
||||
AESKey: key,
|
||||
AESIv: iv,
|
||||
RestoreDir: restoreDir,
|
||||
RestoreNginx: false,
|
||||
RestoreNginxUI: false,
|
||||
VerifyHash: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, restoreDir, result.RestoreDir)
|
||||
assert.True(t, result.HashMatch, "Hash verification should pass even if crypto secret changed")
|
||||
}
|
||||
|
||||
func TestRestoreAcceptsLegacyManifestSignature(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "legacy-signature-backup-test-*")
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
configPath := filepath.Join(tempDir, "config.ini")
|
||||
err = os.WriteFile(configPath, []byte("[app]\nName = Nginx UI Test\n"), 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
dbName := settings.DatabaseSettings.GetName()
|
||||
dbPath := filepath.Join(tempDir, dbName+".db")
|
||||
err = os.WriteFile(dbPath, []byte("CREATE TABLE users (id INT, name TEXT);"), 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
nginxConfigDir := filepath.Join(tempDir, "nginx")
|
||||
err = os.MkdirAll(nginxConfigDir, 0755)
|
||||
assert.NoError(t, err)
|
||||
err = os.WriteFile(filepath.Join(nginxConfigDir, "nginx.conf"), []byte("events {}\nhttp {}\n"), 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
originalConfPath := cosysettings.ConfPath
|
||||
originalNginxConfigDir := settings.NginxSettings.ConfigDir
|
||||
originalCryptoSecret := settings.CryptoSettings.Secret
|
||||
cosysettings.ConfPath = configPath
|
||||
settings.NginxSettings.ConfigDir = nginxConfigDir
|
||||
settings.CryptoSettings.Secret = "legacy-manifest-secret"
|
||||
defer func() {
|
||||
cosysettings.ConfPath = originalConfPath
|
||||
settings.NginxSettings.ConfigDir = originalNginxConfigDir
|
||||
settings.CryptoSettings.Secret = originalCryptoSecret
|
||||
}()
|
||||
|
||||
backupResult, err := Backup()
|
||||
assert.NoError(t, err)
|
||||
|
||||
backupPath := filepath.Join(tempDir, backupResult.BackupName)
|
||||
err = os.WriteFile(backupPath, backupResult.BackupContent, 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
extractedDir := filepath.Join(tempDir, "extracted")
|
||||
err = os.MkdirAll(extractedDir, 0755)
|
||||
assert.NoError(t, err)
|
||||
err = extractZipArchive(backupPath, extractedDir)
|
||||
assert.NoError(t, err)
|
||||
|
||||
manifestBytes, err := os.ReadFile(filepath.Join(extractedDir, ManifestFile))
|
||||
assert.NoError(t, err)
|
||||
legacySigningKey, err := deriveBackupSigningKey()
|
||||
assert.NoError(t, err)
|
||||
err = os.WriteFile(filepath.Join(extractedDir, ManifestSignatureFile), []byte(signManifest(manifestBytes, legacySigningKey)), 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
legacyBackupPath := filepath.Join(tempDir, "legacy-signature.zip")
|
||||
err = createZipArchive(legacyBackupPath, extractedDir)
|
||||
assert.NoError(t, err)
|
||||
|
||||
key, err := DecodeFromBase64(backupResult.AESKey)
|
||||
assert.NoError(t, err)
|
||||
iv, err := DecodeFromBase64(backupResult.AESIv)
|
||||
assert.NoError(t, err)
|
||||
|
||||
restoreDir := filepath.Join(tempDir, "restore")
|
||||
result, err := Restore(RestoreOptions{
|
||||
BackupPath: legacyBackupPath,
|
||||
AESKey: key,
|
||||
AESIv: iv,
|
||||
RestoreDir: restoreDir,
|
||||
RestoreNginx: false,
|
||||
RestoreNginxUI: false,
|
||||
VerifyHash: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, restoreDir, result.RestoreDir)
|
||||
assert.True(t, result.HashMatch, "Hash verification should pass for legacy manifest signatures")
|
||||
}
|
||||
|
||||
func TestHashCalculation(t *testing.T) {
|
||||
// Create temp file
|
||||
tempFile, err := os.CreateTemp("", "hash-test-*.txt")
|
||||
|
||||
@@ -50,13 +50,13 @@ func newManifest(createdAt, version string, files []ManifestEntry) Manifest {
|
||||
}
|
||||
}
|
||||
|
||||
func writeManifestFiles(baseDir string, manifest Manifest) error {
|
||||
func writeManifestFiles(baseDir string, manifest Manifest, aesKey []byte) error {
|
||||
manifestBytes, err := json.Marshal(manifest)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateManifest, err.Error())
|
||||
}
|
||||
|
||||
signingKey, err := deriveBackupSigningKey()
|
||||
signingKey, err := deriveBackupSigningKeyFromAESKey(aesKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -74,18 +74,13 @@ func writeManifestFiles(baseDir string, manifest Manifest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyBackupManifest(baseDir string) error {
|
||||
func verifyBackupManifest(baseDir string, aesKey []byte) error {
|
||||
manifest, manifestBytes, signature, err := loadManifest(baseDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signingKey, err := deriveBackupSigningKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := verifyManifestSignature(manifestBytes, signature, signingKey); err != nil {
|
||||
if err := verifyManifestSignatureWithFallback(manifestBytes, signature, aesKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -158,6 +153,15 @@ func loadManifest(baseDir string) (Manifest, []byte, string, error) {
|
||||
return manifest, manifestBytes, strings.TrimSpace(string(signatureBytes)), nil
|
||||
}
|
||||
|
||||
func deriveBackupSigningKeyFromAESKey(aesKey []byte) ([]byte, error) {
|
||||
if len(aesKey) == 0 {
|
||||
return nil, ErrInvalidAESKey
|
||||
}
|
||||
|
||||
sum := sha256.Sum256(append([]byte(manifestKeyContext), aesKey...))
|
||||
return sum[:], nil
|
||||
}
|
||||
|
||||
func deriveBackupSigningKey() ([]byte, error) {
|
||||
secret := strings.TrimSpace(settings.CryptoSettings.Secret)
|
||||
if secret == "" {
|
||||
@@ -174,6 +178,20 @@ func signManifest(manifestBytes []byte, signingKey []byte) string {
|
||||
return hex.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
func verifyManifestSignatureWithFallback(manifestBytes []byte, signature string, aesKey []byte) error {
|
||||
aesSigningKey, err := deriveBackupSigningKeyFromAESKey(aesKey)
|
||||
if err == nil && verifyManifestSignature(manifestBytes, signature, aesSigningKey) == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
legacySigningKey, err := deriveBackupSigningKey()
|
||||
if err == nil && verifyManifestSignature(manifestBytes, signature, legacySigningKey) == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ErrInvalidManifestSig
|
||||
}
|
||||
|
||||
func verifyManifestSignature(manifestBytes []byte, signature string, signingKey []byte) error {
|
||||
decodedSignature, err := hex.DecodeString(signature)
|
||||
if err != nil {
|
||||
|
||||
@@ -50,7 +50,7 @@ func Restore(options RestoreOptions) (RestoreResult, error) {
|
||||
nginxUIZipPath := filepath.Join(options.RestoreDir, NginxUIZipName)
|
||||
nginxZipPath := filepath.Join(options.RestoreDir, NginxZipName)
|
||||
|
||||
if err := verifyBackupManifest(options.RestoreDir); err != nil {
|
||||
if err := verifyBackupManifest(options.RestoreDir, options.AESKey); err != nil {
|
||||
return RestoreResult{}, err
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user