AWS 数据库加密 SDK 的信标:截断 HMAC 做等值查询,误报、分区与扇出怎么权衡
文档:
信标的定义
可搜索加密让数据库不必解密整库就能按字段值检索加密记录。机制叫信标(beacon):写入字段时对明文值算一次 HMAC,截断后作为一个新字段,和密文一起存进同一条记录。查信标就能命中对应记录,SDK 在返回结果前解密并过滤掉误报。信标不改变字段的加密状态,明文值依旧不会交给数据库,库里存的是随机密文。
- 信标是截断的 HMAC 标签,未截断时与明文值 1:1 对应。
- 截断是刻意为之:不同明文碰撞到同一信标,产生误报,削弱攻击者从频率模式推断信息的能力。
- 分区(partition)让同一明文在不同分区生成不同信标;只用一个分区时,行为等价于传统的非分区信标。
SDK 里的可搜索加密与学术研究中定义的可搜索对称加密(SSE)不是同一套东西。
误报量级怎么估
均匀分布假设下,单分区内的期望误报数大致为:
E[误报] ≈ (N_p - 1) / 2^b
N_p 是该分区内字段的唯一值数,b 是截断后保留的位数。按分区计数时把总量除以分区数再代入。直观例子:保留 16 位、单分区 6.5 万个唯一值,查一个值平均会多带回 1 条误报;保留 32 位,同样的数据量误报降到万分之一量级。文档给出的结论与此一致——每个信标的平均误报数由截断后剩余长度和分区数共同决定。注意这只是均值估计,分布越不均匀,这个估计越不可靠。
截断长度与分区数的取舍
- 短信标、多分区:碰撞更多、频率泄漏更少,安全性更高;代价是误报变多、查询要按分区扇出。
- 长信标、少分区:误报少、扇出小,查询效率高;代价是暴露更多底层分布信息。
所有信标配置之前,先过一遍威胁模型、安全要求和性能需求,再看数据集本身的特征。可搜索加密未必对每个数据集都能同时满足安全和性能。
存量数据无法回填
信标只映射配置之后写入的新记录。在已有数据库上配置信标,旧数据不会被覆盖,必须先重新写入。可搜索加密应当规划在未填充的新库上。
偏斜分布与相关字段
偏斜字段要格外小心。以存储伊利诺伊州居民城市的库为例,City 字段上建信标后,“Chicago” 的信标会明显过量出现,攻击者仅凭密文和信标就能定位芝加哥居民对应的记录。截断能靠碰撞压制这种泄漏,但要把严重偏斜藏住,信标就得截得很短,误报和性能开销随之上升。
极端情况更明显:人口极少的属性,或高度不平衡的二值结果(比如以阴性为主的医学检查),单分区下短信标会把所有值塌成一个标签,长信标又能直接识别稀有值。这时只能靠分区把过量表示的值摊到多个分区里,缩小等价类。
相关字段不要各建一个信标。City 和 ZIPCode 高度关联,攻击者容易判定哪些结果是误报,从而反推出邮编;mobilePhone 与 preferredPhone 可能存同一个号码,两个字段用不同密钥派生,同一明文会得到两个不同的 HMAC 标签,误报不重合,攻击者反而能区分出电话号码。这类字段需要更短的截断长度,并且要分析熵和联合分布后才能定,若算出的长度满足不了性能要求,信标就不适合这份数据。
先看频率分布
配置信标前先量化数据。以 PostgreSQL 为例:
-- 高频值 Top 20
SELECT city, COUNT(*) AS n
FROM employees
GROUP BY city
ORDER BY n DESC
LIMIT 20;
-- 唯一值数与偏斜度:max/avg 越大越偏
WITH f AS (
SELECT lastname, COUNT(*) AS n
FROM employees
GROUP BY lastname
)
SELECT COUNT(*) AS distinct_values,
MAX(n) AS max_n,
ROUND(AVG(n), 2) AS avg_n,
ROUND(MAX(n)::numeric / AVG(n), 2) AS max_over_avg
FROM f;
distinct_values 代进上面的公式估误报,max_over_avg 判断是否需要用多分区压制高频值。
Go 示意代码
以下代码是示意,不是 AWS Database Encryption SDK 的官方 API,只演示 HMAC-SHA256 截断、分区号参与派生、查询按分区扇出这三件事。
const (
BeaconBytes = 4 // 保留 32 位,按误报预算调整
Partitions = 16 // 分区数,写入时与记录绑定
)
// 按字段派生独立信标密钥,避免同值在不同字段撞出同一标签
func DeriveKey(masterKey []byte, field string) []byte {
m := hmac.New(sha256.New, masterKey)
m.Write([]byte("beacon-key:" + field))
return m.Sum(nil)
}
// 写入与查询共用:分区号先进 MAC,再写明文
func Compute(key []byte, partition int, plaintext string) string {
m := hmac.New(sha256.New, key)
m.Write([]byte(strconv.Itoa(partition)))
m.Write([]byte{0})
m.Write([]byte(plaintext))
return hex.EncodeToString(m.Sum(nil)[:BeaconBytes])
}
// 查询:每个分区各发一次等值查询,合并后解密并过滤误报
func Lookup(ctx context.Context, db *sql.DB, key []byte, plaintext string) ([]string, error) {
var hits []string
for p := 0; p 。