Passed
Push — master ( 5a9348...48ccd3 )
by Thomas
03:36
created

EncryptTest::writeDataFromYml()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

675
        $this->assertTrue($encryptedFile2->/** @scrutinizer ignore-call */ isEncrypted());
Loading history...
676
677
        $encryptedFile->encryptFileIfNeeded();
678
679
        $this->assertTrue($encryptedFile->isEncrypted());
680
        $this->assertTrue($encryptedFile->Encrypted);
681
682
        // still encrypted?
683
        $encryptedFile->encryptFileIfNeeded();
684
        $this->assertTrue($encryptedFile->isEncrypted());
685
        $this->assertTrue($encryptedFile->Encrypted);
686
687
        // set something new
688
        $string = 'Some content';
689
        $stream = fopen('php://memory', 'r+');
690
        fwrite($stream, $string);
691
        rewind($stream);
692
        $encryptedFile->setFromStream($stream, 'secret.doc');
693
        $encryptedFile->write();
694
        $encryptedFile2->setFromStream($stream, 'secret.doc');
695
        $encryptedFile2->write();
696
697
        // we need to update manually
698
        $encryptedFile->updateEncryptionStatus();
699
700
        // It is not encrypted nor marked as such
701
        $this->assertFalse($encryptedFile->isEncrypted());
702
        $this->assertFalse($encryptedFile->Encrypted);
703
        // Ir was automatically encrypted again
704
        $this->assertTrue($encryptedFile2->isEncrypted());
705
        $this->assertTrue($encryptedFile2->Encrypted);
706
707
        // No file => no encryption
708
        $encryptedFile2->deleteFile();
709
        $this->assertFalse($encryptedFile->isEncrypted());
710
    }
711
712
    /**
713
     * @group only
714
     */
715
    public function testMessageEncryption()
716
    {
717
        $admin = $this->getAdminMember();
718
        $user1 = $this->getUser1Member();
719
720
        $adminKeys = Test_EncryptionKey::getKeyPair($admin->ID);
721
        $user1Keys = Test_EncryptionKey::getKeyPair($user1->ID);
722
723
        $this->assertArrayHasKey("public", $adminKeys);
0 ignored issues
show
Bug introduced by
It seems like $adminKeys can also be of type false; however, parameter $array of PHPUnit_Framework_Assert::assertArrayHasKey() does only seem to accept ArrayAccess|array, maybe add an additional type check? ( Ignorable by Annotation )

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

723
        $this->assertArrayHasKey("public", /** @scrutinizer ignore-type */ $adminKeys);
Loading history...
724
        $this->assertArrayHasKey("secret", $adminKeys);
725
        $this->assertArrayHasKey("public", $user1Keys);
726
        $this->assertArrayHasKey("secret", $user1Keys);
727
728
        // $pairs = sodium_crypto_box_keypair();
729
        // $adminKeys['secret'] = sodium_crypto_box_secretkey($pairs);
730
        // $adminKeys['public'] = sodium_crypto_box_publickey($pairs);
731
732
        // $pairs = sodium_crypto_box_keypair();
733
        // $user1Keys['secret'] = sodium_crypto_box_secretkey($pairs);
734
        // $user1Keys['public'] = sodium_crypto_box_publickey($pairs);
735
736
        // $adminKeys['secret'] = Hex::encode($adminKeys['secret']);
737
        // $adminKeys['public'] = Hex::encode($adminKeys['public']);
738
        // $user1Keys['secret'] = Hex::encode($user1Keys['secret']);
739
        // $user1Keys['public'] = Hex::encode($user1Keys['public']);
740
741
        // $adminKeys['secret'] = Hex::decode($adminKeys['secret']);
742
        // $adminKeys['public'] = Hex::decode($adminKeys['public']);
743
        // $user1Keys['secret'] = Hex::decode($user1Keys['secret']);
744
        // $user1Keys['public'] = Hex::decode($user1Keys['public']);
745
746
        $message = 'hello';
747
        // 24
748
        $nonce = random_bytes(SODIUM_CRYPTO_BOX_NONCEBYTES);
749
        $encryption_key = sodium_crypto_box_keypair_from_secretkey_and_publickey($adminKeys['secret'], $user1Keys['public']);
750
        $encrypted = sodium_crypto_box($message, $nonce, $encryption_key);
751
        $this->assertNotEmpty($encrypted);
752
        $this->assertNotEquals($message, $encrypted);
753
754
        // Revert keys to decrypt
755
        $decryption_key = sodium_crypto_box_keypair_from_secretkey_and_publickey($user1Keys['secret'], $adminKeys['public']);
756
        $decrypted = sodium_crypto_box_open($encrypted, $nonce, $decryption_key);
757
        $this->assertNotEmpty($decrypted);
758
        $this->assertEquals($message, $decrypted);
759
    }
760
761
    protected function getMultiTenantProvider()
762
    {
763
        $members = $this->getAllMembers();
764
        $tenants = [];
765
        foreach ($members as $member) {
766
            // You can also use the secret key from a keypair
767
            // $key = Test_EncryptionKey::getForMember($member->ID);
768
            $keyPair = Test_EncryptionKey::getKeyPair($member->ID);
769
            if ($keyPair) {
770
                $tenants[$member->ID] = new StringProvider($keyPair['secret']);
771
                // $tenants[$member->ID] = new StringProvider($key);
772
            }
773
        }
774
        $provider = new MemberKeyProvider($tenants);
775
        return $provider;
776
    }
777
778
    /**
779
     * @group multi-tenant
780
     * @group only
781
     */
782
    public function testMultiTenantProvider()
783
    {
784
        // echo '<pre>';
785
        // print_r(EncryptHelper::generateKeyPair());
786
        // die();
787
        $admin = $this->getAdminMember();
788
        $user1 = $this->getUser1Member();
789
        $user2 = $this->getUser2Member();
790
791
        $adminModel = $this->getAdminTestModel();
792
        $user1Model = $this->getUser1TestModel();
793
        $user2Model = $this->getUser2TestModel();
794
795
        $provider = $this->getMultiTenantProvider();
796
797
        Security::setCurrentUser($admin);
798
        EncryptHelper::clearCipherSweet();
799
        $cs = EncryptHelper::getCipherSweet($provider);
800
801
        $this->assertInstanceOf(MultiTenantSafeBackendInterface::class, $cs->getBackend());
802
803
        $string = "my content";
804
        $record = new Test_EncryptedModel();
805
        // $record = Test_EncryptedModel::get()->filter('ID', $user2Model->ID)->first();
806
        $record->MyText = $string;
807
        // We need to set active tenant ourselves because orm records fields one by one
808
        // it doesn't go through injectMetadata
809
        $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...
810
        $record->write();
811
812
        // echo '<pre>';
813
        // print_r($this->fetchRawData(Test_EncryptedModel::class, $record->ID));
814
        // die();
815
816
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $record->ID)->first();
817
818
        $this->assertEquals($admin->ID, Security::getCurrentUser()->ID, "Make sure the right member is logged in");
819
        // He can decode
820
        $this->assertEquals($string, $freshRecord->MyText);
821
822
        // He can also decode his content from the db
823
        $adminRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
824
        // echo '<pre>';print_r($adminRecord);die();
825
        $this->assertEquals($string, $adminRecord->MyText);
826
827
        // He cannot decode
828
        Security::setCurrentUser($user1);
829
        // We don't need to set active tenant because our MemberKeyProvider reads currentUser automatically
830
        // $provider->setActiveTenant($user1->ID);
831
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $record->ID)->first();
832
        $this->assertNotEquals($string, $freshRecord->MyText);
833
834
        // Test tenant from row
835
        $this->assertEquals($admin->ID, $cs->getTenantFromRow($adminModel->toMap()));
836
        $this->assertEquals($user1->ID, $cs->getTenantFromRow($user1Model->toMap()));
837
        $this->assertEquals($user2->ID, $cs->getTenantFromRow($user2Model->toMap()));
838
839
        // Current user can decode what he can
840
        Security::setCurrentUser($admin);
841
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
842
        $this->assertEquals($string, $freshRecord->MyText, "Invalid content for admin model #{$adminModel->ID}");
843
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $user2Model->ID)->first();
844
        $this->assertNotEquals($string, $freshRecord->MyText, "Invalid content for user2 model #{$user2Model->ID}");
845
846
        // Thanks to getTenantFromRow we should be able to rotate encryption
847
        // rotate from admin to user2
848
        Security::setCurrentUser($user2);
849
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
850
        $freshRecord->MemberID = $user2->ID;
851
        $freshRecord->write();
852
        $this->assertNotEquals($string, $freshRecord->MyText);
853
        // We can keep the same provider but we need to clone it and change the active tenant
854
        $cs->setActiveTenant($user2->ID);
855
856
        // clone will not deep clone the key provider with the active tenant
857
        // $old = clone $cs;
858
        $clonedProvider = clone $provider;
859
        $clonedProvider->setForcedTenant($admin->ID);
860
        $old = EncryptHelper::getEngineWithProvider(EncryptHelper::getBackendForEncryption("brng"), $clonedProvider);
861
862
        $freshRecord->rotateEncryption($old);
863
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
864
        $this->assertEquals($string, $freshRecord->MyText);
865
866
        // Admin can't read anymore, don't forget to refresh record from db
867
        Security::setCurrentUser($admin);
868
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
869
        $this->assertNotEquals($string, $freshRecord->MyText);
870
871
        // Cleanup
872
        EncryptHelper::clearCipherSweet();
873
    }
874
875
    public function testJsonField()
876
    {
877
        $model = $this->getTestModel();
878
879
        $longstring = str_repeat("lorem ipsum loquor", 100);
880
        $array = [];
881
        foreach (range(1, 100) as $i) {
882
            $array["key_$i"] = $longstring . $i;
883
        }
884
885
        $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...
886
        $model->write();
887
888
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $model->ID)->first();
889
890
        $this->assertEquals(json_encode($array), $freshRecord->MyJson);
891
        $this->assertEquals(json_decode(json_encode($array)), $freshRecord->dbObject('MyJson')->decode());
892
        $this->assertEquals($array, $freshRecord->dbObject('MyJson')->toArray());
893
        $this->assertEquals($array, $freshRecord->dbObject('MyJson')->decodeArray());
894
        $this->assertEquals($model->dbObject('MyJson')->toArray(), $freshRecord->dbObject('MyJson')->toArray());
895
    }
896
}
897