Skip to content

Commit 4a91444

Browse files
authored
test(NODE-4296): add csfle decryption events prose tests (#3295)
1 parent aa652ae commit 4a91444

File tree

1 file changed

+222
-1
lines changed

1 file changed

+222
-1
lines changed

test/integration/client-side-encryption/client_side_encryption.prose.test.js

Lines changed: 222 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ const path = require('path');
77
const { deadlockTests } = require('./client_side_encryption.prose.deadlock');
88
const { dropCollection, APMEventCollector } = require('../shared');
99

10-
const { EJSON } = BSON;
10+
const { EJSON, Binary } = BSON;
1111
const { LEGACY_HELLO_COMMAND } = require('../../../src/constants');
12+
const { MongoNetworkError } = require('../../../src/error');
1213

1314
const 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

Comments
 (0)