同态加密和SEAL库的介绍(七)密文 - 旋转

写在前面:
        前面分别介绍了三种方案,下面单独讲一下比较重要的操作 “循环旋转”,因为在实现一些功能的时候,比如矩阵乘法,就需要旋转操作了。当然,这里单独讲是因为自己踩过坑,并且之前强调的 Batch Encoder 编码的时候,逻辑上分成两行带来的差异,在这里也能体现。
        题目强调是密文(Ciphertext)的旋转,因为这里不对明文(Plaintext)进行旋转,如果要转明文当然最好是转完数组再编码就好了,这样时间代价会少。后面(下一篇会单独介绍性能)比较时间代价的时候也能看出来,旋转本身是很耗时的

一、旋转引入

        在 Microsoft SEAL 库中,BFV 和 BGV 方案 (使用 BatchEncoder ) 以及 CKKS 方案都支持在加密数字上的本地矢量化计算。除了按槽位计算外,还可以循环旋转加密向量。旋转操作是一种重要的同态运算,特别是在处理批量加密数据时非常有用因为可以在不解密的情况下对数据进行重新排列

1.1 旋转的特点

  1. 同态操作:旋转操作是在密文层面上进行的,不需要解密数据,因此保持了数据的加密状态。
  2. 循环移动:旋转可以将密文数组中的元素循环移动,支持左旋转和右旋转。
  3. 批量处理:旋转通常与批量编码(Batching)结合使用,以便同时处理多个数据元素。
  4. 需要伽罗瓦密钥(Galois Keys):执行旋转操作需要伽罗瓦密钥,这些密钥在密钥生成阶段由 KeyGenerator 生成。

1.2 旋转的应用

  1. 矩阵乘法:在同态加密的矩阵乘法中,旋转操作用于对齐矩阵的行和列。
  2. 循环卷积:在同态加密的卷积操作中,旋转用于实现循环卷积,这在图像处理和深度学习中非常常见。
  3. 批处理任务:在批量处理数据时,旋转可以帮助重新排列数据以便进行进一步的同态操作。
  4. 数据对齐:旋转可以用于对齐加密数据,以便进行诸如累加、平均等操作。

1.3 旋转的优缺点

优点

  • 保持数据隐私:由于旋转是在密文层面上操作,数据始终保持加密状态,保证了隐私。
  • 灵活性:旋转操作与批量编码结合,提供了在加密数据上进行复杂操作的能力。
  • 高效性:与其他需要解密再操作的方法相比,旋转操作更高效。

缺点

  • 计算复杂度:旋转操作的计算复杂度较高,特别是在处理大规模数据时。
  • 需要额外的密钥:执行旋转操作需要伽罗瓦密钥,这增加了密钥管理的复杂性。
  • 噪声增长:同态旋转操作可能会增加密文中的噪声,需要噪声管理策略来控制噪声的累积。

二、BFV / BGV 方案的旋转

        在本示例中,用BFV进行演示,当然只需将 scheme_type::bfv 更改为 scheme_type::bgv  即可适用于 BGV 方案。

2.1 参数配置

这里就不冗余介绍了,直接用跟前面相似的配置:

EncryptionParameters parms(scheme_type::bfv);

size_t poly_modulus_degree = 8192;
parms.set_poly_modulus_degree(poly_modulus_degree);
parms.set_coeff_modulus(CoeffModulus::BFVDefault(poly_modulus_degree));
parms.set_plain_modulus(PlainModulus::Batching(poly_modulus_degree, 20));

SEALContext context(parms);

KeyGenerator keygen(context);
SecretKey secret_key = keygen.secret_key();
PublicKey public_key;
keygen.create_public_key(public_key);
RelinKeys relin_keys;
keygen.create_relin_keys(relin_keys);
Encryptor encryptor(context, public_key);
Evaluator evaluator(context);
Decryptor decryptor(context, secret_key);

BatchEncoder batch_encoder(context);
size_t slot_count = batch_encoder.slot_count();
size_t row_size = slot_count / 2;

        这里 coeff_modulus 是通过默认的 BFVDefault 生成的。补充一下,要求是几位的和小于等于218位(可以通过 CoeffModulus::MaxBitCount 获得,在第二篇有表格介绍);
        这里的 plain_modulus 是由 Batching 生成的,强调下,Batch Encoder 要求其必须是与 2 * poly_modulus_degree 同余1的素数,自己指定的时候要额外注意下。

2.2 输入的编码与加密

旋转是针对逻辑上的两行,这里为了展示,输入直接放在每行的前面,方便大家观察:

vector<uint64_t> pod_matrix(slot_count, 0ULL);
pod_matrix[0] = 0ULL;
pod_matrix[1] = 1ULL;
pod_matrix[2] = 2ULL;
pod_matrix[3] = 3ULL;
pod_matrix[row_size] = 4ULL;
pod_matrix[row_size + 1] = 5ULL;
pod_matrix[row_size + 2] = 6ULL;
pod_matrix[row_size + 3] = 7ULL;

输出展示如下:


继续对输入进行编码和加密:

Plaintext plain_matrix;
batch_encoder.encode(pod_matrix, plain_matrix);
Ciphertext encrypted_matrix;
encryptor.encrypt(plain_matrix, encrypted_matrix);

2.3 密文的旋转

旋转需要另一种特殊密钥,称为 `Galois 密钥`,密钥可以直接地从 KeyGenerator 获得。

GaloisKeys galois_keys;
keygen.create_galois_keys(galois_keys);

        先将矩阵的两行向左旋转 3 步(这里注意,虽然一块能编码 poly_modulus_degree 个数,但是根据逻辑上存储为两行的矩阵,行旋转是同时针对两行一起的!

evaluator.rotate_rows_inplace(encrypted_matrix, 3, galois_keys);
Plaintext plain_result;
cout << "+ Noise budget after rotation: " << decryptor.invariant_noise_budget(encrypted_matrix) << " bits"  << endl;
decryptor.decrypt(encrypted_matrix, plain_result);
batch_encoder.decode(plain_result, pod_matrix);

解密解码后,输出如下:

可以看出,的确是循环左移,并且是两行一起旋转的


下面展示旋转列,即交换行。

evaluator.rotate_columns_inplace(encrypted_matrix, galois_keys);
cout << "+ Noise budget after rotation: " << decryptor.invariant_noise_budget(encrypted_matrix) << " bits" << endl;
decryptor.decrypt(encrypted_matrix, plain_result);
batch_encoder.decode(plain_result, pod_matrix);

解密解码输出如下:

        可以看出,上下交换,即 Ciphertext 中(共 poly_modulus_degree 个)前半部分和后半部分的数互换


最后,将行向右旋转 4 步:

evaluator.rotate_rows_inplace(encrypted_matrix, -4, galois_keys);
cout << "+ Noise budget after rotation: " << decryptor.invariant_noise_budget(encrypted_matrix) << " bits" << endl;
decryptor.decrypt(encrypted_matrix, plain_result);
batch_encoder.decode(plain_result, pod_matrix);

解密解码输出如下:

        这里可以注意到,向左和向右旋转的函数都是 evaluator.rotate_rows(),区别在于第二个指定步数的参数,正数向左,负数向右。并且该参数有上限(矩阵的列数,即一半的 poly_modulus_degree )。

2.4 特别补充

        上面特别打印了噪声预算,可以发现:旋转不会消耗任何噪声预算。然而,这仅在特殊素数大于等于其他素数大小时才成立,对重新线性化也同样适用。
        SEAL 并不要求特殊素数具有任何特定的大小, 因此确保这一点的责任由用户来承担。


三、CKKS 方案的旋转

        在 CKKS 方案中的旋转操作与在 BFV 中的旋转操作非常相似,但是由于Encoder不同,故这里可以看成直接对数组的旋转。(只不过可用的长度是 poly_modulus_degree 的一半

3.1 参数配置

EncryptionParameters parms(scheme_type::ckks);

size_t poly_modulus_degree = 8192;
parms.set_poly_modulus_degree(poly_modulus_degree);
parms.set_coeff_modulus(CoeffModulus::Create(poly_modulus_degree, { 40, 40, 40, 40, 40 }));

SEALContext context(parms);

KeyGenerator keygen(context);
SecretKey secret_key = keygen.secret_key();
PublicKey public_key;
keygen.create_public_key(public_key);
RelinKeys relin_keys;
keygen.create_relin_keys(relin_keys);
GaloisKeys galois_keys;
keygen.create_galois_keys(galois_keys);
Encryptor encryptor(context, public_key);
Evaluator evaluator(context);
Decryptor decryptor(context, secret_key);

CKKSEncoder ckks_encoder(context);
size_t slot_count = ckks_encoder.slot_count();

这里再强调下,CKKS方案没有 plain_modulus编码器是 CKKSEncoder ,输出参数:

        CKKS这里的 循环旋转单位和可用槽的大小都是 poly_modulus_degree 的一半,而 Batch Encoder 那里 循环旋转单位是一行(poly_modulus_degree 的一半),但是整个槽都可用(poly_modulus_degree),注意区分。


3.2 输入的编码与加密

 这里配置输入,跟前面CKKS方案示例那里一样。

vector<double> input;
input.reserve(slot_count);
double curr_point = 0;
double step_size = 1.0 / (static_cast<double>(slot_count) - 1);
for (size_t i = 0; i < slot_count; i++, curr_point += step_size)
{
    input.push_back(curr_point);
}

输入如下:


在对输入进行编码和加密:

auto scale = pow(2.0, 50);

Plaintext plain;
ckks_encoder.encode(input, scale, plain);
Ciphertext encrypted;
encryptor.encrypt(plain, encrypted);

这里需要注意,CKKSEncoder 的 encode 函数需要传入 scale。

3.3 密文的旋转

这里演示向左转两位:

Ciphertext rotated;
evaluator.rotate_vector(encrypted, 2, galois_keys, rotated);
decryptor.decrypt(rotated, plain);
vector<double> result;
ckks_encoder.decode(plain, result);

        注意CKKS方案和上面旋转的函数不同,是 rotate_vector 注意区分。同时,这里为了区分上面的原地旋转,所以生成了一个 Ciphertext 来接收旋转结果,当然也有 rotate_vector_inplace 供选择。

 3.4 补充

        在 CKKS 方案中,还可以对加密复数向量进行复共轭运算,使用Evaluator::complex_conjugate。这实际上是一种旋转操作,也需要使用 Galois 密钥。


四、总结

        旋转操作是比较重要也很常见的操作,所以示例中单独拿出来讲。当然其实在很多同态方案中,旋转操作是时间成本的大头,故针对其进行雕琢很有必要。
        大家也需要关注下旋转时间成本,因为密文的内部构造是类似于链表的结构,所以往左转和往右转的时间代价会不同,故需要细颗粒度的实验来优化性能。

        下篇会比较不同方案的不同操作的性能差距。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr.Ants

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值