Python でフィールドを暗号化する
このページを終えると、ssn を暗号化したまま検索できる PyMongo スクリプトが手元に残り、cluster には ciphertext しか保存されません。
始める前に
暗号化フィールドの制限がこのコードにできること、できないことを決めるため、先に Queryable Encryption を読んでください。下表の条件がすべてそろってはじめて、スクリプトが動きます。
| 項目 | 要件 |
|---|---|
| Cluster | MongoDB Enterprise 7.0 以降 |
| 暗号化モード | Automatic Encryption |
| アプリケーション実行ホスト | crypt_shared (mongo_crypt_v1.so) を導入済み。バージョンはサーバーと同じか新しいこと |
| Python | Python 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 と接続情報を書く
ここから先の 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)