AES算法笔记

1. Introduction

标准为FIPS-197 2001,1997-2000年期间通过竞赛形式选用Daemen和Rijmen两位参选者的算法Rijndael。

其它参选算法:

  • MARS, by IBM company
  • RC6, by RSA company
  • Serpent, by Anderson, Biham, Knudsen
  • Twofish, by Counterpane company

AES简介:

  • 分组长度:128 bits == 16 bytes.
  • 密钥长度:
    • AES-128, 128 bits == 16 bytes
    • AES-192, 192 bits == 24 bytes
    • AES-256, 256 bits == 32 bytes

原Rijndael算法的分组长度和密钥长度则是支持[128 bits, 256 bits, step = 32 bits]

3. Notation and Conventions

3.2 Bytes

在AES算法中,字节可表示为有限域(finite field)上的多项式(polynomial)。
b 7 x 7 + b 6 x 6 + b 5 x 5 + b 4 x 4 + b 3 x 3 + b 2 x 2 + b 1 x 1 + b 0 = ∑ i = 0 7 b i x i b_7x^7 + b_6x^6 + b_5x^5 + b_4x^4 + b_3x^3 + b_2x^2 + b_1x^1 + b_0 = \sum_{i=0}^{7} b_ix^i b7x7+b6x6+b5x5+b4x4+b3x3+b2x2+b1x1+b0=i=07bixi

输入的比特流在字节中以大端序存储。

3.4 State

AES的操作对象是一个状态(State)二维数组(以字节为单位):

  • 4行,下标记为r;
  • Nb列,Nb * Rows == Block Length,所以Nb取值为16 / 4 == 4,下标记为c;

输入字节和输出结果都是按列进行存储的:

state[r, c] = input[r + 4c];
out[r + 4c] = s[r, c];

在AES中可以把每一列看作一个大端序4字节整型(uint32_t)进行处理,所以Nb既可以理解为列数,也可以理解为word数。

4. Mathematical Preliminaries

Finite field elements can be added and multiplied.

4.1 Addition

其实就是异或操作。

0x57 ^ 0x83 == 0xd4

( x 6 + x 4 + x 2 + x + 1 ) + ( x 7 + x + 1 ) = x 7 + x 6 + x 4 + x 2 (x^6 + x^4 + x^2 + x +1) + (x^7 + x +1) = x^7 + x^6 + x^4 + x^2 (x6+x4+x2+x+1)+(x7+x+1)=x7+x6+x4+x2

4.2 Multiplication

In the polynomial representation, multiplication in GF(28) (denoted by •) corresponds with the multiplication of polynomials modulo an irreducible polynomial(不可约多项式) of degree 8. A polynomial is irreducible if its only divisors(因子) are one and itself.

AES中的不可约多项式如下:
m ( x ) = x 8 + x 4 + x 3 + x 1 + 1 m(x) = x^8 + x^4 + x^3 + x^1 + 1 m(x)=x8+x4+x3+x1+1
也可记为0x011B(==0b100011011)。

The modular reduction by m(x) ensures that the result will be a binary polynomial of degree less than 8, and thus can be represented by a byte.

至于有限域求乘法逆元,则需要用到扩展欧几里得算法(the extended Euclidean algorithm)。

4.2.1 Multiplication by x-xtime()

将字节的多项式乘以x,则变成了如下公式:
b 7 x 8 + b 6 x 7 + b 5 x 6 + b 4 x 5 + b 3 x 4 + b 2 x 3 + b 1 x 2 + b 0 x b_7x^8 + b_6x^7 + b_5x^6 + b_4x^5 + b_3x^4 + b_2x^3 + b_1x^2 + b_0x b7x8+b6x7+b5x6+b4x5+b3x4+b2x3+b1x2+b0x
乘以x,在代码中其实就是左移一位。

若b71,则与AES中的不可约多项式m(x)异或;若b70,则上式就是最简形式。这种操作在AES中叫做xtime()。

以0x57乘以所有高次幂多项式为例:

a = 0x57
mx = 0x011b
print("0x57 * 0x1 == 0x57")
for i in range(1, 8):
    a = a << 1;
    print("0x57 * %s == xtime(%s)" %(hex(2**i), hex(a>>1)), end="")
    if(a & 0x0100):
        print(" == %s ^ %s->%s" % (hex(a), hex(mx), hex(a^mx)))
        a = a ^ mx
    else:
        print(" == %s" % hex(a))
# 0x57 * 0x1 == 0x57
# 0x57 * 0x2 == xtime(0x57) == 0xae
# 0x57 * 0x4 == xtime(0xae) == 0x15c ^ 0x11b->0x47
# 0x57 * 0x8 == xtime(0x47) == 0x8e
# 0x57 * 0x10 == xtime(0x8e) == 0x11c ^ 0x11b->0x7
# 0x57 * 0x20 == xtime(0x7) == 0xe
# 0x57 * 0x40 == xtime(0xe) == 0x1c
# 0x57 * 0x80 == xtime(0x1c) == 0x38

0x57乘以0x13的结果可以用结合律计算:

0x57 * 0x13 
= 0x57 * (0x01 ^ 0x02 ^ 0x10)
= 0x57 ^ 0xae ^ 0x07 
= 0xfe

换成c语言实现:

#include <stdio.h>
#include <stdint.h>

uint8_t xtime(uint16_t a,  uint8_t b)
{
	static const uint16_t mx = 0x011B;
	uint16_t nRet = 0;
	int i = 0;
	for (i = 0; i < 8; ++i)
	{
		if (b & 0x01)
		{
			nRet ^= a;
		}
		b >>= 1;
		a <<= 1;
		if (a & 0x0100)	// x^8
		{
			a ^= mx;
		}
	}
	return nRet & 0xFF;
}

int main()
{
	uint8_t a = xtime(0x57, 0x13);
	printf("%x\n", a);	// fe
	return 0;
}

4.3 Polynomials with Coefficients in GF(28)

有限域也适用于字(word,4字节整型)操作。
a ( x ) = a 3 x 3 + a 2 x 2 + a 1 x + a 0 b ( x ) = b 3 x 3 + b 2 x 2 + b 1 x + b 0 a ( x ) + b ( x ) = ( a 3 + b 3 ) x 3 + ( a 2 + b 2 ) x 2 + ( a 1 + b 1 ) x + ( a 0 + b 0 ) c ( x ) = a ( x ) ∗ b ( x ) = c 6 x 6 + c 5 x 5 + c 4 x 4 + c 3 x 3 + c 2 x 2 + c 1 x + c 0 a(x) = a_3 x^3 + a_2 x^2 + a_1 x + a_0 \\ b(x) = b_3 x^3 + b_2 x^2 + b_1 x + b_0 \\ a(x) + b(x) = (a_3 + b_3) x^3 + (a_2+ b_2) x^2 + (a_1+ b_1) x + (a_0+ b_0) \\ c(x) = a(x) * b(x) = c_6 x^6 + c_5 x^5 + c_4 x^4 + c_3 x^3 + c_2 x ^2 + c_1 x + c_0 a(x)=a3x3+a2x2+a1x+a0b(x)=b3x3+b2x2+b1x+b0a(x)+b(x)=(a3+b3)x3+(a2+b2)x2+(a1+b1)x+(a0+b0)c(x)=a(x)b(x)=c6x6+c5x5+c4x4+c3x3+c2x2+c1x+c0
其中乘法c(x)无法表示为4字节整型,在AES中需要取余操作:
x i m o d ( x 4 + 1 ) = x i m o d 4 x^i mod(x^4 +1) = x^{i mod 4} ximod(x4+1)=ximod4
计算的结果为d(x):
d ( x ) = d 3 x 3 + d 2 x 2 + d 1 x + d 0 d 0 = ( a 0 ∗ b 0 ) + ( a 3 ∗ b 1 ) + ( a 2 ∗ b 2 ) + ( a 1 ∗ b 3 ) = c 4 + c 0 d 1 = ( a 1 ∗ b 0 ) + ( a 0 ∗ b 1 ) + ( a 3 ∗ b 2 ) + ( a 2 ∗ b 3 ) = c 5 + c 1 d 2 = ( a 2 ∗ b 0 ) + ( a 1 ∗ b 1 ) + ( a 0 ∗ b 2 ) + ( a 3 ∗ b 3 ) = c 6 + c 2 d 3 = ( a 3 ∗ b 0 ) + ( a 2 ∗ b 1 ) + ( a 1 ∗ b 2 ) + ( a 0 ∗ b 3 ) = c 3 d(x) = d_3 x^3 + d_2 x^2 + d_1 x + d_0 \\ d_0 = (a_0 * b_0 ) + (a_3 * b_1 ) + (a_2 * b_2 ) + (a_1 * b_3 ) =c_4+c_0 \\ d_1 = (a_1 * b_0 ) + (a_0 * b_1 ) + (a_3 * b_2 ) + (a_2 * b_3 ) =c_5+c_1 \\ d_2 = (a_2 * b_0 ) + (a_1 * b_1 ) + (a_0 * b_2 ) + (a_3 * b_3 ) =c_6+c_2 \\ d_3 = (a_3 * b_0 ) + (a_2 * b_1 ) + (a_1 * b_2 ) + (a_0 * b_3 ) =c_3 \\ d(x)=d3x3+d2x2+d1x+d0d0=(a0b0)+(a3b1)+(a2b2)+(a1b3)=c4+c0d1=(a1b0)+(a0b1)+(a3b2)+(a2b3)=c5+c1d2=(a2b0)+(a1b1)+(a0b2)+(a3b3)=c6+c2d3=(a3b0)+(a2b1)+(a1b2)+(a0b3)=c3

如果a(x)是一个固定的多项式,则可以写成矩阵(matrix)形式:

[ d 0 d 1 d 2 d 3 ] = [ a 0 a 3 a 2 a 1 a 1 a 0 a 3 a 2 a 2 a 1 a 0 a 3 a 3 a 2 a 1 a 0 ] [ b 0 b 1 b 2 b 3 ] \begin{bmatrix} d_0 \\ d_1 \\ d_2 \\ d_3 \end{bmatrix}= \begin{bmatrix} a_0 & a_3 & a_2 & a_1 \\ a_1 & a_0 & a_3 & a_2 \\ a_2 & a_1 & a_0 & a_3 \\ a_3 & a_2 & a_1 & a_0 \\ \end{bmatrix} \begin{bmatrix} b_0 \\ b_1 \\ b_2 \\ b_3 \end{bmatrix} d0d1d2d3 = a0a1a2a3a3a0a1a2a2a3a0a1a1a2a3a0 b0b1b2b3

x4+1 is not an irreducible polynomial over GF(28), multiplication by a fixed four-term polynomial is not necessarily invertible.

乘法运算,一共256*256种情况,可以提前将运算结果硬编码为数组,前提是确保所运行的平台有足够的空间。

5. Algorithm Specification

重申几个常量和变量:

  • 分组长度:128 bits == 16 bytes == Nb words(4).
  • 密钥长度:
    • AES-128, 128 bits == 16 bytes == Nk words(4)
    • AES-192, 192 bits == 24 bytes == Nk words(6)
    • AES-256, 256 bits == 32 bytes == Nk words(8)
  • 轮数:
    • AES-128, Nr==10
    • AES-192, Nr==12
    • AES-256, Nr==14

加密和解密的轮函数(round function)流程可以简述为4步:

  1. byte substitution using a substitution table (S-box)
  2. shifting rows of the State array by different offsets
  3. mixing the data within each column of the State array
  4. adding a Round Key to the State.

5.1 Cipher

官方伪代码:

Cipher(byte in[4*Nb], byte out[4*Nb], word w[Nb*(Nr+1)])
begin
    byte state[4,Nb]
    state = in
    AddRoundKey(state, w[0, Nb-1]) // See Sec. 5.1.4
    for round = 1 step 1 to Nr–1
        SubBytes(state) // See Sec. 5.1.1
        ShiftRows(state) // See Sec. 5.1.2
        MixColumns(state) // See Sec. 5.1.3
        AddRoundKey(state, w[round*Nb, (round+1)*Nb-1])
    end for
    SubBytes(state)
    ShiftRows(state)
    AddRoundKey(state, w[Nr*Nb, (Nr+1)*Nb-1])
    out = state
end

5.1.1 SubBytes()Transformation

通过一个16*16的S-box(substitution table),进行字节替换。

State数组元素的高4位为行索引,低4位为列索引。

static const uint8_t sbox[16][16] = {
{0x63,0x7C,0x77,0x7B,0xF2,0x6B,0x6F,0xC5,0x30,0x01,0x67,0x2B,0xFE,0xD7,0xAB,0x76},	{0xCA,0x82,0xC9,0x7D,0xFA,0x59,0x47,0xF0,0xAD,0xD4,0xA2,0xAF,0x9C,0xA4,0x72,0xC0},
{0xB7,0xFD,0x93,0x26,0x36,0x3F,0xF7,0xCC,0x34,0xA5,0xE5,0xF1,0x71,0xD8,0x31,0x15},
{0x04,0xC7,0x23,0xC3,0x18,0x96,0x05,0x9A,0x07,0x12,0x80,0xE2,0xEB,0x27,0xB2,0x75},
{0x09,0x83,0x2C,0x1A,0x1B,0x6E,0x5A,0xA0,0x52,0x3B,0xD6,0xB3,0x29,0xE3,0x2F,0x84},
{0x53,0xD1,0x00,0xED,0x20,0xFC,0xB1,0x5B,0x6A,0xCB,0xBE,0x39,0x4A,0x4C,0x58,0xCF},
{0xD0,0xEF,0xAA,0xFB,0x43,0x4D,0x33,0x85,0x45,0xF9,0x02,0x7F,0x50,0x3C,0x9F,0xA8},
{0x51,0xA3,0x40,0x8F,0x92,0x9D,0x38,0xF5,0xBC,0xB6,0xDA,0x21,0x10,0xFF,0xF3,0xD2},
{0xCD,0x0C,0x13,0xEC,0x5F,0x97,0x44,0x17,0xC4,0xA7,0x7E,0x3D,0x64,0x5D,0x19,0x73},
{0x60,0x81,0x4F,0xDC,0x22,0x2A,0x90,0x88,0x46,0xEE,0xB8,0x14,0xDE,0x5E,0x0B,0xDB},
{0xE0,0x32,0x3A,0x0A,0x49,0x06,0x24,0x5C,0xC2,0xD3,0xAC,0x62,0x91,0x95,0xE4,0x79},
{0xE7,0xC8,0x37,0x6D,0x8D,0xD5,0x4E,0xA9,0x6C,0x56,0xF4,0xEA,0x65,0x7A,0xAE,0x08},
{0xBA,0x78,0x25,0x2E,0x1C,0xA6,0xB4,0xC6,0xE8,0xDD,0x74,0x1F,0x4B,0xBD,0x8B,0x8A},{0x70,0x3E,0xB5,0x66,0x48,0x03,0xF6,0x0E,0x61,0x35,0x57,0xB9,0x86,0xC1,0x1D,0x9E},
{0xE1,0xF8,0x98,0x11,0x69,0xD9,0x8E,0x94,0x9B,0x1E,0x87,0xE9,0xCE,0x55,0x28,0xDF},
{0x8C,0xA1,0x89,0x0D,0xBF,0xE6,0x42,0x68,0x41,0x99,0x2D,0x0F,0xB0,0x54,0xBB,0x16}
};

5.1.2 ShiftRows() Transformation

每行以字节为单位循环左移:

  • 第1行不变
  • 第2行循环左移1字节
  • 第3行循环左移2字节
  • 第4行循环左移3字节

5.1.3 MixColumns() Transformation

以列为单位,把每列看成一个在GF(28)域上的多项式,计算公式如下:

S'(x) = a(x) * s(x) mod (x^4+1)

其中a(x)为固定的多项式:
a ( x ) = { 03 } x 3 + { 01 } x 2 + { 01 } x + { 02 } [ 02 03 01 01 01 02 03 01 01 01 02 03 03 01 01 02 ] a(x) = \{03\} x^3 + \{01\} x^2 + \{01\} x + \{02\} \\ \begin{bmatrix} 02 & 03 & 01 & 01 \\ 01 & 02 & 03 & 01 \\ 01 & 01 & 02 & 03 \\ 03 & 01 & 01 & 02 \\ \end{bmatrix} a(x)={03}x3+{01}x2+{01}x+{02} 02010103030201010103020101010302
然后将a(x)套用到4.3的公式就行了。

5.1.4 AddRoundKey() Transformation

将State中每列4字节(即一个word)与轮密钥(Round Key)相加(异或)。对应轮密钥下标如下:

round*Nb+c
0 <= round <= Nr
0 <= c <= Nb

Each Round Key consists of Nb words from the key schedule (described in Sec.5.2).

5.2 Key Expansion

The AES algorithm takes the Cipher Key, K, and performs a Key Expansion routine to generate a key schedule.

伪代码:

KeyExpansion(byte key[4*Nk], word w[Nb*(Nr+1)], Nk)
// Nb*(Nr+1) == 
// 4 * 11
// 4 * 13
// 4 * 15
begin
    word temp
    i = 0
    while (i < Nk)	// Nk == 4/6/8 words
        w[i] = word(key[4*i], key[4*i+1], key[4*i+2], key[4*i+3])
        i = i+1
    end while
    i = Nk
	while (i < Nb * (Nr+1))
        temp = w[i-1]
        if (i mod Nk = 0)
        	temp = SubWord(RotWord(temp)) xor Rcon[i/Nk]
        else if (Nk > 6 and i mod Nk = 4)
        	temp = SubWord(temp)
        end if
        w[i] = w[i-Nk] xor temp
        i = i + 1
    end while
end

SubWord()将一个word根据S-Box进行替换。

RotWord()将一个word循环左移1字节。

Rcon[i], The round constant word array, contains the values given by [xi-1,{00},{00},{00}], as discussed in Sec. 4.2. i starts at 1.

a = 0x01
mx = 0x011b
listRcon = []
for i in range(15):
    # print("0x57 * %s == %s" %(hex(i+1), hex(a)) )
    if(a & 0x0100):
        a = a ^ mx
        # print("^ mx->%s" % hex(a))
    listRcon.append(a << 24)
    a = a << 1;

for i in listRcon:
    print("0x%08x" % i, end=",")
# 0x01000000,0x02000000,0x04000000,0x08000000,0x10000000,
# 0x20000000,0x40000000,0x80000000,0x1b000000,0x36000000,
# 0x6c000000,0xd8000000,0xab000000,0x4d000000,0x9a000000,

原文中Rcon下标从1开始,所以为代码中的Rcon[i/Nk]在实现时应该改成Rcon[(i-1)/Nk]

5.3 Inverse Cipher

解密流程官方伪代码:

InvCipher(byte in[4*Nb], byte out[4*Nb], word w[Nb*(Nr+1)])
begin
    byte state[4,Nb]
    state = in
    AddRoundKey(state, w[Nr*Nb, (Nr+1)*Nb-1]) // See Sec. 5.1.4
    for round = Nr-1 step -1 downto 1
        InvShiftRows(state) // See Sec. 5.3.1
        InvSubBytes(state) // See Sec. 5.3.2
        AddRoundKey(state, w[round*Nb, (round+1)*Nb-1])
        InvMixColumns(state) // See Sec. 5.3.3
    end for
    InvShiftRows(state)
    InvSubBytes(state)
    AddRoundKey(state, w[0, Nb-1])
    out = state
end

InvShiftRows()是ShiftRows()的逆过程。

InvSubBytes()依赖于inverse S-box(逆替换表)。

static const uint8_t aes_invsbox[16][16] = {
{0x52,0x09,0x6A,0xD5,0x30,0x36,0xA5,0x38,0xBF,0x40,0xA3,0x9E,0x81,0xF3,0xD7,0xFB},
{0x7C,0xE3,0x39,0x82,0x9B,0x2F,0xFF,0x87,0x34,0x8E,0x43,0x44,0xC4,0xDE,0xE9,0xCB},
{0x54,0x7B,0x94,0x32,0xA6,0xC2,0x23,0x3D,0xEE,0x4C,0x95,0x0B,0x42,0xFA,0xC3,0x4E},
{0x08,0x2E,0xA1,0x66,0x28,0xD9,0x24,0xB2,0x76,0x5B,0xA2,0x49,0x6D,0x8B,0xD1,0x25},
{0x72,0xF8,0xF6,0x64,0x86,0x68,0x98,0x16,0xD4,0xA4,0x5C,0xCC,0x5D,0x65,0xB6,0x92},
{0x6C,0x70,0x48,0x50,0xFD,0xED,0xB9,0xDA,0x5E,0x15,0x46,0x57,0xA7,0x8D,0x9D,0x84},
{0x90,0xD8,0xAB,0x00,0x8C,0xBC,0xD3,0x0A,0xF7,0xE4,0x58,0x05,0xB8,0xB3,0x45,0x06},
{0xD0,0x2C,0x1E,0x8F,0xCA,0x3F,0x0F,0x02,0xC1,0xAF,0xBD,0x03,0x01,0x13,0x8A,0x6B},
{0x3A,0x91,0x11,0x41,0x4F,0x67,0xDC,0xEA,0x97,0xF2,0xCF,0xCE,0xF0,0xB4,0xE6,0x73},
{0x96,0xAC,0x74,0x22,0xE7,0xAD,0x35,0x85,0xE2,0xF9,0x37,0xE8,0x1C,0x75,0xDF,0x6E},
{0x47,0xF1,0x1A,0x71,0x1D,0x29,0xC5,0x89,0x6F,0xB7,0x62,0x0E,0xAA,0x18,0xBE,0x1B},
{0xFC,0x56,0x3E,0x4B,0xC6,0xD2,0x79,0x20,0x9A,0xDB,0xC0,0xFE,0x78,0xCD,0x5A,0xF4},
{0x1F,0xDD,0xA8,0x33,0x88,0x07,0xC7,0x31,0xB1,0x12,0x10,0x59,0x27,0x80,0xEC,0x5F},
{0x60,0x51,0x7F,0xA9,0x19,0xB5,0x4A,0x0D,0x2D,0xE5,0x7A,0x9F,0x93,0xC9,0x9C,0xEF},
{0xA0,0xE0,0x3B,0x4D,0xAE,0x2A,0xF5,0xB0,0xC8,0xEB,0xBB,0x3C,0x83,0x53,0x99,0x61},
{0x17,0x2B,0x04,0x7E,0xBA,0x77,0xD6,0x26,0xE1,0x69,0x14,0x63,0x55,0x21,0x0C,0x7D}
};

InvMixColumns()依赖于a(x)的逆多项式a-1(x)
a − 1 ( x ) = { 0 b } x 3 + { 0 d } x 2 + { 09 } x + { 0 e } [ 0 e 0 b 0 d 09 09 0 e 0 b 0 d 0 d 09 0 e 0 b 0 b 0 d 09 0 e ] a^{-1}(x) = \{0b\} x^3 + \{0d\} x^2 + \{09\} x + \{0e\} \\ \begin{bmatrix} 0e & 0b & 0d & 09 \\ 09 & 0e & 0b & 0d \\ 0d & 09 & 0e & 0b \\ 0b & 0d & 09 & 0e \\ \end{bmatrix} a1(x)={0b}x3+{0d}x2+{09}x+{0e} 0e090d0b0b0e090d0d0b0e09090d0b0e

5.3.5 Equivalent Inverse Cipher

上述解密流程的置换顺序和加密有些不同,但使用了同样的key schedules。

通过修改key schedules,也可以用加密中的置换流程实现解密,并且效率更高。

EqInvCipher(byte in[4*Nb], byte out[4*Nb], word dw[Nb*(Nr+1)])
begin
   byte state[4,Nb]
   state = in
   AddRoundKey(state, dw[Nr*Nb, (Nr+1)*Nb-1])
   for round = Nr-1 step -1 downto 1
      InvSubBytes(state)
      InvShiftRows(state)
      InvMixColumns(state)
      AddRoundKey(state, dw[round*Nb, (round+1)*Nb-1])
   end for
   InvSubBytes(state)
   InvShiftRows(state)
   AddRoundKey(state, dw[0, Nb-1])
   out = state
end

新的密钥扩展流程(Key Expansion routine):

for i = 0 step 1 to (Nr+1)*Nb-1
dw[i] = w[i]
end for
for round = 1 step 1 to Nr-1
   InvMixColumns(dw[round*Nb, (round+1)*Nb-1]) // note change of type
   // two-dimensional array of bytes
   // -->
   // one-dimensional array of words
end for

实现

https://github.com/C0deStarr/CryptoImp/tree/main/Cipher/BlockCipher

  • aes.h
  • aes.c
  • gf_mul.h
  • gf_mul.c

参考资料

FIPS 197, Advanced Encryption Standard | CSRC (nist.gov)

https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值