MySQL 列级加密

716 阅读5分钟

MySQL 列级加密

Mike Benshoof [hudson译]

2022年9月22号

在今年早些时候的一篇文章中 –Percona Server for MySQL Encryption Options and Choices –我讨论了MySQL中加密的一些选项。作为一个如此复杂的话题,该帖子旨在澄清和强调不同级别的“加密”的各个方面。最近,我再次提到了这个话题,特别是关于列级加密和各种选项,所以我想更详细地谈谈这个话题。

截至当前版本的 Percona Server for MySQL ,没有内置方法将单个列定义为加密列。理想情况下,可以在create语句中传递一些元数据,这将自动发生,例如:

CREATE TABLE pii_data (

user_id int unsigned PRIMARY KEY,

super_secret  varchar(255) ENCRYPTED,

…

… ) ENGINE=InnoDB 

不幸的是,这个选项不可用,我们需要在读/写时间或之前进行一些数据操作。

内置MySQL加密功能

最常见的方法之一是使用此处介绍的内置MySQL加密函数:dev.mysql.com/doc/refman/….

标准流程大致如下:

INSERT INTO mytable (id, secret_data)  VALUES (1, TO_BASE64(AES_ENCRYPT(“ultra-secret-data”, “my-key”)));

…

SELECT AES_DECRYPT(FROM_BASE64(secret_data), “my-key”) as plaintext FROM mytable;

这工作得很好,该列数据将加密存储。如果未经授权的人要访问运行表,没有秘钥,他们将无法读取该列数据。这种方法最大的问题是明文密钥和明文数据都是在查询中指定的。这会导致日志文件中潜在的泄漏(慢查询、二进制日志等),以及在不安全的网络上潜在的嗅探(即不使用SSL的连接)。

此外,密钥存储可能会变得更加麻烦。如果您计划为整个表共享同一个键,那么键轮换和管理可能会变得非常重要。一般最佳实践建议您至少每年旋转一次键。在大表上,这可能是一项艰巨的任务。让我们看看信封加密模式作为替代方案。

信封加密

与使用内置加密不同,信封加密使用单个数据密钥的概念。虽然有很多方法可以做到这一点,但我个人的经验是使用AWS KMS服务。在KMS中,您可以创建所谓的“客户主密钥”(CMK)。这对于加密像密码这样的小字符串非常有用,但仅限于加密高达4KB的字符串。

更灵活的方法是使用单个数据密钥,在应用程序中本地加密数据,然后将加密数据与加密数据密钥一起存储。这可以在不同的粒度级别上完成,从行级别到表级别,再到数据库级别。以下是信封加密的一般过程:

plaintextData = “super secret content”

datakey = kms.generate_data_key(“alias/mymasterkey”)

encryptedData = crypto.encrypt(plaintextData, datakey.plaintext)

storableKey = datakey.ciphertext
 
INSERT INTO mytable (id, secretData, key)  VALUES (NULL, encryptedData, storableKey);

当需要解密数据时,首先解密密钥,然后使用密钥解密数据:

SELECT secretData, key FROM mytable;

datakey = kms.decrypt(key)

decryptedData = crypto.decrypt(secretData, datakey)

因为数据密钥和数据都是加密的,所以可以安全地存储在一起。这是这种方法的主要优点之一。您可以拥有数百到数百万个相互独立的数据密钥,但受单个主密钥保护。这允许您通过禁用一个主密钥来禁用所有单个密钥。

它还简化了键的旋转–您可以简单地旋转主键并开始使用新键生成新的数据键。请注意,这不会重新加密数据,但它允许您遵循定期旋转密钥的最佳实践。只要您不删除任何旧密钥,KMS就可以从密钥本身决定使用哪个密钥进行解密,并自动解密您的数据(即您不必指定加密数据的密钥)。

我在Python中包含了一个示例脚本的链接,该脚本更详细地显示了这个过程。虽然演示中实际上没有活动数据库,但代码打印出可以在活动服务器上运行的INSERT和SELECT语句: github.com/mbenshoof/k…

列级加密面临的挑战

如何搜索

不用说,引入列级加密并不是一件小事。最大的挑战之一是审查如何检索加密数据。例如,如果我存储单独加密的社会保险号码,那么如何搜索SSN为123-45-6789的用户?如果我对整个表/模式使用共享键,那么可以使用适当的索引。我只需要将加密的值传递给where子句,如果它存在,就应该找到它。 但是,对于每行使用唯一键的每行模型,这不再可能。由于我不知道加密值所用的密钥,因此无法在表中搜索加密值。在这种情况下,可以考虑搜索一个单向散列字段。例如,您可以将SSN的SHA256哈希存储为一个附加列,用于搜索,然后解密任何其他敏感信息。

CPU和空间开销

另一个挑战是增加额外的写/读开销来处理加密和解密。虽然这可能是一个问题,也可能不是,这取决于用例,但应用程序端或MySQL端所需的额外CPU可能会发挥作用。在规划资源规模时,您需要考虑所需的额外处理并将其考虑在内。

此外,根据使用的加密库,可能需要额外的空间来存储加密值。在某些情况下,加密值(特别是存储在base64中时)可能最终需要更高的存储占用空间。如果在附加的哈希列上使用索引,则可以合成空间。对于较小的值(如SSN),哈希值可能比实际数据大得多。当应用于数百万条记录时,这可能会导致更大的存储占用空间。

总结

加密和安全是非常重要和复杂的主题。在MySQL中考虑列级加密时,您肯定有一些选择。最简单的方法是利用MySQL中的内置加密功能。然而,您可以更进一步,处理应用程序中的所有加密和解密。 与此类复杂主题的情况一样,选择和方法完全取决于您的需求和用例。只要知道MySQL的灵活性,很可能有一种适合您的设计和方法!