Passed
Push — master ( 71d23f...5a582b )
by Thomas
02:20
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 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
 * @property array $record
34
 * @method DBField dbObject()
35
 */
36
trait HasEncryptedFields
37
{
38
    /**
39
     * This value will return exactly one record, taking care of false positives
40
     *
41
     * @param string $field
42
     * @param string $value
43
     * @return $this
44
     */
45
    public static function getByBlindIndex($field, $value)
46
    {
47
        /** @var DataObject $singl  */
48
        $singl = singleton(get_called_class());
49
        /** @var EncryptedDBField $obj  */
50
        $obj = $singl->dbObject($field);
51
        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...
52
    }
53
54
    /**
55
     * This value will return a list of records
56
     *
57
     * @param string $field
58
     * @param string $value
59
     * @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...
60
     */
61
    public static function getAllByBlindIndex($field, $value)
62
    {
63
        /** @var DataObject $singl  */
64
        $singl = singleton(get_called_class());
65
        /** @var EncryptedDBField $obj  */
66
        $obj = $singl->dbObject($field);
67
        return $obj->fetchDataList($value);
68
    }
69
70
    /**
71
     * Check if the record needs to be reencrypted with a new key or algo
72
     * @param CipherSweet $old
73
     * @return bool
74
     */
75
    public function needsToRotateEncryption(CipherSweet $old)
76
    {
77
        $class = get_class($this);
78
        $tableName = DataObject::getSchema()->tableName($class);
79
        $columnIdentifier = DataObject::getSchema()->sqlColumnForField($class, 'ID');
80
81
        $new = EncryptHelper::getCipherSweet();
82
83
        $oldRow = $this->getEncryptedRow($old);
84
        $newRow = $this->getEncryptedRow($new);
85
86
        $rotator = new RowRotator($oldRow, $newRow);
87
        $query = new SQLSelect("*", $tableName, [$columnIdentifier => $this->ID]);
88
        $ciphertext = $query->execute()->first();
89
        $ciphertext = EncryptHelper::removeNulls($ciphertext);
90
        if ($rotator->needsReEncrypt($ciphertext)) {
91
            return true;
92
        }
93
        return false;
94
    }
95
96
    /**
97
     * Rotate encryption with current engine without using orm
98
     * @param CipherSweet $old
99
     * @return bool
100
     * @throws SodiumException
101
     * @throws InvalidCiphertextException
102
     */
103
    public function rotateEncryption(CipherSweet $old)
104
    {
105
        $class = get_class($this);
106
        $tableName = DataObject::getSchema()->tableName($class);
107
        $columnIdentifier = DataObject::getSchema()->sqlColumnForField($class, 'ID');
108
109
        $new = EncryptHelper::getCipherSweet();
110
111
        $oldRow = $this->getEncryptedRow($old);
112
        $newRow = $this->getEncryptedRow($new);
113
114
        $rotator = new RowRotator($oldRow, $newRow);
115
        $encryptedFields = array_keys(EncryptHelper::getEncryptedFields($class, true));
116
        $query = new SQLSelect($encryptedFields, $tableName, [$columnIdentifier => $this->ID]);
117
        $ciphertext = $query->execute()->first();
118
        $ciphertext = EncryptHelper::removeNulls($ciphertext);
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
     * @return EncryptedRow
136
     */
137
    public function getEncryptedRow(CipherSweet $engine = null)
138
    {
139
        if ($engine === null) {
140
            $engine = EncryptHelper::getCipherSweet();
141
        }
142
        $class = get_class($this);
143
        $tableName = DataObject::getSchema()->tableName($class);
144
        $encryptedRow = new EncryptedRow($engine, $tableName);
145
        $encryptedFields = array_keys(EncryptHelper::getEncryptedFields($class));
146
        foreach ($encryptedFields as $field) {
147
            /** @var EncryptedField $encryptedField */
148
            $encryptedField = $this->dbObject($field)->getEncryptedField($engine);
0 ignored issues
show
Unused Code introduced by
The call to LeKoala\Encrypt\HasEncryptedFields::dbObject() has too many arguments starting with $field. ( Ignorable by Annotation )

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

148
            $encryptedField = $this->/** @scrutinizer ignore-call */ dbObject($field)->getEncryptedField($engine);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
149
            $blindIndexes = $encryptedField->getBlindIndexObjects();
150
            if (count($blindIndexes)) {
151
                $encryptedRow->addField($field . "Value");
152
                foreach ($encryptedField->getBlindIndexObjects() as $blindIndex) {
153
                    $encryptedRow->addBlindIndex($field . "Value", $blindIndex);
154
                }
155
            } else {
156
                $encryptedRow->addField($field);
157
            }
158
        }
159
        return $encryptedRow;
160
    }
161
162
    /**
163
     * Extend getField to support retrieving encrypted value transparently
164
     * @param string $field The name of the field
165
     * @return mixed The field value
166
     */
167
    public function getEncryptedField($field)
168
    {
169
        // We cannot check directly $this->record[$field] because it may
170
        // contain encrypted value that needs to be decoded first
171
172
        // If it's an encrypted field
173
        if ($this->hasEncryptedField($field)) {
174
            $fieldObj = $this->dbObject($field);
0 ignored issues
show
Unused Code introduced by
The call to LeKoala\Encrypt\HasEncryptedFields::dbObject() has too many arguments starting with $field. ( Ignorable by Annotation )

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

174
            /** @scrutinizer ignore-call */ 
175
            $fieldObj = $this->dbObject($field);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
175
            // Set decrypted value directly on the record for later use
176
            // it can be fetched by dbObject calls
177
            $this->record[$field] = $fieldObj->getValue();
178
            return $this->record[$field];
179
        }
180
        return parent::getField($field);
181
    }
182
183
    /**
184
     * Extend setField to support setting encrypted value transparently
185
     * @param string $field
186
     * @param mixed $val
187
     * @return $this
188
     */
189
    public function setEncryptedField($field, $val)
190
    {
191
        // If it's an encrypted field
192
        if ($this->hasEncryptedField($field) && $val && is_scalar($val)) {
193
            /** @var DataObjectSchema $schema  */
194
            $schema = static::getSchema();
195
196
            // In case of composite fields, return the DBField object
197
            // Eg: if we call MyIndexedVarchar instead of MyIndexedVarcharValue
198
            $compositeClass = $schema->compositeField(static::class, $field);
199
            if ($compositeClass) {
200
                $fieldObj = $this->dbObject($field);
0 ignored issues
show
Unused Code introduced by
The call to LeKoala\Encrypt\HasEncryptedFields::dbObject() has too many arguments starting with $field. ( Ignorable by Annotation )

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

200
                /** @scrutinizer ignore-call */ 
201
                $fieldObj = $this->dbObject($field);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
201
                $fieldObj->setValue($val);
202
                // Keep a reference for isChange checks
203
                // and also can be queried by dbObject
204
                $this->record[$field] = $fieldObj;
205
                // Proceed with DBField instance, that will call saveInto
206
                // and call this method again for distinct fields
207
                $val = $fieldObj;
208
            }
209
        }
210
        parent::setField($field, $val);
211
        return $this;
212
    }
213
214
    /**
215
     * @param string $field
216
     * @return boolean
217
     */
218
    public function hasEncryptedField($field)
219
    {
220
        return EncryptHelper::isEncryptedField(get_class($this), $field);
221
    }
222
}
223