最近在深入的学习关于密码学的相关知识,因为密码学中涉及到了加密和解密,学习内容中穿插了关于Base64编码算法的问题。为此,我特地去了解了下关于Base64编码的算法。

关于Base64编码的概念,其实不必我做过多地解释了,这里就贴一下百科的解释:Base64是网络上最常见的用于传输8Bit字节码的编码方式之一,Base64就是一种基于64个可打印字符来表示二进制数据的方法。说白一点儿就是在数据加密和解密的过程中,就是把传输数据的每个字节映射成ASCII码表中的某些字符,这样在传输的过程中,就不会出现乱码的问题了。

一、Base64和加密解密算法是如何结合使用的的呢?

  这里我们拿DES对称性加密算法结合代码实现给大家看看,DES算法的入口参数有三个:Key、Data、Mode。其中Key为7个字节共56位(实际输入需要8个字节,因为java6对DES算法仅支持56位密钥长度,8个字节的话就是64位,多出的1个字节8位就拿来作为奇偶校验位),是DES算法的工作密钥;Data表示是要被加密或被解密的数据;Mode为DES的工作方式,有两种:加密或解密。

①DES加密:

  通过选择加密模式ENCRYPT_MODE以及定义加密的规则,在doFinal中传入需要经过Base64加密编码的的参数base64.encode(xxx)。

 /**
     * DES加密
     * @param content 明文
     * @param desKey 公共密钥
     * @param algorithmType 加密的类型
     * @return 通过将明文加密后产生的密文
     */
    private static String encryptDES(String content, String desKey, String algorithmType) {
             Base64 base64=new Base64();
        try {
            Cipher cipher = Cipher.getInstance(algorithmType);
            //定义加密规则
            SecretKeySpec Key = new SecretKeySpec(desKey.getBytes(), algorithmType);
            //加密:ENCRYPT_MODE
            cipher.init(Cipher.ENCRYPT_MODE, Key);
            //要将传输的明文编码(base64.encode(content.getBytes()))
            byte[] bytes = cipher.doFinal(base64.encode(content.getBytes()));
            return base64.encodeToString(bytes);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

我们可以执行案例,如下:

public static void main(String[] args) {
    //明文内容
    String content = "Kotlin 是最好使用的语言!!!";
    //DES密钥(加密和解密都使用同一个,密钥严格规定是8个字节(64位),否则报 Wrong key size的错误)
    String desKey = "25245621";
    String algorithmType = "DES";
    
   //DES加密
   String ciphertext = encryptDES(content, desKey, algorithmType);
   System.out.println("DES加密结果: " + ciphertext);
}    

你会发现原来的明文文本内容通过DES加密后,会发现:

> Task :DES.main()
> DES加密结果: IVklbaloTXNLCVRQrQJbjEF/kEpVh9FK/0VE5edkMFuHsHgxylGZS5SaVzFwYg/OsNNC0rd4Ydu6X7XsEDYKCw==

②DES解密:

  与加密相反,解密是通过选择解密模式ENCRYPT_MODE以及定义解密的规则,在doFinal中传入需要经过Base64解密编码的的参数base64.decode(xxx)。

private static String decryptDES(String ciphertext, String desKey, String algorithmType) {
    Base64 base64=new Base64();
    try {
        Cipher cipher = Cipher.getInstance(algorithmType);
        SecretKeySpec Key = new SecretKeySpec(desKey.getBytes(), algorithmType);
        //DECRYPT_MODE:解密
        cipher.init(Cipher.DECRYPT_MODE, Key);
        //要将解码的密文编码
        byte[] bytes = cipher.doFinal(base64.decode(ciphertext));
        return new String(base64.decode(bytes), StandardCharsets.UTF_8);
    } catch (Exception e) {
        e.printStackTrace();
    }

    return null;
}

  由上面的明文,我们获取到了加密后的得到的密文ciphertext,然后通过此密文我们做进一步的解密的操作

public static void main(String[] args) {
    //此处省略其他代码
    //DES加密
    String ciphertext = encryptDES(content, desKey, algorithmType);

    //DES解密
    String decryptText = decryptDES(ciphertext, desKey, algorithmType);

    System.out.println("DES加密结果: " + ciphertext + "\n DES解密结果: " + decryptText);
}

得到结果如下:

> Task :DES.main()
> DES加密结果: IVklbaloTXNLCVRQrQJbjEF/kEpVh9FK/0VE5edkMFuHsHgxylGZS5SaVzFwYg/OsNNC0rd4Ydu6X7XsEDYKCw==
> DES解密结果: Kotlin 是最好使用的语言!!!

  假设我们在加密环节中的doFinal中都去掉Base64的编码的代码,直接传入文本内容转换成的字节数组,其结果如下:

> Task :DES.main()
> DES加密结果: ��:��{��1�4X��(�	��G���c��ra�_��c�dk��_���6�
> DES解密结果: null

  通过上面的案例输出的结果,我们发现加密打印出来的结果是一堆乱码,无法让人读懂里面的内容,所以,Base64的本质并不是加密或解密算法,它只是使得加密或解密过程中通过将乱码映射成ASCII码表中的某些字符,令我们对结果更具易读性。

二、Base64的由来是什么,它的算法原理是怎样的?

  首先,Base64编码中的“64”的很明显的由来是只支持64种不同可打印字符,这些字符在源码中可以查到,这一般情况下它的64个字符如下:

private static final byte[] STANDARD_ENCODE_TABLE = {
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
        'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
        'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
        'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
};

当时用的是URL-SAFE模式时,编码表中的最后两个字符会变成下面的样子:

private static final byte[] URL_SAFE_ENCODE_TABLE = {
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
        'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
        'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
        'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_'
};

  那它为什么是64个可打印字符呢,它的实现算法思路是怎样的呢?以下是我自己的理解,这也仅仅是我自己的个人理解:首先,我们知道正常的一个英文字符是一个字节(1个字节是8位),我们学习的中文的一个字符是两个字节组成的(2个字节是16位),在UTF-8情况下,中文字符等于3个字节,UTF-6时,中文字符等于4个字节,如果拿UTF-8情况下去做字符分组 , 即是分成 3 * 8=24(3个字节 * 单字节8位 = 24位),也就是可以通过二进制表示:00000000 00000000 00000000。因为前面我已经知道一个字符最大可能占用4个字节,在此的前提下,又进一步分组,因为24是8和6的最大公约数,所以分组结果是 4 * 6 = 24(4个字节 * 6 =24位),使用二进制的方式表示:000000 000000 000000 000000,我知道单字节是8位呀,怎么就变成6位了呢,缺少的两位怎么半呀!很简单,缺少的两位就从高位补两个0,这样就满足一个字节8位的条件了。所以就有了表示二进制的方式:00000000 00000000 00000000 00000000,我们从这里面拿一组字节来说一下,最小值是(00000000 min=0),最大值是(00111111 max=63),所以Base64种每组是64种字符。