加解密数据填充方案介绍

529 阅读9分钟

密码学中,不论是对称加密还是非对称加密,都用到了数据填充。较常见的是PKCS5Padding、PKCS7Padding 和 PKCS1Padding。 前两者常用于对称加密,后一个用于非对称加密。

PKCS5Padding

PKCS5Padding方案针对数据块大小blockSize为8字节, 分块后不足8字节的部分(假设长度为x)将被填充8-x个字节的(8-x),如果都能被8整除,则填充8字节的8。设数据长度为x,填充的数据长度为y,填充后数据总长度为z。则:

y = 8 - (x % 8)
z = x + y

可见当x能被8整除时,z = 8, 即此时也填充了8字节。

如果L是最后一个不完整的数据块,其长度length(L), 使用等于填充字节数量的值作为填充值进行填充 ,那么它将按照如下方式进行填充:

L 01 if length(L) = 7
L 02 02 if length(L) = 6
L 03 03 03 if length(L) = 5
L 04 04 04 04 if length(L) = 4
L 05 05 05 05 05 if length(L) = 3
L 06 06 06 06 06 06 if length(L) = 2
L 07 07 07 07 07 07 07 if length(L) = 1

最后,如果输入的数据长度确实能被8整除,那么就会将下面的数据块附加到数据块后。

08 08 08 08 08 08 08 08 08

典型的数据块为8字节的对称算法为DES和3DES。而AES和SM4的块大小都为16字节,在它们上应用PKCS5Padding填充方案时,不满足blockSize = 8,此时Cipher类会自动升级为PKCS7Padding。可以分别使用 "AES/XXX/PKCS5Padding"、"AES/XXX/PKCS7Padding"来初始化Cipher.ganInstance(String transformation)并加密数据来进行验证。

注:"XXX"为ECB、CBC的一种, 若选择"NoPadding",需满足原文长度能被blockSize整除。后面介绍的代码实现,可保证使用"NoPadding"前的原文长度满足要求。

PKCS7Padding

上面提到了PKCS5Padding只适用于块大小为8字节的对称算法,实际上PKCS5Padding为PKCS7Padding的一个子集,PKCS7Padding支持的数据块大小的范围为1-255。 假设某种加密算法的blockSize = n, 分块后不足n字节的部分将数据填充到n字节。设数据长度为x,填充的数据长度为y,填充后数据总长度为z。则:

y = n - (x % 8)
z = x + y

如果x能被n整除,则会填充一个数据块的padding,即y = n, z = x + n。

比如AES加密,其blockSize为16字节, 使用PKCS7Padding。 如果L是最后一个不完整的数据块,其长度length(L), 使用等于填充字节数量的值作为填充值进行填充 ,那么它将按照如下方式进行填充:

L 01 if length(L) = 15
L 02 02 if length(L) = 14
L 03 03 03 if length(L) = 13
L 04 04 04 04 if length(L) = 12
L 05 05 05 05 05 if length(L) = 11
L 06 06 06 06 06 06 if length(L) = 10
L 07 07 07 07 07 07 07 if length(L) = 9
L 08 08 08 08 08 08 08 if length(L) = 8
...
L 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F if length(L) = 1

如果输入的数据长度能被16整除,那么就会将下面的数据块附加到数据块后,并进行加密。

10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10

解密时去除Padding

在解密时,由密文得到的明文的最后一个字节就是要丢弃的填充字符数,即如果最后一字节为n,则丢弃明文的最后的n个字节。

EBC 和 CBC

对称加密是一种块加密。加密时需指定工作模式,CBC 和 ECB 比较常用。

ECB

电子密码本模式(Electronic codebook) , 块加密, 明文分组与密文分组一一对应

优点:分块处理、并行处理; 误差不会传递
缺点:同样的原文得到相同的密文 ,明文的结构容易被泄露、攻击

CBC

密码分组链接(Cipher-block chaining) , 每块加密依赖前一块的密文。 明文分组首先与一个密文分组XOR运算,然后再加密,这样做的目的就是改变明文分组的结构,由于第一个明文分组前没有密文分组,需增添IV初始化向量,与第一个明文分组XOR运算,并加密,产生第一个密文分组。

优点:同样的原文,IV的不同,得到不同的密文; 原文微小的改变影响后面全部密文
缺点:加密需要串行处理 ,效率较低;误差会传递

PKCS1Padding

PKCS1Padding通常指PKCS1Padding(v1.5) , 是目前RSA算法的默认补位算法。在com.sun.crypto.provider包下的RSACipher中,可以看到paddingType的默认值:


public final class RSACipher extends CipherSpi {
...

private int mode;
private String paddingType = "PKCS1Padding";
...

RSA 常用的加密填充模式为:RSA/None/PKCS1Padding 和 RSA/ECB/PKCS1Padding。 Java 默认的 RSA 实现是 RSA/None/PKCS1Padding , 默认实现如下:

Cipher cipher = Cipher.getInstance("RSA");

// 等同于
Cipher cipher = Cipher.getInstance("RSA/None/PKCS1Padding");

而 Bouncy Castle的默认 RSA 实现是 RSA/None/NoPadding:

Cipher cipher = Cipher.getInstance("RSA", "BC");
// 等同于
Cipher cipher = Cipher.getInstance("RSA/None/NoPadding");

PKCS#1_v1.主要解决公钥不变、采用无补位(NoPadding)下每次加密产生的密文都相同的问题,作用类似于对称加密中的CBC模式。

PKCS1Padding(v1.5) 是由几个固定位 + 随机数 + 明文消息 构成,其格式如下:

EB = 00+ BT+PS +00 + D

EB: Hex密文块,大小为公钥长度/8个字节。

00:起始为一个字节的0。

BT:一个字节, 有三个值00 01 02。02:使用公钥,00或01:用私钥。

PS:随机数填充位,由k-3-D这么多个字节构成,k:密钥的字节长度, D:明文数据的字节长度。PKCS#1建议最小的随机数为8字节。

BT为00时:PS部分全部为00;
BT为01时:PS部分全部为FF;
BT为02时:PS部分随机产生非0数据;

00:D前用一个字节的00表示填充结束。

D:明文

PKCS1Padding补位后的明文大小应该等于公钥长度, 而PKCS#1建议最小的随机数为8字节才足够安全,所以明文最大字节长度是:

明文字节长度 = 公钥n字节长度 - 3 - 8 = k - 11

故对于如下公钥长度,它们可处理的明文最大字节长度为:

n长度明文最大字节长度
1024(1024 - 11 * 8) / 8 = 117
2048(2048 - 11 * 8) / 8 = 245
4096(4096 - 11 * 8) / 8 = 501

这也是RSA加密算法的一个短板:一次性加密的明文大小有限,4096位的公钥也只能一次加密501个字节的明文。当要加密数据大于 k-11字节时, 需要把明文数据按照D的最大长度分块然后逐块加密,最后把密文拼起来。

例子:

当前RSA的公钥长度是1024b, 即128字节,使用PKCS1Padding填充,原文最大块长度为117B。假定原文数据长度为96B,则填充处理后字符串分别如下:

原文数据:

61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70

71 72 73 74 75 76 77 78 79 7A 30 31 32 33 34 35

61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70

71 72 73 74 75 76 77 78 79 7A 30 31 32 33 34 35

61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70

71 72 73 74 75 76 77 78 79 7A 30 31 32 33 34 35

私钥操作,00型,填充后数据:

00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70

……

私钥操作,01型,填充后数据:

00 01 FF FF FF FF FF FF FF FF FF FF FF FF FF FF

FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF 00

61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70

……

公钥操作,02型,填充后数据:

00 02 58 DE B9 E7 15 46 16 D9 74 9D EC BE C0 EA

B5 EC BB B5 0D C4 29 95 6C 18 17 BE 41 57 19 00

61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70

……

从填充后的数据可以看出,对于00型的私钥操作,要求原文数据必须不能包含0x00, 或者知晓数据长度,否则将不能准确移去填充数据。

因此在非对称加解密操作中,常使用01型的私钥操作和02型的公钥操作。

算法实现

明白了PKCS填充方案的原理后,我们可以自己实现一套算法。这样原文可以先经过我们字节的填充算法后,选择"NoPadding"模式初始化Cipher即可。

PKCS5Padding


public static byte[] processWithPKCS5padding(final boolean isEncrypt, final byte[] data) {
if (isEncrypt) {
    return addPKCS5padding(data);
    } else { //解密时,将添加的padding字节移除
        byte padding = data[data.length - 1];
        byte[] temp = new byte[data.length - padding];
        System.arraycopy(data, 0, temp, 0, temp.length);
        SoftReference<byte[]> ref_data = new SoftReference<>(temp);
        return ref_data.get();
    }
}

  
private static byte[] addPKCS5padding(final byte[] data) {
    final int blockSize = 8;
    int remaining = data.length % blockSize;
    byte[] result = null;
    byte[] paddings = null; //填充的字节数组
    byte padding; //填充的字节
    if (remaining == 0) { //能整除8,填充8字节8
        padding = (byte) blockSize;
        paddings = new byte[blockSize];

        for (int i=0; i<paddings.length; i++) {
            paddings[i] = padding;
        }
        result = new byte[data.length + blockSize];
    } else { //不能整除8,填充 8 - remaining 字节(8 - remaining)
        padding = (byte) (blockSize - remaining);
        paddings = new byte[blockSize - remaining];
        for (int i=0; i<paddings.length; i++) {
            paddings[i] = padding;
        }
        result = new byte[data.length + padding];
    }

    System.arraycopy(data, 0, result, 0, data.length);
    System.arraycopy(paddings, 0, result, data.length, paddings.length);
    SoftReference<byte[]> ref_data = new SoftReference<>(result);
    return ref_data.get();
}

PKCS7Padding

public static byte[] processWithPKCS7padding(final boolean isEncrypt, String alg, final byte[] data) {
    if (isEncrypt) {
        return addPKCS7padding(alg, data);
    } else { //解密时,将添加的padding字节移除
        byte padding = data[data.length - 1];
        byte[] temp = new byte[data.length - padding];
        System.arraycopy(data, 0, temp, 0, temp.length);
        SoftReference<byte[]> ref_data = new SoftReference<>(temp);
        return ref_data.get();
    }
}

  
private static byte[] addPKCS7padding(String alg, final byte[] data) {
    int blockSize = 0;
    if(alg.contains("DES")) { //"DES"、"DESede"的blockSize都为8字节
        blockSize = 8;
    }else if(alg.contains("AES")) {
        blockSize = 16;
    }else if(alg.contains("SM4")) {
        blockSize = 16;
    }

    int remaining = data.length % blockSize;
    byte[] result = null;
    byte[] paddings = null; //填充的字节数组
    byte padding; //填充的字节
    if (remaining == 0) { //能整除blockSize,填充blockSize字节blockSize
        padding = (byte) blockSize;
        paddings = new byte[blockSize];
        for (int i=0; i<paddings.length; i++) {
            paddings[i] = padding;
        }
        result = new byte[data.length + blockSize];
    } else { //不能整除blockSize,填充 blockSize - remaining 字节(blockSize - remaining)
        padding = (byte) (blockSize - remaining);
        paddings = new byte[blockSize - remaining];
        for (int i=0; i<paddings.length; i++) {
            paddings[i] = padding;
        }
        result = new byte[data.length + padding];
    }
    System.arraycopy(data, 0, result, 0, data.length);
    System.arraycopy(paddings, 0, result, data.length, paddings.length);
    SoftReference<byte[]> ref_data = new SoftReference<>(result);
    return ref_data.get();
}

PKCS1Padding

/**
* 对于bt为00型的私钥操作,要求原文数据必须不能包含0x00,或者知晓数据长度,否则将不能准确移去填充数据。
* 因此在加解密操作中,常使用01型的私钥操作和02型的公钥操作。
*/

public static byte[] processWithPKC1padding(final boolean isEncrypt, String alg, int keySize, final byte[] data) throws IOException {
    if (isEncrypt) {
        return addPKCS1padding(alg, keySize, 2, data);
    } else { //解密时,将添加的padding字节移除
        if (data.length % (keySize / 8) != 0) {
            throw new InvalidParameterException("data.length % (keySize / 8) != 0");
        }

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        int blockSize = keySize / 8;
        int blocks = data.length / blockSize;
        for (int i=0; i < blocks - 1; i++) { //前blocks-1个块统一处理:每个块的前11个字节移除
            bos.write(data, i * blockSize + 11, blockSize - 11);
        }

        //最后一个块特殊处理
        int start = blockSize * (blocks - 1);
        int offset = 1; //每个块的第一个字节为0,从第二个字节开始
        while (data[start + offset] != (byte) 0x00) { //不适合bt == 0的情况
            offset++;
        }

        offset += 1; //跳出循环时指向00字节,下一个字节才是D
        int len = blockSize - offset;
        bos.write(data, start + offset, len);
        SoftReference<byte[]> ref_data = new SoftReference<>(bos.toByteArray());
        return ref_data.get();
    }
}

  
private static byte[] addPKCS1padding(String alg, int keySize, int bt, final byte[] data) throws IOException {
    if (bt != 1 && bt != 2) {
        throw new InvalidParameterException("bt must be 1 or 2");
    }

    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    int blockSize = keySize/8 - 11;
    if(alg.equalsIgnoreCase("RSA")) {

    }else if(alg.equalsIgnoreCase("SM2")) {

    }

    int start = 0;
    int inLength = data.length;
    boolean hasMore = true;
    byte[] inBytes = null;
    byte[] output = new byte[keySize / 8];
    while (hasMore) {
        int end = Math.min(start + blockSize, inLength);
        inBytes = Arrays.copyOfRange(data, start, end);
        if (inBytes.length == blockSize) {
            bos.write((byte) 0x00);
            bos.write((byte) bt);
            byte[] ps = new byte[8];
            if (bt == 0) {
                Arrays.fill(ps, (byte) 0);
            } else if (bt == 1) {
                Arrays.fill(ps, (byte)0xff);
            } else if (bt == 2) {
                SecureRandom random = new SecureRandom();
                boolean isLegal = false;
                while (!isLegal) {
                    random.nextBytes(ps);
                    boolean find = false;
                    for(byte b: ps) {
                        if (b == (byte) 0) {
                            find = true;
                            break;
                        }
                    }
                    if(!find) {
                        isLegal = true;
                    }
                }
            }
            bos.write(ps);
            bos.write((byte) 0x00);
            bos.write(inBytes);
            start += blockSize;
        } else {
            hasMore = false;
        }
    }

    if(inBytes.length > 0) {
        bos.write((byte) 0x00);
        bos.write((byte) bt);
        int remaining = blockSize - inBytes.length;
        byte[] ps = new byte[8 + remaining];
        if (bt == 0) {
            Arrays.fill(ps, (byte) 0);
        } else if (bt == 1) {
            Arrays.fill(ps, (byte)0xff);
        } else if (bt == 2) {
            SecureRandom random = new SecureRandom();
            boolean isLegal = false;
            while (!isLegal) {
                random.nextBytes(ps);
                boolean find = false;
                for(byte b: ps) {
                    if (b == (byte) 0) {
                        find = true;
                        break;
                    }
                }
                if(!find) {
                    isLegal = true;
                }
            }
        }
        bos.write(ps);
        bos.write((byte) 0x00);
        bos.write(inBytes);
    }

    SoftReference<byte[]> ref_data = new SoftReference<>(bos.toByteArray());
    return ref_data.get();
}