From a711fef07956478719db0d1c4d3186d498952b64 Mon Sep 17 00:00:00 2001 From: Petros Petrou Date: Wed, 24 Jul 2024 23:29:29 +0300 Subject: [PATCH] Optimize storage and speed when making an encrypted backup --- .../advanced-usage/encrypt-backup-archives.md | 5 - src/BackupServiceProvider.php | 4 - src/Listeners/EncryptBackupArchive.php | 67 ------------- src/Tasks/Backup/BackupJob.php | 6 +- src/Tasks/Backup/Encryption.php | 27 +++++ src/Tasks/Backup/Zip.php | 49 +++++++++ tests/Commands/BackupCommandTest.php | 68 ++++++++++++- tests/FileSelectionTest.php | 4 - tests/Listeners/EncryptBackupArchiveTest.php | 93 ------------------ tests/TestCase.php | 21 ++++ tests/stubs/archive.zip | Bin 871 -> 0 bytes 11 files changed, 164 insertions(+), 180 deletions(-) delete mode 100644 src/Listeners/EncryptBackupArchive.php create mode 100644 src/Tasks/Backup/Encryption.php delete mode 100644 tests/Listeners/EncryptBackupArchiveTest.php delete mode 100644 tests/stubs/archive.zip diff --git a/docs/advanced-usage/encrypt-backup-archives.md b/docs/advanced-usage/encrypt-backup-archives.md index 33eec06e..db22561c 100644 --- a/docs/advanced-usage/encrypt-backup-archives.md +++ b/docs/advanced-usage/encrypt-backup-archives.md @@ -10,11 +10,6 @@ By default you only have to define the `BACKUP_ARCHIVE_PASSWORD` environment var If you want to customize this you can configure the `backup.backup.password` and `backup.backup.encryption` keys in your `config/backup.php` file. -The whole encryption is done with an event listener. -The `\Spatie\Backup\Listeners\EncryptBackupArchive` listener is attached to the `\Spatie\Backup\Events\BackupZipWasCreated` event. -The listener is added to the event when both required config keys are not `null`. -You are free to add this listener your own or override it. - It's important to try this workflow and also to decrypt a backup archive. So you know that it works and you have a working backup restore solution. diff --git a/src/BackupServiceProvider.php b/src/BackupServiceProvider.php index b16219d1..bb961cb8 100644 --- a/src/BackupServiceProvider.php +++ b/src/BackupServiceProvider.php @@ -37,10 +37,6 @@ public function configurePackage(Package $package): void public function packageBooted() { $this->app['events']->subscribe(EventHandler::class); - - if (EncryptBackupArchive::shouldEncrypt()) { - Event::listen(BackupZipWasCreated::class, EncryptBackupArchive::class); - } } public function packageRegistered() diff --git a/src/Listeners/EncryptBackupArchive.php b/src/Listeners/EncryptBackupArchive.php deleted file mode 100644 index 2277e7e2..00000000 --- a/src/Listeners/EncryptBackupArchive.php +++ /dev/null @@ -1,67 +0,0 @@ -open($event->pathToZip); - - $this->encrypt($zip); - - $zip->close(); - } - - protected function encrypt(ZipArchive $zip): void - { - $zip->setPassword(static::getPassword()); - - foreach (range(0, $zip->numFiles - 1) as $i) { - $zip->setEncryptionIndex($i, static::getAlgorithm()); - } - } - - public static function shouldEncrypt(): bool - { - $password = static::getPassword(); - $algorithm = static::getAlgorithm(); - - if ($password === null) { - return false; - } - - if (! is_int($algorithm)) { - return false; - } - - return true; - } - - protected static function getPassword(): ?string - { - return config('backup.backup.password'); - } - - protected static function getAlgorithm(): ?int - { - $encryption = config('backup.backup.encryption'); - - if ($encryption === 'default') { - $encryption = defined("\ZipArchive::EM_AES_256") - ? ZipArchive::EM_AES_256 - : null; - } - - return $encryption; - } -} diff --git a/src/Tasks/Backup/BackupJob.php b/src/Tasks/Backup/BackupJob.php index 2a242b85..9b945ae0 100644 --- a/src/Tasks/Backup/BackupJob.php +++ b/src/Tasks/Backup/BackupJob.php @@ -228,11 +228,7 @@ protected function createZipContainingEveryFileInManifest(Manifest $manifest): s consoleOutput()->info("Created zip containing {$zip->count()} files and directories. Size is {$zip->humanReadableSize()}"); - if ($this->sendNotifications) { - $this->sendNotification(new BackupZipWasCreated($pathToZip)); - } else { - app()->call('\Spatie\Backup\Listeners\EncryptBackupArchive@handle', ['event' => new BackupZipWasCreated($pathToZip)]); - } + $this->sendNotification(new BackupZipWasCreated($pathToZip)); return $pathToZip; } diff --git a/src/Tasks/Backup/Encryption.php b/src/Tasks/Backup/Encryption.php new file mode 100644 index 00000000..f56f9e07 --- /dev/null +++ b/src/Tasks/Backup/Encryption.php @@ -0,0 +1,27 @@ +password = $password; + + $this->method = $method; + } + + public function getPassword(): string + { + return $this->password; + } + + public function getMethod(): int + { + return $this->method; + } +} diff --git a/src/Tasks/Backup/Zip.php b/src/Tasks/Backup/Zip.php index c45927f9..786ee21a 100644 --- a/src/Tasks/Backup/Zip.php +++ b/src/Tasks/Backup/Zip.php @@ -14,6 +14,8 @@ class Zip protected string $pathToZip; + protected ?Encryption $encryption = null; + public static function createForManifest(Manifest $manifest, string $pathToZip): self { $relativePath = config('backup.backup.source.files.relative_path') ? @@ -27,6 +29,8 @@ public static function createForManifest(Manifest $manifest, string $pathToZip): $zip->add($file, self::determineNameOfFileInZip($file, $pathToZip, $relativePath)); } + $zip->encrypt(); + $zip->close(); return $zip; @@ -87,6 +91,11 @@ public function close(): void $this->zipFile->close(); } + public function setPassword(string $password): void + { + $this->zipFile->setPassword($password); + } + public function add(string|iterable $files, ?string $nameInZip = null): self { if (is_array($files)) { @@ -126,4 +135,44 @@ public function count(): int { return $this->fileCount; } + + public function encrypt() + { + $this->loadEncryption(); + + if ($this->getEncryption()) { + $this->setPassword($this->getEncryption()->getPassword()); + + foreach (range(0, $this->zipFile->numFiles - 1) as $i) { + $this->zipFile->setEncryptionIndex($i, $this->getEncryption()->getMethod()); + } + } + } + + public function getEncryption(): ?Encryption + { + return $this->encryption; + } + + public function loadEncryption() + { + $password = config('backup.backup.password'); + $method = config('backup.backup.encryption'); + + if ($method === 'default') { + $method = defined("\ZipArchive::EM_AES_256") + ? ZipArchive::EM_AES_256 + : null; + } + + if ($password === null) { + return false; + } + + if (! is_int($method)) { + return false; + } + + $this->encryption = new Encryption($password, $method); + } } diff --git a/tests/Commands/BackupCommandTest.php b/tests/Commands/BackupCommandTest.php index c25089ad..11a5fde0 100644 --- a/tests/Commands/BackupCommandTest.php +++ b/tests/Commands/BackupCommandTest.php @@ -78,7 +78,6 @@ $testFiles = [ '.dotfile', - 'archive.zip', '1Mb.file', 'directory1/', 'directory1/directory1/', @@ -358,8 +357,73 @@ app()['db']->disconnect(); }); +it('keeps archive unencrypted without password', function () { + config()->set('backup.backup.password', null); + config()->set('backup.backup.source.files.include', [$this->getStubDirectory()]); + config()->set('backup.backup.source.files.relative_path', $this->getStubDirectory()); + + $this->artisan('backup:run --only-files')->assertExitCode(0); + Storage::disk('local')->assertExists($this->expectedZipPath); + + $zip = new ZipArchive(); + $zip->open(Storage::disk('local')->path($this->expectedZipPath)); + + $this->assertEncryptionMethod($zip, ZipArchive::EM_NONE); + + $this->assertTrue($zip->extractTo(Storage::disk('local')->path('temp/extraction'))); + $this->assertValidExtractedFiles(); + + $zip->close(); +}); + +/** + * @param int $algorithm + */ +it('encrypts archive with password', function (int $algorithm) { + config()->set('backup.backup.password', $this->fakePassword()); + config()->set('backup.backup.encryption', $algorithm); + config()->set('backup.backup.source.files.include', [$this->getStubDirectory()]); + config()->set('backup.backup.source.files.relative_path', $this->getStubDirectory()); + + $this->artisan('backup:run --only-files')->assertExitCode(0); + + Storage::disk('local')->assertExists($this->expectedZipPath); + + $zip = new ZipArchive(); + $zip->open(Storage::disk('local')->path($this->expectedZipPath)); + + $this->assertEncryptionMethod($zip, $algorithm); + + $zip->setPassword($this->fakePassword()); + $this->assertTrue($zip->extractTo(Storage::disk('local')->path('temp/extraction'))); + $this->assertValidExtractedFiles(); + + $zip->close(); +})->with([ + [ZipArchive::EM_AES_128], + [ZipArchive::EM_AES_192], + [ZipArchive::EM_AES_256], +]); + +it('can not open encrypted archive without password', function () { + config()->set('backup.backup.password', $this->fakePassword()); + + $this->artisan('backup:run --only-files')->assertExitCode(0); + + Storage::disk('local')->assertExists($this->expectedZipPath); + + $zip = new ZipArchive(); + $zip->open(Storage::disk('local')->path($this->expectedZipPath)); + + $this->assertEncryptionMethod($zip, ZipArchive::EM_AES_256); + + expect($zip->extractTo(Storage::disk('local')->path('temp/extraction')))->toBeFalse(); + + $zip->close(); +}); + it('will encrypt backup when notifications are disabled', function () { - config()->set('backup.backup.password', '24dsjF6BPjWgUfTu'); + config()->set('backup.backup.password', $this->fakePassword()); config()->set('backup.backup.source.databases', ['db1']); $this->artisan('backup:run --disable-notifications --only-db --db-name=db1 --only-to-disk=local')->assertExitCode(0); diff --git a/tests/FileSelectionTest.php b/tests/FileSelectionTest.php index bea0a09d..76d396d7 100644 --- a/tests/FileSelectionTest.php +++ b/tests/FileSelectionTest.php @@ -13,7 +13,6 @@ '.dot', '.dot/file1.txt', '.dotfile', - 'archive.zip', '1Mb.file', 'directory1', 'directory1/directory1', @@ -43,7 +42,6 @@ '.dot', '.dot/file1.txt', '.dotfile', - 'archive.zip', '1Mb.file', 'directory2', 'directory2/directory1', @@ -69,7 +67,6 @@ $testFiles = getTestFiles([ '.dot', '.dotfile', - 'archive.zip', '1Mb.file', 'directory1', 'directory1/file2.txt', @@ -113,7 +110,6 @@ '.dot', '.dot/file1.txt', '.dotfile', - 'archive.zip', '1Mb.file', 'directory1', 'directory1/file1.txt', diff --git a/tests/Listeners/EncryptBackupArchiveTest.php b/tests/Listeners/EncryptBackupArchiveTest.php deleted file mode 100644 index 806f3e5b..00000000 --- a/tests/Listeners/EncryptBackupArchiveTest.php +++ /dev/null @@ -1,93 +0,0 @@ -set('backup.backup.password', fakePassword()); -}); - -it('keeps archive unencrypted without password', function () { - config()->set('backup.backup.password', null); - - $path = zip(); - - $zip = new ZipArchive; - $zip->open($path); - - assertEncryptionMethod($zip, ZipArchive::EM_NONE); - - $this->assertTrue($zip->extractTo(__DIR__.'/../temp/extraction')); - assertValidExtractedFiles(); - - $zip->close(); -}); - -/** - * @param int $algorithm - */ -it('encrypts archive with password', function (int $algorithm) { - config()->set('backup.backup.encryption', $algorithm); - - $path = zip(); - - $zip = new ZipArchive; - $zip->open($path); - - assertEncryptionMethod($zip, $algorithm); - - $zip->setPassword(fakePassword()); - $this->assertTrue($zip->extractTo(__DIR__.'/../temp/extraction')); - assertValidExtractedFiles(); - - $zip->close(); -})->with([ - [ZipArchive::EM_AES_128], - [ZipArchive::EM_AES_192], - [ZipArchive::EM_AES_256], -]); - -it('can not open encrypted archive without password', function () { - $path = zip(); - - $zip = new ZipArchive; - $zip->open($path); - - assertEncryptionMethod($zip, ZipArchive::EM_AES_256); - - expect($zip->extractTo(__DIR__.'/../temp/extraction'))->toBeFalse(); - - $zip->close(); -}); - -function zip(): string -{ - $source = __DIR__.'/../stubs/archive.zip'; - $target = __DIR__.'/../temp/archive.zip'; - - copy($source, $target); - - app()->call('\Spatie\Backup\Listeners\EncryptBackupArchive@handle', ['event' => new BackupZipWasCreated($target)]); - - return $target; -} - -function assertEncryptionMethod(ZipArchive $zip, int $algorithm): void -{ - foreach (range(0, $zip->numFiles - 1) as $i) { - expect($zip->statIndex($i)['encryption_method'])->toBe($algorithm); - } -} - -function assertValidExtractedFiles(): void -{ - foreach (['file1.txt', 'file2.txt', 'file3.txt'] as $filename) { - $filepath = __DIR__.'/../temp/extraction/'.$filename; - expect(file_exists($filepath))->toBeTrue(); - expect(file_get_contents($filepath))->toBe('lorum ipsum'); - } -} - -function fakePassword(): string -{ - return '24dsjF6BPjWgUfTu'; -} diff --git a/tests/TestCase.php b/tests/TestCase.php index aa9193c1..3a4040b2 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -242,4 +242,25 @@ public function makeHealthCheckFail(?Exception $customException = null): self return $this; } + + public function assertEncryptionMethod(ZipArchive $zip, int $algorithm): void + { + foreach (range(0, $zip->numFiles - 1) as $i) { + expect($zip->statIndex($i)['encryption_method'])->toBe($algorithm); + } + } + + public function assertValidExtractedFiles(): void + { + foreach (['file1.txt', 'file2.txt', 'file3.txt'] as $filename) { + $filepath = 'temp/extraction/'.$filename; + Storage::disk('local')->assertExists($filepath); + expect(Storage::disk('local')->get($filepath))->toBe('lorum ipsum'); + } + } + + public function fakePassword(): string + { + return '24dsjF6BPjWgUfTu'; + } } diff --git a/tests/stubs/archive.zip b/tests/stubs/archive.zip deleted file mode 100644 index bda13458cde6a3afd7920dad887a2ccb96c93847..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 871 zcmWIWW@Zs#-~d9?j_@D`DBuRtoD2#KX_+~xhI%CxC7~g_4D8R67|eXG2fuXy~0~WeD(Q=UD2R!^aDh0GSkk)s78784;iz@$tTn&i=s> z`g-x$tqTWQiDq4Lf@pGjLQ=v9U!SlK{9zpd4NMaxnAHWO84G0vntw=3bTIWPDmn(J zc^qd_5OWAV)FCk4>)iRP-Z~m5Jg=Vi)X~$__4D=g)bR}E<6zVLVs7lqa4~xQQSGJ- zuVquR8Q0$}mE5IAoSmdAsMK262X-ok!$%$-q8*n>Zi4o#(q9TNNW80v{5)}sl U-mGjOKeGT~I#A;)AZB0y0El9^GXMYp