The Crypto Shredding示例应用演示了如何利用 MongoDB 的 客户端字段级加密(CSFLE) 来强化删除敏感数据的程序。
关于示例应用程序
删除权,也称为被遗忘权,是根据一般数据保护法规 (GDPR)等法律法规赋予个人的权利。这意味着存储个人数据的公司必须能够根据请求将其删除。由于数据可能分布在多个系统中,因此对这些公司来说,识别数据并将其从所有位置删除在技术上具有挑战性。即使执行正确,也存在将来可以从备份中恢复已删除数据的风险,从而可能产生法律和财务风险。
警告
MongoDB不ACID 一致性保证本文中描述的解决方案和技术满足有关擦除权的所有监管要求。您的组织必须确定适当、充分的措施,以符合一般数据保护法规 (GDPR)等监管要求。
Crypto Shredding示例应用程序演示了一种实现擦除权的方法。演示应用程序是一个Python (Flask) Web应用程序,其前端用于添加用户、登录和输入数据。它还包括一个“管理”页面,用于展示加密粉碎功能。
您可以按照 Github存储库中的说明安装并运行该应用程序。
什么是加密粉碎?
加密粉碎(也称为加密擦除)是一种数据销毁技术,在该技术中,您不是销毁加密的数据,而是销毁解密数据所需的加密密钥。这使得数据无法破译。
示例,假设您正在为多个用户存储数据。首先为每个用户提供自己唯一的数据加密密钥(DEK),并将其映射到该客户。
图中,“User A”和“User B”在密钥存储中都有自己唯一的 DEK。每个密钥都用于为其相应用户加密或解密数据:

假设您要删除用户 B 的所有数据。如果删除用户 B 的 DEK,您将无法再解密其数据。数据存储中的所有内容都变成无法破译的密文。用户 A 的数据不受影响,因为其 DEK 仍然存在:

什么是 CSFLE?
借助CSFLE,应用程序可在将数据传输到服务器之前对文档中的敏感字段进行加密。即使数据库在内存中使用数据,也绝不是纯文本形式。数据库存储和传输只有客户端才能解密的加密数据。
CSFLE 使用信封加密,这是使用数据密钥加密明文数据的做法,而数据密钥本身由顶级信封密钥(也称为“客户主密钥”或集合扫描 )加密。

加密密钥管理
CMK 通常由KMS (KMS )托管。 CSFLE 支持多种 KMS,包括Amazon Web Services (Amazon Web Services)、 Azure Key Vault、 Google Cloud Platform (GCP) 以及支持KMIP 标准的密钥库,例如 Hashicorp Keyvault。示例应用使用Amazon Web Services作为KMS。
自动显式加密
CSFLE 可以用于自动或显式模式,或两者的组合。示例应用使用显式加密。
通过自动加密,您可以根据定义的加密模式执行加密的读取和写入操作,因此不需要应用程序代码来指定如何加密或解密字段。
通过显式加密,您可以使用MongoDB驱动程序的加密库手动加密或解密应用程序中的字段。
示例应用演练
示例应用使用显式加密的 CSFLE 和Amazon Web Services作为KMS:

添加用户
该应用通过初始化 app.mongodb_encryption_client
对象来实例化 ClientEncryption
类。该加密客户端负责生成 DEK,然后使用来自Amazon Web Services KMS的集合扫描对其进行加密。
当用户注册时,应用程序使用 create_data_key
方法为其生成唯一的 DEK,然后返回 data_key_id
:
# flaskapp/db_queries.py def create_key(userId): data_key_id = \app.mongodb_encryption_client.create_data_key (kms_provider, master_key, key_alt_names=[userId]) return data_key_id
然后,应用在保存用户信息时会使用此方法:
# flaskapp/user.py def save(self): dek_id = db_queries.create_key(self.username) result = app.mongodb[db_name].user.insert_one( { "username": self.username, "password_hash": self.password_hash, "dek_id": dek_id, "createdAt": datetime.now(), } ) if result: self.id = result.inserted_id return True else: return False
添加和加密数据
注册后,用户可以登录并通过输入表单以键值对的形式输入数据:

数据库将此数据存储在名为“data”的MongoDB集合中,其中的每个文档都包含用户名和键值对:
{ "name": "shoe size", "value": "10", "username": "tom" }
示例应用会加密 value
和 username
字段,但不会加密 name
。应用使用用户的 DEK 和指定的加密算法对字段进行加密:
# flaskapp/db_queries.py # Fields to encrypt, and the algorithm to encrypt them with ENCRYPTED_FIELDS = { # Deterministic encryption for username, because we need to search on it "username": Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic, # Random encryption for value, as we don't need to search on it "value": Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random, }
insert_data
函数获取未加密文档并循环遍历 ENCRYPTED_FIELDS
以对其进行加密:
# flaskapp/db_queries.py def insert_data(document): document["username"] = current_user.username # Loop over the field names (and associated algorithm) we want to encrypt for field, algo in ENCRYPTED_FIELDS.items(): # if the field exists in the document, encrypt it if document.get(field): document[field] = encrypt_field(document[field], algo) # Insert document (now with encrypted fields) to the data collection app.data_collection.insert_one(document)
如果文档中存在指定的字段,该函数将调用 encrypt_field
以使用指定的算法对其进行加密:
# flaskapp/db_queries.py # Encrypt a single field with the given algorithm def encrypt_field(field, algorithm): try: field = app.mongodb_encryption_client.encrypt( field, algorithm, key_alt_name=current_user.username, ) return field except pymongo.errors.EncryptionError as ex: # Catch this error in case the DEK doesn't exist. Log a warning and # re-raise the exception if "not all keys requested were satisfied" in ex._message: app.logger.warn( f"Encryption failed: could not find data encryption key for user: {current_user.username}" ) raise ex
添加数据后,您可以在 Web应用中看到它:

删除加密密钥
现在让我们看看删除DEK 后会发生什么。示例应用通过管理页面执行此操作,该页面应仅限于授权管理密钥的人员使用:

“删除数据加密密钥”选项会删除 DEK,但会保留用户的加密数据。此后,应用程序将无法再解密数据。尝试检索已登录用户的数据会引发错误:

注意
删除 DEK 后,应用程序仍可解密并显示数据,直到其缓存过期(最多 60 秒后)。
但数据库中实际还剩下什么?您可以返回“管理”页面并单击 Fetch data for all users,查看信息。如果应用程序无法解密数据,此视图不会引发异常。相反,它会准确显示数据库中存储的内容。
即使您尚未实际删除用户的数据,由于数据加密密钥已不复存在,应用程序只能显示加密字段“用户名”和“ value ”的密文。

以下是用于获取此数据的代码。它使用与前面所示的 encrypt
方法类似的逻辑。应用程序运行不带任何筛选器的 find
操作来检索所有数据,然后循环遍历 ENCRYPTED_FIELDS
字典以解密字段:
# flaskapp/db_queries.py def fetch_all_data_unencrypted(decrypt=False): results = list(app.data_collection.find()) if decrypt: for field in ENCRYPTED_FIELDS.keys(): for result in results: if result.get(field): result[field], result["encryption_succeeded"] = decrypt_field(result[field]) return results
系统会为每个要解密的字段调用 decrypt_field
函数,但在本例中,如果由于缺少 DEK 而无法成功解密,应用程序会捕获错误:
# flaskapp/db_queries.py # Try to decrypt a field, returning a tuple of (value, status). This will be either (decrypted_value, True), or (raw_cipher_text, False) if we couldn't decrypt def decrypt_field(field): try: # We don't need to pass the DEK or algorithm to decrypt a field field = app.mongodb_encryption_client.decrypt(field) return field, True # Catch this error in case the DEK doesn't exist. except pymongo.errors.EncryptionError as ex: if "not all keys requested were satisfied" in ex._message: app.logger.warn( "Decryption failed: could not find data encryption key to decrypt the record." ) # If we can't decrypt due to missing DEK, return the "raw" value. return field, False raise ex
您还可以使用mongosh Shell直接检查数据库,以证明没有任何内容可读:

点,用户的加密数据仍然存在。有人可以通过恢复加密密钥(例如从数据库备份)来访问权限它。
为防止这种情况,示例应用程序使用两个独立的数据库集群:一个用于存储数据,另一个用于存储 DEK(“密钥保管库”)。使用单独的集群可以解耦应用程序数据和密钥保管库的备份恢复。从备份恢复数据集群不会恢复从密钥保管库集群中删除的任何 DEK。
结论
客户端字段级加密可以简化“忘记”某些数据的任务。通过删除数据键,您可以有效地忘记存在于不同数据库、集合、备份和日志中的数据。
在生产应用程序中,除了删除加密密钥之外,您还可以删除加密数据本身。这种“纵深防御”方法有助于确保数据确实消失。在删除数据的基础上实施加密粉碎,可以最大限度地减少删除操作失败或不包括应擦除的数据时的影响。