Passed
Push — master ( 461cd7...06c7d6 )
by Thomas
02:35
created

EncryptTest::testMessageEncryption()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 44
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 18
c 0
b 0
f 0
dl 0
loc 44
rs 9.6666
cc 1
nc 1
nop 0
1
<?php
2
3
namespace LeKoala\Encrypt\Test;
4
5
use Exception;
6
use SilverStripe\ORM\DB;
7
use SilverStripe\Assets\File;
8
use SilverStripe\ORM\DataList;
9
use ParagonIE\ConstantTime\Hex;
10
use SilverStripe\ORM\ArrayList;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\Security\Member;
13
use LeKoala\Encrypt\EncryptHelper;
14
use SilverStripe\Core\Environment;
15
use SilverStripe\Dev\SapphireTest;
16
use Symfony\Component\Yaml\Parser;
17
use SilverStripe\Security\Security;
18
use LeKoala\Encrypt\EncryptedDBField;
19
use LeKoala\Encrypt\MemberKeyProvider;
20
use ParagonIE\CipherSweet\CipherSweet;
21
use LeKoala\Encrypt\HasEncryptedFields;
22
use SilverStripe\ORM\Queries\SQLSelect;
23
use SilverStripe\ORM\Queries\SQLUpdate;
24
use SilverStripe\i18n\Messages\YamlReader;
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
        parent::setUp();
58
        EncryptHelper::setAutomaticDecryption(true);
59
60
        // test extension is available
61
        if (!extension_loaded('sodium')) {
62
            throw new Exception("You must load sodium extension for this");
63
        }
64
65
        // The rows are already decrypted and changed due to the fixtures going through the ORM layer
66
        // $result = DB::query("SELECT * FROM EncryptedModel");
67
        // echo '<pre>';
68
        // print_r(iterator_to_array($result));
69
        // die();
70
71
        // $ymlParser = new Parser;
72
        // $ymlData = $ymlParser->parseFile(__DIR__ . '/EncryptTest.yml');
73
74
        // foreach ($ymlData["LeKoala\\Encrypt\\Test\\Test_EncryptedModel"] as $name => $data) {
75
        //     unset($data['Member']);
76
        //     $update = new SQLUpdate("EncryptedModel", $data, ["Name" => $data['Name']]);
77
        //     $update->execute();
78
        // }
79
    }
80
81
    public function tearDown()
82
    {
83
        parent::tearDown();
84
    }
85
86
    /**
87
     * @return Test_EncryptedModel
88
     */
89
    public function getTestModel()
90
    {
91
        return $this->objFromFixture(Test_EncryptedModel::class, 'demo');
92
    }
93
94
    /**
95
     * @return Test_EncryptedModel
96
     */
97
    public function getTestModel2()
98
    {
99
        return $this->objFromFixture(Test_EncryptedModel::class, 'demo2');
100
    }
101
102
    /**
103
     * @return Test_EncryptedModel
104
     */
105
    public function getTestModel3()
106
    {
107
        return $this->objFromFixture(Test_EncryptedModel::class, 'demo3');
108
    }
109
110
    /**
111
     * @return Test_EncryptedModel
112
     */
113
    public function getAdminTestModel()
114
    {
115
        return $this->objFromFixture(Test_EncryptedModel::class, 'admin_record');
116
    }
117
118
    /**
119
     * @return Test_EncryptedModel
120
     */
121
    public function getUser1TestModel()
122
    {
123
        return $this->objFromFixture(Test_EncryptedModel::class, 'user1_record');
124
    }
125
126
    /**
127
     * @return Test_EncryptedModel
128
     */
129
    public function getUser2TestModel()
130
    {
131
        return $this->objFromFixture(Test_EncryptedModel::class, 'user2_record');
132
    }
133
134
    /**
135
     * @return Member
136
     */
137
    public function getAdminMember()
138
    {
139
        return $this->objFromFixture(Member::class, 'admin');
140
    }
141
142
    /**
143
     * @return Member
144
     */
145
    public function getUser1Member()
146
    {
147
        return $this->objFromFixture(Member::class, 'user1');
148
    }
149
150
    /**
151
     * @return Member
152
     */
153
    public function getUser2Member()
154
    {
155
        return $this->objFromFixture(Member::class, 'user2');
156
    }
157
158
    /**
159
     * @return DataList|Member[]
160
     */
161
    public function getAllMembers()
162
    {
163
        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...
164
            $this->getAdminMember(),
165
            $this->getUser1Member(),
166
            $this->getUser2Member(),
167
        ]);
168
    }
169
170
    /**
171
     * @return File
172
     */
173
    public function getRegularFile()
174
    {
175
        return $this->objFromFixture(File::class, 'regular');
176
    }
177
178
    /**
179
     * @return File
180
     */
181
    public function getEncryptedFile()
182
    {
183
        return $this->objFromFixture(File::class, 'encrypted');
184
    }
185
186
    /**
187
     * @param string $class
188
     * @param int $id
189
     * @return array
190
     */
191
    protected function fetchRawData($class, $id)
192
    {
193
        $tableName = DataObject::getSchema()->tableName($class);
194
        $columnIdentifier = DataObject::getSchema()->sqlColumnForField($class, 'ID');
195
        $sql = new SQLSelect('*', [$tableName], [$columnIdentifier => $id]);
196
        $dbRecord = $sql->firstRow()->execute()->first();
197
        return $dbRecord;
198
    }
199
200
    public function testEncryption()
201
    {
202
        $someText = 'some text';
203
        $encrypt = EncryptHelper::encrypt($someText);
204
        $decryptedValue = EncryptHelper::decrypt($encrypt);
205
206
        $this->assertEquals($someText, $decryptedValue);
207
    }
208
209
    public function testIndexes()
210
    {
211
        $indexes = DataObject::getSchema()->databaseIndexes(Test_EncryptedModel::class);
212
        $keys = array_keys($indexes);
213
        $this->assertContains('MyIndexedVarcharBlindIndex', $keys, "Index is not defined in : " . implode(", ", $keys));
214
        $this->assertContains('MyNumberLastFourBlindIndex', $keys, "Index is not defined in : " . implode(", ", $keys));
215
    }
216
217
    public function testSearch()
218
    {
219
        $singl = singleton(Test_EncryptedModel::class);
220
221
        /** @var EncryptedDBField $obj  */
222
        $obj = $singl->dbObject('MyIndexedVarchar');
223
        $record = $obj->fetchRecord('some_searchable_value');
224
225
        // echo '<pre>';print_r("From test: " . $record->MyIndexedVarchar);die();
226
        $this->assertNotEmpty($record);
227
        $this->assertEquals("some text text", $record->MyText);
228
        $this->assertEquals("some_searchable_value", $record->MyIndexedVarchar);
229
        $this->assertEquals("some_searchable_value", $record->dbObject('MyIndexedVarchar')->getValue());
230
231
        // Also search our super getter method
232
        $recordAlt = Test_EncryptedModel::getByBlindIndex('MyIndexedVarchar', 'some_searchable_value');
233
        $this->assertNotEmpty($record);
234
        $this->assertEquals($recordAlt->ID, $record->ID);
235
236
        // Can we get a list ?
237
        $list = Test_EncryptedModel::getAllByBlindIndex('MyIndexedVarchar', 'some_searchable_value');
238
        $this->assertInstanceOf(DataList::class, $list);
239
240
        $record = $obj->fetchRecord('some_unset_value');
241
        $this->assertEmpty($record);
242
243
        // Let's try our four digits index
244
        $obj = $singl->dbObject('MyNumber');
245
        $record = $obj->fetchRecord('6789', 'LastFourBlindIndex');
246
        $searchValue = $obj->getSearchValue('6789', 'LastFourBlindIndex');
247
        // $searchParams = $obj->getSearchParams('6789', 'LastFourBlindIndex');
248
        // print_r($searchParams);
249
        $this->assertNotEmpty($record, "Nothing found for $searchValue");
250
        $this->assertEquals("0123456789", $record->MyNumber);
251
    }
252
253
    public function testSearchFilter()
254
    {
255
        $record = Test_EncryptedModel::get()->filter('MyIndexedVarchar:Encrypted', 'some_searchable_value')->first();
256
        $this->assertNotEmpty($record);
257
        $this->assertEquals(1, $record->ID);
258
        $this->assertNotEquals(2, $record->ID);
259
260
        $record = Test_EncryptedModel::get()->filter('MyIndexedVarchar:Encrypted', 'some_unset_value')->first();
261
        $this->assertEmpty($record);
262
    }
263
264
    public function testRotation()
265
    {
266
        $model = $this->getTestModel3();
267
        $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...
268
269
        $old = EncryptHelper::getEngineForEncryption("nacl");
270
        $result = $model->needsToRotateEncryption($old);
271
        $this->assertTrue($result);
272
273
        $result = $model->rotateEncryption($old);
274
        $this->assertTrue($result);
275
    }
276
277
    public function testCompositeOptions()
278
    {
279
        $model = $this->getTestModel();
280
281
        /** @var EncryptedDBField $myNumber */
282
        $myNumber = $model->dbObject('MyNumber');
283
284
        $this->assertEquals(10, $myNumber->getDomainSize());
285
        $this->assertEquals(4, $myNumber->getOutputSize());
286
        $this->assertEquals(EncryptedDBField::LARGE_INDEX_SIZE, $myNumber->getIndexSize());
287
288
        /** @var EncryptedDBField $MyIndexedVarchar */
289
        $MyIndexedVarchar = $model->dbObject('MyIndexedVarchar');
290
291
        // Default config values
292
        $this->assertEquals(EncryptHelper::DEFAULT_DOMAIN_SIZE, $MyIndexedVarchar->getDomainSize());
293
        $this->assertEquals(EncryptHelper::DEFAULT_OUTPUT_SIZE, $MyIndexedVarchar->getOutputSize());
294
        $this->assertEquals(EncryptedDBField::LARGE_INDEX_SIZE, $MyIndexedVarchar->getIndexSize());
295
    }
296
297
    public function testIndexPlanner()
298
    {
299
        $sizes = EncryptHelper::planIndexSizesForClass(Test_EncryptedModel::class);
300
        $this->assertNotEmpty($sizes);
301
        $this->assertArrayHasKey("min", $sizes);
302
        $this->assertArrayHasKey("max", $sizes);
303
        $this->assertArrayHasKey("indexes", $sizes);
304
        $this->assertArrayHasKey("estimated_population", $sizes);
305
        $this->assertArrayHasKey("coincidence_count", $sizes);
306
    }
307
308
    public function testFixture()
309
    {
310
        // this one use nacl encryption and will be rotated transparently
311
        $model = $this->getTestModel();
312
313
        $result = $model->needsToRotateEncryption(EncryptHelper::getEngineForEncryption("nacl"));
314
        $this->assertTrue($result);
315
316
        // Ensure we have our blind indexes
317
        $this->assertTrue($model->hasDatabaseField('MyIndexedVarcharValue'));
318
        $this->assertTrue($model->hasDatabaseField('MyIndexedVarcharBlindIndex'));
319
        $this->assertTrue($model->hasDatabaseField('MyNumberValue'));
320
        $this->assertTrue($model->hasDatabaseField('MyNumberBlindIndex'));
321
        $this->assertTrue($model->hasDatabaseField('MyNumberLastFourBlindIndex'));
322
323
        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

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

413
        if (class_uses($model, /** @scrutinizer ignore-type */ HasEncryptedFields::class)) {
Loading history...
414
            $this->assertTrue($model->hasEncryptedField('MyVarchar'));
415
            $this->assertTrue($model->hasEncryptedField('MyIndexedVarchar'));
416
        }
417
418
419
        // print_r($model);
420
        /*
421
        [record:protected] => Array
422
        (
423
            [ClassName] => LeKoala\Encrypt\Test\Test_EncryptedModel
424
            [LastEdited] => 2021-07-07 13:38:48
425
            [Created] => 2021-07-07 13:38:48
426
            [Name] => demo2
427
            [MyText] => brng:XLzehy47IgENco4DcZj75u9D2p53UjDMCmTFGPNdmzYYxVVbDsaVWuZP1dTvIDaYagVggNAxT8S9fUTXw55VyIv6OxYJrQ==
428
            [MyHTMLText] => brng:bJ-6iGa-gjl9M6-UaNvtSrRuFLwDTLC6SIekrPHTcN_nmIUaK_VEFNAGVd3q__siNsvVXLreSlunpSyJ4JmF8eyI12ltz_s-eV6WVXw=
429
            [MyVarchar] => brng:qNEVUW3TS6eACSS4v1_NK0FOiG5JnbihmOHR1DU4L8Pt63OXQIJr_Kpd34J1IHaJXZWt4uuk2SZgskmvf8FrfApag_sRypca87MegXg_wQ==
430
            [RegularFileID] => 0
431
            [EncryptedFileID] => 0
432
            [MyNumberValue] => brng:pKYd8mXDduwhudwWeoE_ByO6IkvVlykVa6h09DTYFdHcb52yA1R5yhTEqQQjz1ndADFRa9WLLM3_e1U8PfPTiP4E
433
            [MyNumberBlindIndex] => a1de44f9
434
            [MyNumberLastFourBlindIndex] => addb
435
            [MyIndexedVarcharValue] => brng:TBD63tu-P9PluzI_zKTZ17P-4bhFvhbW7eOeSOOnDEf7n3Ytv2_52rlvGTVSJeWr5f6Z5eqrxi-RL5B6V0PrUmEqhfE2TGt-IdH5hfU=
436
            [MyIndexedVarcharBlindIndex] => 216d113a
437
            [ID] => 2
438
            [RecordClassName] => LeKoala\Encrypt\Test\Test_EncryptedModel
439
        )
440
        */
441
442
        $varcharValue = 'encrypted varchar value';
443
        $varcharWithIndexValue = 'some_searchable_value';
444
        // regular fields are not affected
445
        $this->assertEquals('demo2', $model->Name);
446
447
        // get value
448
        $this->assertEquals($varcharValue, $model->dbObject('MyVarchar')->getValue());
449
        // encrypted fields work transparently when using trait
450
        $this->assertEquals($varcharValue, $model->MyVarchar);
451
452
453
        $this->assertTrue($model->dbObject('MyIndexedVarchar') instanceof EncryptedDBField);
454
        $this->assertTrue($model->dbObject('MyIndexedVarchar')->hasField('Value'));
455
456
        $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...
457
        $model->write();
458
        $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...
459
460
        $dbRecord = $this->fetchRawData(get_class($model), $model->ID);
461
        // print_r($dbRecord);
462
        /*
463
        Array
464
(
465
    [ID] => 2
466
    [ClassName] => LeKoala\Encrypt\Test\Test_EncryptedModel
467
    [LastEdited] => 2021-07-07 13:52:10
468
    [Created] => 2021-07-07 13:52:08
469
    [Name] => demo2
470
    [MyText] => brng:IQ-6VoXJedlAGdoCPFUVTSnipUPR4k9YSi3Ik8_oPfUmMVDhA1kgTBFdG_6k08xLhD39G0ksVD_nMtUF4Opo6Zxgkc5Qww==
471
    [MyHTMLText] => brng:ATmS8Tooc0j2FN5zB8ojmhgNHD-vncvm1ljX8aF7rR6bbsD8pEwyX7BJ3mPg6WEzwyye4uriGskFy30GL9LEKsGs1hs40JJgs6rgwKA=
472
    [MyVarchar] => brng:zxu2RFNjqDGV0JmxF1WPMtxDKTyfOtvVztXfbnV3aYJAzro7RwHhSs8HhasHvdPOQ2Vxi_oDieRgcE8XeP3nyoF3tYJrJp3Mo9XdYXj2tw==
473
    [RegularFileID] => 0
474
    [EncryptedFileID] => 0
475
    [MyNumberValue] => brng:pKYd8mXDduwhudwWeoE_ByO6IkvVlykVa6h09DTYFdHcb52yA1R5yhTEqQQjz1ndADFRa9WLLM3_e1U8PfPTiP4E
476
    [MyNumberBlindIndex] => a1de44f9
477
    [MyNumberLastFourBlindIndex] => addb
478
    [MyIndexedVarcharValue] => brng:0ow_r7UD3FXYXxq7kjVzA3uY1ThFYfAWxZFAHA0aRoohLfQW_ZBa0Q8w5A3hyLJhT6djM6xR43O_jeEfP-w_fRaH3nXRI5RW7tO78JY=
479
    [MyIndexedVarcharBlindIndex] => 216d113a
480
)
481
*/
482
        $this->assertNotEquals($varcharValue, $dbRecord['MyVarchar']);
483
        $this->assertNotEmpty($dbRecord['MyVarchar']);
484
        $this->assertTrue(EncryptHelper::isEncrypted($dbRecord['MyVarchar']));
485
    }
486
487
    public function testRecordIsEncrypted()
488
    {
489
        $model = new Test_EncryptedModel();
490
491
        // echo "*** start \n";
492
        // Let's write some stuff
493
        $someText = 'some text';
494
        $model->MyText = $someText . ' text';
495
        $model->MyHTMLText = '<p>' . $someText . ' html</p>';
496
        $model->MyVarchar = 'encrypted varchar value';
497
        $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...
498
        $model->MyNumber = "0123456789";
499
        // All fields are marked as changed, including "hidden" fields
500
        // MyNumber will mark as changed MyNumber, MyNumberValue, MuNumberBlindIndex, MyNumberLastFourBlindIndex
501
        // echo '<pre>';
502
        // print_r(array_keys($model->getChangedFields()));
503
        // die();
504
        $id = $model->write();
505
506
        $this->assertNotEmpty($id);
507
508
        // For the model, its the same
509
        $this->assertEquals($someText . ' text', $model->MyText);
510
        $this->assertEquals($someText . ' text', $model->dbObject('MyText')->getValue());
511
        $this->assertEquals($someText . ' text', $model->getField('MyText'));
512
        $this->assertEquals('<p>' . $someText . ' html</p>', $model->MyHTMLText);
513
514
        // In the db, it's not the same
515
        $dbRecord = $this->fetchRawData(get_class($model), $model->ID);
516
517
        if (!EncryptHelper::isEncrypted($dbRecord['MyIndexedVarcharValue'])) {
518
            print_r($dbRecord);
519
        }
520
521
        /*
522
(
523
    [ID] => 2
524
    [ClassName] => LeKoala\Encrypt\Test\Test_EncryptedModel
525
    [LastEdited] => 2020-12-15 10:20:39
526
    [Created] => 2020-12-15 10:20:39
527
    [Name] =>
528
    [MyText] => nacl:yA3XhjUpxE6cS3VMOVI4eqpolP1vRZDYjFySULZiazi9V3HSugC3t8KgImnGV5jP1VzEytVX
529
    [MyHTMLText] => nacl:F3D33dZ2O7qtlmkX-fiaYwSjAo6RC03aiAWRTkfSJOZikcSfezjwmi9DPJ4EO0hYeVc9faRgA3RmTDajRA==
530
    [MyVarchar] => nacl:POmdt3mTUSgPJw3ttfi2G9HgHAE4FRX4FQ5CSBicj4JsEwyPwrP-JKYGcs5drFYLId3cMVf6m8daUY7Ao4Cz
531
    [RegularFileID] => 0
532
    [EncryptedFileID] => 0
533
    [MyNumberValue] => nacl:2wFOX_qahm-HmzQPXvcBFhWCG1TaGQgeM7vkebLxRXDfMpzAxhxkExVgBi8caPYrwvA=
534
    [MyNumberBlindIndex] => 5e0bd888
535
    [MyNumberLastFourBlindIndex] => 276b
536
    [MyIndexedVarcharValue] => nacl:BLi-zF02t0Zet-ADP3RT8v5RTsM11WKIyjlJ1EVHIai2HwjxCIq92gfsay5zqiLic14dXtwigb1kI179QQ==
537
    [MyIndexedVarcharBlindIndex] => 04bb6edd
538
)
539
        */
540
        $text = isset($dbRecord['MyText']) ? $dbRecord['MyText'] : null;
541
        $this->assertNotEmpty($text);
542
        $this->assertNotEquals($someText, $text, "Data is not encrypted in the database");
543
        // Composite fields should work as well
544
        $this->assertNotEmpty($dbRecord['MyIndexedVarcharValue']);
545
        $this->assertNotEmpty($dbRecord['MyIndexedVarcharBlindIndex']);
546
547
        // Test save into
548
        $modelFieldsBefore = $model->getQueriedDatabaseFields();
549
        $model->MyIndexedVarchar = 'new_value';
550
        $dbObj = $model->dbObject('MyIndexedVarchar');
551
        // $dbObj->setValue('new_value', $model);
552
        // $dbObj->saveInto($model);
553
        $modelFields = $model->getQueriedDatabaseFields();
554
        // print_r($modelFields);
555
        $this->assertTrue($dbObj->isChanged());
556
        $changed = implode(", ", array_keys($model->getChangedFields()));
557
        $this->assertNotEquals($modelFieldsBefore['MyIndexedVarchar'], $modelFields['MyIndexedVarchar'], "It should not have the same value internally anymore");
558
        $this->assertTrue($model->isChanged('MyIndexedVarchar'), "Field is not properly marked as changed, only have : " . $changed);
559
        $this->assertEquals('new_value', $dbObj->getValue());
560
        $this->assertNotEquals('new_value', $modelFields['MyIndexedVarcharValue'], "Unencrypted value is not set on value field");
561
562
        // Somehow this is not working on travis? composite fields don't save encrypted data although it works locally
563
        $this->assertNotEquals("some_searchable_value", $dbRecord['MyIndexedVarcharValue'], "Data is not encrypted in the database");
564
565
        // if we load again ?
566
        // it should work thanks to our trait
567
        // by default, data will be loaded encrypted if we don't use the trait and call getField directly
568
        $model2 = $model::get()->byID($model->ID);
569
        $this->assertEquals($someText . ' text', $model2->MyText, "Data does not load properly");
570
        $this->assertEquals('<p>' . $someText . ' html</p>', $model2->MyHTMLText, "Data does not load properly");
571
    }
572
573
    public function testFileEncryption()
574
    {
575
        $regularFile = $this->getRegularFile();
576
        $encryptedFile = $this->getEncryptedFile();
577
578
        $this->assertEquals(0, $regularFile->Encrypted);
579
        $this->assertEquals(1, $encryptedFile->Encrypted);
580
581
        // test encryption
582
583
        $string = 'Some content';
584
585
        $stream = fopen('php://memory', 'r+');
586
        fwrite($stream, $string);
587
        rewind($stream);
588
589
        $encryptedFile->setFromStream($stream, 'secret.doc');
590
        $encryptedFile->write();
591
592
        $this->assertFalse($encryptedFile->isEncrypted());
593
594
        $encryptedFile->encryptFileIfNeeded();
595
596
        $this->assertTrue($encryptedFile->isEncrypted());
597
    }
598
599
    /**
600
     * @group only
601
     */
602
    public function testMessageEncryption()
603
    {
604
        $admin = $this->getAdminMember();
605
        $user1 = $this->getUser1Member();
606
607
        $adminKeys = Test_EncryptionKey::getKeyPair($admin->ID);
608
        $user1Keys = Test_EncryptionKey::getKeyPair($user1->ID);
609
610
        $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

610
        $this->assertArrayHasKey("public", /** @scrutinizer ignore-type */ $adminKeys);
Loading history...
611
        $this->assertArrayHasKey("secret", $adminKeys);
612
        $this->assertArrayHasKey("public", $user1Keys);
613
        $this->assertArrayHasKey("secret", $user1Keys);
614
615
        // $pairs = sodium_crypto_box_keypair();
616
        // $adminKeys['secret'] = sodium_crypto_box_secretkey($pairs);
617
        // $adminKeys['public'] = sodium_crypto_box_publickey($pairs);
618
619
        // $pairs = sodium_crypto_box_keypair();
620
        // $user1Keys['secret'] = sodium_crypto_box_secretkey($pairs);
621
        // $user1Keys['public'] = sodium_crypto_box_publickey($pairs);
622
623
        // $adminKeys['secret'] = Hex::encode($adminKeys['secret']);
624
        // $adminKeys['public'] = Hex::encode($adminKeys['public']);
625
        // $user1Keys['secret'] = Hex::encode($user1Keys['secret']);
626
        // $user1Keys['public'] = Hex::encode($user1Keys['public']);
627
628
        // $adminKeys['secret'] = Hex::decode($adminKeys['secret']);
629
        // $adminKeys['public'] = Hex::decode($adminKeys['public']);
630
        // $user1Keys['secret'] = Hex::decode($user1Keys['secret']);
631
        // $user1Keys['public'] = Hex::decode($user1Keys['public']);
632
633
        $message = 'hello';
634
        // 24
635
        $nonce = random_bytes(SODIUM_CRYPTO_BOX_NONCEBYTES);
636
        $encryption_key = sodium_crypto_box_keypair_from_secretkey_and_publickey($adminKeys['secret'], $user1Keys['public']);
637
        $encrypted = sodium_crypto_box($message, $nonce, $encryption_key);
638
        $this->assertNotEmpty($encrypted);
639
        $this->assertNotEquals($message, $encrypted);
640
641
        // Revert keys to decrypt
642
        $decryption_key = sodium_crypto_box_keypair_from_secretkey_and_publickey($user1Keys['secret'], $adminKeys['public']);
643
        $decrypted = sodium_crypto_box_open($encrypted, $nonce, $decryption_key);
644
        $this->assertNotEmpty($decrypted);
645
        $this->assertEquals($message, $decrypted);
646
    }
647
648
    /**
649
     * @group multi-tenant
650
     * @group only
651
     */
652
    public function testMultiTenantProvider()
653
    {
654
        // echo '<pre>';
655
        // print_r(EncryptHelper::generateKeyPair());
656
        // die();
657
        $admin = $this->getAdminMember();
658
        $user1 = $this->getUser1Member();
659
        $user2 = $this->getUser2Member();
660
        $members = $this->getAllMembers();
661
        $tenants = [];
662
        foreach ($members as $member) {
663
            // You can also use the secret key from a keypair
664
            // $key = Test_EncryptionKey::getForMember($member->ID);
665
            $keyPair = Test_EncryptionKey::getKeyPair($member->ID);
666
            if ($keyPair) {
667
                $tenants[$member->ID] = new StringProvider($keyPair['secret']);
668
                // $tenants[$member->ID] = new StringProvider($key);
669
            }
670
        }
671
        $adminModel = $this->getAdminTestModel();
672
        $user1Model = $this->getUser1TestModel();
673
        $user2Model = $this->getUser2TestModel();
674
675
        Security::setCurrentUser($admin);
676
        $provider = new MemberKeyProvider($tenants);
677
678
        EncryptHelper::clearCipherSweet();
679
        $cs = EncryptHelper::getCipherSweet($provider);
680
681
        $this->assertInstanceOf(MultiTenantSafeBackendInterface::class, $cs->getBackend());
682
683
        $string = "my content";
684
        $record = new Test_EncryptedModel();
685
        $record->MyText = $string;
686
        // We need to set active tenant ourselves because orm records fields one by one
687
        // it doesn't go through injectMetadata
688
        $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...
689
        $record->write();
690
691
        // echo '<pre>';
692
        // print_r($this->fetchRawData(Test_EncryptedModel::class, $record->ID));
693
        // die();
694
695
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $record->ID)->first();
696
697
        // He can decode
698
        $this->assertEquals($string, $freshRecord->MyText);
699
700
        // He can also decode his content from the db
701
        $adminRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
702
        $this->assertEquals($string, $adminRecord->MyText);
703
704
        // He cannot decode
705
        Security::setCurrentUser($user1);
706
        // We don't need to set active tenant because our MemberKeyProvider reads currentUser automatically
707
        // $provider->setActiveTenant($user1->ID);
708
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $record->ID)->first();
709
        $this->assertNotEquals($string, $freshRecord->MyText);
710
711
        // Test tenant from row
712
        $this->assertEquals($admin->ID, $cs->getTenantFromRow($adminModel->toMap()));
713
        $this->assertEquals($user1->ID, $cs->getTenantFromRow($user1Model->toMap()));
714
        $this->assertEquals($user2->ID, $cs->getTenantFromRow($user2Model->toMap()));
715
716
        // Current user can decode what he can
717
        Security::setCurrentUser($admin);
718
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
719
        $this->assertEquals($string, $freshRecord->MyText, "Invalid content for admin model #{$adminModel->ID}");
720
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $user2Model->ID)->first();
721
        $this->assertNotEquals($string, $freshRecord->MyText, "Invalid content for user2 model #{$user2Model->ID}");
722
723
        // Thanks to getTenantFromRow we should be able to rotate encryption
724
        // rotate from admin to user2
725
        Security::setCurrentUser($user2);
726
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
727
        $freshRecord->MemberID = $user2->ID;
728
        $freshRecord->write();
729
        $this->assertNotEquals($string, $freshRecord->MyText);
730
        // We can keep the same provider but we need to clone it and change the active tenant
731
        $cs->setActiveTenant($user2->ID);
732
733
        // clone will not deep clone the key provider with the active tenant
734
        // $old = clone $cs;
735
        $clonedProvider = clone $provider;
736
        $clonedProvider->setForcedTenant($admin->ID);
737
        $old = EncryptHelper::getEngineWithProvider(EncryptHelper::getBackendForEncryption("brng"), $clonedProvider);
738
739
        $freshRecord->rotateEncryption($old);
740
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
741
        $this->assertEquals($string, $freshRecord->MyText);
742
743
        // Admin can't read anymore, don't forget to refresh record from db
744
        Security::setCurrentUser($admin);
745
        $freshRecord = Test_EncryptedModel::get()->filter('ID', $adminModel->ID)->first();
746
        $this->assertNotEquals($string, $freshRecord->MyText);
747
748
        // Cleanup
749
        EncryptHelper::clearCipherSweet();
750
    }
751
}
752