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');
}
1 Answer 1
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;
}