Passed
Push — master ( 1d51bb...57a30c )
by Thomas
12:26
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
            'MyNumber' => "0123456789",
92
        ];
93
        $record = Test_EncryptedModel::get()->filter('Name', 'demo')->first();
94
        foreach ($data as $k => $v) {
95
            $record->$k = $v;
96
        }
97
        $record->write();
98
        EncryptHelper::clearCipherSweet();
99
        $record = Test_EncryptedModel::get()->filter('Name', 'demo3')->first();
100
        foreach ($data as $k => $v) {
101
            $record->$k = $v;
102
        }
103
        $record->write();
104
        // use regular engine
105
        EncryptHelper::clearCipherSweet();
106
        EncryptHelper::setForcedEncryption(null);
107
        $record = Test_EncryptedModel::get()->filter('Name', 'demo2')->first();
108
        foreach ($data as $k => $v) {
109
            $record->$k = $v;
110
        }
111
        $record->write();
112
    }
113
114
    protected function showRowsFromDb()
115
    {
116
        $result = DB::query("SELECT * FROM EncryptedModel");
117
        echo '<pre>' . "\n";
118
        // print_r(iterator_to_array($result));
119
        foreach ($result as $row) {
120
            $this->showAsYml($row);
121
        }
122
        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...
123
    }
124
125
    protected function writeDataFromYml()
126
    {
127
        $ymlParser = new Parser;
128
        $ymlData = $ymlParser->parseFile(__DIR__ . '/EncryptTest.yml');
129
130
        foreach ($ymlData["LeKoala\\Encrypt\\Test\\Test_EncryptedModel"] as $name => $data) {
131
            unset($data['Member']);
132
            $update = new SQLUpdate("EncryptedModel", $data, ["Name" => $data['Name']]);
133
            $update->execute();
134
        }
135
    }
136
137
    protected function showAsYml($row)
138
    {
139
        $fields = [
140
            'Name', 'MyText', 'MyHTMLText', 'MyVarchar',
141
            'MyNumberValue', 'MyNumberBlindIndex', 'MyNumberLastFourBlindIndex',
142
            'MyIndexedVarcharValue', 'MyIndexedVarcharBlindIndex'
143
        ];
144
        echo "  " . $row['Name'] . ":\n";
145
        foreach ($row as $k => $v) {
146
            if (!in_array($k, $fields)) {
147
                continue;
148
            }
149
            echo "    $k: '$v'\n";
150
        }
151
    }
152
153
    /**
154
     * @return Test_EncryptedModel
155
     */
156
    public function getTestModel()
157
    {
158
        return $this->objFromFixture(Test_EncryptedModel::class, 'demo');
159
    }
160
161
    /**
162
     * @return Test_EncryptedModel
163
     */
164
    public function getTestModel2()
165
    {
166
        return $this->objFromFixture(Test_EncryptedModel::class, 'demo2');
167
    }
168
169
    /**
170
     * @return Test_EncryptedModel
171
     */
172
    public function getTestModel3()
173
    {
174
        return $this->objFromFixture(Test_EncryptedModel::class, 'demo3');
175
    }
176
177
    /**
178
     * @return Test_EncryptedModel
179
     */
180
    public function getAdminTestModel()
181
    {
182
        return $this->objFromFixture(Test_EncryptedModel::class, 'admin_record');
183
    }
184
185
    /**
186
     * @return Test_EncryptedModel
187
     */
188
    public function getUser1TestModel()
189
    {
190
        return $this->objFromFixture(Test_EncryptedModel::class, 'user1_record');
191
    }
192
193
    /**
194
     * @return Test_EncryptedModel
195
     */
196
    public function getUser2TestModel()
197
    {
198
        return $this->objFromFixture(Test_EncryptedModel::class, 'user2_record');
199
    }
200
201
    /**
202
     * @return Test_EncryptedModel
203
     */
204
    public function getNewTestModel()
205
    {
206
        return new Test_EncryptedModel();
207
    }
208
209
    /**
210
     * @return Member
211
     */
212
    public function getAdminMember()
213
    {
214
        return $this->objFromFixture(Member::class, 'admin');
215
    }
216
217
    /**
218
     * @return Member
219
     */
220
    public function getUser1Member()
221
    {
222
        return $this->objFromFixture(Member::class, 'user1');
223
    }
224
225
    /**
226
     * @return Member
227
     */
228
    public function getUser2Member()
229
    {
230
        return $this->objFromFixture(Member::class, 'user2');
231
    }
232
233
    /**
234
     * @return DataList|Member[]
235
     */
236
    public function getAllMembers()
237
    {
238
        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...
239
            $this->getAdminMember(),
240
            $this->getUser1Member(),
241
            $this->getUser2Member(),
242
        ]);
243
    }
244
245
    /**
246
     * @return File
247
     */
248
    public function getRegularFile()
249
    {
250
        return $this->objFromFixture(File::class, 'regular');
251
    }
252
253
    /**
254
     * @return File
255
     */
256
    public function getEncryptedFile()
257
    {
258
        return $this->objFromFixture(File::class, 'encrypted');
259
    }
260
261
    /**
262
     * @return EncryptedFile
263
     */
264
    public function getEncryptedFile2()
265
    {
266
        // Figure out how to do this properly in yml
267
        $record = $this->objFromFixture(File::class, 'encrypted2');
268
        $file = new EncryptedFile($record->toMap());
269
        return $file;
270
    }
271
272
    /**
273
     * @param string $class
274
     * @param int $id
275
     * @return array
276
     */
277
    protected function fetchRawData($class, $id)
278
    {
279
        $tableName = DataObject::getSchema()->tableName($class);
280
        $columnIdentifier = DataObject::getSchema()->sqlColumnForField($class, 'ID');
281
        $sql = new SQLSelect('*', [$tableName], [$columnIdentifier => $id]);
282
        $dbRecord = $sql->firstRow()->execute()->first();
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\ORM\Connect\Query::first() has been deprecated: 4.13.0 Will be replaced by getIterator() in CMS 5 ( Ignorable by Annotation )

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

282
        $dbRecord = /** @scrutinizer ignore-deprecated */ $sql->firstRow()->execute()->first();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
283
        return $dbRecord;
284
    }
285
286
    public function testEncryption()
287
    {
288
        $someText = 'some text';
289
        $encrypt = EncryptHelper::encrypt($someText);
290
        $decryptedValue = EncryptHelper::decrypt($encrypt);
291
292
        $this->assertEquals($someText, $decryptedValue);
293
    }
294
295
    public function testIndexes()
296
    {
297
        $indexes = DataObject::getSchema()->databaseIndexes(Test_EncryptedModel::class);
298
        $keys = array_keys($indexes);
299
        $this->assertContains('MyIndexedVarcharBlindIndex', $keys, "Index is not defined in : " . implode(", ", $keys));
300
        $this->assertContains('MyNumberLastFourBlindIndex', $keys, "Index is not defined in : " . implode(", ", $keys));
301
    }
302
303
    public function testSearch()
304
    {
305
        $singl = singleton(Test_EncryptedModel::class);
306
307
        /** @var EncryptedDBField $obj  */
308
        $obj = $singl->dbObject('MyIndexedVarchar');
309
        $record = $obj->fetchRecord('some_searchable_value');
310
311
        // echo '<pre>';print_r("From test: " . $record->MyIndexedVarchar);die();
312
        $this->assertNotEmpty($record);
313
        $this->assertEquals("some text text", $record->MyText);
314
        $this->assertEquals("some_searchable_value", $record->MyIndexedVarchar);
315
        $this->assertEquals("some_searchable_value", $record->dbObject('MyIndexedVarchar')->getValue());
316
317
        // Also search our super getter method
318
        $recordAlt = Test_EncryptedModel::getByBlindIndex('MyIndexedVarchar', 'some_searchable_value');
319
        $this->assertNotEmpty($record);
320
        $this->assertEquals($recordAlt->ID, $record->ID);
321
322
        // Can we get a list ?
323
        $list = Test_EncryptedModel::getAllByBlindIndex('MyIndexedVarchar', 'some_searchable_value');
324
        $this->assertInstanceOf(DataList::class, $list);
325
326
        $record = $obj->fetchRecord('some_unset_value');
327
        $this->assertEmpty($record);
328
329
        // Let's try our four digits index
330
        $obj = $singl->dbObject('MyNumber');
331
        $record = $obj->fetchRecord('6789', 'LastFourBlindIndex');
332
        $searchValue = $obj->getSearchValue('6789', 'LastFourBlindIndex');
333
        // $searchParams = $obj->getSearchParams('6789', 'LastFourBlindIndex');
334
        // print_r($searchParams);
335
        $this->assertNotEmpty($record, "Nothing found for $searchValue");
336
        $this->assertEquals("0123456789", $record->MyNumber);
337
    }
338
339
    public function testSearchFilter()
340
    {
341
        $record = Test_EncryptedModel::get()->filter('MyIndexedVarchar:Encrypted', 'some_searchable_value')->first();
342
        $this->assertNotEmpty($record);
343
        $this->assertEquals(1, $record->ID);
344
        $this->assertNotEquals(2, $record->ID);
345
346
        $record = Test_EncryptedModel::get()->filter('MyIndexedVarchar:Encrypted', 'some_unset_value')->first();
347
        $this->assertEmpty($record);
348
    }
349
350
    public function testRotation()
351
    {
352
        $model = $this->getTestModel3();
353
        $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...
354
355
        $old = EncryptHelper::getEngineForEncryption("nacl");
356
        $result = $model->needsToRotateEncryption($old);
357
        $this->assertTrue($result);
358
359
        $result = $model->rotateEncryption($old);
360
        $this->assertTrue($result);
361
    }
362
363
    public function testCompositeOptions()
364
    {
365
        $model = $this->getTestModel();
366
367
        /** @var EncryptedDBField $myNumber */
368
        $myNumber = $model->dbObject('MyNumber');
369
370
        $this->assertEquals(10, $myNumber->getDomainSize());
371
        $this->assertEquals(4, $myNumber->getOutputSize());
372
        $this->assertEquals(EncryptedDBField::LARGE_INDEX_SIZE, $myNumber->getIndexSize());
373
374
        /** @var EncryptedDBField $MyIndexedVarchar */
375
        $MyIndexedVarchar = $model->dbObject('MyIndexedVarchar');
376
377
        // Default config values
378
        $this->assertEquals(EncryptHelper::DEFAULT_DOMAIN_SIZE, $MyIndexedVarchar->getDomainSize());
379
        $this->assertEquals(EncryptHelper::DEFAULT_OUTPUT_SIZE, $MyIndexedVarchar->getOutputSize());
380
        $this->assertEquals(EncryptedDBField::LARGE_INDEX_SIZE, $MyIndexedVarchar->getIndexSize());
381
    }
382
383
    public function testIndexPlanner()
384
    {
385
        $sizes = EncryptHelper::planIndexSizesForClass(Test_EncryptedModel::class);
386
        $this->assertNotEmpty($sizes);
387
        $this->assertArrayHasKey("min", $sizes);
388
        $this->assertArrayHasKey("max", $sizes);
389
        $this->assertArrayHasKey("indexes", $sizes);
390
        $this->assertArrayHasKey("estimated_population", $sizes);
391
        $this->assertArrayHasKey("coincidence_count", $sizes);
392
    }
393
394
    public function testFixture()
395
    {
396
        // this one use nacl encryption and will be rotated transparently
397
        $model = $this->getTestModel();
398
399
        $result = $model->needsToRotateEncryption(EncryptHelper::getEngineForEncryption("nacl"));
400
        $this->assertTrue($result);
401
402
        // Ensure we have our blind indexes
403
        $this->assertTrue($model->hasDatabaseField('MyIndexedVarcharValue'));
404
        $this->assertTrue($model->hasDatabaseField('MyIndexedVarcharBlindIndex'));
405
        $this->assertTrue($model->hasDatabaseField('MyNumberValue'));
406
        $this->assertTrue($model->hasDatabaseField('MyNumberBlindIndex'));
407
        $this->assertTrue($model->hasDatabaseField('MyNumberLastFourBlindIndex'));
408
409
        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

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

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

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

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

979
        $decoded = json_decode($encryptedJsonData, /** @scrutinizer ignore-type */ JSON_OBJECT_AS_ARRAY);
Loading history...
980
981
        // it is properly encrypted if required
982
        $this->assertEquals($data['not_encrypted'], $decoded['not_encrypted']);
983
        $this->assertNotEquals($data['name'], $decoded['name']);
984
985
        // we can write
986
        $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...
987
        $model->write();
988
989
        $dbData = DB::query("SELECT MyEncryptedJson FROM EncryptedModel WHERE ID = " . $model->ID)->value();
990
        $decodedDbData = json_decode($dbData, JSON_OBJECT_AS_ARRAY);
991
992
        // data is properly stored with partially encrypted json
993
        $this->assertNotNull($decodedDbData, "got $dbData");
994
        $this->assertEquals($data['not_encrypted'], $decodedDbData['not_encrypted']);
995
        $this->assertNotEquals($data['name'], $decodedDbData['name']);
996
997
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $model->ID)->first();
998
        $freshValue = $freshRecord->dbObject('MyEncryptedJson')->toArray();
999
1000
        // It is decoded transparently
1001
        $this->assertEquals($data, $freshValue);
1002
    }
1003
1004
    public function testFashHash()
1005
    {
1006
        $model = $this->getTestModel();
1007
1008
        /** @var EncryptedDBField $encrField */
1009
        $encrField = $model->dbObject('MyIndexedVarchar');
1010
1011
        $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...
1012
        $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...
1013
1014
        $aad = '';
1015
1016
        $t = microtime(true);
1017
        $slowBi = $encrField->getEncryptedField(null, false)->prepareForStorage($value, $aad);
1018
        $slowBi2 = $encrField->getEncryptedField(null, false)->prepareForStorage($value, $aad);
1019
        $et = microtime(true) - $t;
1020
1021
        $t2 = microtime(true);
1022
        $fastBi = $encrField->getEncryptedField(null, true)->prepareForStorage($value, $aad);
1023
        $fastBi2 = $encrField->getEncryptedField(null, true)->prepareForStorage($value, $aad);
1024
        $et2 = microtime(true) - $t2;
1025
1026
        // Values are not equals, but blind indexes are
1027
        $this->assertNotEquals($slowBi2[0], $slowBi[0]);
1028
        $this->assertEquals($slowBi2[1]['MyIndexedVarcharBlindIndex'], $slowBi[1]['MyIndexedVarcharBlindIndex']);
1029
        $this->assertEquals($bi, $slowBi[1]['MyIndexedVarcharBlindIndex']);
1030
        $this->assertNotEquals($fastBi2[0], $fastBi[0]);
1031
        $this->assertEquals($fastBi2[1]['MyIndexedVarcharBlindIndex'], $fastBi[1]['MyIndexedVarcharBlindIndex']);
1032
1033
        // Slow indexes are not the same as fast indexes
1034
        $this->assertNotEquals($fastBi[1]['MyIndexedVarcharBlindIndex'], $slowBi[1]['MyIndexedVarcharBlindIndex']);
1035
        $this->assertNotEquals($fastBi2[1]['MyIndexedVarcharBlindIndex'], $slowBi2[1]['MyIndexedVarcharBlindIndex']);
1036
1037
        // It is faster to generate fast indexes
1038
        // $et2 = 0.0004119873046875
1039
        // $et = 0.0683131217956543
1040
1041
        $this->assertTrue($et2 <= $et);
1042
1043
        // We can convert and keep stored values readable
1044
1045
        $result = EncryptHelper::convertHashType($model, 'MyIndexedVarchar');
1046
        $this->assertTrue($result);
1047
1048
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $model->ID)->first();
1049
        $freshValue = (string)$freshRecord->MyIndexedVarchar;
1050
        $this->assertEquals($value, $freshValue);
1051
1052
        // We can find it using new hash
1053
        /** @var EncryptedDBField $freshEncrField */
1054
        $freshEncrField = $freshRecord->dbObject('MyIndexedVarchar');
1055
1056
        $blindIndex = $freshEncrField->getEncryptedField(null, true)->getBlindIndex($freshValue, 'MyIndexedVarcharBlindIndex');
1057
        $freshRecord2 = Test_EncryptedModel::get()->filter('MyIndexedVarcharBlindIndex', $blindIndex)->first();
1058
        $this->assertEquals($freshRecord2->ID, $freshRecord->ID);
1059
    }
1060
}
1061