加密后的数据,真的还能做模糊查询吗?
从手机号后四位到任意子串:标签如何生成,候选为什么会误报,以及索引会暴露什么。
假设我们正在做一个客服后台。 手机号不能明文存储,这个要求很合理。于是,原本的 phone 字段经过加密,数据库里只剩下一串无法直接阅读的字节。 随后,一个同样合理的需求来了:客户记不清完整手机号,希望输入后四位就能找到订单。再过几天,需求又变成了“输入中间几位也应该能搜”。
到这里,原来那句 SQL 就不管用了:
SELECT id FROM users WHERE phone LIKE '%0138%';
问题不在于数据库不够聪明。对于我们通常采用的随机化字段加密方案,隐藏明文之间的关系,本来就是它应该做的事。不能一边要求别人看不懂数据,一边又希望数据库从密文中直接认出哪几条记录包含同一个片段。
但这也不意味着加密之后就只能放弃搜索。 我更倾向于换一种拆法:让密文负责保护原始数据,让另一套经过设计的索引负责有限的查询。 这条路线在 Blind Index 和一些商业加密 SDK 中都能看到,不过,“有限”两个字不能省略。索引提供了能力,也会暴露原本被加密隐藏的关系。
实现之前,先明确要恢复哪些查询能力、数据库因此会看到什么。
搜后四位、搜子串和纠错,是三种需求
“支持模糊查询”不是一个足够精确的技术需求。 下面几种操作,在产品界面上可能只是同一个搜索框,在实现上却是不同的问题。
| 用户想做什么 | 实际查询语义 | 例子 |
|---|---|---|
| 输入完整号码找人 | 精确匹配 | 完整手机号相同 |
| 输入后四位找订单 | 固定长度后缀匹配 | 末尾是 8000 |
| 输入开头几位查找 | 前缀匹配 | 以 1380 开头 |
| 输入中间一段查找 | 子串匹配 | 包含 0138 |
| 输错几个字也能找到 | 容错匹配 | Erci 仍能匹配 Eric |
SQL 的 LIKE '%xxx%',在这里主要对应子串搜索,不等于拼写纠错。后者还要定义允许什么错误、距离怎么算、结果怎么排序。本文先解决按字面内容查找子串,不尝试实现任意 SQL 通配符表达式。
查询语义会决定需要建立哪些索引,以及暴露哪些关系。 只查后四位,不需要把整个手机号拆成所有四位片段。只允许输入完整邮箱,也不需要预先建立邮箱中每一段字符的索引。为了一个尚未确认的未来需求,把所有可搜索特征提前存下来,在敏感数据场景里未必是“扩展性好”,也可能只是提前扩大泄漏面。
还有一个更基础的问题:我们想防谁? 磁盘被盗、数据库备份泄漏、攻击者拿到数据库查询权限,以及应用服务器被完全控制,要求的防护并不相同。磁盘加密保护不了一个已经能正常查询数据库的攻击者;反过来,数据库具备解密能力,也不代表单独一份不含密钥的离线备份必然能被解开。加密应该放在哪一层,要跟威胁模型一起决定。
本文采用的主要边界是:应用服务可以处理明文,数据库只保存密文和搜索索引;攻击者可能读到数据库,但没有应用侧的密钥。 后面再讨论长期观察查询、主动写入数据,以及应用自身失守时会发生什么。
扫描、库内解密和明文副本
逐条解密扫描,先看能否缩小范围
这是最容易写出来的版本:从数据库读取记录,在应用里逐条解密,再执行 contains。
它不一定一无是处。一个经过授权、只有几百条记录的租户,或者一次低频离线核查,完全可能适合这种做法。采用游标或分批读取,也不必把所有明文一次性塞进内存。
但流式处理只能缓解内存问题,不能消除扫描工作。如果没有其他选择性较好的过滤条件,要检查一千万条记录,仍然需要读取并处理这一千万条记录。瓶颈可能是存储读取、网络传输、对象处理,也可能是解密和字符串比较,不能只盯着加密算法有多快。
所以我不会简单把它归为“不能用”,而是会问:扫描范围能否被限制,频率有多高,延迟是否可接受?
解密搬进 SQL,信任边界也跟着移动
第二种做法,是把解密搬进 SQL。下面只是表达思路的伪 SQL,并不是某个数据库可以直接执行的通用函数:
WHERE decrypt(phone_ciphertext) LIKE '%0138%'
它省下了把所有密文传给应用的开销,却没有自动解决筛选成本。已有的密文字段索引,通常不能帮助数据库判断解密后的内容是否包含某个子串。
这里还要避免一个常见的过度概括:不能把问题说成“用了函数就一定无法建立索引”,也不能说“所有 LIKE '%xxx%' 都无法优化”。PostgreSQL 支持表达式索引,pg_trgm 的 GIN/GiST 索引也能支持一些包含前导通配符的搜索;普通 B-tree 则不是这种任意子串查询的通用答案。
然而,如果为了加速查询,把解密后的文本或可直接还原的明文特征物化到索引里,敏感信息又进入了数据库。这已经不是单纯的性能优化,而是在改变安全边界。
同样,把解密能力交给数据库,也意味着必须信任具备相应权限的数据库执行环境。明文会在那个环境中出现;SQL 参数、诊断信息和日志如何记录,也都需要重新检查。不能笼统地说每一条明文都会进入备份或慢查询日志,但暴露面确实需要逐项评估。
搜索字段也在保护范围内
还有一种很直接的设计:主字段加密,旁边再放一个 phone_search,里面保存完整明文,用它来查。
在“数据库泄漏时仍要保护手机号”这个目标下,这相当于把问题绕回了起点。攻击者不需要解密主字段,读取搜索字段就够了。
即便只保存后四位,也应该明确承认:这四位是有意公开给数据库的,而不是因为完整号码加密了,它们就自动变得安全。
安全评估要覆盖副本、索引和执行路径。主表里有一列叫 ciphertext,不足以说明数据库泄漏时数据仍然受保护。
密文之外,再建一份 Blind Index
我会先保留一份正常的随机化加密数据,而不是为了搜索去修改它的加密方式。
对于本文的示例,可以选择成熟密码库提供的 AES-GCM;ChaCha20-Poly1305 也是一种标准化的认证加密方案。重点不只是保密,还要在解密时验证数据是否被篡改。新系统没有必要沿用旧教程里的 DES,也不应该自行发明一套“既加密又保留字符规律”的算法。
以 AES-GCM 为例,存储的加密封装通常还要包含算法或格式版本、密钥版本、nonce 和认证标签。nonce 不需要保密,但同一密钥下不能重复使用。采用随机 nonce 时,也必须遵守方案的随机源、使用量和轮换要求,不能把“每次生成随机数”理解成无需管理的无限保证。
搜索走另一条路径。 先从最简单的精确匹配开始。应用对规范化后的手机号计算一个带密钥的确定性标签:
原始手机号 ──认证加密──> phone_envelope
规范化手机号 ──HMAC-SHA-256(K_search, value)──> phone_index
用户输入完整号码时,应用使用同一套规则生成查询标签,数据库执行标签的等值匹配,再由应用解密候选记录并核对。
这类辅助结构通常称为 Blind Index,盲索引。不同实现可能采用不同的带密钥哈希、密钥派生或截断策略;本文使用 HMAC-SHA-256 来说明确定性搜索标签的构造,不把它等同于任何现成库的完整实现。
它不是一种可以反向解密的“另一份密文”,更不是匿名化证明。数据库虽然不知道标签对应哪个号码,却能在同一个索引域内识别相同标签。
四位数字经不起无密钥哈希枚举
对于四位数字,可能的输入只有 0000 到 9999,总共一万种。 如果索引是 SHA256("1380"),拿到数据库的人可以自行计算这一万种输入的哈希,建立对照表。哈希输出再长,也不会增加输入本身的可能性。
HMAC 引入了一个不交给数据库的秘密密钥。在密钥未泄漏、攻击者也没有可利用的标签生成接口时,他不能再像处理普通 SHA-256 那样,自行计算完整对照表。公开的固定 salt 不提供这一性质。
但这句话的边界很重要:不能直接计算对照表,不等于不能通过统计、关联或接口反馈猜出标签含义。 后面还要回来讨论这件事。
后四位搜索,一个后缀标签就够
在介绍 n-gram 之前,我想先把最容易被忽略的优化放在前面。 假如需求确实只是“搜索手机号后四位”,可以只对规范化号码的后四位生成一个标签,并为这个用途设置独立的索引域。例如,手机号 13800138000 只需要一个对应 8000 的后缀标签。
查询时,对用户输入的四位数字使用同一规则生成标签,再做等值查询和解密核验。
这样,数据库不需要知道这个号码中还出现了 1380、3800 或 0013。索引数量更少,写入维护更简单,暴露的特征也少于为同一字段再额外建立一套全片段索引。
当然,四位后缀仍然是低基数特征。很多号码会共享同一个后缀,它也不能被当成身份验证凭据。限制查询范围、限制返回内容和防止批量枚举,仍然不可缺少。
我的顺序会是:完整值查询先用完整值索引;固定后缀查询就用固定后缀索引;前缀查询只建立业务实际需要的那些长度。只有确认必须搜索任意位置的片段,才进入下一步。
用 4-gram 建立子串候选索引
假设需求已经明确:允许查找手机号中任意连续四位及以上的数字。 这时可以用长度为 4 的滑动窗口,把规范化后的号码拆成片段,也就是 4-gram。以 13800138000 为例:
原文:13800138000
按位置生成的 4-gram:
1380 3800 8001 0013 0138 1380 3800 8000
去重后的片段:
1380 3800 8001 0013 0138 8000
这里有八个窗口位置,但只有六个不同片段。对于“是否包含”的候选筛选,同一条记录中的重复片段可以去重,没必要重复存储。 然后,对每个片段生成 HMAC 标签。数据库保存的是“某个标签出现在哪些记录中”的关系,而不是把所有标签拼成一大段字符串,再执行一次 LIKE '%标签%'。
后者既容易重新引入扫描问题,也没有充分利用标签本来就适合等值匹配的特点。独立的标签表,实际上是在构造一个简单的倒排索引:由标签找到记录 ID。
下面是一份用于说明结构的 PostgreSQL 最小表设计。它不是完整的用户模型,省略了具体业务字段和授权策略:
CREATE TABLE users (
tenant_id BIGINT NOT NULL,
id BIGINT NOT NULL,
phone_envelope BYTEA NOT NULL,
PRIMARY KEY (tenant_id, id)
);
CREATE TABLE phone_search_tokens (
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
index_version INTEGER NOT NULL,
token BYTEA NOT NULL CHECK (octet_length(token) = 32),
PRIMARY KEY (tenant_id, index_version, token, user_id),
FOREIGN KEY (tenant_id, user_id)
REFERENCES users (tenant_id, id) ON DELETE CASCADE
);
CREATE INDEX phone_search_tokens_by_user
ON phone_search_tokens (tenant_id, user_id, index_version);
这个例子使用完整的 32 字节 HMAC-SHA-256 输出。index_version 对应一套明确的索引配置,包括密钥版本、规范化规则和分片规则。主键既支持按租户、版本和标签查找,也阻止同一记录重复写入相同标签。另一个索引用于按记录维护和删除标签。
查询 01380:取两个标签的交集
它会被拆成两个片段:0138 和 1380。应用生成两个查询标签,然后查找同时包含这两个标签的记录。
更一般地,可以使用下面的参数化查询。第三个参数是已经去重、非空、且不含 NULL 的标签数组,不是用户输入的原文:
PREPARE search_phone (BIGINT, INTEGER, BYTEA[]) AS
SELECT user_id
FROM phone_search_tokens
WHERE tenant_id = $1
AND index_version = $2
AND token = ANY ($3)
GROUP BY user_id
HAVING COUNT(*) = cardinality($3)
ORDER BY user_id;
这里使用了 PostgreSQL 的预备语句和数组参数。COUNT(*) 能直接使用,是因为表的主键已保证每条记录的标签不重复,查询数组也已经去重。
这个示例假设调用者已获准搜索该租户内的全部记录。若系统还有记录级权限,就要在候选筛选中加入相应授权条件;租户 ID 也必须来自经过验证的身份上下文,不能相信请求随便传来的值。
到这一步,我们得到了候选 ID,还没有得到最终搜索结果。
片段都出现过,顺序却可能不对
看两个用于演示的数字串:
A:13800138000
B:13801110138
查询:01380
查询片段:0138、1380
A 确实包含 01380。 B 不包含这个完整子串,但它开头有 1380,末尾有 0138。如果索引只记录“出现过哪些片段”,B 也会进入候选集。
这不是 HMAC 碰撞,更不是加密失效。即使标签从不发生碰撞,结果也一样,因为索引没有保存片段之间的顺序和相邻关系。
重复次数也会产生类似问题:查询 11111 时,去重后只有一个 1111 片段。某条记录中出现了四个连续的 1,不代表出现了五个。
因此,完整查询路径应该是:
验证查询权限与输入
→ 使用指定版本的规则规范化
→ 分片、生成标签并去重
→ 在授权范围内查候选 ID
→ 读取候选密文并验证、解密
→ 对规范化后的明文执行真正的子串判断
→ 按权限输出结果
为什么这种方法仍然有用?因为子串匹配具有一个单向关系:如果字段确实包含完整查询串,那么查询串的每一个 n-gram,也必然出现在字段中。
所以,在索引完整且一致、规范化规则相同、查询长度不小于 n、没有随意截断候选集的前提下,这种全片段交集筛选不会因为分片规则本身漏掉真正的子串匹配。反方向不成立,所以需要核验。
对于少于四位的输入,这套 4-gram 索引没有对应的查询片段。应该明确拒绝、要求补充条件,或切换到另一个经过设计的流程,而不是让“空数组”意外变成一次全量搜索。
分页也必须按这个逻辑处理。数据库返回一百个候选,不代表最终会有一百个命中;候选数量更不能直接当作结果总数。如果为了性能只取前一百个候选,就不能悄悄宣称“没有更多结果”。可以继续分批核验,也可以明确要求用户缩小范围,但必须让接口语义与实际行为一致。
一千万条记录,会剩下多少候选
讲到这里,很容易得出一个过于乐观的结论:全表扫描是 O(N),加了索引就变成 O(log N),问题解决了。 但标签的查找成本,只是整个查询的一部分。 还要读取标签对应的记录列表,计算交集,读取候选密文,再解密核验。一个标签如果匹配了几十万条记录,不能因为用了 B-tree,就把这些工作当作不存在。
用一个刻意简化的模型估算:假设一千万条十一位数字串,每条有八个四位窗口,数字分布近似独立均匀。对某个固定四位片段,按稀有事件近似,命中的记录数大约是:
10,000,000 × 8 ÷ 10,000 ≈ 8,000
这只是帮助判断数量级的估算,不是实际手机号数据的性能测试。真实号码有前缀规则、重复片段和业务分布偏差,结果可能很不一样。但它足以说明:不能随手画一个“一千万条变二十三条”的箭头,就当作索引的效果。
存储成本同样需要算清楚。 长度为 L 的字符串,固定 n 的滑动窗口数量是 max(L - n + 1, 0)。十一位数字采用 4-gram,最多八个标签。按本文每个标签 32 字节计算,标签值本身最多占 256 字节;上面的重复号码去重后是六个,也就是 192 字节。这还没有算租户 ID、记录 ID、版本号、行开销和数据库索引。
讨论 n 的取值时,还需要分别看窗口数量和候选选择性。 首先,n 变小,不代表单条记录的索引数量必然“爆炸”。同一个十一位字符串,从 4-gram 改成 2-gram,未去重的窗口数只是从八个变成十个;去重后的数量还取决于内容。真正明显变化的,往往是片段的区分能力和候选集大小。
其次,如果同时保存许多长度的片段,甚至保存全部子串,候选片段数量才可能增长到关于字符串长度的平方级。不能把“固定一种 n”与“支持所有长度”混为一谈。
上线前,我会用脱敏或合成数据覆盖真实的分布特征,测候选集大小、尾延迟、索引体积、写入放大和维护成本。要不要增加更长片段、限制最短查询长度,或者先按租户和业务范围过滤,应该由这些结果决定,而不是由一个通用的“建议四位起搜”决定。
数据库仍然看得到频率与关联
假设攻击者拿到了数据库。虽然不知道某个标签代表 1380 还是 8000,但他仍然能看到:哪些记录共享标签、每个标签关联多少条记录、哪些标签经常共同出现,以及一条记录有多少个不同标签。
如果他能持续观察数据库上的查询,还可能看到哪些查询标签反复出现、哪些记录被访问、每次返回多少候选。静态数据库快照和持续观察查询,泄漏的信息不是同一回事。
如果他既能通过正常业务写入自己选择的数据,又能观察数据库中的标签变化,还可能逐步建立“已知输入—标签”的对应关系。仅仅不提供一个名为 generateHmac 的 API,并不足以排除这种能力。
可搜索加密研究把这类利用额外知识和泄漏信息恢复查询或明文的攻击称为 leakage-abuse attacks。Cash 等人的研究表明,在特定泄漏模型和攻击者知识条件下,系统即使使用了密码学机制,泄漏仍然可以被实际利用。这里被攻击的,不一定是加密算法,而可能是数据之间的关系。
对于手机号、短编号这类高度结构化的数据,我尤其不愿意把“全量 n-gram 加 HMAC”写成无条件的通用推荐。每个四位片段的输入空间只有一万种,片段还彼此重叠。字段之间若存在关联,其他表中的信息也可能帮助缩小猜测范围。
AWS Database Encryption SDK 的文档也明确提醒:可搜索标签会暴露数据分布;分布偏斜和相关字段会影响安全性,不能只看算法名称来判断一个索引是否合适。
片段长度与标签位数影响不同
n-gram 的 n,指明文片段长度。HMAC 标签保留多少 bit,指密码学输出长度。这是两个不同参数。
把标签从 256 bit 截断到 128 bit,可以减少标签值的存储,但不会让一万种四位数字突然拥有 128 bit 的输入熵。对这么小的输入域,128 bit 标签几乎不会产生随机碰撞,仍然近似保留片段的等值关系。
一些现成方案会主动截断得更短,让不同输入共享同一标签,通过增加误报来减弱区分能力,再在解密后过滤。这个策略需要结合数据分布、关联性和误报成本设计,不是“短一点就安全”,更不能把某个产品的配置结论直接套到任意自制 n-gram 索引上。
因此,我不会给 n-gram 长度画一张“越长越安全”的星级表。较长片段可能改善选择性,也可能让罕见值更容易被区分。风险取决于整个数据集和攻击者掌握的信息,不取决于一个参数的单调变化。
“受控泄漏”也不是贴在架构图上的免责标签。它意味着我们已经列出允许暴露什么,检查这些暴露能否组合出更多信息,并确认剩余风险确实可以接受。
写入一致性、密钥轮换和接口权限
规范化规则要版本化
数据库写入时去掉空格,查询时却没有去掉;旧版本保留国家码,新版本只保留本地号码;同一个字符在不同系统里使用不同的 Unicode 表示——这些差异都可能让本应相同的输入生成不同标签。
完整手机号的规范化与局部搜索串的处理,也不能不加区分地共用一个“补全号码”函数。对局部查询 0138,我们要验证它是允许搜索的片段,而不是把它解析成一个完整号码。
姓名、地址和邮箱更需要先定义业务语义:是否区分大小写、是否忽略标点、是否进行字符归一化,不能为了方便一律删除或转换。索引生成和最终核验必须使用对应版本的同一套语义。
规则变化意味着需要重建索引。它不是换一个工具函数就结束的事情。
按租户、字段和用途隔离密钥
我不会把一把密钥同时拿去做字段加密和所有搜索标签。应使用独立生成的密钥,或者通过成熟的密钥派生方案,按用途派生彼此分离的子密钥。
例如,索引域可以包含租户、表、字段、查询类型、分片长度和规范化版本。HKDF 的 info 参数就是用来绑定应用上下文的一种标准机制;上下文需要采用无歧义的编码方式,不能随意拼接几个字符串。
这样,同一个 1380 在不同租户或不同字段中,不必形成一个可以直接跨域关联的共同标签。但这只能减少某类直接关联,不能自动消除业务数据本身的相关性。
另一个常见误区,是给每条记录都加独立随机 salt,以为能够同时隐藏重复值并保留一次等值查询。这样做以后,相同输入在不同记录中不再得到相同标签,原来的直接查找方式也就不成立了。随机化放在哪里,必须和查询流程一起设计。
密钥分离也不是万能的失陷隔离。若同一个应用进程能取得数据密钥和搜索密钥,应用被完全控制时,两者都可能暴露。即便只泄漏搜索密钥,攻击者也能枚举低熵片段,进而推断原始数据。分离是在限制用途和部分泄漏影响,不是在承诺“丢一把密钥也没事”。
KMS 可以用于保护和管理密钥,例如通过信封加密保护数据密钥;这不意味着每生成一个片段标签都必须远程调用一次 KMS。应用中的明文密钥生命周期、缓存、权限和审计仍需要单独设计。
AAD 认证密文,无法认证整个结果集
加密时,可以把租户 ID、记录 ID 和字段标识等上下文绑定为 AAD,也就是参与认证但不被加密的附加数据。这样,把某条密文直接换到另一条记录或另一个字段下,通常不能通过原有上下文的认证检查。
但不要把它扩大解释成“数据库无法作弊”。 数据库仍然可以漏掉某条索引记录,让应用查不到结果;也可能返回同一上下文下以前保存过的合法旧版本。单条密文的认证,不会自动证明整个查询结果是完整、最新的。
如果威胁模型要求抵抗这种恶意服务端行为,就需要额外的完整性、新鲜度或可验证查询设计。本文的标签表加解密核验,并没有提供这些保证。
密文和索引必须一起维护
手机号修改后,旧标签要删除,新标签要生成,密文也要更新。在同一个关系数据库中,可以用事务把它们作为一次一致的变更提交;并发搜索还需要定义读取一致性,避免候选查询和密文读取落在互不相容的版本上。
若索引放在外部搜索引擎,必须处理异步同步、重试、幂等和版本对齐。不能一边接受最终一致性,一边保证“号码刚改完一定能立即搜到”。
搜索密钥轮换同样不能只改配置。新密钥生成的标签与旧标签不同,需要安排双写或其他迁移策略,回填存量数据,覆盖必要版本的查询,再安全移除旧索引。规范化规则变化时也是一样。
还要区分“替换实际的数据或搜索密钥”与“只更换包裹它们的上层密钥”。后者可能只涉及重新包裹,而不需要重算全部搜索标签;前者通常不能省略数据或索引迁移。
删除也不止删主表。索引、缓存、搜索引擎副本和备份保留策略,都必须在数据生命周期里有明确安排。
搜索接口能成为枚举入口
数据库看不到搜索词,不代表调用搜索接口的人也看不到。 一个可以无限尝试号码片段、返回完整客户信息、又没有业务范围限制的接口,即使背后全部采用 HMAC,也会成为批量枚举入口。
我会把授权、查询范围、速率限制、异常枚举检测和返回字段最小化放在同一份设计中。日志也不能因为记录的是 HMAC 标签,就被当作无害数据:稳定标签仍然能关联多次请求和多条记录。
这些措施不会消除数据库索引本身的泄漏,但可以减少另一个经常被忽略的风险——系统通过自己提供的正常功能,把敏感信息交出去。
子串索引之外,还有哪些需求没解决
编辑距离救不回没有召回的记录
假设希望把 Erci 匹配到 Eric,可以在解密后计算编辑距离。但这只解决了“拿到候选以后怎么判断”,没有解决“怎样把它先捞出来”。
前面的子串索引要求查询的全部 n-gram 都出现。对容错匹配,这个条件通常过严;一个字符变化就可能破坏多个片段。
把条件改成“至少共享一个片段”也不是完整答案。一个简单反例是 abc 与 axc:它们只相差一次字符替换,但未加边界标记的 2-gram 分别是 {ab, bc} 和 {ax, xc},没有任何共同片段。
所以,容错方案需要围绕指定的距离函数、长度范围和允许错误数设计召回过滤;短字符串可能需要额外通道,或者在已经受限的授权集合内直接扫描。必须明确是否保证召回完整,再用测试验证。Levenshtein 距离按插入、删除和替换计数;若要把相邻字符交换视为一次错误,还需要选用相应的距离定义。
编辑距离可以排除已召回候选中的误报,第一阶段漏掉的记录则需要修改召回条件。
Elasticsearch 负责执行标签查询
当标签数量、查询并发或组合检索需求超过当前关系数据库方案的合适范围,可以评估专门的搜索引擎。
对本文这种标签索引,存入 Elasticsearch 的应是应用已经生成的稳定标签,而不是解密后的原始手机号。通常应把标签当作 keyword 这类精确值处理,而不是交给普通文本分析器再次分词;多个标签的匹配条件,可以用对应的精确集合查询来表达。
但搜索引擎不会因此替我们解决密码学问题。标签频率、记录关联、查询模式和返回规模,仍然需要分析。也不能因为数据到了“千万级”,就自动认定必须迁移:倒排列表长度、租户划分、更新模式和实际负载,比总行数更有判断价值。
SSE 要按具体协议评估
可搜索对称加密,也就是 SSE,是一个研究领域,不是一种固定算法。它研究如何在外包存储的同时支持搜索,并在明确的模型下定义与证明安全性质。
因此,不能给“自制 HMAC 标签”“SSE”“全同态加密”依次标上三颗、四颗、五颗安全星。具体协议支持什么查询、泄漏什么、怎样更新、信任谁,以及需要多少交互,必须分别讨论。形式化安全也总是相对于具体定义成立,不意味着不存在可利用的泄漏。
如果需求还包括隐藏重复查询或访问了哪些记录,就需要重新评估专门协议及其计算、通信和运维成本,而不是给现有标签表再换一个更长的哈希。
回到客服后台,该提供哪种搜索
对于最初的客服需求,我会先确认完整号码查询是否已经够用。若客服确实只记得后四位,就只建立后缀索引,把搜索限制在必要的租户和订单范围。数据量小、查询频率低时,先过滤再逐条解密扫描,也值得测一测:它省去了长期维护一套搜索索引的成本。
任意子串搜索需要更多理由。确认需求不可替代后,才用实际数据分布测候选规模,评估频率、关联和已知输入带来的推断风险。高度敏感、取值空间很小或关联性很强的字段,评估结果可能要求调整客服流程。
随机化认证加密、最小搜索索引、授权范围内召回、解密核验,这几步可以形成一个待评估的设计。密钥隔离、规范化版本、写入一致性和接口限制也要同时成立;只写下“AEAD + HMAC + n-gram”,还不足以交付这套系统。
加密后的数据能支持某些搜索,但索引会留下关系。验收时除了检查密文能否解密、查询能否命中,我还会检查另一件事:拿到数据库的人,能从搜索结构里推断出多少关于客户的信息?
本文的起点是阅读《被问懵了,加密后的数据如何进行模糊查询?》后的重新梳理。本文使用独立推导的标签结构、SQL 和反例,不沿用原文的算法选择与性能估算。写作缘起:原文链接。
示例数字串为教学用途;候选规模估算不是生产基准测试。本文给出的是架构分析与最小示例,不代表任何具体部署已经通过密码学安全审计。