Encrypt a field with Node.js
By the end of this page you have a working Node.js script that encrypts ssn and still searches it, while the cluster holds nothing but ciphertext.
Before you start
Read Queryable Encryption first, because the limits on encrypted fields shape what this code can and cannot do. Everything in this table has to be in place before the script will run.
| Item | Requirement |
|---|---|
| Cluster | MongoDB Enterprise 7.0 or later |
| Encryption mode | Automatic Encryption |
| Application host | crypt_shared (mongo_crypt_v1.so) installed, at a version equal to or newer than the server |
| Node.js | Node.js 18 or later, 20 LTS recommended |
| Key management | A local key file is enough for a trial. Production needs a KMS (AWS, Azure, GCP, or KMIP) |
crypt_shared runs on the machine that runs your application, not on the MongoDB server. Download the Enterprise package for your operating system and server version from the MongoDB Download Center, then extract it locally, for example to /opt/crypt_shared/lib/mongo_crypt_v1.so.
Install the driver
Both packages must be on the same major version.
npm install mongodb@6 mongodb-client-encryption@6
If npm install fails with mongocrypt/mongocrypt.h: No such file, Node.js is too old — the v12 that apt installs is a common cause. Upgrade to Node.js 18 or later rather than installing extra C libraries.
Set the imports and connection details
From here down, every JavaScript block belongs to a single file, qe_demo.js, appended in the order shown. Create it now, and run it at the end with node qe_demo.js.
Take the hosts, replica set name, and credentials from the cluster's connection details in the console.
const fs = require("fs");
const crypto = require("crypto");
const { MongoClient, ClientEncryption } = require("mongodb");
const HOSTS = "<HOST_1>:27017,<HOST_2>:27017,<HOST_3>:27017";
const REPLICA_SET = "<REPLICA_SET_NAME>";
const USER = "<USERNAME>";
const PASSWORD = process.env.MONGO_PASSWORD; // never hard-code the password
const URI =
`mongodb://${encodeURIComponent(USER)}:${encodeURIComponent(PASSWORD)}@${HOSTS}` +
`/admin?replicaSet=${REPLICA_SET}`; // encodeURIComponent escapes special characters
const DB = "qe_demo";
const COLL = "users";
const KEY_VAULT_NS = "encryption.__keyVault"; // where Data Encryption Keys are stored
const CRYPT_SHARED_PATH = "/opt/crypt_shared/lib/mongo_crypt_v1.so";
Create the master key
The key file is generated once and reused on every run. For production, replace this block with a KMS provider.
async function main() {
if (!fs.existsSync("master_key.bin")) {
fs.writeFileSync("master_key.bin", crypto.randomBytes(96));
}
const kmsProviders = { local: { key: fs.readFileSync("master_key.bin") } };
const plainClient = new MongoClient(URI);
await plainClient.connect();
let encryptedClient;
Data encrypted with a lost master key cannot be recovered. Back master_key.bin up somewhere separate from the database before you put real data behind it.
Declare the fields and create the collection
This example declares ssn with queryType: equality, which covers the most common case of looking a record up by an exact value. The collection must not already exist. This call also creates the enxcol_.users.esc and enxcol_.users.ecoc metadata collections on the server.
try {
const clientEncryption = new ClientEncryption(plainClient, {
keyVaultNamespace: KEY_VAULT_NS,
kmsProviders,
});
const { encryptedFields } = await clientEncryption.createEncryptedCollection(
plainClient.db(DB),
COLL,
{
provider: "local",
createCollectionOptions: {
encryptedFields: {
fields: [
{ path: "ssn", bsonType: "string", keyId: null, queries: { queryType: "equality" } },
],
},
},
}
);
Build the encrypting client
This has to come after the collection exists, because encryptedFields now carries the key IDs.
encryptedClient = new MongoClient(URI, {
autoEncryption: {
keyVaultNamespace: KEY_VAULT_NS,
kmsProviders,
encryptedFieldsMap: { [`${DB}.${COLL}`]: encryptedFields },
extraOptions: { cryptSharedLibPath: CRYPT_SHARED_PATH, cryptSharedLibRequired: true },
},
});
await encryptedClient.connect();
const users = encryptedClient.db(DB).collection(COLL);
Read and write as usual
Inserts need no special handling. The driver encrypts ssn before it leaves the process.
await users.insertOne({ name: "An", ssn: "123-45-6789" });
await users.insertMany([
{ name: "Binh", ssn: "987-65-4321" },
{ name: "Chi", ssn: "111-22-3333" },
]);
Queries on ssn work because the field was declared with queryType: equality.
console.log(await users.findOne({ ssn: "123-45-6789" })); // decrypted automatically
console.log(await users.find({ ssn: { $in: ["987-65-4321", "111-22-3333"] } }).toArray());
console.log(await users.countDocuments({ ssn: "123-45-6789" }));
Updates to an encrypted field accept $set and $unset only, and findOneAndUpdate must return the document as it was before the change.
await users.updateOne({ name: "Binh" }, { $set: { ssn: "000-00-0001" } });
await users.findOneAndUpdate(
{ ssn: "000-00-0001" },
{ $set: { name: "Binh (updated)" } },
{ returnDocument: "before" }
);
Aggregation can match on an encrypted field, and deletes are one document at a time.
console.log(
await users
.aggregate([{ $match: { ssn: "123-45-6789" } }, { $project: { _id: 0, name: 1, ssn: 1 } }])
.toArray()
);
await users.deleteOne({ ssn: "111-22-3333" });
Confirm the cluster holds only ciphertext
plainClient has no encryption options, so it sees the stored form.
const plainColl = plainClient.db(DB).collection(COLL);
const raw = await plainColl.findOne({ name: "An" });
console.log("ssn via plain client: Binary subtype", raw.ssn.sub_type); // 6 = ciphertext
console.log("plaintext search via plain client:",
await plainColl.findOne({ ssn: "123-45-6789" })); // null
Running it prints:
ssn via plain client: Binary subtype 6
plaintext search via plain client: null
Subtype 6 is the BSON marker for a Queryable Encryption ciphertext, and the plaintext search finds nothing.
Clean up and close
Dropping the collection with the plain client would leave the metadata collections behind, so use the encrypted client.
await encryptedClient.db(DB).dropCollection(COLL);
} finally {
if (encryptedClient) await encryptedClient.close();
await plainClient.close();
}
}
main().catch((e) => { console.error(e.message || e); process.exit(1); });