文章目录
模糊可搜索加密是密文检索的一个分支,最早由Jin Li 提出的基于编辑距离构建模糊集合,相较于以前论文,对于关键字的搜索都是要求完全正确,不能有一点格式和拼写错误,如果存在拼写错误会影响搜索结果,整体消耗太高。需要用户付出庞大的数据存储空间,且只实现了基于单关键字的模糊密文搜索。
博主最近看了一篇论文《面向多关键字的模糊密文搜索方法》,使用LSH和布隆过滤器来代替模糊集合,实现了多关键字的模糊搜索。下面简单介绍一下思路:
基础知识:
局部敏感哈希LSH(Locality Sensitive Hashing)
详细的介绍请看这篇博文:https://blog.csdn.net/guoziqing506/article/details/53019049
简单来说LSH最根本的作用,就是能高效处理海量高维数据的最近邻问题,使得2个相似度很高的数据以较高的概率映射成同一个hash值,而令2个相似度很低的数据以极低的概率映射成同一个hash值。(类似于bloom filter)
总体的流程如图:
文中的方案是使用P-stable hash 来构建。
布隆过滤器(Bloom filter)
在学数据结构的时候,相信大家都了解过布隆过滤器(后面简称BF)的原理(没学过的推荐极客时间王争老师的《数据结构与算法之美》,给老师打波广告,哈哈)。
布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。
布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
位图
布隆过滤器的实现是基于位图的,也就是位数组,在位数组上的每一个位都只有1bit,而且每个bit只有0、1两种状态。假设我们申请一个大小为 1 亿、数据类型为布尔类型(true 或者 false)的数组。我们将这 1 千万个整数作为数组下标,将对应的数组值设置成 1。比如,整数 5 对应下标为 5 的数组值设置为 1,也就是 array[5]=1。
那么我们要查询某个整数K是否在这1千万个整数里,我们只需要查询数组对应下标 array[K] 是否为 1。如果是1,说明整数K在1千万个数里;否则就不存在。
Bloom filter
布隆过滤器是对位图的一种改进。假设仍然有1千万个数,位图的大小是n=1亿。们使用 K 个哈希函数,对同一个数字进行求哈希值,那会得到 K 个不同的哈希值,我们分别记作 X 1 , X 2 , X 3 , … , X K X1,X2,X3,…,XK X1,X2,X3,…,XK。我们把这 K 个数字作为位图中的下标,将对应的 B i t M a p [ X 1 ] , B i t M a p [ X 2 ] , B i t M a p [ X 3 ] , … , B i t M a p [ X K ] BitMap[X1],BitMap[X2],BitMap[X3],…,BitMap[XK] BitMap[X1],BitMap[X2],BitMap[X3],…,BitMap[XK] 都设置成 1,也就是说,我们用 K 个二进制位,来表示一个数字的存在。
如果查询某个数是否在位图中,我们只需对该数字进行hash,得到其K个不同的哈希值,判断位置下标是否为1即可
布隆过滤器的误判有一个特点,那就是,它只会对存在的情况有误判。如果某个数字经过布隆过滤器判断不存在,那说明这个数字真的不存在,不会发生误判;如果某个数字经过布隆过滤器判断存在,这个时候才会有可能误判,有可能并不存在。不过,只要我们调整哈希函数的个数、位图大小跟要存储数字的个数之间的比例,那就可以将这种误判的概率降到非常低。
对偶编码函数
对偶编码函数. 给定字符串 S 1 = C 1 C 2 … C n S_1=C_1C_2…C_n S1=C1C2…Cn和二进制向量 S 2 = b 0 b 1 … b m − 1 S_2=b_0b_1…b_{m-1} S2=b0b1…bm−1, S 2 S_2 S2中每位元素的初始值为0, 其中n<m. 通过 Hash 函数 H, 把 S 1 S_1 S1中相邻2个字符散列映射为0~(m-1) 之间的数,当且仅当 H ( C j , C j + 1 ) = i H(C_j,C_{j+1})=i H(Cj,Cj+1)=i时, b i = 1 b_i=1 bi=1. 把 S 1 → S 2 S_1→S_2 S1→S2的函数称为对偶编码函数
方案的实现
① 生成密钥
s k ← K e y G e n ( 1 k ) sk \leftarrow KeyGen(1^k) sk←KeyGen(1k) 生成密钥,具体步骤如下(k>128):
(1)随机构建 2 个 k × k 维的矩阵 M 1 , M 2 ∈ R k × k M_1,M_2 \inR^{k × k} M1,M2∈Rk×k ;
(2)随机构建一个k维的向量 S ∈ { 0 , 1 } k S \in \left\{ 0,1\right\}^k S∈{0,1}k,且S中0和1的数量需要大致相同
(3)输出 sk = ( M 1 , M 2 , S M_1,M_2,S M1,M2,S)作为生成加密索引和生成陷门的密钥.
def keygen(k):
'''
# 输入参数k,返回密钥sk
:param k: 随机矩阵的维度
:return: sk=(M1,M2,S)
'''
M1=np.random.randint(0,2,[k,k])
M2=np.random.randint(0,2,[k,k])
S=np.random.randint(0,2,[k])
sk = [M1,M2,S]
return sk
② 生成安全索引
(1) I ← B u i l d I n d e x ( s k , F , W ) I \leftarrow BuildIndex(sk,F,W) I←BuildIndex(sk,F,W) 其中, F = ( f 1 , f 2 , . . . . . , f n ) F= (f_1,f_2,.....,f_n) F=(f1,f2,.....,fn)为文档集合。为每个文件 f i f_i fi构建一个k位的布隆滤波器 B i B_i Bi。
(2)每个文件f对应的关键字集合 W i = { w 1 , w 2 , . . . . w t } W_i = \left\{w_1,w_2,....w_t \right\} Wi={w1,w2,....wt},使用对偶编码将其转换成向量集合 V = { v 1 , v 2 , . . . . v t } V=\left\{v_1,v_2,....v_t \right\} V={v1,v2,....vt}中每个向量 v i ∈ { 0 , 1 } 676 v_i \in \left\{ 0,1\right\}^{676} vi∈{0,1}676,使用LSH敏感哈希函数 { H 1 , H 2 , . . . . H l } \left\{H_1,H_2,....H_l \right\} {H1,H2,....Hl} 进行计算 ( H 1 ( v i ) , H 2 ( v i ) , . . . . . H l ( v i ) ) (H_1(v_i),H_2(v_i),.....H_l(v_i)) (H1(vi),H2(vi),.....Hl(vi)),并将结果插入到Bloom 过滤器 B i B_i Bi中。(一共n个布隆滤波器,每个代表文档的关键字)
(3)提取sk中的S,并表示
S
=
(
s
1
,
s
2
,
.
.
.
.
.
,
s
k
)
S=(s_1,s_2,.....,s_k)
S=(s1,s2,.....,sk),对于每一个文件
f
i
f_i
fi的过滤器
B
i
=
(
b
1
b
2
,
.
.
.
.
b
k
)
B_i=(b_1b_2,....b_k)
Bi=(b1b2,....bk),并随机选择一个参数r。因为S和B都是由 {0,1} 构成的k位向量。对于
B
i
B_i
Bi中的每一个
b
j
b_j
bj,若在S中对应的
s
j
=
1
s_j=1
sj=1,则令
b
j
′
=
b
j
′
′
=
b
j
b_j'=b_j''=b_j
bj′=bj′′=bj;若对应的
s
j
=
0
s_j=0
sj=0,则令:
b
j
′
=
1
2
b
j
+
r
,
b
j
′
′
=
1
2
b
j
−
r
b_j'=\frac{1}{2} b_j+r,b_j''=\frac{1}{2}b_j-r
bj′=21bj+r,bj′′=21bj−r
令
B
i
′
=
(
b
1
′
,
b
2
′
,
b
3
′
,
.
.
.
,
b
k
′
)
,
B
i
′
′
=
(
b
1
′
′
,
b
2
′
′
,
.
.
.
,
b
k
′
′
)
B_i'=(b_1',b_2',b_3',...,b_k'),B_i''=(b_1'',b_2'',...,b_k'')
Bi′=(b1′,b2′,b3′,...,bk′),Bi′′=(b1′′,b2′′,...,bk′′)
I i = ( I i ′ , I i ′ ′ ) I_i=(I_i',I_i'') Ii=(Ii′,Ii′′),其中 I i ′ = M 1 T ⋅ B i ′ , I i ′ ′ = M 2 T ⋅ B i ′ ′ I_i'=M_1^T\cdot B_i',I_i''=M_2^T \cdot B_i'' Ii′=M1T⋅Bi′,Ii′′=M2T⋅Bi′′
(4)最后输出 I = ( F , I 1 , I 2 , . . . , I n ) I=(F,I_1,I_2,...,I_n) I=(F,I1,I2,...,In)
例如:给定3个关键字:Network,Search,Bloom,先使用对偶编码转换成676bit长的数组,其中每个元素都表示每 2 个字符的组合(0,1),然后利用选择的 l 个位置敏感 Hash函数(这个示例中 l =2 ),计算单个关键词在布隆过滤器中对应的位置,最后该对应的位置置位为1.
def BuildIndex(sk,F,W):
'''
param: sk=(M1,M2,S), F是文档集合,W={w1,w2....,wn}是文档对应的关键字
return:I=(F,I',I'')
'''
V=[] #V储存关键字转换成编码后的值
# 为每个文件生成布隆过滤器,布隆过滤器的长度k=128
B=[BF.BloomFilter(capacity=1024,error_rate=0.001) for i in range(len(W))]
for val in W:
# Dual_code是对偶编码函数,输入一个数组,将数组内的关键字转换成向量
v_i = Dual_code(val)
V.append(v_i)
# 将向量集合V中每一个向量vi,通过位置敏感函数Hash进行计算,注意,此时vi每一个元素都是字符串,后面需要将其转换成浮点型
for idx,val in enumerate(V): # 遍历每一个文档对应关键字的数据向量
'''
e2LSH是符合P-STABLE分布的哈希敏感函数,
p返回4个元素,分别是①哈希列表,里面储存有20个(自行设定)哈希函数
②hashFun哈希功能表,储存5个(自行设定)哈希列表,③fpRand 随机数集合,④hash_collect 每个数据向量hash多次后后的值
'''
p=LSH.e2LSH(val,k=20, L=5, r=1, tableSize=20)
H_p=p[3] #提取每个数据向量hash 20次 后的值
# 将结果插入布隆过滤器
for j in range(len(H_p)):
# print(H_p[j])
B[idx].add(H_p[j]) # 逐个数据向量输入bloom
S = sk[2] #提取私钥中的S
r = random.randint(1,10) # 生成一个随机数
# B_i = {b1,b2...bk} 每个文档对应哈希列表的每一位
# 遍历S和B中的每一位,如果S中对应的s_j=1,则b_j'=b_j''=b_j;若s_j=0,令b_j' = 1/2*b_j+r 和b_j''=1/2*b_j-r。
B_P=[]
B_DP=[] #B'、B''
I=[F] # 索引
for i,b in enumerate(B):
for j,vals in enumerate(S):
if vals==1:
# 如果s_j=1
# print(j)
b_jp=b_jpp=int(b.bitarray[j]) # b_j'=b_j''=b_j ,v是第i个bloomfilter
B_P.append(b_jp)
B_DP.append(b_jpp)
else:
# 如果s_j=0
b_jp = 0.5*int(b.bitarray[j])+r
b_jpp = 0.5*int(b.bitarray[j])-r
B_P.append(b_jp)
B_DP.append(b_jpp)
I_P = np.inner(np.transpose([sk[0]]).reshape((128,128)),B_P) # 将M1矩阵转置后与B'点乘
I_DP = np.inner(np.transpose([sk[1]]).reshape((128,128)),B_DP) # 将M2矩阵转置后与B''点乘
I_i = [I_P,I_DP]
I.append(I_i)
B_DP = [] # 清空B'和B''
B_P = []
return I # I是由多个文件夹构成的索引组成
其中dual_code 和e2LSH,BF分别是对偶编码函数和局部敏感hash和布隆过滤器,dual返回的是关键字转换成二进制后的 { 0 , 1 } 676 \left\{ 0,1 \right\}^{676} {0,1}676,e2LSH返回的是一个列表,分别是
① 哈希列表,里面储存有20个(自行设定)哈希函数
② hashFun哈希功能表,储存5个(自行设定)哈希列表
③ fpRand 随机数集合
④ hash_collect 每个数据向量hash多次后后的值,我们主要使用这个
附带dual_code函数:
def Dual_code(S1): #S1 是一个数组
temp = []
for index,value in enumerate(S1):
x = 0
temp1 = [x for i in range(676)] #建立一个全为0的数组
# print(value)
length=len(value)
for i,v in enumerate(value): # 遍历当前关键字
if(i==length-1):
C = ord(v)+ord(' ') #如果是最后一个字符,将最后一个字符转换成ascii相加
C = C%676
while(temp1[C]==1): #将hash表中对应位置置1,如果该位置已经有1,则后移
if(C!=675):
C+=1
else:
C=0
temp1[C] = 1
else:
C = ord(v)+ord(S1[index][i+1])
C = C%676
while (temp1[C] == 1):
if (C != 675):
C += 1
else:
C = 0
temp1[C] = 1
temp.append(temp1)
return temp
if __name__ == '__main__':
S1=['jugment ae12','kugou']
S2=Dual_code(S1)
print(S2[0].count(1))
print(S2[0])
e2LSH代码(太长了只放一部分)
def e2LSH(dataSet, k, L, r, tableSize):
"""
generate hash table
* hash table: a list, [node1, node2, ... node_{tableSize - 1}] 构建一个哈希表
** node: node.val = index; node.buckets = {}
*** node.buckets: a dictionary, {fp:[v1, ..], ...}
:param dataSet: a set of vector(list) 数据向量
:param k: k表示hash函数的数量 与 k个数组成的整数向量
:param L: L组hash组,L组hash函数,每组由k个hash函数构成
:param r:
:param tableSize:
:return: 3 elements, hash table, hash functions, fpRand
"""
# TableNode 是一个类,将表的编号作为其内部的值
hashTable = [TableNode(i) for i in range(tableSize)]
# n=len(dataSet[0]) 获得列表的列数,数据集传入5个向量,m=len(dataSet)获得行数
n = len(dataSet[0])
# print(n)
m = len(dataSet)
# print(m)
C = pow(2, 32) - 5
hashFuncs = []
fpRand = [random.randint(-10, 10) for i in range(k)] #fpRand 是用于计算一个数据向量的“指纹”随机数
#构造函数簇,之后会从中取出k个hahs函数,而且LSH函数簇中hash函数计算公式是:h_ab=[(a▪v+b)/r]
e2LSH_family = gen_e2LSH_family(n, k, r)
# hashFuncs: [[h1, ...hk], [h1, ..hk], ..., [h1, ...hk]]
# hashFuncs include L hash functions group, and each group contain k hash functions
hashFuncs.append(e2LSH_family)
hash_collect=[] # 记录hash数据向量后不同数据向量的值
for dataIndex in range(m): # 对文档中所有向量进行运算
# print(dataSet[dataIndex])
# 对一个数据向量生成k个哈希值
hashVals = gen_HashVals(e2LSH_family, dataSet[dataIndex], r)
hash_collect.append(hashVals)
# 生成指纹向量
fp = H2(hashVals, fpRand, k, C)
# generate index,也就是H1的值
index = fp % tableSize
# 查找到在哈希列表的节点,每个节点储存具有相同或者相近指纹向量的数据。经过H1哈希后index相同,储存在同一个桶中,就代表他们是邻近的
node = hashTable[index]
# node.buckets 是一个字典:{123:[1],0:[362,585]} , 123表示指纹向量,1代表第一个数据向量;同理,后面的0表示指纹向量,362,585表示数据向量的索引
if fp in node.buckets:
# bucket is vector list
bucket = node.buckets[fp]
# add the data index into bucket
bucket.append(dataIndex)
else:
node.buckets[fp] = [dataIndex]
return hashTable, hashFuncs, fpRand,hash_collect
③陷门构造
(1) I ← T r a p d o o r ( s k , Q ) I \leftarrow Trapdoor(sk,Q) I←Trapdoor(sk,Q) 根据查询关键词集合生成安全陷门:
(2)构建一个k位的布隆滤波器 B B B。
(2)对于查询关键字集合 Q = { q 1 , q 2 , . . . . q t } Q = \left\{q_1,q_2,....q_t \right\} Q={q1,q2,....qt},使用对偶编码将其转换成向量集合 V = { v 1 , v 2 , . . . . v t } V=\left\{v_1,v_2,....v_t \right\} V={v1,v2,....vt}中每个向量 v i ∈ { 0 , 1 } 676 v_i \in \left\{ 0,1\right\}^{676} vi∈{0,1}676,使用LSH敏感哈希函数 { H 1 , H 2 , . . . . H l } \left\{H_1,H_2,....H_l \right\} {H1,H2,....Hl} 进行计算 ( H 1 ( v i ) , H 2 ( v i ) , . . . . . H l ( v i ) ) (H_1(v_i),H_2(v_i),.....H_l(v_i)) (H1(vi),H2(vi),.....Hl(vi)),并将结果插入到Bloom 过滤器 B i B_i Bi中。
(3)提取sk中的S,并表示
S
=
(
s
1
,
s
2
,
.
.
.
.
.
,
s
k
)
S=(s_1,s_2,.....,s_k)
S=(s1,s2,.....,sk),对于每一个文件
f
i
f_i
fi的过滤器
B
i
=
(
b
1
b
2
,
.
.
.
.
b
k
)
B_i=(b_1b_2,....b_k)
Bi=(b1b2,....bk),并随机选择一个参数r’。因为S和B都是由 {0,1} 构成的k位向量。对于
B
i
B_i
Bi中的每一个
b
j
b_j
bj,若在S中对应的
s
j
=
0
s_j=0
sj=0,则令
b
j
′
=
b
j
′
′
=
b
j
b_j'=b_j''=b_j
bj′=bj′′=bj;若对应的$s_j=01,则令:
b
j
′
=
1
2
b
j
+
r
′
,
b
j
′
′
=
1
2
b
j
−
r
′
b_j'=\frac{1}{2} b_j+r',b_j''=\frac{1}{2}b_j-r'
bj′=21bj+r′,bj′′=21bj−r′
令
B
′
=
(
b
1
′
,
b
2
′
,
b
3
′
,
.
.
.
,
b
k
′
)
,
B
′
′
=
(
b
1
′
′
,
b
2
′
′
,
.
.
.
,
b
k
′
′
)
B'=(b_1',b_2',b_3',...,b_k'),B''=(b_1'',b_2'',...,b_k'')
B′=(b1′,b2′,b3′,...,bk′),B′′=(b1′′,b2′′,...,bk′′)
t i = ( t ′ , t ′ ′ ) t_i=(t',t'') ti=(t′,t′′),其中 t ′ = M 1 − 1 ⋅ B ′ , t ′ ′ = M 2 − 1 ⋅ B ′ ′ t'=M_1^{-1}\cdot B',t''=M_2^{-1} \cdot B'' t′=M1−1⋅B′,t′′=M2−1⋅B′′
(4)输出陷门t。
其中要注意,在构造陷门时, s j = 0 s_j=0 sj=0时, b j ′ = b j ′ ′ = b j b_j'=b_j''=b_j bj′=bj′′=bj (构造索引是 s j = 1 s_j=1 sj=1)
s j = 1 s_j=1 sj=1时, b j ′ = 1 2 b j + r , b j ′ ′ = 1 2 b j − r b_j'=\frac{1}{2} b_j+r,b_j''=\frac{1}{2}b_j-r bj′=21bj+r,bj′′=21bj−r ,且最后组成陷门时是 M 1 − 1 , M 2 − 1 M_1^{-1},M_2^{-1} M1−1,M2−1
def Trapdoor(sk,Q):
'''
:param sk: 密钥
:param Q: 查询关键字
:return: t={t',t''}
'''
V = [] # V储存关键字转换成编码后的值
# 生成1个布隆过滤器
B_t = BF.BloomFilter(capacity=1024, error_rate=0.001)
# Dual_code是对偶编码函数,输入一个数组,将数组内的关键字转换成向量。此时查询关键字集合相当于只在一个文档内部,无需循环
v_i = Dual_code(Q)
V.append(v_i)
# 将向量集合V中每一个向量vi,通过位置敏感函数Hash进行计算,注意,此时vi每一个元素都是字符串,后面需要将其转换成浮点型
for idx,val in enumerate(V): # 遍历每一个文档对应关键字的数据向量
'''
e2LSH是符合P-STABLE分布的哈希敏感函数,
p返回4个元素,分别是①哈希列表,里面储存有20个(自行设定)哈希函数
②hashFun哈希功能表,储存5个(自行设定)哈希列表,③fpRand 随机数集合,④hash_collect 每个数据向量hash多次后后的值
'''
p=LSH.e2LSH(val,k=20, L=5, r=1, tableSize=20)
H_p=p[3] #提取每个数据向量hash 20次 后的值
# 将结果插入布隆过滤器
for j in range(len(H_p)):
# print(H_p[j])
B_t.add(H_p[j]) # 逐个数据向量输入bloom
S = sk[2] # 提取私钥中的S
r = random.randint(1, 10) # 生成一个随机数
# B_i = {b1,b2...bk} 每个文档对应哈希列表的每一位
# 遍历S和B中的每一位,如果S中对应的s_j=0,则b_j'=b_j''=b_j;若s_j=1,令b_j' = 1/2*b_j+r 和b_j''=1/2*b_j-r。
B_P = []
B_DP = [] # B'、B''
for j,vals in enumerate(S):
if vals==0:
# 如果s_j=0
# print(j)
b_jp=b_jpp=int(B_t.bitarray[j]) # b_j'=b_j''=b_j ,v是第i个bloomfilter
B_P.append(b_jp)
B_DP.append(b_jpp)
else:
# 如果s_j=1
b_jp = 0.5*int(B_t.bitarray[j])+r
b_jpp = 0.5*int(B_t.bitarray[j])-r
B_P.append(b_jp)
B_DP.append(b_jpp)
T_P = np.inner(np.linalg.inv([sk[0]]).reshape((128, 128)), B_P) # 将M1矩阵取逆后与B'点乘
T_DP = np.inner(np.linalg.inv([sk[1]]).reshape((128, 128)), B_DP) # 将M2矩阵取逆后与B''点乘
t=[T_P,T_DP]
return t
④ 搜索函数
F R ← S e a r c h ( I , t ) F_R \leftarrow Search(I,t) FR←Search(I,t) 服务器根据陷门和加密索引执行搜索
将索引 I I I表示为 I = ( F , I 1 , I 2 . . . . I n ) I=(F,I_1,I_2....I_n) I=(F,I1,I2....In),对于每一个 I i I_i Ii:
① 将 I i I_i Ii表示为 I i = ( I i ′ , I i ′ ′ ) I_i=(I_i',I_i'') Ii=(Ii′,Ii′′),将t表示为 t = ( t ′ , t ′ ′ ) t=(t',t'') t=(t′,t′′)。
② 计算向量积: R I = I t ′ ⋅ t ′ + I i ′ ′ ⋅ t ′ ′ R_I = I_t'\cdot t'+I_i'' \cdot t'' RI=It′⋅t′+Ii′′⋅t′′
③ 之后将内积做排序,将前λ条记录加入到集合
def Search(I,t):
'''
:param I: 文档索引
:param t: 搜索陷门
:return: File 包含关键字的文档集合
'''
File=[]
for I_i in I[1:]:
I_iP = I_i[0] # I_i'
I_iDP = I_i[1] # I_i''
R_i = np.inner(I_iP,t[0])+np.inner(I_iDP,t[1])
File.append(R_i)
return File
假设在文档中搜索 “he” 这个单词,可以看到第1,2,3个文档都有 “he”,这个关键字。当然了可能因为复现的不完全,准确率不是很高,这个可能要到以后看看有没有时间再修改修改。
代码我就只放这么多了,主要的部分已经写的比较清晰了,剩下的我相信各位也能写出来。对于安全性证明这一方面大家可以参考一下论文,我就不放出来了。
参考文献:
[1] 王恺璇, 李宇溪, 周福才,等. 面向多关键字的模糊密文搜索方法[J]. 计算机研究与发展, 2017(2).