Mã hóa một field bằng Python
Hết trang này bạn có một script PyMongo 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ục | Yêu cầu |
|---|---|
| Cluster | MongoDB Enterprise 7.0 trở lên |
| Chế độ mã hóa | Automatic 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 |
| Python | Python 3.9 trở lên |
| Quản lý khóa | Khóa local là đủ để dùng thử. Production cần KMS (AWS, Azure, GCP hoặc KMIP) |
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
pip install 'pymongo[encryption]'
Khai báo import và thông tin kết nối
Từ đây trở xuống, mỗi khối code Python đều thuộc về một file duy nhất qe_demo.py, 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 python qe_demo.py.
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.
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"
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.
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())
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 cần mã hóa
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. keyId để None ở bước này. Driver sẽ tự điền khi tạo collection.
encrypted_fields = {
"fields": [
{"path": "ssn", "bsonType": "string", "keyId": None,
"queries": {"queryType": "equality"}},
]
}
Tạo collection đã mã hóa
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.
_, encrypted_fields = client_encryption.create_encrypted_collection(
plain_client[DB], COLL, encrypted_fields, "local"
) # returns encrypted_fields with keyId filled in
Tạo client có mã hóa
Phần này bắt buộc nằm sau khi collection đã được tạo, vì field map lúc đó mới có keyId.
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]
Đọ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.
users.insert_one({"name": "An", "ssn": "123-45-6789"})
users.insert_many([
{"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.
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 trên field mã hóa chỉ nhận $set và $unset, còn find_one_and_update phải trả về bản ghi trước khi sửa.
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 được trên field mã hóa, còn lệnh xóa thì xóa từng bản ghi một.
print(list(users.aggregate([
{"$match": {"ssn": "123-45-6789"}},
{"$project": {"_id": 0, "name": 1, "ssn": 1}},
])))
users.delete_one({"ssn": "111-22-3333"})
Kiểm chứng cluster chỉ lưu ciphertext
plain_client 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.
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
Chạy lên sẽ in ra:
ssn via plain client: <class 'bson.binary.Binary'> | has __safeContent__: True
plaintext search via plain client: None
ssn trả về là Binary subtype 6, bản ghi có thêm __safeContent__, và tìm theo plaintext trả về None.
Dọn dẹp
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.
encrypted_client[DB].drop_collection(COLL)