austinheap /
laravel-database-encryption
| 1 | <?php |
||||||
| 2 | /** |
||||||
| 3 | * src/Traits/HasEncryptedAttributes.php. |
||||||
| 4 | * |
||||||
| 5 | * @author Austin Heap <[email protected]> |
||||||
| 6 | * @version v0.2.1 |
||||||
| 7 | */ |
||||||
| 8 | declare(strict_types=1); |
||||||
| 9 | |||||||
| 10 | namespace AustinHeap\Database\Encryption\Traits; |
||||||
| 11 | |||||||
| 12 | use AustinHeap\Database\Encryption\EncryptionFacade as DatabaseEncryption; |
||||||
| 13 | use Illuminate\Contracts\Encryption\DecryptException; |
||||||
| 14 | use Illuminate\Contracts\Encryption\EncryptException; |
||||||
| 15 | use Illuminate\Support\Facades\Crypt; |
||||||
| 16 | use Illuminate\Support\Facades\Log; |
||||||
| 17 | |||||||
| 18 | /** |
||||||
| 19 | * HasEncryptedAttributes. |
||||||
| 20 | * |
||||||
| 21 | * Automatically encrypt and decrypt Laravel 5.5+ Eloquent values |
||||||
| 22 | * |
||||||
| 23 | * ### Example |
||||||
| 24 | * |
||||||
| 25 | * <code> |
||||||
| 26 | * use AustinHeap\Database\Encryption\Traits\HasEncryptedAttributes; |
||||||
| 27 | * |
||||||
| 28 | * class User extends Eloquent { |
||||||
| 29 | * |
||||||
| 30 | * use HasEncryptedAttributes; |
||||||
| 31 | * |
||||||
| 32 | * protected $encrypted = [ |
||||||
| 33 | * 'address_line_1', 'first_name', 'last_name', 'postcode' |
||||||
| 34 | * ]; |
||||||
| 35 | * } |
||||||
| 36 | * </code> |
||||||
| 37 | * |
||||||
| 38 | * ### Summary of Methods in Illuminate\Database\Eloquent\Model |
||||||
| 39 | * |
||||||
| 40 | * This surveys the major methods in the Laravel Model class as of |
||||||
| 41 | * Laravel v5.5 and checks to see how those models set attributes |
||||||
| 42 | * and hence how they are affected by this trait. |
||||||
| 43 | * |
||||||
| 44 | * - __construct -- calls fill() |
||||||
| 45 | * - fill() -- calls setAttribute() which has been overridden. |
||||||
| 46 | * - hydrate() -- TBD |
||||||
| 47 | * - create() -- calls constructor and hence fill() |
||||||
| 48 | * - firstOrCreate -- calls constructor |
||||||
| 49 | * - firstOrNew -- calls constructor |
||||||
| 50 | * - updateOrCreate -- calls fill() |
||||||
| 51 | * - update() -- calls fill() |
||||||
| 52 | * - toArray() -- calls attributesToArray() |
||||||
| 53 | * - jsonSerialize() -- calls toArray() |
||||||
| 54 | * - toJson() -- calls toArray() |
||||||
| 55 | * - attributesToArray() -- has been over-ridden here. |
||||||
| 56 | * - getAttribute -- calls getAttributeValue() |
||||||
| 57 | * - getAttributeValue -- calls getAttributeFromArray() |
||||||
| 58 | * - getAttributeFromArray -- calls getArrayableAttributes |
||||||
| 59 | * - getArrayableAttributes -- has been over-ridden here. |
||||||
| 60 | * - setAttribute -- has been over-ridden here. |
||||||
| 61 | * - getAttributes -- has been over-ridden here. |
||||||
| 62 | * |
||||||
| 63 | * @see \Illuminate\Support\Facades\Crypt |
||||||
| 64 | * @see \Illuminate\Contracts\Encryption\Encrypter |
||||||
| 65 | * @see \Illuminate\Encryption\Encrypter |
||||||
| 66 | * @link http://laravel.com/docs/5.5/eloquent |
||||||
| 67 | * @link https://github.com/austinheap/laravel-database-encryption |
||||||
| 68 | * @link https://packagist.org/packages/austinheap/laravel-database-encryption |
||||||
| 69 | * @link https://austinheap.github.io/laravel-database-encryption/classes/AustinHeap.Database.Encryption.EncryptionServiceProvider.html |
||||||
| 70 | */ |
||||||
| 71 | trait HasEncryptedAttributes |
||||||
| 72 | { |
||||||
| 73 | /** |
||||||
| 74 | * Private copy of last Encryption exception to occur. |
||||||
| 75 | * |
||||||
| 76 | * @var null|EncryptException|DecryptException |
||||||
| 77 | */ |
||||||
| 78 | private $lastEncryptionException = null; |
||||||
| 79 | |||||||
| 80 | /** |
||||||
| 81 | * Get the last encryption-related exception to occur, if any. |
||||||
| 82 | * |
||||||
| 83 | * @return null|EncryptException|DecryptException |
||||||
| 84 | */ |
||||||
| 85 | 2 | public function getLastEncryptionException() |
|||||
| 86 | { |
||||||
| 87 | 2 | return $this->lastEncryptionException; |
|||||
| 88 | } |
||||||
| 89 | |||||||
| 90 | /** |
||||||
| 91 | * Set the last encryption-related exception to occur, if any. |
||||||
| 92 | * |
||||||
| 93 | * @param null|EncryptException|DecryptException $exception |
||||||
| 94 | * @param null|string $function |
||||||
| 95 | * |
||||||
| 96 | * @return self |
||||||
| 97 | */ |
||||||
| 98 | 2 | protected function setLastEncryptionException($exception, ?string $function = null): self |
|||||
| 99 | { |
||||||
| 100 | 2 | Log::debug('Ignored exception "'.get_class($exception).'" in function "'.(is_null($function) ? '(unknown)' : $function).'": '.$exception->getMessage()); |
|||||
|
0 ignored issues
–
show
|
|||||||
| 101 | |||||||
| 102 | 2 | $this->lastEncryptionException = $exception; |
|||||
| 103 | |||||||
| 104 | 2 | return $this; |
|||||
| 105 | } |
||||||
| 106 | |||||||
| 107 | /** |
||||||
| 108 | * Get the configuration setting for the prefix used to determine if a string is encrypted. |
||||||
| 109 | * |
||||||
| 110 | * @return string |
||||||
| 111 | */ |
||||||
| 112 | 21 | protected function getEncryptionPrefix(): string |
|||||
| 113 | { |
||||||
| 114 | 21 | return DatabaseEncryption::getHeaderPrefix(); |
|||||
|
0 ignored issues
–
show
The method
getHeaderPrefix() does not exist on AustinHeap\Database\Encryption\EncryptionFacade. Since you implemented __callStatic, consider adding a @method annotation.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||||
| 115 | } |
||||||
| 116 | |||||||
| 117 | /** |
||||||
| 118 | * Determine whether an attribute should be encrypted. |
||||||
| 119 | * |
||||||
| 120 | * @param string $key |
||||||
| 121 | * |
||||||
| 122 | * @return bool |
||||||
| 123 | */ |
||||||
| 124 | 21 | protected function shouldEncrypt($key): bool |
|||||
| 125 | { |
||||||
| 126 | 21 | $encrypt = DatabaseEncryption::isEnabled() && isset($this->encrypted) && is_array($this->encrypted) ? $this->encrypted : []; |
|||||
|
0 ignored issues
–
show
The method
isEnabled() does not exist on AustinHeap\Database\Encryption\EncryptionFacade. Since you implemented __callStatic, consider adding a @method annotation.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||||
| 127 | |||||||
| 128 | 21 | return in_array($key, $encrypt, true); |
|||||
| 129 | } |
||||||
| 130 | |||||||
| 131 | /** |
||||||
| 132 | * Determine whether a model is ready for encryption. |
||||||
| 133 | * |
||||||
| 134 | * @return bool |
||||||
| 135 | */ |
||||||
| 136 | 21 | protected function isEncryptable(): bool |
|||||
| 137 | { |
||||||
| 138 | 21 | $exists = property_exists($this, 'exists'); |
|||||
| 139 | |||||||
| 140 | 21 | return $exists === false || ($exists === true && $this->exists === true); |
|||||
| 141 | } |
||||||
| 142 | |||||||
| 143 | /** |
||||||
| 144 | * Determine whether a string has already been encrypted. |
||||||
| 145 | * |
||||||
| 146 | * @param mixed $value |
||||||
| 147 | * |
||||||
| 148 | * @return bool |
||||||
| 149 | */ |
||||||
| 150 | 21 | protected function isEncrypted($value): bool |
|||||
| 151 | { |
||||||
| 152 | 21 | return strpos((string) $value, $this->getEncryptionPrefix()) === 0; |
|||||
| 153 | } |
||||||
| 154 | |||||||
| 155 | /** |
||||||
| 156 | * Return the encrypted value of an attribute's value. |
||||||
| 157 | * |
||||||
| 158 | * This has been exposed as a public method because it is of some |
||||||
| 159 | * use when searching. |
||||||
| 160 | * |
||||||
| 161 | * @param string $value |
||||||
| 162 | * |
||||||
| 163 | * @return null|string |
||||||
| 164 | */ |
||||||
| 165 | 18 | public function encryptedAttribute($value): ?string |
|||||
| 166 | { |
||||||
| 167 | 18 | return DatabaseEncryption::buildHeader($value).Crypt::encrypt($value); |
|||||
|
0 ignored issues
–
show
The method
buildHeader() does not exist on AustinHeap\Database\Encryption\EncryptionFacade. Since you implemented __callStatic, consider adding a @method annotation.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||||
| 168 | } |
||||||
| 169 | |||||||
| 170 | /** |
||||||
| 171 | * Return the decrypted value of an attribute's encrypted value. |
||||||
| 172 | * |
||||||
| 173 | * This has been exposed as a public method because it is of some |
||||||
| 174 | * use when searching. |
||||||
| 175 | * |
||||||
| 176 | * @param string $value |
||||||
| 177 | * |
||||||
| 178 | * @return null|mixed |
||||||
| 179 | * @throws \Throwable |
||||||
| 180 | */ |
||||||
| 181 | 11 | public function decryptedAttribute($value) |
|||||
| 182 | { |
||||||
| 183 | 11 | $characters = DatabaseEncryption::getControlCharacters('header'); |
|||||
|
0 ignored issues
–
show
The method
getControlCharacters() does not exist on AustinHeap\Database\Encryption\EncryptionFacade. Since you implemented __callStatic, consider adding a @method annotation.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||||
| 184 | |||||||
| 185 | 11 | throw_if(! array_key_exists('stop', $characters), DecryptException::class, 'Cannot decrypt model attribute not originally encrypted by this package!'); |
|||||
| 186 | |||||||
| 187 | 11 | $offset = strpos($value, $characters['stop']['string']); |
|||||
| 188 | |||||||
| 189 | 11 | throw_if($offset === false, DecryptException::class, 'Cannot decrypt model attribute with no package header!'); |
|||||
| 190 | |||||||
| 191 | 11 | $value = substr($value, $offset); |
|||||
| 192 | |||||||
| 193 | 11 | return Crypt::decrypt($value); |
|||||
| 194 | } |
||||||
| 195 | |||||||
| 196 | /** |
||||||
| 197 | * Encrypt a stored attribute. |
||||||
| 198 | * |
||||||
| 199 | * @param string $key |
||||||
| 200 | * |
||||||
| 201 | * @return self |
||||||
| 202 | */ |
||||||
| 203 | 22 | protected function doEncryptAttribute($key): self |
|||||
| 204 | { |
||||||
| 205 | 22 | if ($this->shouldEncrypt($key) && ! $this->isEncrypted($this->attributes[$key])) { |
|||||
| 206 | try { |
||||||
| 207 | 22 | $this->attributes[$key] = $this->encryptedAttribute($this->attributes[$key]); |
|||||
|
0 ignored issues
–
show
|
|||||||
| 208 | 1 | } catch (EncryptException $exception) { |
|||||
| 209 | 1 | $this->setLastEncryptionException($exception, __FUNCTION__); |
|||||
| 210 | } |
||||||
| 211 | } |
||||||
| 212 | |||||||
| 213 | 22 | return $this; |
|||||
| 214 | } |
||||||
| 215 | |||||||
| 216 | /** |
||||||
| 217 | * Decrypt an attribute if required. |
||||||
| 218 | * |
||||||
| 219 | * @param string $key |
||||||
| 220 | * @param mixed $value |
||||||
| 221 | * |
||||||
| 222 | * @return mixed |
||||||
| 223 | */ |
||||||
| 224 | 17 | protected function doDecryptAttribute($key, $value) |
|||||
| 225 | { |
||||||
| 226 | 17 | if ($this->shouldEncrypt($key) && $this->isEncrypted($value)) { |
|||||
| 227 | try { |
||||||
| 228 | 14 | return $this->decryptedAttribute($value); |
|||||
| 229 | 1 | } catch (DecryptException $exception) { |
|||||
| 230 | 1 | $this->setLastEncryptionException($exception, __FUNCTION__); |
|||||
| 231 | } |
||||||
| 232 | } |
||||||
| 233 | |||||||
| 234 | 13 | return $value; |
|||||
| 235 | } |
||||||
| 236 | |||||||
| 237 | /** |
||||||
| 238 | * Decrypt each attribute in the array as required. |
||||||
| 239 | * |
||||||
| 240 | * @param array $attributes |
||||||
| 241 | * |
||||||
| 242 | * @return array |
||||||
| 243 | */ |
||||||
| 244 | 3 | public function doDecryptAttributes($attributes) |
|||||
| 245 | { |
||||||
| 246 | 3 | foreach ($attributes as $key => $value) { |
|||||
| 247 | 3 | $attributes[$key] = $this->doDecryptAttribute($key, $value); |
|||||
| 248 | } |
||||||
| 249 | |||||||
| 250 | 3 | return $attributes; |
|||||
| 251 | } |
||||||
| 252 | |||||||
| 253 | // |
||||||
| 254 | // Methods below here override methods within the base Laravel/Illuminate/Eloquent |
||||||
| 255 | // model class and may need adjusting for later releases of Laravel. |
||||||
| 256 | // |
||||||
| 257 | |||||||
| 258 | /** |
||||||
| 259 | * Decrypt encrypted data before it is processed by cast attribute. |
||||||
| 260 | * |
||||||
| 261 | * @param $key |
||||||
| 262 | * @param $value |
||||||
| 263 | * |
||||||
| 264 | * @return mixed |
||||||
| 265 | */ |
||||||
| 266 | 7 | protected function castAttribute($key, $value) |
|||||
| 267 | { |
||||||
| 268 | 7 | return parent::castAttribute($key, $this->doDecryptAttribute($key, $value)); |
|||||
| 269 | } |
||||||
| 270 | |||||||
| 271 | /** |
||||||
| 272 | * Get the attributes that have been changed since last sync. |
||||||
| 273 | * |
||||||
| 274 | * @return array |
||||||
| 275 | */ |
||||||
| 276 | 17 | public function getDirty() |
|||||
| 277 | { |
||||||
| 278 | 17 | $dirty = []; |
|||||
| 279 | |||||||
| 280 | 17 | foreach ($this->attributes as $key => $value) { |
|||||
| 281 | 17 | if (! $this->originalIsEquivalent($key, $value)) { |
|||||
|
0 ignored issues
–
show
It seems like
originalIsEquivalent() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||||
| 282 | 17 | $dirty[$key] = $value; |
|||||
| 283 | } |
||||||
| 284 | } |
||||||
| 285 | |||||||
| 286 | 17 | return $dirty; |
|||||
| 287 | } |
||||||
| 288 | |||||||
| 289 | /** |
||||||
| 290 | * Set a given attribute on the model. |
||||||
| 291 | * |
||||||
| 292 | * @param string $key |
||||||
| 293 | * @param mixed $value |
||||||
| 294 | * |
||||||
| 295 | * @return void |
||||||
| 296 | */ |
||||||
| 297 | 21 | public function setAttribute($key, $value) |
|||||
| 298 | { |
||||||
| 299 | 21 | parent::setAttribute($key, $value); |
|||||
| 300 | |||||||
| 301 | 21 | $this->doEncryptAttribute($key); |
|||||
| 302 | 21 | } |
|||||
| 303 | |||||||
| 304 | /** |
||||||
| 305 | * Get an attribute from the $attributes array. |
||||||
| 306 | * |
||||||
| 307 | * @param string $key |
||||||
| 308 | * |
||||||
| 309 | * @return mixed |
||||||
| 310 | */ |
||||||
| 311 | 13 | protected function getAttributeFromArray($key) |
|||||
| 312 | { |
||||||
| 313 | 13 | return $this->doDecryptAttribute($key, parent::getAttributeFromArray($key)); |
|||||
| 314 | } |
||||||
| 315 | |||||||
| 316 | /** |
||||||
| 317 | * Get an attribute array of all arrayable attributes. |
||||||
| 318 | * |
||||||
| 319 | * @return array |
||||||
| 320 | */ |
||||||
| 321 | 1 | protected function getArrayableAttributes() |
|||||
| 322 | { |
||||||
| 323 | 1 | return $this->doDecryptAttributes(parent::getArrayableAttributes()); |
|||||
| 324 | } |
||||||
| 325 | |||||||
| 326 | /** |
||||||
| 327 | * Get all of the current attributes on the model. |
||||||
| 328 | * |
||||||
| 329 | * @return array |
||||||
| 330 | */ |
||||||
| 331 | 19 | public function getAttributes() |
|||||
| 332 | { |
||||||
| 333 | 19 | return $this->isEncryptable() ? $this->doDecryptAttributes(parent::getAttributes()) : parent::getAttributes(); |
|||||
| 334 | } |
||||||
| 335 | } |
||||||
| 336 |
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.
This is most likely a typographical error or the method has been renamed.