一、概述
在平时生活中或者开发工作中只要遇到涉及信息安全的话题永远也离不开这几个术语“编码”,“加密”和“摘要”。我在刚接触的时候会很迷惑这几个名词究竟有什么区别呢?它们的内涵又是什么?下面就来梳理一下。
二、编码(Encoding)
关于编码维基百科给出了如下的定义:“编码是信息从一种形式或格式转换为另一种形式的过程;解码则是编码的逆过程。”定义给的很宽泛,但从实际的工作中来看,我们平时经常接触的主要有两种形式:第一种是字符编码;第二种是二进制编码。前者规定了一个字符在计算机中该如何转变成二进制比特位的形式;后者规定了非字符类的二进制比特位该如何转变成可读字符的形式。
1. 字符编码
我们现在在屏幕前看到的字母abc和“中文”对于计算机来说都只是一连串二进制比特位而已,之所以能够渲染出文本来是因为存在着字符编码的规范。例如字母’A’的ASCII码为01000001
;字符“中”的UTF-8的编码为11100100 10111000 10101101
。不过需要注意的是不同的编码对某一字符的二进制位规定可能是不同的,若使用错误的编码格式去解码比特位则会导致乱码的情况出现。例如“中”的GBK编码为11010110 11010000
,若使用UTF-8去格式打开一个GBK的文本文件,则内容是不可读的。另外值得一提的是UTF-8是ASCII码的扩展集,换句话说字母和一些字符在UTF-8中的二进制位表示是相同的。
/*
输出:
中
中
��
*/
@Test
public void testEncoding() throws UnsupportedEncodingException {
System.out.println(new String(new byte[]{(byte) 0b11100100, (byte) 0b10111000, (byte) 0b10101101}, "UTF-8"));
System.out.println(new String(new byte[]{(byte) 0b11010110, (byte) 0b11010000}, "GBK"));
System.out.println(new String(new byte[]{(byte) 0b11010110, (byte) 0b11010000}, "UTF-8"));
}
还有一种常用的字符编码是URL编码,之所以要用URL编码是因为标准规定了只允许在URL使用英文字母和特定字符,若要在URL中带中文或特殊字符那就只能使用编码了,将它们转变为规范允许的字符类型。详细情况可以参阅关于URL编码–阮一峰。下面举个例子来看下:在百度搜索“不爱学习的灰灰”,看看请求的URL会发生什么变化。
@Test
public void testEncoding() throws UnsupportedEncodingException {
System.out.println(URLEncoder.encode("#不爱学习的灰灰", "UTF-8"));
// 输出:%23%E4%B8%8D%E7%88%B1%E5%AD%A6%E4%B9%A0%E7%9A%84%E7%81%B0%E7%81%B0
}
2. 二进制编码
上述字符的二进制位通过字符编码规范可以渲染成为文本,但是有些二进制位本身就不具备文本字符属性(例如图片),却又想查看这些比特位的内容那么该怎么办呢?这听上去很奇怪,非字符的内容我为什么还需要读取呢?
可是有时候我们确实需要对比两个二进制文件是否有偏差。这时候可以借鉴编码的思想,将文件呈现出来。最简单直接的方法就是直接使用二进制01表示出来,不过这样有个问题那就是一个字节就有8位,呈现出来的内容太长了,而且人眼去辨别每一个二进制位是非常痛苦的事情。因此用16进制编码是一个不错的选择,16进制编码每一个字符代表了4个比特位,两个字符就可以代表一个字节,所以相比二进制来说能够大幅减少文本内容。另外16进制使用0-10 A-F的字符来呈现内容,这对肉眼观察来说更为友好一点。
// 导入commons-codec:commons-codec:1.14
import org.apache.commons.codec.binary.Hex;
@Test
public void testHexEncoding(){
System.out.println(Hex.encodeHex(new byte[]{(byte) 0b11100100, (byte) 0b10111000, (byte) 0b10101101}));
// 输出:e4b8ad
}
还有一种常见的方式是Base64。它的原理是把3字节的二进制数据按6bit一组,用4个int整数表示,然后查表,把int整数用索引对应到字符,得到编码后的字符串。
import java.util.Base64;
@Test
public void testBase64Encoding(){
System.out.println(Base64.getEncoder().encodeToString(new byte[]{(byte) 0b11100100, (byte) 0b10111000, (byte) 0b10101101}));
// 输出:5Lit
}
三、加密(Encryption)
加密在维基百科中的定义是:将明文信息改变为难以读取的密文内容,使之不可读的过程。只有拥有解密方法的对象,经由解密过程,才能将密文还原为正常可读的内容。这时你可能会说将文本转成字节然后再用base64编码之后的结果也是不可读的啊,但是编码之所以为编码那就是只要知道编码方式的话就能将结果转为原文,而加密解密的过程中则需要一把钥匙——密钥。
1. 对称加密
所谓的对称加密就是在加密和解密过程中使用同一个密钥。最常用的算法是AES(Advanced Encryption Standard)、DES(Data Encryption Standard)等。这两者采用的都是分组加密,即将明文分成多个等长的区块(block),使用确定的算法和对称密钥对每组分别加密解密。DES的秘钥长度为56位(表面为64位),块长度为64位;AES的秘钥长度为128,192或256,块长度为128位。由于秘钥长度过短会使得暴力破解的难度大大降低,所以DES已经不太使用了。目前最为流行的还是AES256加密方式。
既然分组加密需要将明文分为等长的区块,那么如果明文无法按照区块长度等分怎么办呢?这时可以对明文最后一个区块进行填充,填充模式有好几种例如NoPadding/PKCS5Padding/PKCS7Padding/…其中NoPadding表示不填充,在此填充下原始数据必须是分组大小的整数倍,非整数倍时无法使用该模式;PKCS7Padding表示填充至符合块大小的整数倍,填充值为填充数量数,块大小是个参数。 PKCS5Padding可以看做是PKCS7Padding的子集,只是块大小固定为8字节。还有一些填充方式可以看看这篇博文分组密码算法的填充模式
分组加密还有一个重要的参数是工作模式,例如几个典型的有ECB(Electronic Code Book)、CBC(Cipher Block Chaining)等。ECB就是单纯地将明文分块然后加密,这样会带来一个问题就是同样的明文块会被加密成相同的密文块,因此,它不能很好的隐藏数据模式。而在CBC模式中,每个明文块先与前一个密文块进行异或后,再进行加密。在这种方法中,每个密文块都依赖于它前面的所有明文块,所以不会产生ECB的问题。但是第一个区块怎么办呢?它又没有可以异或的对象。因此,CBC引入了一个初始化向量的概念,也称为IV(Initialization Vector),它一般为随机生成的16个字节即128位,用于与第一个区块异或,同时又由于它随机的特性,所以就算是同一个明文,每次用CBC模式产生的密文都会是不一样的,安全性大大增加,所以也是最为常用的工作模式。
算法 | 秘钥长度 | 区块长度 | 工作模式 | 填充模式 |
---|---|---|---|---|
AES | 128/192/256 | 128 | ECB/CBC/PCBC/CTR/… | NoPadding/PKCS5Padding/PKCS7Padding/… |
DES | 56 | 64 | ECB/CBC/PCBC/CTR/… | NoPadding/PKCS5Padding/… |
下面用一段Java代码演示一下AES256/CBC/PKCS5Padding,需要注意的是由于随机iv的存在,每次运行产生的密文可能都不一样。
/*
输出:
Message: Hello, world!
Encrypted: LQHZ9LHgi9CpXFjhAFKFaA==
Decrypted: Hello, world!
*/
@Test
public void testCipher() throws Exception {
// 原文:
String message = "Hello, world!";
System.out.println("Message: " + message);
// 256位密钥 = 32 bytes Key:
byte[] key = "1234567890abcdef1234567890abcdef".getBytes("UTF-8");
// 加密:
byte[] data = message.getBytes("UTF-8");
Map<String, byte[]> encryptedResult = encrypt(key, data);
byte[] ciphertext = encryptedResult.get("ciphertext");
byte[] iv = encryptedResult.get("iv");
System.out.println("Encrypted: " + Base64.getEncoder().encodeToString(ciphertext));
// 解密:
byte[] decrypted = decrypt(key, iv, ciphertext);
System.out.println("Decrypted: " + new String(decrypted, "UTF-8"));
}
// 加密:
public static Map<String, byte[]> encrypt(byte[] key, byte[] plaintext) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
// CBC模式需要生成一个16 bytes的initialization vector:
SecureRandom sr = SecureRandom.getInstanceStrong();
byte[] iv = sr.generateSeed(16);
IvParameterSpec ivps = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivps);
byte[] ciphertext = cipher.doFinal(plaintext);
// IV不需要保密,把IV和密文一起返回:
Map<String, byte[]> result = new HashMap<>();
result.put("iv", iv);
result.put("ciphertext", ciphertext);
return result;
}
// 解密:
public static byte[] decrypt(byte[] key, byte[] iv, byte[] ciphertext) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
IvParameterSpec ivps = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivps);
return cipher.doFinal(ciphertext);
}
2. 非对称加密
与对称加密相比,非对称加密最大的不同在于它有两个相互配对的密钥,一个是公钥,一个是私钥。其中公钥是可以公开给其他人的,而私钥则必须自己保留,不能泄露。如果用其中一个密钥加密,则必须用对应的另一个密钥解密。那么究竟用哪个密钥加密,哪个密钥解密呢?这其实要看具体的使用场景,例如最为常见的秘密通信场景下:消息发送方希望自己发送的信息只能被接收方识别而不被其他人破解,这时则采用接收方公钥加密,接收方私钥解密的方式;另一种情况是消息发送方希望自己是消息的唯一来源,而不是其他人伪造的谣言,这时采用的是发送方私钥加密,发送发公钥解密的方式,这种场景又可以称为数字签名。
非对称加密最为流行的算法是RSA算法,由发明者Rivest、Shmir和Adleman姓氏首字母缩写而来。它的密钥有256/512/1024/2048/4096等不同的长度。长度越长,密码强度越大,当然计算速度也越慢,通常会比对称加密算法要慢几倍甚至几百倍。因此RSA通常还是会和对称加密一起配合使用,例如HTTPS协议下,浏览器和服务器先通过RSA交换AES口令,接下来双方通信实际上采用的是速度较快的AES对称加密。
下面用一段Java代码来演示一下RSA2048
static byte[] encrypt(File publicKeyFile, String message) throws Exception {
try (FileInputStream publicKeyIn = new FileInputStream(publicKeyFile)) {
// 从文件中生成publicKey对象
byte[] publicKeyBytes = new byte[publicKeyIn.available()];
publicKeyIn.read(publicKeyBytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
PublicKey publicKey = kf.generatePublic(new X509EncodedKeySpec(publicKeyBytes));
// 消息加密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(message.getBytes(StandardCharsets.UTF_8));
}
}
static String decrypt(File privateKeyFile, byte[] ciphertext) throws Exception {
try (FileInputStream privateKeyIn = new FileInputStream(privateKeyFile)) {
// 从文件中生成privateKey对象
byte[] privateKeyBytes = new byte[privateKeyIn.available()];
privateKeyIn.read(privateKeyBytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
PrivateKey privateKey = kf.generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes));
// 消息解密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext, StandardCharsets.UTF_8);
}
}
四、摘要(Digest)
摘要算法又称之为哈希算法或者散列算法,它的作用是:对任意一组输入数据进行计算,得到一个固定长度的输出摘要。并且相同的输入一定得到相同的输出;不同的输入大概率得到不同的输出(有可能会存在哈希碰撞)。不过需要注意的是不同于编码和加密,摘要算法是不可逆的,也就是说你无法通过一个摘要值反推出原始数据。计算摘要常用的算法有MD5、SHA1、SHA256、SHA512等。
摘要最主要的目的是为了验证原始数据是否被篡改或者损坏,很多正规的网站提供软件下载链接的同时还会附上摘要值,就是为了让用户下载完能够验证你下载的软件是否是完好的,没有损坏的。例如下图是Tomcat的下载页面,不同的下载链接都会附上sha512摘要,以MacOS为例用户下载完可以在命令行输入shasum -a 512 [FILE]
来得到一个文件的sha512摘要,然后和官网摘要进行对比。
下表是常用的算法,可以看到不同的算法会输出不同长度的结果,长度越长哈希碰撞产生的概率也越低。
算法 | 输出长度 |
---|---|
MD5 | 128位(16字节) |
SHA-1 | 160位(20字节) |
SHA-256 | 256位(32字节) |
SHA-512 | 512位(64字节) |
老规矩,还是用一段Java代码来演示一下sha512算法
static String getDigest(File file) throws Exception {
// 创建一个MessageDigest实例:
MessageDigest md = MessageDigest.getInstance("SHA-512");
try (FileInputStream in = new FileInputStream(file);
BufferedInputStream bufferedInputStream = new BufferedInputStream(in)) {
int r;
// 反复调用update输入数据:
while ((r = bufferedInputStream.read()) != -1) {
md.update((byte) r);
}
// 计算结果
byte[] result = md.digest();
return new BigInteger(1, result).toString(16);
}
}
五、小结
类型 | 是否可逆 | 常见算法 |
---|---|---|
编码 | √ | UTF-8;Base64 |
加密 | √ | AES;RSA |
摘要 | × | MD5;SHA512 |
以上完整代码demo可以访问我的GitHub