メインコンテンツまでスキップ

Node.js でフィールドを暗号化する

このページを終えると、ssn を暗号化したまま検索できる Node.js スクリプトが手元に残り、cluster には ciphertext しか保存されません。

始める前に​

暗号化フィールドの制限がこのコードにできること、できないことを決めるため、先に Queryable Encryption を読んでください。下表の条件がすべてそろってはじめて、スクリプトが動きます。

項目要件
ClusterMongoDB Enterprise 7.0 以降
暗号化モードAutomatic Encryption
アプリケーション実行ホストcrypt_shared (mongo_crypt_v1.so) を導入済み。バージョンはサーバーと同じか新しいこと
Node.jsNode.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 と接続情報を書く​

以下はすべて 1 つのファイルに書きます

ここから先の 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); });

次のステップ​