Issues (25)

src/Traits/HasEncryptedAttributes.php (7 issues)

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
The method getMessage() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

100
        Log::debug('Ignored exception "'.get_class($exception).'" in function "'.(is_null($function) ? '(unknown)' : $function).'": '.$exception->/** @scrutinizer ignore-call */ getMessage());

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.

Loading history...
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 ignore-call  annotation

114
        return DatabaseEncryption::/** @scrutinizer ignore-call */ getHeaderPrefix();
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 ignore-call  annotation

126
        $encrypt = DatabaseEncryption::/** @scrutinizer ignore-call */ isEnabled() && isset($this->encrypted) && is_array($this->encrypted) ? $this->encrypted : [];
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 ignore-call  annotation

167
        return DatabaseEncryption::/** @scrutinizer ignore-call */ buildHeader($value).Crypt::encrypt($value);
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 ignore-call  annotation

183
        /** @scrutinizer ignore-call */ 
184
        $characters = DatabaseEncryption::getControlCharacters('header');
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
Bug Best Practice introduced by
The property attributes does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
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 ignore-call  annotation

281
            if (! $this->/** @scrutinizer ignore-call */ originalIsEquivalent($key, $value)) {
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