Chuyển tới nội dung chính

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ụcYêu cầu
ClusterMongoDB Enterprise 7.0 trở lên
Chế độ mã hóaAutomatic 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
PythonPython 3.9 trở lên
Quản lý khóaKhóa local là đủ để dùng thử. Production cần KMS (AWS, Azure, GCP hoặc KMIP)
ghi chú

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​

Toàn bộ phần dưới nằm trong một file

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())
cảnh báo

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)

Bước tiếp theo​