|
10 | 10 | use MongoDB\Driver\ClientEncryption;
|
11 | 11 | use MongoDB\Driver\Exception\AuthenticationException;
|
12 | 12 | use MongoDB\Driver\Exception\BulkWriteException;
|
| 13 | +use MongoDB\Driver\Exception\CommandException; |
13 | 14 | use MongoDB\Driver\Exception\ConnectionException;
|
14 | 15 | use MongoDB\Driver\Exception\ConnectionTimeoutException;
|
15 | 16 | use MongoDB\Driver\Exception\EncryptionException;
|
16 | 17 | use MongoDB\Driver\Exception\RuntimeException;
|
| 18 | +use MongoDB\Driver\Monitoring\CommandFailedEvent; |
| 19 | +use MongoDB\Driver\Monitoring\CommandStartedEvent; |
| 20 | +use MongoDB\Driver\Monitoring\CommandSubscriber; |
| 21 | +use MongoDB\Driver\Monitoring\CommandSucceededEvent; |
17 | 22 | use MongoDB\Driver\WriteConcern;
|
18 | 23 | use MongoDB\Tests\CommandObserver;
|
19 | 24 | use PHPUnit\Framework\Assert;
|
|
37 | 42 | use function sprintf;
|
38 | 43 | use function str_repeat;
|
39 | 44 | use function strlen;
|
| 45 | +use function substr; |
40 | 46 | use function unserialize;
|
41 | 47 | use function version_compare;
|
42 | 48 |
|
@@ -1393,6 +1399,155 @@ static function (self $test, ClientEncryption $clientEncryption, Client $encrypt
|
1393 | 1399 | ];
|
1394 | 1400 | }
|
1395 | 1401 |
|
| 1402 | + /** |
| 1403 | + * Prose test 14: Decryption Events |
| 1404 | + * |
| 1405 | + * @see https://github.com/mongodb/specifications/tree/master/source/client-side-encryption/tests#decryption-events |
| 1406 | + * @dataProvider provideDecryptionEventsTests |
| 1407 | + */ |
| 1408 | + public function testDecryptionEvents(Closure $test): void |
| 1409 | + { |
| 1410 | + // Test setup |
| 1411 | + $setupClient = static::createTestClient(); |
| 1412 | + $setupClient->selectCollection('db', 'decryption_events')->drop(); |
| 1413 | + |
| 1414 | + // Ensure that the key vault is dropped with a majority write concern |
| 1415 | + self::insertKeyVaultData($setupClient, []); |
| 1416 | + |
| 1417 | + $clientEncryption = new ClientEncryption([ |
| 1418 | + 'keyVaultClient' => $setupClient->getManager(), |
| 1419 | + 'keyVaultNamespace' => 'keyvault.datakeys', |
| 1420 | + 'kmsProviders' => ['local' => ['key' => new Binary(base64_decode(self::LOCAL_MASTERKEY), 0)]], |
| 1421 | + ]); |
| 1422 | + |
| 1423 | + $keyId = $clientEncryption->createDataKey('local'); |
| 1424 | + |
| 1425 | + $cipherText = $clientEncryption->encrypt('hello', [ |
| 1426 | + 'keyId' => $keyId, |
| 1427 | + 'algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC, |
| 1428 | + ]); |
| 1429 | + |
| 1430 | + // Flip the last byte in the encrypted string |
| 1431 | + $malformedCipherText = new Binary(substr($cipherText->getData(), 0, -1) . ~$cipherText->getData()[-1], Binary::TYPE_ENCRYPTED); |
| 1432 | + |
| 1433 | + $autoEncryptionOpts = [ |
| 1434 | + 'keyVaultNamespace' => 'keyvault.datakeys', |
| 1435 | + 'kmsProviders' => ['local' => ['key' => new Binary(base64_decode(self::LOCAL_MASTERKEY), 0)]], |
| 1436 | + ]; |
| 1437 | + |
| 1438 | + $encryptedClient = static::createTestClient(null, ['retryReads' => false], ['autoEncryption' => $autoEncryptionOpts]); |
| 1439 | + |
| 1440 | + $subscriber = new class implements CommandSubscriber { |
| 1441 | + public $lastAggregateReply; |
| 1442 | + public $lastAggregateError; |
| 1443 | + |
| 1444 | + public function commandStarted(CommandStartedEvent $event): void |
| 1445 | + { |
| 1446 | + } |
| 1447 | + |
| 1448 | + public function commandSucceeded(CommandSucceededEvent $event): void |
| 1449 | + { |
| 1450 | + if ($event->getCommandName() === 'aggregate') { |
| 1451 | + $this->lastAggregateReply = $event->getReply(); |
| 1452 | + } |
| 1453 | + } |
| 1454 | + |
| 1455 | + public function commandFailed(CommandFailedEvent $event): void |
| 1456 | + { |
| 1457 | + if ($event->getCommandName() === 'aggregate') { |
| 1458 | + $this->lastAggregateError = $event->getError(); |
| 1459 | + } |
| 1460 | + } |
| 1461 | + }; |
| 1462 | + |
| 1463 | + $encryptedClient->getManager()->addSubscriber($subscriber); |
| 1464 | + |
| 1465 | + $test($this, $setupClient, $clientEncryption, $encryptedClient, $subscriber, $cipherText, $malformedCipherText); |
| 1466 | + |
| 1467 | + $encryptedClient->getManager()->removeSubscriber($subscriber); |
| 1468 | + } |
| 1469 | + |
| 1470 | + public static function provideDecryptionEventsTests() |
| 1471 | + { |
| 1472 | + // See: https://github.com/mongodb/specifications/tree/master/source/client-side-encryption/tests#case-1-command-error |
| 1473 | + yield 'Case 1: Command Error' => [ |
| 1474 | + static function (self $test, Client $setupClient, ClientEncryption $clientEncryption, Client $encryptedClient, CommandSubscriber $subscriber, Binary $cipherText, Binary $malformedCipherText): void { |
| 1475 | + $setupClient->selectDatabase('admin')->command([ |
| 1476 | + 'configureFailPoint' => 'failCommand', |
| 1477 | + 'mode' => ['times' => 1], |
| 1478 | + 'data' => [ |
| 1479 | + 'errorCode' => 123, |
| 1480 | + 'failCommands' => ['aggregate'], |
| 1481 | + ], |
| 1482 | + ]); |
| 1483 | + |
| 1484 | + try { |
| 1485 | + $encryptedClient->selectCollection('db', 'decryption_events')->aggregate([]); |
| 1486 | + $test->fail('Expected exception to be thrown'); |
| 1487 | + } catch (CommandException $e) { |
| 1488 | + $test->assertSame(123, $e->getCode()); |
| 1489 | + } |
| 1490 | + |
| 1491 | + $test->assertNotNull($subscriber->lastAggregateError); |
| 1492 | + }, |
| 1493 | + ]; |
| 1494 | + |
| 1495 | + // See: https://github.com/mongodb/specifications/tree/master/source/client-side-encryption/tests#case-2-network-error |
| 1496 | + yield 'Case 2: Network Error' => [ |
| 1497 | + static function (self $test, Client $setupClient, ClientEncryption $clientEncryption, Client $encryptedClient, CommandSubscriber $subscriber, Binary $cipherText, Binary $malformedCipherText): void { |
| 1498 | + $setupClient->selectDatabase('admin')->command([ |
| 1499 | + 'configureFailPoint' => 'failCommand', |
| 1500 | + 'mode' => ['times' => 1], |
| 1501 | + 'data' => [ |
| 1502 | + 'closeConnection' => true, |
| 1503 | + 'failCommands' => ['aggregate'], |
| 1504 | + ], |
| 1505 | + ]); |
| 1506 | + |
| 1507 | + try { |
| 1508 | + $encryptedClient->selectCollection('db', 'decryption_events')->aggregate([]); |
| 1509 | + $test->fail('Expected exception to be thrown'); |
| 1510 | + } catch (ConnectionTimeoutException $e) { |
| 1511 | + $test->addToAssertionCount(1); |
| 1512 | + } |
| 1513 | + |
| 1514 | + $test->assertNotNull($subscriber->lastAggregateError); |
| 1515 | + }, |
| 1516 | + ]; |
| 1517 | + |
| 1518 | + // See: https://github.com/mongodb/specifications/tree/master/source/client-side-encryption/tests#case-3-decrypt-error |
| 1519 | + yield 'Case 3: Decrypt Error' => [ |
| 1520 | + static function (self $test, Client $setupClient, ClientEncryption $clientEncryption, Client $encryptedClient, CommandSubscriber $subscriber, Binary $cipherText, Binary $malformedCipherText): void { |
| 1521 | + $collection = $encryptedClient->selectCollection('db', 'decryption_events'); |
| 1522 | + |
| 1523 | + $collection->insertOne(['encrypted' => $malformedCipherText]); |
| 1524 | + |
| 1525 | + try { |
| 1526 | + $collection->aggregate([]); |
| 1527 | + $test->fail('Expected exception to be thrown'); |
| 1528 | + } catch (EncryptionException $e) { |
| 1529 | + $test->assertStringContainsString('HMAC validation failure', $e->getMessage()); |
| 1530 | + } |
| 1531 | + |
| 1532 | + $test->assertNotNull($subscriber->lastAggregateReply); |
| 1533 | + $test->assertEquals($malformedCipherText, $subscriber->lastAggregateReply->cursor->firstBatch[0]->encrypted ?? null); |
| 1534 | + }, |
| 1535 | + ]; |
| 1536 | + |
| 1537 | + // See: https://github.com/mongodb/specifications/tree/master/source/client-side-encryption/tests#case-4-decrypt-success |
| 1538 | + yield 'Case 4: Decrypt Success' => [ |
| 1539 | + static function (self $test, Client $setupClient, ClientEncryption $clientEncryption, Client $encryptedClient, CommandSubscriber $subscriber, Binary $cipherText, Binary $malformedCipherText): void { |
| 1540 | + $collection = $encryptedClient->selectCollection('db', 'decryption_events'); |
| 1541 | + |
| 1542 | + $collection->insertOne(['encrypted' => $cipherText]); |
| 1543 | + $collection->aggregate([]); |
| 1544 | + |
| 1545 | + $test->assertNotNull($subscriber->lastAggregateReply); |
| 1546 | + $test->assertEquals($cipherText, $subscriber->lastAggregateReply->cursor->firstBatch[0]->encrypted ?? null); |
| 1547 | + }, |
| 1548 | + ]; |
| 1549 | + } |
| 1550 | + |
1396 | 1551 | private function createInt64(string $value): Int64
|
1397 | 1552 | {
|
1398 | 1553 | $array = sprintf('a:1:{s:7:"integer";s:%d:"%s";}', strlen($value), $value);
|
|
0 commit comments