@@ -7,8 +7,9 @@ const path = require('path');
7
7
const { deadlockTests } = require ( './client_side_encryption.prose.deadlock' ) ;
8
8
const { dropCollection, APMEventCollector } = require ( '../shared' ) ;
9
9
10
- const { EJSON } = BSON ;
10
+ const { EJSON , Binary } = BSON ;
11
11
const { LEGACY_HELLO_COMMAND } = require ( '../../../src/constants' ) ;
12
+ const { MongoNetworkError } = require ( '../../../src/error' ) ;
12
13
13
14
const getKmsProviders = ( localKey , kmipEndpoint , azureEndpoint , gcpEndpoint ) => {
14
15
const result = BSON . EJSON . parse ( process . env . CSFLE_KMS_PROVIDERS || '{}' ) ;
@@ -1424,4 +1425,224 @@ describe('Client Side Encryption Prose Tests', metadata, function () {
1424
1425
} ) ;
1425
1426
} ) ;
1426
1427
} ) ;
1428
+
1429
+ context ( '14. Decryption Events' , metadata , function ( ) {
1430
+ let setupClient ;
1431
+ let clientEncryption ;
1432
+ let keyId ;
1433
+ let cipherText ;
1434
+ let malformedCiphertext ;
1435
+ let encryptedClient ;
1436
+ let aggregateSucceeded ;
1437
+ let aggregateFailed ;
1438
+
1439
+ beforeEach ( async function ( ) {
1440
+ const mongodbClientEncryption = this . configuration . mongodbClientEncryption ;
1441
+ // Create a MongoClient named ``setupClient``.
1442
+ setupClient = this . configuration . newClient ( ) ;
1443
+ // Drop and create the collection ``db.decryption_events``.
1444
+ const db = setupClient . db ( 'db' ) ;
1445
+ await dropCollection ( db , 'decryption_events' ) ;
1446
+ await db . createCollection ( 'decryption_events' ) ;
1447
+ // Create a ClientEncryption object named ``clientEncryption`` with these options:
1448
+ // ClientEncryptionOpts {
1449
+ // keyVaultClient: <setupClient>,
1450
+ // keyVaultNamespace: "keyvault.datakeys",
1451
+ // kmsProviders: { "local": { "key": <base64 decoding of LOCAL_MASTERKEY> } }
1452
+ // }
1453
+ clientEncryption = new mongodbClientEncryption . ClientEncryption ( setupClient , {
1454
+ keyVaultNamespace : 'keyvault.datakeys' ,
1455
+ kmsProviders : getKmsProviders ( LOCAL_KEY ) ,
1456
+ bson : BSON
1457
+ } ) ;
1458
+ // Create a data key with the "local" KMS provider.
1459
+ // Storing the result in a variable named ``keyID``.
1460
+ keyId = await clientEncryption . createDataKey ( 'local' ) ;
1461
+ // Use ``clientEncryption`` to encrypt the string "hello" with the following ``EncryptOpts``:
1462
+ // EncryptOpts {
1463
+ // keyId: <keyID>,
1464
+ // algorithm: "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
1465
+ // }
1466
+ // Store the result in a variable named ``ciphertext``.
1467
+ cipherText = await clientEncryption . encrypt ( 'hello' , {
1468
+ keyId : keyId ,
1469
+ algorithm : 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
1470
+ } ) ;
1471
+ // Copy ``ciphertext`` into a variable named ``malformedCiphertext``.
1472
+ // Change the last byte to 0. This will produce an invalid HMAC tag.
1473
+ const buffer = Buffer . from ( cipherText . buffer ) ;
1474
+ buffer . writeInt8 ( 0 , buffer . length - 1 ) ;
1475
+ malformedCiphertext = new Binary ( buffer , 6 ) ;
1476
+ // Create a MongoClient named ``encryptedClient`` with these ``AutoEncryptionOpts``:
1477
+ // AutoEncryptionOpts {
1478
+ // keyVaultNamespace: "keyvault.datakeys";
1479
+ // kmsProviders: { "local": { "key": <base64 decoding of LOCAL_MASTERKEY> } }
1480
+ // }
1481
+ // Configure ``encryptedClient`` with "retryReads=false".
1482
+ encryptedClient = this . configuration . newClient (
1483
+ { } ,
1484
+ {
1485
+ retryReads : false ,
1486
+ monitorCommands : true ,
1487
+ autoEncryption : {
1488
+ keyVaultNamespace : 'keyvault.datakeys' ,
1489
+ kmsProviders : getKmsProviders ( LOCAL_KEY )
1490
+ }
1491
+ }
1492
+ ) ;
1493
+ // Register a listener for CommandSucceeded events on ``encryptedClient``.
1494
+ encryptedClient . on ( 'commandSucceeded' , event => {
1495
+ if ( event . commandName === 'aggregate' ) {
1496
+ aggregateSucceeded = event ;
1497
+ }
1498
+ } ) ;
1499
+ // The listener must store the most recent CommandFailedEvent error for the "aggregate" command.
1500
+ encryptedClient . on ( 'commandFailed' , event => {
1501
+ if ( event . commandName === 'aggregate' ) {
1502
+ aggregateFailed = event ;
1503
+ }
1504
+ } ) ;
1505
+ } ) ;
1506
+
1507
+ afterEach ( async function ( ) {
1508
+ aggregateSucceeded = undefined ;
1509
+ aggregateFailed = undefined ;
1510
+ await setupClient . close ( ) ;
1511
+ await encryptedClient . close ( ) ;
1512
+ } ) ;
1513
+
1514
+ context ( 'Case 1: Command Error' , metadata , function ( ) {
1515
+ beforeEach ( async function ( ) {
1516
+ // Use ``setupClient`` to configure the following failpoint:
1517
+ // {
1518
+ // "configureFailPoint": "failCommand",
1519
+ // "mode": {
1520
+ // "times": 1
1521
+ // },
1522
+ // "data": {
1523
+ // "errorCode": 123,
1524
+ // "failCommands": [
1525
+ // "aggregate"
1526
+ // ]
1527
+ // }
1528
+ // }
1529
+ await setupClient
1530
+ . db ( )
1531
+ . admin ( )
1532
+ . command ( {
1533
+ configureFailPoint : 'failCommand' ,
1534
+ mode : {
1535
+ times : 1
1536
+ } ,
1537
+ data : {
1538
+ errorCode : 123 ,
1539
+ failCommands : [ 'aggregate' ]
1540
+ }
1541
+ } ) ;
1542
+ } ) ;
1543
+
1544
+ it ( 'expects an error and a command failed event' , async function ( ) {
1545
+ // Use ``encryptedClient`` to run an aggregate on ``db.decryption_events``.
1546
+ // Expect an exception to be thrown from the command error. Expect a CommandFailedEvent.
1547
+ const collection = encryptedClient . db ( 'db' ) . collection ( 'decryption_events' ) ;
1548
+ try {
1549
+ await collection . aggregate ( [ ] ) . toArray ( ) ;
1550
+ expect . fail ( 'aggregate must fail with error' ) ;
1551
+ } catch ( error ) {
1552
+ expect ( error . code ) . to . equal ( 123 ) ;
1553
+ }
1554
+ expect ( aggregateFailed . failure . code ) . to . equal ( 123 ) ;
1555
+ } ) ;
1556
+ } ) ;
1557
+
1558
+ context ( 'Case 2: Network Error' , metadata , function ( ) {
1559
+ beforeEach ( async function ( ) {
1560
+ // Use ``setupClient`` to configure the following failpoint:
1561
+ // {
1562
+ // "configureFailPoint": "failCommand",
1563
+ // "mode": {
1564
+ // "times": 1
1565
+ // },
1566
+ // "data": {
1567
+ // "errorCode": 123,
1568
+ // "closeConnection": true,
1569
+ // "failCommands": [
1570
+ // "aggregate"
1571
+ // ]
1572
+ // }
1573
+ // }
1574
+ await setupClient
1575
+ . db ( )
1576
+ . admin ( )
1577
+ . command ( {
1578
+ configureFailPoint : 'failCommand' ,
1579
+ mode : {
1580
+ times : 1
1581
+ } ,
1582
+ data : {
1583
+ errorCode : 123 ,
1584
+ closeConnection : true ,
1585
+ failCommands : [ 'aggregate' ]
1586
+ }
1587
+ } ) ;
1588
+ } ) ;
1589
+
1590
+ it ( 'expects an error and a command failed event' , async function ( ) {
1591
+ // Use ``encryptedClient`` to run an aggregate on ``db.decryption_events``.
1592
+ // Expect an exception to be thrown from the network error. Expect a CommandFailedEvent.
1593
+ const collection = encryptedClient . db ( 'db' ) . collection ( 'decryption_events' ) ;
1594
+ try {
1595
+ await collection . aggregate ( [ ] ) . toArray ( ) ;
1596
+ expect . fail ( 'aggregate must fail with error' ) ;
1597
+ } catch ( error ) {
1598
+ expect ( error ) . to . be . instanceOf ( MongoNetworkError ) ;
1599
+ }
1600
+ expect ( aggregateFailed . failure . message ) . to . include ( 'closed' ) ;
1601
+ } ) ;
1602
+ } ) ;
1603
+
1604
+ context ( 'Case 3: Decrypt Error' , metadata , function ( ) {
1605
+ it ( 'errors on decryption but command succeeds' , async function ( ) {
1606
+ // Use ``encryptedClient`` to insert the document ``{ "encrypted": <malformedCiphertext> }``
1607
+ // into ``db.decryption_events``.
1608
+ // Use ``encryptedClient`` to run an aggregate on ``db.decryption_events``.
1609
+ // Expect an exception to be thrown from the decryption error.
1610
+ // Expect a CommandSucceededEvent. Expect the CommandSucceededEvent.reply
1611
+ // to contain BSON binary for the field
1612
+ // ``cursor.firstBatch.encrypted``.
1613
+ const collection = encryptedClient . db ( 'db' ) . collection ( 'decryption_events' ) ;
1614
+ await collection . insertOne ( { encrypted : malformedCiphertext } ) ;
1615
+ try {
1616
+ await collection . aggregate ( [ ] ) . toArray ( ) ;
1617
+ expect . fail ( 'aggregate must fail with error' ) ;
1618
+ } catch ( error ) {
1619
+ expect ( error . message ) . to . include ( 'HMAC validation failure' ) ;
1620
+ }
1621
+ const doc = aggregateSucceeded . reply . cursor . firstBatch [ 0 ] ;
1622
+ expect ( doc . encrypted ) . to . be . instanceOf ( Binary ) ;
1623
+ } ) ;
1624
+ } ) ;
1625
+
1626
+ context ( 'Case 4: Decrypt Success' , metadata , function ( ) {
1627
+ it ( 'succeeds on decryption and command succeeds' , async function ( ) {
1628
+ // Use ``encryptedClient`` to insert the document ``{ "encrypted": <ciphertext> }``
1629
+ // into ``db.decryption_events``.
1630
+ // Use ``encryptedClient`` to run an aggregate on ``db.decryption_events``.
1631
+ // Expect no exception.
1632
+ // Expect a CommandSucceededEvent. Expect the CommandSucceededEvent.reply
1633
+ // to contain BSON binary for the field ``cursor.firstBatch.encrypted``.
1634
+ const collection = encryptedClient . db ( 'db' ) . collection ( 'decryption_events' ) ;
1635
+ await collection . insertOne ( { encrypted : cipherText } ) ;
1636
+ let result ;
1637
+ try {
1638
+ result = await collection . aggregate ( [ ] ) . toArray ( ) ;
1639
+ } catch ( error ) {
1640
+ expect . fail ( `aggregate must not fail, got ${ error . message } ` ) ;
1641
+ }
1642
+ expect ( result [ 0 ] . encrypted ) . to . equal ( 'hello' ) ;
1643
+ const doc = aggregateSucceeded . reply . cursor . firstBatch [ 0 ] ;
1644
+ expect ( doc . encrypted ) . to . be . instanceOf ( Binary ) ;
1645
+ } ) ;
1646
+ } ) ;
1647
+ } ) ;
1427
1648
} ) ;
0 commit comments