我们从一个简单的场景开始(这可能与很多地方政府或医疗保健应用非常相关):
  您正在建立一个新系统,需要从其用户收集社会保障号码(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许可证发布的,对于大多数人来说,这个许可证与“公共领域”相同。