Mã hóa một field bằng Node.js
Hết trang này bạn có một script Node.js chạy được, mã hóa ssn mà vẫn tìm kiếm được trên field đó, trong khi cluster chỉ giữ ciphertext.
Trước khi bắt đầu
Hãy đọc Queryable Encryption trước, vì những giới hạn trên field mã hóa quyết định đoạn code này làm được gì và không làm được gì. Mọi thứ trong bảng dưới phải sẵn sàng thì script mới chạy được.
| Hạng mục | Yêu cầu |
|---|---|
| Cluster | MongoDB Enterprise 7.0 trở lên |
| Chế độ mã hóa | Automatic Encryption |
| Máy chạy ứng dụng | Đã cài crypt_shared (mongo_crypt_v1.so), phiên bản bằng hoặc mới hơn server |
| Node.js | Node.js 18 trở lên, khuyến nghị 20 LTS |
| Quản lý khóa | Khóa local là đủ để dùng thử. Production cần KMS (AWS, Azure, GCP hoặc KMIP) |
crypt_shared chạy trên máy chạy ứng dụng, không cài trên server MongoDB. Hãy tải gói Enterprise đúng hệ điều hành và phiên bản server từ MongoDB Download Center, rồi giải nén tại máy đó, ví dụ /opt/crypt_shared/lib/mongo_crypt_v1.so.
Cài driver
Hai gói phải cùng major version.
npm install mongodb@6 mongodb-client-encryption@6
Nếu npm install lỗi mongocrypt/mongocrypt.h: No such file thì Node.js quá cũ — bản v12 do apt cài là nguyên nhân hay gặp. Hãy nâng lên Node.js 18 trở lên thay vì cài thêm thư viện C.
Khai báo import và thông tin kết nối
Từ đây trở xuống, mỗi khối code JavaScript đều thuộc về một file duy nhất qe_demo.js, nối tiếp nhau theo đúng thứ tự xuất hiện. Hãy tạo file ngay bây giờ, rồi chạy nó ở cuối bằng node qe_demo.js.
Lấy host, tên replica set và thông tin đăng nhập từ thông tin kết nối của cluster trên 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";
Tạo master key
File khóa chỉ sinh một lần và dùng lại ở mọi lần chạy sau. Khi lên production, thay khối này bằng một 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;
Dữ liệu đã mã hóa bằng master key bị mất là không thể khôi phục. Hãy backup master_key.bin ở nơi tách biệt với database trước khi đưa dữ liệu thật vào.
Khai báo field và tạo collection
Ví dụ này khai báo ssn với queryType: equality, đáp ứng tình huống phổ biến nhất là tra cứu bản ghi theo giá trị chính xác. Collection phải chưa tồn tại. Lệnh này đồng thời tạo các collection metadata enxcol_.users.esc và enxcol_.users.ecoc trên 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" } },
],
},
},
}
);
Tạo client có mã hóa
Phần này bắt buộc nằm sau khi collection đã được tạo, vì encryptedFields lúc đó mới có keyId.
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);
Đọc ghi như bình thường
Lệnh insert không cần xử lý gì thêm. Driver mã hóa ssn trước khi dữ liệu rời khỏi tiến trình.
await users.insertOne({ name: "An", ssn: "123-45-6789" });
await users.insertMany([
{ name: "Binh", ssn: "987-65-4321" },
{ name: "Chi", ssn: "111-22-3333" },
]);
Truy vấn trên ssn chạy được vì field này đã khai báo 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" }));
Update trên field mã hóa chỉ nhận $set và $unset, còn findOneAndUpdate phải trả về bản ghi trước khi sửa.
await users.updateOne({ name: "Binh" }, { $set: { ssn: "000-00-0001" } });
await users.findOneAndUpdate(
{ ssn: "000-00-0001" },
{ $set: { name: "Binh (updated)" } },
{ returnDocument: "before" }
);
Aggregation match được trên field mã hóa, còn lệnh xóa thì xóa từng bản ghi một.
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" });
Kiểm chứng cluster chỉ lưu ciphertext
plainClient không bật tùy chọn mã hóa, nên nó nhìn thấy đúng dạng dữ liệu được lưu.
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
Chạy lên sẽ in ra:
ssn via plain client: Binary subtype 6
plaintext search via plain client: null
Subtype 6 là dấu hiệu BSON của ciphertext Queryable Encryption, còn tìm theo plaintext thì không ra gì.
Dọn dẹp và đóng kết nối
Xóa collection bằng client thường sẽ để sót các collection metadata, nên hãy dùng client đã bật mã hóa.
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); });