diff --git a/readme.md b/readme.md index 9c39f41..ee8b1c2 100644 --- a/readme.md +++ b/readme.md @@ -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
+Add the `BjornVoesten\CipherSweet\Concerns\WithAttributeEncryption` trait to your model
and add the `BjornVoesten\CipherSweet\Casts\Encrypted` cast to the attributes you want to encrypt. + ```php + +#### `whereEncrypted`, `orWhereEncrypted` ```php User::query() ->whereEncrypted('social_security_number', '=', '123-456-789') + ->orWhereEncrypted('social_security_number', '=', '123-456-789') ->get(); ``` -#### `orWhereEncrypted` +
+ +#### `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 diff --git a/src/Concerns/WithAttributeEncryption.php b/src/Concerns/WithAttributeEncryption.php index 4e9699e..5e9ef14 100644 --- a/src/Concerns/WithAttributeEncryption.php +++ b/src/Concerns/WithAttributeEncryption.php @@ -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]); - } } } diff --git a/tests/Unit/QueryTest.php b/tests/Unit/QueryTest.php index 9ac8f8c..0fa2cad 100644 --- a/tests/Unit/QueryTest.php +++ b/tests/Unit/QueryTest.php @@ -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); + } }