Java加密與安全

Java伴我餘生發表於2020-07-11

資料安全

  什麼是資料安全?假如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的變化會造成輸出完全不同
  • 很難以從輸出反推輸入(只能依靠暴力窮舉)

常用的摘要演算法

演算法輸出長度
MD5128 bits16 bytes
SHA-1160 bits20 bytes
SHA-256256 bits32 bytes
RipeMD-160160 bits20 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/...

相關文章