Passed
Push — master ( 16d3a8...13fa2f )
by Thomas
14:53
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\EncryptedFile;
14
use LeKoala\Encrypt\EncryptHelper;
15
use SilverStripe\Core\Environment;
16
use SilverStripe\Dev\SapphireTest;
17
use Symfony\Component\Yaml\Parser;
18
use SilverStripe\Security\Security;
19
use LeKoala\Encrypt\EncryptedDBJson;
20
use LeKoala\Encrypt\EncryptedDBField;
21
use LeKoala\Encrypt\MemberKeyProvider;
22
use ParagonIE\CipherSweet\CipherSweet;
23
use LeKoala\Encrypt\HasEncryptedFields;
24
use ParagonIE\CipherSweet\JsonFieldMap;
25
use SilverStripe\ORM\Queries\SQLSelect;
26
use SilverStripe\ORM\Queries\SQLUpdate;
27
use ParagonIE\CipherSweet\KeyProvider\StringProvider;
28
use ParagonIE\CipherSweet\Contract\MultiTenantSafeBackendInterface;
29
30
/**
31
 * Test for Encrypt
32
 *;
33
 * Run with the following command : ./vendor/bin/phpunit ./encrypt/tests/EncryptTest.php
34
 *
35
 * You may need to run:
36
 * php ./framework/cli-script.php dev/build ?flush=all
37
 * before (remember manifest for cli is not the same...)
38
 *
39
 * @group Encrypt
40
 */
41
class EncryptTest extends SapphireTest
42
{
43
    /**
44
     * Defines the fixture file to use for this test class
45
     * @var string
46
     */
47
    protected static $fixture_file = 'EncryptTest.yml';
48
49
    protected static $extra_dataobjects = [
50
        Test_EncryptedModel::class,
51
        Test_EncryptionKey::class,
52
    ];
53
54
    public function setUp(): void
55
    {
56
        // We need to disable automatic decryption to avoid fixtures being re encrypted with the wrong keys
57
        EncryptHelper::setAutomaticDecryption(false);
58
        Environment::setEnv('ENCRYPTION_KEY', '502370dfc69fd6179e1911707e8a5fb798c915900655dea16370d64404be04e5');
59
        Environment::setEnv('OLD_ENCRYPTION_KEY', '502370dfc69fd6179e1911707e8a5fb798c915900655dea16370d64404be04e4');
60
        parent::setUp();
61
        EncryptHelper::setAutomaticDecryption(true);
62
63
        // test extension is available
64
        if (!extension_loaded('sodium')) {
65
            throw new Exception("You must load sodium extension for this");
66
        }
67
68
        // Generate our test data from scratch
69
        // Use some old engine first
70
        // $this->generateData();
71
72
        // $this->showRowsFromDb();
73
        // $this->writeDataFromYml();
74
    }
75
76
    public function tearDown(): void
77
    {
78
        parent::tearDown();
79
    }
80
81
    protected function generateData()
82
    {
83
        EncryptHelper::clearCipherSweet();
84
        EncryptHelper::setForcedEncryption("nacl");
85
        $someText = 'some text';
86
        $data = [
87
            'MyText' => $someText . ' text',
88
            'MyHTMLText' => '<p>' . $someText . ' html</p>',
89
            'MyVarchar' => 'encrypted varchar value',
90
            'MyIndexedVarchar' => "some_searchable_value",
91
            'MyNullIndexedVarchar' => null,
92
            'MyNumber' => "0123456789",
93
        ];
94
        $record = Test_EncryptedModel::get()->filter('Name', 'demo')->first();
95
        foreach ($data as $k => $v) {
96
            $record->$k = $v;
97
        }
98
        $record->write();
99
        EncryptHelper::clearCipherSweet();
100
        $record = Test_EncryptedModel::get()->filter('Name', 'demo3')->first();
101
        foreach ($data as $k => $v) {
102
            $record->$k = $v;
103
        }
104
        $record->write();
105
        // use regular engine
106
        EncryptHelper::clearCipherSweet();
107
        EncryptHelper::setForcedEncryption(null);
108
        $record = Test_EncryptedModel::get()->filter('Name', 'demo2')->first();
109
        foreach ($data as $k => $v) {
110
            $record->$k = $v;
111
        }
112
        $record->write();
113
    }
114
115
    protected function showRowsFromDb()
116
    {
117
        $result = DB::query("SELECT * FROM EncryptedModel");
118
        echo '<pre>' . "\n";
119
        // print_r(iterator_to_array($result));
120
        foreach ($result as $row) {
121
            $this->showAsYml($row);
122
        }
123
        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...
124
    }
125
126
    protected function writeDataFromYml()
127
    {
128
        $ymlParser = new Parser;
129
        $ymlData = $ymlParser->parseFile(__DIR__ . '/EncryptTest.yml');
130
131
        foreach ($ymlData["LeKoala\\Encrypt\\Test\\Test_EncryptedModel"] as $name => $data) {
132
            unset($data['Member']);
133
            $update = new SQLUpdate("EncryptedModel", $data, ["Name" => $data['Name']]);
134
            $update->execute();
135
        }
136
    }
137
138
    protected function showAsYml($row)
139
    {
140
        $fields = [
141
            'Name', 'MyText', 'MyHTMLText', 'MyVarchar',
142
            'MyNumberValue', 'MyNumberBlindIndex', 'MyNumberLastFourBlindIndex',
143
            'MyIndexedVarcharValue', 'MyIndexedVarcharBlindIndex'
144
        ];
145
        echo "  " . $row['Name'] . ":\n";
146
        foreach ($row as $k => $v) {
147
            if (!in_array($k, $fields)) {
148
                continue;
149
            }
150
            echo "    $k: '$v'\n";
151
        }
152
    }
153
154
    /**
155
     * @return Test_EncryptedModel
156
     */
157
    public function getTestModel()
158
    {
159
        return $this->objFromFixture(Test_EncryptedModel::class, 'demo');
160
    }
161
162
    /**
163
     * @return Test_EncryptedModel
164
     */
165
    public function getTestModel2()
166
    {
167
        return $this->objFromFixture(Test_EncryptedModel::class, 'demo2');
168
    }
169
170
    /**
171
     * @return Test_EncryptedModel
172
     */
173
    public function getTestModel3()
174
    {
175
        return $this->objFromFixture(Test_EncryptedModel::class, 'demo3');
176
    }
177
178
    /**
179
     * @return Test_EncryptedModel
180
     */
181
    public function getAdminTestModel()
182
    {
183
        return $this->objFromFixture(Test_EncryptedModel::class, 'admin_record');
184
    }
185
186
    /**
187
     * @return Test_EncryptedModel
188
     */
189
    public function getUser1TestModel()
190
    {
191
        return $this->objFromFixture(Test_EncryptedModel::class, 'user1_record');
192
    }
193
194
    /**
195
     * @return Test_EncryptedModel
196
     */
197
    public function getUser2TestModel()
198
    {
199
        return $this->objFromFixture(Test_EncryptedModel::class, 'user2_record');
200
    }
201
202
    /**
203
     * @return Test_EncryptedModel
204
     */
205
    public function getNewTestModel()
206
    {
207
        return new Test_EncryptedModel();
208
    }
209
210
    /**
211
     * @return Member
212
     */
213
    public function getAdminMember()
214
    {
215
        return $this->objFromFixture(Member::class, 'admin');
216
    }
217
218
    /**
219
     * @return Member
220
     */
221
    public function getUser1Member()
222
    {
223
        return $this->objFromFixture(Member::class, 'user1');
224
    }
225
226
    /**
227
     * @return Member
228
     */
229
    public function getUser2Member()
230
    {
231
        return $this->objFromFixture(Member::class, 'user2');
232
    }
233
234
    /**
235
     * @return DataList|Member[]
236
     */
237
    public function getAllMembers()
238
    {
239
        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...
240
            $this->getAdminMember(),
241
            $this->getUser1Member(),
242
            $this->getUser2Member(),
243
        ]);
244
    }
245
246
    /**
247
     * @return File
248
     */
249
    public function getRegularFile()
250
    {
251
        return $this->objFromFixture(File::class, 'regular');
252
    }
253
254
    /**
255
     * @return File
256
     */
257
    public function getEncryptedFile()
258
    {
259
        return $this->objFromFixture(File::class, 'encrypted');
260
    }
261
262
    /**
263
     * @return EncryptedFile
264
     */
265
    public function getEncryptedFile2()
266
    {
267
        // Figure out how to do this properly in yml
268
        $record = $this->objFromFixture(File::class, 'encrypted2');
269
        $file = new EncryptedFile($record->toMap());
270
        return $file;
271
    }
272
273
    /**
274
     * @param string $class
275
     * @param int $id
276
     * @return array
277
     */
278
    protected function fetchRawData($class, $id)
279
    {
280
        $tableName = DataObject::getSchema()->tableName($class);
281
        $columnIdentifier = DataObject::getSchema()->sqlColumnForField($class, 'ID');
282
        $sql = new SQLSelect('*', [$tableName], [$columnIdentifier => $id]);
283
        $dbRecord = $sql->firstRow()->execute()->record();
284
        return $dbRecord;
285
    }
286
287
    public function testEncryption()
288
    {
289
        $someText = 'some text';
290
        $encrypt = EncryptHelper::encrypt($someText);
291
        $decryptedValue = EncryptHelper::decrypt($encrypt);
292
293
        $this->assertEquals($someText, $decryptedValue);
294
    }
295
296
    public function testIndexes()
297
    {
298
        $indexes = DataObject::getSchema()->databaseIndexes(Test_EncryptedModel::class);
299
        $keys = array_keys($indexes);
300
        $this->assertContains('MyIndexedVarcharBlindIndex', $keys, "Index is not defined in : " . implode(", ", $keys));
301
        $this->assertContains('MyNumberLastFourBlindIndex', $keys, "Index is not defined in : " . implode(", ", $keys));
302
    }
303
304
    public function testSearch()
305
    {
306
        $singl = singleton(Test_EncryptedModel::class);
307
308
        /** @var EncryptedDBField $obj  */
309
        $obj = $singl->dbObject('MyIndexedVarchar');
310
        $record = $obj->fetchRecord('some_searchable_value');
311
312
        // echo '<pre>';print_r("From test: " . $record->MyIndexedVarchar);die();
313
        $this->assertNotEmpty($record);
314
        $this->assertEquals("some text text", $record->MyText);
0 ignored issues
show
Bug introduced by
The property MyText does not exist on false.
Loading history...
315
        $this->assertEquals("some_searchable_value", $record->MyIndexedVarchar);
0 ignored issues
show
Bug introduced by
The property MyIndexedVarchar does not exist on false.
Loading history...
316
        $this->assertEquals("some_searchable_value", $record->dbObject('MyIndexedVarchar')->getValue());
317
318
        // Also search our super getter method
319
        $recordAlt = Test_EncryptedModel::getByBlindIndex('MyIndexedVarchar', 'some_searchable_value');
320
        $this->assertNotEmpty($record);
321
        $this->assertEquals($recordAlt->ID, $record->ID);
0 ignored issues
show
Bug introduced by
The property ID does not exist on false.
Loading history...
322
323
        // Can we get a list ?
324
        $list = Test_EncryptedModel::getAllByBlindIndex('MyIndexedVarchar', 'some_searchable_value');
325
        $this->assertInstanceOf(DataList::class, $list);
326
327
        $record = $obj->fetchRecord('some_unset_value');
328
        $this->assertEmpty($record);
329
330
        // Let's try our four digits index
331
        $obj = $singl->dbObject('MyNumber');
332
        $record = $obj->fetchRecord('6789', 'LastFourBlindIndex');
333
        $searchValue = $obj->getSearchValue('6789', 'LastFourBlindIndex');
334
        // $searchParams = $obj->getSearchParams('6789', 'LastFourBlindIndex');
335
        // print_r($searchParams);
336
        $this->assertNotEmpty($record, "Nothing found for $searchValue");
337
        $this->assertEquals("0123456789", $record->MyNumber);
338
    }
339
340
    public function testSearchFilter()
341
    {
342
        $record = Test_EncryptedModel::get()->filter('MyIndexedVarchar:Encrypted', 'some_searchable_value')->first();
343
        $this->assertNotEmpty($record);
344
        $this->assertEquals(1, $record->ID);
345
        $this->assertNotEquals(2, $record->ID);
346
347
        $record = Test_EncryptedModel::get()->filter('MyIndexedVarchar:Encrypted', 'some_unset_value')->first();
348
        $this->assertEmpty($record);
349
    }
350
351
    public function testRotation()
352
    {
353
        $model = $this->getTestModel3();
354
        $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...
355
356
        $old = EncryptHelper::getEngineForEncryption("nacl");
357
        $result = $model->needsToRotateEncryption($old);
358
        $this->assertTrue($result);
359
360
        $result = $model->rotateEncryption($old);
361
        $this->assertTrue($result);
362
    }
363
364
    public function testCompositeOptions()
365
    {
366
        $model = $this->getTestModel();
367
368
        /** @var EncryptedDBField $myNumber */
369
        $myNumber = $model->dbObject('MyNumber');
370
371
        $this->assertEquals(10, $myNumber->getDomainSize());
372
        $this->assertEquals(4, $myNumber->getOutputSize());
373
        $this->assertEquals(EncryptedDBField::LARGE_INDEX_SIZE, $myNumber->getIndexSize());
374
375
        /** @var EncryptedDBField $MyIndexedVarchar */
376
        $MyIndexedVarchar = $model->dbObject('MyIndexedVarchar');
377
378
        // Default config values
379
        $this->assertEquals(EncryptHelper::DEFAULT_DOMAIN_SIZE, $MyIndexedVarchar->getDomainSize());
380
        $this->assertEquals(EncryptHelper::DEFAULT_OUTPUT_SIZE, $MyIndexedVarchar->getOutputSize());
381
        $this->assertEquals(EncryptedDBField::LARGE_INDEX_SIZE, $MyIndexedVarchar->getIndexSize());
382
    }
383
384
    public function testIndexPlanner()
385
    {
386
        $sizes = EncryptHelper::planIndexSizesForClass(Test_EncryptedModel::class);
387
        $this->assertNotEmpty($sizes);
388
        $this->assertArrayHasKey("min", $sizes);
389
        $this->assertArrayHasKey("max", $sizes);
390
        $this->assertArrayHasKey("indexes", $sizes);
391
        $this->assertArrayHasKey("estimated_population", $sizes);
392
        $this->assertArrayHasKey("coincidence_count", $sizes);
393
    }
394
395
    public function testFixture()
396
    {
397
        // this one use nacl encryption and will be rotated transparently
398
        $model = $this->getTestModel();
399
400
        $result = $model->needsToRotateEncryption(EncryptHelper::getEngineForEncryption("nacl"));
401
        $this->assertTrue($result);
402
403
        // Ensure we have our blind indexes
404
        $this->assertTrue($model->hasDatabaseField('MyIndexedVarcharValue'));
405
        $this->assertTrue($model->hasDatabaseField('MyIndexedVarcharBlindIndex'));
406
        $this->assertTrue($model->hasDatabaseField('MyNumberValue'));
407
        $this->assertTrue($model->hasDatabaseField('MyNumberBlindIndex'));
408
        $this->assertTrue($model->hasDatabaseField('MyNumberLastFourBlindIndex'));
409
410
        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

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

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

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

692
        $this->assertTrue($encryptedFile2->/** @scrutinizer ignore-call */ isEncrypted());
Loading history...
693
694
        $encryptedFile->encryptFileIfNeeded();
695
696
        $this->assertTrue($encryptedFile->isEncrypted());
697
        $this->assertTrue($encryptedFile->Encrypted);
698
699
        // still encrypted?
700
        $encryptedFile->encryptFileIfNeeded();
701
        $this->assertTrue($encryptedFile->isEncrypted());
702
        $this->assertTrue($encryptedFile->Encrypted);
703
704
        // set something new
705
        $string = 'Some content';
706
        $stream = fopen('php://memory', 'r+');
707
        fwrite($stream, $string);
708
        rewind($stream);
709
        $encryptedFile->setFromStream($stream, 'secret.doc');
710
        $encryptedFile->write();
711
        $encryptedFile2->setFromStream($stream, 'secret.doc');
712
        $encryptedFile2->write();
713
714
        // we need to update manually
715
        $encryptedFile->updateEncryptionStatus();
716
717
        // It is not encrypted nor marked as such
718
        $this->assertFalse($encryptedFile->isEncrypted());
719
        $this->assertFalse($encryptedFile->Encrypted);
720
        // Ir was automatically encrypted again
721
        $this->assertTrue($encryptedFile2->isEncrypted());
722
        $this->assertTrue($encryptedFile2->Encrypted);
723
724
        // No file => no encryption
725
        $encryptedFile2->deleteFile();
726
        $this->assertFalse($encryptedFile->isEncrypted());
727
    }
728
729
    /**
730
     * @group only
731
     */
732
    public function testMessageEncryption()
733
    {
734
        $admin = $this->getAdminMember();
735
        $user1 = $this->getUser1Member();
736
737
        $adminKeys = Test_EncryptionKey::getKeyPair($admin->ID);
738
        $user1Keys = Test_EncryptionKey::getKeyPair($user1->ID);
739
740
        $this->assertArrayHasKey("public", $adminKeys);
741
        $this->assertArrayHasKey("secret", $adminKeys);
742
        $this->assertArrayHasKey("public", $user1Keys);
743
        $this->assertArrayHasKey("secret", $user1Keys);
744
745
        // $pairs = sodium_crypto_box_keypair();
746
        // $adminKeys['secret'] = sodium_crypto_box_secretkey($pairs);
747
        // $adminKeys['public'] = sodium_crypto_box_publickey($pairs);
748
749
        // $pairs = sodium_crypto_box_keypair();
750
        // $user1Keys['secret'] = sodium_crypto_box_secretkey($pairs);
751
        // $user1Keys['public'] = sodium_crypto_box_publickey($pairs);
752
753
        // $adminKeys['secret'] = Hex::encode($adminKeys['secret']);
754
        // $adminKeys['public'] = Hex::encode($adminKeys['public']);
755
        // $user1Keys['secret'] = Hex::encode($user1Keys['secret']);
756
        // $user1Keys['public'] = Hex::encode($user1Keys['public']);
757
758
        // $adminKeys['secret'] = Hex::decode($adminKeys['secret']);
759
        // $adminKeys['public'] = Hex::decode($adminKeys['public']);
760
        // $user1Keys['secret'] = Hex::decode($user1Keys['secret']);
761
        // $user1Keys['public'] = Hex::decode($user1Keys['public']);
762
763
        $message = 'hello';
764
        // 24
765
        $nonce = random_bytes(SODIUM_CRYPTO_BOX_NONCEBYTES);
766
        $encryption_key = sodium_crypto_box_keypair_from_secretkey_and_publickey($adminKeys['secret'], $user1Keys['public']);
767
        $encrypted = sodium_crypto_box($message, $nonce, $encryption_key);
768
        $this->assertNotEmpty($encrypted);
769
        $this->assertNotEquals($message, $encrypted);
770
771
        // Revert keys to decrypt
772
        $decryption_key = sodium_crypto_box_keypair_from_secretkey_and_publickey($user1Keys['secret'], $adminKeys['public']);
773
        $decrypted = sodium_crypto_box_open($encrypted, $nonce, $decryption_key);
774
        $this->assertNotEmpty($decrypted);
775
        $this->assertEquals($message, $decrypted);
776
    }
777
778
    protected function getMultiTenantProvider()
779
    {
780
        $members = $this->getAllMembers();
781
        $tenants = [];
782
        foreach ($members as $member) {
783
            // You can also use the secret key from a keypair
784
            // $key = Test_EncryptionKey::getForMember($member->ID);
785
            $keyPair = Test_EncryptionKey::getKeyPair($member->ID);
786
            if ($keyPair) {
787
                $tenants[$member->ID] = new StringProvider($keyPair['secret']);
788
                // $tenants[$member->ID] = new StringProvider($key);
789
            }
790
        }
791
        $provider = new MemberKeyProvider($tenants);
792
        return $provider;
793
    }
794
795
    /**
796
     * @group multi-tenant
797
     * @group only
798
     */
799
    public function testMultiTenantProvider()
800
    {
801
        // echo '<pre>';
802
        // print_r(EncryptHelper::generateKeyPair());
803
        // die();
804
        $admin = $this->getAdminMember();
805
        $user1 = $this->getUser1Member();
806
        $user2 = $this->getUser2Member();
807
808
        $adminModel = $this->getAdminTestModel();
809
        $user1Model = $this->getUser1TestModel();
810
        $user2Model = $this->getUser2TestModel();
811
812
        $provider = $this->getMultiTenantProvider();
813
814
        Security::setCurrentUser($admin);
815
        EncryptHelper::clearCipherSweet();
816
        $cs = EncryptHelper::getCipherSweet($provider);
817
818
        $this->assertInstanceOf(MultiTenantSafeBackendInterface::class, $cs->getBackend());
819
820
        $string = "my content";
821
        $record = new Test_EncryptedModel();
822
        // $record = Test_EncryptedModel::get()->filter('ID', $user2Model->ID)->first();
823
        $record->MyText = $string;
824
        // We need to set active tenant ourselves because orm records fields one by one
825
        // it doesn't go through injectMetadata
826
        $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...
827
        $record->write();
828
829
        // echo '<pre>';
830
        // print_r($this->fetchRawData(Test_EncryptedModel::class, $record->ID));
831
        // die();
832
833
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $record->ID)->first();
834
835
        $this->assertEquals($admin->ID, Security::getCurrentUser()->ID, "Make sure the right member is logged in");
836
        // He can decode
837
        $this->assertEquals($string, $freshRecord->MyText);
838
839
        // He can also decode his content from the db
840
        $adminRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
841
        // echo '<pre>';print_r($adminRecord);die();
842
        $this->assertEquals($string, $adminRecord->MyText);
843
844
        // He cannot decode
845
        Security::setCurrentUser($user1);
846
        // We don't need to set active tenant because our MemberKeyProvider reads currentUser automatically
847
        // $provider->setActiveTenant($user1->ID);
848
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $record->ID)->first();
849
        $this->assertNotEquals($string, $freshRecord->MyText);
850
851
        // Test tenant from row
852
        $this->assertEquals($admin->ID, $cs->getTenantFromRow($adminModel->toMap()));
853
        $this->assertEquals($user1->ID, $cs->getTenantFromRow($user1Model->toMap()));
854
        $this->assertEquals($user2->ID, $cs->getTenantFromRow($user2Model->toMap()));
855
856
        // Current user can decode what he can
857
        Security::setCurrentUser($admin);
858
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
859
        $this->assertEquals($string, $freshRecord->MyText, "Invalid content for admin model #{$adminModel->ID}");
860
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $user2Model->ID)->first();
861
        $this->assertNotEquals($string, $freshRecord->MyText, "Invalid content for user2 model #{$user2Model->ID}");
862
863
        // Thanks to getTenantFromRow we should be able to rotate encryption
864
        // rotate from admin to user2
865
        Security::setCurrentUser($user2);
866
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
867
        $freshRecord->MemberID = $user2->ID;
868
        $freshRecord->write();
869
        $this->assertNotEquals($string, $freshRecord->MyText);
870
        // We can keep the same provider but we need to clone it and change the active tenant
871
        $cs->setActiveTenant($user2->ID);
872
873
        // clone will not deep clone the key provider with the active tenant
874
        // $old = clone $cs;
875
        $clonedProvider = clone $provider;
876
        $clonedProvider->setForcedTenant($admin->ID);
877
        $old = EncryptHelper::getEngineWithProvider(EncryptHelper::getBackendForEncryption("brng"), $clonedProvider);
878
879
        $freshRecord->rotateEncryption($old);
880
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
881
        $this->assertEquals($string, $freshRecord->MyText);
882
883
        // Admin can't read anymore, don't forget to refresh record from db
884
        Security::setCurrentUser($admin);
885
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
886
        $this->assertNotEquals($string, $freshRecord->MyText);
887
888
        // Cleanup
889
        EncryptHelper::clearCipherSweet();
890
    }
891
892
    /**
893
     * @group aad
894
     */
895
    public function testAad()
896
    {
897
        $new = $this->getNewTestModel();
898
899
        // Without AAD, it should work fine
900
        EncryptHelper::setAadSource("");
0 ignored issues
show
Bug introduced by
'' of type string is incompatible with the type boolean expected by parameter $aadSource of LeKoala\Encrypt\EncryptHelper::setAadSource(). ( Ignorable by Annotation )

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

900
        EncryptHelper::setAadSource(/** @scrutinizer ignore-type */ "");
Loading history...
901
902
        $new->MyText = "TEST";
903
        $new->MyIndexedVarchar = "TEST";
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...
904
        $new->write();
905
906
        $this->assertEquals("TEST", $new->dbObject("MyText")->getValue());
907
        $this->assertEquals("TEST", $new->dbObject("MyIndexedVarchar")->getValue());
908
909
        // With AAD, it should work fine as well
910
        // var_dump("*** CREATE ***");
911
        $new = $this->getNewTestModel();
912
913
        EncryptHelper::setAadSource("ID");
914
915
        // var_dump("*** ASSIGN ***");
916
        $new->MyText = "TEST";
917
        $new->MyIndexedVarchar = "TEST";
918
919
        // var_dump("*** WRITE ***");
920
        $new->write();
921
        // On first write, we need to make sure we pick up the ID from the writeBaseRecord operation
922
923
        // The second write should NOT be needed
924
        // $new->MyIndexedVarchar = "TEST";
925
        // $new->write();
926
927
        $dbObj = $new->dbObject("MyIndexedVarchar");
928
929
        $this->assertEquals("TEST", $new->dbObject("MyText")->getValue());
930
        $this->assertEquals("TEST", $dbObj->getValue());
931
        $this->assertNull($dbObj->getEncryptionException());
932
    }
933
934
    public function testJsonField()
935
    {
936
        $model = $this->getTestModel();
937
938
        $longstring = str_repeat("lorem ipsum loquor", 100);
939
        $array = [];
940
        foreach (range(1, 100) as $i) {
941
            $array["key_$i"] = $longstring . $i;
942
        }
943
944
        $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...
945
        $model->write();
946
947
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $model->ID)->first();
948
949
        $this->assertEquals(json_encode($array), $freshRecord->MyJson);
950
        $this->assertEquals(json_decode(json_encode($array)), $freshRecord->dbObject('MyJson')->decode());
951
        $this->assertEquals($array, $freshRecord->dbObject('MyJson')->toArray());
952
        $this->assertEquals($array, $freshRecord->dbObject('MyJson')->decodeArray());
953
        $this->assertEquals($model->dbObject('MyJson')->toArray(), $freshRecord->dbObject('MyJson')->toArray());
954
    }
955
956
    public function testEncryptedJsonField()
957
    {
958
        $model = $this->getTestModel();
959
960
        /** @var EncryptedDBJson $field */
961
        $field = $model->dbObject('MyEncryptedJson');
962
963
        $map = (new JsonFieldMap())
964
            ->addTextField('name')
965
            ->addBooleanField('active')
966
            ->addIntegerField('age');
967
968
        $definition = EncryptHelper::convertJsonMapToDefinition($map);
969
        $this->assertIsString($definition);
970
971
        $encryptedJsonField = $field->getEncryptedJsonField();
972
973
        $data = [
974
            'name' => 'test name',
975
            'active' => true,
976
            'age' => 42,
977
            'not_encrypted' => "this is not encrypted"
978
        ];
979
980
        $aad = (string)$model->ID;
981
        $encryptedJsonData = $encryptedJsonField->encryptJson($data, $aad);
982
983
        $this->assertFalse(EncryptHelper::isJsonEncrypted($data));
984
        $this->assertTrue(EncryptHelper::isJsonEncrypted($encryptedJsonData));
985
986
        $decoded = json_decode($encryptedJsonData, JSON_OBJECT_AS_ARRAY);
0 ignored issues
show
Bug introduced by
LeKoala\Encrypt\Test\JSON_OBJECT_AS_ARRAY of type integer is incompatible with the type boolean|null expected by parameter $associative of json_decode(). ( Ignorable by Annotation )

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

986
        $decoded = json_decode($encryptedJsonData, /** @scrutinizer ignore-type */ JSON_OBJECT_AS_ARRAY);
Loading history...
987
988
        // it is properly encrypted if required
989
        $this->assertEquals($data['not_encrypted'], $decoded['not_encrypted']);
990
        $this->assertNotEquals($data['name'], $decoded['name']);
991
992
        // we can write
993
        $model->MyEncryptedJson = $data;
0 ignored issues
show
Documentation Bug introduced by
It seems like $data of type array<string,integer|string|true> is incompatible with the declared type string of property $MyEncryptedJson.

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...
994
        $model->write();
995
996
        $dbData = DB::query("SELECT MyEncryptedJson FROM EncryptedModel WHERE ID = " . $model->ID)->value();
997
        $decodedDbData = json_decode($dbData, JSON_OBJECT_AS_ARRAY);
998
999
        // data is properly stored with partially encrypted json
1000
        $this->assertNotNull($decodedDbData, "got $dbData");
1001
        $this->assertEquals($data['not_encrypted'], $decodedDbData['not_encrypted']);
1002
        $this->assertNotEquals($data['name'], $decodedDbData['name']);
1003
1004
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $model->ID)->first();
1005
        $freshValue = $freshRecord->dbObject('MyEncryptedJson')->toArray();
1006
1007
        // It is decoded transparently
1008
        $this->assertEquals($data, $freshValue);
1009
    }
1010
1011
    public function testFashHash()
1012
    {
1013
        $model = $this->getTestModel();
1014
1015
        /** @var EncryptedDBField $encrField */
1016
        $encrField = $model->dbObject('MyIndexedVarchar');
1017
1018
        $value = (string)$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...
1019
        $bi = $model->MyIndexedVarcharBlindIndex;
0 ignored issues
show
Bug Best Practice introduced by
The property MyIndexedVarcharBlindIndex does not exist on LeKoala\Encrypt\Test\Test_EncryptedModel. Since you implemented __get, consider adding a @property annotation.
Loading history...
1020
1021
        $aad = '';
1022
1023
        $t = microtime(true);
1024
        $slowBi = $encrField->getEncryptedField(null, false)->prepareForStorage($value, $aad);
1025
        $slowBi2 = $encrField->getEncryptedField(null, false)->prepareForStorage($value, $aad);
1026
        $et = microtime(true) - $t;
1027
1028
        $t2 = microtime(true);
1029
        $fastBi = $encrField->getEncryptedField(null, true)->prepareForStorage($value, $aad);
1030
        $fastBi2 = $encrField->getEncryptedField(null, true)->prepareForStorage($value, $aad);
1031
        $et2 = microtime(true) - $t2;
1032
1033
        // Values are not equals, but blind indexes are
1034
        $this->assertNotEquals($slowBi2[0], $slowBi[0]);
1035
        $this->assertEquals($slowBi2[1]['MyIndexedVarcharBlindIndex'], $slowBi[1]['MyIndexedVarcharBlindIndex']);
1036
        $this->assertEquals($bi, $slowBi[1]['MyIndexedVarcharBlindIndex']);
1037
        $this->assertNotEquals($fastBi2[0], $fastBi[0]);
1038
        $this->assertEquals($fastBi2[1]['MyIndexedVarcharBlindIndex'], $fastBi[1]['MyIndexedVarcharBlindIndex']);
1039
1040
        // Slow indexes are not the same as fast indexes
1041
        $this->assertNotEquals($fastBi[1]['MyIndexedVarcharBlindIndex'], $slowBi[1]['MyIndexedVarcharBlindIndex']);
1042
        $this->assertNotEquals($fastBi2[1]['MyIndexedVarcharBlindIndex'], $slowBi2[1]['MyIndexedVarcharBlindIndex']);
1043
1044
        // It is faster to generate fast indexes
1045
        // $et2 = 0.0004119873046875
1046
        // $et = 0.0683131217956543
1047
1048
        $this->assertTrue($et2 <= $et);
1049
1050
        // We can convert and keep stored values readable
1051
1052
        $result = EncryptHelper::convertHashType($model, 'MyIndexedVarchar');
1053
        $this->assertTrue($result);
1054
1055
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $model->ID)->first();
1056
        $freshValue = (string)$freshRecord->MyIndexedVarchar;
1057
        $this->assertEquals($value, $freshValue);
1058
1059
        // We can find it using new hash
1060
        /** @var EncryptedDBField $freshEncrField */
1061
        $freshEncrField = $freshRecord->dbObject('MyIndexedVarchar');
1062
1063
        $blindIndex = $freshEncrField->getEncryptedField(null, true)->getBlindIndex($freshValue, 'MyIndexedVarcharBlindIndex');
1064
        $freshRecord2 = Test_EncryptedModel::get()->filter('MyIndexedVarcharBlindIndex', $blindIndex)->first();
1065
        $this->assertEquals($freshRecord2->ID, $freshRecord->ID);
1066
    }
1067
1068
    public function testNullValue()
1069
    {
1070
        $model = $this->getTestModel();
1071
1072
        /** @var EncryptedDBField $field */
1073
        $field = $model->dbObject('MyNullIndexedVarchar');
1074
1075
        $e = null;
1076
        try {
1077
            $record = $field->fetchRecord(null);
0 ignored issues
show
Unused Code introduced by
The assignment to $record is dead and can be removed.
Loading history...
1078
        } catch (Exception $e) {
1079
            $this->assertStringContainsString('Cannot search an empty value', $e->getMessage());
1080
        }
1081
        $this->assertNotEmpty($e);
1082
    }
1083
}
1084