Node.js でフィールドを暗号化する
このページを終えると、ssn を暗号化したまま検索できる Node.js スクリプトが手元に残り、cluster には ciphertext しか保存されません。
始める前に
暗号化フィールドの制限がこのコードにできること、できないことを決めるため、先に Queryable Encryption を読んでください。下表の条件がすべてそろってはじめて、スクリプトが動きます。
| 項目 | 要件 |
|---|---|
| Cluster | MongoDB Enterprise 7.0 以降 |
| 暗号化モード | Automatic Encryption |
| アプリケーション実行ホスト | crypt_shared (mongo_crypt_v1.so) を導入済み。バージョンはサーバーと同じか新しいこと |
| Node.js | Node.js 18 以降 (20 LTS 推奨) |
| 鍵管理 | 試用はローカル鍵ファイルで十分。production では KMS (AWS、Azure、GCP、KMIP) が必要 |
crypt_shared は MongoDB サーバーではなく、アプリケーションを実行するマシンで動作します。OS とサーバーバージョンに合う Enterprise パッケージを MongoDB Download Center から取得し、そのマシンに展開してください (例: /opt/crypt_shared/lib/mongo_crypt_v1.so)。
driver をインストールする
2 つのパッケージは同じ major version である必要があります。
npm install mongodb@6 mongodb-client-encryption@6
npm install が mongocrypt/mongocrypt.h: No such file で失敗する場合、Node.js が古すぎます (apt 同梱の v12 など)。C ライブラリを追加導入せず、Node.js 18 以降へ更新してください。
import と接続情報を書く
ここから先の JavaScript ブロックはすべて、記載順に追記していく 1 つのファイル qe_demo.js の一部です。いまのうちにファイルを作成し、最後に node qe_demo.js で実行します。
host、replica set 名、認証情報は console の cluster 接続情報から取得します。
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";
master key を作成する
鍵ファイルは一度だけ生成され、以降の実行で再利用されます。production ではこのブロックを 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;
master key を失うと、そのデータは復元できません。実データを預ける前に、master_key.bin をデータベースとは別の場所にバックアップしてください。
フィールドを宣言し collection を作成する
この例では ssn を queryType: equality で宣言します。正確な値でレコードを引く、もっとも一般的な場面をカバーします。collection は未作成である必要があります。この呼び出しで metadata collection enxcol_.users.esc と enxcol_.users.ecoc もサーバー上に作成されます。
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" } },
],
},
},
}
);
暗号化する client を作成する
encryptedFields に keyId が入るのは collection の作成後なので、必ずその後に実行します。
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);
通常どおり読み書きする
insert に特別な処理は不要です。driver がプロセスを出る前に ssn を暗号化します。
await users.insertOne({ name: "An", ssn: "123-45-6789" });
await users.insertMany([
{ name: "Binh", ssn: "987-65-4321" },
{ name: "Chi", ssn: "111-22-3333" },
]);
ssn は 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 は $set と $unset のみを受け付け、findOneAndUpdate は変更前のドキュメントを返す必要があります。
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 でき、削除は 1 件ずつ行います。
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" });
cluster に ciphertext しかないことを確認する
plainClient は暗号化オプションを持たないため、保存された形式をそのまま見ます。
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
実行すると次のように表示されます。
ssn via plain client: Binary subtype 6
plaintext search via plain client: null
subtype 6 は Queryable Encryption の ciphertext を示す BSON のマーカーで、平文検索は何も返しません。
後片付けをして接続を閉じる
通常の client で collection を削除すると metadata collection が残るため、暗号化を有効にした 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); });