加密与安全
目录
一、URL编码:
二、Base64编码:
三、哈希算法:
四、Hmac算法:
五、对称加密算法:
一、URL编码:
URL
编码是浏览器发送数据给服务器时使用的编码,它通常附加在URL
的参数部分。之所以需要URL
编码,是因为出于兼容性考虑,很多服务器只识别ASCII
字符。但如果URL
中包含中文、日文这些非ASCII
字符怎么办?不要紧,URL
编码有一套规则:
- 如果字符是
A
~Z
,a
~z
,0
~9
以及-
、_
、.
、*
,则保持不变; - 如果是其他字符,先转换为
UTF-8
编码,然后对每个字节以%XX
表示。
例如:字符"中"的UTF-8
编码是0xe4b8ad
,因此,它的URL
编码是%E4%B8%AD
。URL
编码总是大写。
Java
标准库提供了一个URLEncoder
类来对任意字符串进行URL
编码:
import java.net.URLEncoder;
public class Main {public static void main(String[] args) {String encoded = URLEncoder.encode("中文!", "utf-8");System.out.println(encoded);}
}
URL
编码是编码算法,不是加密算法。URL
编码的目的是把任意文本数据编码为%
前缀表示的文本,编码后的文本仅包含A
~Z
,a
~z
,0
~9
,-
,_
,.
,*
和%
,便于浏览器和服务器处理。
二、Base64编码:
Base64
编码是对二进制数据进行编码,表示成文本格式。Base64
编码可以把任意长度的二进制数据变为纯文本,并且纯文本内容中且只包含指定字符内容:A
~Z
、a
~z
、0
~9
、+
、/
、=
。它的原理是把3
字节的二进制数据按6bit
一组,用4
个整数表示,然后查表,把整数用索引对应到字符,得到编码后的字符串。6
位整数的范围总是0
~63
,所以,能用64
个字符表示:字符A
~Z
对应索引0
~25
,字符a
~z
对应索引26
~51
,字符0
~9
对应索引52
~61
,最后两个索引62
、63
分别用字符+
和/
表示。
base64码表:
码值 | 字符 | 码值 | 字符 | 码值 | 字符 | 码值 | 字符 |
---|---|---|---|---|---|---|---|
0 | A | 16 | Q | 32 | g | 48 | w |
1 | B | 17 | R | 33 | h | 49 | x |
2 | C | 18 | S | 34 | i | 50 | y |
3 | D | 19 | T | 35 | j | 51 | z |
4 | E | 20 | U | 36 | k | 52 | 0 |
5 | F | 21 | V | 37 | l | 53 | 1 |
6 | G | 22 | W | 38 | m | 54 | 2 |
7 | H | 23 | X | 39 | n | 55 | 3 |
8 | I | 24 | Y | 40 | o | 56 | 4 |
9 | J | 25 | Z | 41 | p | 57 | 5 |
10 | K | 26 | a | 42 | q | 58 | 6 |
11 | L | 27 | b | 43 | r | 59 | 7 |
12 | M | 28 | c | 44 | s | 60 | 8 |
13 | N | 29 | d | 45 | t | 61 | 9 |
14 | O | 30 | e | 46 | u | 62 | + |
15 | P | 31 | f | 47 | v | 63 | / |
在Java
中,二进制数据就是byte[]
数组。Java
标准库提供了Base64
来对byte[]
数组进行编解码:
public class Main {public static void main(String[] args) {byte[] input = new byte[] { (byte) 0xe4, (byte) 0xb8, (byte) 0xad };String b64encoded = Base64.getEncoder().encodeToString(input);System.out.println(b64encoded);}
}
编码后得到字符串结果:5Lit
。要对这个字符使用Base64
解码,仍然用Base64
这个类:
public class Main {public static void main(String[] args) {byte[] output = Base64.getDecoder().decode("5Lit");System.out.println(Arrays.toString(output)); // [-28, -72, -83]}
}
三、哈希算法:
哈希算法(Hash
)又称摘要算法(Digest
),它的作用是:对任意一组输入数据进行计算,得到一个固定长度的输出摘要。哈希算法最重要的特点就是:
- 相同的输入一定得到相同的输出;
- 不同的输入大概率得到不同的输出。
所以,哈希算法的目的:为了验证原始数据是否被篡改。Java
字符串的hashCode()
就是一个哈希算法,它的输入是任意字符串,输出是固定的4
字节int
整数:
"hello".hashCode(); // 0x5e918d2
"hello, java".hashCode(); // 0x7a9d88e8
"hello, bob".hashCode(); // 0xa0dbae2f
1、哈希碰撞:两个不同的输入得到了相同的输出。(不能避免,安全性低)
"AaAaAa".hashCode(); // 0x7460e8c0
"BBAaBB".hashCode(); // 0x7460e8c0"通话".hashCode(); // 0x11ff03
"重地".hashCode(); // 0x11ff03
2、常用的哈希算法:
算法 | 输出长度(位) | 输出长度(字节) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1、MD5算法:
使用MessageDigest
时,我们首先根据哈希算法获取一个MessageDigest
实例,然后,反复调用update(byte[])
输入数据。当输入结束后,调用digest()
方法获得byte[]
数组表示的摘要,最后,把它转换为十六进制的字符串。
import java.security.MessageDigest;public class main {public static void main(String[] args) {// 创建一个MessageDigest实例:MessageDigest md = MessageDigest.getInstance("MD5");// 反复调用update输入数据:md.update("Hello".getBytes("UTF-8"));md.update("World".getBytes("UTF-8"));// 16 bytes: 68e109f0f40ca72a15e05cc22786f8e6byte[] results = md.digest(); StringBuilder sb = new StringBuilder();for(byte bite : results) {sb.append(String.format("%02x", bite));}System.out.println(sb.toString());}
}
2、哈希算法的用途:
(1)校验下载文件
(2)存储用户密码:(要避免彩虹表攻击,对每个口令额外添加随机数,这个方法称之为加盐(salt
)digest = md5(salt + inputPassword)
)
3、SHA-1算法:
SHA-1
也是一种哈希算法,它的输出是160 bits
,即20
字节。SHA-1
是由美国国家安全局开发的,SHA
算法实际上是一个系列,包括SHA-0
(已废弃)、SHA-1
、SHA-256
、SHA-512
等。在Java
中使用SHA-1
,和MD5
完全一样,只需要把算法名称改为"SHA-1
":
import java.security.MessageDigest;public class main {public static void main(String[] args) {// 创建一个MessageDigest实例:MessageDigest md = MessageDigest.getInstance("SHA-1");// 反复调用update输入数据:md.update("Hello".getBytes("UTF-8"));md.update("World".getBytes("UTF-8"));// 20 bytes: db8ac1c259eb89d4a131b253bacfca5f319d54f2byte[] results = md.digest(); StringBuilder sb = new StringBuilder();for(byte bite : results) {sb.append(String.format("%02x", bite));}System.out.println(sb.toString());}
}
4、RipeMD160:
Java
标准库的java.security
包提供了一种标准机制,允许第三方提供商无缝接入。我们要使用BouncyCastle
提供的RipeMD160
算法,需要先把BouncyCastle
注册一下
public class Main {public static void main(String[] args) throws Exception {// 注册BouncyCastle提供的通知类对象BouncyCastleProviderSecurity.addProvider(new BouncyCastleProvider());// 获取RipeMD160算法的"消息摘要对象"(加密对象)MessageDigest md = MessageDigest.getInstance("RipeMD160");// 更新原始数据md.update("HelloWorld".getBytes());// 获取消息摘要(加密)byte[] result = md.digest();// 消息摘要的字节长度和内容System.out.println(result.length); // 160位=20字节System.out.println(Arrays.toString(result));// 16进制内容字符串String hex = new BigInteger(1,result).toString(16);System.out.println(hex.length()); // 20字节=40个字符System.out.println(hex);}
}
四、Hmac算法:
mac
算法就是一种基于密钥的消息认证码算法,它的全称是Hash-based Message Authentication Code
,是一种更安全的消息摘要算法。Hmac
算法总是和某种哈希算法配合起来用的。例如我们使用MD5
算法,对应的就是Hmac MD5
算法,它相当于“加盐”的MD5
:HmacMD5 ≈md5(secure_random_key, input)
HmacMD5加密:
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;public class main {public static void main(String[] args) throws NoSuchAlgorithmException, IllegalStateException, UnsupportedEncodingException, InvalidKeyException {// 获取HmacMD5秘钥生成器KeyGenerator keyGen = KeyGenerator.getInstance("HmacMD5");// 产生秘钥SecretKey secreKey = keyGen.generateKey();// 打印随机生成的秘钥:byte[] keyArray = secreKey.getEncoded();StringBuilder key = new StringBuilder();for(byte bite:keyArray) {key.append(String.format("%02x", bite));}System.out.println(key);// 使用HmacMD5加密Mac mac = Mac.getInstance("HmacMD5");mac.init(secreKey); // 初始化秘钥mac.update("HelloWorld".getBytes("UTF-8"));byte[] resultArray = mac.doFinal();StringBuilder result = new StringBuilder();for(byte bite:resultArray) {result.append(String.format("%02x", bite));}System.out.println(result);}
}
解密:
// 原始密码
String password = "nhmyzgq";// 通过"秘钥的字节数组",恢复秘钥
byte[] keyByteArray = {126, 49, 110, 126, -79, -5, 66, 34, -122, 123, 107, -63, 106, 100, -28, 67, 19, 23, 1, 23, 47, 63, 47, 109, 123, -111, -27, -121, 103, -11, 106, -26, 110, -27, 107, 40, 19, -8, 57, 20, -46, -98, -82, 102, -104, 96, 87, -16, 93, -107, 25, -56, -113, 12, -49, 96, 6, -78, -31, -17, 100, 19, -61, -58};// 恢复秘钥
SecretKey key = new SecretKeySpec(keyByteArray,"HmacMD5");// 加密
Mac mac = Mac.getInstance("HmacMD5");
mac.init(key);
mac.update(password.getBytes());
byte[] resultByteArray = mac.doFinal();StringBuilder resultStr = new StringBuilder();
for(byte b : resultByteArray) {resultStr.append(String.format("%02x", b));
}
System.out.println("加密结果:" + resultStr);
五、对称加密算法:
对称加密算法就是传统的用一个秘钥进行加密和解密。例如,我们常用的WinZIP
和WinRAR
对压缩包的加密和解密,就是使用对称加密算法:
从程序的角度看,所谓加密,就是这样一个函数,它接收密码和明文,然后输出密文:
secret = encrypt(key, message);
而解密则相反,它接收密码和密文,然后输出明文:
plain = decrypt(key, secret);
在软件开发中,常用的对称加密算法有:
算法 | 密钥长度 | 工作模式 | 填充模式 |
DES | 56/64 | ECB/CBC/PCBC/CTR/... | NoPadding/PKCS5Padding/... |
AES | 128/192/256 | ECB/CBC/PCBC/CTR/... | NoPadding/PKCS5Padding/PKCS7Padding/... |
IDEA | 128 | ECB | PKCS5Padding/PKCS7Padding/... |
密钥长度直接决定加密强度,而工作模式和填充模式可以看成是对称加密算法的参数和格式选择。Java
标准库提供的算法实现并不包括所有的工作模式和所有填充模式。
1、使用AES加密:
(1)ECB模式:ECB
模式是最简单的AES
加密模式,它需要一个固定长度的密钥,固定的明文会生成固定的密文。
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();// 加密:byte[] data = message.getBytes();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));}// 加密:public static byte[] encrypt(byte[] key, byte[] input) throws GeneralSecurityException {// 创建密码对象,需要传入算法/工作模式/填充模式Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");// 根据key的字节内容,"恢复"秘钥对象SecretKey keySpec = new SecretKeySpec(key, "AES");// 初始化秘钥:设置加密模式ENCRYPT_MODEcipher.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");// 根据key的字节内容,"恢复"秘钥对象SecretKey keySpec = new SecretKeySpec(key, "AES");// 初始化秘钥:设置解密模式DECRYPT_MODEcipher.init(Cipher.DECRYPT_MODE, keySpec);// 根据原始内容(字节),进行解密return cipher.doFinal(input);}
}
Java
标准库提供的对称加密接口非常简单,使用时按以下步骤编写代码:
(1)根据算法名称/工作模式/填充模式获取Cipher
实例;
(2)根据算法名称初始化一个SecretKey
实例,密钥必须是指定长度;
(3)使用SerectKey
初始化Cipher
实例,并设置加密或解密模式;
(4)传入明文或密文,获得密文或明文。
2、CBC模式: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();// 加密:byte[] data = message.getBytes();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));}// 加密:public static byte[] encrypt(byte[] key, byte[] input) throws GeneralSecurityException {// 设置算法/工作模式CBC/填充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); // 生成16个字节的随机数System.out.println(Arrays.toString(iv));IvParameterSpec ivps = new IvParameterSpec(iv); // 随机数封装成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); // IVSystem.arraycopy(input, 16, data, 0, data.length); //密文System.out.println(Arrays.toString(iv));// 解密:Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); // 密码对象SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); // 恢复秘钥IvParameterSpec ivps = new IvParameterSpec(iv); // 恢复IV// 初始化秘钥:操作模式、秘钥、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;}
}