编码算法
ASCII码就是一种编码,字母A的编码是十六进制的0x41,字母B是0x42,以此类推。
而中文的中使用Unicode编码就是0x4e2d,使用UTF-8则需要3个字节编码。
URL编码
URL编码是浏览器发送数据给服务器时使用的编码,它通常附加在URL的参数部分,例如:
https://www.baidu.com/s?wd=%E4%B8%AD%E6%96%87
出于兼容性考虑,很多服务器只识别ASCII字符,URL中若包含中日文的话,就需要URL编码:
- 如果字符是AZ,az,0~9以及-、_、.、*,则保持不变;
- 如果是其他字符,先转换为UTF-8编码,然后对每个字节以%XX表示。
URL编码总是大写。
URLEncoder类——对任意字符串进行URL编码
String encoded = URLEncoder.encode("中文!", StandardCharsets.UTF_8);
服务器收到URL编码的字符串,URLDecoder类对其进行解码,还原成原始字符串。
String decoded = URLDecoder.decode("%E4%B8%AD%E6%96%87%21", StandardCharsets.UTF_8);
URL编码是编码算法,不是加密算法。URL编码的目的是把任意文本数据编码为%前缀表示的文本,编码后的文本仅包含AZ,az,0~9,-,_,.,*和%,便于浏览器和服务器处理。
Base64编码
Base64编码是对二进制数据进行编码,表示成文本格式。
Java中,二进制数据就是byte[ ]数组,用Base类对byte[ ]数组进行编解码:
byte[] input = new byte[] { (byte) 0xe4, (byte) 0xb8, (byte) 0xad };
String b64encoded = Base64.getEncoder().encodeToString(input);
byte[] output = Base64.getDecoder().decode(b64encoded);
Base64编码的目的是把二进制数据变成文本格式,这样在很多文本中就可以处理二进制数据。例如,电子邮件协议就是文本协议,如果要在电子邮件中添加一个二进制文件,就可以用Base64编码,然后以文本的形式传送。
Base64编码的缺点是传输效率会降低,因为它把原始数据的长度增加了1/3。
和URL编码一样,Base64编码是一种编码算法,不是加密算法。
哈希算法
哈希算法(Hash):对任意一组输入数据进行计算,得到一个固定长度的输出摘要,特点:
- 相同输入一定得到相同输出
- 不同输入大概率得到不同输出
字符串的hashcode()就是一个哈希算法,输入是任意字符串,输出是固定的4字节int整数,两个相同字符串会计算出相同的hashCode,HashMap就是基于hashcode定位来工作的。
"hello".hashCode(); // 0x5e918d2
哈希碰撞
哈希碰撞——两个不同的输入得到了相同的输出
碰撞无法避免:哈希算法把一个无线的输入集合映射到有限的输出集合,必然产生碰撞。碰撞概率的高低关系到哈希算法的安全性。一个安全的哈希算法必须满足:碰撞概率低;不能猜测输出。(无法从输出反推输入:输出没规律)
Java标准库提供了常用的哈希算法,且有一套的统一接口,以MD5算法为例:
import java.math.BigInteger;
import java.security.MessageDigest;
public class Main {
public static void main(String[] args) throws Exception {
//创建一个MessagegeDigest实例:
MessageDigest md = MessageDigest.getInstance();
// 反复调用update输入数据:
md.update("Hello".getBytes("UTF-8"));
md.update("World".getBytes("UTF-8"));
byte[] result = md.digest();
System.out.println(new BigInteger(1, result).toString(16));
}
}
使用MessageDigest时,首先根据哈希算法获取一个MessageDigest实例,然后,反复调用update(byte[])输入数据。当输入结束后,调用digest()方法获得byte[]数组表示的摘要,最后,把它转换为十六进制的字符串。
哈希算法的用途
- 断下载到本地的软件是原始的、未经篡改的文件
- 存储用户口令,不存储用户的原始口令,而是存储用户口令的哈希(防止数据泄露)
也可以加盐处理对抗彩虹表(常用口令MD5对照表)攻击
SHA-1
在Java中使用SHA-1,和MD5完全一样,只需要把算法名称改为"SHA-1":
MD5因为输出长度较短,短时间内破解是可能的,目前已经不推荐使用
小结
哈希算法可用于验证数据完整性,具有防篡改检测的功能;
常用的哈希算法有MD5、SHA-1等;
用哈希存储口令时要考虑彩虹表攻击
BouncyCastle
BouncyCastle就是一个提供了很多哈希算法和加密算法的第三方库。它提供了Java标准库没有的一些算法,例如,RipeMD160哈希算法。
BouncyCastle是一个开源的第三方算法提供商;
BouncyCastle提供了很多Java标准库没有提供的哈希算法和加密算法;
使用第三方算法前需要通过Security.addProvider()注册。
Hmac算法
Hmac算法就是一种基于密钥的消息认证码算法,它的全称是Hash-based Message Authentication Code,是一种更安全的消息摘要算法。
Hmac算法总是和某种哈希算法配合起来用的。例如,我们使用MD5算法,对应的就是HmacMD5算法,它相当于“加盐”的MD5:HmacMD5 ≈ md5(secure_random_key, input)
HmacMD5可以看作带有一个安全的key的MD5。使用HmacMD5而不是用MD5加salt,有如下好处:
- HmacMD5使用的key长度是64字节,更安全;
- Hmac是标准算法,同样适用于SHA-1等其他哈希算法;
- Hmac输出和原有的哈希算法长度一致。
Hmac本质上就是把key混入摘要的算法。验证此哈希时,除了原始的输入数据,还要提供key。
通过Java标准库的KeyGenerator生成一个安全的随机的key。
import java.math.BigInteger;
import javax.crypto.*;
public class Main {
public static void main(String[] args) throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance("HmacMD5");
SecretKey key = keyGen.generateKey();
// 打印随机生成的key:
byte[] skey = key.getEncoded();
System.out.println(new BigInteger(1, skey).toString(16));
Mac mac = Mac.getInstance("HmacMD5");
mac.init(key);
mac.update("HelloWorld".getBytes("UTF-8"));
byte[] result = mac.doFinal();
System.out.println(new BigInteger(1, result).toString(16));
}
}
使用HmacMD5的步骤是:
1、通过名称HmacMD5获取KeyGenerator实例;
2、通过KeyGenerator创建一个SecretKey实例;
3、通过名称HmacMD5获取Mac实例;
4、用SecretKey初始化Mac实例;
5、对Mac实例反复调用update(byte[])输入数据;
6、调用Mac实例的doFinal()获取最终的哈希值。
对称加密算法
对称加密算法就是传统的用一个密码进行加密和解密。例如,我们常用的WinZIP和WinRAR对压缩包的加密和解密,就是使用对称加密算法。
从程序的角度看,所谓加密,就是这样一个函数,它接收密码和明文,然后输出密文:
secret = encrypt(key, message);
而解密则相反,它接收密码和密文,然后输出明文:
plain = decrypt(key, secret);
密钥长度直接决定加密强度,而工作模式和填充模式可以看成是对称加密算法的参数和格式选择。DES算法由于密钥过短,可以在短时间内被暴力破解,现在已经不安全了。
使用AES加密
ES算法是目前应用最广泛的加密算法。我们先用ECB模式加密并解密:
Java标准库提供的对称加密接口非常简单,使用时按以下步骤编写代码:
根据算法名称/工作模式/填充模式获取Cipher实例;
根据算法名称初始化一个SecretKey实例,密钥必须是指定长度;
使用SerectKey初始化Cipher实例,并设置加密或解密模式;
传入明文或密文,获得密文或明文。
import java.security.*;
import java.util.Base64;
import javax.crypto.*;
import javax.crypto.spec.*;
public class Main {
public static void main(String[] args) throws Exception {
// 原文:
String message = "Hello, world!";
System.out.println("Message: " + message);
// 128位密钥 = 16 bytes Key:
byte[] key = "1234567890abcdef".getBytes("UTF-8");
// 加密:
byte[] data = message.getBytes("UTF-8");
byte[] encrypted = encrypt(key, data);
System.out.println("Encrypted: " + Base64.getEncoder().encodeToString(encrypted));
// 解密:
byte[] decrypted = decrypt(key, encrypted);
System.out.println("Decrypted: " + new String(decrypted, "UTF-8"));
}
// 加密:
public static byte[] encrypt(byte[] key, byte[] input) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKey keySpec = new SecretKeySpec(key, "AES");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
return cipher.doFinal(input);
}
// 解密:
public static byte[] decrypt(byte[] key, byte[] input) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKey keySpec = new SecretKeySpec(key, "AES");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
return cipher.doFinal(input);
}
}
ECB模式是最简单的AES加密模式,它只需要一个固定长度的密钥,固定的明文会生成固定的密文,这种一对一的加密方式会导致安全性降低,更好的方式是通过CBC模式,它需要一个随机数作为IV参数,这样对于同一份明文,每次生成的密文都不同:
import java.security.*;
import java.util.Base64;
import javax.crypto.*;
import javax.crypto.spec.*;
public class Main {
public static void main(String[] args) 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");
byte[] encrypted = encrypt(key, data);
System.out.println("Encrypted: " + Base64.getEncoder().encodeToString(encrypted));
// 解密:
byte[] decrypted = decrypt(key, encrypted);
System.out.println("Decrypted: " + new String(decrypted, "UTF-8"));
}
// 加密:
public static byte[] encrypt(byte[] key, byte[] input) 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[] data = cipher.doFinal(input);
// IV不需要保密,把IV和密文一起返回:
return join(iv, data);
}
// 解密:
public static byte[] decrypt(byte[] key, byte[] input) throws GeneralSecurityException {
// 把input分割成IV和密文:
byte[] iv = new byte[16];
byte[] data = new byte[input.length - 16];
System.arraycopy(input, 0, iv, 0, 16);
System.arraycopy(input, 16, data, 0, data.length);
// 解密:
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(data);
}
public static byte[] join(byte[] bs1, byte[] bs2) {
byte[] r = new byte[bs1.length + bs2.length];
System.arraycopy(bs1, 0, r, 0, bs1.length);
System.arraycopy(bs2, 0, r, bs1.length, bs2.length);
return r;
}
}
口令加密算法
用户输入的口令,通常还需要使用PBE算法,采用随机数杂凑计算出真正的密钥,再进行加密。
key = generate(userPassword, secureRandomPassword);
PBE的作用就是把用户输入的口令和一个安全随机的口令采用杂凑后计算出真正的密钥。以AES密钥为例,我们让用户输入一个口令,然后生成一个随机数,通过PBE算法计算出真正的AES口令,再进行加密。
PBE算法通过用户口令和安全的随机salt计算出Key,然后再进行加密;
Key通过口令和安全的随机salt计算得出,大大提高了安全性;
PBE算法内部使用的仍然是标准对称加密算法(例如AES)。
密钥交换算法
对称加密算法问题:不安全信道上如何传递密钥?——密钥交换算法(DH算法)应运而生
DH算法并未解决中间人攻击,即甲乙双方并不能确保与自己通信的是否真的是对方。消除中间人攻击需要其他方法。
非对称加密算法
公钥-私钥组成的密钥对是非常有用的加密方式,因为公钥是可以公开的,而私钥是完全保密的,由此奠定了非对称加密的基础。非对称加密就是加密和解密使用的不是相同的密钥:只有同一个公钥-私钥对才能正常加解密。
非对称加密的典型算法就是RSA算法。
非对称加密相比对称加密的显著优点在于,对称加密需要协商密钥,而非对称加密可以安全地公开各自的公钥,在N个人之间通信的时候:使用非对称加密只需要N个密钥对,每个人只管理自己的密钥对。而使用对称加密需要则需要N*(N-1)/2个密钥,因此每个人需要管理N-1个密钥,密钥管理难度大,而且非常容易泄漏。
非对称加密的缺点就是运算速度非常慢,比对称加密要慢很多。非对称加密总是和对称加密一起使用。假设小明需要给小红需要传输加密文件,他俩首先交换了各自的公钥,然后:
小明生成一个随机的AES口令,然后用小红的公钥通过RSA加密这个口令,并发给小红;
小红用自己的RSA私钥解密得到AES口令;
双方使用这个共享的AES口令用AES加密通信。
非对称加密实际上应用在第一步,即加密“AES口令”。
Java标准库提供了RSA算法的实现。
只使用非对称加密算法不能防止中间人攻击。
签名算法
私钥加密得到的密文实际上就是数字签名,要验证这个签名是否正确,只要用私钥持有者的公钥进行解密验证即可。使用数字签名的目的是为了确认某个信息确实是由某个发送方发送的,任何人都不可能伪造消息,并且,发送方也不能抵赖。
在实际应用的时候,签名实际上并不是针对原始消息,而是针对原始消息的哈希进行签名
DSA签名
除了RSA可以签名外,还可以使用DSA算法进行签名,使用ElGamal数字签名算法。
ECDSA签名
椭圆曲线签名算法ECDSA:可以从私钥推出公钥。比特币的签名算法就采用了ECDSA算法,使用标准椭圆曲线secp256k1。BouncyCastle提供了ECDSA的完整实现。
数字证书
摘要算法用来确保数据没有被篡改,非对称加密算法可以对数据进行加解密,签名算法可以确保数据完整性和抗否认性,把这些算法集合到一起,并搞一套完善的标准,就是数字证书。
数字证书采用链式签名管理,顶级的Root CA证书已内置在操作系统中。
数字证书存储的是公钥,可以安全公开,而私钥必须严格保密。