From e5f4d032fcd4786d9413196da709fed0fbf9a6f9 Mon Sep 17 00:00:00 2001 From: Jasper Zonneveld Date: Tue, 21 Oct 2025 20:55:21 +0200 Subject: [PATCH 1/4] refactor: encrypted disk now decorates another disk --- src/Commands/ReEncryptFiles.php | 7 ++-- src/EncryptedDataServiceProvider.php | 52 +++++++++------------------- src/EncryptedFilesystemAdapter.php | 16 +++++++++ tests/Feature/ReEncryptFilesTest.php | 4 +-- tests/Unit/FilesystemTest.php | 2 +- 5 files changed, 40 insertions(+), 41 deletions(-) create mode 100644 src/EncryptedFilesystemAdapter.php diff --git a/src/Commands/ReEncryptFiles.php b/src/Commands/ReEncryptFiles.php index 07cb31e..87d35ef 100644 --- a/src/Commands/ReEncryptFiles.php +++ b/src/Commands/ReEncryptFiles.php @@ -7,7 +7,7 @@ use Illuminate\Console\Command; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Storage; -use Swis\Flysystem\Encrypted\EncryptedFilesystemAdapter; +use Swis\Laravel\Encrypted\EncryptedFilesystemAdapter; use Symfony\Component\Console\Output\OutputInterface; class ReEncryptFiles extends Command @@ -75,6 +75,7 @@ protected function reEncryptFiles(string $disk): Collection $directories->push(''); } + /** @var \Swis\Laravel\Encrypted\EncryptedFilesystemAdapter $filesystem */ $filesystem = Storage::disk($disk); return $directories @@ -93,7 +94,7 @@ protected function reEncryptFiles(string $disk): Collection ->unique() ->each(function (string $file) use ($filesystem) { $this->line($file, verbosity: OutputInterface::VERBOSITY_VERBOSE); - $filesystem->put($file, $filesystem->get($file)); + $filesystem->reEncrypt($file); }); } @@ -133,6 +134,6 @@ protected function disksCanBeReEncrypted(Collection $disks): bool */ protected function diskCanBeReEncrypted(string $disk): bool { - return rescue(static fn (): bool => Storage::disk($disk)->getAdapter() instanceof EncryptedFilesystemAdapter, false, false); + return rescue(static fn (): bool => Storage::disk($disk) instanceof EncryptedFilesystemAdapter, false, false); } } diff --git a/src/EncryptedDataServiceProvider.php b/src/EncryptedDataServiceProvider.php index 22c09ae..77e7df8 100644 --- a/src/EncryptedDataServiceProvider.php +++ b/src/EncryptedDataServiceProvider.php @@ -3,15 +3,9 @@ namespace Swis\Laravel\Encrypted; use Illuminate\Contracts\Foundation\Application; -use Illuminate\Filesystem\FilesystemAdapter; -use Illuminate\Support\Arr; use Illuminate\Support\Facades\Storage; use Illuminate\Support\ServiceProvider; -use League\Flysystem\Filesystem; -use League\Flysystem\Local\LocalFilesystemAdapter; -use League\Flysystem\UnixVisibility\PortableVisibilityConverter; -use League\Flysystem\Visibility; -use Swis\Flysystem\Encrypted\EncryptedFilesystemAdapter; +use Swis\Flysystem\Encrypted\EncryptedFilesystemAdapter as EncryptedAdapter; use Swis\Laravel\Encrypted\Commands\ReEncryptFiles; use Swis\Laravel\Encrypted\Commands\ReEncryptModels; @@ -42,40 +36,28 @@ public function boot(): void protected function setupStorageDriver(): void { Storage::extend( - 'local-encrypted', - function (Application $app, array $config) { - $visibility = PortableVisibilityConverter::fromArray( - $config['permissions'] ?? [], - $config['directory_visibility'] ?? $config['visibility'] ?? Visibility::PRIVATE + 'encrypted', + (function (Application $app, array $config) { + /* @var \Illuminate\Filesystem\FilesystemManager $this */ + if (empty($config['disk'])) { + throw new \InvalidArgumentException('Encrypted disk is missing "disk" configuration option.'); + } + + $parent = $this->build( + is_string($config['disk']) ? $this->getConfig($config['disk']) : $config['disk'] ); - $links = ($config['links'] ?? null) === 'skip' - ? LocalFilesystemAdapter::SKIP_LINKS - : LocalFilesystemAdapter::DISALLOW_LINKS; - - $adapter = new EncryptedFilesystemAdapter( - new LocalFilesystemAdapter( - $config['root'], - $visibility, - $config['lock'] ?? LOCK_EX, - $links - ), + $encryptedAdapter = new EncryptedAdapter( + $parent->getAdapter(), $app->make('encrypted-data.encrypter') ); - $driver = new Filesystem( - $adapter, - Arr::only($config, [ - 'directory_visibility', - 'disable_asserts', - 'temporary_url', - 'url', - 'visibility', - ]) + return new EncryptedFilesystemAdapter( + $this->createFlysystem($encryptedAdapter, $parent->getConfig()), + $parent->getAdapter(), + $parent->getConfig() ); - - return new FilesystemAdapter($driver, $adapter, $config); - } + })->bindTo(Storage::getFacadeRoot(), Storage::getFacadeRoot()) ); } } diff --git a/src/EncryptedFilesystemAdapter.php b/src/EncryptedFilesystemAdapter.php new file mode 100644 index 0000000..8894c99 --- /dev/null +++ b/src/EncryptedFilesystemAdapter.php @@ -0,0 +1,16 @@ +put($path, $this->get($path)); + } +} diff --git a/tests/Feature/ReEncryptFilesTest.php b/tests/Feature/ReEncryptFilesTest.php index 95846b0..557527d 100644 --- a/tests/Feature/ReEncryptFilesTest.php +++ b/tests/Feature/ReEncryptFilesTest.php @@ -52,7 +52,7 @@ protected function hasLocalDisk($app): void protected function hasEncryptedDisk($app): void { $app['config']->set('filesystems.default', 'local'); - $app['config']->set('filesystems.disks.local', ['driver' => 'local-encrypted', 'root' => $this->diskRoot]); + $app['config']->set('filesystems.disks.local', ['driver' => 'encrypted', 'disk' => ['driver' => 'local', 'root' => $this->diskRoot]]); $this->filesystem->ensureDirectoryExists($this->diskRoot); } @@ -60,7 +60,7 @@ protected function hasEncryptedDisk($app): void protected function hasExtraEncryptedDisk($app): void { $diskRoot = dirname($this->diskRoot).'/extra'; - $app['config']->set('filesystems.disks.extra', ['driver' => 'local-encrypted', 'root' => $diskRoot]); + $app['config']->set('filesystems.disks.extra', ['driver' => 'encrypted', 'disk' => ['driver' => 'local', 'root' => $diskRoot]]); $this->filesystem->ensureDirectoryExists($diskRoot); } diff --git a/tests/Unit/FilesystemTest.php b/tests/Unit/FilesystemTest.php index e4df68e..43edc52 100644 --- a/tests/Unit/FilesystemTest.php +++ b/tests/Unit/FilesystemTest.php @@ -12,7 +12,7 @@ final class FilesystemTest extends TestCase protected function usesEncryptedDisk($app): void { $app['config']->set('filesystems.default', 'local'); - $app['config']->set('filesystems.disks.local', ['driver' => 'local-encrypted', 'root' => dirname(__DIR__).'/_files/']); + $app['config']->set('filesystems.disks.local', ['driver' => 'encrypted', 'disk' => ['driver' => 'local', 'root' => dirname(__DIR__).'/_files/']]); } #[Test] From 1045e393d2a784e1138c914e68b4d7fc31f058a7 Mon Sep 17 00:00:00 2001 From: Jasper Zonneveld Date: Tue, 21 Oct 2025 21:09:44 +0200 Subject: [PATCH 2/4] docs: update filesystem instructions --- README.md | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 03b7d19..e81ffc8 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ composer require swisnl/laravel-encrypted-data > Older versions of this package needed a custom model class to encrypt data. This is now replaced with custom casts. Please see [MIGRATING](MIGRATING.md) for a step-by-step guide on how to migrate. > -You can use the Eloquent casts provided by this package and everything will be encrypted/decrypted under the hood! +You can use the Eloquent casts provided by this package just like any other cast. Encryption/decryption is handled automatically behind the scenes. #### Boolean @@ -51,18 +51,37 @@ protected $casts = [ ### Filesystem -Configure the storage driver in `config/filesystems.php`. +This package provides a filesystem driver named `encrypted`, which transparently wraps another disk. You can continue using Laravel's standard storage methods and encryption/decryption is handled automatically behind the scenes. + +To configure the `encrypted` driver, update `config/filesystems.php` with either a full inline disk configuration: ```php 'disks' => [ 'local' => [ - 'driver' => 'local-encrypted', - 'root' => storage_path('app'), + 'driver' => 'encrypted', + 'disk' => [ + 'driver' => 'local', + 'root' => storage_path('app'), + ], ], ], ``` -You can now simply use the storage methods as usual and everything will be encrypted/decrypted under the hood! +Or reference an existing disk by name: + +```php +'disks' => [ + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app'), + ], + + 'local-encrypted' => [ + 'driver' => 'encrypted', + 'disk' => 'local', + ], +], +``` ### Commands From 1ace362a029292b7b334b3c3006d7f905ecd02d5 Mon Sep 17 00:00:00 2001 From: Jasper Zonneveld Date: Tue, 21 Oct 2025 21:19:52 +0200 Subject: [PATCH 3/4] docs(migrating): add migration instructions for filesystem --- MIGRATING.md | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/MIGRATING.md b/MIGRATING.md index f7a1134..608731d 100644 --- a/MIGRATING.md +++ b/MIGRATING.md @@ -1,6 +1,8 @@ # Migrating swisnl/laravel-encrypted-data -## To Laravel Encrypted Casting +## To 3.x from 2.x + +### Laravel Encrypted Casting The main difference between this package and [Laravel Encrypted Casting](https://laravel.com/docs/eloquent-mutators#encrypted-casting) is that this package serializes the data before encrypting it, while Laravel Encrypted Casting encrypts the data directly. This means that the data is not compatible between the two packages. In order to migrate from this package to Laravel Encrypted Casting, you will need to decrypt the data and then re-encrypt it using Laravel Encrypted Casting. Here is a step-by-step guide on how to do this: 1. Make sure you're running on Laravel 12.20 or higher. @@ -54,3 +56,29 @@ php artisan encrypted-data:re-encrypt:models --quietly --no-touch ``` N.B. Use `--help` to see all available options and modify as needed! 8. Remove our custom model encrypter from your `AppServiceProvider` (step 6). + +### Filesystem +If you're using the encrypted filesystem, make sure to update your configuration in `config/filesystems.php` as shown below: + +**Before:** +```php +'disks' => [ + 'local' => [ + 'driver' => 'local-encrypted', + 'root' => storage_path('app'), + ], +], +``` + +**After:** +```php +'disks' => [ + 'local' => [ + 'driver' => 'encrypted', + 'disk' => [ + 'driver' => 'local', + 'root' => storage_path('app'), + ], + ], +], +``` From f74048f33ac0ac4551cf8782157e767742af9620 Mon Sep 17 00:00:00 2001 From: Jasper Zonneveld Date: Tue, 21 Oct 2025 21:44:17 +0200 Subject: [PATCH 4/4] test: add tests for other types of parent disks --- composer.json | 2 + tests/Unit/FilesystemTest.php | 69 +++++++++++++++++++++++++++++++++-- tests/_files/prefix/read.txt | 1 + 3 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 tests/_files/prefix/read.txt diff --git a/composer.json b/composer.json index 250111d..800f010 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,8 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.0", + "league/flysystem-ftp": "^3.0", + "league/flysystem-path-prefixing": "^3.0", "orchestra/testbench": "^10.0", "phpunit/phpunit": "^11.5", "symfony/console": "^7.3.3" diff --git a/tests/Unit/FilesystemTest.php b/tests/Unit/FilesystemTest.php index 43edc52..cd715b8 100644 --- a/tests/Unit/FilesystemTest.php +++ b/tests/Unit/FilesystemTest.php @@ -3,24 +3,87 @@ namespace Swis\Laravel\Encrypted\Tests\Unit; use Illuminate\Support\Facades\Storage; +use League\Flysystem\Ftp\FtpAdapter; use Orchestra\Testbench\Attributes\DefineEnvironment; use PHPUnit\Framework\Attributes\Test; use Swis\Laravel\Encrypted\Tests\TestCase; final class FilesystemTest extends TestCase { - protected function usesEncryptedDisk($app): void + protected function hasEncryptedInlineDisk($app): void { $app['config']->set('filesystems.default', 'local'); $app['config']->set('filesystems.disks.local', ['driver' => 'encrypted', 'disk' => ['driver' => 'local', 'root' => dirname(__DIR__).'/_files/']]); } + protected function hasEncryptedReferencedDisk($app): void + { + $app['config']->set('filesystems.default', 'local'); + $app['config']->set('filesystems.disks.other', ['driver' => 'local', 'root' => dirname(__DIR__).'/_files/']); + $app['config']->set('filesystems.disks.local', ['driver' => 'encrypted', 'disk' => 'other']); + } + + protected function hasEncryptedFtpDisk($app): void + { + $app['config']->set('filesystems.default', 'ftp'); + $app['config']->set('filesystems.disks.ftp', ['driver' => 'encrypted', 'disk' => ['driver' => 'ftp', 'host' => 'localhost']]); + } + + protected function hasEncryptedDiskWithPrefix($app): void + { + $app['config']->set('filesystems.default', 'local'); + $app['config']->set('filesystems.disks.local', ['driver' => 'encrypted', 'disk' => ['driver' => 'local', 'root' => dirname(__DIR__).'/_files/', 'prefix' => 'prefix']]); + } + + protected function hasIncorrectEncryptedDisk($app): void + { + $app['config']->set('filesystems.default', 'local'); + $app['config']->set('filesystems.disks.local', ['driver' => 'encrypted']); + } + + #[Test] + #[DefineEnvironment('hasEncryptedInlineDisk')] + public function itRegistersTheFilesystemDriverWithInlineDisk(): void + { + $contents = Storage::get('read.txt'); + + $this->assertSame('YSvdOxSZ8pyTdDWeN8qI', $contents); + } + #[Test] - #[DefineEnvironment('usesEncryptedDisk')] - public function itRegistersTheFilesystemDriver(): void + #[DefineEnvironment('hasEncryptedReferencedDisk')] + public function itRegistersTheFilesystemDriverWithReferencedDisk(): void { $contents = Storage::get('read.txt'); $this->assertSame('YSvdOxSZ8pyTdDWeN8qI', $contents); } + + #[Test] + #[DefineEnvironment('hasEncryptedFtpDisk')] + public function itRegistersTheFilesystemDriverWithFtpDisk(): void + { + $filesystem = Storage::disk(); + + $this->assertInstanceOf(FtpAdapter::class, $filesystem->getAdapter()); + } + + #[Test] + #[DefineEnvironment('hasEncryptedDiskWithPrefix')] + public function itRegistersTheFilesystemDriverWithPrefixedDisk(): void + { + $contents = Storage::get('read.txt'); + + $this->assertSame('hi7OJgUQlfk00nd3jmM1', $contents); + } + + #[Test] + #[DefineEnvironment('hasIncorrectEncryptedDisk')] + public function itFailsWhenDiskIsMissing(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Encrypted disk is missing "disk" configuration option.'); + + Storage::get('read.txt'); + } } diff --git a/tests/_files/prefix/read.txt b/tests/_files/prefix/read.txt new file mode 100644 index 0000000..27463e0 --- /dev/null +++ b/tests/_files/prefix/read.txt @@ -0,0 +1 @@ +eyJpdiI6IllsYmJDQTdlaXBiaVc4K1NXQ0w4M0E9PSIsInZhbHVlIjoiS2k2YVJqcHZ5eEh5MEFoTmE2bXJ1TXlFeTE1dE1jb0psN2tTWU9LWW9VZz0iLCJtYWMiOiJlNWJkYzAwNTJkZjY0Y2M4MTMxNzg1OTcxNzU5YTM0MmI1YWIxNWZiZjA1OGRmZjQwOTVkNWY1OTNlMzdlZmEwIiwidGFnIjoiIn0= \ No newline at end of file