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);
+ }
}