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:
0xJacky
2026-04-04 14:26:34 +00:00
parent edf92e4ffe
commit 0b0f854f9b
4 changed files with 166 additions and 11 deletions
+1 -1
View File
@@ -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
}
+137
View File
@@ -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")
+27 -9
View File
@@ -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 {
+1 -1
View File
@@ -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
}