1. 程式人生 > 實用技巧 >Java加密與安全

Java加密與安全

資料安全

  什麼是資料安全?假如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演演算法。

  1. 甲首先選擇一個素數P=509,然後在選擇一個底數g和一個隨機數a,然後計算 A=\(g^a\) mod p => 215
  2. 甲傳送P=509,g=5,A=215,乙收到以後,也選擇一個隨機數b=456,然後計算 B=\(g^b\) mod p => 181,然後接著計算 s = \(A^b\) mod p => 121
  3. 乙把計算的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/...