Passed
Push — master ( 1d51bb...57a30c )
by Thomas
12:26
created

HasEncryptedFields::getByBlindIndex()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 7
rs 10
cc 1
nc 1
nop 2
1
<?php
2
3
namespace LeKoala\Encrypt;
4
5
use SodiumException;
6
use SilverStripe\ORM\DataObject;
7
use ParagonIE\CipherSweet\CipherSweet;
8
use SilverStripe\ORM\DataObjectSchema;
9
use ParagonIE\CipherSweet\EncryptedRow;
10
use SilverStripe\ORM\FieldType\DBField;
11
use SilverStripe\ORM\Queries\SQLSelect;
12
use SilverStripe\ORM\Queries\SQLUpdate;
13
use ParagonIE\CipherSweet\KeyRotation\RowRotator;
14
use ParagonIE\CipherSweet\Exception\InvalidCiphertextException;
15
16
/**
17
 * This trait helps to override the default getField method in order to return
18
 * the value of a field directly instead of the db object instance
19
 *
20
 * Simply define this in your code
21
 *
22
 * public function getField($field)
23
 * {
24
 *    return $this->getEncryptedField($field);
25
 * }
26
 *
27
 * public function setField($fieldName, $val)
28
 * {
29
 *     return $this->setEncryptedField($fieldName, $val);
30
 * }
31
 */
32
trait HasEncryptedFields
33
{
34
    /**
35
     * This value will return exactly one record, taking care of false positives
36
     *
37
     * @param string $field
38
     * @param string $value
39
     * @return $this
40
     */
41
    public static function getByBlindIndex($field, $value)
42
    {
43
        /** @var DataObject $singl  */
44
        $singl = singleton(get_called_class());
45
        /** @var EncryptedDBField $obj  */
46
        $obj = $singl->dbObject($field);
47
        return $obj->fetchRecord($value);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $obj->fetchRecord($value) returns the type SilverStripe\ORM\DataObject which is incompatible with the documented return type LeKoala\Encrypt\HasEncryptedFields.
Loading history...
48
    }
49
50
    /**
51
     * This value will return a list of records
52
     *
53
     * @param string $field
54
     * @param string $value
55
     * @return DataList|static[]
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...
56
     */
57
    public static function getAllByBlindIndex($field, $value)
58
    {
59
        /** @var DataObject $singl  */
60
        $singl = singleton(get_called_class());
61
        /** @var EncryptedDBField $obj  */
62
        $obj = $singl->dbObject($field);
63
        return $obj->fetchDataList($value);
64
    }
65
66
    /**
67
     * Check if the record needs to be reencrypted with a new key or algo
68
     * @param CipherSweet $old
69
     * @return bool
70
     */
71
    public function needsToRotateEncryption(CipherSweet $old)
72
    {
73
        $class = get_class($this);
74
        $tableName = DataObject::getSchema()->tableName($class);
75
        $columnIdentifier = DataObject::getSchema()->sqlColumnForField($class, 'ID');
76
77
        $new = EncryptHelper::getCipherSweet();
78
79
        $oldRow = $this->getEncryptedRow($old);
80
        $newRow = $this->getEncryptedRow($new);
81
82
        $rotator = new RowRotator($oldRow, $newRow);
83
        $query = new SQLSelect("*", $tableName, [$columnIdentifier => $this->ID]);
84
        $ciphertext = $query->execute()->first();
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\ORM\Connect\Query::first() has been deprecated: 4.13.0 Will be replaced by getIterator() in CMS 5 ( Ignorable by Annotation )

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

84
        $ciphertext = /** @scrutinizer ignore-deprecated */ $query->execute()->first();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
85
        // not needed anymore since 3.0.1
86
        // $ciphertext = EncryptHelper::removeNulls($ciphertext);
87
        if ($rotator->needsReEncrypt($ciphertext)) {
88
            return true;
89
        }
90
        return false;
91
    }
92
93
    /**
94
     * Rotate encryption with current engine without using orm
95
     * @param CipherSweet $old
96
     * @return bool
97
     * @throws SodiumException
98
     * @throws InvalidCiphertextException
99
     */
100
    public function rotateEncryption(CipherSweet $old)
101
    {
102
        $class = get_class($this);
103
        $tableName = DataObject::getSchema()->tableName($class);
104
        $columnIdentifier = DataObject::getSchema()->sqlColumnForField($class, 'ID');
105
106
        $new = EncryptHelper::getCipherSweet();
107
108
        $encryptedFields = array_keys(EncryptHelper::getEncryptedFields($class, true));
109
        if (EncryptHelper::getAadSource()) {
110
            $encryptedFields[] = EncryptHelper::getAadSource();
111
        }
112
        $query = new SQLSelect($encryptedFields, $tableName, [$columnIdentifier => $this->ID]);
113
        $ciphertext = $query->execute()->first();
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\ORM\Connect\Query::first() has been deprecated: 4.13.0 Will be replaced by getIterator() in CMS 5 ( Ignorable by Annotation )

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

113
        $ciphertext = /** @scrutinizer ignore-deprecated */ $query->execute()->first();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
114
        $ciphertext = array_filter($ciphertext);
115
116
        // Get only what we need
117
        $oldRow = $this->getEncryptedRow($old, $ciphertext);
118
        $newRow = $this->getEncryptedRow($new, $ciphertext);
119
120
        $rotator = new RowRotator($oldRow, $newRow);
121
122
        $indices = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $indices is dead and can be removed.
Loading history...
123
        if ($rotator->needsReEncrypt($ciphertext)) {
124
            list($ciphertext, $indices) = $rotator->prepareForUpdate($ciphertext);
125
            $assignment = $ciphertext;
126
            foreach ($indices as $name => $arr) {
127
                $assignment[$name] = $arr["value"];
128
            }
129
            $update = new SQLUpdate($tableName, $assignment, ["ID" => $this->ID]);
130
            $update->execute();
131
            return true;
132
        }
133
        return false;
134
    }
135
136
    /**
137
     * @param CipherSweet $engine
138
     * @param array $onlyFields
139
     * @return EncryptedRow
140
     */
141
    public function getEncryptedRow(CipherSweet $engine = null, $onlyFields = [])
142
    {
143
        if ($engine === null) {
144
            $engine = EncryptHelper::getCipherSweet();
145
        }
146
        $class = get_class($this);
147
        $tableName = DataObject::getSchema()->tableName($class);
148
        $encryptedRow = new EncryptedRow($engine, $tableName);
149
        $encryptedFields = array_keys(EncryptHelper::getEncryptedFields($class));
150
        $aadSource = EncryptHelper::getAadSource();
151
        foreach ($encryptedFields as $field) {
152
            if (!empty($onlyFields) && !array_key_exists($field, $onlyFields)) {
153
                continue;
154
            }
155
            /** @var EncryptedField $encryptedField */
156
            $encryptedField = $this->dbObject($field)->getEncryptedField($engine);
0 ignored issues
show
Bug introduced by
It seems like dbObject() 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

156
            $encryptedField = $this->/** @scrutinizer ignore-call */ dbObject($field)->getEncryptedField($engine);
Loading history...
157
            $blindIndexes = $encryptedField->getBlindIndexObjects();
158
            if (count($blindIndexes)) {
159
                $encryptedRow->addTextField($field . EncryptedDBField::VALUE_SUFFIX, $aadSource);
160
                foreach ($encryptedField->getBlindIndexObjects() as $blindIndex) {
161
                    $encryptedRow->addBlindIndex($field . EncryptedDBField::VALUE_SUFFIX, $blindIndex);
162
                }
163
            } else {
164
                $encryptedRow->addTextField($field, $aadSource);
165
            }
166
        }
167
        return $encryptedRow;
168
    }
169
170
    /**
171
     * Extend getField to support retrieving encrypted value transparently
172
     * @param string $field The name of the field
173
     * @return mixed The field value
174
     */
175
    public function getEncryptedField($field)
176
    {
177
        // We cannot check directly $this->record[$field] because it may
178
        // contain encrypted value that needs to be decoded first
179
180
        // If it's an encrypted field
181
        if ($this->hasEncryptedField($field)) {
182
            /** @var EncryptedDBField $fieldObj  */
183
            $fieldObj = $this->dbObject($field);
184
            // Set decrypted value directly on the record for later use
185
            // it can be fetched by dbObject calls
186
            $this->record[$field] = $fieldObj->getValue();
0 ignored issues
show
Bug Best Practice introduced by
The property record does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
187
            return $this->record[$field];
188
        }
189
        return parent::getField($field);
190
    }
191
192
    /**
193
     * Extend setField to support setting encrypted value transparently
194
     * @param string $field
195
     * @param mixed $val
196
     * @return $this
197
     */
198
    public function setEncryptedField($field, $val)
199
    {
200
        // If it's an encrypted field
201
        if ($this->hasEncryptedField($field) && $val && is_scalar($val)) {
202
            /** @var DataObjectSchema $schema  */
203
            $schema = static::getSchema();
204
205
            // In case of composite fields, return the DBField object
206
            // Eg: if we call MyIndexedVarchar instead of MyIndexedVarcharValue
207
            $compositeClass = $schema->compositeField(static::class, $field);
208
            if ($compositeClass) {
209
                /** @var EncryptedDBField $fieldObj  */
210
                $fieldObj = $this->dbObject($field);
211
                $fieldObj->setValue($val);
212
                // Keep a reference for isChange checks
213
                // and also can be queried by dbObject
214
                $this->record[$field] = $fieldObj;
0 ignored issues
show
Bug Best Practice introduced by
The property record does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
215
                // Proceed with DBField instance, that will call saveInto
216
                // and call this method again for distinct fields
217
                $val = $fieldObj;
218
            }
219
        }
220
        parent::setField($field, $val);
221
        return $this;
222
    }
223
224
    /**
225
     * @param string $field
226
     * @return boolean
227
     */
228
    public function hasEncryptedField($field)
229
    {
230
        return EncryptHelper::isEncryptedField(get_class($this), $field);
231
    }
232
233
    /**
234
     * Rebind record value for aad
235
     *
236
     * It would be better to deal with this in writeToManipulation but it is not called
237
     * for CompositeFields
238
     *
239
     * @return void
240
     */
241
    protected function resetFieldValues()
242
    {
243
        if (EncryptHelper::getAadSource() === "ID") {
244
            $db = $this->config()->db;
0 ignored issues
show
Bug introduced by
It seems like config() 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

244
            $db = $this->/** @scrutinizer ignore-call */ config()->db;
Loading history...
245
            foreach ($this->record as $k => $v) {
246
                $dbClass = $db[$k] ?? null;
247
                if ($dbClass && EncryptHelper::isEncryptedDbClass($dbClass)) {
248
                    $this->dbObject($k)->setValue($v);
249
                }
250
            }
251
        }
252
    }
253
}
254