資料安全
什麼是資料安全?假如Bob要給Alice傳送一封郵件,在傳送郵件的過程中,黑客可能會竊取到郵件的內容,所以我們需要防竊聽;黑客也有可能會篡改郵件的內容,所以Alice必須要有能有去識別郵件是否被篡改;最後,黑客也可能假冒Bob給Alice傳送郵件,所以Alice還必須有能力識別出偽造的郵件。所以資料安全的幾個要點就是:防竊聽、防篡改和防偽造。
古代的加密方式:
- 移位密碼:HELLO => IFMMP (把英文字母按順序往後移動幾位,這裡就是HELLO中的每個字母向後移動一位,就變成了IFMMP)
- 替代密碼:HELLO => p12,5,3(用某個書籍的某一頁某一行的第幾個單詞來記錄資訊)
現代計算機加密:
- 建立在嚴格的數學理論基礎上
- 密碼學逐漸發展成一門科學
總結:
- 設計一個安全的加密演算法非常困難
- 驗證一個加密演算法是否安全更加困難
- 當前被認為安全的加密演算法僅僅是迄今為止尚未被攻破
- 不要自己去設計加密演算法
- 不要自己去實現加密演算法
- 不要自己修改已有的加密演算法
編碼演算法
ASCII編碼就是一種編碼,部分編碼如下:
字母 | 編碼(16進位制) |
---|---|
A | 0x41 |
B | 0x42 |
C | 0x43 |
D | 0x44 |
... | ... |
漢字使用不同的編碼演算法,得到的編碼是不一樣的,漢字是使用Unicode編碼後是兩個位元組,經過UTF-8編碼後得到三個位元組:
漢字 | Unicode編碼 | UTF-8編碼 |
---|---|---|
中 | 0x4e2d | 0xe4b8ad |
文 | 0x6587 | 0xe69687 |
編 | 0x7f16 | 0xe7bc96 |
碼 | 0x7801 | 0xe7a081 |
... | ... | ... |
URL編碼是瀏覽器傳送資料給伺服器時使用的編碼:
- key1=value1&key2=value2&key3=value3
- q=%E4%B8%AD%E6%95%87
URL編碼規則: - A~Z,a~z,0~9以及-_.*保持不變
- 其它字元以%xx(以%開頭的16進位制來表示)
- <: %3C
- 中:%E4%B8%AD (正好對應UTF-8編碼的16進位制: 0xe4b8ad)
public static void main(String[] args) throws Exception {
String orginal = "URL 引數";
// URL 編碼
String encode = URLEncoder.encode(orginal, "UTF-8");
System.out.println(encode); // URL+%E5%8F%82%E6%95%B0
// URL解碼
String decode = URLDecoder.decode(encode, "UTF-8");
System.out.println(decode); // URL 引數
}
通過執行結果可以看到:URL編碼英文字母保持不變,空格編碼為"+",一箇中文經過UTF-8編碼後,通常是以%開頭的16進位制編碼。
總結:URL編碼是編碼演算法,不是加密演算法;URL編碼的目的是把任意文字資料編碼為%字首表示的文字,編碼後的文字僅包含A~Z,a~z,0~9,-_.*,%,便於瀏覽器和伺服器處理。
Base64編碼:一種把二進位制資料用文字表示的編碼演算法,例如我們有一個位元組陣列byte[]{0xe4,0xb8,0xad},通過Base64編碼後得到的字串為"5Lit"。如何使用Base64進行編碼?假如我們把漢字“中”用UTF8表示的位元組表示出來,它就是{0xe4,0xb8,0xad},這三個位元組就是24位(11100100 10111000 10101101),我們把這24位按照每6位分組就形成4個位元組,這四個位元組對應的16進位制就是{0x39,0x0b,0x22,0x2d},通過查表就可得到分別對應的是{5,L,i,T},所以最終編碼出來的字串就是5LiT。Base64對應的編碼表從索引0開始,如下:
索引 | 編碼 | 索引 | 編碼 | 索引 | 編碼 | 索引 | 編碼 |
---|---|---|---|---|---|---|---|
0 | A | 25 | Z | 51 | z | 61 | 9 |
1 | B | 26 | a | 52 | 0 | 62 | + |
2 | C | 27 | b | 53 | 1 | 63 | / |
3 | D | 28 | c | 54 | 2 | ||
... | ... | ... | ... | ... | ... |
使用Base64編碼的目的:一種用文字(A~Z,a~z,0~9,+/=)表示二進位制內容的方式,適用於文字協議,但效率會下降(因為二進位制經過Bse64編碼長度會增加1/3),應用比如電子郵件協議。如果陣列的長度不是3的整數倍,末尾補0x00或0x00 0x00,編碼後加=表示補充了一個位元組,編碼後加==表示補充了2個位元組。在解碼時就可以去掉補充的位元組。
public static void main(String[] args) throws UnsupportedEncodingException {
String orignal = "Hello\u00ff編碼測試";
// String b64 = Base64.getEncoder().encodeToString(orignal.getBytes("UTF-8"));
//去掉等號,實際上有沒等號在解碼時是不影響的
String b64 = Base64.getEncoder().withoutPadding().encodeToString(orignal.getBytes("UTF-8"));
System.out.println(b64);
String ori = new String(Base64.getDecoder().decode(b64), "UTF-8");
System.out.println(ori);
//實現URL的Base64編碼和解碼
String urlB64 = Base64.getUrlEncoder().withoutPadding().encodeToString(orignal.getBytes("UTF-8"));
System.out.println(urlB64);
String urlOri = new String(Base64.getUrlDecoder().decode(urlB64), "UTF-8");
System.out.println(urlOri);
//在Java中,使用URL的Base64編碼,它會把"+"變為"-",把"/"變為"_",這樣我們在傳遞URL引數的時候,就不會引起衝突
}
總結:Base64是編碼演算法,不是加密演算法;Base64編碼的目的是把任意二進位制資料編碼為文字(長度增加1/3);其它編碼:Base32,Base48,Base58
摘要演算法
摘要演算法(雜湊演算法/Hash/Digst/數字指紋),計算任意長度資料的摘要(固定長度),相同資料的輸入始終得到相同的輸出,不同的輸入資料儘量得到不同的輸出,目的是為了驗證原始資料是否被篡改。如果我們的輸入是任意長度的資料,而輸出的是固定長度的資料,我們就可以稱之為摘要演算法。Java中Object的hashCode()方法就是一個摘要演算法。什麼是碰撞呢?碰撞是指兩個不同的輸入得到了相同的輸出,而且碰撞是不能避免的,這是因為輸出的位元組長度是固定的,而輸入的位元組的長度是不固定的,所以hash演算法實際上是將一個無限的輸入集合對映到一個有限的輸出集合。
Hash演算法的安全性:
- 碰撞率低
- 不能猜測輸出
- 輸入的任意一個bit的變化會造成輸出完全不同
- 很難以從輸出反推輸入(只能依靠暴力窮舉)
常用的摘要演算法
演算法 | 輸出長度 | |
MD5 | 128 bits | 16 bytes |
SHA-1 | 160 bits | 20 bytes |
SHA-256 | 256 bits | 32 bytes |
RipeMD-160 | 160 bits | 20 bytes |
MD5演算法
在Java中使用MD5:
public static void main(String[] args) throws Exception {
MessageDigest digest1 = MessageDigest.getInstance("MD5");
digest1.update("helloworld".getBytes("UTF-8"));
byte[] result1 = digest1.digest();
for (byte b : result1) {
System.out.print(b + "\t"); // -4 94 3 -115 56 -91 112 50 8 84 65 -25 -2 112 16 -80
}
System.out.println();
//輸入的資料可以分片輸入,得到的結果是一樣的
MessageDigest digest2 = MessageDigest.getInstance("MD5");
digest2.update("hello".getBytes("UTF-8"));
digest2.update("world".getBytes("UTF-8"));
byte[] result2 = digest2.digest();
for (byte b : result2) {
System.out.print(b + "\t"); // -4 94 3 -115 56 -91 112 50 8 84 65 -25 -2 112 16 -80
}
}
MD5用途:可以用來驗證檔案的完整性,比如我們在MySQL網站下載mysql時,mysql網站會給出每一個下載檔案的MD5值,在下完檔案後,通過計算MD5和網站給出的MD5對比,就可以計算出檔案在下載過程中是否出現錯誤。
MD5儲存使用者口令,由於系統不儲存使用者原始口令(例如資料庫中儲存的密碼),系統儲存使用者原始口令的MD5。如何判斷使用者口令是否正確?系統計算使用者輸入的原始口令的MD5和資料庫儲存的MD5進行對比,相同則口令正確,不相同則口令錯誤。使用MD5要避免彩虹表攻擊,什麼是彩虹表呢?彩虹表就是預先計算好的常用口令。為了抵禦彩虹表攻擊,通常我們需要對每個口令額外新增隨機數salt。
SAH-1演算法
SAH-1演算法是一種雜湊演算法,輸出160 bits / 20 bytes,美國國家安全域性開發,常見的有SHA-1 / SHA-256 / SHA-512。SAH-1演算法是比MD5更安全的雜湊演算法。
BouncyCastle演算法
BouncyCastle是第三方提供的一組加密/雜湊演算法,提供JDK沒有提供的演算法(RipeMD160 演算法),如何使用第三方提供的演算法?先新增第三方jar至classpath,註冊第三方演算法提供方(通過Security.addProvider()註冊),正常使用JDK提供的介面。
Hmac演算法
Hmac:Hash-based Message Authentication Code的縮寫,基於金鑰的訊息認證碼演算法,是更安全的訊息摘要演算法。HmacMD5相當於md5(secure_random_key,data),所以HmacMD5可以看作帶安全Salt的MD5。Hmac是把key混入摘要的演算法,並不是新發明的一種演算法,必須配合MD5,SHA-1等摘要演算法,摘要長度和原摘要演算法長度相同。
加密演算法
對稱加密演算法
對稱加密演算法的加密和解密使用同一個金鑰,例如WinRAR,我們在對檔案進行壓縮時,可以設一個密碼,再解壓時,我們需要使用 同一個密碼才能進行解壓,winRAR就是使用的對稱加密演算法。加密:encrypt(金鑰key,原文message)->密文s,解密:decrypt(金鑰key,密文s)-> 原文message。常用的對稱加密演算法有DES,AES,IDEA等。由於DES的金鑰較短,可以在短時間內暴力破解,現在已經不使用了。
Java使用 AES的ECB模式下的加密和解密:
public class AES_ECB_Cipher {
private 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);
//通過doFinal()得到加密後的位元組陣列
return cipher.doFinal(input);
}
//解密
public static byte[] decrypt(byte[] key, byte[] input) throws Exception {
Cipher cipher = Cipher.getInstance(CIPHER_NAME);
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
//使用解密模式
cipher.init(Cipher.DECRYPT_MODE, keySpec);
//通過doFinal()將密文還原為原文
return cipher.doFinal(input);
}
public static void main(String[] args) throws Exception {
//原文
String message = "Hello, World! encrypted using AES";
System.out.println("Message: " + message); // message: Hello, World! encrypted using AES
//128位金鑰 = 16 bytes key
byte[] key = "1234567890abcdef".getBytes("UTF-8");
//加密
byte[] data = message.getBytes(StandardCharsets.UTF_8);
byte[] encrypted = encrypt(key, data);
//加密後的密文: Encrypted data: g89TtEMHXpwwjrEbXcljDQIUi09dPO9fVx4OgZ7ozsFgo8Zilj6cypxChst75GTR
System.out.println("Encrypted data: " + Base64.getEncoder().encodeToString(encrypted));
//解密
byte[] decrypted = decrypt(key, encrypted);
//解密後得到結果與原文相同:Decrypted data: Hello, World! encrypted using AES
System.out.println("Decrypted data: " + new String(decrypted,"UTF-8"));
}
}
Java使用 AES的CBC模式下的加密和解密:
public class AES_CBC_Cipher {
private 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位元組的initiallization vector
SecureRandom sr = SecureRandom.getInstanceStrong();
//獲取向量,即16位位元組的隨機數
byte[] iv = sr.generateSeed(16);
//把位元組陣列轉為IvParameterSpec物件
IvParameterSpec ivps = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivps);
byte[] data = cipher.doFinal(input);
//IV不需要保密,把IV和密文一起返回
return join(iv, data);
}
private static byte[] join(byte[] iv, byte[] data) {
byte[] r = new byte[iv.length + data.length];
System.arraycopy(iv, 0 ,r, 0, iv.length);
System.arraycopy(data, 0 ,r, iv.length, data.length);
return r;
}
//解密
public static byte[] decrypt(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 void main(String[] args) throws Exception {
//原文
String message = "Hello, World! encrypted using AES";
System.out.println("Message: " + message); // message: Hello, World! encrypted using AES
//128位金鑰 = 16 bytes key
byte[] key = "1234567890abcdef".getBytes("UTF-8");
//加密
byte[] data = message.getBytes(StandardCharsets.UTF_8);
byte[] encrypted = encrypt(key, data);
//加密後的密文: Encrypted data: 3iwMkdAqR0eQYQqaxOEKao+N0gSp/05i+mULmLvndSKq4Z2xz122wmFARWbAwF6dElmnceO/x5pJHcwXSr8inQ==
System.out.println("Encrypted data: " + Base64.getEncoder().encodeToString(encrypted));
//解密
byte[] decrypted = decrypt(key, encrypted);
//解密後得到結果與原文相同:Decrypted data: Hello, World! encrypted using AES
System.out.println("Decrypted data: " + new String(decrypted,"UTF-8"));
}
}
口令加密演算法
PBE(Passwoord Based Encrytion)演算法:由使用者輸入口令,採用隨機數雜湊計算出金鑰再進行加密,password:使用者口令,例如"hello123",Salt:隨機生成的byte[],金鑰Key:generate(byte[] salt, String password)。如果把隨機Salt儲存在U盤,就得到了一個“口令”+USB Key加密軟體,這樣做的好處是即時使用者使用非常弱的口令,沒有USB Key仍然無法解密。
總結:PBE演算法通過使用者口令和隨機數Salt計算Key然後加密,Key通過使用者口令和隨機數Salt計算得出,提高了安全性,PBE演算法內部仍然使用的是標準對稱加密演算法(例如AES)。
金鑰交換演算法
我們在使用對稱加密演算法的時候,我們的加密和解密使用的是同一個金鑰Key。我們以AES加密為例,當我們要加密明文,我們需要使用一個隨機生成的Key作為金鑰進行加解密,最後我們的問題就是如何傳遞金鑰?因為不給對方金鑰,對方就無法解密,而直接傳遞金鑰,會被黑客監聽,所以問題就變成了:如何在不安全的通道上安全地傳輸金鑰?金鑰交換演算法也就是Diff-Hellman演算法,即DH演算法。
- 甲首先選擇一個素數P=509,然後在選擇一個底數g和一個隨機數a,然後計算 A=\(g^a\) mod p => 215
- 甲傳送P=509,g=5,A=215,乙收到以後,也選擇一個隨機數b=456,然後計算 B=\(g^b\) mod p => 181,然後接著計算 s = \(A^b\) mod p => 121
- 乙把計算的B=181傳送給甲,甲通過 s=\(B^a\) mod p 可以計算出也等於121。所以雙方協商出的金鑰就是121。
要注意這個金鑰並沒有在網路上進行傳輸,通過網路傳輸的是p=509,g=5, A=215, B=181,但是通過這四個數,黑客是無法推算出金鑰s的。更確切的說,DH演算法它是一個金鑰協商演算法,雙發最終協商出一個共同的金鑰。我們把a看成是甲的私鑰,A看成是甲的公鑰,b看成是乙的私鑰,B看成是乙的公鑰,DH演算法的本質就是:雙方各自生成自己的私鑰和公鑰,然後交換公鑰,並且根據自己的私鑰和對方的公鑰生成最終的金鑰。DH演算法根據數學定律保證了雙方各自計算出來的key是相同的。
import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.crypto.SecretKey;
import java.io.IOException;
import java.math.BigInteger;
import java.security.*;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
class Person {
public final String name; // 表示人的名字
public PublicKey publicKey; // 表示這個人的公鑰
public PrivateKey privateKey; // 表示這個人的私鑰
public SecretKey secretKey; //表示最終的金鑰
public Person(String name) {
this.name = name;
}
//生成本地的KeyPair
public void generateKeyPair() {
try {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("DH");
keyGen.initialize(512); //建立一個512位的keyPair
KeyPair keyPair = keyGen.generateKeyPair();
this.privateKey = keyPair.getPrivate();
this.publicKey = keyPair.getPublic();
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
public void generateSecreteKey(byte[] recivedPUblickeyBytes) {
//從byte[]恢復PublcKey
try {
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(recivedPUblickeyBytes);
KeyFactory kf = KeyFactory.getInstance("DH");
PublicKey recivedPublicKey = kf.generatePublic(keySpec);
//生成本地金鑰
KeyAgreement keyAgreement = KeyAgreement.getInstance("DH");
keyAgreement.init(this.privateKey); // 自己的私鑰
keyAgreement.doPhase(recivedPublicKey,true); // 對方的公鑰
//生成AES金鑰
this.secretKey = keyAgreement.generateSecret("AES");
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
public void printKeys(){
System.out.printf("Name: %s\n", this.name);
System.out.printf("private key: %x\n",new BigInteger(1,this.privateKey.getEncoded()));
System.out.printf("public key: %x\n",new BigInteger(1,this.publicKey.getEncoded()));
System.out.printf("secrete key: %x\n",new BigInteger(1,this.secretKey.getEncoded()));
}
//傳送加密資訊
public String sendMessage(String message){
try {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE,this.secretKey);
byte[] data = cipher.doFinal(message.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(data);
} catch (GeneralSecurityException |IOException e) {
throw new RuntimeException(e);
}
}
//接收加密資訊並解密
public String reciveMessage(String message){
try {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE,this.secretKey);
byte[] data = cipher.doFinal(Base64.getDecoder().decode(message));
return new String(data,"UTF-8");
} catch (GeneralSecurityException |IOException e) {
throw new RuntimeException(e);
}
}
}
public class DH {
public static void main(String[] args) {
//Bob和Alice
Person bob = new Person("Bob");
Person alice = new Person("Alice");
//生成各自的keyPair
bob.generateKeyPair();
alice.generateKeyPair();
//雙方交換各自的public Key
//Bob根據Alice的public Key生成自己的本地金鑰
bob.generateSecreteKey(alice.publicKey.getEncoded());
//Alice根據Bob的public Key生成自己的本地金鑰
alice.generateSecreteKey(bob.publicKey.getEncoded());
//檢查雙方的本地金鑰是否相同
bob.printKeys();
alice.printKeys();
//雙方的SecretKey相同,後續通訊將使用SecretKey作為金鑰進行AES加解密
String msgBobToAlice = bob.sendMessage("Hello, Alice!");
System.out.println("Bob -> Alice: " + msgBobToAlice);
String aliceDecrypted = alice.reciveMessage(msgBobToAlice);
System.out.println("Alice decrypted: " + aliceDecrypted);
}
}
執行結果如下:
如果在執行過程中出現: Unsupported secret key algorithm: AES 異常資訊,這是由於金鑰所用的演算法不被支援,這個是由於JDK8 update 161之後,DH的金鑰長度至少為512位,但AES演算法金鑰不能達到這樣的長度,長度不一致所以導致報錯。
解決辦法:將 -Djdk.crypto.KeyAgreement.legacyKDF=true 寫入JVM系統變數中。可以在IEDA中的Run - Edit Configurations -> VM options中配置,如下圖:
但DH演算法不能避免中間人攻擊,如果黑客假冒乙和甲交換金鑰,同時又假冒甲和乙交換金鑰,這樣就可以成功地進行工具。DH演算法是一種安全的金鑰交換協議,通訊雙方通過不安全的通道協商金鑰,然後進行對稱加密傳輸。
非對稱加密演算法
非對稱加密就是加密和解密使用不同的金鑰,非對稱加密的典型演算法就是RSA演算法,
- 加密:用對方的公鑰加密,然後傳送給對方 encrypt(publicKeyB,message) -> encrypted
- 解密:對方用自己私鑰解密 decrypt(privateKeyB,encrypted) -> message
import javax.crypto.Cipher;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class RSAKeyPair {
//私鑰
private PrivateKey sk;
//公鑰
private PublicKey pk;
//生成公鑰/私鑰對
public RSAKeyPair() throws GeneralSecurityException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(1024);
KeyPair kp = keyGen.generateKeyPair();
this.sk = kp.getPrivate();
this.pk = kp.getPublic();
}
//從已儲存的位元組中(例如讀取檔案)恢復公鑰/金鑰
public RSAKeyPair(byte[] pk, byte[] sk) throws GeneralSecurityException {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(pk);
this.pk = keyFactory.generatePublic(keySpec);
PKCS8EncodedKeySpec skSpec = new PKCS8EncodedKeySpec(sk);
this.sk = keyFactory.generatePrivate(skSpec);
}
//把私鑰到處為位元組
public byte[] getPrivateKey(){
return this.sk.getEncoded();
}
//把公鑰匯出為位元組
public byte[] getPublicKey(){
return this.pk.getEncoded();
}
//用公鑰加密
public byte[] encrypt(byte[] message) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE,this.pk);
return cipher.doFinal(message);
}
//用私鑰解密
public byte[] decrypt(byte[] input) throws GeneralSecurityException{
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, this.sk);
return cipher.doFinal(input);
}
public static void main(String[] args) throws Exception {
//明文
byte[] plain = "Hello,使用RSA非對稱加密演算法對資料進行加密".getBytes();
//建立公鑰/私鑰 對
RSAKeyPair rsa = new RSAKeyPair();
//加密
byte[] encrypt = rsa.encrypt(plain);
System.out.println("encrypted: " + Base64.getEncoder().encodeToString(encrypt));
//解密
byte[] decrypt = rsa.decrypt(encrypt);
System.out.println("decrypted: " + new String(decrypt,"UTF-8"));
//儲存公鑰/私鑰 對
byte[] sk = rsa.getPrivateKey();
byte[] pk = rsa.getPublicKey();
System.out.println("sk: " + Base64.getEncoder().encodeToString(sk));
System.out.println("pk: " + Base64.getEncoder().encodeToString(pk));
//重新恢復公鑰/私鑰
RSAKeyPair rsaKeyPair = new RSAKeyPair(pk, sk);
//加密
byte[] encrypted = rsaKeyPair.encrypt(plain);
System.out.println("encrypted: " + Base64.getEncoder().encodeToString(encrypted));
//解密
byte[] decrypted = rsa.decrypt(encrypted);
System.out.println("decrypted: " + new String(decrypted,"UTF-8"));
}
}
執行結果:
非堆成加密演算法有如下優點:
- 對稱加密需要協商金鑰,而非對稱加密可以安全地公開各自的公鑰
- N個人之間通訊
- 使用非對稱加密只需要N個金鑰對,每個人只管理自己的金鑰對
- 使用對稱加密需要N*(N-1)/2個金鑰,每個人需要管理N-1個金鑰
非對稱加密的缺點:
- 運算速度慢
- 不能防止中間人攻擊
數字簽名演算法
RSA簽名演算法
在非對稱加密中,我們可以看到甲乙雙方要進行通訊,甲可以使用乙的publicKey對訊息進行加密,然後乙使用自己的privateKey對訊息進行解密,這個時候會出現一個問題,如果黑客使用乙的publicKey對訊息進行加密,然後冒充甲傳送給乙,那麼乙怎麼識別這個訊息是甲傳送的還是冒充的呢?所以我們就需要數字簽名演算法。甲在傳送加密資訊的時候,同時還要傳送自己的簽名,而這個簽名是使用甲的privateKey計算的,而乙要驗證這個簽名是否是合法的,它會用甲的publicKey進行驗證,如果驗證成功,則說明這個訊息確實是甲傳送的。所以數字簽名就是傳送方用自己的私鑰對訊息進行簽名(sig=signature(privateKey,'message')),接收方用傳送方的公鑰驗證簽名是否有效(boolen valid = verify(publicKey,sig,'message')),我們可以把數字簽名理解為混入了私鑰和公鑰的摘要。
數字簽名的目的:
- 確認資訊是某個傳送方發的(因為只有它用他自己的privateKey簽名,其他人才可以用它的publickey來驗證這個簽名)
- 傳送發不能抵賴它傳送了訊息(因為用誰的publicKey成功的驗證了簽名,則這個 簽名也是用誰的privateKey進行的簽名)
- 資料在傳輸過程中沒有被修改
常用的數字簽名演算法:
- MD5withRSA
- SHA1withRSA
- SHA256withRSA
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class SecRSASignature {
private PublicKey pk;
private PrivateKey sk;
public SecRSASignature() throws GeneralSecurityException {
//生成 KeyPair
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(1024);
KeyPair kp = keyGen.generateKeyPair();
this.sk = kp.getPrivate();
this.pk = kp.getPublic();
}
//從已儲存的位元組中(例如讀取檔案)恢復公鑰/金鑰
public SecRSASignature(byte[] pk, byte[] sk) throws GeneralSecurityException {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(pk);
this.pk = keyFactory.generatePublic(keySpec);
PKCS8EncodedKeySpec skSpec = new PKCS8EncodedKeySpec(sk);
this.sk = keyFactory.generatePrivate(skSpec);
}
//把私鑰到處為位元組
public byte[] getPrivateKey(){
return this.sk.getEncoded();
}
//把公鑰匯出為位元組
public byte[] getPublicKey(){
return this.pk.getEncoded();
}
//對訊息進行簽名
public byte[] sign(byte[] message) throws GeneralSecurityException {
//sign by sk
Signature signature = Signature.getInstance("SHA1withRSA");
signature.initSign(this.sk);
signature.update(message);
return signature.sign();
}
//私用公鑰驗證簽名
public boolean verify(byte[] message, byte[] sign) throws GeneralSecurityException {
//verify by pk
Signature sha1withRSA = Signature.getInstance("SHA1withRSA");
sha1withRSA.initVerify(this.pk);
sha1withRSA.update(message);
return sha1withRSA.verify(sign);
}
public static void main(String[] args) throws GeneralSecurityException {
byte[] message = "Hello,使用SHA1withRSA演算法進行數字簽名!".getBytes(StandardCharsets.UTF_8);
SecRSASignature rsas = new SecRSASignature();
byte[] sign = rsas.sign(message);
System.out.println("sign: " + Base64.getEncoder().encodeToString(sign));
boolean verified = rsas.verify(message, sign);
System.out.println("verified: " + verified);
//用另一個公鑰驗證
boolean verified02 = new SecRSASignature().verify(message, sign);
System.out.println("verify with another public key: " + verified02);
//修改原始資訊
message[0] = 100;
boolean verified03 = rsas.verify(message, sign);
System.out.println("verify changed message: " + verified03);
}
}
執行結果如下:
DSA簽名演算法
DSA(Digital Signature Algorithm),使用EIGamal數字簽名演算法,DSA只能配合SHA演算法使用,所以有SHA1withDSA,SHA256withDSA,SHA512withDSA演算法。和RSA數字簽名演算法相比,DSA演算法更快。測試程式碼和測試RSA數字簽名演算法的程式碼一致,只需要修改演算法名稱就行了。
數字證照
數字正數:
- 非對稱加密演算法:對資料進行加密、解密
- 簽名演算法:確保資料的完整性和抗否認性
- 摘要演算法:確保證照本身沒有被篡改
數字證照可以防止中間人攻擊,因為它採用鏈式簽名認證,即通過根證照(Root CA)去簽名下一級證照,這樣層層簽名,直到最終的使用者證照。而Root CA證照內建於作業系統中,所以,任何經過CA認證的數字證照都可以對其本身進行校驗,確保證照本身不是偽造的。
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.X509Certificate;
public class X509 {
private final PrivateKey privateKey;
public final X509Certificate certificate; // 證照和證照包含的公鑰和摘要資訊
public X509(KeyStore keyStore, String certName, String password) {
try {
this.privateKey = (PrivateKey) keyStore.getKey(certName,password.toCharArray());
this.certificate = (X509Certificate) keyStore.getCertificate(certName);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
//加密
public byte[] encrypt(byte[] message) {
try {
//獲得加密演算法
Cipher cipher = Cipher.getInstance(this.privateKey.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE,this.privateKey);
return cipher.doFinal(message);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
//解密
public byte[] decrypt(byte[] message) {
try {
PublicKey publicKey = this.certificate.getPublicKey();
Cipher cipher = Cipher.getInstance(publicKey.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE,publicKey);
return cipher.doFinal(message);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
public byte[] sign(byte[] message) {
try {
Signature signature = Signature.getInstance(this.certificate.getSigAlgName());
signature.initSign(this.privateKey);
signature.update(message);
return signature.sign();
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
public boolean verify(byte[] message, byte[] sign) {
try {
Signature signature = Signature.getInstance(this.certificate.getSigAlgName());
signature.initVerify(this.certificate);
signature.update(message);
return signature.verify(sign);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
//Java中的數字證照是儲存在keyStore中的
public static KeyStore loadKeyStore(String keyStoreFile, String password) {
try (InputStream input = new BufferedInputStream(new FileInputStream(keyStoreFile))) {
if (input == null) {
throw new RuntimeException("file not found in classpath: " + keyStoreFile);
}
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(input, password.toCharArray());
return ks;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws Exception {
byte[] message = "Hello, 使用X.509證照進行加密和簽名!".getBytes("UTF-8");
// 讀取KeyStore:
KeyStore ks = loadKeyStore("my.keystore", "123456");
// 讀取證照
X509 x509 = new X509(ks,"mycert", "123456");
// 加密:
byte[] encrypted = x509.encrypt(message);
System.out.println(String.format("encrypted: %x", new BigInteger(1, encrypted)));
// 解密:
byte[] decrypted = x509.decrypt(encrypted);
System.out.println("decrypted: " + new String(decrypted, "UTF-8"));
// 簽名:
byte[] sign = x509.sign(message);
System.out.println(String.format("signature: %x", new BigInteger(1, sign)));
// 驗證簽名:
boolean verified = x509.verify(message, sign);
System.out.println("verify: " + verified);
}
}
執行結果如下:
開啟命令列,進入當前工程所在目錄,輸入命令:keytool -storepass 123456 -genkeypair -keyalg RSA -keysize 1024 -sigalg SHA1withRSA -validity 36500 -alias mycert -keystore my.keystore -dname "CN=www.sample.com, OU=sample, O=sample, L=BJ, ST=BJ, C=CN" 即可生成keystore檔案,通過命令:keytool -list -keystore my.keystore -storepass 123456 可以看到keySore中的證照。
數字證照的應用:
- https: HTTP over SSL
- 伺服器傳送證照給客戶端(傳送公鑰/簽名/CA)
- 客服端驗證伺服器證照(確認伺服器身份)
- 客戶端用證照加密隨機口令併傳送給伺服器端(公鑰加密)
- 伺服器端解密獲得口令(私鑰解密)
- 雙方隨後使用AES加密進行通訊(對稱加密)
總結:數字證照就是集合了多種密碼學演算法,用於實現資料加解密、身份認證、簽名等多種功能的一種網路安全標準,數字證照採用鏈式簽名管理,頂級CA證照已經內建於作業系統中,常用演算法:MD5/SHA1/SHA256/RSA/DSA/...