CKKS与BFV算法的图片同态加密、密态检索与同态解密
本文基于TenSEAL库实现,是《数据安全与隐私保护》课程Project,由于是课程作业性质,这里的重点放在了运行效率的比较,没有将server端和client段做严谨区分,context的公私钥分离也略显潦草。本文的源码可以在这里找到,仅供参考,若想深入学习请查看官方文档。
如果不了解同态加密,以下是一些供参考的文章:
这是同态加密实现的经典逻辑框图:
以下是正文:
介绍
全同态加密(Fully Homomorphic Encryption,FHE)是一种密码学技术,允许在加密状态下执行计算操作,而无需解密数据。本实验选用两种常见的全同态加密方案,即CKKS(Cheon-Kim-Kim-Song)和BFV(Brakerski-Vaikuntanathan-Gentry)。
CKKS(Cheon-Kim-Kim-Song)
CKKS 是一种基于环的全同态加密方案。环是一种数学结构,允许在加密域上进行多项式运算。CKKS 方案主要用于处理实数和复数,因此在机器学习和数据分析等领域中得到广泛应用。
主要特点:
- 支持加法和乘法运算。
- 适用于实数和复数的加密计算。
- 允许动态调整安全参数以平衡安全性和性能。
BFV(Brakerski-Vaikuntanathan-Gentry)
BFV 是一种基于多项式环的全同态加密方案。它是 Gentry 的经典全同态加密方案的改进版本,旨在提高效率和应用范围。
主要特点:
- 支持加法、乘法和混合运算(同时进行加法和乘法)。
- 允许在多项式环上进行计算。
- 提供更高的效率和灵活性,适用于更广泛的应用场景。
TenSEAL
TenSEAL是一个基于 Microsoft Research 的 Simple Encrypted Arithmetic Library(SEAL)的同态加密库。它是专为支持深度学习任务中的同态加密操作而设计的,与 PySyft 框架集成,以支持联邦学习和安全多方计算。
主要特点和功能:
- SEAL 基础: TenSEAL建立在 Microsoft Research 提供的 SEAL 库之上,该库专注于提供同态加密的基础原语。这使得 TenSEAL 可以充分利用 SEAL 的强大功能,同时为用户提供更高层次的接口。
- 同态加密支持: TenSEAL 提供了同态加密的功能,使得用户可以在加密状态下执行计算,而无需解密敏感数据。
- PySyft 集成: TenSEAL 与 PySyft 框架集成,支持联邦学习和安全多方计算。这使得 TenSEAL 能够与 PySyft 中的其他工具和库协同工作,实现更广泛的隐私保护应用。
- CKKS 和 BFV 算法: TenSEAL 实现了两种常见的同态加密算法,即 CKKS 和 BFV。这两种算法分别适用于不同的使用场景,CKKS 用于处理实数和复数,而 BFV 用于通用的多项式环上的计算。
实验内容
从开源的库中选择一个开源库验证比较不同全同态加密算法的效率。
- 场景设置:
1)选取M张64*64(或更大的图片)加密存储于数据库 or 文件中;
2) 对于一张查询图片,在密态下跟数据库 or文件中加密图像进行匹配。 - 要求:
1) report实验环境(如,操作系统,CPU型号,内存等)
2) report同一开源库中不同FHE算法的计算效率(多测几次求平均)
3) 总结分析所测试算法的优劣
实验环境
操作系统 | CPU型号 | 内存 |
---|---|---|
Linux Ubuntu 22.04.3 | AMD Ryzen 7 5800H(16 CPUs) | 16Gb |
本实验的图片数据集使用CelebA数据集,选取前100张做加密测试。
实验流程与代码实现
本实验选用基于python的TenSEAL库,实现了CKKS和BFV两种全同态加密算法对批量图片文件的加密、解密、存储和密态检索,同时本实验进行多次测试,比较了同一同态加密库不同算法的加密、解密和检索效率,本部分主要汇报实验和代码实现流程,因为两算法实现流程类似,因此以CKKS为例子进行详细分析。
主函数
本代码需包含以下库:
import tenseal as ts
import sqlite3
from PIL import Image
import numpy as np
import os
from time import time
本代码主函数如下:
if __name__ == "__main__":
# 定义 CKKS context,并分离公私钥
context = ts.context(
ts.SCHEME_TYPE.CKKS, poly_modulus_degree=8192, coeff_mod_bit_sizes=[60, 40, 40, 60]
)
context.generate_galois_keys()
context.global_scale = 2**40 # 设置全局缩放因子
ckks_secret_key = context.secret_key()
context.make_context_public()
# 创建或连接到 SQLite 数据库
conn = sqlite3.connect("ckks_encrypted_images.db")
cursor = conn.cursor()
# 创建表以存储加密向量
cursor.execute('''
CREATE TABLE IF NOT EXISTS EncryptedImages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
encrypted_red BLOB,
encrypted_green BLOB,
encrypted_blue BLOB
)
''')
t_start_add = time()
# 加密并将每个图像存储到数据库中
restored_image()
t_end_add = time()
print("ckks同态加密用时: {} ms".format((t_end_add - t_start_add) * 1000))
# 提交更改并关闭连接
conn.commit()
conn.close()
print("加密存储完成")
# 读取要查询的图片并加密
query_image_path = "./data/5.jpg"
encrypted_query_red, encrypted_query_green, encrypted_query_blue = encrypt_image(query_image_path, context, ts.SCHEME_TYPE.CKKS)
# 连接到 SQLite 数据库
conn = sqlite3.connect("ckks_encrypted_images.db")
cursor = conn.cursor()
# 查询数据库以获取加密向量
cursor.execute("SELECT encrypted_red, encrypted_green, encrypted_blue FROM EncryptedImages")
rows = cursor.fetchall()
# 计算余弦相似度并排序
similarities = []
for i, row in enumerate(rows, start=1):
# 反序列化数据库中的密文向量
serialized_red, serialized_green, serialized_blue = row
encrypted_red = ts.ckks_vector_from(context, serialized_red)
encrypted_green = ts.ckks_vector_from(context, serialized_green)
encrypted_blue = ts.ckks_vector_from(context, serialized_blue)
# 计算余弦相似度
similarity_red = calculate_cosine_similarity(encrypted_query_red, encrypted_red)
similarity_green = calculate_cosine_similarity(encrypted_query_green, encrypted_green)
similarity_blue = calculate_cosine_similarity(encrypted_query_blue, encrypted_blue)
# 将三个通道的相似度取平均作为整体相似度
average_similarity = (similarity_red + similarity_green + similarity_blue) / 3
similarities.append((i, average_similarity))
print(f"计算第{i}张图片的相似度")
# 对相似度进行排序
print("相似度排序中...")
similarities.sort(key=lambda x: abs(x[1] - 1))
# 输出相似度最接近1的图像编号和相似度值
print(similarities)
closest_result = similarities[0]
print(f"最相似的图片为: {closest_result[0]}, 二者余弦相似度为: {closest_result[1]}")
# 关闭数据库连接
conn.close()
# 解密并还原图像
decrypt_all_image()
主函数的每一个关键步骤均在代码中以注释说明,其流程可总结如下:
- 定义 CKKS context,并分离公私钥;
- 创建并连接到 SQLite 数据库,创建表;
- 将图片数据进行同态加密,并存储到数据库中;
- 读取要查询的图片;
- 从数据库中获取加密数据,反序列化,并与查询图片计算余弦相似度,选取相似度最高的图片,输出;
- 将数据库中的所有加密数据全部解密,并还原为jpg图片。
context是TenSEAL库实现同态加密的核心参数,在加密前要根据加密算法对context进行相应初始化,同时需要生成galois_keys、global_scale等特定参数。在默认生成的context中是同时包含公钥和私钥的,为了代码在现实中有使用价值,需要对context进行公私钥分离,使用分离了私钥只含有公钥的context加密,而使用单独保管的私钥进行解密和密文检索等工作。
本实验使用轻量级的SQLite数据库来存储加密后的密文数据,为将CKKSVector或BFVVector对象存入数据库,需要调用该类的序列化方法,相应地要对数据库中的数据进行解密或密文检索,需要使用反序列化方法后再进行后续操作。
图像加密存储
本实验的图像加密存储分为图像加密和图像存储两部分,在代码实现中也分为restored_image()和encrypt_image两个函数,restored_image函数中会调用encrypt_image函数,restored_image函数实现如下:
def restored_image():
# 加密并将每个图像存储到数据库中
for i in range(1, 101): # 读取图片
image_path = f"./data/{i}.jpg"
values_red, values_green, values_blue = encrypt_image(image_path, context, ts.SCHEME_TYPE.CKKS)
# 将加密向量的值序列化
serialized_red = values_red.serialize()
serialized_green = values_green.serialize()
serialized_blue = values_blue.serialize()
# 将序列化的加密向量插入到数据库中
cursor.execute('''
INSERT OR REPLACE INTO EncryptedImages (id, encrypted_red, encrypted_green, encrypted_blue)
VALUES (?, ?, ?, ?)
''', (i, sqlite3.Binary(serialized_red), sqlite3.Binary(serialized_green), sqlite3.Binary(serialized_blue)))
print(f"加密存储第{i}张图片")
这一函数首先从./data文件夹中读取100张图片(本实验中图片数M取100),调用encrypt_image函数得到R、G、B三个加密数据列表,调用序列化方法,然后将加密数据按R、G、B三通道分别存入数据库中。
encrypt_image函数实现如下:
def encrypt_image(image_path, context_public, encryption_algorithm):
image = Image.open(image_path)
image = image.resize((64, 64)) # 调整大小
image_array = np.array(image)
# 分离每个通道
red_channel = image_array[:, :, 0].flatten().tolist()
green_channel = image_array[:, :, 1].flatten().tolist()
blue_channel = image_array[:, :, 2].flatten().tolist()
# 加密每个通道,使用公钥
encrypted_red = ts.ckks_vector(context_public, red_channel)
encrypted_green = ts.ckks_vector(context_public, green_channel)
encrypted_blue = ts.ckks_vector(context_public, blue_channel)
return encrypted_red, encrypted_green, encrypted_blue
这一函数获取图片路径后,读取图片并调整图片分辨率(本实验图片分辨率取为64*64),将图片分为R、G、B三通道,分别调用TenSEAL加密方法ts.ckks_vector(或ts.bfv_vector)使用公钥进行加密。
图像检索
在图像加密存储到数据库之后,下一个任务是给定图像,在加密数据中进行图像检索,这一部分代码的核心逻辑在主函数中实现。
首先要读取希望查询的图片,并调用一次同态加密算法获取查询图片的密文:
if __name__ == "__main__":
# ...
query_image_path = "./data/5.jpg"
encrypted_query_red, encrypted_query_green, encrypted_query_blue = encrypt_image(query_image_path, context, ts.SCHEME_TYPE.CKKS)
# ...
图像检索核心逻辑如下:
if __name__ == "__main__":
# ...
# 计算余弦相似度并排序
similarities = []
for i, row in enumerate(rows, start=1):
# 反序列化数据库中的密文向量
serialized_red, serialized_green, serialized_blue = row
encrypted_red = ts.ckks_vector_from(context, serialized_red)
encrypted_green = ts.ckks_vector_from(context, serialized_green)
encrypted_blue = ts.ckks_vector_from(context, serialized_blue)
# 计算余弦相似度
similarity_red = calculate_cosine_similarity(encrypted_query_red, encrypted_red)
similarity_green = calculate_cosine_similarity(encrypted_query_green, encrypted_green)
similarity_blue = calculate_cosine_similarity(encrypted_query_blue, encrypted_blue)
# 将三个通道的相似度取平均作为整体相似度
average_similarity = (similarity_red + similarity_green + similarity_blue) / 3
similarities.append((i, average_similarity))
print(f"计算第{i}张图片的相似度")
# 对相似度进行排序
print("相似度排序中...")
similarities.sort(key=lambda x: abs(x[1] - 1))
# 输出相似度最接近1的图像编号和相似度值
print(similarities)
closest_result = similarities[0]
print(f"最相似的图片为: {closest_result[0]}, 二者余弦相似度为: {closest_result[1]}")
# ...
如上所说,首先要对数据库中的数据进行反序列化以获得密文,然后调用calculate_cosine_similarity函数计算三通道的余弦相似度,将三通道的平均余弦相似度作为匹配标准,因为同态加密往往涉及浮点数运算,密文之间无无法做精确比较,因此这里认为平均余弦相似度最接近1图片即为要匹配的图片,最后输出匹配图片的序号。
这里匹配的核心是余弦相似度的计算,其公式为:$\text{Similarity}(\mathbf{A}, \mathbf{B}) = \frac{\sum_{i=1}^{n} A_i \cdot B_i}{\sqrt{\sum_{i=1}^{n} A_i^2} \cdot \sqrt{\sum_{i=1}^{n} B_i^2}}$。
由于CKKSVector和BFVVector类的成员函数差异,在两个算法中,calculate_cosine_similarity函数的实现略有不同,如下:
def calculate_cosine_similarity(vector1, vector2):
# 分子(ckks)
numerator = vector1.dot(vector2)
numerator = numerator.pow(2)
# 如果是bfv算法,分子的计算方法替换为下面两行
# numerator = vector1.dot(vector2)
# numerator = numerator * numerator
# 分母
denominator1 = vector1.dot(vector1)
denominator2 = vector2.dot(vector2)
denominator = denominator1 * denominator2
numerator = numerator.decrypt(ckks_secret_key)
denominator = denominator.decrypt(ckks_secret_key)
numerator_array = np.array(numerator)
denominator_array = np.array(denominator)
similarity = np.sum(numerator_array) / np.sum(denominator_array)
return similarity
图像解密与还原
为了验证全此处全同态加密的有效性,这部分对加密数据进行解密和还原,这一部分实现封装在decrypt_all_image中,函数如下:
def decrypt_all_image():
# 创建或连接到 SQLite 数据库
conn = sqlite3.connect("bfv_encrypted_images.db")
cursor = conn.cursor()
# 查询数据库以获取加密向量
cursor.execute("SELECT encrypted_red, encrypted_green, encrypted_blue FROM EncryptedImages")
rows = cursor.fetchall()
# 创建保存目录
save_directory = "./restored_images/"
os.makedirs(save_directory, exist_ok=True)
# 解密并还原每个图像
for i, row in enumerate(rows, start=1):
# 反序列化加密向量的值
serialized_red, serialized_green, serialized_blue = row
# 反序列化加密向量
deserialized_red = ts.bfv_vector_from(context, serialized_red)
deserialized_green = ts.bfv_vector_from(context, serialized_green)
deserialized_blue = ts.bfv_vector_from(context, serialized_blue)
# 使用私钥解密
decrypted_red = deserialized_red.decrypt(secret_key = bfv_secret_key)
decrypted_green = deserialized_green.decrypt(secret_key = bfv_secret_key)
decrypted_blue = deserialized_blue.decrypt(secret_key = bfv_secret_key)
# 转换为 NumPy 数组
red_channel = np.array(decrypted_red)
green_channel = np.array(decrypted_green)
blue_channel = np.array(decrypted_blue)
# 由于解密后是一维数组,需要重新调整为图像形状
shape = (int(len(decrypted_red) ** 0.5), int(len(decrypted_red) ** 0.5), 1)
red_channel = red_channel.reshape(shape)
green_channel = green_channel.reshape(shape)
blue_channel = blue_channel.reshape(shape)
# 合并通道并进行值的缩放和截断
restored_image_array = np.clip(np.concatenate([red_channel, green_channel, blue_channel], axis=2), 0, 255).astype('uint8')
# 将还原后的图像数组转换为图像对象
restored_image = Image.fromarray(restored_image_array.astype('uint8'))
# 保存还原后的图像为 JPG 文件
restored_image.save(f"./restored_images/{i}_restored.jpg")
print(f"解密还原第{i}张图片")
conn.close()
print("解密完成")
其中关键步骤均用注释进行说明,需要注意的部分主要是:调用反序列化方法、使用私钥解密、图片形状reshape。
以上均基于CKKS同态加密算法进行分析,如果要换用BFV算法,需要更改的地方主要有:
数据库名称:
conn = sqlite3.connect("bfv_encrypted_images.db")
context初始化:
# 定义 BFV context,并分离公私钥 context = ts.context(ts.SCHEME_TYPE.BFV,poly_modulus_degree=8192,plain_modulus=1032193) context.global_scale = 2**20 context.generate_galois_keys() bfv_secret_key = context.secret_key() context.make_context_public()
加密方法:
encrypted_red = ts.bfv_vector(context, red_channel) encrypted_green = ts.bfv_vector(context, green_channel) encrypted_blue = ts.bfv_vector(context, blue_channel)
反序列化方法:
serialized_red, serialized_green, serialized_blue = row encrypted_red = ts.bfv_vector_from(context, serialized_red) encrypted_green = ts.bfv_vector_from(context, serialized_green) encrypted_blue = ts.bfv_vector_from(context, serialized_blue)
calculate_cosine_similarity函数中分子计算方法,上文已做解释。
此外还有一些相应的变量名称需要更换,不再赘述。
实验结果和效率比较
为保证算法比较的客观性,本实验实现两种算法的代码流程基本一致,影响算法复杂度的两个关键参数poly_modulus_degree和global_scale也保持一致,设置为:context.poly_modulus_degree=8192
、context.global_scale = 2**40
。在实验数据上,统一使用了CelebA人脸数据集的前100张图片作为测试数据,在读取图片时统一将图片分辨率更改为64*64。
下面对CKKS和BFV两个算法的三个测试维度(同态加密、密态搜索、同态解密)分别测试用时,测试过程截图如下:
每次执行算法的用时都略有不同,这里对每种算法的每项任务的平均用时取3次测试结果的平均值,以便进行效率比较和分析,测试结果如下:
同态加密用时:
测试次数\算法 | CKKS | BFV |
---|---|---|
1 | 6877ms | 7896ms |
2 | 6957ms | 7337ms |
3 | 6699ms | 7398ms |
平均 | 6844.3ms | 7543.7ms |
密态搜索用时:
测试次数\算法 | CKKS | BFV |
---|---|---|
1 | 21818ms | 64848ms |
2 | 20206ms | 64673ms |
3 | 20475ms | 65213ms |
平均 | 20833ms | 64911.3ms |
同态解密用时:
测试次数\算法 | CKKS | BFV |
---|---|---|
1 | 3255ms | 4545ms |
2 | 3341ms | 4044ms |
3 | 3449ms | 4185ms |
平均 | 3348.3ms | 4258ms |
上述测试结果,从不同任务的角度来看,密态搜索任务的用时是最长的,而密态解密的用时最短;从算法的角度来看,三项任务中CKKS算法的运行速度均比BFV算法快,CKKS算法在同态加密平均比BFV算法快10.2%,在密态搜索中快211.6%,在同态解密中快27.7%,整体上看,CKKS算法效率始终高于BFV算法,效率优势在密态搜索任务上体现得非常显著。
分析与总结
在本次实验从不同的任务角度对比了CKKS和BFV两种同态加密算法在图像处理任务中的性能。结合实验过程和结果,做出以下分析与总结:
在实验环境方面,我在Window宿主系统使用wsl2的Linux Ubuntu操作系统(22.04.3)、CPU型号为AMD Ryzen 7 5800H(16核)以及16 Gb运行内存下进行了TenSEAL库中CKKS和BFV两种同态加密算法执行同态加密、密态检索、同态解密三个任务的效率测试。
在任务性能方面,我针对同一开源库中的CKKS和BFV两种同态加密算法进行了多次测试,以获取更为稳定的平均性能数据。在图像加密存储任务中,可以观察到CKKS算法相对于BFV算法,平均快了10.2%。在密态搜索任务中,CKKS算法表现出更为显著的优势,其运行速度相比BFV算法平均快了211.6%。而在同态解密任务中,CKKS算法仍然保持较高的效率,平均快了27.7%。
从不同算法的角度看,CKKS算法在所有测试任务中都表现出了更高的效率。在密态搜索任务中,其性能提升尤为显著,这可能归因于CKKS算法对多项式环上计算的适用性以及其更高的并行性。相较之下,BFV算法在同态加密任务中的性能相对较低,可能由于其更为复杂的计算模型导致的。
此外,在不同任务中,我们还观察到密态搜索任务的耗时相对较长,而密态解密任务的用时最短。这可以解释为,在同态加密中,解密操作相对简单,而密文搜索可能涉及到更多的计算,导致相对较高的执行时间。但是,更短的时间不能与算法的优劣直接关联,由于BFV更复杂的计算机制,虽然效率偏低,但可能会会同态加密的安全性或其他方面有提升,这里不做展开。
综上所述,从测试结果中可以得出结论,CKKS算法在多项测试任务中表现出更高的性能和效率,而BFV算法相对较慢。在选择同态加密算法时,需要根据具体应用场景和任务需求来权衡算法的优劣,以确保系统在安全性和性能之间取得合适的平衡。