1

I'm working on an AWS Lambda that needs to encrypt some data going to Mongo Atlas via CSFLE. I have logic in place that creates and saves the DEK to Atlas no problem, but when I use Mongoose to actually write a document to the DB, it just writes it in plaintext.

import { Handler } from 'aws-lambda';
import { ClientEncryption, KMSProviders, MongoClient } from 'mongodb';
import mongoose, { Schema } from 'mongoose';
import path from 'path';
const keyVaultDatabase = 'encryption';
const keyVaultCollection = '__keyVault';
const keyVaultNamespace = `${keyVaultDatabase}.${keyVaultCollection}`;
export const handler: Handler = async () => {
 try {
 const dekUUID = await getOrCreateDEK();
 const encryptSchema = new Schema(
 {
 name: String,
 ssn: {
 type: String,
 encrypt: {
 keyId: dekUUID,
 queries: 'equality',
 algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
 }
 },
 sequence: Number,
 requestingAgentCode: [String]
 },
 { encryptionType: 'csfle' }
 );
 const connection = mongoose.createConnection();
 const db = connection.useDb(process.env.MONGO_DB_NAME!);
 const encryptModel = db.model('work_requests', encryptSchema);
 await db.openUri(getMongoURI(), getEncryptedConnectionOptions());
 await encryptModel.create({
 name: 'Alan',
 ssn: '12345',
 sequence: 1234567,
 requestingAgentCode: ['23413412']
 });
 return {
 statusCode: 201,
 body: {}
 };
 } catch (error) {
 return {
 statusCode: 500,
 body: JSON.stringify({
 message: 'Internal Server Error',
 error: (error as Error)?.message
 })
 };
 }
};
function getMongoURI(): string {
 return `${process.env.MONGODB_CONNECTION_STRING!}/?authSource=%24external&authMechanism=MONGODB-AWS&retryWrites=true`;
}
function getKmsProviders(): KMSProviders {
 return {
 aws: {
 accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
 secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
 sessionToken: process.env.AWS_SESSION_TOKEN!
 }
 };
}
function getEncryptedConnectionOptions() {
 return {
 serverSelectionTimeoutMS: 5000,
 autoEncryption: {
 keyVaultNamespace: `${keyVaultDatabase}.${keyVaultCollection}`,
 kmsProviders: getKmsProviders(),
 cryptSharedLibPath: path.resolve(__dirname, './lib/mongo_crypt_v1.so')
 }
 };
}
function getConnectionOptions() {
 return {
 serverSelectionTimeoutMS: 5000
 };
}
async function getOrCreateDEK(): Promise<string> {
 const keyAltName = 'ddbplus-csfle-dek';
 const keyVaultClient = new MongoClient(getMongoURI(), getConnectionOptions());
 await keyVaultClient.connect();
 const keyVaultDB = keyVaultClient.db(keyVaultDatabase);
 const keyVaultColl = keyVaultDB.collection(keyVaultCollection);
 // Check if the dek already exists before trying to create anything
 const existingKey = await keyVaultColl.findOne({ keyAltNames: { $in: [keyAltName] } });
 if (existingKey) {
 keyVaultClient.close();
 return existingKey._id.toString('base64');
 }
 await keyVaultColl.createIndex(
 { keyAltNames: 1 },
 {
 unique: true,
 partialFilterExpression: { keyAltNames: { $exists: true } }
 }
 );
 const kmsProviders = getKmsProviders();
 const encryption = new ClientEncryption(keyVaultClient, {
 keyVaultNamespace,
 kmsProviders
 });
 const keyUUID = await encryption.createDataKey('aws', {
 masterKey: {
 region: process.env.AWS_KEY_REGION!,
 key: process.env.AWS_KEY_ARN!
 },
 keyAltNames: [keyAltName]
 });
 keyVaultClient.close();
 return keyUUID.toString('base64');
}
jonrsharpe
124k32 gold badges290 silver badges493 bronze badges
asked Feb 9 at 14:02

1 Answer 1

2

When you configure autoEncryption on your connection, the driver needs an encryption schema map that explicitly defines which fields to encrypt. Simply marking fields with Mongoose schema options doesn't communicate this to the driver level where actual encryption happens. Mongoose's encrypt field option and encryptionType: 'csfle' schema option are Mongoose-specific abstractions that don't directly translate to the wire protocol encryption that the MongoDB driver's autoEncryption feature expects. You need to add a schemaMap to your autoEncryption configuration that explicitly tells the driver which fields to encrypt:

function getEncryptedConnectionOptions() {
 return {
 serverSelectionTimeoutMS: 5000,
 autoEncryption: {
 keyVaultNamespace: `${keyVaultDatabase}.${keyVaultCollection}`,
 kmsProviders: getKmsProviders(),
 cryptSharedLibPath: path.resolve(__dirname, './lib/mongo_crypt_v1.so'),
 schemaMap: ...
 }
 };
}

This also requires a binary DEK you should generate using the envelope encryption pattern with, for example, AWS's CMK:

const keyUUID = await encryption.createDataKey('aws', {
 masterKey: {
 region: process.env.AWS_KEY_REGION!,
 key: process.env.AWS_KEY_ARN! // ARN of your CMK in AWS KMS
 },
 keyAltNames: [keyAltName]
});

You'll find an example of using the schemaMap and binary DEKs here

** UPDATE **

This is the full working code, using an explicit schemaMap passed directly to the MongoDB driver's autoEncryption options, which properly instructs the driver to encrypt the fields before writing to MongoDB, and returning the DEK as a Binary UUID object instead of a base64 string. I also registers the Mongoose model before opening the connection (as per Mongoose docs).

import { Handler } from 'aws-lambda';
import { Binary, ClientEncryption, MongoClient } from 'mongodb';
import mongoose, { Schema } from 'mongoose';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const KEY_VAULT_DB = 'encryption';
const KEY_VAULT_COLL = '__keyVault';
const KEY_VAULT_NS = `${KEY_VAULT_DB}.${KEY_VAULT_COLL}`;
const buildKmsProviders = () => {
 const providers: any = {
 aws: {
 accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
 secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!
 }
 };
 if (process.env.AWS_SESSION_TOKEN) {
 providers.aws.sessionToken = process.env.AWS_SESSION_TOKEN;
 }
 return providers;
};
export const handler: Handler = async () => {
 try {
 const dekUUID = await getOrCreateDEK();
 const schemaMap = {
 [`${process.env.MONGO_DB_NAME}.work_requests`]: {
 bsonType: 'object',
 properties: {
 ssn: {
 encrypt: {
 keyId: [dekUUID],
 bsonType: 'string',
 algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
 }
 }
 }
 }
 };
 const connection = mongoose.createConnection();
 const model = connection.model('work_requests', new Schema({
 name: String,
 ssn: String,
 sequence: Number,
 requestingAgentCode: [String]
 }));
 await connection.openUri(process.env.MONGODB_CONNECTION_STRING!, {
 autoEncryption: {
 keyVaultNamespace: KEY_VAULT_NS,
 kmsProviders: buildKmsProviders(),
 schemaMap,
 extraOptions: {
 cryptSharedLibPath: path.resolve(__dirname, '../lib/mongo_crypt_v1.dylib')
 }
 }
 });
 return await model.create({
 name: 'Alan',
 ssn: '12345',
 sequence: 1234567,
 requestingAgentCode: ['23413412']
 });
 } catch (error) {
 return {
 statusCode: 500,
 body: JSON.stringify({
 message: 'Internal Server Error',
 error: (error as Error)?.message
 })
 };
 }
};
async function getOrCreateDEK(): Promise<Binary> {
 const keyAltName = 'ddbplus-csfle-dek';
 const client = new MongoClient(process.env.MONGODB_CONNECTION_STRING!);
 await client.connect();
 const coll = client.db(KEY_VAULT_DB).collection(KEY_VAULT_COLL);
 const existingKey = await coll.findOne({ keyAltNames: { $in: [keyAltName] } });
 if (existingKey) {
 await client.close();
 return existingKey._id as unknown as Binary;
 }
 await coll.createIndex(
 { keyAltNames: 1 },
 { unique: true, partialFilterExpression: { keyAltNames: { $exists: true } } }
 );
 const encryption = new ClientEncryption(client, {
 keyVaultNamespace: KEY_VAULT_NS,
 kmsProviders: buildKmsProviders()
 });
 const keyUUID = await encryption.createDataKey('aws', {
 masterKey: {
 region: process.env.AWS_KEY_REGION!,
 key: process.env.AWS_KEY_ARN!
 },
 keyAltNames: [keyAltName]
 });
 await client.close();
 return keyUUID;
}
answered Feb 9 at 15:17
Sign up to request clarification or add additional context in comments.

So that's how it's supposed to work for the official Mongo package, but Mongoose is supposed to handle the scheaMap portion of that for you, at least according to their docs - mongoosejs.com/docs/…
I updated the answer to add the entire code sample, hope this helps!
That did the trick, thanks! Frustrating that it needed the schemaMap despite the mongoose docs not bringing that up at all, but that definitely did the trick.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.