密文字段做不了 LIKE '%张%',可行路径不是让数据库去解密,而是额外建一列”可比对的索引”:前缀场景用盲索引(对明文片段做 HMAC 后建索引),中缀场景用 N-Gram 分词哈希,全文场景把令牌化后的索引下沉到独立检索服务。保序加密能撑范围查询但抗不住频率分析,全同态加密目前只适合离线计算。真实业务里绝大多数需求是”精确查询 + 少量前缀模糊”,用确定性加密叠盲索引就能覆盖,不必上密码学重武器。
一、为什么 AES 密文天然不支持 LIKE
AES-GCM 每次加密都用随机 IV,同一个手机号加密两次得到两串完全不同的密文。数据库看到的是无序字节流,字符位置关系被彻底打散,LIKE、>、ORDER BY 全部失效。想恢复这些能力,只有两条路:把明文的某些特征”可控地泄露”到一个辅助列上,或者换一种保留特定运算性质的加密算法。前者是工程主流,后者是密码学方案。
绕不开的取舍在于:任何支持模糊匹配的密文索引,都会泄露一部分明文信息。设计时要回答的问题不是”泄不泄露”,而是”泄露多少可以接受”。
二、主流方案横向对比
| 方案 | 支持的查询 | 查询性能 | 安全强度 | 实现难度 |
|---|---|---|---|---|
| 确定性加密(固定 IV / SIV) | 精确等值 | 高 | 中(暴露重复值分布) | 低 |
| 盲索引(HMAC 前缀摘要) | 前缀模糊 | 中 | 中 | 中 |
| N-Gram 分词哈希 | 前缀、中缀 | 中 | 中低(片段频率可统计) | 中 |
| 加密布隆过滤器 | 包含判断 | 高 | 中 | 中高 |
| 可搜索加密 SSE | 关键词等值 | 中 | 高 | 高(缺成熟中间件) |
| 保序加密 OPE | 范围、排序 | 高 | 低(已知明文/频率攻击) | 中 |
| 全同态加密 FHE | 任意运算 | 极低 | 极高 | 极高 |
选型建议来自公开实践的一条经验值:若业务构成接近”80% 精确查询 + 15% 前缀模糊 + 5% 全文模糊”,采用确定性加密 + 盲索引 + Elasticsearch 的混合架构即可。
三、盲索引:落地成本最低的方案
3.1 原理与表结构
盲索引的做法是给密文列配一个”指纹列”:对明文取若干长度的前缀,逐个用独立派生密钥算 HMAC,截断后存入索引列。查询时对入参做同样处理,把模糊查询翻译成等值匹配。
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name_cipher VARBINARY(256), -- AES-256-GCM 全字段密文
name_bi1 CHAR(16), -- 前 1 字符盲索引
name_bi2 CHAR(16), -- 前 2 字符盲索引
name_bi3 CHAR(16), -- 前 3 字符盲索引
KEY idx_bi1 (name_bi1),
KEY idx_bi2 (name_bi2),
KEY idx_bi3 (name_bi3)
);
多级索引(首字 → 前 2 字 → 前 3 字)能让查询词越长、命中越精准,是常见优化手段。派生密钥必须与主加密密钥分离,否则一旦索引密钥泄露,攻击者可以离线爆破整列明文。
3.2 生成与查询改写
import hmac, hashlib
BI_KEY = b"blind-index-derived-key-not-master"
def blind(token: str, bits: int = 64) -> str:
"""截断 HMAC 作为盲索引,截断长度决定碰撞率与泄露量的平衡"""
digest = hmac.new(BI_KEY, token.encode, hashlib.sha256).digest
keep = bits // 8
return digest[:keep].hex
def build_prefix_index(plain: str, max_len: int = 3) -> dict:
return {f"bi{i}": blind(plain[:i]) for i in range(1, max_len + 1)}
# 写入:"张三丰" -> {'bi1': ..., 'bi2': ..., 'bi3': ...}
print(build_prefix_index("张三丰"))
def rewrite_like(keyword: str):
"""LIKE '张三%' 改写成盲索引等值条件"""
n = min(len(keyword), 3)
return f"name_bi{n} = %s", (blind(keyword[:n]),)
print(rewrite_like("张三")) # ("name_bi2 = %s", ('...',))
公开测试数据显示,10 万条记录规模下盲索引查询耗时约 0.8 秒,相比全表解密再比对提升约 82%,但存在约 5% 的假阳性。假阳性来自哈希截断碰撞,处理方式是把索引命中的结果集拉回应用层解密后二次过滤。
四、N-Gram 分词哈希:把中缀查询变成集合匹配
前缀索引撑不住 LIKE '%三%'。此时把字段切成滑动窗口片段,每个片段单独哈希,存成一张倒排副表。
def ngrams(text: str, n: int = 2):
if len(text) < n:
return [text]
return [text[i:i + n] for i in range(len(text) - n + 1)]
print(ngrams("张三丰")) # ['张三', '三丰']
print(ngrams("13812345678", 3)) # ['138', '381', '812', ...]
# 副表:ngram_index(record_id, gram_hash)
def index_rows(record_id: int, text: str):
return [(record_id, blind(g)) for g in set(ngrams(text))]
def search_infix(keyword: str):
grams = [blind(g) for g in set(ngrams(keyword))]
ph = ",".join(["%s"] * len(grams))
return (
f"SELECT record_id FROM ngram_index WHERE gram_hash IN ({ph}) "
f"GROUP BY record_id HAVING COUNT(DISTINCT gram_hash) = {len(grams)}",
tuple(grams),
)
HAVING COUNT 保证候选记录包含查询词的全部片段,等价于”包含”语义。代价明确:索引存储膨胀,一个 20 字的地址在 2-gram 下要写 19 行;单字查询会退化,因为单字生不出 2-gram,需要额外维护 1-gram 表。
加密布隆过滤器是同一思路的位图变体:把各片段哈希映射进位数组,加密后存为一个字段,查询时比对位模式。它省存储、比对快,但同样有假阳性,且不支持排序与范围查询。
五、保序加密与同态加密的边界
保序加密保证密文字典序与明文一致,因此能直接跑 BETWEEN 和 ORDER BY,并复用原生 B+ 树索引。问题是它把顺序关系原样保留,攻击者靠统计密文分布就能推测明文,属于已知明文攻击与频率分析的高危区,不适合高机密字段,也做不到字符串 LIKE。
全同态加密理论上是终极答案:服务器直接在密文上算 LIKE。现实是性能差着数量级——BGV 方案查 1 万条记录需要数分钟级耗时,CKKS 支持浮点但精度受限,公开建议是仅用于离线分析场景。若要在 FHE 上做字符串匹配,还得把 ASCII 比对改写成多项式运算,工程量集中在 Microsoft SEAL、IBM HElib 这类库的算法层。
对称可搜索加密 SSE 的定位介于两者之间:客户端本地建密文索引与陷门 Token,服务端只做 Token 匹配,不接触明文与密钥。安全性最贴近理论最优,但工业界成熟中间件稀缺,且主流实现只支持关键词精确相等,通配符模糊支持很弱。
六、混合架构的落地步骤
- 统计线上真实查询,按精确 / 前缀 / 中缀 / 全文四类打标,得出各自占比;
- 对精确类字段改用确定性加密(AES-SIV 或固定 IV 派生),直接建唯一索引;
- 对前缀类字段加多级盲索引列,用 ORM 拦截器在写入时自动填充;
- 对中缀与全文需求,把 N-Gram 哈希索引下沉到 Elasticsearch,只存 Token 与主键,不存明文;
- 在 DAO 层加查询改写器,把上层
LIKE语义统一翻译成索引条件,业务代码不感知; - 应用层对索引命中结果解密并做二次精确过滤,消除哈希碰撞导致的假阳性;
- 为盲索引与 N-Gram 索引配置独立派生密钥,纳入与主密钥不同的轮换周期;
- 上线前压测,并对模糊查询接口加返回条数上限与审计日志。
七、别忽略的安全折衷
模糊查询接口本身会泄露统计信息。攻击者反复用不同前缀试探,通过返回结果集大小就能推断姓氏分布、号段分布。可行的缓解手段包括:限制单次返回条数、对高频探测做频次管控、记录所有模糊查询的操作者与结果集规模。
某电商的分段加密实践把模糊查询性能损失控制在 15% 以内并通过等保三级评估,说明手机号、身份证号这类结构规整、可按段拆分的字段最适合分段索引方案;地址、备注这类自由文本,仍应走独立检索服务。
常见问题(FAQ)
Q1:盲索引会不会被暴力破解?
会。手机号空间有限,攻击者拿到索引密钥即可枚举,密钥必须独立托管在 KMS。
Q2:中缀模糊一定要用 N-Gram 吗?
不一定。数据量小可全量解密后内存过滤;量大且频繁则用 N-Gram 倒排更划算。
Q3:Elasticsearch 存 Token 安全吗?
只存哈希 Token 与主键、不存明文时可接受,需另配集群访问控制与传输加密。