PKCS(Public Key Cryptography Standards)公钥加密标准包含了一系列公钥密码学标准,其中包含了数据填充的相关规范。

当涉及到块加密算法(例如AES)时,PKCS#7(扩展了PKCS#5)是一种常用的填充方式。

当数据长度不是加密算法所要求块大小(AES块大小为16字节)的整数倍时,就需要使用填充算法来确保数据能够被正确地加密和解密。

PKCS#7

在PKCS#7中,如果数据的长度不是块大小的整数倍,那么会在数据的末尾添加额外的字节来进行填充。填充字节的数量由最后一个字节值给出,该值表示添加了多少个填充字节(取值在1到块大小之间)

如果数据已经是块大小的整数倍,那么添加一个额外的块作为补充,其中每个字节的值都是块的大小(对于AES,填充值为16,即0x10)。

解密后如何判断最后一个字节不是明文,而是填充值?

如果解密后最后一个字节是明文的话,那么可以确定明文的长度刚好为数据块大小的整数倍;然而当明文长度为数据块大小的整数倍时,PKCS#7要求要填充一个额外的块(块中所有字节值为0x10),这显然是相互矛盾的。

如果明文的最后一个字节值为0x10且其长度刚好为数据块的整数倍,那么依据PKCS#7还要填充一个值为0x10的额外的块作为补充,此时解密后的数据末尾至少具有17字节的值为0x10

综上所述,可以根据解密后数据的最后一个字节值来判断填充了多少个字节,最后一个字节值不会为0(会用0x10代替)。

填充原理

  1. 确定填充字节的数量:首先,计算原始数据长度与加密算法块大小(对于AES,块大小通常为128位,即16字节)之间的差值。这个差值就是需要填充的字节数量。
  2. 填充数据:在原始数据的末尾添加填充字节。每个填充字节的值都等于需要填充的字节数量如果数据已经是块大小的整数倍,那么还需要添加一个完整的块,该块的所有字节都设置为块大小的值(对于16字节的块,这些字节的值就是16,即十六进制的10

数学表示如下:

  • 假设原始数据长度为 x 字节。
  • 块大小为 b 字节(对于AES,b = 16)。
  • 需要填充的字节数 n = b - (x % b)
  • 填充后的数据将是原始数据后跟 n 个值为 n 的字节。

示例1

假设我们有一个10字节长的数据块,并且我们使用的是16字节的块大小(如AES)。那么,我们需要填充6个字节(因为16 - 10 = 6)。这6个字节的值都是6(因为我们需要填充6个字节)。所以,填充后的数据块将是原始数据后跟6个值为6的字节。

示例2

如果原始数据是 FF FF FF FF(4字节),并且我们使用的是16字节的块大小,那么需要填充12个字节(因为16 - 4 = 12)。这12个字节的值都是12(因为我们需要填充12个字节)。所以,填充后的数据将是 FF FF FF FF 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C

注意事项

  1. 填充是为了确保数据长度是加密算法块大小的整数倍
  2. 每个填充字节的值都等于需要填充的字节数量。
  3. 如果数据已经是块大小的整数倍,则添加一个完整的块,该块的所有字节都设置为块大小的值
  4. PKCS#5是PKCS#7的一个子集,当块大小为8字节时,两者是等效的。对于大于8字节的块大小,PKCS#7允许使用更大的块大小。

实现

下面的示例中实现了PKCS#7填充算法:

// base为基于填充的块大小
int pkcs7_padding(unsigned char* plaintext, int plaintext_len, unsigned char** output, int* output_len, int base)
{
    int iRet = -1;
    if (plaintext == NULL || plaintext_len < 0)
        return iRet;

    // 如果数据长度为块大小的整数倍,则额外填充一个块
    unsigned char padding_val = base - (plaintext_len % base);
    int padding_len = plaintext_len + (base - (plaintext_len % base));
    unsigned char* padding = (unsigned char*)malloc(padding_len);
    if (padding == NULL)
        return iRet;

    memset(padding, 0x00, padding_len);
    memcpy(padding, plaintext, plaintext_len);
    memset(padding + plaintext_len, padding_val, padding_len - plaintext_len);

    *output = padding;
    *output_len = padding_len;

    iRet = 0;
    return iRet;
}

其中base参数用于指定数据块的大小。

在使用完毕之后需要手动调用free释放output

测试代码以及结果如下:


int main()
{
    char str1[] = "0123456789abcdef";
    unsigned char* output = NULL;
    int output_len = 0;
    int base = 16;

    printf("plaintext: ");
    for (int i = 0; i < strlen(str1); i++)
    {
        printf("%2X ", str1[i]);
    }
    printf("\n");
    if (pkcs7_padding((unsigned char*)str1, strlen(str1), &output, &output_len, base) == 0 && output != NULL &&
        output_len > 0)
    {
        printf("padding_val:");
        for (int i = 0; i < output_len; i++)
        {
            printf("%2X ", output[i]);
        }
        printf("\n");
        free(output);
    }

    char str2[] = "0123456789";

    printf("plaintext: ");
    for (int i = 0; i < strlen(str2); i++)
    {
        printf("%2X ", str2[i]);
    }
    printf("\n");
    if (pkcs7_padding((unsigned char*)str2, strlen(str2), &output, &output_len, base) == 0 && output != NULL &&
        output_len > 0)
    {
        printf("padding_val:");
        for (int i = 0; i < output_len; i++)
        {
            printf("%2X ", output[i]);
        }
        printf("\n");
        free(output);
    }
}
// plaintext: 30 31 32 33 34 35 36 37 38 39 61 62 63 64 65 66
// padding_val:30 31 32 33 34 35 36 37 38 39 61 62 63 64 65 66 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10
// plaintext: 30 31 32 33 34 35 36 37 38 39
// padding_val:30 31 32 33 34 35 36 37 38 39  6  6  6  6  6  6

可以看见执行结果与PKCS#7加密原理一致。