Passed
Push — master ( 5a582b...461cd7 )
by Thomas
02:36
created

HasEncryptedFields::getEncryptedRow()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 26
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 18
c 0
b 0
f 0
dl 0
loc 26
rs 8.8333
cc 7
nc 10
nop 2
1
<?php
2
3
namespace LeKoala\Encrypt;
4
5
use Exception;
6
use SodiumException;
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\KeyRotation\RowRotator;
15
use ParagonIE\CipherSweet\Exception\InvalidCiphertextException;
16
17
/**
18
 * This trait helps to override the default getField method in order to return
19
 * the value of a field directly instead of the db object instance
20
 *
21
 * Simply define this in your code
22
 *
23
 * public function getField($field)
24
 * {
25
 *    return $this->getEncryptedField($field);
26
 * }
27
 *
28
 * public function setField($fieldName, $val)
29
 * {
30
 *     return $this->setEncryptedField($fieldName, $val);
31
 * }
32
 */
33
trait HasEncryptedFields
34
{
35
    /**
36
     * This value will return exactly one record, taking care of false positives
37
     *
38
     * @param string $field
39
     * @param string $value
40
     * @return $this
41
     */
42
    public static function getByBlindIndex($field, $value)
43
    {
44
        /** @var DataObject $singl  */
45
        $singl = singleton(get_called_class());
46
        /** @var EncryptedDBField $obj  */
47
        $obj = $singl->dbObject($field);
48
        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...
49
    }
50
51
    /**
52
     * This value will return a list of records
53
     *
54
     * @param string $field
55
     * @param string $value
56
     * @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...
57
     */
58
    public static function getAllByBlindIndex($field, $value)
59
    {
60
        /** @var DataObject $singl  */
61
        $singl = singleton(get_called_class());
62
        /** @var EncryptedDBField $obj  */
63
        $obj = $singl->dbObject($field);
64
        return $obj->fetchDataList($value);
65
    }
66
67
    /**
68
     * Check if the record needs to be reencrypted with a new key or algo
69
     * @param CipherSweet $old
70
     * @return bool
71
     */
72
    public function needsToRotateEncryption(CipherSweet $old)
73
    {
74
        $class = get_class($this);
75
        $tableName = DataObject::getSchema()->tableName($class);
76
        $columnIdentifier = DataObject::getSchema()->sqlColumnForField($class, 'ID');
77
78
        $new = EncryptHelper::getCipherSweet();
79
80
        $oldRow = $this->getEncryptedRow($old);
81
        $newRow = $this->getEncryptedRow($new);
82
83
        $rotator = new RowRotator($oldRow, $newRow);
84
        $query = new SQLSelect("*", $tableName, [$columnIdentifier => $this->ID]);
85
        $ciphertext = $query->execute()->first();
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
        $query = new SQLSelect($encryptedFields, $tableName, [$columnIdentifier => $this->ID]);
110
        $ciphertext = $query->execute()->first();
111
        $ciphertext = array_filter($ciphertext);
112
113
        // Get only what we need
114
        $oldRow = $this->getEncryptedRow($old, $ciphertext);
115
        $newRow = $this->getEncryptedRow($new, $ciphertext);
116
117
        $rotator = new RowRotator($oldRow, $newRow);
118
119
        $indices = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $indices is dead and can be removed.
Loading history...
120
        if ($rotator->needsReEncrypt($ciphertext)) {
121
            list($ciphertext, $indices) = $rotator->prepareForUpdate($ciphertext);
122
            $assignment = $ciphertext;
123
            foreach ($indices as $name => $arr) {
124
                $assignment[$name] = $arr["value"];
125
            }
126
            $update = new SQLUpdate($tableName, $assignment, ["ID" => $this->ID]);
127
            $update->execute();
128
            return true;
129
        }
130
        return false;
131
    }
132
133
    /**
134
     * @param CipherSweet $engine
135
     * @param array $onlyFields
136
     * @return EncryptedRow
137
     */
138
    public function getEncryptedRow(CipherSweet $engine = null, $onlyFields = [])
139
    {
140
        if ($engine === null) {
141
            $engine = EncryptHelper::getCipherSweet();
142
        }
143
        $class = get_class($this);
144
        $tableName = DataObject::getSchema()->tableName($class);
145
        $encryptedRow = new EncryptedRow($engine, $tableName);
146
        $encryptedFields = array_keys(EncryptHelper::getEncryptedFields($class));
147
        foreach ($encryptedFields as $field) {
148
            if (!empty($onlyFields) && !array_key_exists($field, $onlyFields)) {
149
                continue;
150
            }
151
            /** @var EncryptedField $encryptedField */
152
            $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

152
            $encryptedField = $this->/** @scrutinizer ignore-call */ dbObject($field)->getEncryptedField($engine);
Loading history...
153
            $blindIndexes = $encryptedField->getBlindIndexObjects();
154
            if (count($blindIndexes)) {
155
                $encryptedRow->addField($field . "Value");
156
                foreach ($encryptedField->getBlindIndexObjects() as $blindIndex) {
157
                    $encryptedRow->addBlindIndex($field . "Value", $blindIndex);
158
                }
159
            } else {
160
                $encryptedRow->addField($field);
161
            }
162
        }
163
        return $encryptedRow;
164
    }
165
166
    /**
167
     * Extend getField to support retrieving encrypted value transparently
168
     * @param string $field The name of the field
169
     * @return mixed The field value
170
     */
171
    public function getEncryptedField($field)
172
    {
173
        // We cannot check directly $this->record[$field] because it may
174
        // contain encrypted value that needs to be decoded first
175
176
        // If it's an encrypted field
177
        if ($this->hasEncryptedField($field)) {
178
            $fieldObj = $this->dbObject($field);
179
            // Set decrypted value directly on the record for later use
180
            // it can be fetched by dbObject calls
181
            $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...
182
            return $this->record[$field];
183
        }
184
        return parent::getField($field);
185
    }
186
187
    /**
188
     * Extend setField to support setting encrypted value transparently
189
     * @param string $field
190
     * @param mixed $val
191
     * @return $this
192
     */
193
    public function setEncryptedField($field, $val)
194
    {
195
        // If it's an encrypted field
196
        if ($this->hasEncryptedField($field) && $val && is_scalar($val)) {
197
            /** @var DataObjectSchema $schema  */
198
            $schema = static::getSchema();
199
200
            // In case of composite fields, return the DBField object
201
            // Eg: if we call MyIndexedVarchar instead of MyIndexedVarcharValue
202
            $compositeClass = $schema->compositeField(static::class, $field);
203
            if ($compositeClass) {
204
                $fieldObj = $this->dbObject($field);
205
                $fieldObj->setValue($val);
206
                // Keep a reference for isChange checks
207
                // and also can be queried by dbObject
208
                $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...
209
                // Proceed with DBField instance, that will call saveInto
210
                // and call this method again for distinct fields
211
                $val = $fieldObj;
212
            }
213
        }
214
        parent::setField($field, $val);
215
        return $this;
216
    }
217
218
    /**
219
     * @param string $field
220
     * @return boolean
221
     */
222
    public function hasEncryptedField($field)
223
    {
224
        return EncryptHelper::isEncryptedField(get_class($this), $field);
225
    }
226
}
227