抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

CKKS与BFV算法的图片同态加密、密态检索与同态解密

本文基于TenSEAL库实现,是《数据安全与隐私保护》课程Project,由于是课程作业性质,这里的重点放在了运行效率的比较,没有将server端和client段做严谨区分,context的公私钥分离也略显潦草。本文的源码可以在这里找到,仅供参考,若想深入学习请查看官方文档。

如果不了解同态加密,以下是一些供参考的文章:

同态加密Homomorphic Encryption介绍

全同态加密算法深入解析

同态加密详解

这是同态加密实现的经典逻辑框图:

image-20231211201406187

以下是正文:

介绍

全同态加密(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 框架集成,以支持联邦学习和安全多方计算。

主要特点和功能:

  1. SEAL 基础: TenSEAL建立在 Microsoft Research 提供的 SEAL 库之上,该库专注于提供同态加密的基础原语。这使得 TenSEAL 可以充分利用 SEAL 的强大功能,同时为用户提供更高层次的接口。
  2. 同态加密支持: TenSEAL 提供了同态加密的功能,使得用户可以在加密状态下执行计算,而无需解密敏感数据。
  3. PySyft 集成: TenSEAL 与 PySyft 框架集成,支持联邦学习和安全多方计算。这使得 TenSEAL 能够与 PySyft 中的其他工具和库协同工作,实现更广泛的隐私保护应用。
  4. 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()

主函数的每一个关键步骤均在代码中以注释说明,其流程可总结如下:

  1. 定义 CKKS context,并分离公私钥;
  2. 创建并连接到 SQLite 数据库,创建表;
  3. 将图片数据进行同态加密,并存储到数据库中;
  4. 读取要查询的图片;
  5. 从数据库中获取加密数据,反序列化,并与查询图片计算余弦相似度,选取相似度最高的图片,输出;
  6. 将数据库中的所有加密数据全部解密,并还原为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算法,需要更改的地方主要有:

  1. 数据库名称:conn = sqlite3.connect("bfv_encrypted_images.db")

  2. 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()
    
  3. 加密方法:

    encrypted_red = ts.bfv_vector(context, red_channel)
    encrypted_green = ts.bfv_vector(context, green_channel)
    encrypted_blue = ts.bfv_vector(context, blue_channel)
    
  4. 反序列化方法:

    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)
    
  5. calculate_cosine_similarity函数中分子计算方法,上文已做解释。

此外还有一些相应的变量名称需要更换,不再赘述。

实验结果和效率比较

为保证算法比较的客观性,本实验实现两种算法的代码流程基本一致,影响算法复杂度的两个关键参数poly_modulus_degree和global_scale也保持一致,设置为:context.poly_modulus_degree=8192context.global_scale = 2**40。在实验数据上,统一使用了CelebA人脸数据集的前100张图片作为测试数据,在读取图片时统一将图片分辨率更改为64*64。

下面对CKKS和BFV两个算法的三个测试维度(同态加密、密态搜索、同态解密)分别测试用时,测试过程截图如下:

image-20231211133207261

image-20231211133257989

image-20231211133548923

image-20231211133602023

每次执行算法的用时都略有不同,这里对每种算法的每项任务的平均用时取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算法相对较慢。在选择同态加密算法时,需要根据具体应用场景和任务需求来权衡算法的优劣,以确保系统在安全性和性能之间取得合适的平衡。

Comments