Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
100.00% |
1 / 1 |
|
100.00% |
16 / 16 |
CRAP | |
100.00% |
40 / 40 |
HasEncryptedAttributes | |
100.00% |
1 / 1 |
|
100.00% |
16 / 16 |
31 | |
100.00% |
40 / 40 |
getLastEncryptionException | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
setLastEncryptionException | |
100.00% |
1 / 1 |
3 | |
100.00% |
3 / 3 |
|||
getEncryptionPrefix | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
shouldEncrypt | |
100.00% |
1 / 1 |
4 | |
100.00% |
2 / 2 |
|||
isEncrypted | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
encryptedAttribute | |
100.00% |
1 / 1 |
2 | |
100.00% |
1 / 1 |
|||
decryptedAttribute | |
100.00% |
1 / 1 |
1 | |
100.00% |
6 / 6 |
|||
doEncryptAttribute | |
100.00% |
1 / 1 |
4 | |
100.00% |
5 / 5 |
|||
doDecryptAttribute | |
100.00% |
1 / 1 |
4 | |
100.00% |
5 / 5 |
|||
doDecryptAttributes | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
castAttribute | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
getDirty | |
100.00% |
1 / 1 |
3 | |
100.00% |
5 / 5 |
|||
setAttribute | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
getAttributeFromArray | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
getArrayableAttributes | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
getAttributes | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
<?php | |
/** | |
* src/Traits/HasEncryptedAttributes.php. | |
* | |
* @author Austin Heap <me@austinheap.com> | |
* @version v0.2.0 | |
*/ | |
declare(strict_types=1); | |
namespace AustinHeap\Database\Encryption\Traits; | |
use Illuminate\Support\Facades\Log; | |
use Illuminate\Support\Facades\Crypt; | |
use Illuminate\Contracts\Encryption\DecryptException; | |
use Illuminate\Contracts\Encryption\EncryptException; | |
use AustinHeap\Database\Encryption\EncryptionFacade as DatabaseEncryption; | |
/** | |
* HasEncryptedAttributes. | |
* | |
* Automatically encrypt and decrypt Laravel 5.5+ Eloquent values | |
* | |
* ### Example | |
* | |
* <code> | |
* use AustinHeap\Database\Encryption\Traits\HasEncryptedAttributes; | |
* | |
* class User extends Eloquent { | |
* | |
* use HasEncryptedAttributes; | |
* | |
* protected $encrypted = [ | |
* 'address_line_1', 'first_name', 'last_name', 'postcode' | |
* ]; | |
* } | |
* </code> | |
* | |
* ### Summary of Methods in Illuminate\Database\Eloquent\Model | |
* | |
* This surveys the major methods in the Laravel Model class as of | |
* Laravel v5.5 and checks to see how those models set attributes | |
* and hence how they are affected by this trait. | |
* | |
* - __construct -- calls fill() | |
* - fill() -- calls setAttribute() which has been overridden. | |
* - hydrate() -- TBD | |
* - create() -- calls constructor and hence fill() | |
* - firstOrCreate -- calls constructor | |
* - firstOrNew -- calls constructor | |
* - updateOrCreate -- calls fill() | |
* - update() -- calls fill() | |
* - toArray() -- calls attributesToArray() | |
* - jsonSerialize() -- calls toArray() | |
* - toJson() -- calls toArray() | |
* - attributesToArray() -- has been over-ridden here. | |
* - getAttribute -- calls getAttributeValue() | |
* - getAttributeValue -- calls getAttributeFromArray() | |
* - getAttributeFromArray -- calls getArrayableAttributes | |
* - getArrayableAttributes -- has been over-ridden here. | |
* - setAttribute -- has been over-ridden here. | |
* - getAttributes -- has been over-ridden here. | |
* | |
* @see Illuminate\Support\Facades\Crypt | |
* @see Illuminate\Contracts\Encryption\Encrypter | |
* @see Illuminate\Encryption\Encrypter | |
* @link http://laravel.com/docs/5.5/eloquent | |
* @link https://github.com/austinheap/laravel-database-encryption | |
* @link https://packagist.org/packages/austinheap/laravel-database-encryption | |
* @link https://austinheap.github.io/laravel-database-encryption/classes/AustinHeap.Database.Encryption.EncryptionServiceProvider.html | |
*/ | |
trait HasEncryptedAttributes | |
{ | |
/** | |
* Private copy of last Encryption exception to occur. | |
* | |
* @var null|EncryptException|DecryptException | |
*/ | |
private $lastEncryptionException = null; | |
/** | |
* Get the last encryption-related exception to occur, if any. | |
* | |
* @return null|EncryptException|DecryptException | |
*/ | |
public function getLastEncryptionException() | |
{ | |
return $this->lastEncryptionException; | |
} | |
/** | |
* Set the last encryption-related exception to occur, if any. | |
* | |
* @param null|EncryptException|DecryptException $exception | |
* @param null|string $function | |
* | |
* @return self | |
*/ | |
protected function setLastEncryptionException($exception, ?string $function = null): self | |
{ | |
Log::debug('Ignored exception "'.get_class($exception).'" in function "'.(is_null($function) ? '(unknown)' : $function).'": '.$exception->getMessage()); | |
$this->lastEncryptionException = $exception; | |
return $this; | |
} | |
/** | |
* Get the configuration setting for the prefix used to determine if a string is encrypted. | |
* | |
* @return string | |
*/ | |
protected function getEncryptionPrefix(): string | |
{ | |
return DatabaseEncryption::getHeaderPrefix(); | |
} | |
/** | |
* Determine whether an attribute should be encrypted. | |
* | |
* @param string $key | |
* | |
* @return bool | |
*/ | |
protected function shouldEncrypt($key): bool | |
{ | |
$encrypt = DatabaseEncryption::isEnabled() && isset($this->encrypted) && is_array($this->encrypted) ? $this->encrypted : []; | |
return in_array($key, $encrypt); | |
} | |
/** | |
* Determine whether a string has already been encrypted. | |
* | |
* @param mixed $value | |
* | |
* @return bool | |
*/ | |
protected function isEncrypted($value): bool | |
{ | |
return strpos((string) $value, $this->getEncryptionPrefix()) === 0; | |
} | |
/** | |
* Return the encrypted value of an attribute's value. | |
* | |
* This has been exposed as a public method because it is of some | |
* use when searching. | |
* | |
* @param string $value | |
* | |
* @return null|string | |
*/ | |
public function encryptedAttribute($value): ?string | |
{ | |
return DatabaseEncryption::buildHeader($value).Crypt::encrypt($value); | |
} | |
/** | |
* Return the decrypted value of an attribute's encrypted value. | |
* | |
* This has been exposed as a public method because it is of some | |
* use when searching. | |
* | |
* @param string $value | |
* | |
* @return null|mixed | |
* @throws \Throwable | |
*/ | |
public function decryptedAttribute($value) | |
{ | |
$characters = DatabaseEncryption::getControlCharacters('header'); | |
throw_if(! array_key_exists('stop', $characters), DecryptException::class, 'Cannot decrypt model attribute not originally encrypted by this package!'); | |
$offset = strpos($value, $characters['stop']['string']); | |
throw_if($offset === false, DecryptException::class, 'Cannot decrypt model attribute with no package header!'); | |
$value = substr($value, $offset); | |
return Crypt::decrypt($value); | |
} | |
/** | |
* Encrypt a stored attribute. | |
* | |
* @param string $key | |
* | |
* @return self | |
*/ | |
protected function doEncryptAttribute($key): self | |
{ | |
if ($this->shouldEncrypt($key) && ! $this->isEncrypted($this->attributes[$key])) { | |
try { | |
$this->attributes[$key] = $this->encryptedAttribute($this->attributes[$key]); | |
} catch (EncryptException $exception) { | |
$this->setLastEncryptionException($exception, __FUNCTION__); | |
} | |
} | |
return $this; | |
} | |
/** | |
* Decrypt an attribute if required. | |
* | |
* @param string $key | |
* @param mixed $value | |
* | |
* @return mixed | |
*/ | |
protected function doDecryptAttribute($key, $value) | |
{ | |
if ($this->shouldEncrypt($key) && $this->isEncrypted($value)) { | |
try { | |
return $this->decryptedAttribute($value); | |
} catch (DecryptException $exception) { | |
$this->setLastEncryptionException($exception, __FUNCTION__); | |
} | |
} | |
return $value; | |
} | |
/** | |
* Decrypt each attribute in the array as required. | |
* | |
* @param array $attributes | |
* | |
* @return array | |
*/ | |
public function doDecryptAttributes($attributes) | |
{ | |
foreach ($attributes as $key => $value) { | |
$attributes[$key] = $this->doDecryptAttribute($key, $value); | |
} | |
return $attributes; | |
} | |
// | |
// Methods below here override methods within the base Laravel/Illuminate/Eloquent | |
// model class and may need adjusting for later releases of Laravel. | |
// | |
/** | |
* Decrypt encrypted data before it is processed by cast attribute. | |
* | |
* @param $key | |
* @param $value | |
* | |
* @return mixed | |
*/ | |
protected function castAttribute($key, $value) | |
{ | |
return parent::castAttribute($key, $this->doDecryptAttribute($key, $value)); | |
} | |
/** | |
* Get the attributes that have been changed since last sync. | |
* | |
* @return array | |
*/ | |
public function getDirty() | |
{ | |
$dirty = []; | |
foreach ($this->attributes as $key => $value) { | |
if (! $this->originalIsEquivalent($key, $value)) { | |
$dirty[$key] = $value; | |
} | |
} | |
return $dirty; | |
} | |
/** | |
* Set a given attribute on the model. | |
* | |
* @param string $key | |
* @param mixed $value | |
* | |
* @return void | |
*/ | |
public function setAttribute($key, $value) | |
{ | |
parent::setAttribute($key, $value); | |
$this->doEncryptAttribute($key); | |
} | |
/** | |
* Get an attribute from the $attributes array. | |
* | |
* @param string $key | |
* | |
* @return mixed | |
*/ | |
protected function getAttributeFromArray($key) | |
{ | |
return $this->doDecryptAttribute($key, parent::getAttributeFromArray($key)); | |
} | |
/** | |
* Get an attribute array of all arrayable attributes. | |
* | |
* @return array | |
*/ | |
protected function getArrayableAttributes() | |
{ | |
return $this->doDecryptAttributes(parent::getArrayableAttributes()); | |
} | |
/** | |
* Get all of the current attributes on the model. | |
* | |
* @return array | |
*/ | |
public function getAttributes() | |
{ | |
return $this->doDecryptAttributes(parent::getAttributes()); | |
} | |
} |