← 返回

格式保留加密、同态加密与零知识证明

作者:林 | 系列:密码学 | 适合读者:后端开发 / 对前沿密码学感兴趣的工程师


一、前言

前六篇覆盖了工程中更常用的密码学技术。这篇聊几个"高级货",它们目前在特定场景已有落地,未来几年会越来越重要:

技术一句话典型场景
格式保留加密(FPE)加密后格式不变数据脱敏、存量系统改造
同态加密(HE/FHE)密文上直接计算隐私计算、联邦学习
零知识证明(ZKP)证明我知道但不告诉你区块链隐私、身份认证
后量子密码(PQC)抗量子计算机攻击未来标准迁移

二、格式保留加密(FPE)

2.1 什么是 FPE

FPE(Format-Preserving Encryption):加密后的密文与明文格式完全相同

普通 AES 加密:
  "13800001111" → "a3Kf8bQ2xR7m9p..." (Base64 乱码,长度也变了)

FPE 加密:
  "13800001111" → "15927463850"       (还是 11 位数字)
  "张三"        → "李七"              (还是 2 个汉字)
  "320106199001011234" → "410523198712255678" (还是 18 位身份证号格式)

2.2 为什么需要 FPE

存量系统改造时,数据库字段类型、长度、校验规则都不能改。用 AES 加密后字段变成了长 Base64 字符串,需要改表结构、改校验逻辑、改前端展示,改动太大。

FPE 让加密对上下游透明:字段类型不变,长度不变,甚至格式校验(如身份证校验位)都能保留。

2.3 核心算法:FF1 和 FF3-1

NIST SP 800-38G 标准定义了两种 FPE 算法:

算法全称特点
FF1Format-preserving, Feistel-based encryption 110 轮 Feistel,支持 tweak
FF3-1FF3 修订版8 轮 Feistel,tweak 缩短为 56 bit
FF1 结构(简化):

输入:明文 X(radix 进制表示),密钥 K,Tweak T
  1. 将 X 拆成左右两半:A, B
  2. 10 轮 Feistel 迭代:
     for i = 0 to 9:
       C = A + F(K, T, i, B) mod radix^m
       A = B
       B = C
  3. 输出密文 = A || B

其中 F 是基于 AES 的伪随机函数
radix = 基数(数字=10,字母=26,etc.)

2.4 Java 示例(BouncyCastle)

import org.bouncycastle.crypto.fpe.FPEFF1Engine;
import org.bouncycastle.crypto.fpe.FPEEngine;
import org.bouncycastle.crypto.params.FPEParameters;
import org.bouncycastle.crypto.params.KeyParameter;

/**
 * FF1 格式保留加密示例
 * 对数字字符串加密,结果仍是相同长度的数字字符串
 */
public class FpeExample {

    private static final int RADIX = 10; // 数字:0-9

    /**
     * FPE 加密数字字符串
     * @param plaintext 明文数字字符串(如 "13800001111")
     * @param key AES 密钥(16/24/32 字节)
     * @param tweak 调柄(可用于区分不同字段)
     * @return 密文数字字符串(格式相同)
     */
    public static String encrypt(String plaintext, byte[] key, byte[] tweak) {
        FPEEngine engine = new FPEFF1Engine();
        engine.init(true, new FPEParameters(
            new KeyParameter(key), RADIX, tweak));

        // 将数字字符串转为 short 数组
        short[] input = toShortArray(plaintext);
        short[] output = new short[input.length];

        engine.processBlock(input, 0, input.length, output, 0);

        return fromShortArray(output);
    }

    /**
     * FPE 解密
     */
    public static String decrypt(String ciphertext, byte[] key, byte[] tweak) {
        FPEEngine engine = new FPEFF1Engine();
        engine.init(false, new FPEParameters(
            new KeyParameter(key), RADIX, tweak));

        short[] input = toShortArray(ciphertext);
        short[] output = new short[input.length];

        engine.processBlock(input, 0, input.length, output, 0);

        return fromShortArray(output);
    }

    private static short[] toShortArray(String digits) {
        short[] arr = new short[digits.length()];
        for (int i = 0; i < digits.length(); i++) {
            arr[i] = (short) (digits.charAt(i) - '0');
        }
        return arr;
    }

    private static String fromShortArray(short[] arr) {
        StringBuilder sb = new StringBuilder(arr.length);
        for (short s : arr) {
            sb.append((char) ('0' + s));
        }
        return sb.toString();
    }
}
// 使用示例
byte[] key = "0123456789abcdef".getBytes(); // 16 字节 AES 密钥
byte[] tweak = "phone".getBytes();          // tweak 区分字段

String phone = "13800001111";
String encrypted = FpeExample.encrypt(phone, key, tweak);
// encrypted = "15927463850" (还是 11 位数字)

String decrypted = FpeExample.decrypt(encrypted, key, tweak);
// decrypted = "13800001111" 正确还原

2.5 FPE 的局限

限制说明
不支持模糊搜索“张三” 加密后第一个字和 “张” 单独加密结果不同
确定性加密同密钥 + 同 tweak 下,相同明文→相同密文(泄露相等关系)
安全性弱于 AES-GCM格式保留本身就是在安全性上的妥协
短输入安全性差输入越短(如 2 位数字),安全性越低

适用场景:数据脱敏(测试环境)、存量系统改造(改不动表结构)、合规展示(只显示部分,但存储全加密)。


三、同态加密(Homomorphic Encryption)

3.1 什么是同态加密

普通加密:先解密,再计算,再加密回去,计算方必须看到明文。

同态加密:直接在密文上计算,结果解密后等于对明文计算的结果。计算方全程看不到明文。

普通流程:
  密文 → 解密 → 明文 → 计算 → 结果 → 加密 → 密文结果
  (计算方看到了明文 ❌)

同态加密:
  密文 → 直接在密文上计算 → 密文结果 → 解密 → 明文结果
  (计算方全程只看到密文 ✅)

数学表达:
  Enc(a) ⊕ Enc(b) = Enc(a + b)    -- 加法同态
  Enc(a) ⊗ Enc(b) = Enc(a × b)    -- 乘法同态

3.2 同态加密的分类

类型支持的运算性能代表方案
部分同态(PHE)仅加法或仅乘法快(ms级)Paillier(加法)、RSA(乘法)
有限同态(SHE)有限次加法和乘法中等BGV、BFV
全同态(FHE)任意次加法和乘法极慢(万倍开销)CKKS、TFHE

3.3 Paillier 加法同态示例

Paillier 是更实用的部分同态方案,支持密文加法:

场景:多方工资求和,任何一方都不想暴露自己的工资

Alice 工资:10000 → Enc(10000)
Bob 工资:  15000 → Enc(15000)
Carol 工资:12000 → Enc(12000)

服务端计算(全程不解密):
  Enc(10000) ⊕ Enc(15000) ⊕ Enc(12000) = Enc(37000)

只有拥有私钥的一方能解密得到总和 37000
没有人看到其他人的工资
/**
 * Paillier 同态加密示意
 * 生产环境推荐使用 java-paillier 库或 SEAL/OpenFHE
 */
public class PaillierDemo {

    // Paillier 密钥生成
    public static PaillierKeyPair generateKeyPair(int bitLength) {
        BigInteger p = BigInteger.probablePrime(bitLength / 2, new SecureRandom());
        BigInteger q = BigInteger.probablePrime(bitLength / 2, new SecureRandom());
        BigInteger n = p.multiply(q);
        BigInteger nSquared = n.multiply(n);
        BigInteger lambda = lcm(p.subtract(ONE), q.subtract(ONE));
        BigInteger g = n.add(ONE); // 简化选择 g = n+1
        BigInteger mu = lambda.modInverse(n); // L(g^lambda mod n^2)^-1 mod n

        return new PaillierKeyPair(
            new PaillierPublicKey(n, g),
            new PaillierPrivateKey(lambda, mu, n));
    }

    // 加密:Enc(m) = g^m * r^n mod n^2
    public static BigInteger encrypt(BigInteger m, PaillierPublicKey pub) {
        BigInteger r = randomCoprime(pub.n);
        BigInteger nSquared = pub.n.multiply(pub.n);
        return pub.g.modPow(m, nSquared)
            .multiply(r.modPow(pub.n, nSquared))
            .mod(nSquared);
    }

    // 同态加法:Enc(a) * Enc(b) mod n^2 = Enc(a+b)
    public static BigInteger add(BigInteger encA, BigInteger encB,
                                 PaillierPublicKey pub) {
        BigInteger nSquared = pub.n.multiply(pub.n);
        return encA.multiply(encB).mod(nSquared);
    }

    // 密文标量乘:Enc(a)^k mod n^2 = Enc(a*k)
    public static BigInteger scalarMultiply(BigInteger encA, BigInteger k,
                                            PaillierPublicKey pub) {
        BigInteger nSquared = pub.n.multiply(pub.n);
        return encA.modPow(k, nSquared);
    }

    // 解密:Dec(c) = L(c^lambda mod n^2) * mu mod n
    public static BigInteger decrypt(BigInteger c, PaillierPrivateKey priv) {
        BigInteger nSquared = priv.n.multiply(priv.n);
        BigInteger x = c.modPow(priv.lambda, nSquared);
        BigInteger l = x.subtract(ONE).divide(priv.n); // L函数
        return l.multiply(priv.mu).mod(priv.n);
    }
}

3.4 全同态加密(FHE)的现状

FHE 的性能开销(2024 年水平):

操作          | 明文耗时  | FHE 密文耗时  | 倍率
加法          | ~1 ns    | ~0.1 ms      | 100,000×
乘法          | ~1 ns    | ~10 ms       | 10,000,000×
比较/排序     | ~5 ns    | ~100 ms      | 20,000,000×
密文大小      | 4 bytes  | ~32 KB       | 8,000×

FHE 目前的实际应用:

  • 隐私保护的机器学习推理(模型在密文上运行)
  • 基因组数据分析(医院间共享加密数据)
  • 金融风控联合建模
FHE 离普及还有多远
FHE 的性能在过去 10 年提升了约 10 万倍(从秒级单次加法到微秒级),但距离通用场景仍有 4-5 个数量级的差距。目前只适合计算量小、隐私要求极高的场景。预计 5-10 年内在特定领域(医疗、金融)会有更多落地。

四、零知识证明(ZKP)

4.1 什么是零知识证明

零知识证明:证明者向验证者证明某个陈述为真,但不泄露除"陈述为真"之外的任何信息。

经典比喻——阿里巴巴洞穴:

  洞穴有两条路(A 和 B),中间有一扇密码门连通。
  证明者知道密码,验证者不知道。

  1. 证明者随机选一条路进去(验证者看不到选了哪条)
  2. 验证者喊:"从 A 路出来!"(或 B)
  3. 如果证明者知道密码 → 无论要求哪条路都能出来
     如果证明者不知道密码 → 50% 概率出不来

  重复 20 次,每次都能正确出来 → 99.9999% 证明者知道密码
  但验证者全程没学到密码本身

4.2 ZKP 的三个性质

性质含义
完备性如果陈述为真,诚实的证明者总能说服验证者
可靠性如果陈述为假,作弊的证明者无法说服验证者(概率可忽略)
零知识性验证者除了"陈述为真"之外学不到任何额外信息

4.3 实际应用

场景做法
区块链隐私交易证明"我有足够余额"但不暴露余额(Zcash)
身份认证证明"我年满 18 岁"但不暴露出生日期
密码验证证明"我知道密码"但不传输密码
合规审计证明"资产 > 负债"但不暴露具体数字
zkRollup证明"1000 笔交易都合法",链上只验证一个证明

4.4 主流 ZKP 方案对比

方案类型证明大小验证时间是否需要可信设置
Groth16zk-SNARK~200 bytes~ms
PLONKzk-SNARK~500 bytes~ms✅(通用)
STARKzk-STARK~50 KB~ms
Bulletproofs~1 KB~100ms
zk-SNARK:证明小、验证快,但需要可信设置(有人要生成参数然后销毁)
zk-STARK:无需可信设置、抗量子,但证明较大

4.5 简单示例:证明知道哈希原像

场景:我知道一个 x 使得 SHA256(x) = H,但我不想告诉你 x 是什么

传统方式:
  把 x 给你,你自己算 SHA256(x) 看是不是等于 H
  → x 暴露了 ❌

零知识方式(简化思路):
  1. 将 SHA256 电路化(转为算术电路)
  2. 我用 x 作为秘密输入,生成一个证明 π
  3. 你验证 π 对于公开输入 H 是否成立
  4. 验证通过 → 你确信我知道 x,但你不知道 x 是什么
# 概念示例(使用 snarkjs 风格的伪代码)
# 实际 ZKP 开发通常用 Circom/Rust/Go 框架

# 电路定义(Circom 风格)
"""
template HashPreimage() {
    signal private input preimage;  // 秘密输入
    signal input hash;              // 公开输入

    // 约束:SHA256(preimage) == hash
    component hasher = SHA256();
    hasher.in <== preimage;
    hash === hasher.out;
}
"""

# 证明者(知道 preimage)
proof = generate_proof(circuit, private_input=preimage, public_input=hash)

# 验证者(不知道 preimage)
is_valid = verify_proof(proof, public_input=hash)  # true/false

五、后量子密码(PQC)

5.1 量子计算机的威胁

Shor 算法(1994):
  量子计算机上可以在多项式时间内分解大整数、求解离散对数

影响:
  RSA    → 被破解(基于大整数分解)
  ECDSA  → 被破解(基于椭圆曲线离散对数)
  SM2    → 被破解(同上)
  AES    → 安全性减半(Grover 算法,AES-256 → 128 bit 安全)
  SHA    → 安全性减半(同上)

结论:
  非对称加密全军覆没,对称加密和哈希加倍密钥长度即可

5.2 NIST 后量子标准(2024 年发布)

算法用途基于的数学难题标准编号
ML-KEM (Kyber)密钥封装格上模块学习问题(M-LWE)FIPS 203
ML-DSA (Dilithium)数字签名格上模块学习问题FIPS 204
SLH-DSA (SPHINCS+)数字签名哈希函数安全性FIPS 205

5.3 后量子 vs 经典算法

指标ECDSA P-256ML-DSA-65 (Dilithium)倍率
公钥大小64 bytes1,952 bytes30×
签名大小64 bytes3,293 bytes51×
密钥生成~0.05 ms~0.1 ms
签名速度~0.1 ms~0.3 ms
验签速度~0.3 ms~0.3 ms

后量子算法的主要代价是密钥和签名变大,计算速度差距不大。

5.4 迁移建议

当前(2024-2026):
  - 新系统可以开始支持"混合模式"(经典 + 后量子双算法)
  - TLS 已支持混合密钥交换(X25519 + ML-KEM)
  - 不需要急着全面切换

中期(2027-2030):
  - NIST 标准成熟,库和硬件支持完善
  - 高安全需求系统开始迁移

长期(2030+):
  - 大规模量子计算机可能出现
  - 全面迁移到后量子算法
Harvest Now, Decrypt Later
攻击者可以现在录制加密流量,等未来量子计算机成熟后再解密。如果你的数据需要保密 10 年以上(政府、军事、医疗),现在就应该开始使用后量子算法或混合方案。

六、技术对比总结

技术成熟度性能落地程度适合谁关注
FPE成熟已广泛使用做数据脱敏/存量改造的
部分同态(Paillier)成熟联邦学习已用做隐私计算的
全同态(FHE)研究→工程少量落地研究方向/未来
ZKP(SNARK/STARK)工程化中区块链广泛使用做区块链/隐私的
后量子密码标准刚发布开始试点所有人(长期)

七、总结

对于日常工程开发:

  1. FPE 是更实用的"高级"密码学技术,数据脱敏、测试环境、存量改造都能用
  2. 同态加密 目前只需了解概念,除非你做隐私计算/联邦学习方向
  3. 零知识证明 在区块链领域已经是核心基础设施,其他领域还在早期
  4. 后量子密码 所有人都需要关注,但不需要现在行动(除非数据需保密 10 年+)

密码学的发展方向:从"保护数据不被看到"到"数据可用但不可见",在不暴露原始数据的前提下完成计算和验证,这是隐私计算的核心命题。


八、系列回顾

  1. 密码学基础:Hash、对称加密、非对称加密
  2. 散列函数:MD5、SHA、SM3 与 HMAC
  3. 对称加密:AES 与 SM4 模式选择指南
  4. 非对称加密与签名:RSA、ECC、SM2
  5. 数字信封与密钥协商:SM2+SM4、ECDH、TLS
  6. 可搜索加密:加密数据的模糊检索方案
  7. 格式保留加密、同态加密与零知识证明(本文)

上一篇可搜索加密:加密数据的模糊检索方案