跳至正文
Eric®
作品与手记EN

← 文章  /  系统架构

加密后的数据,真的还能做模糊查询吗?

从手机号后四位到任意子串:标签如何生成,候选为什么会误报,以及索引会暴露什么。

假设我们正在做一个客服后台。 手机号不能明文存储,这个要求很合理。于是,原本的 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%'

它省下了把所有密文传给应用的开销,却没有自动解决筛选成本。已有的密文字段索引,通常不能帮助数据库判断解密后的内容是否包含某个子串。

数据库有自己的优化工具: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 不提供这一性质。

统计频率、记录关联和接口反馈,是接下来更难处理的攻击面。攻击者可以绕开直接计算标签这条路,利用数据分布和系统正常提供的功能去猜。密钥保护住了 HMAC 运算,索引泄漏的关系还得继续算账。

后四位搜索,一个后缀标签就够

在介绍 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 也会进入候选集。

误报来自索引丢掉的顺序信息:两个片段分别出现过,交集筛选就会把记录留下。要验证完整的 01380,应用还得解密候选,检查片段是否在原文里连续排列。

重复次数也会丢失。查询 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 主要影响片段的区分能力。十一位字符串从 4-gram 改成 2-gram,未去重窗口从八个增加到十个,真正可能大幅膨胀的是候选集:更短的片段会被更多记录共享。窗口数量和候选选择性,得在容量表和延迟表里分别列出来。

其次,如果同时保存许多长度的片段,甚至保存全部子串,候选片段数量才可能增长到关于字符串长度的平方级。不能把“固定一种 n”与“支持所有长度”混为一谈。

上线前,我会用脱敏或合成数据覆盖真实的分布特征,测候选集大小、尾延迟、索引体积、写入放大和维护成本。要不要增加更长片段、限制最短查询长度,或者先按租户和业务范围过滤,应该由这些结果决定,而不是由一个通用的“建议四位起搜”决定。

数据库仍然看得到频率与关联

假设攻击者拿到了数据库。虽然不知道某个标签代表 1380 还是 8000,但他仍然能看到:哪些记录共享标签、每个标签关联多少条记录、哪些标签经常共同出现,以及一条记录有多少个不同标签。

如果他能持续观察数据库上的查询,还可能看到哪些查询标签反复出现、哪些记录被访问、每次返回多少候选。静态数据库快照和持续观察查询,泄漏的信息不是同一回事。

如果他既能通过正常业务写入自己选择的数据,又能观察数据库中的标签变化,还可能逐步建立“已知输入—标签”的对应关系。仅仅不提供一个名为 generateHmac 的 API,并不足以排除这种能力。

可搜索加密研究把这类利用额外知识和泄漏信息恢复查询或明文的攻击称为 leakage-abuse attacks。Cash 等人的研究表明,在特定泄漏模型和攻击者知识条件下,系统即使使用了密码学机制,泄漏仍然可以被实际利用。这里被攻击的,不一定是加密算法,而可能是数据之间的关系。

手机号和短编号恰好是全量 n-gram 索引最该慎重的字段。四位片段只有一万种输入,窗口还彼此重叠,再加上前缀规则和跨表关联,攻击者手里的拼图远比一串随机标签丰富。省掉几次扫描,可能换来长期可利用的关联信息。

AWS Database Encryption SDK 的文档也明确提醒:可搜索标签会暴露数据分布;分布偏斜和相关字段会影响安全性,不能只看算法名称来判断一个索引是否合适。

片段长度与标签位数影响不同

n-gram 的 n,指明文片段长度。HMAC 标签保留多少 bit,指密码学输出长度。这是两个不同参数。

把标签从 256 bit 截断到 128 bit,可以减少标签值的存储,但不会让一万种四位数字突然拥有 128 bit 的输入熵。对这么小的输入域,128 bit 标签几乎不会产生随机碰撞,仍然近似保留片段的等值关系。

一些现成方案会主动截断得更短,让不同输入共享同一标签,通过增加误报来减弱区分能力,再在解密后过滤。这个策略需要结合数据分布、关联性和误报成本设计,不是“短一点就安全”,更不能把某个产品的配置结论直接套到任意自制 n-gram 索引上。

片段越长,候选通常越有区分度,罕见值也可能越容易被识别。片段越短,误报和核验开销又会上来。把实际分布、辅助信息和误报成本一起评估,才能选参数;用“越长越安全”的星级表,只是把取舍藏起来。

“受控泄漏”应该是一张能审查的清单:允许暴露哪些关系,这些关系怎样与其他信息组合,业务为什么接受剩下的风险。架构图上写下这四个字,分析工作才刚开始。

写入一致性、密钥轮换和接口权限

规范化规则要版本化

数据库写入时去掉空格,查询时却没有去掉;旧版本保留国家码,新版本只保留本地号码;同一个字符在不同系统里使用不同的 Unicode 表示——这些差异都可能让本应相同的输入生成不同标签。

完整手机号的规范化与局部搜索串的处理,也不能不加区分地共用一个“补全号码”函数。对局部查询 0138,我们要验证它是允许搜索的片段,而不是把它解析成一个完整号码。

姓名、地址和邮箱更需要先定义业务语义:是否区分大小写、是否忽略标点、是否进行字符归一化,不能为了方便一律删除或转换。索引生成和最终核验必须使用对应版本的同一套语义。

规则变化意味着需要重建索引。它不是换一个工具函数就结束的事情。

按租户、字段和用途隔离密钥

字段加密和搜索标签各用独立用途的密钥。可以分别生成,也可以通过成熟的密钥派生方案生成彼此分离的子密钥,把用途和权限管理清楚。

例如,索引域可以包含租户、表、字段、查询类型、分片长度和规范化版本。HKDF 的 info 参数就是用来绑定应用上下文的一种标准机制;上下文需要采用无歧义的编码方式,不能随意拼接几个字符串。

这样,同一个 1380 在不同租户或不同字段中,不必形成一个可以直接跨域关联的共同标签。但这只能减少某类直接关联,不能自动消除业务数据本身的相关性。

另一个常见误区,是给每条记录都加独立随机 salt,以为能够同时隐藏重复值并保留一次等值查询。这样做以后,相同输入在不同记录中不再得到相同标签,原来的直接查找方式也就不成立了。随机化放在哪里,必须和查询流程一起设计。

密钥分离之后,还得看谁能同时取得它们。应用进程若能读取数据密钥和搜索密钥,进程失守就会同时暴露两者;单独泄漏搜索密钥,也足以让攻击者枚举低熵片段。密钥用途、进程权限和失陷后的影响范围,要放在同一张图里审。

KMS 可以用于保护和管理密钥,例如通过信封加密保护数据密钥;这不意味着每生成一个片段标签都必须远程调用一次 KMS。应用中的明文密钥生命周期、缓存、权限和审计仍需要单独设计。

AAD 认证密文,无法认证整个结果集

加密时,可以把租户 ID、记录 ID 和字段标识等上下文绑定为 AAD,也就是参与认证但不被加密的附加数据。这样,把某条密文直接换到另一条记录或另一个字段下,通常不能通过原有上下文的认证检查。

结果集还有另一类完整性问题。数据库可以漏掉索引记录,或者在同一上下文下返回以前保存的合法旧密文;AAD 仍可能验证通过。要确认一批结果完整且最新,还得验证检索过程和版本。

对需要抵抗恶意数据库的业务,完整性、新鲜度和可验证查询应成为独立需求,追加相应协议与验证机制。标签表加逐条解密核验,负责的是候选筛选和单条内容验证。

密文和索引必须一起维护

手机号修改后,旧标签要删除,新标签要生成,密文也要更新。在同一个关系数据库中,可以用事务把它们作为一次一致的变更提交;并发搜索还需要定义读取一致性,避免候选查询和密文读取落在互不相容的版本上。

若索引放在外部搜索引擎,必须处理异步同步、重试、幂等和版本对齐。不能一边接受最终一致性,一边保证“号码刚改完一定能立即搜到”。

搜索密钥轮换同样不能只改配置。新密钥生成的标签与旧标签不同,需要安排双写或其他迁移策略,回填存量数据,覆盖必要版本的查询,再安全移除旧索引。规范化规则变化时也是一样。

还要区分“替换实际的数据或搜索密钥”与“只更换包裹它们的上层密钥”。后者可能只涉及重新包裹,而不需要重算全部搜索标签;前者通常不能省略数据或索引迁移。

删除也不止删主表。索引、缓存、搜索引擎副本和备份保留策略,都必须在数据生命周期里有明确安排。

搜索接口能成为枚举入口

一个允许反复试号码片段、返回完整客户信息、又缺少业务范围限制的搜索接口,会成为现成的枚举工具。攻击者从结果里拿数据,后台 HMAC 做得再规范,也拦不住这条正常业务路径。

我会把授权、查询范围、速率限制、异常枚举检测和返回字段最小化放在同一份设计中。日志也不能因为记录的是 HMAC 标签,就被当作无害数据:稳定标签仍然能关联多次请求和多条记录。

这些措施不会消除数据库索引本身的泄漏,但可以减少另一个经常被忽略的风险——系统通过自己提供的正常功能,把敏感信息交出去。

子串索引之外,还有哪些需求没解决

编辑距离救不回没有召回的记录

假设希望把 Erci 匹配到 Eric,可以在解密后计算编辑距离。但这只解决了“拿到候选以后怎么判断”,没有解决“怎样把它先捞出来”。

前面的子串索引要求查询的全部 n-gram 都出现。对容错匹配,这个条件通常过严;一个字符变化就可能破坏多个片段。

把条件改成“至少共享一个片段”也不是完整答案。一个简单反例是 abc 与 axc:它们只相差一次字符替换,但未加边界标记的 2-gram 分别是 {ab, bc} 和 {ax, xc},没有任何共同片段。

所以,容错方案需要围绕指定的距离函数、长度范围和允许错误数设计召回过滤;短字符串可能需要额外通道,或者在已经受限的授权集合内直接扫描。必须明确是否保证召回完整,再用测试验证。Levenshtein 距离按插入、删除和替换计数;若要把相邻字符交换视为一次错误,还需要选用相应的距离定义。

编辑距离可以排除已召回候选中的误报,第一阶段漏掉的记录则需要修改召回条件。

Elasticsearch 负责执行标签查询

当标签数量、查询并发或组合检索需求超过当前关系数据库方案的合适范围,可以评估专门的搜索引擎。

对本文这种标签索引,存入 Elasticsearch 的应是应用已经生成的稳定标签,而不是解密后的原始手机号。通常应把标签当作 keyword 这类精确值处理,而不是交给普通文本分析器再次分词;多个标签的匹配条件,可以用对应的精确集合查询来表达。

换搜索引擎,解决的是倒排查询的执行和扩展。标签频率、记录关联、查询模式和返回规模,仍由索引设计决定。是否迁移,就看倒排列表长度、租户划分、更新模式和实际负载;“千万级”三个字,给不了部署答案。

SSE 要按具体协议评估

可搜索对称加密,也就是 SSE,是一个研究领域,不是一种固定算法。它研究如何在外包存储的同时支持搜索,并在明确的模型下定义与证明安全性质。

评估 SSE,要逐项看协议支持的查询、泄漏函数、更新方式、信任假设与交互成本。形式化证明会明确这些前提,泄漏利用研究则追问攻击者还能怎样组合信息。“自制 HMAC 标签”“SSE”“全同态加密”各有具体方案,单按名字排安全星级,省掉了最重要的分析。

如果需求还包括隐藏重复查询或访问了哪些记录,就需要重新评估专门协议及其计算、通信和运维成本,而不是给现有标签表再换一个更长的哈希。

回到客服后台,该提供哪种搜索

对于最初的客服需求,我会先确认完整号码查询是否已经够用。若客服确实只记得后四位,就只建立后缀索引,把搜索限制在必要的租户和订单范围。数据量小、查询频率低时,先过滤再逐条解密扫描,也值得测一测:它省去了长期维护一套搜索索引的成本。

任意子串搜索需要更多理由。确认需求不可替代后,才用实际数据分布测候选规模,评估频率、关联和已知输入带来的推断风险。高度敏感、取值空间很小或关联性很强的字段,评估结果可能要求调整客服流程。

随机化认证加密、最小搜索索引、授权范围内召回、解密核验,这几步可以形成一个待评估的设计。密钥隔离、规范化版本、写入一致性和接口限制也要同时成立;只写下“AEAD + HMAC + n-gram”,还不足以交付这套系统。

加密后的数据能支持某些搜索,但索引会留下关系。验收时除了检查密文能否解密、查询能否命中,我还会检查另一件事:拿到数据库的人,能从搜索结构里推断出多少关于客户的信息?

《被问懵了,加密后的数据如何进行模糊查询?》提出的客服场景,是我梳理这套方案的起点。一路推下来,我更倾向于先缩小搜索需求,再决定值得为它暴露多少关系。索引加得越多,能搜的内容越多,今后要保护和解释的东西也越多。