再谈Java、Android AES加密算法填充方式

今天发布的博客有些临时赶工成分

天气一如既往的炎热,下班到了家习惯性的打开空调,然后从冰箱掏出冰棒享受着透心凉的赶脚。待身上的高能辐射褪去殆尽,便去开锅做起牛肉粉丝汤,嗯,今天的晚餐。做好,盛碗,端进卧室,在空调的风口下吃口味更佳,嗯,还不起劲,拧开了“王者农药”进行一场刺激的峡谷之战,边吃边玩,意境更佳。

完事,洗碗刷锅后看看手机上的时间,打算时间充足的话就去游个泳,然而“一不小心”看到了今天的日期,F**K!31号了!这个月一篇文章都没发!

本可以度过一个轻松愉快的夜晚,然而......

图片[1]-再谈Java、Android AES加密算法填充方式

赶紧开机干活,于是有了今天的意外产出,下面正式开始

此前写过一篇关于AES加密的文章《Android加密算法之对称加密AES》,介绍了相关概念,使用以及脱坑姿势。

之所以写续篇,是因为最近项目中出现了AES加密的bug,折腾了许久最终确认为Android端错误地使用PKCS5Padding填充方式,而服务端则是PKCS7Padding,改为PKCS7Padding即可,还好bug只会出现在Android4.3和4.4的设备上,仅影响了极少数的用户。呵呵呵......

希望这篇文章没有被领导看到...看到也没事,反正又不是我弄的

图片[2]-再谈Java、Android AES加密算法填充方式

嗯哼,求知欲强的童鞋可能会抛出以下问题:

  1. 为什么Android可以使用PKCS7Padding
  2. 为什么PKCS5Padding可以解密PKCS7Padding加密的数据?

接下来一一作答

Android可以使用PKCS7Padding

我们知道Java是不支持PKCS7Padding填充的,标准的方式是PKCS5Padding。而Android实现了Java SE API,但没有使用相同的源代码,没有使用相同的提供程序,也没有作AES限制的实现。

这句话引自某stackoverflow issue

Android implements the Java SE API, but does not use the same source code, not the same provider and the AES restrictions are not implemented either.

一句话概括:Android没有使用标准Java的AES加密,而是自己实现了一套,顺便实现了PKCS7Padding

PKCS5Padding可以解密PKCS7Padding加密的数据

从AES加密原理说起

加密流程
  1. 把明文按照128bit(16byte)拆分成若干个明文块

  2. 按照选择的填充方式来填充最后一个明文块

  3. 利用AES加密器和密钥将每一个明文块加密成密文块

  4. 拼接所有的密文块,成为最终的密文结果

流程图示

图片[3]-再谈Java、Android AES加密算法填充方式

填充原理

需要填充的字节长度 = (块长度 - (数据长度 % 块长度))

    假定块长度为8,数据长度为3,则填充字节数等于5
    原数据为: FF FF FF 
    填充结果: FF FF FF 05 05 05 05 05 
    假定块长度为8,数据长度为9,则填充字节数等于7
    原数据为:FF FF FF FF FF FF FF FF FF
    填充结果:FF FF FF FF FF FF FF FF FF 07 07 07 07 07 07 07
    假定块长度为8,数据长度为8,则填充字节数等于8
    原数据为:FF FF FF FF FF FF FF FF
    填充结果:FF FF FF FF FF FF FF FF 08 08 08 08 08 08 08 08

总结如下:

  1. 填充的字节都是一个相同的字节
  2. 该字节的值,就是要填充的字节的个数
  3. 数据长度是块长度的整数倍时,还需另补块长度个值为块长度的字节

这样如果填充1个字节,那么填充的字节的值就是0x01;要填充7个字节,那么填充的字节的值全是0×07;长度恰好8的整数倍时,还要补8个值为0×08的字节。

思考一个问题:

为啥为整数倍时,还是要多补8个0x08?

答案:解密的需要!

嗯哼,一脸懵逼?看下面高能举栗:

假设当某数据是8的整数倍,没有填充8个0x08:
解密过程中,程序检查数据末尾的最后一个字节发现这个字节恰好是0x01,那这个字节是填充上去的呢?
还是实际的数据呢?要不要做去填充操作呢?此时程序心里一万个草泥马。

图片[4]-再谈Java、Android AES加密算法填充方式

填充方式和填充结果

假设某明文字节长度为m,拆分密文块后剩余字节e=m%16,最终密文长度为l

算法/模式/填充 e=0时密文长度 e!=0密文长度
AES/CBC/NoPadding l=m 不支持
AES/CBC/PKCS5Padding l=m+(16-e)=m+16 l=m+(16-e)
AES/CFB/NoPadding l=m l=m
AES/CFB/PKCS5Padding l=m+(16-e)=m+16 l=m+(16-e)
AES/ECB/NoPadding l=m 不支持
AES/ECB/PKCS5Padding l=m+(16-e)=m+16 l=m+(16-e)
AES/OFB/NoPadding l=m l=m
AES/OFB/PKCS5Padding l=m+(16-e)=m+16 l=m+(16-e)
... ... ...

PKCS5Padding与PKCS7Padding

  • PKCS5Padding填充和PKCS7Padding填充算法没有任何区别。
  • PKCS5Padding在填充方面,是PKCS7Padding的一个子集:

    PKCS5Padding只是对于8字节(BlockSize=8)进行填充,填充内容为0x01-0x08

    但是PKCS7Padding不仅仅是对8字节填充,其BlockSize范围是1-255字节

所以,PKCS5Padding可以向上转换为PKCS7Padding,但是PKCS7Padding不一定可以转换到PKCS5Padding。一定条件下,两者可以互换使用的。

参考资料

https://crypto.stackexchange.com/questions/9043/what-is-the-difference-between-pkcs5-padding-and-pkcs7-padding

拓展

关于NoPadding

顾名思义,不填充。数据分完块,不去做填充操作,直接加密。

嗯哼,想到一个骚操作,就是让NoPadding转换成PKCS5Padding效果

NoPadding加密 →→→ PKCS5Padding解密

贴代码
public class AESUtils {
    private static final String AES_CBC_PKCS5_PADDING = "AES/CBC/PKCS5Padding";
    private static final String AES_CBC_NO_PADDING = "AES/CBC/NoPadding";
    private static final String AES = "AES";
    private static final String UTF_8 = "UTF-8";
    private static final String AES_KEY = "286751244B391A6DBB84778E0D6A8926";

    public static void main(String[] args) {
        String plaintext = "12345678";
        String encrypt = encryptByNoPadding(plaintext, AES_KEY);
        System.out.println("          数据加密前:" + plaintext);
        System.out.println("   `NoPadding`加密后:" + encrypt);
        String desEncrypt = decryptByPKCS5Padding(encrypt, AES_KEY);
        System.out.println("`PKCS5Padding`解密后:" + desEncrypt);
    }

    private static String encryptByNoPadding(String plaintext, String key) {
        try {
            // 加密初始化实例,NoPadding形式
            final Cipher cipher = Cipher.getInstance(AES_CBC_NO_PADDING);
            final int blockSize = cipher.getBlockSize();
            final byte[] dataBytes = plaintext.getBytes(UTF_8);
            final int length = dataBytes.length;
            final int remainder = length % blockSize;
            //计算需要填充的字节长度
            final int paddingLength = blockSize - remainder;
            //计算填充后的字节长度
            int newLength = length + paddingLength;
            final byte[] newDataBytes = new byte[newLength];
            //填充
            for (int i = 0; i < paddingLength; i++) {
                // 填充的字节长度也是填充的值,需要转成字节(byte) paddingLength
                newDataBytes[length + i] = (byte) paddingLength;
            }
            //将老数据迁移到新数据字节数组里
            System.arraycopy(dataBytes, 0, newDataBytes, 0, dataBytes.length);
            // 还原密钥对象
            SecretKey secretKey = new SecretKeySpec(key.getBytes(UTF_8), AES);
            // 偏移量
            IvParameterSpec ivParameterSpec = new IvParameterSpec(new byte[cipher.getBlockSize()]);
            // CBC模式需要添加一个参数IvParameterSpec
            cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec);
            byte[] result = cipher.doFinal(newDataBytes);
            return parseByte2HexStr(result);
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }
    }

    private static String decryptByPKCS5Padding(String data, String key) {
        try {
            byte[] encryp = parseHexStr2Byte(data);
            Cipher cipher = Cipher.getInstance(AES_CBC_PKCS5_PADDING);
            // 还原密钥对象
            SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(UTF_8), AES);
            // 偏移量
            IvParameterSpec ivParameterSpec = new IvParameterSpec(new byte[cipher.getBlockSize()]);
            // CBC模式需要添加一个参数IvParameterSpec
            cipher.init(Cipher.DECRYPT_MODE, keySpec, ivParameterSpec);
            byte[] original = cipher.doFinal(encryp);
            return new String(original);
        } catch (Exception e) {
        }
        return null;
    }

    private static String parseByte2HexStr(byte[] buf) {
        StringBuffer sb = new StringBuffer();

        for (int i = 0; i < buf.length; ++i) {
            String hex = Integer.toHexString(buf[i] & 255);
            if (hex.length() == 1) {
                hex = '0' + hex;
            }
            sb.append(hex.toUpperCase());
        }

        return sb.toString();
    }

    private static byte[] parseHexStr2Byte(String hexStr) {
        if (hexStr.length() < 1) {
            return null;
        } else {
            byte[] result = new byte[hexStr.length() / 2];
            for (int i = 0; i < hexStr.length() / 2; ++i) {
                int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
                int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16);
                result[i] = (byte) (high * 16 + low);
            }
            return result;
        }
    }
}
输出结果
          数据加密前:12345678
   `NoPadding`加密后:76D277F787AB75E354061B2319CAE945
`PKCS5Padding`解密后:12345678

ok,转换成功了。至此,本文就告一段落了。

写在最后

文章写得时间比较赶,逻辑还不够紧凑清晰,希望童鞋们能指出问题或给出宝贵的意见,以帮助我更好地完善本文。

另外,上文提到bug只会出现在Android4.3和4.4的设备上的具体分析将在后期的文章给出。

© 版权声明
THE END
喜欢就支持以下吧
点赞4 分享
评论 共3条
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容