Completed
Push — master ( 57a30c...6f382a )
by Thomas
20s queued 13s
created

EncryptedDBField::getBlindIndexField()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
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
13
/**
14
 * Value will be set on parent record through built in getField
15
 * mechanisms for composite fields
16
 */
17
class EncryptedDBField extends DBComposite
18
{
19
    use HasBaseEncryption;
20
21
    const LARGE_INDEX_SIZE = 32;
22
    const SMALL_INDEX_SIZE = 16;
23
    const VALUE_SUFFIX = "Value";
24
    const INDEX_SUFFIX = "BlindIndex";
25
26
    /**
27
     * @config
28
     * @var int
29
     */
30
    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...
31
32
    /**
33
     * @config
34
     * @var int
35
     */
36
    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...
37
38
    /**
39
     * @param array
40
     */
41
    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...
42
        "Value" => "Varchar(191)",
43
        "BlindIndex" => 'Varchar(32)',
44
    );
45
46
    /**
47
     * Output size is the number of bits (not bytes) of a blind index.
48
     * Eg: 4 for a 4 digits year
49
     * Note: the larger the output size, the smaller the index should be
50
     * @return int
51
     */
52
    public function getOutputSize()
53
    {
54
        if (array_key_exists('output_size', $this->options)) {
55
            $outputSize = $this->options['output_size'];
56
        } else {
57
            $outputSize = static::config()->get('output_size');
58
        }
59
        return $outputSize;
60
    }
61
62
    /**
63
     * Input domain is the set of all possible distinct inputs.
64
     * 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).
65
     * @return int
66
     */
67
    public function getDomainSize()
68
    {
69
        if (array_key_exists('domain_size', $this->options)) {
70
            $domainSize = $this->options['domain_size'];
71
        } else {
72
            $domainSize = static::config()->get('domain_size');
73
        }
74
        return $domainSize;
75
    }
76
77
    /**
78
     * @param int $default
79
     * @return int
80
     */
81
    public function getIndexSize($default = null)
82
    {
83
        if (array_key_exists('index_size', $this->options)) {
84
            return $this->options['index_size'];
85
        }
86
        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...
87
            return $default;
88
        }
89
        return self::LARGE_INDEX_SIZE;
90
    }
91
92
    /**
93
     * @return string
94
     */
95
    public function getValueField()
96
    {
97
        return $this->getField(self::VALUE_SUFFIX);
98
    }
99
100
    /**
101
     * @return $this
102
     */
103
    public function setValueField($value, $markChanged = true)
104
    {
105
        return $this->setField(self::VALUE_SUFFIX, $value, $markChanged);
106
    }
107
108
    /**
109
     * @return string
110
     */
111
    public function getBlindIndexField()
112
    {
113
        return $this->getField(self::INDEX_SUFFIX);
114
    }
115
116
    /**
117
     * @return $this
118
     */
119
    public function setBlindIndexField($value, $markChanged = true)
120
    {
121
        return $this->setField(self::INDEX_SUFFIX, $value, $markChanged);
122
    }
123
124
    /**
125
     * @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...
126
     * @param bool $fashHash
127
     * @return EncryptedField
128
     */
129
    public function getEncryptedField($engine = null, $fashHash = null)
130
    {
131
        if ($engine === null) {
132
            $engine = EncryptHelper::getCipherSweet();
133
        }
134
        if ($fashHash === null) {
135
            $fashHash = EncryptHelper::getFashHash();
136
        }
137
        $indexSize = $this->getIndexSize(self::LARGE_INDEX_SIZE);
138
        // fieldName needs to match exact db name for row rotator to work properly
139
        $encryptedField = (new EncryptedField($engine, $this->tableName, $this->name . self::VALUE_SUFFIX))
140
            ->addBlindIndex(new BlindIndex($this->name . self::INDEX_SUFFIX, [], $indexSize, $fashHash));
141
        return $encryptedField;
142
    }
143
144
    /**
145
     * This is not called anymore, we rely on saveInto for now
146
     * @link https://github.com/silverstripe/silverstripe-framework/issues/8800
147
     * @param array $manipulation
148
     * @return void
149
     */
150
    public function writeToManipulation(&$manipulation)
151
    {
152
        $encryptedField = $this->getEncryptedField();
153
        $aad = $this->encryptionAad;
154
        if ($this->value) {
155
            $dataForStorage = $encryptedField->prepareForStorage($this->value, $aad);
156
            $encryptedValue = $this->prepValueForDB($dataForStorage[0]);
157
            $blindIndexes = $dataForStorage[1];
158
        } else {
159
            $encryptedValue = null;
160
            $blindIndexes = [];
161
        }
162
163
        $manipulation['fields'][$this->name . self::VALUE_SUFFIX] = $encryptedValue;
164
        foreach ($blindIndexes as $blindIndexName => $blindIndexValue) {
165
            $manipulation['fields'][$blindIndexName] = $blindIndexValue;
166
        }
167
    }
168
169
    /**
170
     * @param SQLSelect $query
171
     */
172
    public function addToQuery(&$query)
173
    {
174
        parent::addToQuery($query);
175
        $query->selectField(sprintf('"%s' . self::VALUE_SUFFIX . '"', $this->name));
176
        $query->selectField(sprintf('"%s' . self::INDEX_SUFFIX . '"', $this->name));
177
    }
178
179
    /**
180
     * Return the blind index value to search in the database
181
     *
182
     * @param string $val The unencrypted value
183
     * @param string $indexSuffix The blind index. Defaults to full index
184
     * @return string
185
     */
186
    public function getSearchValue($val, $indexSuffix = null)
187
    {
188
        if (!$this->tableName && $this->record) {
189
            $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

189
            $this->tableName = DataObject::getSchema()->tableName(get_class(/** @scrutinizer ignore-type */ $this->record));
Loading history...
190
        }
191
        if (!$this->tableName) {
192
            throw new Exception("Table name not set for search value. Please set a dataobject.");
193
        }
194
        if (!$this->name) {
195
            throw new Exception("Name not set for search value");
196
        }
197
        if ($indexSuffix === null) {
198
            $indexSuffix = self::INDEX_SUFFIX;
199
        }
200
        $field = $this->getEncryptedField();
201
        $index = $field->getBlindIndex($val, $this->name . $indexSuffix);
202
        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...
203
    }
204
205
    /**
206
     * Return a ready to use array params for a where clause
207
     *
208
     * @param string $val The unencrypted value
209
     * @param string $indexSuffix The blind index. Defaults to full index
210
     * @return array
211
     */
212
    public function getSearchParams($val, $indexSuffix = null)
213
    {
214
        if (!$indexSuffix) {
215
            $indexSuffix = self::INDEX_SUFFIX;
216
        }
217
        $searchValue = $this->getSearchValue($val, $indexSuffix);
218
        $blindIndexField = $this->name . $indexSuffix;
219
        return array($blindIndexField . ' = ?' => $searchValue);
220
    }
221
222
    /**
223
     * @param string $val The unencrypted value
224
     * @param string $indexSuffix The blind index. Defaults to full index
225
     * @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...
226
     */
227
    public function fetchDataList($val, $indexSuffix = null)
228
    {
229
        if (!$this->record) {
230
            throw new Exception("No record set for this field");
231
        }
232
        if (!$indexSuffix) {
233
            $indexSuffix = self::INDEX_SUFFIX;
234
        }
235
        $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

235
        $class = get_class(/** @scrutinizer ignore-type */ $this->record);
Loading history...
236
237
        // A blind index can return false positives
238
        $params = $this->getSearchParams($val, $indexSuffix);
239
        $blindIndexes = $this->getEncryptedField()->getBlindIndexObjects();
0 ignored issues
show
Unused Code introduced by
The assignment to $blindIndexes is dead and can be removed.
Loading history...
240
        $list = $class::get()->where($params);
241
        return $list;
242
    }
243
244
    /**
245
     * @param string $val The unencrypted value
246
     * @param string $indexSuffix The blind index. Defaults to full index
247
     * @return DataObject
248
     */
249
    public function fetchRecord($val, $indexSuffix = null)
250
    {
251
        if (!$indexSuffix) {
252
            $indexSuffix = self::INDEX_SUFFIX;
253
        }
254
        $list = $this->fetchDataList($val, $indexSuffix);
255
        $blindIndexes = $this->getEncryptedField()->getBlindIndexObjects();
256
        $blindIndex = $blindIndexes[$this->name . $indexSuffix];
257
        $name = $this->name;
258
        /** @var DataObject $record  */
259
        foreach ($list as $record) {
260
            $obj = $record->dbObject($name);
261
            // Value might be transformed
262
            if ($blindIndex->getTransformed($obj->getValue()) == $val) {
263
                return $record;
264
            }
265
        }
266
        // throw exception if there where matches but none with the right value
267
        if ($list->count()) {
268
            throw new Exception($list->count() . " records were found but none matched the right value");
269
        }
270
        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...
271
    }
272
273
    public function setValue($value, $record = null, $markChanged = true)
274
    {
275
        $this->setEncryptionAad($record);
276
277
        // Return early if we keep encrypted value in memory
278
        if (!EncryptHelper::getAutomaticDecryption()) {
279
            parent::setValue($value, $record, $markChanged);
280
            return $this;
281
        }
282
283
        if ($markChanged) {
284
            $this->isChanged = true;
285
        }
286
287
        // When given a dataobject, bind this field to that
288
        if ($record instanceof DataObject) {
289
            $this->bindTo($record);
290
        }
291
292
        // Convert an object to an array
293
        if ($record && $record instanceof DataObject) {
294
            $record = $record->getQueriedDatabaseFields();
295
            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...
296
                throw new Exception("Could not convert record to array");
297
            }
298
        }
299
300
        // Set the table name if it was not set earlier
301
        if (!$this->tableName && $record) {
302
            $class = is_array($record) && isset($record['ClassName']) ? $record['ClassName'] : get_class($record);
303
            $this->tableName = DataObject::getSchema()->tableName($class);
304
            if (!$this->tableName) {
305
                throw new Exception("Could not get table name from record from " . gettype($record));
306
            }
307
        }
308
309
        // Value will store the decrypted value
310
        if ($value instanceof EncryptedDBField) {
311
            $this->value = $value->getValue();
312
        } elseif ($record && isset($record[$this->name . self::VALUE_SUFFIX])) {
313
            // In that case, the value come from the database and might be encrypted
314
            $encryptedValue = $record[$this->name . self::VALUE_SUFFIX];
315
            $this->value = $this->decryptValue($encryptedValue);
316
        } elseif (is_array($value)) {
317
            if (array_key_exists(self::VALUE_SUFFIX, $value)) {
318
                $this->value = $value;
319
            }
320
        } elseif (is_string($value) || !$value) {
321
            $this->value = $value;
322
        } else {
323
            throw new Exception("Unexcepted value of type " . gettype($value));
324
        }
325
326
        // Forward changes since writeToManipulation are not called
327
        // $this->setValueField($value, $markChanged);
328
329
        return $this;
330
    }
331
332
    /**
333
     * @return string
334
     */
335
    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

335
    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...
336
    {
337
        return $this->getValue();
338
    }
339
340
    /**
341
     * @return boolean
342
     */
343
    public function exists()
344
    {
345
        return strlen($this->value ?? '') > 0;
346
    }
347
348
    /**
349
     * This is called by getChangedFields() to check if a field is changed
350
     *
351
     * @return boolean
352
     */
353
    public function isChanged()
354
    {
355
        return $this->isChanged;
356
    }
357
358
    /**
359
     * If we pass a DBField to the setField method, it will
360
     * trigger this method
361
     *
362
     * We save encrypted value on sub fields. They will be collected
363
     * by write() operation by prepareManipulationTable
364
     *
365
     * Currently prepareManipulationTable ignores composite fields
366
     * so we rely on the sub field mechanisms
367
     *
368
     * @param DataObject $dataObject
369
     * @return void
370
     */
371
    public function saveInto($dataObject)
372
    {
373
        $encryptedField = $this->getEncryptedField();
374
        $aad = $this->encryptionAad;
375
        if ($this->value) {
376
            $dataForStorage = $encryptedField->prepareForStorage($this->value, $aad);
377
            $encryptedValue = $this->prepValueForDB($dataForStorage[0]);
378
            $blindIndexes = $dataForStorage[1];
379
        } else {
380
            $encryptedValue = null;
381
            $blindIndexes = [];
382
        }
383
384
        // This cause infinite loops
385
        // $dataObject->setField($this->getName(), $this->value);
386
387
        // Encrypt value
388
        $key = $this->getName() . self::VALUE_SUFFIX;
389
        $dataObject->setField($key, $encryptedValue);
390
391
        // Build blind indexes
392
        foreach ($blindIndexes as $blindIndexName => $blindIndexValue) {
393
            $dataObject->setField($blindIndexName, $blindIndexValue);
394
        }
395
    }
396
397
    /**
398
     * @param string $title Optional. Localized title of the generated instance
399
     * @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...
400
     */
401
    public function scaffoldFormField($title = null, $params = null)
402
    {
403
        $field = TextField::create($this->getName());
404
        return $field;
405
    }
406
407
    /**
408
     * Returns the string value
409
     */
410
    public function __toString()
411
    {
412
        return (string) $this->getValue();
413
    }
414
415
    public function scalarValueOnly()
416
    {
417
        return false;
418
    }
419
}
420