Chuyển tới nội dung chính

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ụcYêu cầu
ClusterMongoDB Enterprise 7.0 trở lên
Chế độ mã hóaAutomatic 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.jsNode.js 18 trở lên, khuyến nghị 20 LTS
Quản lý khóaKhóa local là đủ để dùng thử. Production cần KMS (AWS, Azure, GCP hoặc KMIP)
ghi chú

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
ghi chú

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​

Toàn bộ phần dưới nằm trong một file

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;
cảnh báo

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); });

Bước tiếp theo​