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

EncryptTest::testMultiTenantProvider()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 92
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 53
c 0
b 0
f 0
dl 0
loc 92
rs 9.0254
cc 3
nc 3
nop 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace LeKoala\Encrypt\Test;
4
5
use Exception;
6
use SilverStripe\ORM\DB;
7
use SilverStripe\Assets\File;
8
use SilverStripe\ORM\DataList;
9
use SilverStripe\ORM\DataObject;
10
use SilverStripe\Security\Member;
11
use LeKoala\Encrypt\EncryptHelper;
12
use SilverStripe\Core\Environment;
13
use SilverStripe\Dev\SapphireTest;
14
use SilverStripe\Security\Security;
15
use LeKoala\Encrypt\EncryptedDBField;
16
use LeKoala\Encrypt\MemberKeyProvider;
17
use ParagonIE\CipherSweet\CipherSweet;
18
use LeKoala\Encrypt\HasEncryptedFields;
19
use SilverStripe\ORM\Queries\SQLSelect;
20
use SilverStripe\ORM\Queries\SQLUpdate;
21
use ParagonIE\CipherSweet\KeyProvider\StringProvider;
22
use ParagonIE\CipherSweet\Contract\MultiTenantSafeBackendInterface;
23
use SilverStripe\ORM\ArrayList;
24
25
/**
26
 * Test for Encrypt
27
 *
28
 * Run with the following command : ./vendor/bin/phpunit ./encrypt/tests/EncryptTest.php
29
 *
30
 * You may need to run:
31
 * php ./framework/cli-script.php dev/build ?flush=all
32
 * before (remember manifest for cli is not the same...)
33
 *
34
 * @group Encrypt
35
 */
36
class EncryptTest extends SapphireTest
37
{
38
    /**
39
     * Defines the fixture file to use for this test class
40
     * @var string
41
     */
42
    protected static $fixture_file = 'EncryptTest.yml';
43
44
    protected static $extra_dataobjects = [
45
        Test_EncryptedModel::class,
46
        Test_EncryptionKey::class,
47
    ];
48
49
    public function setUp()
50
    {
51
        // EncryptHelper::setForcedEncryption("nacl");
52
        EncryptHelper::setAutomaticRotation(false);
53
        Environment::setEnv('ENCRYPTION_KEY', '502370dfc69fd6179e1911707e8a5fb798c915900655dea16370d64404be04e5');
54
        parent::setUp();
55
        EncryptHelper::setAutomaticRotation(true);
56
57
        // test extension is available
58
        if (!extension_loaded('sodium')) {
59
            throw new Exception("You must load sodium extension for this");
60
        }
61
62
        // The rows are already decrypted and changed due to the fixtures going through the ORM layer
63
        $result = DB::query("SELECT * FROM EncryptedModel");
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
64
        // echo '<pre>';
65
        // print_r(iterator_to_array($result));
66
        // die();
67
68
        // Replace with actual yml values
69
        $data = [
70
            'MyText' => 'nacl:_nKhFvZkFwWJssMLf2s6wsucM-6zMmT862XGiYG9KQaL5fgl3CSA_O0gcs3OGPPB4AJoNInC',
71
            'MyHTMLText' => 'nacl:jetKPUBgETbLtlfAx1VkZiWJFG65hCuWVSmrwVDX4TTysmJkj2vnhI329oa9eCTlX1kKSjCp7AyFXDKT7Q==',
72
            'MyVarchar' => 'nacl:_-5ZsG9txfqMNkHY7xl0JlmCLzrx9BemtC0CwGjYZOt9pCwle9PjHmIZcqJcoEdxpJXplLtKPF-xPGax_pIy',
73
            'MyIndexedVarcharValue' => 'nacl:V1G-EPYeHP5-OQu2XAcPp4ym0HuvLpseBqytg3VVddAWoyC3Lm5aAE3G9xx_2uW6QwO0dcnrLFBPNFZ6eQ==',
74
            'MyNumberValue' => 'nacl:u4-luf5o0pi-LuGOwLvKVGgD_tLlO8YJ7GbIx4VYYcUvoPNM-9pQfv05iwb0HQ6ugW4=',
75
        ];
76
        $update = new SQLUpdate("EncryptedModel", $data, "ID IN (1,3)");
0 ignored issues
show
Bug introduced by
'ID IN (1,3)' of type string is incompatible with the type array expected by parameter $where of SilverStripe\ORM\Queries\SQLUpdate::__construct(). ( Ignorable by Annotation )

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

76
        $update = new SQLUpdate("EncryptedModel", $data, /** @scrutinizer ignore-type */ "ID IN (1,3)");
Loading history...
77
        $update->execute();
78
        $data = [
79
            'MyText' => 'brng:DwBtLvR876mbL5qfX1IIUDZ7NbrDtyXAgXjB8dDKxfYAYKlajKr9J9NGMY47tkNcrLv4PBbHQ4bAOTZHGsTvmQuGT7he0w==',
80
            'MyHTMLText' => 'brng:Nf7VDpEPIQABaZvch8wDY3jCuctBcy0x6sIJO_BkWJL2H86b6O0WvXfDldFihhXxnhzJH3cy-Ygx6sMbgBttDPcT6j8SXFqxeG2pxHI=',
81
            'MyVarchar' => 'brng:hoE5xdUMwdJRjXi4jsGs8d_FzBzibUqmiRdoC-oo7_JsFPtz55FzAIb_Qcl-SreatW0uZViRGUvhLGbszKuIUejswmXIqYtSglkc9nHk5g==',
82
            'MyIndexedVarcharValue' => 'brng:_RZQDZXqeISYm3WtfTSAS2p0hZz-QDHAWmFSKvcaWLQ5ODRyUKKcPvsGOiIvfBPYmOJH35zh1Hrm2K2LY4ElLNVfAQN_QgcXpxWWNWI=',
83
            'MyNumberValue' => 'brng:4AB4YlC-AZHrb6d-t6aiDjZDVdg0BHRN2jAb5CoxiFN89XssvReGkbQMp9jGbXtstk1W94745WWJeiI4n05HsDPu',
84
        ];
85
        $update = new SQLUpdate("EncryptedModel", $data, "ID IN (2)");
86
        $update->execute();
87
    }
88
89
    public function tearDown()
90
    {
91
        parent::tearDown();
92
    }
93
94
    /**
95
     * @return Test_EncryptedModel
96
     */
97
    public function getTestModel()
98
    {
99
        return $this->objFromFixture(Test_EncryptedModel::class, 'demo');
100
    }
101
102
    /**
103
     * @return Test_EncryptedModel
104
     */
105
    public function getTestModel2()
106
    {
107
        return $this->objFromFixture(Test_EncryptedModel::class, 'demo2');
108
    }
109
110
    /**
111
     * @return Test_EncryptedModel
112
     */
113
    public function getTestModel3()
114
    {
115
        return $this->objFromFixture(Test_EncryptedModel::class, 'demo3');
116
    }
117
118
    /**
119
     * @return Test_EncryptedModel
120
     */
121
    public function getAdminTestModel()
122
    {
123
        return $this->objFromFixture(Test_EncryptedModel::class, 'admin_record');
124
    }
125
126
    /**
127
     * @return Test_EncryptedModel
128
     */
129
    public function getUser1TestModel()
130
    {
131
        return $this->objFromFixture(Test_EncryptedModel::class, 'user1_record');
132
    }
133
134
    /**
135
     * @return Test_EncryptedModel
136
     */
137
    public function getUser2TestModel()
138
    {
139
        return $this->objFromFixture(Test_EncryptedModel::class, 'user2_record');
140
    }
141
142
    /**
143
     * @return Member
144
     */
145
    public function getAdminMember()
146
    {
147
        return $this->objFromFixture(Member::class, 'admin');
148
    }
149
150
    /**
151
     * @return Member
152
     */
153
    public function getUser1Member()
154
    {
155
        return $this->objFromFixture(Member::class, 'user1');
156
    }
157
158
    /**
159
     * @return Member
160
     */
161
    public function getUser2Member()
162
    {
163
        return $this->objFromFixture(Member::class, 'user2');
164
    }
165
166
    /**
167
     * @return DataList|Member[]
168
     */
169
    public function getAllMembers()
170
    {
171
        return new ArrayList([
0 ignored issues
show
Bug Best Practice introduced by
The expression return new SilverStripe\...his->getUser2Member())) returns the type SilverStripe\ORM\ArrayList which is incompatible with the documented return type SilverStripe\Security\Me...lverStripe\ORM\DataList.
Loading history...
172
            $this->getAdminMember(),
173
            $this->getUser1Member(),
174
            $this->getUser2Member(),
175
        ]);
176
    }
177
178
    /**
179
     * @return File
180
     */
181
    public function getRegularFile()
182
    {
183
        return $this->objFromFixture(File::class, 'regular');
184
    }
185
186
    /**
187
     * @return File
188
     */
189
    public function getEncryptedFile()
190
    {
191
        return $this->objFromFixture(File::class, 'encrypted');
192
    }
193
194
    /**
195
     * @param string $class
196
     * @param int $id
197
     * @return array
198
     */
199
    protected function fetchRawData($class, $id)
200
    {
201
        $tableName = DataObject::getSchema()->tableName($class);
202
        $columnIdentifier = DataObject::getSchema()->sqlColumnForField($class, 'ID');
203
        $sql = new SQLSelect('*', [$tableName], [$columnIdentifier => $id]);
204
        $dbRecord = $sql->firstRow()->execute()->first();
205
        return $dbRecord;
206
    }
207
208
    public function testEncryption()
209
    {
210
        $someText = 'some text';
211
        $encrypt = EncryptHelper::encrypt($someText);
212
        $decryptedValue = EncryptHelper::decrypt($encrypt);
213
214
        $this->assertEquals($someText, $decryptedValue);
215
    }
216
217
    public function testIndexes()
218
    {
219
        $indexes = DataObject::getSchema()->databaseIndexes(Test_EncryptedModel::class);
220
        $keys = array_keys($indexes);
221
        $this->assertContains('MyIndexedVarcharBlindIndex', $keys, "Index is not defined in : " . implode(", ", $keys));
222
        $this->assertContains('MyNumberLastFourBlindIndex', $keys, "Index is not defined in : " . implode(", ", $keys));
223
    }
224
225
    public function testSearch()
226
    {
227
        $singl = singleton(Test_EncryptedModel::class);
228
229
        /** @var EncryptedDBField $obj  */
230
        $obj = $singl->dbObject('MyIndexedVarchar');
231
        $record = $obj->fetchRecord('some_searchable_value');
232
233
        // echo '<pre>';print_r("From test: " . $record->MyIndexedVarchar);die();
234
        $this->assertNotEmpty($record);
235
        $this->assertEquals("some text text", $record->MyText);
236
        $this->assertEquals("some_searchable_value", $record->MyIndexedVarchar);
237
        $this->assertEquals("some_searchable_value", $record->dbObject('MyIndexedVarchar')->getValue());
238
239
        // Also search our super getter method
240
        $recordAlt = Test_EncryptedModel::getByBlindIndex('MyIndexedVarchar', 'some_searchable_value');
241
        $this->assertNotEmpty($record);
242
        $this->assertEquals($recordAlt->ID, $record->ID);
243
244
        // Can we get a list ?
245
        $list = Test_EncryptedModel::getAllByBlindIndex('MyIndexedVarchar', 'some_searchable_value');
246
        $this->assertInstanceOf(DataList::class, $list);
247
248
        $record = $obj->fetchRecord('some_unset_value');
249
        $this->assertEmpty($record);
250
251
        // Let's try our four digits index
252
        $obj = $singl->dbObject('MyNumber');
253
        $record = $obj->fetchRecord('6789', 'LastFourBlindIndex');
254
        $searchValue = $obj->getSearchValue('6789', 'LastFourBlindIndex');
255
        // $searchParams = $obj->getSearchParams('6789', 'LastFourBlindIndex');
256
        // print_r($searchParams);
257
        $this->assertNotEmpty($record, "Nothing found for $searchValue");
258
        $this->assertEquals("0123456789", $record->MyNumber);
259
    }
260
261
    public function testSearchFilter()
262
    {
263
        $record = Test_EncryptedModel::get()->filter('MyIndexedVarchar:Encrypted', 'some_searchable_value')->first();
264
        $this->assertNotEmpty($record);
265
        $this->assertEquals(1, $record->ID);
266
        $this->assertNotEquals(2, $record->ID);
267
268
        $record = Test_EncryptedModel::get()->filter('MyIndexedVarchar:Encrypted', 'some_unset_value')->first();
269
        $this->assertEmpty($record);
270
    }
271
272
    public function testRotation()
273
    {
274
        $model = $this->getTestModel3();
275
        $data = $this->fetchRawData(Test_EncryptedModel::class, $model->ID);
0 ignored issues
show
Unused Code introduced by
The assignment to $data is dead and can be removed.
Loading history...
276
277
        $old = EncryptHelper::getEngineForEncryption("nacl");
278
        $result = $model->needsToRotateEncryption($old);
279
        $this->assertTrue($result);
280
281
        $result = $model->rotateEncryption($old);
282
        $this->assertTrue($result);
283
    }
284
285
    public function testCompositeOptions()
286
    {
287
        $model = $this->getTestModel();
288
289
        /** @var EncryptedDBField $myNumber */
290
        $myNumber = $model->dbObject('MyNumber');
291
292
        $this->assertEquals(10, $myNumber->getDomainSize());
293
        $this->assertEquals(4, $myNumber->getOutputSize());
294
        $this->assertEquals(EncryptedDBField::LARGE_INDEX_SIZE, $myNumber->getIndexSize());
295
296
        /** @var EncryptedDBField $MyIndexedVarchar */
297
        $MyIndexedVarchar = $model->dbObject('MyIndexedVarchar');
298
299
        // Default config values
300
        $this->assertEquals(EncryptHelper::DEFAULT_DOMAIN_SIZE, $MyIndexedVarchar->getDomainSize());
301
        $this->assertEquals(EncryptHelper::DEFAULT_OUTPUT_SIZE, $MyIndexedVarchar->getOutputSize());
302
        $this->assertEquals(EncryptedDBField::LARGE_INDEX_SIZE, $MyIndexedVarchar->getIndexSize());
303
    }
304
305
    public function testIndexPlanner()
306
    {
307
        $sizes = EncryptHelper::planIndexSizesForClass(Test_EncryptedModel::class);
308
        $this->assertNotEmpty($sizes);
309
        $this->assertArrayHasKey("min", $sizes);
310
        $this->assertArrayHasKey("max", $sizes);
311
        $this->assertArrayHasKey("indexes", $sizes);
312
        $this->assertArrayHasKey("estimated_population", $sizes);
313
        $this->assertArrayHasKey("coincidence_count", $sizes);
314
    }
315
316
    public function testFixture()
317
    {
318
        // this one use nacl encryption and will be rotated transparently
319
        $model = $this->getTestModel();
320
321
        $result = $model->needsToRotateEncryption(EncryptHelper::getEngineForEncryption("nacl"));
322
        $this->assertTrue($result);
323
324
        // Ensure we have our blind indexes
325
        $this->assertTrue($model->hasDatabaseField('MyIndexedVarcharValue'));
326
        $this->assertTrue($model->hasDatabaseField('MyIndexedVarcharBlindIndex'));
327
        $this->assertTrue($model->hasDatabaseField('MyNumberValue'));
328
        $this->assertTrue($model->hasDatabaseField('MyNumberBlindIndex'));
329
        $this->assertTrue($model->hasDatabaseField('MyNumberLastFourBlindIndex'));
330
331
        if (class_uses($model, HasEncryptedFields::class)) {
0 ignored issues
show
Bug introduced by
LeKoala\Encrypt\HasEncryptedFields::class of type string is incompatible with the type boolean expected by parameter $autoload of class_uses(). ( Ignorable by Annotation )

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

331
        if (class_uses($model, /** @scrutinizer ignore-type */ HasEncryptedFields::class)) {
Loading history...
332
            $this->assertTrue($model->hasEncryptedField('MyVarchar'));
333
            $this->assertTrue($model->hasEncryptedField('MyIndexedVarchar'));
334
        }
335
336
        // print_r($model);
337
        /*
338
         [record:protected] => Array
339
        (
340
            [ClassName] => LeKoala\Encrypt\Test\Test_EncryptedModel
341
            [LastEdited] => 2020-12-15 10:09:47
342
            [Created] => 2020-12-15 10:09:47
343
            [Name] => demo
344
            [MyText] => nacl:mQ1g5ugjYSWjFd-erM6-xlB_EbWp1bOAUPbL4fa3Ce5SX6LP7sFCczkFx_lRABvZioWJXx-L
345
            [MyHTMLText] => nacl:836In6YCaEf3_mRJR7NOC_s0P8gIFESgmPnHCefTe6ycY_6CLKVmT0_9KWHgnin-WGXMJawkS1hS87xwQw==
346
            [MyVarchar] => nacl:ZeOw8-dcBdFemtGm-MRJ5pCSipOtAO5-zBRms8F5Elex08GuoL_JKbdN-CiOP-u009MJfvGZUkx9Ru5Zn0_y
347
            [RegularFileID] => 2
348
            [EncryptedFileID] => 3
349
            [MyIndexedVarcharBlindIndex] => 04bb6edd
350
            [ID] => 1
351
            [RecordClassName] => LeKoala\Encrypt\Test\Test_EncryptedModel
352
        )
353
        */
354
355
        $varcharValue = 'encrypted varchar value';
356
        $varcharWithIndexValue = 'some_searchable_value';
357
        // regular fields are not affected
358
        $this->assertEquals('demo', $model->Name);
359
360
        // automatically rotated fields store an exception
361
        $this->assertNotEmpty($model->dbObject("MyVarchar")->getEncryptionException());
362
363
        // get value
364
        $this->assertEquals($varcharValue, $model->dbObject('MyVarchar')->getValue());
365
        // encrypted fields work transparently when using trait
366
        $this->assertEquals($varcharValue, $model->MyVarchar);
367
368
        // since dbobject cache can be cleared, exception is gone
369
        $this->assertEmpty($model->dbObject("MyVarchar")->getEncryptionException());
370
371
372
        $this->assertTrue($model->dbObject('MyIndexedVarchar') instanceof EncryptedDBField);
373
        $this->assertTrue($model->dbObject('MyIndexedVarchar')->hasField('Value'));
374
375
        $model->MyIndexedVarchar = $varcharWithIndexValue;
0 ignored issues
show
Bug Best Practice introduced by
The property MyIndexedVarchar does not exist on LeKoala\Encrypt\Test\Test_EncryptedModel. Since you implemented __set, consider adding a @property annotation.
Loading history...
376
        $model->write();
377
        $this->assertEquals($varcharWithIndexValue, $model->MyIndexedVarchar);
0 ignored issues
show
Bug Best Practice introduced by
The property MyIndexedVarchar does not exist on LeKoala\Encrypt\Test\Test_EncryptedModel. Since you implemented __get, consider adding a @property annotation.
Loading history...
378
379
        $dbRecord = $this->fetchRawData(get_class($model), $model->ID);
380
        // print_r($dbRecord);
381
        /*
382
        Array
383
(
384
    [ID] => 1
385
    [ClassName] => LeKoala\Encrypt\Test\Test_EncryptedModel
386
    [LastEdited] => 2020-12-15 10:10:27
387
    [Created] => 2020-12-15 10:10:27
388
    [Name] => demo
389
    [MyText] => nacl:aDplmA9hs7naqiPwWdNRMcYNUltf4mOs8KslRQZ4vCdnJylnbjAJYChtVH7wiiygsAHWqbM6
390
    [MyHTMLText] => nacl:dMvk5Miux0bsSP1SjaXQRlbGogNTu7UD3p6AlNHFMAEGXOQz03hkBx43C-WelCS0KUdAN9ewuwuXZqMmRA==
391
    [MyVarchar] => nacl:sZRenCG6En7Sg_HmsUHkNy_1MXOstly7eHm0i2iq83kTFH40UsQj-HTqxxYfx0ghuWSKbcqHQ7_OAEy4pcPm
392
    [RegularFileID] => 2
393
    [EncryptedFileID] => 3
394
    [MyNumberValue] =>
395
    [MyNumberBlindIndex] =>
396
    [MyNumberLastFourBlindIndex] =>
397
    [MyIndexedVarcharValue] =>
398
    [MyIndexedVarcharBlindIndex] => 04bb6edd
399
)
400
*/
401
        $this->assertNotEquals($varcharValue, $dbRecord['MyVarchar']);
402
        $this->assertNotEmpty($dbRecord['MyVarchar']);
403
        $this->assertTrue(EncryptHelper::isEncrypted($dbRecord['MyVarchar']));
404
    }
405
406
    public function testFixture2()
407
    {
408
        // this one has only brng encryption
409
        $model = $this->getTestModel2();
410
411
        $result = $model->needsToRotateEncryption(EncryptHelper::getCipherSweet());
412
        $this->assertFalse($result);
413
414
        // Ensure we have our blind indexes
415
        $this->assertTrue($model->hasDatabaseField('MyIndexedVarcharValue'));
416
        $this->assertTrue($model->hasDatabaseField('MyIndexedVarcharBlindIndex'));
417
        $this->assertTrue($model->hasDatabaseField('MyNumberValue'));
418
        $this->assertTrue($model->hasDatabaseField('MyNumberBlindIndex'));
419
        $this->assertTrue($model->hasDatabaseField('MyNumberLastFourBlindIndex'));
420
421
        if (class_uses($model, HasEncryptedFields::class)) {
0 ignored issues
show
Bug introduced by
LeKoala\Encrypt\HasEncryptedFields::class of type string is incompatible with the type boolean expected by parameter $autoload of class_uses(). ( Ignorable by Annotation )

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

421
        if (class_uses($model, /** @scrutinizer ignore-type */ HasEncryptedFields::class)) {
Loading history...
422
            $this->assertTrue($model->hasEncryptedField('MyVarchar'));
423
            $this->assertTrue($model->hasEncryptedField('MyIndexedVarchar'));
424
        }
425
426
427
        // print_r($model);
428
        /*
429
        [record:protected] => Array
430
        (
431
            [ClassName] => LeKoala\Encrypt\Test\Test_EncryptedModel
432
            [LastEdited] => 2021-07-07 13:38:48
433
            [Created] => 2021-07-07 13:38:48
434
            [Name] => demo2
435
            [MyText] => brng:XLzehy47IgENco4DcZj75u9D2p53UjDMCmTFGPNdmzYYxVVbDsaVWuZP1dTvIDaYagVggNAxT8S9fUTXw55VyIv6OxYJrQ==
436
            [MyHTMLText] => brng:bJ-6iGa-gjl9M6-UaNvtSrRuFLwDTLC6SIekrPHTcN_nmIUaK_VEFNAGVd3q__siNsvVXLreSlunpSyJ4JmF8eyI12ltz_s-eV6WVXw=
437
            [MyVarchar] => brng:qNEVUW3TS6eACSS4v1_NK0FOiG5JnbihmOHR1DU4L8Pt63OXQIJr_Kpd34J1IHaJXZWt4uuk2SZgskmvf8FrfApag_sRypca87MegXg_wQ==
438
            [RegularFileID] => 0
439
            [EncryptedFileID] => 0
440
            [MyNumberValue] => brng:pKYd8mXDduwhudwWeoE_ByO6IkvVlykVa6h09DTYFdHcb52yA1R5yhTEqQQjz1ndADFRa9WLLM3_e1U8PfPTiP4E
441
            [MyNumberBlindIndex] => a1de44f9
442
            [MyNumberLastFourBlindIndex] => addb
443
            [MyIndexedVarcharValue] => brng:TBD63tu-P9PluzI_zKTZ17P-4bhFvhbW7eOeSOOnDEf7n3Ytv2_52rlvGTVSJeWr5f6Z5eqrxi-RL5B6V0PrUmEqhfE2TGt-IdH5hfU=
444
            [MyIndexedVarcharBlindIndex] => 216d113a
445
            [ID] => 2
446
            [RecordClassName] => LeKoala\Encrypt\Test\Test_EncryptedModel
447
        )
448
        */
449
450
        $varcharValue = 'encrypted varchar value';
451
        $varcharWithIndexValue = 'some_searchable_value';
452
        // regular fields are not affected
453
        $this->assertEquals('demo2', $model->Name);
454
455
        // get value
456
        $this->assertEquals($varcharValue, $model->dbObject('MyVarchar')->getValue());
457
        // encrypted fields work transparently when using trait
458
        $this->assertEquals($varcharValue, $model->MyVarchar);
459
460
461
        $this->assertTrue($model->dbObject('MyIndexedVarchar') instanceof EncryptedDBField);
462
        $this->assertTrue($model->dbObject('MyIndexedVarchar')->hasField('Value'));
463
464
        $model->MyIndexedVarchar = $varcharWithIndexValue;
0 ignored issues
show
Bug Best Practice introduced by
The property MyIndexedVarchar does not exist on LeKoala\Encrypt\Test\Test_EncryptedModel. Since you implemented __set, consider adding a @property annotation.
Loading history...
465
        $model->write();
466
        $this->assertEquals($varcharWithIndexValue, $model->MyIndexedVarchar);
0 ignored issues
show
Bug Best Practice introduced by
The property MyIndexedVarchar does not exist on LeKoala\Encrypt\Test\Test_EncryptedModel. Since you implemented __get, consider adding a @property annotation.
Loading history...
467
468
        $dbRecord = $this->fetchRawData(get_class($model), $model->ID);
469
        // print_r($dbRecord);
470
        /*
471
        Array
472
(
473
    [ID] => 2
474
    [ClassName] => LeKoala\Encrypt\Test\Test_EncryptedModel
475
    [LastEdited] => 2021-07-07 13:52:10
476
    [Created] => 2021-07-07 13:52:08
477
    [Name] => demo2
478
    [MyText] => brng:IQ-6VoXJedlAGdoCPFUVTSnipUPR4k9YSi3Ik8_oPfUmMVDhA1kgTBFdG_6k08xLhD39G0ksVD_nMtUF4Opo6Zxgkc5Qww==
479
    [MyHTMLText] => brng:ATmS8Tooc0j2FN5zB8ojmhgNHD-vncvm1ljX8aF7rR6bbsD8pEwyX7BJ3mPg6WEzwyye4uriGskFy30GL9LEKsGs1hs40JJgs6rgwKA=
480
    [MyVarchar] => brng:zxu2RFNjqDGV0JmxF1WPMtxDKTyfOtvVztXfbnV3aYJAzro7RwHhSs8HhasHvdPOQ2Vxi_oDieRgcE8XeP3nyoF3tYJrJp3Mo9XdYXj2tw==
481
    [RegularFileID] => 0
482
    [EncryptedFileID] => 0
483
    [MyNumberValue] => brng:pKYd8mXDduwhudwWeoE_ByO6IkvVlykVa6h09DTYFdHcb52yA1R5yhTEqQQjz1ndADFRa9WLLM3_e1U8PfPTiP4E
484
    [MyNumberBlindIndex] => a1de44f9
485
    [MyNumberLastFourBlindIndex] => addb
486
    [MyIndexedVarcharValue] => brng:0ow_r7UD3FXYXxq7kjVzA3uY1ThFYfAWxZFAHA0aRoohLfQW_ZBa0Q8w5A3hyLJhT6djM6xR43O_jeEfP-w_fRaH3nXRI5RW7tO78JY=
487
    [MyIndexedVarcharBlindIndex] => 216d113a
488
)
489
*/
490
        $this->assertNotEquals($varcharValue, $dbRecord['MyVarchar']);
491
        $this->assertNotEmpty($dbRecord['MyVarchar']);
492
        $this->assertTrue(EncryptHelper::isEncrypted($dbRecord['MyVarchar']));
493
    }
494
495
    public function testRecordIsEncrypted()
496
    {
497
        $model = new Test_EncryptedModel();
498
499
        // echo "*** start \n";
500
        // Let's write some stuff
501
        $someText = 'some text';
502
        $model->MyText = $someText . ' text';
503
        $model->MyHTMLText = '<p>' . $someText . ' html</p>';
504
        $model->MyVarchar = 'encrypted varchar value';
505
        $model->MyIndexedVarchar = "some_searchable_value";
0 ignored issues
show
Bug Best Practice introduced by
The property MyIndexedVarchar does not exist on LeKoala\Encrypt\Test\Test_EncryptedModel. Since you implemented __set, consider adding a @property annotation.
Loading history...
506
        $model->MyNumber = "0123456789";
507
        // All fields are marked as changed, including "hidden" fields
508
        // MyNumber will mark as changed MyNumber, MyNumberValue, MuNumberBlindIndex, MyNumberLastFourBlindIndex
509
        // echo '<pre>';
510
        // print_r(array_keys($model->getChangedFields()));
511
        // die();
512
        $id = $model->write();
513
514
        $this->assertNotEmpty($id);
515
516
        // For the model, its the same
517
        $this->assertEquals($someText . ' text', $model->MyText);
518
        $this->assertEquals($someText . ' text', $model->dbObject('MyText')->getValue());
519
        $this->assertEquals($someText . ' text', $model->getField('MyText'));
520
        $this->assertEquals('<p>' . $someText . ' html</p>', $model->MyHTMLText);
521
522
        // In the db, it's not the same
523
        $dbRecord = $this->fetchRawData(get_class($model), $model->ID);
524
525
        if (!EncryptHelper::isEncrypted($dbRecord['MyIndexedVarcharValue'])) {
526
            print_r($dbRecord);
527
        }
528
529
        /*
530
(
531
    [ID] => 2
532
    [ClassName] => LeKoala\Encrypt\Test\Test_EncryptedModel
533
    [LastEdited] => 2020-12-15 10:20:39
534
    [Created] => 2020-12-15 10:20:39
535
    [Name] =>
536
    [MyText] => nacl:yA3XhjUpxE6cS3VMOVI4eqpolP1vRZDYjFySULZiazi9V3HSugC3t8KgImnGV5jP1VzEytVX
537
    [MyHTMLText] => nacl:F3D33dZ2O7qtlmkX-fiaYwSjAo6RC03aiAWRTkfSJOZikcSfezjwmi9DPJ4EO0hYeVc9faRgA3RmTDajRA==
538
    [MyVarchar] => nacl:POmdt3mTUSgPJw3ttfi2G9HgHAE4FRX4FQ5CSBicj4JsEwyPwrP-JKYGcs5drFYLId3cMVf6m8daUY7Ao4Cz
539
    [RegularFileID] => 0
540
    [EncryptedFileID] => 0
541
    [MyNumberValue] => nacl:2wFOX_qahm-HmzQPXvcBFhWCG1TaGQgeM7vkebLxRXDfMpzAxhxkExVgBi8caPYrwvA=
542
    [MyNumberBlindIndex] => 5e0bd888
543
    [MyNumberLastFourBlindIndex] => 276b
544
    [MyIndexedVarcharValue] => nacl:BLi-zF02t0Zet-ADP3RT8v5RTsM11WKIyjlJ1EVHIai2HwjxCIq92gfsay5zqiLic14dXtwigb1kI179QQ==
545
    [MyIndexedVarcharBlindIndex] => 04bb6edd
546
)
547
        */
548
        $text = isset($dbRecord['MyText']) ? $dbRecord['MyText'] : null;
549
        $this->assertNotEmpty($text);
550
        $this->assertNotEquals($someText, $text, "Data is not encrypted in the database");
551
        // Composite fields should work as well
552
        $this->assertNotEmpty($dbRecord['MyIndexedVarcharValue']);
553
        $this->assertNotEmpty($dbRecord['MyIndexedVarcharBlindIndex']);
554
555
        // Test save into
556
        $modelFieldsBefore = $model->getQueriedDatabaseFields();
557
        $model->MyIndexedVarchar = 'new_value';
558
        $dbObj = $model->dbObject('MyIndexedVarchar');
559
        // $dbObj->setValue('new_value', $model);
560
        // $dbObj->saveInto($model);
561
        $modelFields = $model->getQueriedDatabaseFields();
562
        // print_r($modelFields);
563
        $this->assertTrue($dbObj->isChanged());
564
        $changed = implode(", ", array_keys($model->getChangedFields()));
565
        $this->assertNotEquals($modelFieldsBefore['MyIndexedVarchar'], $modelFields['MyIndexedVarchar'], "It should not have the same value internally anymore");
566
        $this->assertTrue($model->isChanged('MyIndexedVarchar'), "Field is not properly marked as changed, only have : " . $changed);
567
        $this->assertEquals('new_value', $dbObj->getValue());
568
        $this->assertNotEquals('new_value', $modelFields['MyIndexedVarcharValue'], "Unencrypted value is not set on value field");
569
570
        // Somehow this is not working on travis? composite fields don't save encrypted data although it works locally
571
        $this->assertNotEquals("some_searchable_value", $dbRecord['MyIndexedVarcharValue'], "Data is not encrypted in the database");
572
573
        // if we load again ?
574
        // it should work thanks to our trait
575
        // by default, data will be loaded encrypted if we don't use the trait and call getField directly
576
        $model2 = $model::get()->byID($model->ID);
577
        $this->assertEquals($someText . ' text', $model2->MyText, "Data does not load properly");
578
        $this->assertEquals('<p>' . $someText . ' html</p>', $model2->MyHTMLText, "Data does not load properly");
579
    }
580
581
    public function testFileEncryption()
582
    {
583
        $regularFile = $this->getRegularFile();
584
        $encryptedFile = $this->getEncryptedFile();
585
586
        $this->assertEquals(0, $regularFile->Encrypted);
587
        $this->assertEquals(1, $encryptedFile->Encrypted);
588
589
        // test encryption
590
591
        $string = 'Some content';
592
593
        $stream = fopen('php://memory', 'r+');
594
        fwrite($stream, $string);
595
        rewind($stream);
596
597
        $encryptedFile->setFromStream($stream, 'secret.doc');
598
        $encryptedFile->write();
599
600
        $this->assertFalse($encryptedFile->isEncrypted());
601
602
        $encryptedFile->encryptFileIfNeeded();
603
604
        $this->assertTrue($encryptedFile->isEncrypted());
605
    }
606
607
    /**
608
     * @group multi-tenant
609
     */
610
    public function testMultiTenantProvider()
611
    {
612
        $admin = $this->getAdminMember();
613
        $user1 = $this->getUser1Member();
614
        $user2 = $this->getUser2Member();
615
        $members = $this->getAllMembers();
616
        $tenants = [];
617
        foreach ($members as $member) {
618
            $key = Test_EncryptionKey::getForMember($member->ID);
619
            if ($key) {
620
                $tenants[$member->ID] = new StringProvider($key);
621
            }
622
        }
623
        $adminModel = $this->getAdminTestModel();
624
        $user1Model = $this->getUser1TestModel();
625
        $user2Model = $this->getUser2TestModel();
626
627
        Security::setCurrentUser($admin);
628
        $provider = new MemberKeyProvider($tenants);
629
630
        EncryptHelper::clearCipherSweet();
631
        $cs = EncryptHelper::getCipherSweet($provider);
632
633
        $this->assertInstanceOf(MultiTenantSafeBackendInterface::class, $cs->getBackend());
634
635
        $string = "my content";
636
        $record = new Test_EncryptedModel();
637
        $record->MyText = $string;
638
        // We need to set active tenant ourselves because orm records fields one by one
639
        // it doesn't go through injectMetadata
640
        $record->MemberID = Security::getCurrentUser()->ID ?? 0;
0 ignored issues
show
Bug Best Practice introduced by
The property MemberID does not exist on LeKoala\Encrypt\Test\Test_EncryptedModel. Since you implemented __set, consider adding a @property annotation.
Loading history...
641
        $record->write();
642
643
        // echo '<pre>';
644
        // print_r($this->fetchRawData(Test_EncryptedModel::class, $record->ID));
645
        // die();
646
647
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $record->ID)->first();
648
649
        // He can decode
650
        $this->assertEquals($string, $freshRecord->MyText);
651
652
        // He can also decode his content from the db
653
        $adminRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
654
        $this->assertEquals($string, $adminRecord->MyText);
655
656
        // He cannot decode
657
        Security::setCurrentUser($user1);
658
        // We don't need to set active tenant because our MemberKeyProvider reads currentUser automatically
659
        // $provider->setActiveTenant($user1->ID);
660
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $record->ID)->first();
661
        $this->assertNotEquals($string, $freshRecord->MyText);
662
663
        // Test tenant from row
664
        $this->assertEquals($admin->ID, $cs->getTenantFromRow($adminModel->toMap()));
665
        $this->assertEquals($user1->ID, $cs->getTenantFromRow($user1Model->toMap()));
666
        $this->assertEquals($user2->ID, $cs->getTenantFromRow($user2Model->toMap()));
667
668
        // Current user can decode what he can
669
        Security::setCurrentUser($admin);
670
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
671
        $this->assertEquals($string, $freshRecord->MyText, "Invalid content for admin model #{$adminModel->ID}");
672
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $user2Model->ID)->first();
673
        $this->assertNotEquals($string, $freshRecord->MyText, "Invalid content for user2 model #{$user2Model->ID}");
674
675
        // Thanks to getTenantFromRow we should be able to rotate encryption
676
        // rotate from admin to user2
677
        Security::setCurrentUser($user2);
678
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
679
        $freshRecord->MemberID = $user2->ID;
680
        $freshRecord->write();
681
        $this->assertNotEquals($string, $freshRecord->MyText);
682
        // We can keep the same provider but we need to clone it and change the active tenant
683
        $cs->setActiveTenant($user2->ID);
684
685
        // clone will not deep clone the key provider with the active tenant
686
        // $old = clone $cs;
687
        $clonedProvider = clone $provider;
688
        $clonedProvider->setForcedTenant($admin->ID);
689
        $old = EncryptHelper::getEngineWithProvider(EncryptHelper::getBackendForEncryption("brng"), $clonedProvider);
690
691
        $freshRecord->rotateEncryption($old);
692
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
693
        $this->assertEquals($string, $freshRecord->MyText);
694
695
        // Admin can't read anymore, don't forget to refresh record from db
696
        Security::setCurrentUser($admin);
697
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
698
        $this->assertNotEquals($string, $freshRecord->MyText);
699
700
        // Cleanup
701
        EncryptHelper::clearCipherSweet();
702
    }
703
}
704