Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
/PULL_REQUEST_TEMPLATE.md export-ignore
/ISSUE_TEMPLATE.md export-ignore
/phpunit.xml.dist export-ignore
/workbench export-ignore
/tests export-ignore
/docs export-ignore
9 changes: 2 additions & 7 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,9 @@ jobs:
strategy:
fail-fast: false
matrix:
php: [ '8.1', '8.2', '8.3' ]
laravel: [ '10.*', '11.*', '12.*' ]
php: [ '8.2', '8.3' ]
laravel: [ '^12.18' ]
stability: [ prefer-lowest, prefer-stable ]
exclude:
- laravel: '11.*'
php: '8.1'
- laravel: '12.*'
php: '8.1'

name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }}

Expand Down
56 changes: 56 additions & 0 deletions MIGRATING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Migrating swisnl/laravel-encrypted-data

## To 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.18 or higher.
2. 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
```
3. Set up Encrypted Casting:
```diff
- protected $encrypted = [
- 'secret',
- ];
+ protected $casts = [
+ 'secret' => 'encrypted',
+ ];
```
4. If you're using encrypted booleans or date(time)s, use the custom casts provided by this package:
```diff
- protected $encrypted = [
- 'secret_boolean',
- 'secret_datetime',
- ];
-
- protected $casts = [
- 'secret_boolean' => 'bool',
- 'secret_datetime' => 'datetime',
- ];
+ protected $casts = [
+ 'secret_boolean' => \Swis\Laravel\Encrypted\Casts\AsEncryptedBoolean::class,
+ 'secret_datetime' => \Swis\Laravel\Encrypted\Casts\AsEncryptedDateTime::class,
+ ];
```
5. If you're using other casts for encrypted attributes, or you need serialization support, you should create custom casts yourself, as this package does not provide casts for every situation. Please see [Custom Casts](https://laravel.com/docs/eloquent-mutators#custom-casts) for more information on how to create custom casts. You can use any of the casts provided by this package as a reference.
6. 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 used to extend \Swis\Laravel\Encrypted\EncryptedModel
}
```
This custom model encrypter is backward compatible with the old `EncryptedModel` and will handle the deserialization of the data before casts kick in. Data will **not** be serialized when re-encrypting, so it will be compatible with Laravel Encrypted Casting. This makes sure your application can keep running and the data is not lost during the migration process.
7. 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!
8. Remove our custom model encrypter from your `AppServiceProvider` (step 6).
43 changes: 26 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,45 @@ This package contains several Laravel utilities to work with encrypted data.

Via Composer

``` bash
$ composer require swisnl/laravel-encrypted-data
```bash
composer require swisnl/laravel-encrypted-data
```

## Usage

### Models
### Eloquent casts

> [!WARNING]
> Laravel supports [encrypted casts](https://laravel.com/docs/10.x/eloquent-mutators#encrypted-casting) since version 8.12, so new projects should use that instead of the models provided by this package.
>
> We aim to provide a migration path to encrypted casts. See issue [#1](https://github.com/swisnl/laravel-encrypted-data/issues/1) for more information.
> Older versions of this package needed a custom model class to encrypt data. This is now deprecated in favor of custom casts. Please see [MIGRATING](MIGRATING.md) for a step-by-step guide on how to migrate.
>

Extend `\Swis\Laravel\Encrypted\EncryptedModel` in your model and define the encrypted fields. Make sure your database columns are long enough, so your data isn't truncated!
You can use the Eloquent casts provided by this package and everything will be encrypted/decrypted under the hood!

#### Boolean

``` php
protected $encrypted = [
'secret',
```php
protected $casts = [
'boolean' => \Swis\Laravel\Encrypted\Casts\AsEncryptedBoolean::class,
];
```

You can now simply use the model properties as usual and everything will be encrypted/decrypted under the hood!
#### Datetime

```php
protected $casts = [
'date' => \Swis\Laravel\Encrypted\Casts\AsEncryptedDate::class,
'datetime' => \Swis\Laravel\Encrypted\Casts\AsEncryptedDateTime::class,
'immutable_date' => \Swis\Laravel\Encrypted\Casts\AsEncryptedImmutableDate::class,
'immutable_datetime' => \Swis\Laravel\Encrypted\Casts\AsEncryptedImmutableDateTime::class,
'date_with_custom_format' => \Swis\Laravel\Encrypted\Casts\AsEncryptedDate::format('Y-m-d'),
];
```

### Filesystem

Configure the storage driver in `config/filesystems.php`.

``` php
```php
'disks' => [
'local' => [
'driver' => 'local-encrypted',
Expand All @@ -60,10 +70,9 @@ Due to the encryption, some issues/limitations apply:

1. Encrypted data is — depending on what you encrypt — roughly 30-40% bigger.

### Models
### Casts

1. You can't query or order columns that are encrypted in your SQL-statements, but you can query or sort the results using collection methods;
2. All data is being serialized before it is encrypted — and unserialized after it is decrypted — so everything is stored exactly as how Laravel would insert it into the database. You can use [Eloquent Mutators](https://laravel.com/docs/9.x/eloquent-mutators) as you normally would.
1. You can't query or order columns that are encrypted in your SQL-statements, but you can query or sort the results using collection methods.

### Filesystem

Expand All @@ -76,8 +85,8 @@ Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed re

## Testing

``` bash
$ composer test
```bash
composer test
```

## Contributing
Expand Down
18 changes: 13 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@
],
"require": {
"php": "^8.1",
"laravel/framework": "^10.0|^11.0|^12.0",
"laravel/framework": "^12.18",
"swisnl/flysystem-encrypted": "^2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.0",
"orchestra/testbench": "^8.0|^9.0|^10.0",
"phpunit/phpunit": "^10.5|^11.5"
"orchestra/testbench": "^10.0",
"phpunit/phpunit": "^11.5"
},
"autoload": {
"psr-4": {
Expand All @@ -36,13 +36,21 @@
},
"autoload-dev": {
"psr-4": {
"Swis\\Laravel\\Encrypted\\Tests\\": "tests"
"Swis\\Laravel\\Encrypted\\Tests\\": "tests",
"Workbench\\App\\": "workbench/app/"
}
},
"scripts": {
"test": "phpunit",
"check-style": "php-cs-fixer fix --dry-run -v",
"fix-style": "php-cs-fixer fix"
"fix-style": "php-cs-fixer fix",
"post-autoload-dump": [
"@clear",
"@prepare"
],
"clear": "@php vendor/bin/testbench package:purge-skeleton --ansi",
"prepare": "@php vendor/bin/testbench package:discover --ansi",
"build": "@php vendor/bin/testbench workbench:build --ansi"
},
"extra": {
"branch-alias": {
Expand Down
7 changes: 5 additions & 2 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
stopOnFailure="false"
cacheDirectory=".phpunit.cache">
<testsuites>
<testsuite name="swisnl Test Suite">
<directory>tests</directory>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
</testsuites>
<coverage>
Expand Down
63 changes: 63 additions & 0 deletions src/Casts/AsEncryptedBoolean.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace Swis\Laravel\Encrypted\Casts;

use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Contracts\Database\Eloquent\ComparesCastableAttributes;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Crypt;

class AsEncryptedBoolean implements Castable
{
/**
* @param string[] $arguments
*
* @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<bool, bool>
*/
public static function castUsing(array $arguments): CastsAttributes
{
return new class implements CastsAttributes, ComparesCastableAttributes {
/**
* @param string|null $value
* @param array<string, mixed> $attributes
*/
public function get(Model $model, string $key, mixed $value, array $attributes): ?bool
{
if ($value === null) {
return null;
}

return (bool) ($model::$encrypter ?? Crypt::getFacadeRoot())->decrypt($value, false);
}

/**
* @param bool|null $value
* @param array<string, mixed> $attributes
*/
public function set(Model $model, string $key, #[\SensitiveParameter] mixed $value, array $attributes): ?string
{
if ($value === null) {
return null;
}

return ($model::$encrypter ?? Crypt::getFacadeRoot())->encrypt((string) (int) $value, false);
}

/**
* @param string|null $firstValue
* @param string|null $secondValue
*/
public function compare(Model $model, string $key, mixed $firstValue, mixed $secondValue): bool
{
if (!empty(($model::$encrypter ?? Crypt::getFacadeRoot())->getPreviousKeys())) {
return false;
}

return $this->get($model, $key, $firstValue, []) === $this->get($model, $key, $secondValue, []);
}
};
}
}
29 changes: 29 additions & 0 deletions src/Casts/AsEncryptedDate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Swis\Laravel\Encrypted\Casts;

use Carbon\CarbonImmutable;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Carbon;

class AsEncryptedDate implements Castable
{
/**
* @param string[] $arguments
*/
public static function castUsing(array $arguments): CastsAttributes
{
return new EncryptedDateTime($arguments, fn (#[\SensitiveParameter] Carbon|CarbonImmutable|null $value) => $value?->startOfDay());
}

/**
* Specify the format to use when the model is serialized to an array or JSON.
*/
public static function format(string $format): string
{
return static::class.':'.$format;
}
}
27 changes: 27 additions & 0 deletions src/Casts/AsEncryptedDateTime.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Swis\Laravel\Encrypted\Casts;

use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class AsEncryptedDateTime implements Castable
{
/**
* @param string[] $arguments
*/
public static function castUsing(array $arguments): CastsAttributes
{
return new EncryptedDateTime($arguments);
}

/**
* Specify the format to use when the model is serialized to an array or JSON.
*/
public static function format(string $format): string
{
return static::class.':'.$format;
}
}
29 changes: 29 additions & 0 deletions src/Casts/AsEncryptedImmutableDate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Swis\Laravel\Encrypted\Casts;

use Carbon\CarbonImmutable;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Carbon;

class AsEncryptedImmutableDate implements Castable
{
/**
* @param string[] $arguments
*/
public static function castUsing(array $arguments): CastsAttributes
{
return new EncryptedDateTime($arguments, fn (#[\SensitiveParameter] Carbon|CarbonImmutable|null $value) => $value?->startOfDay()->toImmutable());
}

/**
* Specify the format to use when the model is serialized to an array or JSON.
*/
public static function format(string $format): string
{
return static::class.':'.$format;
}
}
29 changes: 29 additions & 0 deletions src/Casts/AsEncryptedImmutableDateTime.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Swis\Laravel\Encrypted\Casts;

use Carbon\CarbonImmutable;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Carbon;

class AsEncryptedImmutableDateTime implements Castable
{
/**
* @param string[] $arguments
*/
public static function castUsing(array $arguments): CastsAttributes
{
return new EncryptedDateTime($arguments, fn (#[\SensitiveParameter] Carbon|CarbonImmutable|null $value) => $value?->toImmutable());
}

/**
* Specify the format to use when the model is serialized to an array or JSON.
*/
public static function format(string $format): string
{
return static::class.':'.$format;
}
}
Loading