Passed
Push — master ( 67655f...62dbcb )
by Thomas
03:50
created

EncryptTest::getAdminTestModel()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 0
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 ParagonIE\ConstantTime\Hex;
10
use SilverStripe\ORM\ArrayList;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\Security\Member;
13
use LeKoala\Encrypt\EncryptHelper;
14
use SilverStripe\Core\Environment;
15
use SilverStripe\Dev\SapphireTest;
16
use Symfony\Component\Yaml\Parser;
17
use SilverStripe\Security\Security;
18
use LeKoala\Encrypt\EncryptedDBField;
19
use LeKoala\Encrypt\MemberKeyProvider;
20
use ParagonIE\CipherSweet\CipherSweet;
21
use LeKoala\Encrypt\HasEncryptedFields;
22
use SilverStripe\ORM\Queries\SQLSelect;
23
use SilverStripe\ORM\Queries\SQLUpdate;
24
use ParagonIE\CipherSweet\KeyProvider\StringProvider;
25
use ParagonIE\CipherSweet\Contract\MultiTenantSafeBackendInterface;
26
27
/**
28
 * Test for Encrypt
29
 *;
30
 * Run with the following command : ./vendor/bin/phpunit ./encrypt/tests/EncryptTest.php
31
 *
32
 * You may need to run:
33
 * php ./framework/cli-script.php dev/build ?flush=all
34
 * before (remember manifest for cli is not the same...)
35
 *
36
 * @group Encrypt
37
 */
38
class EncryptTest extends SapphireTest
39
{
40
    /**
41
     * Defines the fixture file to use for this test class
42
     * @var string
43
     */
44
    protected static $fixture_file = 'EncryptTest.yml';
45
46
    protected static $extra_dataobjects = [
47
        Test_EncryptedModel::class,
48
        Test_EncryptionKey::class,
49
    ];
50
51
    public function setUp()
52
    {
53
        // We need to disable automatic decryption to avoid fixtures being re encrypted with the wrong keys
54
        EncryptHelper::setAutomaticDecryption(false);
55
        Environment::setEnv('ENCRYPTION_KEY', '502370dfc69fd6179e1911707e8a5fb798c915900655dea16370d64404be04e5');
56
        Environment::setEnv('OLD_ENCRYPTION_KEY', '502370dfc69fd6179e1911707e8a5fb798c915900655dea16370d64404be04e4');
57
        parent::setUp();
58
        EncryptHelper::setAutomaticDecryption(true);
59
60
        // test extension is available
61
        if (!extension_loaded('sodium')) {
62
            throw new Exception("You must load sodium extension for this");
63
        }
64
65
        // Generate our test data from scratch
66
        // Use some old engine first
67
        // $this->generateData();
68
69
        // $this->showRowsFromDb();
70
        // $this->writeDataFromYml();
71
    }
72
73
    protected function generateData()
74
    {
75
        EncryptHelper::clearCipherSweet();
76
        EncryptHelper::setForcedEncryption("nacl");
77
        $someText = 'some text';
78
        $data = [
79
            'MyText' => $someText . ' text',
80
            'MyHTMLText' => '<p>' . $someText . ' html</p>',
81
            'MyVarchar' => 'encrypted varchar value',
82
            'MyIndexedVarchar' => "some_searchable_value",
83
            'MyNumber' => "0123456789",
84
        ];
85
        $record = Test_EncryptedModel::get()->filter('Name', 'demo')->first();
86
        foreach ($data as $k => $v) {
87
            $record->$k = $v;
88
        }
89
        $record->write();
90
        EncryptHelper::clearCipherSweet();
91
        $record = Test_EncryptedModel::get()->filter('Name', 'demo3')->first();
92
        foreach ($data as $k => $v) {
93
            $record->$k = $v;
94
        }
95
        $record->write();
96
        // use regular engine
97
        EncryptHelper::clearCipherSweet();
98
        EncryptHelper::setForcedEncryption(null);
99
        $record = Test_EncryptedModel::get()->filter('Name', 'demo2')->first();
100
        foreach ($data as $k => $v) {
101
            $record->$k = $v;
102
        }
103
        $record->write();
104
    }
105
106
    protected function showRowsFromDb()
107
    {
108
        $result = DB::query("SELECT * FROM EncryptedModel");
109
        echo '<pre>' . "\n";
110
        // print_r(iterator_to_array($result));
111
        foreach ($result as $row) {
112
            $this->showAsYml($row);
113
        }
114
        die();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
115
    }
116
117
    protected function writeDataFromYml()
118
    {
119
        $ymlParser = new Parser;
120
        $ymlData = $ymlParser->parseFile(__DIR__ . '/EncryptTest.yml');
121
122
        foreach ($ymlData["LeKoala\\Encrypt\\Test\\Test_EncryptedModel"] as $name => $data) {
123
            unset($data['Member']);
124
            $update = new SQLUpdate("EncryptedModel", $data, ["Name" => $data['Name']]);
125
            $update->execute();
126
        }
127
    }
128
129
    protected function showAsYml($row)
130
    {
131
        $fields = [
132
            'Name', 'MyText', 'MyHTMLText', 'MyVarchar',
133
            'MyNumberValue', 'MyNumberBlindIndex', 'MyNumberLastFourBlindIndex',
134
            'MyIndexedVarcharValue', 'MyIndexedVarcharBlindIndex'
135
        ];
136
        echo "  " . $row['Name'] . ":\n";
137
        foreach ($row as $k => $v) {
138
            if (!in_array($k, $fields)) {
139
                continue;
140
            }
141
            echo "    $k: '$v'\n";
142
        }
143
    }
144
145
    public function tearDown()
146
    {
147
        parent::tearDown();
148
    }
149
150
    /**
151
     * @return Test_EncryptedModel
152
     */
153
    public function getTestModel()
154
    {
155
        return $this->objFromFixture(Test_EncryptedModel::class, 'demo');
156
    }
157
158
    /**
159
     * @return Test_EncryptedModel
160
     */
161
    public function getTestModel2()
162
    {
163
        return $this->objFromFixture(Test_EncryptedModel::class, 'demo2');
164
    }
165
166
    /**
167
     * @return Test_EncryptedModel
168
     */
169
    public function getTestModel3()
170
    {
171
        return $this->objFromFixture(Test_EncryptedModel::class, 'demo3');
172
    }
173
174
    /**
175
     * @return Test_EncryptedModel
176
     */
177
    public function getAdminTestModel()
178
    {
179
        return $this->objFromFixture(Test_EncryptedModel::class, 'admin_record');
180
    }
181
182
    /**
183
     * @return Test_EncryptedModel
184
     */
185
    public function getUser1TestModel()
186
    {
187
        return $this->objFromFixture(Test_EncryptedModel::class, 'user1_record');
188
    }
189
190
    /**
191
     * @return Test_EncryptedModel
192
     */
193
    public function getUser2TestModel()
194
    {
195
        return $this->objFromFixture(Test_EncryptedModel::class, 'user2_record');
196
    }
197
198
    /**
199
     * @return Member
200
     */
201
    public function getAdminMember()
202
    {
203
        return $this->objFromFixture(Member::class, 'admin');
204
    }
205
206
    /**
207
     * @return Member
208
     */
209
    public function getUser1Member()
210
    {
211
        return $this->objFromFixture(Member::class, 'user1');
212
    }
213
214
    /**
215
     * @return Member
216
     */
217
    public function getUser2Member()
218
    {
219
        return $this->objFromFixture(Member::class, 'user2');
220
    }
221
222
    /**
223
     * @return DataList|Member[]
224
     */
225
    public function getAllMembers()
226
    {
227
        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...
228
            $this->getAdminMember(),
229
            $this->getUser1Member(),
230
            $this->getUser2Member(),
231
        ]);
232
    }
233
234
    /**
235
     * @return File
236
     */
237
    public function getRegularFile()
238
    {
239
        return $this->objFromFixture(File::class, 'regular');
240
    }
241
242
    /**
243
     * @return File
244
     */
245
    public function getEncryptedFile()
246
    {
247
        return $this->objFromFixture(File::class, 'encrypted');
248
    }
249
250
    /**
251
     * @param string $class
252
     * @param int $id
253
     * @return array
254
     */
255
    protected function fetchRawData($class, $id)
256
    {
257
        $tableName = DataObject::getSchema()->tableName($class);
258
        $columnIdentifier = DataObject::getSchema()->sqlColumnForField($class, 'ID');
259
        $sql = new SQLSelect('*', [$tableName], [$columnIdentifier => $id]);
260
        $dbRecord = $sql->firstRow()->execute()->first();
261
        return $dbRecord;
262
    }
263
264
    public function testEncryption()
265
    {
266
        $someText = 'some text';
267
        $encrypt = EncryptHelper::encrypt($someText);
268
        $decryptedValue = EncryptHelper::decrypt($encrypt);
269
270
        $this->assertEquals($someText, $decryptedValue);
271
    }
272
273
    public function testIndexes()
274
    {
275
        $indexes = DataObject::getSchema()->databaseIndexes(Test_EncryptedModel::class);
276
        $keys = array_keys($indexes);
277
        $this->assertContains('MyIndexedVarcharBlindIndex', $keys, "Index is not defined in : " . implode(", ", $keys));
278
        $this->assertContains('MyNumberLastFourBlindIndex', $keys, "Index is not defined in : " . implode(", ", $keys));
279
    }
280
281
    public function testSearch()
282
    {
283
        $singl = singleton(Test_EncryptedModel::class);
284
285
        /** @var EncryptedDBField $obj  */
286
        $obj = $singl->dbObject('MyIndexedVarchar');
287
        $record = $obj->fetchRecord('some_searchable_value');
288
289
        // echo '<pre>';print_r("From test: " . $record->MyIndexedVarchar);die();
290
        $this->assertNotEmpty($record);
291
        $this->assertEquals("some text text", $record->MyText);
292
        $this->assertEquals("some_searchable_value", $record->MyIndexedVarchar);
293
        $this->assertEquals("some_searchable_value", $record->dbObject('MyIndexedVarchar')->getValue());
294
295
        // Also search our super getter method
296
        $recordAlt = Test_EncryptedModel::getByBlindIndex('MyIndexedVarchar', 'some_searchable_value');
297
        $this->assertNotEmpty($record);
298
        $this->assertEquals($recordAlt->ID, $record->ID);
299
300
        // Can we get a list ?
301
        $list = Test_EncryptedModel::getAllByBlindIndex('MyIndexedVarchar', 'some_searchable_value');
302
        $this->assertInstanceOf(DataList::class, $list);
303
304
        $record = $obj->fetchRecord('some_unset_value');
305
        $this->assertEmpty($record);
306
307
        // Let's try our four digits index
308
        $obj = $singl->dbObject('MyNumber');
309
        $record = $obj->fetchRecord('6789', 'LastFourBlindIndex');
310
        $searchValue = $obj->getSearchValue('6789', 'LastFourBlindIndex');
311
        // $searchParams = $obj->getSearchParams('6789', 'LastFourBlindIndex');
312
        // print_r($searchParams);
313
        $this->assertNotEmpty($record, "Nothing found for $searchValue");
314
        $this->assertEquals("0123456789", $record->MyNumber);
315
    }
316
317
    public function testSearchFilter()
318
    {
319
        $record = Test_EncryptedModel::get()->filter('MyIndexedVarchar:Encrypted', 'some_searchable_value')->first();
320
        $this->assertNotEmpty($record);
321
        $this->assertEquals(1, $record->ID);
322
        $this->assertNotEquals(2, $record->ID);
323
324
        $record = Test_EncryptedModel::get()->filter('MyIndexedVarchar:Encrypted', 'some_unset_value')->first();
325
        $this->assertEmpty($record);
326
    }
327
328
    public function testRotation()
329
    {
330
        $model = $this->getTestModel3();
331
        $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...
332
333
        $old = EncryptHelper::getEngineForEncryption("nacl");
334
        $result = $model->needsToRotateEncryption($old);
335
        $this->assertTrue($result);
336
337
        $result = $model->rotateEncryption($old);
338
        $this->assertTrue($result);
339
    }
340
341
    public function testCompositeOptions()
342
    {
343
        $model = $this->getTestModel();
344
345
        /** @var EncryptedDBField $myNumber */
346
        $myNumber = $model->dbObject('MyNumber');
347
348
        $this->assertEquals(10, $myNumber->getDomainSize());
349
        $this->assertEquals(4, $myNumber->getOutputSize());
350
        $this->assertEquals(EncryptedDBField::LARGE_INDEX_SIZE, $myNumber->getIndexSize());
351
352
        /** @var EncryptedDBField $MyIndexedVarchar */
353
        $MyIndexedVarchar = $model->dbObject('MyIndexedVarchar');
354
355
        // Default config values
356
        $this->assertEquals(EncryptHelper::DEFAULT_DOMAIN_SIZE, $MyIndexedVarchar->getDomainSize());
357
        $this->assertEquals(EncryptHelper::DEFAULT_OUTPUT_SIZE, $MyIndexedVarchar->getOutputSize());
358
        $this->assertEquals(EncryptedDBField::LARGE_INDEX_SIZE, $MyIndexedVarchar->getIndexSize());
359
    }
360
361
    public function testIndexPlanner()
362
    {
363
        $sizes = EncryptHelper::planIndexSizesForClass(Test_EncryptedModel::class);
364
        $this->assertNotEmpty($sizes);
365
        $this->assertArrayHasKey("min", $sizes);
366
        $this->assertArrayHasKey("max", $sizes);
367
        $this->assertArrayHasKey("indexes", $sizes);
368
        $this->assertArrayHasKey("estimated_population", $sizes);
369
        $this->assertArrayHasKey("coincidence_count", $sizes);
370
    }
371
372
    public function testFixture()
373
    {
374
        // this one use nacl encryption and will be rotated transparently
375
        $model = $this->getTestModel();
376
377
        $result = $model->needsToRotateEncryption(EncryptHelper::getEngineForEncryption("nacl"));
378
        $this->assertTrue($result);
379
380
        // Ensure we have our blind indexes
381
        $this->assertTrue($model->hasDatabaseField('MyIndexedVarcharValue'));
382
        $this->assertTrue($model->hasDatabaseField('MyIndexedVarcharBlindIndex'));
383
        $this->assertTrue($model->hasDatabaseField('MyNumberValue'));
384
        $this->assertTrue($model->hasDatabaseField('MyNumberBlindIndex'));
385
        $this->assertTrue($model->hasDatabaseField('MyNumberLastFourBlindIndex'));
386
387
        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

387
        if (class_uses($model, /** @scrutinizer ignore-type */ HasEncryptedFields::class)) {
Loading history...
388
            $this->assertTrue($model->hasEncryptedField('MyVarchar'));
389
            $this->assertTrue($model->hasEncryptedField('MyIndexedVarchar'));
390
        }
391
392
        // print_r($model);
393
        /*
394
         [record:protected] => Array
395
        (
396
            [ClassName] => LeKoala\Encrypt\Test\Test_EncryptedModel
397
            [LastEdited] => 2020-12-15 10:09:47
398
            [Created] => 2020-12-15 10:09:47
399
            [Name] => demo
400
            [MyText] => nacl:mQ1g5ugjYSWjFd-erM6-xlB_EbWp1bOAUPbL4fa3Ce5SX6LP7sFCczkFx_lRABvZioWJXx-L
401
            [MyHTMLText] => nacl:836In6YCaEf3_mRJR7NOC_s0P8gIFESgmPnHCefTe6ycY_6CLKVmT0_9KWHgnin-WGXMJawkS1hS87xwQw==
402
            [MyVarchar] => nacl:ZeOw8-dcBdFemtGm-MRJ5pCSipOtAO5-zBRms8F5Elex08GuoL_JKbdN-CiOP-u009MJfvGZUkx9Ru5Zn0_y
403
            [RegularFileID] => 2
404
            [EncryptedFileID] => 3
405
            [MyIndexedVarcharBlindIndex] => 04bb6edd
406
            [ID] => 1
407
            [RecordClassName] => LeKoala\Encrypt\Test\Test_EncryptedModel
408
        )
409
        */
410
411
        $varcharValue = 'encrypted varchar value';
412
        $varcharWithIndexValue = 'some_searchable_value';
413
        // regular fields are not affected
414
        $this->assertEquals('demo', $model->Name);
415
416
        // automatically rotated fields store an exception
417
        $this->assertNotEmpty($model->dbObject("MyVarchar")->getEncryptionException());
418
419
        // get value
420
        $this->assertEquals($varcharValue, $model->dbObject('MyVarchar')->getValue());
421
        // encrypted fields work transparently when using trait
422
        $this->assertEquals($varcharValue, $model->MyVarchar);
423
424
        // since dbobject cache can be cleared, exception is gone
425
        $this->assertEmpty($model->dbObject("MyVarchar")->getEncryptionException());
426
427
428
        $this->assertTrue($model->dbObject('MyIndexedVarchar') instanceof EncryptedDBField);
429
        $this->assertTrue($model->dbObject('MyIndexedVarchar')->hasField('Value'));
430
431
        $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...
432
        $model->write();
433
        $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...
434
435
        $dbRecord = $this->fetchRawData(get_class($model), $model->ID);
436
        // print_r($dbRecord);
437
        /*
438
        Array
439
(
440
    [ID] => 1
441
    [ClassName] => LeKoala\Encrypt\Test\Test_EncryptedModel
442
    [LastEdited] => 2020-12-15 10:10:27
443
    [Created] => 2020-12-15 10:10:27
444
    [Name] => demo
445
    [MyText] => nacl:aDplmA9hs7naqiPwWdNRMcYNUltf4mOs8KslRQZ4vCdnJylnbjAJYChtVH7wiiygsAHWqbM6
446
    [MyHTMLText] => nacl:dMvk5Miux0bsSP1SjaXQRlbGogNTu7UD3p6AlNHFMAEGXOQz03hkBx43C-WelCS0KUdAN9ewuwuXZqMmRA==
447
    [MyVarchar] => nacl:sZRenCG6En7Sg_HmsUHkNy_1MXOstly7eHm0i2iq83kTFH40UsQj-HTqxxYfx0ghuWSKbcqHQ7_OAEy4pcPm
448
    [RegularFileID] => 2
449
    [EncryptedFileID] => 3
450
    [MyNumberValue] =>
451
    [MyNumberBlindIndex] =>
452
    [MyNumberLastFourBlindIndex] =>
453
    [MyIndexedVarcharValue] =>
454
    [MyIndexedVarcharBlindIndex] => 04bb6edd
455
)
456
*/
457
        $this->assertNotEquals($varcharValue, $dbRecord['MyVarchar']);
458
        $this->assertNotEmpty($dbRecord['MyVarchar']);
459
        $this->assertTrue(EncryptHelper::isEncrypted($dbRecord['MyVarchar']));
460
    }
461
462
    public function testFixture2()
463
    {
464
        // this one has only brng encryption
465
        $model = $this->getTestModel2();
466
467
        $result = $model->needsToRotateEncryption(EncryptHelper::getCipherSweet());
468
        $this->assertFalse($result);
469
470
        // Ensure we have our blind indexes
471
        $this->assertTrue($model->hasDatabaseField('MyIndexedVarcharValue'));
472
        $this->assertTrue($model->hasDatabaseField('MyIndexedVarcharBlindIndex'));
473
        $this->assertTrue($model->hasDatabaseField('MyNumberValue'));
474
        $this->assertTrue($model->hasDatabaseField('MyNumberBlindIndex'));
475
        $this->assertTrue($model->hasDatabaseField('MyNumberLastFourBlindIndex'));
476
477
        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

477
        if (class_uses($model, /** @scrutinizer ignore-type */ HasEncryptedFields::class)) {
Loading history...
478
            $this->assertTrue($model->hasEncryptedField('MyVarchar'));
479
            $this->assertTrue($model->hasEncryptedField('MyIndexedVarchar'));
480
        }
481
482
483
        // print_r($model);
484
        /*
485
        [record:protected] => Array
486
        (
487
            [ClassName] => LeKoala\Encrypt\Test\Test_EncryptedModel
488
            [LastEdited] => 2021-07-07 13:38:48
489
            [Created] => 2021-07-07 13:38:48
490
            [Name] => demo2
491
            [MyText] => brng:XLzehy47IgENco4DcZj75u9D2p53UjDMCmTFGPNdmzYYxVVbDsaVWuZP1dTvIDaYagVggNAxT8S9fUTXw55VyIv6OxYJrQ==
492
            [MyHTMLText] => brng:bJ-6iGa-gjl9M6-UaNvtSrRuFLwDTLC6SIekrPHTcN_nmIUaK_VEFNAGVd3q__siNsvVXLreSlunpSyJ4JmF8eyI12ltz_s-eV6WVXw=
493
            [MyVarchar] => brng:qNEVUW3TS6eACSS4v1_NK0FOiG5JnbihmOHR1DU4L8Pt63OXQIJr_Kpd34J1IHaJXZWt4uuk2SZgskmvf8FrfApag_sRypca87MegXg_wQ==
494
            [RegularFileID] => 0
495
            [EncryptedFileID] => 0
496
            [MyNumberValue] => brng:pKYd8mXDduwhudwWeoE_ByO6IkvVlykVa6h09DTYFdHcb52yA1R5yhTEqQQjz1ndADFRa9WLLM3_e1U8PfPTiP4E
497
            [MyNumberBlindIndex] => a1de44f9
498
            [MyNumberLastFourBlindIndex] => addb
499
            [MyIndexedVarcharValue] => brng:TBD63tu-P9PluzI_zKTZ17P-4bhFvhbW7eOeSOOnDEf7n3Ytv2_52rlvGTVSJeWr5f6Z5eqrxi-RL5B6V0PrUmEqhfE2TGt-IdH5hfU=
500
            [MyIndexedVarcharBlindIndex] => 216d113a
501
            [ID] => 2
502
            [RecordClassName] => LeKoala\Encrypt\Test\Test_EncryptedModel
503
        )
504
        */
505
506
        $varcharValue = 'encrypted varchar value';
507
        $varcharWithIndexValue = 'some_searchable_value';
508
        // regular fields are not affected
509
        $this->assertEquals('demo2', $model->Name);
510
511
        // get value
512
        $this->assertEquals($varcharValue, $model->dbObject('MyVarchar')->getValue());
513
        // encrypted fields work transparently when using trait
514
        $this->assertEquals($varcharValue, $model->MyVarchar);
515
516
517
        $this->assertTrue($model->dbObject('MyIndexedVarchar') instanceof EncryptedDBField);
518
        $this->assertTrue($model->dbObject('MyIndexedVarchar')->hasField('Value'));
519
520
        $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...
521
        $model->write();
522
        $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...
523
524
        $dbRecord = $this->fetchRawData(get_class($model), $model->ID);
525
        // print_r($dbRecord);
526
        /*
527
        Array
528
(
529
    [ID] => 2
530
    [ClassName] => LeKoala\Encrypt\Test\Test_EncryptedModel
531
    [LastEdited] => 2021-07-07 13:52:10
532
    [Created] => 2021-07-07 13:52:08
533
    [Name] => demo2
534
    [MyText] => brng:IQ-6VoXJedlAGdoCPFUVTSnipUPR4k9YSi3Ik8_oPfUmMVDhA1kgTBFdG_6k08xLhD39G0ksVD_nMtUF4Opo6Zxgkc5Qww==
535
    [MyHTMLText] => brng:ATmS8Tooc0j2FN5zB8ojmhgNHD-vncvm1ljX8aF7rR6bbsD8pEwyX7BJ3mPg6WEzwyye4uriGskFy30GL9LEKsGs1hs40JJgs6rgwKA=
536
    [MyVarchar] => brng:zxu2RFNjqDGV0JmxF1WPMtxDKTyfOtvVztXfbnV3aYJAzro7RwHhSs8HhasHvdPOQ2Vxi_oDieRgcE8XeP3nyoF3tYJrJp3Mo9XdYXj2tw==
537
    [RegularFileID] => 0
538
    [EncryptedFileID] => 0
539
    [MyNumberValue] => brng:pKYd8mXDduwhudwWeoE_ByO6IkvVlykVa6h09DTYFdHcb52yA1R5yhTEqQQjz1ndADFRa9WLLM3_e1U8PfPTiP4E
540
    [MyNumberBlindIndex] => a1de44f9
541
    [MyNumberLastFourBlindIndex] => addb
542
    [MyIndexedVarcharValue] => brng:0ow_r7UD3FXYXxq7kjVzA3uY1ThFYfAWxZFAHA0aRoohLfQW_ZBa0Q8w5A3hyLJhT6djM6xR43O_jeEfP-w_fRaH3nXRI5RW7tO78JY=
543
    [MyIndexedVarcharBlindIndex] => 216d113a
544
)
545
*/
546
        $this->assertNotEquals($varcharValue, $dbRecord['MyVarchar']);
547
        $this->assertNotEmpty($dbRecord['MyVarchar']);
548
        $this->assertTrue(EncryptHelper::isEncrypted($dbRecord['MyVarchar']));
549
    }
550
551
    public function testRecordIsEncrypted()
552
    {
553
        $model = new Test_EncryptedModel();
554
555
        // echo "*** start \n";
556
        // Let's write some stuff
557
        $someText = 'some text';
558
        $model->MyText = $someText . ' text';
559
        $model->MyHTMLText = '<p>' . $someText . ' html</p>';
560
        $model->MyVarchar = 'encrypted varchar value';
561
        $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...
562
        $model->MyNumber = "0123456789";
563
        // All fields are marked as changed, including "hidden" fields
564
        // MyNumber will mark as changed MyNumber, MyNumberValue, MuNumberBlindIndex, MyNumberLastFourBlindIndex
565
        // echo '<pre>';
566
        // print_r(array_keys($model->getChangedFields()));
567
        // die();
568
        $id = $model->write();
569
570
        $this->assertNotEmpty($id);
571
572
        // For the model, its the same
573
        $this->assertEquals($someText . ' text', $model->MyText);
574
        $this->assertEquals($someText . ' text', $model->dbObject('MyText')->getValue());
575
        $this->assertEquals($someText . ' text', $model->getField('MyText'));
576
        $this->assertEquals('<p>' . $someText . ' html</p>', $model->MyHTMLText);
577
578
        // In the db, it's not the same
579
        $dbRecord = $this->fetchRawData(get_class($model), $model->ID);
580
581
        if (!EncryptHelper::isEncrypted($dbRecord['MyIndexedVarcharValue'])) {
582
            print_r($dbRecord);
583
        }
584
585
        /*
586
(
587
    [ID] => 2
588
    [ClassName] => LeKoala\Encrypt\Test\Test_EncryptedModel
589
    [LastEdited] => 2020-12-15 10:20:39
590
    [Created] => 2020-12-15 10:20:39
591
    [Name] =>
592
    [MyText] => nacl:yA3XhjUpxE6cS3VMOVI4eqpolP1vRZDYjFySULZiazi9V3HSugC3t8KgImnGV5jP1VzEytVX
593
    [MyHTMLText] => nacl:F3D33dZ2O7qtlmkX-fiaYwSjAo6RC03aiAWRTkfSJOZikcSfezjwmi9DPJ4EO0hYeVc9faRgA3RmTDajRA==
594
    [MyVarchar] => nacl:POmdt3mTUSgPJw3ttfi2G9HgHAE4FRX4FQ5CSBicj4JsEwyPwrP-JKYGcs5drFYLId3cMVf6m8daUY7Ao4Cz
595
    [RegularFileID] => 0
596
    [EncryptedFileID] => 0
597
    [MyNumberValue] => nacl:2wFOX_qahm-HmzQPXvcBFhWCG1TaGQgeM7vkebLxRXDfMpzAxhxkExVgBi8caPYrwvA=
598
    [MyNumberBlindIndex] => 5e0bd888
599
    [MyNumberLastFourBlindIndex] => 276b
600
    [MyIndexedVarcharValue] => nacl:BLi-zF02t0Zet-ADP3RT8v5RTsM11WKIyjlJ1EVHIai2HwjxCIq92gfsay5zqiLic14dXtwigb1kI179QQ==
601
    [MyIndexedVarcharBlindIndex] => 04bb6edd
602
)
603
        */
604
        $text = isset($dbRecord['MyText']) ? $dbRecord['MyText'] : null;
605
        $this->assertNotEmpty($text);
606
        $this->assertNotEquals($someText, $text, "Data is not encrypted in the database");
607
        // Composite fields should work as well
608
        $this->assertNotEmpty($dbRecord['MyIndexedVarcharValue']);
609
        $this->assertNotEmpty($dbRecord['MyIndexedVarcharBlindIndex']);
610
611
        // Test save into
612
        $modelFieldsBefore = $model->getQueriedDatabaseFields();
613
        $model->MyIndexedVarchar = 'new_value';
614
        $dbObj = $model->dbObject('MyIndexedVarchar');
615
        // $dbObj->setValue('new_value', $model);
616
        // $dbObj->saveInto($model);
617
        $modelFields = $model->getQueriedDatabaseFields();
618
        // print_r($modelFields);
619
        $this->assertTrue($dbObj->isChanged());
620
        $changed = implode(", ", array_keys($model->getChangedFields()));
621
        $this->assertNotEquals($modelFieldsBefore['MyIndexedVarchar'], $modelFields['MyIndexedVarchar'], "It should not have the same value internally anymore");
622
        $this->assertTrue($model->isChanged('MyIndexedVarchar'), "Field is not properly marked as changed, only have : " . $changed);
623
        $this->assertEquals('new_value', $dbObj->getValue());
624
        $this->assertNotEquals('new_value', $modelFields['MyIndexedVarcharValue'], "Unencrypted value is not set on value field");
625
626
        // Somehow this is not working on travis? composite fields don't save encrypted data although it works locally
627
        $this->assertNotEquals("some_searchable_value", $dbRecord['MyIndexedVarcharValue'], "Data is not encrypted in the database");
628
629
        // if we load again ?
630
        // it should work thanks to our trait
631
        // by default, data will be loaded encrypted if we don't use the trait and call getField directly
632
        $model2 = $model::get()->byID($model->ID);
633
        $this->assertEquals($someText . ' text', $model2->MyText, "Data does not load properly");
634
        $this->assertEquals('<p>' . $someText . ' html</p>', $model2->MyHTMLText, "Data does not load properly");
635
    }
636
637
    public function testFileEncryption()
638
    {
639
        $regularFile = $this->getRegularFile();
640
        $encryptedFile = $this->getEncryptedFile();
641
642
        $this->assertEquals(0, $regularFile->Encrypted);
643
        $this->assertEquals(1, $encryptedFile->Encrypted);
644
645
        // test encryption
646
647
        $string = 'Some content';
648
649
        $stream = fopen('php://memory', 'r+');
650
        fwrite($stream, $string);
651
        rewind($stream);
652
653
        $encryptedFile->setFromStream($stream, 'secret.doc');
654
        $encryptedFile->write();
655
656
        $this->assertFalse($encryptedFile->isEncrypted());
657
658
        $encryptedFile->encryptFileIfNeeded();
659
660
        $this->assertTrue($encryptedFile->isEncrypted());
661
    }
662
663
    /**
664
     * @group only
665
     */
666
    public function testMessageEncryption()
667
    {
668
        $admin = $this->getAdminMember();
669
        $user1 = $this->getUser1Member();
670
671
        $adminKeys = Test_EncryptionKey::getKeyPair($admin->ID);
672
        $user1Keys = Test_EncryptionKey::getKeyPair($user1->ID);
673
674
        $this->assertArrayHasKey("public", $adminKeys);
0 ignored issues
show
Bug introduced by
It seems like $adminKeys can also be of type false; however, parameter $array of PHPUnit_Framework_Assert::assertArrayHasKey() does only seem to accept ArrayAccess|array, maybe add an additional type check? ( Ignorable by Annotation )

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

674
        $this->assertArrayHasKey("public", /** @scrutinizer ignore-type */ $adminKeys);
Loading history...
675
        $this->assertArrayHasKey("secret", $adminKeys);
676
        $this->assertArrayHasKey("public", $user1Keys);
677
        $this->assertArrayHasKey("secret", $user1Keys);
678
679
        // $pairs = sodium_crypto_box_keypair();
680
        // $adminKeys['secret'] = sodium_crypto_box_secretkey($pairs);
681
        // $adminKeys['public'] = sodium_crypto_box_publickey($pairs);
682
683
        // $pairs = sodium_crypto_box_keypair();
684
        // $user1Keys['secret'] = sodium_crypto_box_secretkey($pairs);
685
        // $user1Keys['public'] = sodium_crypto_box_publickey($pairs);
686
687
        // $adminKeys['secret'] = Hex::encode($adminKeys['secret']);
688
        // $adminKeys['public'] = Hex::encode($adminKeys['public']);
689
        // $user1Keys['secret'] = Hex::encode($user1Keys['secret']);
690
        // $user1Keys['public'] = Hex::encode($user1Keys['public']);
691
692
        // $adminKeys['secret'] = Hex::decode($adminKeys['secret']);
693
        // $adminKeys['public'] = Hex::decode($adminKeys['public']);
694
        // $user1Keys['secret'] = Hex::decode($user1Keys['secret']);
695
        // $user1Keys['public'] = Hex::decode($user1Keys['public']);
696
697
        $message = 'hello';
698
        // 24
699
        $nonce = random_bytes(SODIUM_CRYPTO_BOX_NONCEBYTES);
700
        $encryption_key = sodium_crypto_box_keypair_from_secretkey_and_publickey($adminKeys['secret'], $user1Keys['public']);
701
        $encrypted = sodium_crypto_box($message, $nonce, $encryption_key);
702
        $this->assertNotEmpty($encrypted);
703
        $this->assertNotEquals($message, $encrypted);
704
705
        // Revert keys to decrypt
706
        $decryption_key = sodium_crypto_box_keypair_from_secretkey_and_publickey($user1Keys['secret'], $adminKeys['public']);
707
        $decrypted = sodium_crypto_box_open($encrypted, $nonce, $decryption_key);
708
        $this->assertNotEmpty($decrypted);
709
        $this->assertEquals($message, $decrypted);
710
    }
711
712
    protected function getMultiTenantProvider()
713
    {
714
        $members = $this->getAllMembers();
715
        $tenants = [];
716
        foreach ($members as $member) {
717
            // You can also use the secret key from a keypair
718
            // $key = Test_EncryptionKey::getForMember($member->ID);
719
            $keyPair = Test_EncryptionKey::getKeyPair($member->ID);
720
            if ($keyPair) {
721
                $tenants[$member->ID] = new StringProvider($keyPair['secret']);
722
                // $tenants[$member->ID] = new StringProvider($key);
723
            }
724
        }
725
        $provider = new MemberKeyProvider($tenants);
726
        return $provider;
727
    }
728
729
    /**
730
     * @group multi-tenant
731
     * @group only
732
     */
733
    public function testMultiTenantProvider()
734
    {
735
        // echo '<pre>';
736
        // print_r(EncryptHelper::generateKeyPair());
737
        // die();
738
        $admin = $this->getAdminMember();
739
        $user1 = $this->getUser1Member();
740
        $user2 = $this->getUser2Member();
741
742
        $adminModel = $this->getAdminTestModel();
743
        $user1Model = $this->getUser1TestModel();
744
        $user2Model = $this->getUser2TestModel();
745
746
        $provider = $this->getMultiTenantProvider();
747
748
        Security::setCurrentUser($admin);
749
        EncryptHelper::clearCipherSweet();
750
        $cs = EncryptHelper::getCipherSweet($provider);
751
752
        $this->assertInstanceOf(MultiTenantSafeBackendInterface::class, $cs->getBackend());
753
754
        $string = "my content";
755
        $record = new Test_EncryptedModel();
756
        // $record = Test_EncryptedModel::get()->filter('ID', $user2Model->ID)->first();
757
        $record->MyText = $string;
758
        // We need to set active tenant ourselves because orm records fields one by one
759
        // it doesn't go through injectMetadata
760
        $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...
761
        $record->write();
762
763
        // echo '<pre>';
764
        // print_r($this->fetchRawData(Test_EncryptedModel::class, $record->ID));
765
        // die();
766
767
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $record->ID)->first();
768
769
        $this->assertEquals($admin->ID, Security::getCurrentUser()->ID, "Make sure the right member is logged in");
770
        // He can decode
771
        $this->assertEquals($string, $freshRecord->MyText);
772
773
        // He can also decode his content from the db
774
        $adminRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
775
        // echo '<pre>';print_r($adminRecord);die();
776
        $this->assertEquals($string, $adminRecord->MyText);
777
778
        // He cannot decode
779
        Security::setCurrentUser($user1);
780
        // We don't need to set active tenant because our MemberKeyProvider reads currentUser automatically
781
        // $provider->setActiveTenant($user1->ID);
782
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $record->ID)->first();
783
        $this->assertNotEquals($string, $freshRecord->MyText);
784
785
        // Test tenant from row
786
        $this->assertEquals($admin->ID, $cs->getTenantFromRow($adminModel->toMap()));
787
        $this->assertEquals($user1->ID, $cs->getTenantFromRow($user1Model->toMap()));
788
        $this->assertEquals($user2->ID, $cs->getTenantFromRow($user2Model->toMap()));
789
790
        // Current user can decode what he can
791
        Security::setCurrentUser($admin);
792
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
793
        $this->assertEquals($string, $freshRecord->MyText, "Invalid content for admin model #{$adminModel->ID}");
794
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $user2Model->ID)->first();
795
        $this->assertNotEquals($string, $freshRecord->MyText, "Invalid content for user2 model #{$user2Model->ID}");
796
797
        // Thanks to getTenantFromRow we should be able to rotate encryption
798
        // rotate from admin to user2
799
        Security::setCurrentUser($user2);
800
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
801
        $freshRecord->MemberID = $user2->ID;
802
        $freshRecord->write();
803
        $this->assertNotEquals($string, $freshRecord->MyText);
804
        // We can keep the same provider but we need to clone it and change the active tenant
805
        $cs->setActiveTenant($user2->ID);
806
807
        // clone will not deep clone the key provider with the active tenant
808
        // $old = clone $cs;
809
        $clonedProvider = clone $provider;
810
        $clonedProvider->setForcedTenant($admin->ID);
811
        $old = EncryptHelper::getEngineWithProvider(EncryptHelper::getBackendForEncryption("brng"), $clonedProvider);
812
813
        $freshRecord->rotateEncryption($old);
814
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
815
        $this->assertEquals($string, $freshRecord->MyText);
816
817
        // Admin can't read anymore, don't forget to refresh record from db
818
        Security::setCurrentUser($admin);
819
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
820
        $this->assertNotEquals($string, $freshRecord->MyText);
821
822
        // Cleanup
823
        EncryptHelper::clearCipherSweet();
824
    }
825
826
    public function testJsonField()
827
    {
828
        $model = $this->getTestModel();
829
830
        $longstring = str_repeat("lorem ipsum loquor", 100);
831
        $array = [];
832
        foreach (range(1, 100) as $i) {
833
            $array["key_$i"] = $longstring . $i;
834
        }
835
836
        $model->MyJson = $array;
0 ignored issues
show
Documentation Bug introduced by
It seems like $array of type array or array is incompatible with the declared type string of property $MyJson.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
837
        $model->write();
838
839
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $model->ID)->first();
840
841
        $this->assertEquals(json_encode($array), $freshRecord->MyJson);
842
        $this->assertEquals(json_decode(json_encode($array)), $freshRecord->dbObject('MyJson')->decode());
843
        $this->assertEquals($array, $freshRecord->dbObject('MyJson')->toArray());
844
        $this->assertEquals($array, $freshRecord->dbObject('MyJson')->decodeArray());
845
        $this->assertEquals($model->dbObject('MyJson')->toArray(), $freshRecord->dbObject('MyJson')->toArray());
846
    }
847
}
848