Passed
Push — master ( f4fa40...82755f )
by Thomas
02:30
created

HasEncryptedFields::rotateEncryption()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 23
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 17
c 0
b 0
f 0
dl 0
loc 23
rs 9.7
cc 2
nc 2
nop 1
1
<?php
2
3
namespace LeKoala\Encrypt;
4
5
use Exception;
6
use SilverStripe\ORM\DataObject;
7
use ParagonIE\CipherSweet\CipherSweet;
8
use ParagonIE\CipherSweet\EncryptedRow;
9
use ParagonIE\CipherSweet\Exception\InvalidCiphertextException;
10
use ParagonIE\CipherSweet\KeyRotation\RowRotator;
11
use SilverStripe\ORM\Queries\SQLSelect;
12
use SilverStripe\ORM\Queries\SQLUpdate;
13
use SodiumException;
14
15
/**
16
 * This trait helps to override the default getField method in order to return
17
 * the value of a field directly instead of the db object instance
18
 *
19
 * Simply define this in your code
20
 *
21
 * public function getField($field)
22
 * {
23
 *    return $this->getEncryptedField($field);
24
 * }
25
 *
26
 * public function setField($fieldName, $val)
27
 * {
28
 *     return $this->setEncryptedField($fieldName, $val);
29
 * }
30
 */
31
trait HasEncryptedFields
32
{
33
    /**
34
     * Check if the record needs to be reencrypted with a new key or algo
35
     * @param CipherSweet $old
36
     * @return bool
37
     */
38
    public function needsToRotateEncryption(CipherSweet $old)
39
    {
40
        $class = get_class($this);
41
        $tableName = DataObject::getSchema()->tableName($class);
42
        $columnIdentifier = DataObject::getSchema()->sqlColumnForField($class, 'ID');
43
44
        $new = EncryptHelper::getCipherSweet();
45
46
        $oldRow = $this->getEncryptedRow($old);
47
        $newRow = $this->getEncryptedRow($new);
48
49
        $rotator = new RowRotator($oldRow, $newRow);
50
        $query = new SQLSelect("*", $tableName, [$columnIdentifier => $this->ID]);
51
        $ciphertext = $query->execute()->first();
52
        $ciphertext = EncryptHelper::removeNulls($ciphertext);
53
        if ($rotator->needsReEncrypt($ciphertext)) {
54
            return true;
55
        }
56
        return false;
57
    }
58
59
    /**
60
     * Rotate encryption with current engine without using orm
61
     * @param CipherSweet $old
62
     * @return bool
63
     * @throws SodiumException
64
     * @throws InvalidCiphertextException
65
     */
66
    public function rotateEncryption(CipherSweet $old)
67
    {
68
        $class = get_class($this);
69
        $tableName = DataObject::getSchema()->tableName($class);
70
        $columnIdentifier = DataObject::getSchema()->sqlColumnForField($class, 'ID');
71
72
        $new = EncryptHelper::getCipherSweet();
73
74
        $oldRow = $this->getEncryptedRow($old);
75
        $newRow = $this->getEncryptedRow($new);
76
77
        $rotator = new RowRotator($oldRow, $newRow);
78
        $query = new SQLSelect("*", $tableName, [$columnIdentifier => $this->ID]);
79
        $ciphertext = $query->execute()->first();
80
        $ciphertext = EncryptHelper::removeNulls($ciphertext);
81
        $indices = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $indices is dead and can be removed.
Loading history...
82
        if ($rotator->needsReEncrypt($ciphertext)) {
83
            list($ciphertext, $indices) = $rotator->prepareForUpdate($ciphertext);
84
            $assignment = array_merge($ciphertext, $indices);
85
            $update = new SQLUpdate($tableName, $assignment, ["ID" => $this->ID]);
86
            return $update->execute();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $update->execute() returns the type SilverStripe\ORM\Connect\Query which is incompatible with the documented return type boolean.
Loading history...
87
        }
88
        return false;
89
    }
90
91
    /**
92
     * @param CipherSweet $engine
93
     * @return EncryptedRow
94
     */
95
    public function getEncryptedRow(CipherSweet $engine = null)
96
    {
97
        if ($engine === null) {
98
            $engine = EncryptHelper::getCipherSweet();
99
        }
100
        $tableName = DataObject::getSchema()->tableName(get_class($this));
101
        $encryptedRow = new EncryptedRow($engine, $tableName);
102
        $fields = EncryptHelper::getEncryptedFields(get_class($this));
103
        foreach ($fields as $field) {
104
            /** @var EncryptedField $encryptedField */
105
            $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

105
            $encryptedField = $this->/** @scrutinizer ignore-call */ dbObject($field)->getEncryptedField($engine);
Loading history...
106
            $blindIndexes = $encryptedField->getBlindIndexObjects();
107
            if (count($blindIndexes)) {
108
                $encryptedRow->addField($field . "Value");
109
                foreach ($encryptedField->getBlindIndexObjects() as $blindIndex) {
110
                    $encryptedRow->addBlindIndex($field, $blindIndex);
111
                }
112
            } else {
113
                $encryptedRow->addField($field);
114
            }
115
        }
116
        return $encryptedRow;
117
    }
118
119
    /**
120
     * Extend getField to support retrieving encrypted value transparently
121
     * @param string $field The name of the field
122
     * @return mixed The field value
123
     */
124
    public function getEncryptedField($field)
125
    {
126
        // If it's an encrypted field
127
        if ($this->hasEncryptedField($field)) {
128
            $fieldObj = $this->dbObject($field);
129
            // Set decrypted value directly on the record for later use
130
            $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...
131
        }
132
        return parent::getField($field);
133
    }
134
135
    /**
136
     * Extend setField to support setting encrypted value transparently
137
     * @param string $field
138
     * @param mixed $val
139
     * @return $this
140
     */
141
    public function setEncryptedField($field, $val)
142
    {
143
        // If it's an encrypted field
144
        if ($this->hasEncryptedField($field) && $val && is_scalar($val)) {
145
            $schema = static::getSchema();
146
147
            // In case of composite fields, return the DBField object
148
            if ($schema->compositeField(static::class, $field)) {
149
                $fieldObj = $this->dbObject($field);
150
                $fieldObj->setValue($val);
151
                // Keep a reference for isChange checks
152
                $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...
153
                // Proceed with DBField instance, that will call saveInto
154
                // and call this method again for distinct fields
155
                $val = $fieldObj;
156
            }
157
        }
158
        return parent::setField($field, $val);
159
    }
160
161
    /**
162
     * @param string $field
163
     * @return boolean
164
     */
165
    public function hasEncryptedField($field)
166
    {
167
        return EncryptHelper::isEncryptedField(get_class($this), $field);
168
    }
169
}
170