JavaSE(九)加密與安全
加密與安全
加密與安全
資料安全:防竊聽、防篡改、防偽造
編碼演算法
URL編碼
URL編碼的目的是把任意文字資料編碼為%字首表示文字
編碼規則:
1)A-Z,a-z,0-9以及- _ . *保持不變
2)其他字元以%XX表示
<:%3C
中:%E4%B8%AD(UTF-8:0xe4b8ad)
String orginal = "URL 引數";
String encoded = URLEncoder.encode(orginal, "UTF-8");
System.out.println(encoded); // URL+%E5%8F%82%E6%95%B0
String ori = new String(URLDecoder. decode(encoded, "UTF-8"));
System.out.println(ori); // URL 引數
Base64編碼
Base64編碼的目的是把任意二進位制資料編碼為文字(長度增加1/3)
String base64Encode(byte[] data)
byte[]{0xe4, 0xb8, 0xad} -> "5Lit"
Base64編碼是一種文字(a-z,A-Z,0-9,+/=)表示二進位制內容的方式,適用於文字協議,但是效率會下降,Base64應用於:電子郵件協議
Base64編碼長度如果不是3的整數倍,會末尾補0x00或0x00 0x00,編碼後加一個等號(=)表示補充了1個位元組,編碼後加兩個等號(==)表示補充了2個位元組。
public class Main {
public static void main(String[] args) throws Exception {
String original = "Hello\u00ff編碼測試";
String b64 = Base64.getEncoder().encodeToString(original.getBytes());
System.out.println(b64);
String ori = new String(Base64.getDecoder().decode(b64), "UTF-8");
System.out.println(ori);
}
}
// SGVsbG/Dv+e8lueggea1i+ivlQ==
// Helloÿ編碼測試
// 還可以利用withoutPadding()去掉末尾的等號
String b64 = Base64.getEncoder().withoutPadding().encodeToString(original.getBytes());
// SGVsbG/Dv+e8lueggea1i+ivlQ
// 使用URL的Base64編碼是 加號(+)變減號(-),反斜線(/)變下劃線(_)
String original = "Hello\u00ff編碼測試";
String b64 = Base64.getUrlEncoder().withoutPadding().encodeToString(original.getBytes());
System.out.println(b64);
String ori = new String(Base64.getUrlDecoder().decode(b64), "UTF-8");
System.out.println(ori);
// SGVsbG_Dv-e8lueggea1i-ivlQ
// Helloÿ編碼測試
其他類似Base64的編碼有:Base32、Base48、Base58
摘要演算法
摘要演算法(雜湊演算法 / Hash / Digest / 數字指紋)
計算任意長度資料的摘要,輸出固定長度,相同的輸入始終得到相同的輸出,不同的輸入儘量得到不同的輸出
Java的Object.hashCode()方法就是一個摘要演算法
輸入:任意資料
輸出:固定長度資料(int,byte[4])
相同的輸入得到相同的輸出:equals、hashCode
碰撞:兩個不同的輸入得到了相同的輸出
Hash演算法的安全性:
1)碰撞率低
2)不能猜測輸出
3)輸入的任意一個bit的變化會造成輸出完全不同
4)很難從輸出反推輸入(只能窮舉)
常用的摘要演算法
演算法 | 輸出長度 | |
---|---|---|
MD5 | 128bits | 16bytes |
SHA-1 | 160bits | 20bytes |
SHA-256 | 256bits | 32bytes |
RipeMD-160 | 160bits | 20bytes |
MD5
1)驗證原始資料是否被篡改
2)儲存使用者口令
3)需要防止彩虹表攻擊:對每個口令額外新增隨機數salt
用途:驗證檔案的完整性、儲存使用者口令
md5(inputPassword)
md5(salt + inputPassword)
import java.math.BigInteger;
import java.security.MessageDigest;
public class Main {
public static byte[] toMD5(byte[] input) {
MessageDigest md;
try {
md = MessageDigest.getInstance("MD5");
} catch (Exception e) {
throw new RuntimeException(e);
}
md.update(input);
return md.digest();
}
public static void main(String[] args) throws Exception {
String s = "Md5摘要演算法測試";
byte[] r = toMD5(s.getBytes("UTF-8"));
System.out.println(String.format("%032x", new BigInteger(1, r)));
}
}
String password = "Hello, world";
String salt = "Random salt";
byte[] r = toMD5((salt + password).getBytes("UTF-8"));
System.out.println(String.format("%032x", new BigInteger(1, r)));
SHA1
是由美國國家安全域性開發的一種雜湊演算法,輸出160bits或20bytes,主要有SHA-0 / SHA-1 / SHA-256 / SHA-512,比MD5更加安全
演算法 | 輸出長度 | |
---|---|---|
SHA-1 | 160bits | 20bytes |
SHA-256 | 256bits | 32bytes |
SHA-512 | 512bits | 64bytes |
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(input);
...
byte[] result = md.digest(); // 20bytes
BouncyCastle
是第三方演算法提供的一組加密/雜湊演算法,官網:http://www.bouncycastle.org/
如何使用第三方提供的演算法
新增第三方jar至classpath
註冊第三方演算法提供方 Security.addProvider(new BouncyCastleProvider());//註冊
正常使用JDK提供的介面
Security.addProvider(new BouncyCastleProvider());//註冊
MessageDigest md = MessageDigest.getInstance("RipeMD160");
md.update(input);
...
byte[] result = md.digest(); // 20bytes
Hmac
Hash-based Message Authentication Code,是基於祕鑰的訊息認證碼演算法,是一種更安全的訊息摘要演算法
HmacMd5 ≈ md5(secure_ksy, data);
Hmac是把Key混入摘要的演算法
byte data = ...
KeyGenerator keyGen = KeyGenerator.getInstance("HmacMD5");
SecretKey skey = keyGen.generateKey();
Mac mac = Mac.getInstance("HmacMD5");
mac.init(skey);
mac.update(data);
byte[] result = mac.doFinal();
可以配合MD5、SHA-1等摘要演算法
摘要長度和原摘要演算法長度相同
加密演算法
對稱加密演算法
使用同一個金鑰進行加密和解密
常用演算法:DES、AES、IDEA等
演算法 | 金鑰長度 | 工作模式 | 填充模式 |
---|---|---|---|
DES | 56/64 | ECB/CBC/PCBC/CTR/… | NoPadding/PKCS5Padding… |
AES | 128/192/256 | ECB/CBC/PCBC/CTR/… | NoPadding/PKCS5Padding/PKCS7Padding… |
IDEA | 128 | ECB | PKCS5Padding/PKCS5Padding… |
金鑰長度由演算法設計決定,AES的金鑰長度是128 / 192 / 256
使用對稱加密演算法需要指定:演算法名稱 / 工作模式(ECB、CBC、PCBC…)/填充模式(NoPadding、PKCS5Padding、PKCS7Padding…)
ECB模式
public class AES_ECB_Cipher {
static final String CIPHER_NAME = "AES/ECB/PKCS5Padding";
/**
* 加密
*/
public static byte[] encrypt(byte[] key, byte[] input) throws Exception{
Cipher cipher = Cipher.getInstance(CIPHER_NAME);
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
return cipher.doFinal(input);
}
/**
* 解密
*/
public static byte[] decrpty(byte[] key, byte[] input) throws Exception{
Cipher cipher = Cipher.getInstance(CIPHER_NAME);
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
return cipher.doFinal(input);
}
public static void main(String[] args) throws Exception{
// 原文:
String message = "Hello, world! encrypted using AES!";
System.out.println("Message: " + message);
// 128位金鑰 = 16 bytes Key:
byte[] key = "1234567890abcdef".getBytes("UTF-8");
// 加密:
byte[] data = message.getBytes(StandardCharsets.UTF_8);
byte[] encrypted = encrypt(key, data);
System.out.println("Enctypted data: " + Base64.getEncoder().encodeToString(encrypted));
// 解密:
byte[] decrypted = decrpty(key, encrypted);
System.out.println("Decrypted data: " + new String(decrypted, "UTF-8"));
}
}
// Message: Hello, world! encrypted using AES!
// Enctypted data: 6ofAje3dbEseeIBkwKEonQIUi09dPO9fVx4OgZ7ozsE7BWtJJdcJs1+N58l1mWqh
// Decrypted data: Hello, world! encrypted using AES!
CBC模式
public class AES_CBC_Cipher {
static final String CIPHER_NAME = "AES/CBC/PKCS5Padding";
/**
* 加密
*/
public static byte[] encrypt(byte[] key, byte[] input) throws Exception {
Cipher cipher = Cipher.getInstance(CIPHER_NAME);
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
// CBC模式需要生成一個16位的initialization vector
SecureRandom sr = SecureRandom.getInstanceStrong();
byte[] iv = sr.generateSeed(16);
IvParameterSpec ipvs = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ipvs);
byte[] data = cipher.doFinal(input);
// IV不需要保密,把IV和密文一起返回
return join(iv, data);
}
/**
* 解密
*/
public static byte[] decrpty(byte[] key, byte[] input) throws Exception {
// 把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(CIPHER_NAME);
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;
}
public static void main(String[] args) throws Exception {
// 原文:
String message = "Hello, world! encrypted using AES!";
System.out.println("Message: " + message);
// 128位金鑰 = 16 bytes Key:
byte[] key = "1234567890abcdef".getBytes("UTF-8");
// 加密:
byte[] data = message.getBytes(StandardCharsets.UTF_8);
byte[] encrypted = encrypt(key, data);
System.out.println("Enctypted data: "
+ Base64.getEncoder().encodeToString(encrypted));
// 解密:
byte[] decrypted = decrpty(key, encrypted);
System.out.println("Decrypted data: " + new String(decrypted, "UTF-8"));
}
}
// Message: Hello, world! encrypted using AES!
// Enctypted data: 0snryGJc4joIzXM2gq/qIY2Vzh/unufhvXfO4k7sk79whZ7nTf30YvO64SihyO2LaA0Ab7KIO9pDzLW8Z/c9Xg==
// Decrypted data: Hello, world! encrypted using AES!
AES256
使用256位加密需要修改JDK的policy檔案
https://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html下載,切換到目錄D:\Program Files\Java\jdk1.8.0_131\jre\lib\security下替換調 local_policy.jar 和 US_export_policy.jar
public class AES256_ECB_Cipher {
static final String CIPHER_NAME = "AES/ECB/PKCS5Padding";
/**
* 加密
*/
public static byte[] encrypt(byte[] key, byte[] input) throws Exception{
Cipher cipher = Cipher.getInstance(CIPHER_NAME);
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
return cipher.doFinal(input);
}
/**
* 解密
*/
public static byte[] decrpty(byte[] key, byte[] input) throws Exception{
Cipher cipher = Cipher.getInstance(CIPHER_NAME);
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
return cipher.doFinal(input);
}
public static void main(String[] args) throws Exception{
// 原文:
String message = "Hello, world! encrypted using AES!";
System.out.println("Message: " + message);
// 256位金鑰 = 32 bytes Key:
byte[] key = "1234567890abcdef1234567890abcdef".getBytes("UTF-8");
// 加密:
byte[] data = message.getBytes(StandardCharsets.UTF_8);
byte[] encrypted = encrypt(key, data);
System.out.println("Enctypted data: " + Base64.getEncoder().encodeToString(encrypted));
// 解密:
byte[] decrypted = decrpty(key, encrypted);
System.out.println("Decrypted data: " + new String(decrypted, "UTF-8"));
}
}
// Message: Hello, world! encrypted using AES!
// Enctypted data: zXgDofZMm8UHUqJJdyt4SBQToTtvTWjGUeh/XFlTzJr515ZUmGEgTWOL08g++rfL
// Decrypted data: Hello, world! encrypted using AES!
口令加密演算法
PBE(Password Based Encryption)演算法:
1)由使用者輸入口令,採用隨機數雜湊計算出金鑰進行加密
2)Password:使用者口令,例如“hello 123”
3)Salt:隨機生成byte[]
4)Key:generate(byte[] salt, String password)
Key通過口令和隨機salt計算得出,提高安全性
PBE演算法內部使用的仍然是標準對稱加密演算法(例如AES)
public class PBECipher {
static final String CIPHER_NAME = "PBEwithSHA1and128bitAES-CBC-BC";
/**
* 加密
*/
public static byte[] encrypt(String password, byte[] salt, byte[] input) throws Exception {
PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
SecretKeyFactory sKeyFactory = SecretKeyFactory
.getInstance(CIPHER_NAME);
SecretKey skey = sKeyFactory.generateSecret(keySpec);
PBEParameterSpec pbeps = new PBEParameterSpec(salt, 1000);
Cipher cipher = Cipher.getInstance(CIPHER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, skey, pbeps);
return cipher.doFinal(input);
}
/**
* 解密
*/
public static byte[] decrypt(String password, byte[] salt, byte[] input) throws Exception {
PBEKeySpec