AES基于java的算法的具体实现

AES算法的数学基础

1GF(2)域上的多项式

在有限域GF(28)中的元素操作运算可采用几种不同的方法来表示,AES算法主要选择传统的多项式表示法进行的。因为不同的表示对GF28)的运算复杂度是有影响的。

一个由b7b6b5b4b3b2b1b0组成的字节b可表示成系数为[0, 1]的二进制多项式

       b7x7 + b6x6 + b5x5 + b4x4 + b3x3 + b2x2 + b1x1 + b0

例如:十六进制数“6B”对应的二进数为01101011,作为一个字节对应于多项式为:

         x6 + x5 + x3 + x + 1

 

11.多项式加法

GF28)上的加法定义为二进制多项式的加法,其系数满足模2加(即异或)。例如:“6B”和“71”的和为:6B + 71 = 1A,或采用多项式记法:

       (x6 + x5 + x3 + x + 1) + (x6 + x5 + x4 +1) = x4 + x3 + x

显然,多项式加法与以字节为单位的比特异或结果是一致的。

 

12.多项式的乘法

       GF28)上的乘法(用符节“.”表示)定义为二进制多项式的乘积以8次不可约多项式为模的积,此8次不可约多项式为:

              m(x) = x8 + x4 + x3 + x + 1

       十六进制为“11B”,二进制表示为100011011,此乘法在GF28)上满足结合律,且有一个本原元(“01”),例如:

               x6 + x5 + x3 + x + 1)·(x6 + x5 + x4 + 1

              = x12 + x8 + x6 + x5 + x4 + x + 1(mod m(x))

              = x7 + x6 + x + 1

或者:6B·71 = C3

       显而易见,模的结果是一个低于8阶的多项式,与加法不同的是,乘法并不是在字节上的简单运算。

 

13.函数xtime()

       函数xtime()定义为GF28)上的x·b(x),若b7 = 0,则字节b左移一位;若b7 = 1,则字节b左移一位,再异或“1B”。

       bx)为GF28)域中次数小于8的多项式,由欧几里德扩充算法知,存在a(x)c(x),满足:

                     a(x)b(x) + c(x)m(x) = 1                                 ( 2 – 1 )

因此有:       a(x)b(x)mod m(x) = 1                                  ( 2 – 2 )

       或:    a(x)b(x) = 1 mod(m(x))                                ( 2 – 3 )

由此,我们可以获得b(x)的逆元:b-1(x) = a(x) mod m(x)

 

假设:b(x) = b7x7 + b6x6 + x5b5 + b4x4 + b3x3 + b2x2 + b1x1 + b0                  

若把b(x)乘上x,得到:x·b(x) = b7x8+ b6x7 + x5b6 + b4x5 + b3x4 + b2x3 + b1x2 + b0x

相乘的结果再与m(x)相模的结果有如下规律性:

b7 = 0 (b(x)·x) mod m(x) = b(x) · x

b7 = 0 (b(x)·x) mod m(x) = (b(x)·x) m(x)

由此,(b(x)·x) mod m(x)可以被认为是先进行字节的左移操作,根据移位结果对该字节与“1B(m(x))进行条件异或运算不了。  这种类型的操作记为:b = xtime()

例如:计算63·17 = 58

计算步骤如下:

63·17 = 63·(01 + 02 + 04 + 10= 63·01 + 63·02 + 63 ·04 + 63·10

63·01 = xtime(63) = C6;(01100011 b7=0 左移一位)

63·02 = xtime(C6) = 97;(11000110b7=1,左移一位,异或1B

63·04 = xtime(97) = 35;(10010111b7=1,左移一位,异或1B

63·10 = xtime(35) = 6A(00110101b7=0,左移一位)

所以,63·17 = 63·01 + 63·02 + 63 ·04 + 63·10

                     = 63 + C6 + 97 + 6A

                     =58

这样,通过分解可将复杂的相乘操作转化为简单的移位和异或操作。

 

2GF28)域上的多项式

       在有限域GF28)上的多项式系统定义为取自GF28)元素的多项式,则一个4字节的字对应于一个次数小于4的多项式。

 

21.加法运算

       GF28)上的多项式的加法定义为对应项系数相加,即两个带有系数的多项式之各等于相应系数之和的多项式,在GF28)上的和运算等于异或运算。

 

22.乘法运算

       带有系数的多项式相乘与不带系数的多项式相乘相比,稍为复杂一些。设在GF28)上有两个多项式:

       a(x) = a3x3 + a2x2 + a1x + a0

       b(x) = b3x3 + b2x2+ b1x + b0

       两者关于模x4+1的乘积为:c(x) = a(x) b(x) = c3x3 + c2x2 + c1x + c0

             

       其中系数由下面4个式子得到:

       c0 = a0b0a3b1a2b2a1b3

       c1 = a1b0a0b1a3b2a2b3

       c2 = a2b0a1b1a0b2a3b3

       c3 = a3b0a2b1a1a2a0b3

      

       很显然,一个固定多项式a(x)与多项式b(x)相乘所构成的运算可以用矩阵乘法表述:

        c0           a0   a3   a2   a1       b0

        c1           a1   a0   a3   a2       b1

        c2       =     a2   a1   a0   a3      b2

        c3           a3   a2   a1   a0       b3  

 

       其中的矩阵是一个循环矩阵。应当注意,M(x) = x4 + 1 不是GF(28)上的不可约多项式(也不是GF2)上的不可约多项式),因为x4 + 1 = (x +1)(x3 + x2 + x +1)。所以,被一个固定多项式相乘的乘法不一定是可逆的。在AES算法中,乘法算法只限于乘一个固定的有逆元的多项式a3x3 +b2x2 +b1x + b0才能进行乘法,从而保证乘法的可逆性。

 

 

2  分组密码的设计原则

       分组密码的设计就是找到一种算法,能在密钥的控控制下从一个足够大且足够好的置换子集中简单而迅速的选出一个置换,用来对当前输入的明文进行加密交换。一般地,分组密码的设计准则包括安全性准则和实现性准则两种。前者主要研究如何设计安全算法,分组长度和密钥长度,后者主要讨论如何提高算法的执行速度。

 

2.1  安全性原则

       关于实用密码的两个一般的设计原则是Shannon提出混乱原则和扩散原则。

1.混乱原则:人们所设计的密码应使得密钥和明文以及密文之间的信赖关系相当复杂以至于这种信赖性对密码分析者来说是无法利用。

2.扩散原则:人们所设计密码应使得密钥的每一位数字影响密文的多位数字以防止对密钥进行逐段破译,而且明文的每一位数字也影响密文的多位数字以便隐藏明文数字的统计性性。

       影响现代分组密码安全性的因素很多,主要取决于密码算法、分组长度和密钥长度,相应的安全性设计准则也包括三类:

1.  密码算法的设计准则

1)        算法可以公开:密码体制的安全性应当仅依赖于对密钥的保密,而不应基于对算法的保密,这既是数据加密算法标准化所必需要求的,同时也是网络保密通信赖以生存的基础。

2)        混乱原则:密码算法应当保证密钥,明文和密文的依赖关系相当复杂,以至于这种依赖性对密码分析者来说使无法利用的。

3)        扩散原则:人们所设计的密码应使得密钥的每一位数字影响密文的许多位数字,以便隐藏明文数字的统计特性。

4)        能抵抗差分分析和线性分析:为使密码算法能抵抗差分分析,通常选取具有“本原转移概率矩阵”的markov 型密码,通过对一个“弱”密钥函数进行多次迭代,而得到一个“强密码”,为使密码算法抵抗线性分析,通常要求算法中包含高度非线性密码函数。

5)        没有弱密钥:当弱密钥或半弱密钥的个数较少时,它们对体制本身的安全性影响不在,在算法设计中稍作处理就可避免,而且随即选取一个密钥是弱密钥的概率随密钥空间的增大而趋于0

2.  密钥长度的设计准则

为使密码算法能抵抗对密钥强力攻击,必须保证密钥长度尽可能大,比如,

在近几年来新出现的各种算法,密钥长度都已经要求至少128bits

3.  分组密码长度

为阻止对分组密码进行统计分析,分组长度必须足够大,由于分组密码是一种简单代换密码,而明文有一定的多余度,因此理论上可以对密文进行频率统计分析。当分组长度很大时,这种分析需要大量的密文数据,使得计算上不可行。

 

此外还有一个重要的设计原理:密码体制必须能抵抗现有的所有攻击方法。

 

2.2 实现准则

       分组密码可以用软件和硬件来实现。硬件实现的优点是可获得高速率,而软件实现的优点是灵活性强、代价低。

       软件实现的设计原则是加密和解密过程区别应该在密钥的使用方式不同,以便同样的器件既可用来加密,又可以用来解密。尽量使用规则结构,因为密码应有一个标准的组件结构,以便能适应于用超大规模集成电路实现。

 

3.AES基于java的算法的具体实现

3.1  AES算法的整体结构

3.2  AES算法描述与实现

3.2.1  状态矩阵、密钥、轮数

1.描述

       AES算法是一个分组长度和密钥长度都可变的迭代加密算法,分组长度和密钥长度分可以128bits192bits256bits。在加密过程中,每个数据分组都要进行多次变换操作,每次操后的中间结果称为状态(State),它可以用一个4行、NbNb的值等于数据块长度除以32)列的矩阵来表示状态,矩阵中的每一个元素为一个字节。AES加解密时,先将a0,a1,a2a15复制到状态数组;加密过程都对这个状态进行技术处理;等待加解密过程处理结束,再将状态数组复制到b0,b1, b2b15。最后得到加解密的输出结果。操作映射过程如下:

       其中密钥的设计类似二维字节数组,共4行,Nk列,且Nk等于密钥长度除32,如表5-8所示,AES使用了轮变换,算法变换的轮数NrNbNk共同决定,具体如下表所示:

 

Nr取决于NbNk变化

Nr

Nb = 4

Nb = 6

Nb = 8

Nk=4

10

12

14

Nk=6

12

12

14

Nk=8

14

14

14

 

2.在java中实现得到算法变换的轮数Nr的代码如下:

    /**

     * 这个方法是用来得到加密轮数

     * @param Nb32bit为单位的待加密明文的长度

     * @param Nk是初始密钥的长度

     * @return 返回加密轮数(Nr

     */

    public int GetRounds(int Nb, int Nk) {

       switch (Nb) {

       case 4:

           switch (Nk) {

           case 4:

              return 10;

           case 6:

              return 12;

           case 8:

              return 14;

           default:

              return 0;

           }

       case 6:

           switch (Nk) {

           case 4:

           case 6:

              return 12;

           case 8:

              return 14;

           default:

              return 0;

           }

       case 8:

           switch (Nk) {

           case 4:

           case 6:

           case 8:

              return 14;

           default:

              return 0;

           }

       default:

           return 0;

       }

    }

3.2.2   轮变换

1.描述

AES的轮变换主要由四个不同的变换所组成,分别为:字节替换(ByteSub)、行移位(ShiftRow)、列混合(MixColumn)、加密钥(AddRoundkey)。用代码实现如下:

Round(State, RoundKey) {

    byteSub(state, Nb);

    ShiftRows(state, Nb);

    MixColumns(state, Nb);

    AddRoundKey(state, Nb, round * Nb);}

      

    /*加密算法的最后一圈变换稍为不同,MixColumn这一步去掉*/

    FinalRound(State,RoundKey) {

byteSub(state, Nb);

    ShiftRows(state, Nb);

    AddRoundKey(state, Nb, round * Nb);

    }

以上java代码中,“函数”AddRoundKeybyteSubShiftRowsMixColumns等均在state的字符数组中进行运算。

 

11  字节替换

       字节变换byteSub()是一个关于字节的非线性变换,AES设计的目的就是将状态中的每一个字节非线性地变换为另一个字节。字节变换由变换表(S-Box)完成,它是可逆的,且由两个可逆变换复合而成。其中加密S-BOX用于加密过程,逆S-BOX用于解密过程。

1)        首先将字节看作GF28)上的元素,其值用它的乘法逆代替,“00”的逆为其本身。

2)        其次,经1)处理后的字节进行如下(GF2)上的,可逆的)仿射变换:

 

y0                  1  0  0  0  1  1  1  1        x1       0

y1          1  1  0  0  0  1  1  1        x2       1

y2          1  1  1  0  0  0  1  1        x3       1

y3          1  1  1  1  0  0  0  1        x4       0

y4   =      1  1  1  1  1  0  0  0        x5      0

y5          0  1  1  1  1  1  0  0        x6       0

y6          0  0  1  1  1  1  1  0        x7       1

y7          0  0  0  1  1  1  1  1        x8       1

 

       上述两个了变换合的实现,都在一个AES规定的8bit输入或输出的S盒中进行,javaAES加密算法的S盒如下:

//S盒置换

public static char [] S_BOX = {99,124,119,123,242,107,111,197,48,1,103,43,254,215,171,

118,202,130,201,125,250,89,71,240,173,212,162,175,156,164,

114,192,183,253,147,38,54,63,247,204,52,165,229,241,113,216,

49,21,4,199,35,195,24,150,5,154,7,18,128,226,235,39,178,117,

9,131,44,26,27,110,90,160,82,59,214,179,41,227,47,132,83,209,

0,237,32,252,177,91,106,203,190,57,74,76,88,207,208,239,

170,251,67,77,51,133,69,249,2,127,80,60,159,168,81,163,64,

143,146,157,56,245,188,182,218,33,16,255,243,210,205,12,19,236,

95,151,68,23,196,167,126,61,100,93,25,115,96,129,79,220,

34,42,144,136,70,238,184,20,222,94,11,219,224,50,58,10,73,6,

36,92,194,211,172,98,145,149,228,121,231,200,55,109,141,213,

78,169,108,86,244,234,101,122,174,8,186,120,37,46,28,166,

180,198,232,221,116,31,75,189,139,138,112,62,181,102,72,3,

246,14,97,53,87,185,134,193,29,158,225,248,152,17,105,217,142,

148,155,30,135,233,206,85,40,223,140,161,137,13,191,230,

66,104,65,153,45,15,176,84,187,22,};

    通过查表取ByteSub()输出的方式,若要查找状态中的一个字节xy,选取S盒中第x行第y列对应的字节就是ByteSub()输出。

 

 

12 线性混合层的行移位和列混合

    轮变换在利用S-BOX完成字节替换后得到新的状态矩阵,接着就要利用纯属混合层对新状态矩阵进行线性混合,这个变换层包含行移位和列混合两个子变换层。

 

121  行移位

 

    行移位根据不同的分组长度做相应的循环移位运算。行1不移位,行2c1字节,行3c2字节,行4c3字节。而移位值的大小与加密长度Nb有关,见下表

 

 

不同块长移位值

Nb

C1

C2

C3

4

1

2

3

6

1

2

3

8

1

3

4

   

加密算法中的行移位过程

行移位在java中的实现代码如下:

    /**

     * 对新的状态矩阵根据不同的分组长度做相应的循环移位运算

     * @param state 状态数组(新的状态矩阵).

     * @param Nb Nb是以32比特为单位的明文块的大小

     */

    public void ShiftRows(char[] state, int Nb) {

       char[] t = new char[8];

       for (int r = 0; r < 4; r++) {

           for (int c = 0; c < Nb; c++)

              t[c] = state[Nb * r + (r + c) % Nb];

           for (int c = 0; c < Nb; c++)

              state[Nb * r + c] = t[c];

       }

    }

 

122 列混合变换

    在列混合变换中,给定一个状态,逐列进行变换。它将状态矩阵中的每个列视为系数在GF28)上的一个次数小于4的多项式。并与一固定的多项式c(x)进行模X4+1相乘。其中c(x)为模x4+1可逆的多项式(系数为16进制):

C(x) = {03}x3 + {01}x2 + {01}x + {02}

    显然x2 + 1为非可约多项式,但c(x)x2 + 1互素,故关于模x4 + 1可逆。具体示意理解如下:

                  Si(x)  = S3jx3 + S2i x2 + S1iX + S0i

                     S`i(x) = S’3jx3 + S’2i x2 + S’1iX + S`0i

则有S`i = C(x) Si(x)0i3

       因此,列混合运算可以表示为GF(28)上的可逆线性变换:

        S`0i           02   03  01  01       S0i

        S`1i           01   02  03  01       S1i

        S`2i           =      01   01  02  03       S2i

        S`3i           03   01  01  02       S3i  

 

列混合运算在java中的实现如下:

        /**

     * 对新的状态矩阵根据不同的分组长度做相应的列混合运算

     * @param state 状态数组(新的状态矩阵).

     * @param Nb是以32比特为单位的明文块的大小

     */

    public void MixColumns(char[] state, int Nb) {

       int[] t = new int[4];

       for (int c = 0; c < Nb; c++) {

           for (int r = 0; r < 4; r++)

              t[r] = state[Nb * r + c];

           for (int r = 0; r < 4; r++) {

              state[Nb * r + c] = (char) (Ffmul(0x02, t[r])

                     ^ Ffmul(0x03, t[(r + 1) % 4]) ^ t[(r + 2) % 4] ^ t[(r + 3) % 4]);

           }

       }

    }

13  轮密钥加法变换

    在轮密钥加法操作中,轮密钥简单地地使用按位异或操作到一个状态上,轮密钥通过密钥调度过程,对密码密钥进行扩展而得到,一组轮密钥等于数据块长Nb(4个字节,128)

    具体示意过程如下:

       易得:(S`0i, S`1i, S`2i, S`3i = S0iS1iS2iS3i⊕(k0i, k1i, k2i, k3i),0i3。这里(k0i, k1i, k2i, k3i)为扩展密钥中的第r*Nb + i个字,0rNrAES加密算法规定,初始轮密钥加法变换的 r = 0,最后一轮密钥加法变换r = n

 

轮密钥加法变换java实现如下:

    /**

     * 这个方法是用来做轮密钥加法变换的

     * @param state 状态数组(新的状态矩阵).

     * @param Nb是以32比特为单位的明文块的大小

     * @param round为当前加密的轮数

     */

    public void AddRoundKey(char[] state, int Nb, int round) {

       for (int c = 0; c < Nb; c++, round++)

           for (int r = 0; r < 4; r++)

              /* w为子密钥,Nb为明文块的大小,round为当前加密的轮数;*/

              state[r * Nb + c] = (char) (state[r * Nb + c] ^ w[round * 4 + r]);

    }

 

3.2.3  密钥扩展

密钥扩展的作用是在数据加密/解密前得到轮变换中中使用的轮密钥,其基本原则是:

1)        轮密钥的密钥长度为分组长乘以(1 +Nr),如果分组长为128bit,轮数为了10,则轮密钥的长度为128*10+1= 1408bit

2)        种子密钥扩展为扩展密钥,种子密钥长度为4*NK个字节;

3)        轮密钥由以下方法从扩展密钥中获得:对第1轮密钥由前Nb个字构成;第2轮密钥由第二个Nb个字节即第Nb + 1个字到第2Nb个字构成;以下依次类推。

 

扩展密钥用数组w(Nb*(Nr + 1))表示,前Nk个字是种子密钥,其它密钥字通过递归定义生成。

SubByte(w)是一个返回4个字节的函数,每个字节都是它输入字中相应位置字节通过S盒作用后的结果。

而函数RotBytew)返回的是这4个字节经循环置换后的字,即将该字循环左移一个字节,如输入字为w = (a0,a1,a2,a3),则经RotByte(w)后输出字为w = (a1,a2,a3,a0)

扩展密钥的前Nk个字由种子密钥构成,随后的字w[i]等于字w[i-1]经一些变换后得到的字temp和字W[i-Nk]异或而成;而且对位置为Nk倍数的字变换中不仅运用了循环左移变换RotByte和子字节变换SubByte,还运用了轮常数Rcon对变换后的字temp进行异或。

对轮常数的定义为:

Rcon[i] = (Rc[i]000000)

       Rc[i]表示在有限域GF28)中xi-1的值,即:

              Rc[i] = 1  (01)

              Rc[i]=x·(Rc[i-1]) = xi-1             (考虑x 对应字节为02也可以写成(02i-1

 

总结,整个密钥扩展使用下列变换来完成:

1字节的循环移位RotByte àS盒变换SubByteà异或轮常数Rcon

 

以下是java实现密钥扩展的代码:

    /**

     * 这个方法是用来进行密钥扩展的

     * @param strKey  用户输入的密钥

     * @param Nb 32bit为单位的待加密明文的长度

     * @param Nk 32bit为单位的初始密钥的长度.

     * @param Nr 加密的轮加密的轮.

     * @return 扩展后的子密钥存放在数组w

     */

    public void KeyExpansion(String strKey, int Nb, int Nk, int Nr) {

       int i = 0;

       for (; i < 4; i++)

           for (int j = 0; j < Nk; j++)

              key[i * Nk + j] = strKey.charAt(i * 4 + j);

       i = 0;

       while (i < Nk) {

           w[i * 4] = key[i * 4];

           w[i * 4 + 1] = key[i * 4 + 1];

           w[i * 4 + 2] = key[i * 4 + 2];

           w[i * 4 + 3] = key[i * 4 + 3];

           i++;

       }

       i = Nk;

       while (i < Nb * (Nr + 1)) {

           char[] temp = new char[4];

           temp[0] = w[(i - 1) * 4 + 0];

           temp[1] = w[(i - 1) * 4 + 1];

           temp[2] = w[(i - 1) * 4 + 2];

           temp[3] = w[(i - 1) * 4 + 3];

           if ((i % Nk) == 0) {

              RotByte(temp);

              SubByte(temp);

              for (int j = 0; j < 4; j++)

                  temp[j] ^= Rcon[((i - 1) / Nk) * 4 + j];//Rcon异或;

           } else if (Nk == 8 && i % Nk == 4)

              SubByte(temp);

           w[i * 4 + 0] = (char) (w[(i - Nk) * 4 + 0] ^ temp[0]);

           w[i * 4 + 1] = (char) (w[(i - Nk) * 4 + 1] ^ temp[1]);

           w[i * 4 + 2] = (char) (w[(i - Nk) * 4 + 2] ^ temp[2]);

           w[i * 4 + 3] = (char) (w[(i - Nk) * 4 + 3] ^ temp[3]);

           i++;

       }

    }

3.2.4  AES加密算法

AES的加密算法由三大部分组成,并顺序完成各自的操作:

1)        初始化的轮密钥加法;

2)        Nr - 1)轮变换;

3)        最后一轮变换;

 

加密算法用java实现如下:

    /**

     * 这个方法是在AES加密过程中进行的轮变换,(即加密算法)

     * @param state 是一个包含要加密的明文数组

     * @param Nb 32bit为单位的待加密明文的长度.

     * @param Nr 用户密钥的长度

     * @return 密文数组

     */

    public byte[] Transform(char[] state, int Nb, int Nr) {

       int round = 1;

       /*初始化轮密钥加法*/

       AddRoundKey(state, Nb, 0);

       /*Nr - 1)轮变换*/

       for (; round < Nr; round++) {

           byteSub(state, Nb);

           ShiftRows(state, Nb);

           MixColumns(state, Nb);

           AddRoundKey(state, Nb, round * Nb);

       }

       /*加密算法的最后一圈变换稍为不同,MixColumn这一步去掉*/

       byteSub(state, Nb);

       ShiftRows(state, Nb);

       AddRoundKey(state, Nb, round * Nb);

       return CharToByte(state);

    3.2.5  AES解密算法

       AES解密算法的结构类似于加密算法结构,其中解密算法采用的变换主要是加密算法的逆变换。并且在第1轮到第Nr-1 轮之间字节替换逆变换与移位逆变换,逆列混合逆变换和轮密钥加法逆变换交换了位置。

(1)       移位逆变换InvShiftRow()

(2)       字节替换逆变换InvSubByte()

(3)       轮密钥加法逆变换addRoundKey()

(4)       列混合逆变换InvMixColumns()

 

java实现AES解密代码如下:

    /**

     *  这个方法是在AES解密过程中进行的逆轮变换,(即解密算法)

     * @param state 要解密的密文数组

     * @param Nb 32bit为单位的待加密明文的长度

     * @param Nr 用户密钥的长度

     * @return 解密后的明文数组

     */

    public byte[] ReTransform(char[] state, int Nb, int Nr) {

       AddRoundKey(state, Nb, Nr * Nb);

       for (int round = Nr - 1; round >= 1; round--) {

           InvShiftRows(state, Nb);

           InvByteSub(state, Nb);

           AddRoundKey(state, Nb, round * Nb);

           InvMixColumns(state, Nb);

       }

       InvShiftRows(state, Nb);

       InvByteSub(state, Nb);

       AddRoundKey(state, Nb, 0);

       return CharToByte(state);

    }

 

 

 

AES算法源码下载:http://download.csdn.net/user/oklzh

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值