Passed
Push — master ( 461cd7...06c7d6 )
by Thomas
02:35
created

EncryptedDBField::getDecryptedValue()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 6
rs 10
cc 2
nc 2
nop 0
1
<?php
2
3
namespace LeKoala\Encrypt;
4
5
use Exception;
6
use SilverStripe\ORM\DataObject;
7
use SilverStripe\Forms\TextField;
8
use ParagonIE\CipherSweet\BlindIndex;
9
use SilverStripe\ORM\Queries\SQLSelect;
10
use ParagonIE\CipherSweet\EncryptedField;
11
use SilverStripe\ORM\FieldType\DBComposite;
12
use ParagonIE\CipherSweet\Exception\InvalidCiphertextException;
13
14
/**
15
 * Value will be set on parent record through built in getField
16
 * mechanisms for composite fields
17
 */
18
class EncryptedDBField extends DBComposite
19
{
20
    const LARGE_INDEX_SIZE = 32;
21
    const SMALL_INDEX_SIZE = 16;
22
    const VALUE_SUFFIX = "Value";
23
    const INDEX_SUFFIX = "BlindIndex";
24
25
    /**
26
     * @config
27
     * @var int
28
     */
29
    private static $output_size = 15;
0 ignored issues
show
introduced by
The private property $output_size is not used, and could be removed.
Loading history...
30
31
    /**
32
     * @config
33
     * @var int
34
     */
35
    private static $domain_size = 127;
0 ignored issues
show
introduced by
The private property $domain_size is not used, and could be removed.
Loading history...
36
37
    /**
38
     * @param array
39
     */
40
    private static $composite_db = array(
0 ignored issues
show
introduced by
The private property $composite_db is not used, and could be removed.
Loading history...
41
        "Value" => "Varchar(191)",
42
        "BlindIndex" => 'Varchar(32)',
43
    );
44
45
    /**
46
     * @var Exception
47
     */
48
    protected $encryptionException;
49
50
    /**
51
     * @return Exception
52
     */
53
    public function getEncryptionException()
54
    {
55
        return $this->encryptionException;
56
    }
57
58
    /**
59
     * Output size is the number of bits (not bytes) of a blind index.
60
     * Eg: 4 for a 4 digits year
61
     * Note: the larger the output size, the smaller the index should be
62
     * @return int
63
     */
64
    public function getOutputSize()
65
    {
66
        if (array_key_exists('output_size', $this->options)) {
67
            $outputSize = $this->options['output_size'];
68
        } else {
69
            $outputSize = static::config()->get('output_size');
70
        }
71
        return $outputSize;
72
    }
73
74
    /**
75
     * Input domain is the set of all possible distinct inputs.
76
     * Eg : 4 digits have 10,000 possible values (10^4). The log (base 2) of 10,000 is 13.2877; you would want to always round up (so 14).
77
     * @return int
78
     */
79
    public function getDomainSize()
80
    {
81
        if (array_key_exists('domain_size', $this->options)) {
82
            $domainSize = $this->options['domain_size'];
83
        } else {
84
            $domainSize = static::config()->get('domain_size');
85
        }
86
        return $domainSize;
87
    }
88
89
    /**
90
     * @param int $default
91
     * @return int
92
     */
93
    public function getIndexSize($default = null)
94
    {
95
        if (array_key_exists('index_size', $this->options)) {
96
            return $this->options['index_size'];
97
        }
98
        if ($default) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $default of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
99
            return $default;
100
        }
101
        return self::LARGE_INDEX_SIZE;
102
    }
103
104
    /**
105
     * @return string
106
     */
107
    public function getValueField()
108
    {
109
        return $this->getField(self::VALUE_SUFFIX);
110
    }
111
112
    /**
113
     * @return $this
114
     */
115
    public function setValueField($value, $markChanged = true)
116
    {
117
        return $this->setField(self::VALUE_SUFFIX, $value, $markChanged);
118
    }
119
120
    /**
121
     * @return string
122
     */
123
    public function getBlindIndexField()
124
    {
125
        return $this->getField(self::INDEX_SUFFIX);
126
    }
127
128
    /**
129
     * @return $this
130
     */
131
    public function setBlindIndexField($value, $markChanged = true)
132
    {
133
        return $this->setField(self::INDEX_SUFFIX, $value, $markChanged);
134
    }
135
136
    /**
137
     * @param CipherSweet $engine
0 ignored issues
show
Bug introduced by
The type LeKoala\Encrypt\CipherSweet was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
138
     * @return EncryptedField
139
     */
140
    public function getEncryptedField($engine = null)
141
    {
142
        if ($engine === null) {
143
            $engine = EncryptHelper::getCipherSweet();
144
        }
145
        $indexSize = $this->getIndexSize(self::LARGE_INDEX_SIZE);
146
        // fieldName needs to match exact db name for row rotator to work properly
147
        $encryptedField = (new EncryptedField($engine, $this->tableName, $this->name . self::VALUE_SUFFIX))
148
            ->addBlindIndex(new BlindIndex($this->name . self::INDEX_SUFFIX, [], $indexSize));
149
        return $encryptedField;
150
    }
151
152
    /**
153
     * This is not called anymore, we rely on saveInto for now
154
     * @link https://github.com/silverstripe/silverstripe-framework/issues/8800
155
     * @param array $manipulation
156
     * @return void
157
     */
158
    public function writeToManipulation(&$manipulation)
159
    {
160
        $encryptedField = $this->getEncryptedField();
161
162
        if ($this->value) {
163
            $dataForStorage = $encryptedField->prepareForStorage($this->value);
164
            $encryptedValue = $this->prepValueForDB($dataForStorage[0]);
165
            $blindIndexes = $dataForStorage[1];
166
        } else {
167
            $encryptedValue = null;
168
            $blindIndexes = [];
169
        }
170
171
        $manipulation['fields'][$this->name . self::VALUE_SUFFIX] = $encryptedValue;
172
        $manipulation['fields'][$this->name . self::INDEX_SUFFIX] = $blindIndexes[$this->name . self::INDEX_SUFFIX] ?? null;
173
    }
174
175
    /**
176
     * @param SQLSelect $query
177
     */
178
    public function addToQuery(&$query)
179
    {
180
        parent::addToQuery($query);
181
        $query->selectField(sprintf('"%s' . self::VALUE_SUFFIX . '"', $this->name));
182
        $query->selectField(sprintf('"%s' . self::INDEX_SUFFIX . '"', $this->name));
183
    }
184
185
    /**
186
     * Return the blind index value to search in the database
187
     *
188
     * @param string $val The unencrypted value
189
     * @param string $indexSuffix The blind index. Defaults to full index
190
     * @return string
191
     */
192
    public function getSearchValue($val, $indexSuffix = null)
193
    {
194
        if (!$this->tableName && $this->record) {
195
            $this->tableName = DataObject::getSchema()->tableName(get_class($this->record));
0 ignored issues
show
Bug introduced by
It seems like $this->record can also be of type array; however, parameter $object of get_class() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

195
            $this->tableName = DataObject::getSchema()->tableName(get_class(/** @scrutinizer ignore-type */ $this->record));
Loading history...
196
        }
197
        if (!$this->tableName) {
198
            throw new Exception("Table name not set for search value. Please set a dataobject.");
199
        }
200
        if (!$this->name) {
201
            throw new Exception("Name not set for search value");
202
        }
203
        if ($indexSuffix === null) {
204
            $indexSuffix = self::INDEX_SUFFIX;
205
        }
206
        $field = $this->getEncryptedField();
207
        $index = $field->getBlindIndex($val, $this->name . $indexSuffix);
208
        return $index;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $index also could return the type array<string,string> which is incompatible with the documented return type string.
Loading history...
209
    }
210
211
    /**
212
     * Return a ready to use array params for a where clause
213
     *
214
     * @param string $val The unencrypted value
215
     * @param string $indexSuffix The blind index. Defaults to full index
216
     * @return array
217
     */
218
    public function getSearchParams($val, $indexSuffix = null)
219
    {
220
        if (!$indexSuffix) {
221
            $indexSuffix = self::INDEX_SUFFIX;
222
        }
223
        $searchValue = $this->getSearchValue($val, $indexSuffix);
224
        $blindIndexField = $this->name . $indexSuffix;
225
        return array($blindIndexField . ' = ?' => $searchValue);
226
    }
227
228
    /**
229
     * @param string $val The unencrypted value
230
     * @param string $indexSuffix The blind index. Defaults to full index
231
     * @return DataList
0 ignored issues
show
Bug introduced by
The type LeKoala\Encrypt\DataList was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
232
     */
233
    public function fetchDataList($val, $indexSuffix = null)
234
    {
235
        if (!$this->record) {
236
            throw new Exception("No record set for this field");
237
        }
238
        if (!$indexSuffix) {
239
            $indexSuffix = self::INDEX_SUFFIX;
240
        }
241
        $class = get_class($this->record);
0 ignored issues
show
Bug introduced by
It seems like $this->record can also be of type array; however, parameter $object of get_class() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

241
        $class = get_class(/** @scrutinizer ignore-type */ $this->record);
Loading history...
242
243
        // A blind index can return false positives
244
        $params = $this->getSearchParams($val, $indexSuffix);
245
        $blindIndexes = $this->getEncryptedField()->getBlindIndexObjects();
0 ignored issues
show
Unused Code introduced by
The assignment to $blindIndexes is dead and can be removed.
Loading history...
246
        $list = $class::get()->where($params);
247
        return $list;
248
    }
249
250
    /**
251
     * @param string $val The unencrypted value
252
     * @param string $indexSuffix The blind index. Defaults to full index
253
     * @return DataObject
254
     */
255
    public function fetchRecord($val, $indexSuffix = null)
256
    {
257
        if (!$indexSuffix) {
258
            $indexSuffix = self::INDEX_SUFFIX;
259
        }
260
        $list = $this->fetchDataList($val, $indexSuffix);
261
        $blindIndexes = $this->getEncryptedField()->getBlindIndexObjects();
262
        $blindIndex = $blindIndexes[$this->name . $indexSuffix];
263
        $name = $this->name;
264
        /** @var DataObject $record  */
265
        foreach ($list as $record) {
266
            $obj = $record->dbObject($name);
267
            // Value might be transformed
268
            if ($blindIndex->getTransformed($obj->getValue()) == $val) {
269
                return $record;
270
            }
271
        }
272
        // throw exception if there where matches but none with the right value
273
        if ($list->count()) {
274
            throw new Exception($list->count() . " records were found but none matched the right value");
275
        }
276
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type SilverStripe\ORM\DataObject.
Loading history...
277
    }
278
279
    /**
280
     * @return string
281
     */
282
    public function getDecryptedValue()
283
    {
284
        if (EncryptHelper::isEncrypted($this->value)) {
285
            return $this->getEncryptedField()->decryptValue($this->value);
286
        }
287
        return $this->value;
288
    }
289
290
    public function setValue($value, $record = null, $markChanged = true)
291
    {
292
        // Return early if we keep encrypted value in memory
293
        if (!EncryptHelper::getAutomaticDecryption()) {
294
            parent::setValue($value, $record, $markChanged);
295
            return $this;
296
        }
297
298
        if ($markChanged) {
299
            $this->isChanged = true;
300
        }
301
302
        // When given a dataobject, bind this field to that
303
        if ($record instanceof DataObject) {
304
            $this->bindTo($record);
305
        }
306
307
        // Convert an object to an array
308
        if ($record && $record instanceof DataObject) {
309
            $record = $record->getQueriedDatabaseFields();
310
            if (!$record) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $record of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
311
                throw new Exception("Could not convert record to array");
312
            }
313
        }
314
315
        // Set the table name if it was not set earlier
316
        if (!$this->tableName && $record) {
317
            $this->tableName = DataObject::getSchema()->tableName(get_class($record));
318
            if (!$this->tableName) {
319
                throw new Exception("Could not get table name from record from " . gettype($record));
320
            }
321
        }
322
323
        // Value will store the decrypted value
324
        if ($value instanceof EncryptedDBField) {
325
            $this->value = $value->getValue();
326
        } elseif ($record && isset($record[$this->name . self::VALUE_SUFFIX])) {
327
            // In that case, the value come from the database and might be encrypted
328
            $encryptedValue = $record[$this->name . self::VALUE_SUFFIX];
329
330
            // It should always be encrypted from the db, but just in case...
331
            if ($encryptedValue && EncryptHelper::isEncrypted($encryptedValue)) {
332
                try {
333
                    $this->value = $this->getEncryptedField()->decryptValue($encryptedValue);
334
                } catch (InvalidCiphertextException $ex) {
335
                    $this->encryptionException = $ex;
336
                    // rotate backend ?
337
                    if (EncryptHelper::getAutomaticRotation()) {
338
                        $encryption = EncryptHelper::getEncryption($encryptedValue);
339
                        $engine = EncryptHelper::getEngineForEncryption($encryption);
340
                        $oldEncryptedField = $this->getEncryptedField($engine);
341
                        $this->value = $oldEncryptedField->decryptValue($encryptedValue);
342
                    } else {
343
                        $this->value = $encryptedValue;
344
                    }
345
                } catch (Exception $ex) {
346
                    $this->encryptionException = $ex;
347
                    // We cannot decrypt
348
                    $this->value = $this->nullValue();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $this->value is correct as $this->nullValue() targeting SilverStripe\ORM\FieldType\DBField::nullValue() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
349
                }
350
            } else {
351
                if ($encryptedValue) {
352
                    $this->value = $encryptedValue;
353
                } else {
354
                    $this->value = $this->nullValue();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $this->value is correct as $this->nullValue() targeting SilverStripe\ORM\FieldType\DBField::nullValue() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
355
                }
356
            }
357
        } elseif (is_array($value)) {
358
            if (array_key_exists(self::VALUE_SUFFIX, $value)) {
359
                $this->value = $value;
360
            }
361
        } elseif (is_string($value) || !$value) {
362
            $this->value = $value;
363
        } else {
364
            throw new Exception("Unexcepted value of type " . gettype($value));
365
        }
366
367
        // Forward changes since writeToManipulation are not called
368
        // $this->setValueField($value, $markChanged);
369
370
        return $this;
371
    }
372
373
    /**
374
     * @return string
375
     */
376
    public function Nice($options = array())
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

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

376
    public function Nice(/** @scrutinizer ignore-unused */ $options = array())

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
377
    {
378
        return $this->getValue();
379
    }
380
381
    /**
382
     * @return boolean
383
     */
384
    public function exists()
385
    {
386
        return strlen($this->value) > 0;
387
    }
388
389
    /**
390
     * This is called by getChangedFields() to check if a field is changed
391
     *
392
     * @return boolean
393
     */
394
    public function isChanged()
395
    {
396
        return $this->isChanged;
397
    }
398
399
    /**
400
     * If we pass a DBField to the setField method, it will
401
     * trigger this method
402
     *
403
     * We save encrypted value on sub fields. They will be collected
404
     * by write() operation by prepareManipulationTable
405
     *
406
     * Currently prepareManipulationTable ignores composite fields
407
     * so we rely on the sub field mechanisms
408
     *
409
     * @param DataObject $dataObject
410
     * @return void
411
     */
412
    public function saveInto($dataObject)
413
    {
414
        $encryptedField = $this->getEncryptedField();
415
416
        if ($this->value) {
417
            $dataForStorage = $encryptedField->prepareForStorage($this->value);
418
            $encryptedValue = $this->prepValueForDB($dataForStorage[0]);
419
            $blindIndexes = $dataForStorage[1];
420
        } else {
421
            $encryptedValue = null;
422
            $blindIndexes = [];
423
        }
424
425
        // This cause infinite loops
426
        // $dataObject->setField($this->getName(), $this->value);
427
428
        // Encrypt value
429
        $key = $this->getName() . 'Value';
430
        $dataObject->setField($key, $encryptedValue);
431
432
        // Build blind index
433
        $key = $this->getName() . self::INDEX_SUFFIX;
434
        if (isset($blindIndexes[$key])) {
435
            $dataObject->setField($key, $blindIndexes[$key]);
436
        }
437
    }
438
439
    /**
440
     * @param string $title Optional. Localized title of the generated instance
441
     * @return FormField
0 ignored issues
show
Bug introduced by
The type LeKoala\Encrypt\FormField was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
442
     */
443
    public function scaffoldFormField($title = null, $params = null)
444
    {
445
        $field = TextField::create($this->name);
446
        return $field;
447
    }
448
449
    /**
450
     * Returns the string value
451
     */
452
    public function __toString()
453
    {
454
        return (string) $this->getValue();
455
    }
456
457
    public function scalarValueOnly()
458
    {
459
        return false;
460
    }
461
}
462