@@ -7,8 +7,9 @@ const path = require('path');
77const { deadlockTests } = require ( './client_side_encryption.prose.deadlock' ) ;
88const { dropCollection, APMEventCollector } = require ( '../shared' ) ;
99
10- const { EJSON } = BSON ;
10+ const { EJSON , Binary } = BSON ;
1111const { LEGACY_HELLO_COMMAND } = require ( '../../../src/constants' ) ;
12+ const { MongoNetworkError } = require ( '../../../src/error' ) ;
1213
1314const getKmsProviders = ( localKey , kmipEndpoint , azureEndpoint , gcpEndpoint ) => {
1415 const result = BSON . EJSON . parse ( process . env . CSFLE_KMS_PROVIDERS || '{}' ) ;
@@ -1424,4 +1425,224 @@ describe('Client Side Encryption Prose Tests', metadata, function () {
14241425 } ) ;
14251426 } ) ;
14261427 } ) ;
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+ } ) ;
14271648} ) ;
0 commit comments