简单的复现 多关键字模糊可搜索加密

模糊可搜索加密是密文检索的一个分支,最早由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 X1X2X3XK。我们把这 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 S1C1C2Cn和二进制向量 S 2 = b 0 b 1 … b m − 1 S_2=b_0b_1…b_{m-1} S2b0b1bm1 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(CjCj+1)i时, b i = 1 b_i=1 bi. 把 S 1 → S 2 S_1→S_2 S1S2的函数称为对偶编码函数

方案的实现

① 生成密钥

s k ← K e y G e n ( 1 k ) sk \leftarrow KeyGen(1^k) skKeyGen(1k) 生成密钥,具体步骤如下(k>128):

(1)随机构建 2 个 k × k 维的矩阵 M 1 , M 2 ∈ R k × k M_1,M_2 \inR^{k × k} M1,M2×

(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) IBuildIndex(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=(s1s2,.....,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+rbj=21bjr
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=M1TBiIi=M2TBi

(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) ITrapdoor(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=(s1s2,.....,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+rbj=21bjr
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=M11Bt=M21B

(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+rbj=21bjr ,且最后组成陷门时是 M 1 − 1 , M 2 − 1 M_1^{-1},M_2^{-1} M11,M21

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) FRSearch(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=Itt+Iit

③ 之后将内积做排序,将前λ条记录加入到集合

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”,这个关键字。当然了可能因为复现的不完全,准确率不是很高,这个可能要到以后看看有没有时间再修改修改。

在这里插入图片描述

应一些读者的要求,我把我自己写的代码放出来,整体的结构和效果比较一般,希望大家多多见谅。
github 地址:
https://github.com/AltarIbnL/Multi-keyword-fuzzy-searchable-encryption


参考文献:

[1] 王恺璇, 李宇溪, 周福才,等. 面向多关键字的模糊密文搜索方法[J]. 计算机研究与发展, 2017(2).

  • 11
    点赞
  • 60
    收藏
    觉得还不错? 一键收藏
  • 16
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值