Thanks for this package, it was definitely helpful but in our application we were inserting docs into our collection using the native mongodb driver package so we’re already circumventing the insertion part and thus the find/fetch decryption didn’t work. Also we wanted to use AWS which requires a tiny bit of different configuration.
So here’s the code for anyone who wants to do it vanilla nodejs/mongodb within Meteor as of version METEOR@2.5
:
package.json
...
"aws4": "^1.11.0",
"mongodb": "^3.6.10",
"mongodb-client-encryption": "^1.2.7",
...
./settings.json
"kms": {
"accessKeyId": "...",
"secretAccessKey": "...",
"masterKey": "...",
"region": "..."
}
./server/csfle/helpers.js
import { MongoClient } from 'mongodb';
import { ClientEncryption } from 'mongodb-client-encryption';
module.exports = {
CsfleHelper: class {
constructor ({
kmsProviders = null,
masterKey = null,
keyAltNames = 'aws-data-key',
keyDB = 'encryption',
keyColl = '__keyVault',
schema = null,
connectionString = process.env.MONGO_URL,
mongocryptdBypassSpawn = false,
mongocryptdSpawnPath = 'mongocryptd',
} = {}) {
if (kmsProviders === null) {
throw new Error('kmsProviders is required');
}
if (masterKey === null) {
throw new Error('masterKey is required');
}
this.kmsProviders = kmsProviders;
this.masterKey = masterKey;
this.keyAltNames = keyAltNames;
this.keyDB = keyDB;
this.keyColl = keyColl;
this.keyVaultNamespace = `${keyDB}.${keyColl}`;
this.schema = schema;
this.connectionString = connectionString;
this.mongocryptdBypassSpawn = mongocryptdBypassSpawn;
this.mongocryptdSpawnPath = mongocryptdSpawnPath;
this.regularClient = null;
this.csfleClient = null;
}
/**
* Creates a unique, partial index in the key vault collection
* on the ``keyAltNames`` field.
*
* @param {MongoClient} client
*/
async ensureUniqueIndexOnKeyVault (client) {
try {
await client
.db(this.keyDB)
.collection(this.keyColl)
.createIndex('keyAltNames', {
unique: true,
partialFilterExpression: {
keyAltNames: {
$exists: true,
},
},
});
} catch (error) {
console.error(error);
}
}
/**
* In the guide, https://docs.mongodb.com/ecosystem/use-cases/client-side-field-level-encryption-guide/,
* we create the data key and then show that it is created by
* retreiving it using a findOne query. Here, in implementation, we only
* create the key if it doesn't already exist, ensuring we only have one
* local data key.
*
* @param {MongoClient} client
*/
async findOrCreateDataKey (client) {
const encryption = this.getEncryptionClient(client);
let dataKey = await client
.db(this.keyDB)
.collection(this.keyColl)
.findOne({ keyAltNames: { $in: [ this.keyAltNames ]}});
if (dataKey === null) {
dataKey = await encryption.createDataKey('aws', {
masterKey: this.masterKey,
keyAltNames: [ this.keyAltNames ],
});
return dataKey.toString('base64');
}
return dataKey._id.toString('base64');
}
async getRegularClient () {
const client = new MongoClient(this.connectionString, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
return await client.connect();
}
getEncryptionClient (client) {
const encryption = new ClientEncryption(client, {
keyVaultNamespace: this.keyVaultNamespace,
kmsProviders: this.kmsProviders,
});
return encryption;
}
encryptValue ({ encryptionClient, value }) {
return Promise.await(encryptionClient.encrypt(value, { keyAltName: this.keyAltNames, algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' }));
}
decryptValue (
{ encryptionClient,
value }
) {
return Promise.await(encryptionClient.decrypt(new MongoInternals.NpmModule.Binary(Buffer.from(value))));
}
},
};
./server/csfle/index.js
import { Meteor } from 'meteor/meteor';
import { CsfleHelper } from './helpers';
export const csfleHelper = new CsfleHelper({
kmsProviders: {
aws: {
accessKeyId: Meteor.settings.kms.accessKeyId,
secretAccessKey: Meteor.settings.kms.secretAccessKey,
},
},
masterKey: {
key: Meteor.settings.kms.masterKey,
region: Meteor.settings.kms.region,
},
});
./server/main.js
import { csfleHelper } from './csfle/index';
Meteor.startup(async () => {
const client = await csfleHelper.getRegularClient();
await csfleHelper.ensureUniqueIndexOnKeyVault(client);
client.close();
});
./server/methods/secrets
import { MongoInternals } from 'meteor/mongo';
import { MongoID } from 'meteor/mongo-id';
import { csfleHelper } from '../csfle/index';
...
Meteor.methods({
'insertSecret': async ({ name, token }) {
const { dbName } = MongoInternals.defaultRemoteCollectionDriver().mongo.client.s.options;
const client = await csfleHelper.getRegularClient();
const encryptionClient = csfleHelper.getEncryptionClient(client);
const secretCollection = client
.db(dbName)
.collection('secret');
await secretCollection.insertOne({
_id: new MongoID.ObjectID()._str,
name,
token: csfleHelper.encryptValue({ value: token, encryptionClient }),
});
client.close();
},
'getSecretToken': async function({ _id }) {
const { dbName } = MongoInternals.defaultRemoteCollectionDriver().mongo.client.s.options;
const client = await csfleHelper.getRegularClient();
const encryptionClient = csfleHelper.getEncryptionClient(client);
const secretCollection = client
.db(dbName)
.collection('secret');
const secret = await secretCollection.findOne({_id});
let result = csfleHelper.decryptValue({ value: secret.token, encryptionClient });
client.close();
return result;
}
})
These days I always find myself using the native mongodb driver and not the Meteor mongo
package, I honestly think they should deprecate it or something.
Resources: