Added whereIn clause

This commit is contained in:
Bjorn Voesten 2020-12-25 01:37:58 +01:00
parent ddf24dd78f
commit 278719a552
3 changed files with 226 additions and 42 deletions

View File

@ -1,12 +1,14 @@
# CipherSweet for Laravel
A Laravel implementation of [Paragon Initiative Enterprises CipherSweet](https://ciphersweet.paragonie.com) searchable field level encryption.
A Laravel implementation of [Paragon Initiative Enterprises CipherSweet](https://ciphersweet.paragonie.com) searchable
field level encryption.
Make sure you have some basic understanding of CipherSweet before continuing.
## Installation
Install the package using composer:
```
composer require bjorn-voesten/ciphersweet-for-laravel
```
@ -16,10 +18,13 @@ The package will then automatically register itself.
#### Encryption key
In your `.env` file you should add:
```dotenv
CIPHERSWEET_KEY=
```
And then generate an encryption key:
```
php artisan ciphersweet:key
```
@ -27,6 +32,7 @@ php artisan ciphersweet:key
#### Config file
Publish the config file:
```
php artisan vendor:publish --tag=ciphersweet-config
```
@ -35,8 +41,9 @@ php artisan vendor:publish --tag=ciphersweet-config
### Define encryption
Add the `BjornVoesten\CipherSweet\Concerns\WithAttributeEncryption` trait to your model <br>
Add the `BjornVoesten\CipherSweet\Concerns\WithAttributeEncryption` trait to your model <br>
and add the `BjornVoesten\CipherSweet\Casts\Encrypted` cast to the attributes you want to encrypt.
```php
<?php
@ -99,29 +106,42 @@ class User extends Model
Attributes will be automatically encrypted and decrypted when filling and retrieving attribute values.
**Note** Because the package uses Laravel casts it is not possible to combine the `Encrypted` cast and accessors/mutators.
**Note** Because the package uses Laravel casts it is not possible to combine the `Encrypted` cast and
accessors/mutators.
### Searching
**Note** When searching with the `equal to` operator models will be returned when the value is found in one of all available or defined indexes. When searching with the `not equal to` operator all models where the value is not found in any of the available or the defined indexes are returned.
**Note** When searching with the `equal to` operator models will be returned when the value is found in one of all
available or defined indexes. When searching with the `not equal to` operator all models where the value is not found in
any of the available or the defined indexes are returned.
**Note**
Because of the limited search possibilities in CipherSweet only the `=` and `!=` operators are available when searching encrypted attributes.
Because of the limited search possibilities in CipherSweet only the `=` and `!=` operators are available when searching
encrypted attributes.
#### `whereEncrypted`
<br/>
#### `whereEncrypted`, `orWhereEncrypted`
```php
User::query()
->whereEncrypted('social_security_number', '=', '123-456-789')
->orWhereEncrypted('social_security_number', '=', '123-456-789')
->get();
```
#### `orWhereEncrypted`
<br/>
#### `whereInEncrypted`, `orWhereInEncrypted`
```php
User::query()
->whereEncrypted('social_security_number', '=', '123-456-789')
->orWhereEncrypted('social_security_number', '=', '456-123-789')
->whereInEncrypted('social_security_number', [
'123-456-789',
])
->orWhereInEncrypted('social_security_number', [
'456-123-789',
])
->get();
```
@ -131,7 +151,8 @@ Please see [contributing.md](contributing.md) for details and a todolist.
## Security
If you discover any security related issues, please email [security@bjornvoesten.com](mailto:security@bjornvoesten.com) instead of using the issue tracker.
If you discover any security related issues, please email [security@bjornvoesten.com](mailto:security@bjornvoesten.com)
instead of using the issue tracker.
## Testing

View File

@ -3,7 +3,6 @@
namespace BjornVoesten\CipherSweet\Concerns;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Arr;
/**
* @mixin \Illuminate\Database\Eloquent\Model
@ -16,9 +15,8 @@ trait WithAttributeEncryption
* @param string $attribute
* @param string|int|boolean $value
* @return array
* @throws \Exception
*/
public function encrypt(string $attribute, $value)
public function encrypt(string $attribute, $value): array
{
[$ciphertext, $indexes] = $result = app('ciphersweet')->encrypt(
$this, $attribute, $value
@ -37,9 +35,9 @@ trait WithAttributeEncryption
* Encrypt the attribute.
*
* @param string $attribute
* @return $this
* @return string
*/
public function decrypt(string $attribute)
public function decrypt(string $attribute): string
{
return app('ciphersweet')->decrypt(
$this, $attribute, $this->attributes[$attribute]
@ -54,27 +52,25 @@ trait WithAttributeEncryption
* @param $operator
* @param $value
* @param array $indexes
* @param string $boolean
* @return void
* @throws \Exception
*/
public function scopeWhereEncrypted(Builder $query, string $column, $operator, $value, array $indexes = []): void
public function scopeWhereEncrypted(Builder $query, string $column, $operator, $value, array $indexes = [], $boolean = 'and'): void
{
$available = Arr::last(
$this->encrypt($column, $value)
);
/** @var array $available */
$available = $this->encrypt($column, $value)[1];
$indexes = empty($indexes)
? array_keys($available)
: $indexes;
$indexes = empty($indexes) ? array_keys($available) : $indexes;
$first = true;
foreach ($indexes as $index) {
$first
? $query->where($index, $operator, $available[$index])
: $query->orWhere($index, $operator, $available[$index]);
$method = $boolean === 'or'
? 'orWhere'
: 'where';
$first = false;
}
$query->{$method}(function (Builder $query) use ($available, $operator, $indexes) {
foreach ($indexes as $key => $index) {
$query->where($index, $operator, $available[$index]);
}
});
}
/**
@ -83,23 +79,76 @@ trait WithAttributeEncryption
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $column
* @param string $operator
* @param $value
* @param mixed $value
* @param array $indexes
* @param string $boolean
* @return void
*/
public function scopeOrWhereEncrypted(
Builder $query, string $column, string $operator, $value,
array $indexes = [], $boolean = 'or'
): void
{
$this->scopeWhereEncrypted(
$query, $column, $operator, $value, $indexes, $boolean
);
}
/**
* Add a where in clause to the query for an encrypted column.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $column
* @param array $values
* @param array $indexes
* @param string $boolean
* @return void
* @throws \Exception
*/
public function scopeOrWhereEncrypted(Builder $query, string $column, string $operator, $value, array $indexes = []): void
public function scopeWhereInEncrypted(
Builder $query, string $column, array $values, array $indexes = [],
$boolean = 'and'
): void
{
$available = Arr::last(
$this->encrypt($column, $value)
$values = array_map(function (string $value) use ($column) {
return $this->encrypt($column, $value)[1];
}, $values);
$available = array_keys($values[0]);
$indexes = empty($indexes) ? $available : $indexes;
$method = $boolean === 'or'
? 'orWhere'
: 'where';
$query->{$method}(function (Builder $query) use ($values, $indexes) {
foreach ($indexes as $key => $index) {
(bool) $key
? $query->orWhereIn($index, $values)
: $query->whereIn($index, $values);
}
});
}
/**
* Add a or where in clause to the query for an encrypted column.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $column
* @param array $values
* @param array $indexes
* @param string $boolean
* @return void
* @throws \Exception
*/
public function scopeOrWhereInEncrypted(
Builder $query, string $column, array $values, array $indexes = [],
$boolean = 'or'
): void
{
$this->scopeWhereInEncrypted(
$query, $column, $values, $indexes, $boolean
);
$indexes = empty($indexes)
? array_keys($available)
: $indexes;
foreach ($indexes as $index) {
$query->orWhere($index, $operator, $available[$index]);
}
}
}

View File

@ -98,4 +98,118 @@ class QueryTest extends TestCase
])
->get();
}
public function testCanQueryEncryptedAttributeWithWhereInClause(): void
{
$userOne = $this->user('123-456-789');
$userTwo = $this->user('789-456-123');
$userThree = $this->user('456-789-123');
// Assert success.
/** @var \Illuminate\Database\Eloquent\Collection $keys */
$keys = User::query()
->whereInEncrypted(
'social_security_number',
[
'123-456-789',
'789-456-123',
]
)
->get()
->modelKeys();
$this->assertContains($userOne->id, $keys);
$this->assertContains($userTwo->id, $keys);
$this->assertNotContains($userThree->id, $keys);
// Assert success using provided index.
/** @var \Illuminate\Database\Eloquent\Collection $keys */
$keys = User::query()
->whereInEncrypted(
'social_security_number',
[
'123-456-789',
'789-456-123',
],
['social_security_number_index']
)
->get()
->modelKeys();
$this->assertContains($userOne->id, $keys);
$this->assertContains($userTwo->id, $keys);
$this->assertNotContains($userThree->id, $keys);
$keys = User::query()
->whereInEncrypted(
'social_security_number',
['789-456-123'],
['non_existing_index']
)
->get()
->modelKeys();
$this->assertEmpty($keys);
}
public function testCanQueryEncryptedAttributeWithOrWhereInClause(): void
{
$userOne = $this->user('123-456-789');
$userTwo = $this->user('789-456-123');
$userThree = $this->user('456-789-123');
// Assert success.
/** @var \Illuminate\Database\Eloquent\Collection $keys */
$keys = User::query()
->whereInEncrypted(
'social_security_number',
['123-456-789']
)
->orWhereInEncrypted(
'social_security_number',
['789-456-123']
)
->get()
->modelKeys();
$this->assertContains($userOne->id, $keys);
$this->assertContains($userTwo->id, $keys);
$this->assertNotContains($userThree->id, $keys);
// Assert success using provided index.
/** @var \Illuminate\Database\Eloquent\Collection $keys */
$keys = User::query()
->whereInEncrypted(
'social_security_number',
['123-456-789'],
['social_security_number_index']
)
->orWhereInEncrypted(
'social_security_number',
['789-456-123'],
['social_security_number_index']
)
->get()
->modelKeys();
$this->assertContains($userOne->id, $keys);
$this->assertContains($userTwo->id, $keys);
$this->assertNotContains($userThree->id, $keys);
$keys = User::query()
->whereInEncrypted(
'social_security_number',
['789-456-123'],
['non_existing_index']
)
->orWhereInEncrypted(
'social_security_number',
['789-456-123'],
['non_existing_index']
)
->get()
->modelKeys();
$this->assertEmpty($keys);
}
}