导语:你正在构建一个新系统,用来收集用户的社会保障号码SSN,大家都知道SSN在存储时应该加密,这时该怎么做呢?
我们从一个简单的场景开始(这可能与很多地方政府或医疗保健应用非常相关):
您正在建立一个新系统,需要从其用户收集社会保障号码(SSN)。
规则和常识都规定用户的SSN应该在存储时加密。
鉴于他们的SSN,员工需要能够查看用户帐号。
我们先来看看这个问题的一些比较明显的答案。不安全(或其他不明智)的答案是非随机加密,大多数团队(特别是没有安全或加密专家的团队)最明显的答案将是这样做:
<?php class InsecureExampleOne { protected $db; protected $key; public function __construct(PDO $db, string $key = '') { $this->db = $db; $this->key = $key; } public function searchByValue(string $query): array { $stmt = $this->db->prepare('SELECT * FROM table WHERE column = ?'); $stmt->execute([ $this->insecureEncryptDoNotUse($query) ]); return $stmt->fetchAll(PDO::FETCH_ASSOC); } protected function insecureEncryptDoNotUse(string $plaintext): string { return bin2hex( openssl_encrypt( $plaintext, 'aes-128-ecb', $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING ) ); } }
在上面的代码片段中,当使用相同的密钥加密时,相同的明文总是产生相同的密文。但更多的关于ECB模式是每隔16字节的块被单独加密,这可能会产生一些非常不好的后果。
事实上正式的看的话,这些结构不是语义上的安全:如果加密一个大的消息,你会看到密文中的块会出现重复。
为了安全起见,加密必须与随机噪声无法区分给任何不保存解密密钥的人。不安全模式包括ECB模式和CBC模式,静态(或空)IV。
您需要的应该是非确定性加密,这意味着每个消息都会使用一个不重复给定密钥的唯一的随机数或初始化向量。
学术设计
有许多学术研究涉及诸如同态,秩序揭示和订单保存加密技术等主题。
与这项工作一样有趣的是,目前的设计无法在生产环境中使用。
例如,订单显示加密会泄漏足够的数据来推断明文。
同态加密方案通常将重新打包漏洞(实际的选择密文攻击)作为特征。
相对于乘法,未贴合的 RSA是同态的。
如果您将密文乘以整数,则您获得的明文将等于原始消息乘以相同的整数。有几种可能的攻击是无条件的RSA,这就是为什么现实世界中的RSA使用填充(尽管通常是不安全的填充模式)。
计数器模式下的AES是相同XOR的同态。
这就是为什么nonce-reuse在CTR模式下消除了你的消息的机密性(通常是非NMR流密码)。
正如我们在之前的博客文章中所述,当涉及真实世界的加密技术时,没有完整性的机密性与没有保密性相同。如果攻击者获得访问数据库,改变密文,并在解密时研究应用程序的行为,会发生什么?
也许有一天,正在进行的加密研究有可能产生一种创新的加密设计,不会对数十年来对安全加密原语和加密协议设计的研究取消任何进展。但是,我们目前还没有,因此您不需要投资一个不必要的复杂的研究原型来解决问题。
我不希望大多数工程师能够在没有受到任何挫折的情况下得到这个解决方案。这里有一个坏主意是,你需要安全的加密(见下文),你唯一的方法就是查询数据库中的每个密文,然后对它们进行迭代,逐个解密,并在应用程序代码中执行搜索操作。
如果您被迫接受了这种方法,那么实际上你做的将会打开你的应用程序去进行DOS攻击。对您的合法用户来说,速度可能会慢一些。这显然是一个愤世嫉俗的答案,你可以做得比这更好,我们将在下面展示。
安全加密搜索将变得简单
我们首先避免在不安全/不明智的部分中概述的所有问题:所有密文将是经认证的加密方案的结果,优选具有大的随机数(由安全的随机数生成器生成)。
通过认证加密方案,密文是非确定性的(相同的消息和密钥,但是不同的随机数,产生不同的密文)并且被认证标签保护。一些合适的选项包括:XSalsa20-Poly1305,XChacha20-Poly1305和(假设在CAESAR得出结论之前没有破坏)NORX64-4-1。如果你使用NaCl或者libsodium,你可以crypto_secretbox在这里使用。
因此,我们的密文与随机噪声无法区分,并且可以防止选择密文攻击。
然而,这里有一个迫在眉睫的挑战:我们不能仅仅加密任意消息,我们还要查询数据库以匹配密文。幸运的是,有一个聪明的解决方法。
在开始之前,请确保加密实际上使您的数据更安全。非常重要的一点是,“加密存储”不是保护易受SQL注入攻击的CRUD应用程序的解决方案。解决实际问题(即防止SQL注入)是唯一的办法。
如果加密是实现的合适的安全控制,这意味着用于加密/解密数据的加密密钥对于数据库软件是不可访问的。在大多数情况下,将应用程序服务器和数据库服务器保留在单独的硬件上是有意义的。
实现加密数据的字符搜索
可能的用途:存储社会保险号,但仍然可以进行查询。
为了存储加密信息并仍然在SELECT查询中使用明文,我们将遵循一种我们称之为盲索引的策略。一般的想法是将明文的密钥哈希(例如HMAC)存储在单独的列中。重要的是,盲索引键与加密密钥不同,数据库服务器未知。
对于非常敏感的信息,而不是简单的HMAC,您将需要使用键作为静态盐的键的拉伸算法(PBKDF2-SHA256,scrypt,Argon2)来减缓枚举尝试。我们不用担心任何一种情况下的脱机暴力攻击,除非攻击者可以获取密钥(不能存储在数据库中)。
所以如果你的表格模式看起来像这样(在PostgreSQL中):
CREATE TABLE humans ( humanid BIGSERIAL PRIMARY KEY, first_name TEXT, last_name TEXT, ssn TEXT, /* encrypted */ ssn_bidx TEXT /* blind index */ ); CREATE INDEX ON humans (ssn_bidx);
您将存储加密的值humans.ssn。明文SSN的盲目索引将进入humans.ssn_bidx。看起来天真的想法实现的话可能如下所示:
<?php /* This is not production-quality code. * It's optimized for readability and understanding, not security. */ function encryptSSN(string $ssn, string $key): string { $nonce = random_bytes(24); $ciphertext = sodium_crypto_secretbox($ssn, $nonce, $key); return bin2hex($nonce . $ciphertext); } function decryptSSN(string $ciphertext, string $key): string { $decoded = hex2bin($ciphertext); $nonce = mb_substr($decoded, 0, 24, '8bit'); $cipher = mb_substr($decoded, 24, null, '8bit'); return sodium_crypto_secretbox_open($cipher, $nonce, $key); } function getSSNBlindIndex(string $ssn, string $indexKey): string { return bin2hex( sodium_crypto_pwhash( 32, $ssn, $indexKey, SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE, SODIUM_CRYPTO_PWHASH_MEMLIMIT_MODERATE ) ); } function findHumanBySSN(PDO $db, string $ssn, string $indexKey): array { $index = getSSNBlindIndex($ssn, $indexKey); $stmt = $db->prepare('SELECT * FROM humans WHERE ssn_bidx = ?'); $stmt->execute([$index]); return $stmt->fetchAll(PDO::FETCH_ASSOC); }
对于我的B-Sides Orlando 2017年谈话的补充材料,包含更全面的POC。它是根据知识共享CC0许可证发布的,对于大多数人来说,这个许可证与“公共领域”相同。
安全分析与限制
根据你的确切威胁模型,这个解决方案留下了两个必须回答的问题:
是否真的可以安全使用,还是说可能仍然会泄漏数据,就像滤网一样? 它的用途有什么局限性?(这一个其实已经回答了。)
以上我们的例子中,假设您的加密密钥和盲索引键是分开的,两个密钥都存储在Web服务器中,而数据库服务器没有任何方法来获取这些密钥,那么任何攻击者只会破坏数据库服务器但不是Web服务器)只能够了解若干行是否共享一个社会安全号码,而不是共享SSN是什么。为了使索引成为可能,这种重复的条目泄漏是必要的,这又允许从用户提供的值进行快速的SELECT查询。
此外,如果攻击者能够在遵守存储在数据库中的盲目索引的同时观察/更改明文作为应用程序的普通用户,则可以将其利用为选择的明文攻击,在这些攻击中,它们以用户身份迭代每个可能的值然后与所得的盲目索引值相关联。这在HMAC方案中比在Argon2方案中更实际。而对于高熵或低灵敏度值(而不是SSN)来说,我只能说物理学真的就在我们身边。
对于现在这样的情况,犯罪分子要想进行更实际的攻击就要将值从一行替换到另一行,然后正常访问应用程序,而这就将会揭示明文,除非采用了独特的每行密钥(例如hash_hmac('sha256', $rowID, $masterKey, true)甚至可以在这里有效缓解)其他人会更喜欢)。这里最好的防御是使用AEAD模式(将主键作为附加关联数据传递),以便将密文连接到特定的数据库行。(这样做并不会阻止攻击者删除数据,这是一个更大的挑战。)
与其他解决方案泄露的信息量相比,大多数应用程序的威胁模型应该将其视为可接受的权衡。只要您使用身份验证加密进行加密,以及HMAC(用于盲目索引非敏感数据)或密码散列算法(盲目索引敏感数据),就很容易理解应用程序的安全性。
然而,它确实有一个非常严重的限制:它只适用于完全匹配。如果两个字符串以无意义的方式不同,但总是会产生不同的加密散列,则搜索一个不会产生另一个。如果您需要执行更多高级查询,但是仍然希望将解密密钥和明文值保留在数据库服务器的手中,我们将必须获得创造性。
还值得注意的是,虽然HMAC / Argon2可以防止不具有密钥的攻击者学习数据库中存储的明文值,但它可能在现实世界会显示元数据(例如两个看似无关的人共享一个街道地址)。
实现加密数据的模糊搜索
可能的用途:加密人们的法定名称,并能够仅搜索部分匹配内容。
我们在上一节的基础上构建一个盲目索引,让您可以查询数据库的精确匹配。
这一次,我们将不会在现有表中添加列,而是将额外的索引值存储到连接表中。
CREATE TABLE humans ( humanid BIGSERIAL PRIMARY KEY, first_name TEXT, /* encrypted */ last_name TEXT, /* encrypted */ ssn TEXT, /* encrypted */ ); CREATE TABLE humans_filters ( filterid BIGSERIAL PRIMARY KEY, humanid BIGINT REFERENCES humans (humanid), filter_label TEXT, filter_value TEXT ); /* Creates an index on the pair. If your SQL expert overrules this, feel free to omit it. */ CREATE INDEX ON humans_filters (filter_label, filter_value);
这种变化的原因是规范化我们的数据结构。你可以通过添加列到现有的表,但它可能会变得混乱。
接下来的改变是,我们将为每个不同类型的查询(每个都有自己的密钥)为每列存储一个单独的,不同的盲目索引。例如:
需要忽略空格的不区分大小写的查找?
存储盲目索引preg_replace('/[^a-z]/', '', strtolower($value))。
需要查询他们姓氏的第一个字母?
存储盲目索引strtolower(mb_substr($lastName, 0, 1, $locale))。
需要匹配“这个字母,并在那个字母结束”?
存储盲目索引strtolower($string[0] . $string[-1])。
需要查询他们姓氏的前三个字母和他们的名字的第一个字母?
你猜对了!基于部分数据来构建另一个索引。
每个指标都需要有一个独特的关键,应该大力刺激明文隐含的盲目索引,将真正的明文值泄露给一个犯罪分子,用谜语填字游戏。只能为严重的业务需求创建索引,并且积极地记录对应用程序这些部分的访问。
时间与存储的交易
到目前为止,所有设计命题都赞成允许开发人员仔细阅读SELECT查询,同时最小化解密子例程的次数。一般来说,这样已经能做到让大多数人满意了。
但是,有些情况下,如果意味着节省大量的磁盘空间,搜索查询中的轻微性能也可以接受。
这里的技巧很简单:将您的盲目索引截断为16,32或64位,并将其视为 Bloom过滤器:
如果查询中涉及到的盲目索引与给定行匹配,则数据可能是一个匹配项。
您的应用程序代码将需要为每个候选行执行解密,然后仅提供实际匹配。
如果查询中涉及的盲指数与给定行不匹配,则数据绝对不匹配。
如果您的数据库服务器最终会更有效地存储它,也可能会将这些值从字符串转换为整数。