03_DES原理

DES原理及源码分析

一、DES加密步骤

​ 这里以一个分组为例:

  • 明文为:0123456789ABCDEF这个是HEX编码之后的数据
  • 密钥为:133457799BBCDFF1
1. 子密钥生成

​ 首先将密钥转成二进制:

00110001 00110011 00110011 00110100 00110101 00110111 00110111 00111001 00111001 01000010 01000010 01000011 01000100 01000110 01000110 00110001
  • 根据PC1表进行对密钥的重新排列:
int PC1_Table[56] = 
{
        57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18,
        10, 2, 59, 51, 43, 35, 27, 19, 11, 3, 60, 52, 44, 36,
        63, 55, 47, 39, 31, 23, 15, 7, 62, 54, 46, 38, 30, 22,
        14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 28, 20, 12, 4
};

​ 这里可以看到PC1表当中虽然只有56个元素,但是出现了63这些大于56的索引,实际上是因为密钥当中mod 8 = 0的位置作为奇偶校验位,是不取的,这个PC1当中就少了8,16,24,32,40,48,56,64这8个索引,而且这个表的索引值是从1开始的。

​ 首先会遍历这个PC1表,根据当中的索引值在原来密钥二进制串当中找到对应位置的比特位,然后按顺序编排(也就是密钥当中57的比特位放到第1位,49的放到第2位,以此类推)。然后编排成下面这个结果:

1111000 0110011 0010101 0101111 0101010 1011001 1001111 0001111

​ 此时就只剩下56位了,并且每组7位。然后分成左右两个部分:

C0 = 1111000 0110011 0010101 0101111
D0 = 0101010 1011001 1001111 0001111

​ 接着根据循环左移规定的位数,得到C1D0C16D16

key_shift = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1]

经过循环左移的C1D1是这样的:

C1 = 1110000 1100110 0101010 1011111
D1 = 1010101 0110011 0011110 0011110

​ 接着将C1D1拼接起来进行PC2表的置换:

1110000 1100110 0101010 1011111 1010101 0110011 0011110 0011110

PC2表:

int PC2_Table[48] = {
        14, 17, 11, 24, 1, 5, 3, 28, 15, 6, 21, 10,
        23, 19, 12, 4, 26, 8, 16, 7, 27, 20, 13, 2,
        41, 52, 31, 37, 47, 55, 30, 40, 51, 45, 33, 48,
        44, 49, 39, 56, 34, 53, 46, 42, 50, 36, 29, 32
};

此时就只剩下48位了,经过PC2重排CnDn的子密钥成为Kn,用作DES后续的16轮运算中,第n轮就是用Kn, 16个子密钥为:

K1 = 000110 110000 001011 101111 111111 000111 000001 110010
K2 = 011110 011010 111011 011001 110110 111100 100111 100101
K3 = 010101 011111 110010 001010 010000 101100 111110 011001
K4 = 011100 101010 110111 010110 110110 110011 010100 011101
K5 = 011111 001110 110000 000111 111010 110101 001110 101000
K6 = 011000 111010 010100 111110 010100 000111 101100 101111
K7 = 111011 001000 010010 110111 111101 100001 100010 111100
K8 = 111101 111000 101000 111010 110000 010011 101111 111011
K9 = 111000 001101 101111 101011 111011 011110 011110 000001
K10 = 101100 011111 001101 000111 101110 100100 011001 001111
K11 = 001000 010101 111111 010011 110111 101101 001110 000110
K12 = 011101 010111 000111 110101 100101 000110 011111 101001
K13 = 100101 111100 010111 010001 111110 101011 101001 000001
K14 = 010111 110100 001110 110111 111100 101110 011100 111010
K15 = 101111 111001 000110 001101 001111 010011 111100 001010
K16 = 110010 110011 110110 001011 000011 100001 011111 110101
2. 明文编排

​ 先将明文(已经在内存中的值)转成二进制:

00000001 00100011 01000101 01100111 10001001 10101011 11001101 11101111

​ 根据IP表进行初始置换:

int IP_Table[64] = {
        58, 50, 42, 34, 26, 18, 10, 2, 60, 52, 44, 36, 28, 20, 12, 4,
        62, 54, 46, 38, 30, 22, 14, 6, 64, 56, 48, 40, 32, 24, 16, 8,
        57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3,
        61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7
};

​ 明文重新排列后的结果:

11001100 00000000 11001100 11111111 11110000 10101010 11110000 10101010
3. 明文的运算

​ 首先将明文分成左右两部分L0 和 R0:

L0:11001100 00000000 11001100 11111111 
R0:11110000 10101010 11110000 10101010

接着进行16轮Feistel的迭代,逻辑如下图:
img

每轮迭代的公式如下:

\(L_i = R_{i-1}\)

$R_i = L_{i-1} \oplus F(R_{i-1},K_i)$
F函数:

这里传入F函数传入的是\(R_{i-1}\)还有\(K_{i}\),但是\(R_{i-1}\)长度为32比特位,而K为48比特位,此时就需要对\(R_{i-1}\)进行扩展了,这里扩展使用的是E表:

int E_Table[48] = {
        32, 1, 2, 3, 4, 5, 4, 5, 6, 7, 8, 9,
        8, 9, 10, 11, 12, 13, 12, 13, 14, 15, 16, 17,
        16, 17, 18, 19, 20, 21, 20, 21, 22, 23, 24, 25,
        24, 25, 26, 27, 28, 29, 28, 29, 30, 31, 32, 1
};

扩展之后的密钥称为E(R0),然后和K进行异或操作,也就是\(E(R0) \oplus K_i\)

E(R0)	=	011110 100001 010101 010101 011110 100001 010101 010101
K1		=	000110 110000 001011 101111 111111 000111 000001 110010
K1^E(R0)=	011000 010001 011110 111010 100001 100110 010100 100111

​ 紧接着会将这48位分成8组,每组6位:

B1 = 011000
B2 = 010001
B3 = 011110 
B4 = 111010 
B5 = 100001
B6 = 100110 
B7 = 010100 
B8 = 100111

接着在S盒当中查表,查表规则如下,\(B_i\)当中的i就是查哪张表,然后6位二进制的\(b_0b_5\)作为行索引,中间四位当作列索引在S盒当中查表:

int S_Box[8][4][16] = {
    // S1
    14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7,
    0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8,
    4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0,
    15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13,
    // S2
    15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10,
    3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5,
    0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15,
    13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9,
    // S3
    10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8,
    13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1,
    13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7,
    1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12,
    // S4
    7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15,
    13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9,
    10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4,
    3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14,
    // S5
    2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9,
    14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6,
    4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14,
    11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3,
    // S6
    12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11,
    10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8,
    9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6,
    4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13,
    // S7
    4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1,
    13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6,
    1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2,
    6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12,
    // S8
    13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7,
    1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2,
    7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8,
    2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11
};

最终原先的8比特会变成4比特。上面的48位经过查表之后会变成:

0101 1100 1000 0010 1011 0101 1001 0111

得到的这个结果再经过P表的重排得到的值就是F函数的输出

int P_Table[32] = {
        16, 7, 20, 21, 29, 12, 28, 17, 1, 15, 23, 26, 5, 18, 31, 10,
        2, 8, 24, 14, 32, 27, 3, 9, 19, 13, 30, 6, 22, 11, 4, 25
};

新值:

0010 0011 0100 1010 1010 1001 1011 1011

有这个公式:\(F = P(S(K_i \oplus R_{i-1})\)

经过16轮之后得到如下的数据:

L16:01000011 01000010 00110010 00110100
R16:00001010 01001100 11011001 10010101

然后按照\(R_{16}L_{16}\)的方式拼接,得到的二进制比特串与FP表进行置换:

int FP_Table[64] = {
        40, 8, 48, 16, 56, 24, 64, 32, 39, 7, 47, 15, 55, 23, 63, 31,
        38, 6, 46, 14, 54, 22, 62, 30, 37, 5, 45, 13, 53, 21, 61, 29,
        36, 4, 44, 12, 52, 20, 60, 28, 35, 3, 43, 11, 51, 19, 59, 27,
        34, 2, 42, 10, 50, 18, 58, 26, 33, 1, 41, 9, 49, 17, 57, 25
};

这个FP表的置换其实就是初始置换的逆运算,将上面拼接后的01比特串经过置换之后就会输出:

85 e8 13 54 0f 0a b4 05

这个就是单个分组经过DES加密之后的结果:

img

后半部分是填充。

二、DES源码分析

​ DES加密的源码有非常多不同的版本,只要能加密出来就可以,下面是随便找的一个DES加密源码。这里使用的是一个分组,ECB的模式加密。

1. C调用DES
unsigned char key[8] = "12345678";
unsigned char data[8] = "12345678";

generateSubKeys(key);				// 生成子密钥

processBlock(data, 1);				// 加密
printHex("加密结果", data, 8);		 // 输出hex
2. 子密钥生成函数
void generateSubKeys(const unsigned char* key) {
    unsigned char K56[7]; // PC1置换后的56位密钥
    memset(K56, 0, 7);
    permute(key, K56, PC1_Table, 56);

    // 将56位分为 C (前28位) 和 D (后28位)
    unsigned char C[4], D[4];
    memset(C, 0, 4);
    memset(D, 0, 4);

    // 提取 C 和 D
    for (int i = 0; i < 28; i++) setBit(C, i, getBit(K56, i));
    for (int i = 0; i < 28; i++) setBit(D, i, getBit(K56, i + 28));

    // 生成16个K
    for (int round = 0; round < 16; round++) {
        // 1. 循环左移
        leftRotate(C, SHIFT_Table[round]);
        leftRotate(D, SHIFT_Table[round]);

        // 2. 合并 C 和 D
        unsigned char CD[7];
        memset(CD, 0, 7);
        for (int i = 0; i < 28; i++) setBit(CD, i, getBit(C, i));
        for (int i = 0; i < 28; i++) setBit(CD, i + 28, getBit(D, i));

        // 3. PC2 置换生成子密钥
        memset(subKeys[round], 0, 6);
        permute(CD, subKeys[round], PC2_Table, 48);
    }
}

​ 首先通过permute将key进行PC1置换,并将结果存储到K56当中。接着通过setBitK56分成两组存到C,D当中。接着就是16轮循环来生成16个K值。

permute函数如下:

void permute(const unsigned char* input, unsigned char* output, const int* table, int n) 
{
    memset(output, 0, (n + 7) / 8);				
    for (int i = 0; i < n; i++) {
        // DES表是从1开始计数的,所以要减1
        int val = getBit(input, table[i] - 1);
        setBit(output, i, val);
    }
}

​ 先清空output,这里用(n+7)/8是因为,传入的n是比特位的数量,如果超过n/8字节则需要再多一个字节,这里直接加7为了就是加上这个多的字节。

​ 接着就是使用getBit从input当中获取对应位的比特了,getBit函数如下:

int getBit(const unsigned char* data, int pos) 
{
    int bytePos = pos / 8;
    int bitPos = 7 - (pos % 8);
    return (data[bytePos] >> bitPos) & 1;
}

这里取对应的比特位是通过先/8获取所在字节位置,再通过位移之后&1来获取对应的比特位的。同理,setBit的原理也差不多:

void setBit(unsigned char* data, int pos, int val) 
{
    int bytePos = pos / 8;
    int bitPos = 7 - (pos % 8); // 大端序
    if (val)
        data[bytePos] |= (1 << bitPos);
    else
        data[bytePos] &= ~(1 << bitPos);
}

获取到对应的比特位之后,通过或运算和与运算来控制对应位是0还是1。

​ 子密钥生成的最终过程大致就如上面一样。最终会生成16个K存放到subKeys当中

3. 加密过程
void processBlock(unsigned char* block, int isEncrypt) 
{
    // 1. 初始置换 IP
    unsigned char ipBlock[8];
    memset(ipBlock, 0, 8);
    permute(block, ipBlock, IP_Table, 64);

    // 2. 分割为 L 和 R (各32位)
    unsigned char L[4], R[4];
    unsigned char nextL[4], nextR[4];

    for (int i = 0; i < 32; i++) 
    {
        setBit(L, i, getBit(ipBlock, i));
        setBit(R, i, getBit(ipBlock, i + 32));
    }

    // 3. 16轮迭代
    for (int round = 0; round < 16; round++) 
    {
        // 确定使用哪个子密钥
        // 加密: 0 -> 15, 解密: 15 -> 0
        int keyIndex = isEncrypt ? round : (15 - round);

        // L(n) = R(n-1)
        memcpy(nextL, R, 4);

        // R(n) = L(n-1) XOR f(R(n-1), K(n))
        unsigned char fResult[4];
        feistelFunc(R, subKeys[keyIndex], fResult);
        xorBytes(L, fResult, nextR, 4);

        // 更新 L, R
        memcpy(L, nextL, 4);
        memcpy(R, nextR, 4);
    }

    // 4. 最后一轮后交换 L 和 R (或者说是R16L16合并)
    // 标准DES描述中最后输出是 R16 L16 (也就是不交换,直接反序合并)
    unsigned char finalBlock[8];
    for (int i = 0; i < 32; i++) setBit(finalBlock, i, getBit(R, i));
    for (int i = 0; i < 32; i++) setBit(finalBlock, i + 32, getBit(L, i));

    // 5. 逆初始置换 IP^-1
    permute(finalBlock, block, IP_Inv_Table, 64);
}

​ 首先就是先通过permute将分组明文进行IP置换,然后存储到ipBlock当中,然后分组存放到L和R当中。接着就是16轮迭代,判断是加密还是解密来选择子密钥的顺序。

​ 然后开始进行运算:\(L_i=R_{i-1}\)\(R_i = L_{i-1} \oplus F(R_{i-1},K_i)\),最后更新L,R。最后将RL拼接,并进行逆置换就得出密文了。

feistelFunc函数实现细节如下:

void feistelFunc(const unsigned char* R, const unsigned char* K, unsigned char* output) 
{
    // 1. 扩展置换 E (32 -> 48)
    unsigned char er[6];
    memset(er, 0, 6);
    permute(R, er, E_Table, 48);

    // 2. 与子密钥 XOR
    unsigned char xorResult[6];
    xorBytes(er, K, xorResult, 6);

    // 3. S盒替换 (48 -> 32)
    unsigned char sOutput[4];
    memset(sOutput, 0, 4);

    // 遍历8个S盒
    for (int i = 0; i < 8; i++) {
        // 每个S盒处理6位输入
        // 计算行: 第1位和第6位组合
        int row = (getBit(xorResult, i * 6) << 1) | getBit(xorResult, i * 6 + 5);
        // 计算列: 中间4位组合
        int col = (getBit(xorResult, i * 6 + 1) << 3) |
            (getBit(xorResult, i * 6 + 2) << 2) |
            (getBit(xorResult, i * 6 + 3) << 1) |
            getBit(xorResult, i * 6 + 4);

        int val = S_Box[i][row][col];

        // 将4位结果写入输出 (每个字节存2个S盒结果,或直接按位写)
        for (int j = 0; j < 4; j++) {
            setBit(sOutput, i * 4 + j, (val >> (3 - j)) & 1);
        }
    }

    // 4. P盒置换 (32 -> 32)
    permute(sOutput, output, P_Table, 32);
}

DES的加密过程大致如上。

4. 注意事项

​ 在开始介绍子密钥生成的过程的时候,提及到有几个位是做奇偶校验不要的,所以会发生不同的密钥也可以加解密的问题,例如03254769就是一个12345678的另一个密钥(可以用来解12345678加密出来的密文)。

三、3DES算法

​ 这个就是进行3次DES运算,首先进行DES加密,再进行DES解密,最后进行DES加密。密钥长度为24字节。每次运算使用密钥当中的8个字节,所以密钥当中不要出现相同的分组,如果密钥三个分组一样的话,这个3DES实际上就只是一次DES加密而已。

img

posted @ 2025-12-10 20:07  x0rrrr  阅读(2)  评论(0)    收藏  举报