HasEncryptedFields::getAllByBlindIndex()   A
last analyzed

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\DataList;
7
use SilverStripe\ORM\DataObject;
8
use ParagonIE\CipherSweet\CipherSweet;
9
use SilverStripe\ORM\DataObjectSchema;
10
use ParagonIE\CipherSweet\EncryptedRow;
11
use SilverStripe\ORM\FieldType\DBField;
12
use SilverStripe\ORM\Queries\SQLSelect;
13
use SilverStripe\ORM\Queries\SQLUpdate;
14
use ParagonIE\CipherSweet\EncryptedField;
15
use ParagonIE\CipherSweet\KeyRotation\RowRotator;
16
use ParagonIE\CipherSweet\Exception\InvalidCiphertextException;
17
18
/**
19
 * This trait helps to override the default getField method in order to return
20
 * the value of a field directly instead of the db object instance
21
 *
22
 * Simply define this in your code
23
 *
24
 * public function getField($field)
25
 * {
26
 *    return $this->getEncryptedField($field);
27
 * }
28
 *
29
 * public function setField($fieldName, $val)
30
 * {
31
 *     return $this->setEncryptedField($fieldName, $val);
32
 * }
33
 */
34
trait HasEncryptedFields
35
{
36
    /**
37
     * This value will return exactly one record, taking care of false positives
38
     *
39
     * @param string $field
40
     * @param string $value
41
     * @return DataObject|bool
42
     */
43
    public static function getByBlindIndex($field, $value)
44
    {
45
        /** @var DataObject $singl  */
46
        $singl = singleton(get_called_class());
47
        /** @var EncryptedDBField $obj  */
48
        $obj = $singl->dbObject($field);
49
        return $obj->fetchRecord($value);
50
    }
51
52
    /**
53
     * This value will return a list of records
54
     *
55
     * @param string $field
56
     * @param string $value
57
     * @return DataList|static[]
58
     */
59
    public static function getAllByBlindIndex($field, $value)
60
    {
61
        /** @var DataObject $singl  */
62
        $singl = singleton(get_called_class());
63
        /** @var EncryptedDBField $obj  */
64
        $obj = $singl->dbObject($field);
65
        return $obj->fetchDataList($value);
66
    }
67
68
    /**
69
     * Check if the record needs to be reencrypted with a new key or algo
70
     * @param CipherSweet $old
71
     * @return bool
72
     */
73
    public function needsToRotateEncryption(CipherSweet $old)
74
    {
75
        $class = get_class($this);
76
        $tableName = DataObject::getSchema()->tableName($class);
77
        $columnIdentifier = DataObject::getSchema()->sqlColumnForField($class, 'ID');
78
79
        $new = EncryptHelper::getCipherSweet();
80
81
        $oldRow = $this->getEncryptedRow($old);
82
        $newRow = $this->getEncryptedRow($new);
83
84
        $rotator = new RowRotator($oldRow, $newRow);
85
        $query = new SQLSelect("*", $tableName, [$columnIdentifier => $this->ID]);
86
        $ciphertext = $query->execute()->record();
87
        // not needed anymore since 3.0.1
88
        // $ciphertext = EncryptHelper::removeNulls($ciphertext);
89
        if ($rotator->needsReEncrypt($ciphertext)) {
90
            return true;
91
        }
92
        return false;
93
    }
94
95
    /**
96
     * Rotate encryption with current engine without using orm
97
     * @param CipherSweet $old
98
     * @return bool
99
     * @throws SodiumException
100
     * @throws InvalidCiphertextException
101
     */
102
    public function rotateEncryption(CipherSweet $old)
103
    {
104
        $class = get_class($this);
105
        $tableName = DataObject::getSchema()->tableName($class);
106
        $columnIdentifier = DataObject::getSchema()->sqlColumnForField($class, 'ID');
107
108
        $new = EncryptHelper::getCipherSweet();
109
110
        $encryptedFields = array_keys(EncryptHelper::getEncryptedFields($class, true));
111
        if (EncryptHelper::getAadSource()) {
112
            $encryptedFields[] = EncryptHelper::getAadSource();
113
        }
114
        $query = new SQLSelect($encryptedFields, $tableName, [$columnIdentifier => $this->ID]);
115
        $ciphertext = $query->execute()->record();
116
        $ciphertext = array_filter($ciphertext);
117
118
        // Get only what we need
119
        $oldRow = $this->getEncryptedRow($old, $ciphertext);
120
        $newRow = $this->getEncryptedRow($new, $ciphertext);
121
122
        $rotator = new RowRotator($oldRow, $newRow);
123
124
        $indices = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $indices is dead and can be removed.
Loading history...
125
        if ($rotator->needsReEncrypt($ciphertext)) {
126
            list($ciphertext, $indices) = $rotator->prepareForUpdate($ciphertext);
127
            $assignment = $ciphertext;
128
            foreach ($indices as $name => $arr) {
129
                $assignment[$name] = $arr["value"];
130
            }
131
            $update = new SQLUpdate($tableName, $assignment, ["ID" => $this->ID]);
132
            $update->execute();
133
            return true;
134
        }
135
        return false;
136
    }
137
138
    /**
139
     * @param CipherSweet $engine
140
     * @param array $onlyFields
141
     * @return EncryptedRow
142
     */
143
    public function getEncryptedRow(CipherSweet $engine = null, $onlyFields = [])
144
    {
145
        if ($engine === null) {
146
            $engine = EncryptHelper::getCipherSweet();
147
        }
148
        $class = get_class($this);
149
        $tableName = DataObject::getSchema()->tableName($class);
150
        $encryptedRow = new EncryptedRow($engine, $tableName);
151
        $encryptedRow->setPermitEmpty(true);
152
        $encryptedRow->setTypedIndexes(true);
153
        $encryptedFields = array_keys(EncryptHelper::getEncryptedFields($class));
154
        $aadSource = EncryptHelper::getAadSource();
155
        foreach ($encryptedFields as $field) {
156
            if (!empty($onlyFields) && !array_key_exists($field, $onlyFields)) {
157
                continue;
158
            }
159
            /** @var EncryptedDBField $dbField */
160
            $dbField = $this->dbObject($field);
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

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

252
            $db = $this->/** @scrutinizer ignore-call */ config()->db;
Loading history...
253
            foreach ($this->record as $k => $v) {
254
                $dbClass = $db[$k] ?? null;
255
                if ($dbClass && EncryptHelper::isEncryptedDbClass($dbClass)) {
256
                    $this->dbObject($k)->setValue($v);
257
                }
258
            }
259
        }
260
    }
261
}
262