diff --git a/src/Casts/Encrypted.php b/src/Casts/Encrypted.php index c84eac1..50f09f3 100644 --- a/src/Casts/Encrypted.php +++ b/src/Casts/Encrypted.php @@ -13,10 +13,10 @@ class Encrypted implements CastsAttributes * @param string $key * @param mixed $value * @param array $attributes - * @return string + * @return string|null * @throws \Exception */ - public function get($model, string $key, $value, array $attributes) + public function get($model, string $key, $value, array $attributes): ?string { return $model->decrypt($key); } diff --git a/src/CipherSweetService.php b/src/CipherSweetService.php index e67dcd5..9080441 100644 --- a/src/CipherSweetService.php +++ b/src/CipherSweetService.php @@ -108,23 +108,23 @@ class CipherSweetService $field = new EncryptedField( $this->engine, $table = $model->getTable(), - $attribute->column, + $attribute->column ); // Map and add the indexes to the encrypted // field instance. collect($attribute->indexes) - ->map(function (Index $index) { - return $index = new BlindIndex( + ->map(static function (Index $index) { + return new BlindIndex( $index->column, $index->transformers, $index->bits, - $index->fast, + $index->fast ); }) - ->each( - fn($index) => $field->addBlindIndex($index) - ); + ->each(static function ($index) use ($field) { + return $field->addBlindIndex($index); + }); return $field; } @@ -134,18 +134,24 @@ class CipherSweetService * * @param \Illuminate\Database\Eloquent\Model|\BjornVoesten\CipherSweet\Concerns\WithAttributeEncryption $model * @param string $attribute - * @param string|int|boolean $value + * @param string|int|boolean|null $value * @return array * @throws \ParagonIE\CipherSweet\Exception\BlindIndexNameCollisionException * @throws \ParagonIE\CipherSweet\Exception\BlindIndexNotFoundException * @throws \ParagonIE\CipherSweet\Exception\CryptoOperationException * @throws \SodiumException */ - public function encrypt(Model $model, string $attribute, $value) + public function encrypt(Model $model, string $attribute, $value): array { - return $this - ->field($model, $attribute) - ->prepareForStorage($value); + $field = $this->field($model, $attribute); + + if (is_null($value)) { + return [null, array_map(static function () { + return null; + }, $field->getAllBlindIndexes(''))]; + } + + return $field->prepareForStorage($value); } /** @@ -158,8 +164,12 @@ class CipherSweetService * @throws \ParagonIE\CipherSweet\Exception\BlindIndexNameCollisionException * @throws \ParagonIE\CipherSweet\Exception\CryptoOperationException */ - public function decrypt(Model $model, string $attribute, $value) + public function decrypt(Model $model, string $attribute, $value): ?string { + if (is_null($value)) { + return null; + } + return $this ->field($model, $attribute) ->decryptValue($value); diff --git a/src/Concerns/WithAttributeEncryption.php b/src/Concerns/WithAttributeEncryption.php index 5e9ef14..463a055 100644 --- a/src/Concerns/WithAttributeEncryption.php +++ b/src/Concerns/WithAttributeEncryption.php @@ -35,12 +35,12 @@ trait WithAttributeEncryption * Encrypt the attribute. * * @param string $attribute - * @return string + * @return string|null */ - public function decrypt(string $attribute): string + public function decrypt(string $attribute): ?string { return app('ciphersweet')->decrypt( - $this, $attribute, $this->attributes[$attribute] + $this, $attribute, $this->attributes[$attribute] ?? null ); } diff --git a/tests/Concerns/CreateUsersTable.php b/tests/Concerns/CreateUsersTable.php index 8ee98c3..9c96031 100644 --- a/tests/Concerns/CreateUsersTable.php +++ b/tests/Concerns/CreateUsersTable.php @@ -10,8 +10,8 @@ trait CreateUsersTable protected function createUsersTable(): void { Schema::create('users', function (Blueprint $table) { - $table->id('id'); - $table->string('social_security_number'); + $table->id(); + $table->string('social_security_number')->nullable(); $table->string('social_security_number_index')->nullable(); $table->string('custom_index')->nullable(); $table->timestamps(); diff --git a/tests/Concerns/CreatesUsers.php b/tests/Concerns/CreatesUsers.php index 25542d2..59e1da2 100644 --- a/tests/Concerns/CreatesUsers.php +++ b/tests/Concerns/CreatesUsers.php @@ -9,10 +9,10 @@ trait CreatesUsers /** * Create a new user instance. * - * @param string $socialSecurityNumber + * @param string|null $socialSecurityNumber * @return \Tests\Mocks\User|\Illuminate\Database\Eloquent\Model */ - protected function user(string $socialSecurityNumber): User + protected function user(?string $socialSecurityNumber): User { return User::query()->create([ 'social_security_number' => $socialSecurityNumber, diff --git a/tests/Unit/EncryptionTest.php b/tests/Unit/EncryptionTest.php index 284ffb0..3da8a88 100644 --- a/tests/Unit/EncryptionTest.php +++ b/tests/Unit/EncryptionTest.php @@ -27,12 +27,12 @@ class EncryptionTest extends TestCase 'social_security_number' => '123-456-789', ]); - $this->assertSame( + static::assertSame( '123-456-789', $user->social_security_number ); - $this->assertNotEmpty( + static::assertNotEmpty( $user->social_security_number_index ); } @@ -41,12 +41,12 @@ class EncryptionTest extends TestCase { $user = $this->user('123-456-789'); - $this->assertNotSame( + static::assertNotSame( '123-456-789', $user->getRawOriginal('social_security_number') ); - $this->assertNotEmpty( + static::assertNotEmpty( $user->social_security_number_index ); @@ -78,12 +78,12 @@ class EncryptionTest extends TestCase ]) ->save(); - $this->assertNotSame( + static::assertNotSame( '123-456-789', $user->getRawOriginal('social_security_number') ); - $this->assertNotEmpty( + static::assertNotEmpty( $user->getAttribute('custom_index') ); @@ -100,9 +100,29 @@ class EncryptionTest extends TestCase { $user = $this->user('123-456-789'); - $this->assertSame( + static::assertSame( '123-456-789', $user->getAttribute('social_security_number') ); } + + public function testAttributesCanBeMadeNull(): void + { + $user = $this->user('123-456-789'); + + static::assertSame( + '123-456-789', + $user->social_security_number + ); + + $user->social_security_number = null; + + static::assertNull( + $user->social_security_number + ); + + static::assertNull( + $user->social_security_number_index + ); + } } diff --git a/tests/Unit/QueryTest.php b/tests/Unit/QueryTest.php index 0fa2cad..de536ec 100644 --- a/tests/Unit/QueryTest.php +++ b/tests/Unit/QueryTest.php @@ -32,8 +32,8 @@ class QueryTest extends TestCase ->get() ->modelKeys(); - $this->assertContains($userOne->id, $keys); - $this->assertNotContains($userTwo->id, $keys); + static::assertContains($userOne->id, $keys); + static::assertNotContains($userTwo->id, $keys); // Assert success using provided index. /** @var \Illuminate\Database\Eloquent\Collection $keys */ @@ -44,8 +44,8 @@ class QueryTest extends TestCase ->get() ->modelKeys(); - $this->assertContains($userOne->id, $keys); - $this->assertNotContains($userTwo->id, $keys); + static::assertContains($userOne->id, $keys); + static::assertNotContains($userTwo->id, $keys); // Assert undefined index exception. $this->expectException(Exception::class); @@ -57,6 +57,22 @@ class QueryTest extends TestCase ->get(); } + public function testCanQueryNullableAttributes(): void + { + $userOne = $this->user('123-456-789'); + $userTwo = $this->user(null); + + // Assert success. + /** @var \Illuminate\Database\Eloquent\Collection $keys */ + $keys = User::query() + ->whereEncrypted('social_security_number', '=', '123-456-789') + ->get() + ->modelKeys(); + + static::assertContains($userOne->id, $keys); + static::assertNotContains($userTwo->id, $keys); + } + public function testCanQueryEncryptedAttributeWithOrWhereClause(): void { $userOne = $this->user('123-456-789'); @@ -71,9 +87,9 @@ class QueryTest extends TestCase ->get() ->modelKeys(); - $this->assertContains($userOne->id, $keys); - $this->assertNotContains($userTwo->id, $keys); - $this->assertContains($userThree->id, $keys); + static::assertContains($userOne->id, $keys); + static::assertNotContains($userTwo->id, $keys); + static::assertContains($userThree->id, $keys); // Assert success using provided index. /** @var \Illuminate\Database\Eloquent\Collection $keys */ @@ -85,9 +101,9 @@ class QueryTest extends TestCase ->get() ->modelKeys(); - $this->assertContains($userOne->id, $keys); - $this->assertNotContains($userTwo->id, $keys); - $this->assertContains($userThree->id, $keys); + static::assertContains($userOne->id, $keys); + static::assertNotContains($userTwo->id, $keys); + static::assertContains($userThree->id, $keys); // Assert undefined index exception. $this->expectException(Exception::class); @@ -118,9 +134,9 @@ class QueryTest extends TestCase ->get() ->modelKeys(); - $this->assertContains($userOne->id, $keys); - $this->assertContains($userTwo->id, $keys); - $this->assertNotContains($userThree->id, $keys); + static::assertContains($userOne->id, $keys); + static::assertContains($userTwo->id, $keys); + static::assertNotContains($userThree->id, $keys); // Assert success using provided index. /** @var \Illuminate\Database\Eloquent\Collection $keys */ @@ -136,9 +152,9 @@ class QueryTest extends TestCase ->get() ->modelKeys(); - $this->assertContains($userOne->id, $keys); - $this->assertContains($userTwo->id, $keys); - $this->assertNotContains($userThree->id, $keys); + static::assertContains($userOne->id, $keys); + static::assertContains($userTwo->id, $keys); + static::assertNotContains($userThree->id, $keys); $keys = User::query() ->whereInEncrypted( @@ -149,7 +165,7 @@ class QueryTest extends TestCase ->get() ->modelKeys(); - $this->assertEmpty($keys); + static::assertEmpty($keys); } public function testCanQueryEncryptedAttributeWithOrWhereInClause(): void @@ -172,9 +188,9 @@ class QueryTest extends TestCase ->get() ->modelKeys(); - $this->assertContains($userOne->id, $keys); - $this->assertContains($userTwo->id, $keys); - $this->assertNotContains($userThree->id, $keys); + static::assertContains($userOne->id, $keys); + static::assertContains($userTwo->id, $keys); + static::assertNotContains($userThree->id, $keys); // Assert success using provided index. /** @var \Illuminate\Database\Eloquent\Collection $keys */ @@ -192,9 +208,9 @@ class QueryTest extends TestCase ->get() ->modelKeys(); - $this->assertContains($userOne->id, $keys); - $this->assertContains($userTwo->id, $keys); - $this->assertNotContains($userThree->id, $keys); + static::assertContains($userOne->id, $keys); + static::assertContains($userTwo->id, $keys); + static::assertNotContains($userThree->id, $keys); $keys = User::query() ->whereInEncrypted( @@ -210,6 +226,6 @@ class QueryTest extends TestCase ->get() ->modelKeys(); - $this->assertEmpty($keys); + static::assertEmpty($keys); } }