跳到主要內容

搜尋酷無極

选择搜索范围,输入关键词后按回车。

語言 / Language

加解密的藝術

一 對稱加密

對稱加密和非對稱加密是兩種不同的加密方法,它們在資料安全和資訊傳輸中扮演著重要的角色。下面我將分別介紹這兩種加密技術:

1.1 對稱加密(Symmetric Encryption)

對稱加密是指加密和解密都使用相同的金鑰。這意味著傳送方和接收方都必須擁有這個金鑰才能進行加密和解密操作。

常見的對稱加密演算法:

  • AES(進階加密標準)

  • DES(資料加密標準)

  • 3DES(三重資料加密演算法)

1.2 對稱加密特點

一般來說,對稱加密具有如下特點:

  1. 速度較快:由於加密和解密使用相同的金鑰,對稱加密通常比非對稱加密要快。

  2. 金鑰管理:對稱加密的主要挑戰在於金鑰的分發和管理。如果金鑰洩漏,加密的安全性就會受到威脅。

  3. 適用於大量資料:由於速度快,對稱式加密適合加密大量資料。

從這裡可以看到,對稱加密主要有兩大優勢:第一就是運算速度快;第二就是適用於大量資料。

但是,對稱加密有一個致命的問題,就是金鑰管理。如何從伺服器端將金鑰安全地傳輸到用戶端是個問題!另外就是當一對多通訊的時候,如何管理好金鑰不被洩漏也是一個考驗。這是對稱加密的不足之處。

藍綠色牆面與塗料滾筒的藝術配圖

1.3 程式碼範例

接下來松哥給大家示範一下 Java 程式碼如何做對稱加解密。

在 Java 中實作對稱加密,通常使用 Java 加密架構(Java Cryptography Architecture, JCA)提供的類別和介面。

下面是一個使用 AES(進階加密標準)演算法進行對稱加密和解密的簡單範例:

public class SymmetricEncryptionExample {    // 生成密钥
    public static SecretKey generateKey() throws Exception {     
       KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(128); // 可以是128, 192或256位
        return keyGenerator.generateKey();
    }    
    // 加密方法
    public static String encrypt(String data, SecretKey key) throws Exception {      
      Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.ENCRYPT_MODE, key);       
         byte[] encryptedBytes = cipher.doFinal(data.getBytes());        
         return Base64.getEncoder().encodeToString(encryptedBytes);
    }    
    // 解密方法
    public static String decrypt(String encryptedData, SecretKey key) throws Exception {        
    Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.DECRYPT_MODE, key);       
         byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData));       
          return new String(decryptedBytes);
    }    
    public static void main(String[] args) {        
    try {           
             // 生成密钥
            SecretKey key = generateKey();           
             // 原始数据
            String originalData = "Hello, JavaBoy!";            
            // 加密
            String encryptedData = encrypt(originalData, key);
            System.out.println("加密数据: " + encryptedData);            
            // 解密
            String decryptedData = decrypt(encryptedData, key);
            System.out.println("解密数据: " + decryptedData);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  1. 產生金鑰:使用 KeyGenerator 產生一個AES金鑰。

  2. 加密:使用 Cipher 類別進行加密,將資料轉換成位元組後加密,並使用 Base64 編碼轉換為字串,以便於儲存或傳輸。

  3. 解密:將加密的字串解碼回位元組,然後使用相同的金鑰進行解密。

以上程式碼大家需要注意的是:

  • 金鑰長度(如 128 位元)應根據安全需求選擇。

  • 確保金鑰安全儲存,不要在程式碼中硬編碼金鑰。

  • 對於正式環境,應考慮使用更安全的金鑰管理策略。

出於安全考量,我們一般使用上面的方案產生金鑰。這種方案產生的金鑰有一個特點就是系統每次重新啟動就會變。如果你希望能夠自己控制金鑰的產生,那麼可以透過如下方式產生金鑰:

public static SecretKey generateKeyFromPassword(String password, int keySize) throws NoSuchAlgorithmException, InvalidKeySpecException {   
  SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");   
   KeySpec spec = new PBEKeySpec(password.toCharArray(), "salt".getBytes(), 65536, keySize);   
   return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
}

這樣就可以透過自己傳入的參數去控制金鑰。

1.4 前後端搭配

在跨語言(如 JavaScript 和 Java)使用 AES 演算法進行加密和解密時,關鍵是確保兩端使用相同的金鑰、演算法模式(如 CBC, ECB 等)、填充模式(如 PKCS5Padding, PKCS7Padding 等)和初始化向量(IV,如果使用了需要 IV 的模式如 CBC)。

之前有小夥伴說自己前端加密之後後端總是無法解密,松哥這裡也給一個前後端搭配的例子。

前端加密後端解密

前端程式碼:

<!DOCTYPE html><html lang="en"><head>
    <meta charset="UTF-8">
    <title>AES加密示例</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
    </head>
    <body>
    <script>
    function encryptAES(text, secretKey) {        
    const key = CryptoJS.enc.Utf8.parse(secretKey);        
    const iv = CryptoJS.lib.WordArray.random(128 / 8); 
    // 对于CBC模式,需要IV
    
        const encrypted = CryptoJS.AES.encrypt(text, key, {           
         iv: iv,           
          mode: CryptoJS.mode.CBC,            
          padding: CryptoJS.pad.Pkcs7
        });        
        // 返回加密后的文本和IV(Base64格式),实际使用中可能需要安全地传输这些值
        return {          
          ciphertext: encrypted.toString(),          
            iv: iv.toString(CryptoJS.enc.Base64)
        };
    }    
    const secretKey = 'helloworldhelloworldhelloworld11'; /
    / 确保密钥是32个字符长(256位)
    const text = 'Hello, javaboy!';    
    const result = encryptAES(text, secretKey);    
    console.log(result);
    </script>
    </body>
    </html>


後端程式碼:

import javax.crypto.Cipher;import javax.crypto.spec.IvParameterSpec;import javax.crypto.spec.SecretKeySpec;import java.util.Base64;public class AESDecrypt {    public static String decryptAES(String encryptedData, String secretKey, String iv) throws Exception {        IvParameterSpec ivParameterSpec = new IvParameterSpec(Base64.getDecoder().decode(iv));        SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes("UTF-8"), "AES");        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);        byte[] decodedValue = Base64.getDecoder().decode(encryptedData);        byte[] decryptedValue = cipher.doFinal(decodedValue);        return new String(decryptedValue, "UTF-8");
    }    public static void main(String[] args) {        try {            //前端加密后的文本
            String encryptedText = "PYANpAjMsRnBIEhovtEXQw==";            String secretKey = "helloworldhelloworldhelloworld11";            //前端 IV,要和加密后的文本一起传到后端
            String iv = "y/jUHcgSOpOiyNlsfjNUBg==";            String decryptedText = decryptAES(encryptedText, secretKey, iv);
            System.out.println("解密文本: " + decryptedText);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

注意這裡前端加密時會產生一個 iv 參數,要隨著前端加密結果一起傳遞給後端。

後端加密前端解密

在 Java 進行 AES 加密並在 JavaScript 中解密時,同樣需要確保兩端使用相同的金鑰、演算法模式(如 CBC、ECB 等)、填充模式(如 PKCS5Padding、PKCS7Padding 等)以及(如果適用)相同的初始化向量(IV)。

後端程式碼:

public class AESEncrypt {    public static String encryptAES(String plainText, String secretKey, String iv) throws Exception {        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(256); // 对于AES-256
        SecretKey secret = new SecretKeySpec(secretKey.getBytes("UTF-8"), "AES");        IvParameterSpec ivParameterSpec = new IvParameterSpec(Base64.getDecoder().decode(iv));        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, secret, ivParameterSpec);        byte[] encrypted = cipher.doFinal(plainText.getBytes("UTF-8"));        return Base64.getEncoder().encodeToString(encrypted);
    }    public static void main(String[] args) {        try {            String plainText = "Hello, 江南一点雨!";            String secretKey = "helloworldhelloworldhelloworld11"; // 确保密钥是32个字符长(256位)
            String iv = Base64.getEncoder().encodeToString(new byte[16]); // 示例IV,实际应用中应更安全地生成

            String encryptedText = encryptAES(plainText, secretKey, iv);
            System.out.println("Encrypted text: " + encryptedText);
            System.out.println("IV (Base64): " + iv); // 确保将IV发送给解密方

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

前端程式碼:

<!DOCTYPE html><html lang="en"><head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
    <script>
        function decryptAES(ciphertext, secretKey, iv) {            const key = CryptoJS.enc.Utf8.parse(secretKey);            const ivParsed = CryptoJS.enc.Base64.parse(iv);            const decrypted = CryptoJS.AES.decrypt(
                {                    ciphertext: CryptoJS.enc.Base64.parse(ciphertext)
                },
                key,
                {                    iv: ivParsed,                    mode: CryptoJS.mode.CBC,                    padding: CryptoJS.pad.Pkcs7
                }
            );            return decrypted.toString(CryptoJS.enc.Utf8);
        }        const secretKey = 'helloworldhelloworldhelloworld11';        const iv = 'AAAAAAAAAAAAAAAAAAAAAA=='; // 从Java代码获取
        const ciphertext = '1lPNVF1injas78KUWeKp5FusEr6f0pGgcrLAg9ELFr8='; // 从Java代码获取

        const decryptedText = decryptAES(ciphertext, secretKey, iv);        console.log(decryptedText);    </script></head><body></body></html>

以上前後端互動的加解密程式碼,松哥親自測試過,都是沒問題的,大家有這方面的需求記得及時收藏本文,可以作為參考。

二 非對稱加密

2.1 什麼是非對稱式加密

非對稱加密,也稱為公開金鑰加密,是一種使用兩個不同金鑰(公開金鑰和私密金鑰)的加密方式。

一般來說,非對稱加密有如下幾種不同的特點:

  1. 公鑰與私鑰

  • 公鑰與私鑰的產生:非對稱加密使用一對金鑰,公鑰是公開的,任何人都可以存取,而私鑰是私有的,只有金鑰的持有者可以存取。這兩個金鑰是由數學演算法產生的,且相互關聯但不可從一方推導出另一方。

  1. 安全性高

  • 難以破解:由於公鑰和私鑰的複雜數學關係,非對稱加密的安全性較高。攻擊者很難從公鑰中推斷出私鑰,從而確保加密資料的安全性。

  • 抵抗量子運算攻擊:一些區塊鏈專案開始採用抵抗量子運算攻擊的加密演算法,如橢圓曲線數位簽章演算法(ECDSA)的量子安全變體,以應對未來量子運算的威脅。

  1. 公開金鑰分發方便

  • 公鑰的公開性:公鑰可以公開給任何人,因此分發起來非常方便。任何人都可以使用公鑰來加密資料,但只有私鑰持有者才能解密。

  1. 身分驗證與數位簽章

  • 身分驗證:公鑰可以用作使用者的身分識別,其他人可以驗證使用者的身分而無需了解其私鑰。這有助於在區塊鏈等去中心化網路中建立可信身分。

  • 數位簽章:私鑰持有者可以使用私鑰對訊息進行簽署,其他人則可以使用公鑰來驗證簽章的真實性。這確保了訊息的完整性和來源的可靠性。

  1. 安全通訊

  • 加密通訊:公鑰可以用於加密訊息,只有持有相應私鑰的人才能解密。這確保了通訊過程中的資料安全,防止了資訊被未經授權的人員存取。

  1. 數位資產控制

  • 區塊鏈地址與私鑰:區塊鏈地址通常由公鑰衍生而來,使用者透過私鑰來控制與該地址相關聯的數位資產。私鑰的安全性對數位資產的安全至關重要。

  1. 加密解密速度

  • 相對較慢:非對稱加密的加密和解密速度相較於對稱加密要慢得多,因為它需要進行更加複雜的數學計算。然而,這並不影響其在安全通訊、身分驗證等領域的應用。

2.2 非對稱加密的用途

非對稱加密有兩個經典使用場景。

  1. 加密:這是我們最為熟知的用法,就是公鑰加密,私鑰解密。

  2. 簽章:考慮到網路不可信,資料在傳輸過程中可能被竄改,這個時候公私鑰可以反過來用,用私鑰對資料進行簽章,公鑰進行驗章,確保資料安全完整。

針對第二點用途,有些朋友會將其表述為用私鑰進行加密,公鑰進行解密,反正大家知道說的是同一回事。

非對稱加密演算法,儘管在理論上能夠用於資料加密和數位簽章,但在實務中,其高計算複雜度和低效率成為了主要障礙。

相比對稱加密演算法,非對稱加密的運算速度要慢上幾個數量級,這極大地影響了其處理大量資料的能力。此外,由於非對稱加密演算法的加密和解密過程與金鑰長度緊密相關,且不支援區塊加密模式,導致它只能處理不超過金鑰長度的少量資料,無法進行大量資料的加密。

為了克服非對稱加密在效能上的不足,現代加密系統通常採用混合加密策略,即結合對稱加密和非對稱加密的優點。在這種策略中,非對稱加密主要用於安全地傳輸一個對稱加密的金鑰(即「金鑰協商」)給另一方。一旦雙方安全地共享了這個對稱金鑰,就可以使用高效的對稱加密演算法來加密和解密大量資料。這種結合使用的方法不僅提高了加密效率,還增強了通訊的安全性,被廣泛應用於各種安全通訊協定中,如SSL/TLS。

在數位簽章領域,為了提升非對稱加密的效率也做了一些調整。具體做法是,首先對原始資料進行摘要處理(可以利用 MD5、SHA 等),得到一個固定長度的摘要值。然後,使用非對稱加密演算法對這個摘要值進行加密,產生數位簽章。由於摘要演算法能夠高效地將任意長度的資料壓縮為固定長度的摘要,因此不管原始資料多大,摘要資料長度都一樣,簽章過程也能保持高效。驗證簽章時,只需重新計算原始資料的摘要,並與解密後的簽章進行比較,即可快速判斷資料是否被竄改。

2.3 加密案例

Java 程式碼使用 RSA 加解密範例:

public class RsaDemo {    public static void main(String[] args) {        try {            KeyPair keyPair = generateRSAKeyPair();            PublicKey publicKey = keyPair.getPublic();            PrivateKey privateKey = keyPair.getPrivate();            String originalData = "Hello, 江南一点雨!";            String encryptedData = encrypt(publicKey, originalData);            String decryptedData = decrypt(privateKey, encryptedData);

            System.out.println("加密后的数据: " + encryptedData);
            System.out.println("解密后的数据: " + decryptedData);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }    public static String decrypt(PrivateKey privateKey, String encryptedData) throws Exception {        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);        byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData));        return new String(decryptedBytes);
    }    public static String encrypt(PublicKey publicKey, String data) throws Exception {        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);        byte[] bytes = cipher.doFinal(data.getBytes());        return Base64.getEncoder().encodeToString(bytes);
    }    public static KeyPair generateRSAKeyPair() throws NoSuchAlgorithmException {        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
        keyGen.initialize(2048); // 可以指定密钥长度,如2048位
        return keyGen.generateKeyPair();
    }
}

2.4 前後端搭配

前端加密後端解密

後端程式碼和上面案例中一致,不同的是,我們在取得公鑰之後,可以將公鑰列印出來,這個公鑰將來要傳遞給前端:

public static void main(String[] args) {    try {        KeyPair keyPair = generateRSAKeyPair();        PublicKey publicKey = keyPair.getPublic();        PrivateKey privateKey = keyPair.getPrivate();
        System.out.println("公钥: " + Base64.getEncoder().encodeToString(publicKey.getEncoded()));        String originalData = "Hello, 江南一点雨!";        String encryptedData = encrypt(publicKey, originalData);        String decryptedData = decrypt(privateKey, encryptedData);
        System.out.println("加密后的数据: " + encryptedData);
        System.out.println("解密后的数据: " + decryptedData);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

前端程式碼如下:

<!DOCTYPE html><html lang="en"><head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>RSA Encryption Example</title>
    <!-- 引入jsencrypt库 -->
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/bin/jsencrypt.min.js"></script></head><body><script>
    // 重写前端加密方法
    function encryptData(publicKey, data) {        // 创建一个新的JSEncrypt对象
        var encryptor = new JSEncrypt();        // 设置公钥
        encryptor.setPublicKey(publicKey);        // 加密数据
        var encrypted = encryptor.encrypt(data);        return encrypted;
    }    // 示例公钥(实际使用时应该替换为服务器提供的公钥)
    var publicKey = `MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnP5FOXIghidLDE2FgFwzi4Y+qTdYjnc0eMoiL5i4gdOeTE+7Uu9YvDB51GcBWKD9lvmZr5rX8z0OBpwe526qsTekNpXoIVk9DB34US0HOrAXEEwpUTVuSR656cJGAwmWBkVaalQynEz4Dlzrm53zExVYueruYUuzFyuZDaQcFnl3rWUH/XDkCIe23z0R1TfT3Q2OYNKft0u56r0S/ko99utXuYJK9yowe7QGT6q4cSJwsITQTomCARAq9q+bSNuGEYa4FlYCKIKWIhKbMhz0FYIMB2fJN10GyZbbvKASqeMkuCoD2Efgd8/6uMwOaMcx9LgEkcFaQ3qgDutsPXNUswIDAQAB`;    // 要加密的数据
    var data = 'Hello, javaboy!';    // 调用加密方法
    var encryptedData = encryptData(publicKey, data);    // 输出加密后的数据
    console.log('Encrypted Data:', encryptedData);</script></body></html>

後端加密前端解密

在 Java 中使用 RSA 演算法加密資料,並在 JavaScript 中解密這些資料,意味著伺服器端用前端的公鑰加密,前端用自己的私鑰解密,這種情境下前端私鑰很容易被竊取,因此不建議這種用法。我也就不舉例了。

好啦,又和小夥伴們聊了一遍對稱加密和非對稱加密,上面的範例程式碼松哥都測試通過了,小夥伴們可以作為參考。


評論區

傳送中

選擇喜歡的樣式

所選樣式會套用至全站。