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

Python でフィールドを暗号化する

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

始める前に​

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

項目要件
ClusterMongoDB Enterprise 7.0 以降
暗号化モードAutomatic Encryption
アプリケーション実行ホストcrypt_shared (mongo_crypt_v1.so) を導入済み。バージョンはサーバーと同じか新しいこと
PythonPython 3.9 以降
鍵管理試用はローカル鍵ファイルで十分。production では KMS (AWS、Azure、GCP、KMIP) が必要
注記

crypt_shared は MongoDB サーバーではなく、アプリケーションを実行するマシンで動作します。OS とサーバーバージョンに合う Enterprise パッケージを MongoDB Download Center から取得し、そのマシンに展開してください (例: /opt/crypt_shared/lib/mongo_crypt_v1.so)。

driver をインストールする​

pip install 'pymongo[encryption]'

import と接続情報を書く​

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

ここから先の Python ブロックはすべて、記載順に追記していく 1 つのファイル qe_demo.py の一部です。いまのうちにファイルを作成し、最後に python qe_demo.py で実行します。

host、replica set 名、認証情報は console の cluster 接続情報から取得します。

import os
from urllib.parse import quote_plus

from bson.codec_options import CodecOptions
from pymongo import MongoClient, ReturnDocument
from pymongo.encryption import AutoEncryptionOpts, ClientEncryption

HOSTS = "<HOST_1>:27017,<HOST_2>:27017,<HOST_3>:27017"
REPLICA_SET = "<REPLICA_SET_NAME>"
USER = "<USERNAME>"
PASSWORD = os.environ["MONGO_PASSWORD"] # never hard-code the password
URI = (f"mongodb://{quote_plus(USER)}:{quote_plus(PASSWORD)}@{HOSTS}"
f"/admin?replicaSet={REPLICA_SET}") # quote_plus escapes special characters

DB, COLL = "qe_demo", "users"
KEY_VAULT_NS = "encryption.__keyVault" # where Data Encryption Keys are stored
CRYPT_SHARED_PATH = "/opt/crypt_shared/lib/mongo_crypt_v1.so"

master key を作成する​

鍵ファイルは一度だけ生成され、以降の実行で再利用されます。production ではこのブロックを KMS provider に置き換えてください。

if not os.path.exists("master_key.bin"):
with open("master_key.bin", "wb") as f:
f.write(os.urandom(96))
with open("master_key.bin", "rb") as f:
kms_providers = {"local": {"key": f.read()}}

plain_client = MongoClient(URI)
client_encryption = ClientEncryption(kms_providers, KEY_VAULT_NS, plain_client, CodecOptions())
危険

master key を失うと、そのデータは復元できません。実データを預ける前に、master_key.bin をデータベースとは別の場所にバックアップしてください。

暗号化するフィールドを宣言する​

この例では ssn を queryType: equality で宣言します。正確な値でレコードを引く、もっとも一般的な場面をカバーします。ここでは keyId を None のままにします。collection 作成時に driver が設定します。

encrypted_fields = {
"fields": [
{"path": "ssn", "bsonType": "string", "keyId": None,
"queries": {"queryType": "equality"}},
]
}

暗号化された collection を作成する​

collection は未作成である必要があります。この呼び出しで metadata collection enxcol_.users.esc と enxcol_.users.ecoc もサーバー上に作成されます。

_, encrypted_fields = client_encryption.create_encrypted_collection(
plain_client[DB], COLL, encrypted_fields, "local"
) # returns encrypted_fields with keyId filled in

暗号化する client を作成する​

フィールドマップに keyId が入るのは collection の作成後なので、必ずその後に実行します。

opts = AutoEncryptionOpts(
kms_providers,
KEY_VAULT_NS,
encrypted_fields_map={f"{DB}.{COLL}": encrypted_fields},
crypt_shared_lib_path=CRYPT_SHARED_PATH,
crypt_shared_lib_required=True,
)
encrypted_client = MongoClient(URI, auto_encryption_opts=opts)
users = encrypted_client[DB][COLL]

通常どおり読み書きする​

insert に特別な処理は不要です。driver がプロセスを出る前に ssn を暗号化します。

users.insert_one({"name": "An", "ssn": "123-45-6789"})
users.insert_many([
{"name": "Binh", "ssn": "987-65-4321"},
{"name": "Chi", "ssn": "111-22-3333"},
])

ssn は queryType: equality で宣言されているため、クエリが動作します。

print(users.find_one({"ssn": "123-45-6789"})) # decrypted automatically
print(list(users.find({"ssn": {"$in": ["987-65-4321", "111-22-3333"]}})))
print(users.count_documents({"ssn": "123-45-6789"}))

暗号化フィールドへの update は $set と $unset のみを受け付け、find_one_and_update は変更前のドキュメントを返す必要があります。

users.update_one({"name": "Binh"}, {"$set": {"ssn": "000-00-0001"}})
users.find_one_and_update(
{"ssn": "000-00-0001"}, {"$set": {"name": "Binh (updated)"}},
return_document=ReturnDocument.BEFORE,
)

aggregation は暗号化フィールドで match でき、削除は 1 件ずつ行います。

print(list(users.aggregate([
{"$match": {"ssn": "123-45-6789"}},
{"$project": {"_id": 0, "name": 1, "ssn": 1}},
])))

users.delete_one({"ssn": "111-22-3333"})

cluster に ciphertext しかないことを確認する​

plain_client は暗号化オプションを持たないため、保存された形式をそのまま見ます。

raw = plain_client[DB][COLL].find_one({"name": "An"})
print("ssn via plain client:", type(raw["ssn"]),
"| has __safeContent__:", "__safeContent__" in raw)
print("plaintext search via plain client:",
plain_client[DB][COLL].find_one({"ssn": "123-45-6789"})) # None

実行すると次のように表示されます。

ssn via plain client: <class 'bson.binary.Binary'> | has __safeContent__: True
plaintext search via plain client: None

ssn は Binary subtype 6 として返り、ドキュメントには __safeContent__ が含まれ、平文検索は None を返します。

後片付けをする​

通常の client で collection を削除すると metadata collection が残るため、暗号化を有効にした client を使用します。

encrypted_client[DB].drop_collection(COLL)

次のステップ​