-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
289 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# Migrating swisnl/laravel-encrypted-data | ||
|
||
## To Laravel Encrypted Casting | ||
The main difference between this package and [Laravel Encrypted Casting](https://laravel.com/docs/10.x/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: | ||
|
||
[//]: # (TODO: What to do when you need serialized data?) | ||
|
||
1. Set up Encrypted Casting, but keep extending `EncryptedModel` from this package: | ||
```diff | ||
- protected $encrypted = [ | ||
- 'secret', | ||
- ]; | ||
+ protected $casts = [ | ||
+ 'secret' => 'encrypted', | ||
+ ]; | ||
``` | ||
2. Set up our custom model encrypter in your `AppServiceProvider`: | ||
```php | ||
public function boot(): void | ||
{ | ||
$modelEncrypter = new \Swis\Laravel\Encrypted\ModelEncrypter(); | ||
YourEncryptedModel::encryptUsing($modelEncrypter); | ||
// ... all your other models that extend \Swis\Laravel\Encrypted\EncryptedModel | ||
} | ||
``` | ||
3. Run our re-encryption command: | ||
```bash | ||
php artisan encrypted-data:re-encrypt:models --quietly --no-touch | ||
``` | ||
N.B. Use `--help` to see all available options and modify as needed! | ||
4. Remove the `Swis\Laravel\Encrypted\EncryptedModel` from your models and replace it with `Illuminate\Database\Eloquent\Model`: | ||
```diff | ||
- use Swis\Laravel\Encrypted\EncryptedModel | ||
+ use Illuminate\Database\Eloquent\Model | ||
|
||
- class YourEncryptedModel extends EncryptedModel | ||
+ class YourEncryptedModel extends Model | ||
``` | ||
5. Remove our custom model encrypter from your `AppServiceProvider` (step 2). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
<?php | ||
|
||
namespace Swis\Laravel\Encrypted\Commands; | ||
|
||
use Illuminate\Console\Command; | ||
use Illuminate\Database\Eloquent\Model; | ||
use Illuminate\Database\Eloquent\SoftDeletes; | ||
use Illuminate\Support\Collection; | ||
use Illuminate\Support\Str; | ||
use Symfony\Component\Finder\Finder; | ||
use Symfony\Component\Finder\SplFileInfo; | ||
|
||
class ReEncryptModels extends Command | ||
{ | ||
protected $signature = 'encrypted-data:re-encrypt:models | ||
{--model=* : Class names of the models to be re-encrypted} | ||
{--except=* : Class names of the models to be excluded from re-encryption} | ||
{--path=* : Absolute path(s) to directories where models are located} | ||
{--chunk=1000 : The number of models to retrieve per chunk of models to be re-encrypted} | ||
{--quietly : Re-encrypt the models without raising any events} | ||
{--no-touch : Re-encrypt the models without updating timestamps} | ||
{--with-trashed : Re-encrypt trashed models}'; | ||
|
||
protected $description = 'Re-encrypt models'; | ||
|
||
public function handle(): int | ||
{ | ||
$models = $this->models(); | ||
|
||
if ($models->isEmpty()) { | ||
$this->warn('No models found.'); | ||
|
||
return self::FAILURE; | ||
} | ||
|
||
if ($this->confirm('The following models will be re-encrypted: '.PHP_EOL.$models->implode(PHP_EOL).PHP_EOL.'Do you want to continue?') === false) { | ||
return self::FAILURE; | ||
} | ||
|
||
$models->each(function (string $model) { | ||
$this->line("Re-encrypting {$model}..."); | ||
$this->reEncryptModels($model); | ||
}); | ||
|
||
$this->info('Re-encrypting done!'); | ||
|
||
return self::SUCCESS; | ||
} | ||
|
||
/** | ||
* @param class-string<\Illuminate\Database\Eloquent\Model> $modelClass | ||
*/ | ||
protected function reEncryptModels(string $modelClass): void | ||
{ | ||
$modelClass::unguarded(function () use ($modelClass) { | ||
$modelClass::query() | ||
->when($this->option('with-trashed') && in_array(SoftDeletes::class, class_uses_recursive($modelClass), true), function ($query) { | ||
$query->withTrashed(); | ||
}) | ||
->eachById( | ||
function (Model $model) { | ||
if ($this->option('no-touch')) { | ||
$model->timestamps = false; | ||
} | ||
|
||
// Set each encrypted attribute to trigger re-encryption | ||
collect($model->getCasts()) | ||
->keys() | ||
->filter(fn ($key) => $model->hasCast($key, ['encrypted', 'encrypted:array', 'encrypted:collection', 'encrypted:json', 'encrypted:object']))/* | ||
->each(fn ($key) => $model->setAttribute($key, $model->getAttribute($key)))*/; | ||
|
||
if ($this->option('quietly')) { | ||
$model->saveQuietly(); | ||
} else { | ||
$model->save(); | ||
} | ||
}, | ||
$this->option('chunk') | ||
); | ||
}); | ||
} | ||
|
||
/** | ||
* Determine the models that should be re-encrypted. | ||
* | ||
* @return \Illuminate\Support\Collection<int, class-string<\Illuminate\Database\Eloquent\Model>> | ||
*/ | ||
protected function models(): Collection | ||
{ | ||
if (!empty($this->option('model')) && !empty($this->option('except'))) { | ||
throw new \InvalidArgumentException('The --models and --except options cannot be combined.'); | ||
} | ||
|
||
if (!empty($models = $this->option('model'))) { | ||
return collect($models) | ||
->map([$this, 'normalizeModelClass']) | ||
->each(function (string $modelClass): void { | ||
if (!class_exists($modelClass)) { | ||
throw new \InvalidArgumentException(sprintf('Model class %s does not exist.', $modelClass)); | ||
} | ||
}); | ||
} | ||
|
||
if (!empty($except = $this->option('except'))) { | ||
$except = array_map([$this, 'normalizeModelClass'], $except); | ||
} | ||
|
||
return collect(Finder::create()->in($this->getModelsPath())->files()->name('*.php')) | ||
->map(function (SplFileInfo $modelFile): string { | ||
$namespace = $this->laravel->getNamespace(); | ||
|
||
return $namespace.str_replace( | ||
[DIRECTORY_SEPARATOR, '.php'], | ||
['\\', ''], | ||
Str::after($modelFile->getRealPath(), realpath(app_path()).DIRECTORY_SEPARATOR) | ||
); | ||
}) | ||
->when(!empty($except), fn (Collection $modelClasses): Collection => $modelClasses->reject(fn (string $modelClass) => in_array($modelClass, $except, true))) | ||
->filter(fn (string $modelClass): bool => class_exists($modelClass) && is_a($modelClass, Model::class, true)) | ||
->reject(function (string $modelClass): bool { | ||
$model = new $modelClass(); | ||
|
||
return collect($model->getCasts()) | ||
->keys() | ||
->filter(fn (string $key): bool => $model->hasCast($key, ['encrypted', 'encrypted:array', 'encrypted:collection', 'encrypted:json', 'encrypted:object'])) | ||
->isEmpty(); | ||
}) | ||
->values(); | ||
} | ||
|
||
/** | ||
* Get the path where models are located. | ||
* | ||
* @return string[]|string | ||
*/ | ||
protected function getModelsPath(): string|array | ||
{ | ||
if (!empty($path = $this->option('path'))) { | ||
return collect($path) | ||
->map(fn (string $path): string => base_path($path)) | ||
->each(function (string $path): void { | ||
if (!is_dir($path)) { | ||
throw new \InvalidArgumentException(sprintf('The path %s is not a directory.', $path)); | ||
} | ||
}) | ||
->all(); | ||
} | ||
|
||
return is_dir($path = app_path('Models')) ? $path : app_path(); | ||
} | ||
|
||
/** | ||
* Get the namespace of models. | ||
*/ | ||
protected function getModelsNamespace(): string | ||
{ | ||
return is_dir(app_path('Models')) ? $this->laravel->getNamespace().'Models\\' : $this->laravel->getNamespace(); | ||
} | ||
|
||
/** | ||
* Make sure the model class is a FQCN. | ||
*/ | ||
protected function normalizeModelClass(string $modelClass): string | ||
{ | ||
return str_starts_with($modelClass, $this->getModelsNamespace()) ? $modelClass : $this->getModelsNamespace().$modelClass; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
<?php | ||
|
||
namespace Swis\Laravel\Encrypted; | ||
|
||
use Illuminate\Contracts\Encryption\Encrypter; | ||
|
||
/** | ||
* @deprecated only use this when migrating from this package to Laravel's built-in encrypted casting | ||
*/ | ||
class ModelEncrypter implements Encrypter | ||
{ | ||
private ?Encrypter $encrypter; | ||
|
||
public function __construct() | ||
{ | ||
$this->encrypter = app('encrypted-data.encrypter'); | ||
} | ||
|
||
public function encrypt($value, $serialize = true) | ||
{ | ||
return $this->encrypter->encrypt($value, $serialize); | ||
} | ||
|
||
public function decrypt($payload, $unserialize = true) | ||
{ | ||
$decrypted = $this->encrypter->decrypt($payload, false); | ||
|
||
$unserialized = @unserialize($decrypted); | ||
if ($unserialized === false && $decrypted !== 'b:0;') { | ||
return $decrypted; | ||
} | ||
|
||
return $unserialized; | ||
} | ||
|
||
public function getKey() | ||
{ | ||
return $this->encrypter->getKey(); | ||
} | ||
|
||
public function getAllKeys() | ||
{ | ||
return $this->encrypter->getAllKeys(); | ||
} | ||
|
||
public function getPreviousKeys() | ||
{ | ||
return $this->encrypter->getPreviousKeys(); | ||
} | ||
} |