Skip to content

Commit aab10a7

Browse files
committed
develop/v0.1.0: Updated the model trait with improvements and also aded content to the readme.md
1 parent 83062d3 commit aab10a7

File tree

3 files changed

+181
-75
lines changed

3 files changed

+181
-75
lines changed

README.md

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,87 @@
1-
# laravel-db-encryption
2-
A Laravel package that helps encrypt & decrypt certain defined table columns ensuring the data is secure in the database but can be accessed by the models.
1+
# Laravel DB Encryptor
2+
3+
A Laravel package for secure, transparent encryption and decryption of sensitive model attributes, storing them in a dedicated table while keeping your main tables clean and fast.
4+
5+
## Features
6+
- Seamless encryption/decryption of model attributes via a simple trait
7+
- Encrypted data is stored in a separate `encrypted_attributes` table
8+
- Only non-table attributes can be encrypted (enforced at runtime)
9+
- Automatic loading and saving of encrypted attributes using Eloquent events
10+
- No sensitive values are ever logged
11+
- Easy integration: just add the trait and define `$encryptedProperties` in your model
12+
- Compatible with Laravel 9+
13+
14+
## Requirements
15+
- PHP 8.1+
16+
- Laravel 9 or higher
17+
- OpenSSL PHP extension
18+
19+
## How It Works
20+
1. Add the `HasEncryptedAttributes` trait to your Eloquent model.
21+
2. Define a public array property `$encryptedProperties` listing the attributes you want encrypted (these must NOT exist as columns in the model's table).
22+
3. When you load a model, encrypted attributes are automatically decrypted and available as normal properties.
23+
4. When you save a model, encrypted attributes are removed from the main table and securely stored in the `encrypted_attributes` table.
24+
25+
**Example Model:**
26+
```php
27+
use Wazza\DbEncrypt\Traits\HasEncryptedAttributes;
28+
29+
class User extends Model
30+
{
31+
use HasEncryptedAttributes;
32+
33+
protected $fillable = ['name', 'email'];
34+
35+
// Only non-table attributes can be encrypted!
36+
public array $encryptedProperties = [
37+
'social_security_number',
38+
'private_note',
39+
];
40+
}
41+
```
42+
43+
## Usage
44+
- Use your model as normal:
45+
```php
46+
$user = User::find(1);
47+
$user->social_security_number = '123-45-6789';
48+
$user->private_note = 'Sensitive info';
49+
$user->save();
50+
51+
// When you retrieve the user again, encrypted attributes are automatically decrypted:
52+
$user = User::find(1);
53+
echo $user->social_security_number; // '123-45-6789'
54+
```
55+
- If you try to add an attribute to `$encryptedProperties` that already exists as a column, an exception will be thrown.
56+
57+
## Installation Steps
58+
1. Require the package in your Laravel project:
59+
```sh
60+
composer require wazza/laravel-db-encryption
61+
```
62+
2. Publish the config and migration files (if needed):
63+
```sh
64+
php artisan vendor:publish --provider="Wazza\DbEncrypt\DbEncryptServiceProvider"
65+
```
66+
3. Run the migration to create the `encrypted_attributes` table:
67+
```sh
68+
php artisan migrate
69+
```
70+
4. Add the trait and `$encryptedProperties` to your models as shown above.
71+
72+
## Monitoring & Logs
73+
- All encryption/decryption operations are logged (without sensitive values).
74+
- To monitor package logs:
75+
```sh
76+
tail -f storage/logs/laravel.log | grep db-encrypt
77+
```
78+
79+
## Testing
80+
- Run the test suite using Pest:
81+
```sh
82+
./vendor/bin/pest
83+
```
84+
- Ensure your models and encrypted attributes behave as expected.
85+
86+
---
87+
For more details, see the source code and comments. Contributions and issues welcome!

src/Http/Controllers/DbEncryptController.php

Lines changed: 57 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -181,58 +181,66 @@ public function encryptProperty(string $property)
181181
* Encrypt all of the model defined ($this->encryptedProperties) properties.
182182
* If $property is provided, only that property will be encrypted.
183183
*
184-
* @param mixed $property The property to encrypt. If null, all properties will be encrypted.
184+
* @param string|null $property The property to encrypt. If null, all properties will be encrypted.
185185
* @return void
186186
* @throws Exception
187187
*/
188188
public function encrypt(
189-
$property = null
189+
?string $property = null
190190
) {
191+
// check if the model is set
191192
if (!$this->isModelDefined()) {
192193
throw new Exception('Model is not set. Please set the model using the `setModel` method.');
193194
}
195+
196+
// if property is defined, make sure it is in the encrypted properties
194197
if ($property !== null && !in_array($property, $this->encryptedProperties, true)) {
195198
throw new Exception('Property `' . $property . '` is not defined in the encrypted properties.');
196199
}
197200
$this->logger->infoLow('Encrypting properties for model: ' . $this->model->getTable() . ', property: ' . ($property ?? 'all'));
201+
202+
// loop through the defined encrypted properties
198203
foreach ($this->encryptedProperties as $prop) {
204+
// check if the property is defined in the model's attributes
199205
if ($property === null || $prop === $property) {
200-
if (array_key_exists($prop, $this->model->getAttributes())) {
201-
// Use Eloquent attribute check
202-
$value = $this->model->{$prop};
203-
if ($value === null || $value === '') {
204-
// if the value was encrypted, hard delete it form the encrypted_attributes table
205-
EncryptedAttributes::where([
206-
'object_type' => $this->model->getTable(),
207-
'object_id' => $this->model->getKey(),
208-
'attribute' => $prop,
209-
])->delete();
210-
211-
// done
212-
continue;
213-
}
214-
215-
// Encrypt the value and store it in the encrypted_attributes table
216-
$encryptedValue = Encryptor::encrypt($value);
217-
EncryptedAttributes::updateOrCreate(
218-
[
219-
'object_type' => $this->model->getTable(),
220-
'object_id' => $this->model->getKey(),
221-
'attribute' => $prop,
222-
],
223-
[
224-
'hash_index' => Encryptor::hash($value),
225-
'encrypted_value' => $encryptedValue,
226-
]
227-
);
228-
229-
// Do NOT log the actual value
230-
$this->logger->infoLow('Encrypted property: ' . $prop . ' [value hidden for security]');
231-
} else {
206+
if (!array_key_exists($prop, $this->model->getAttributes())) {
232207
continue;
233208
}
209+
210+
// Use Eloquent attribute check
211+
$value = $this->model->{$prop};
212+
if ($value === null || $value === '') {
213+
// if the value was encrypted, hard delete it form the encrypted_attributes table
214+
EncryptedAttributes::where([
215+
'object_type' => $this->model->getTable(),
216+
'object_id' => $this->model->getKey(),
217+
'attribute' => $prop,
218+
])->delete();
219+
220+
// done
221+
continue;
222+
}
223+
224+
// Encrypt the value and store it in the encrypted_attributes table
225+
$encryptedValue = Encryptor::encrypt($value);
226+
EncryptedAttributes::updateOrCreate(
227+
[
228+
'object_type' => $this->model->getTable(),
229+
'object_id' => $this->model->getKey(),
230+
'attribute' => $prop,
231+
],
232+
[
233+
'hash_index' => Encryptor::hash($value),
234+
'encrypted_value' => $encryptedValue,
235+
]
236+
);
237+
238+
// Do NOT log the actual value
239+
$this->logger->infoLow('Encrypted property: ' . $prop . ' [value hidden for security]');
234240
}
235241
}
242+
243+
// done.
236244
$this->logger->infoLow('Encryption completed for model: ' . $this->model->getTable() . ', properties: ' . json_encode($this->encryptedProperties));
237245
}
238246

@@ -246,30 +254,45 @@ public function encrypt(
246254
*/
247255
public function decrypt($property = null)
248256
{
257+
// check if the model is set
249258
if (!$this->isModelDefined()) {
250259
throw new Exception('Model is not set. Please set the model using the `setModel` method.');
251260
}
261+
262+
// if property is defined, make sure it is in the encrypted properties
252263
if ($property !== null && !in_array($property, $this->encryptedProperties, true)) {
253264
throw new Exception('Property `' . $property . '` is not defined in the encrypted properties.');
254265
}
255266
$this->logger->infoLow('Decrypting properties for model: ' . $this->model->getTable() . ', property: ' . ($property ?? 'all'));
267+
268+
// loop through the defined encrypted properties
256269
foreach ($this->encryptedProperties as $prop) {
270+
// check if the property is defined in the model's attributes
257271
if ($property === null || $prop === $property) {
272+
// get the encrypted attribute from the database
258273
$encryptedAttribute = EncryptedAttributes::where([
259274
'object_type' => $this->model->getTable(),
260275
'object_id' => $this->model->getKey(),
261276
'attribute' => $prop,
262277
])->first();
278+
279+
// if located, decrypt the value and set it to the model's property
263280
if ($encryptedAttribute && $encryptedAttribute->encrypted_value) {
281+
// located, decrypt the value
264282
$decryptedValue = Encryptor::decrypt($encryptedAttribute->encrypted_value);
265283
$this->model->{$prop} = $decryptedValue;
266284
$this->logger->infoLow('Decrypted property: ' . $prop . ' - success.');
267285
} else {
286+
// not found, set the property to null
268287
$this->model->{$prop} = null;
269288
$this->logger->infoLow('Decrypted property: ' . $prop . ' - not found, set to null.');
270289
}
290+
291+
// ..next
271292
}
272293
}
294+
295+
// done.
273296
$this->logger->infoLow('Decryption completed for model: ' . $this->model->getTable() . ', properties: ' . json_encode($this->encryptedProperties));
274297
}
275298
}

src/Traits/HasEncryptedAttributes.php

Lines changed: 37 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* Note: You do NOT need to override the save() method or manually call encryptAttributes().
1111
* Encryption and decryption are handled automatically via model events by this trait.
1212
* Be sure to add the `encryptedProperties` array to your model to specify which attributes should be encrypted.
13+
* If `encryptedProperties` is not defined, no attributes will be encrypted.
1314
*/
1415
trait HasEncryptedAttributes
1516
{
@@ -20,30 +21,12 @@ trait HasEncryptedAttributes
2021
*/
2122
protected array $_encryptedAttributesBuffer = [];
2223

23-
/**
24-
* Get the encryption status of the model's attributes.
25-
*
26-
* @return bool
27-
*/
28-
public function isEncrypted(): bool
29-
{
30-
// Get the current model instance the trait is called from
31-
$model = $this;
32-
33-
if (!$model instanceof \Illuminate\Database\Eloquent\Model) {
34-
throw new \InvalidArgumentException('The isEncrypted method can only be called from an Eloquent model instance.');
35-
}
36-
37-
// Check if the model's attributes are encrypted
38-
$dnEncryptController = app(DbEncryptController::class);
39-
return $dnEncryptController->isEncrypted($model);
40-
}
41-
4224
/**
4325
* Boot the HasEncryptedAttributes trait for a model.
44-
* Automatically handles loading and saving encrypted attributes.
26+
*
27+
* @return void
4528
*/
46-
public static function bootHasEncryptedAttributes()
29+
public static function bootHasEncryptedAttributes(): void
4730
{
4831
static::retrieved(function ($model) {
4932
$model->loadEncryptedAttributes();
@@ -58,27 +41,37 @@ public static function bootHasEncryptedAttributes()
5841

5942
/**
6043
* Load and decrypt encrypted attributes from the encrypted_attributes table.
44+
*
45+
* @return void
6146
*/
6247
public function loadEncryptedAttributes(): void
6348
{
64-
if (!property_exists($this, 'encryptedProperties') || empty($this->encryptedProperties)) {
49+
if (empty($this->encryptedProperties ?? [])) {
6550
return;
6651
}
67-
$dnEncryptController = app(\Wazza\DbEncrypt\Http\Controllers\DbEncryptController::class);
68-
$dnEncryptController->setModel($this);
69-
$dnEncryptController->decrypt();
52+
53+
try {
54+
$dnEncryptController = app(DbEncryptController::class);
55+
$dnEncryptController->setModel($this);
56+
$dnEncryptController->decrypt();
57+
} catch (\Throwable $e) {
58+
// Optionally log or handle decryption errors
59+
}
7060
}
7161

7262
/**
7363
* Remove encrypted attributes from the model's attributes before saving.
74-
* Store them temporarily for later encryption.
64+
*
65+
* @return void
7566
*/
7667
public function extractEncryptedAttributesForSave(): void
7768
{
78-
if (!property_exists($this, 'encryptedProperties') || empty($this->encryptedProperties)) {
69+
if (empty($this->encryptedProperties ?? [])) {
7970
return;
8071
}
72+
8173
$this->_encryptedAttributesBuffer = [];
74+
8275
foreach ($this->encryptedProperties as $prop) {
8376
if (array_key_exists($prop, $this->attributes)) {
8477
$this->_encryptedAttributesBuffer[$prop] = $this->attributes[$prop];
@@ -88,23 +81,28 @@ public function extractEncryptedAttributesForSave(): void
8881
}
8982

9083
/**
91-
* Encrypt and save the encrypted attributes to the encrypted_attributes table after saving the model.
84+
* Encrypt and save the encrypted attributes after saving the model.
85+
*
86+
* @return void
9287
*/
9388
public function saveEncryptedAttributes(): void
9489
{
95-
if (
96-
!property_exists($this, 'encryptedProperties') ||
97-
empty($this->encryptedProperties) ||
98-
empty($this->_encryptedAttributesBuffer ?? [])
99-
) {
90+
if (empty($this->encryptedProperties ?? []) || empty($this->_encryptedAttributesBuffer)) {
10091
return;
10192
}
102-
$dnEncryptController = app(\Wazza\DbEncrypt\Http\Controllers\DbEncryptController::class);
103-
$dnEncryptController->setModel($this);
104-
foreach ($this->_encryptedAttributesBuffer as $prop => $value) {
105-
$this->{$prop} = $value; // restore for encryption
106-
$dnEncryptController->encryptProperty($prop);
93+
94+
try {
95+
$dnEncryptController = app(DbEncryptController::class);
96+
$dnEncryptController->setModel($this);
97+
98+
foreach ($this->_encryptedAttributesBuffer as $prop => $value) {
99+
$this->{$prop} = $value;
100+
$dnEncryptController->encryptProperty($prop);
101+
}
102+
} catch (\Throwable $e) {
103+
// Optionally log or handle encryption errors
107104
}
108-
unset($this->_encryptedAttributesBuffer);
105+
106+
$this->_encryptedAttributesBuffer = [];
109107
}
110108
}

0 commit comments

Comments
 (0)