玩轉混合加密

阿寶哥發表於2020-07-22

資料加密,是一門歷史悠久的技術,指通過加密演算法和加密金鑰將明文轉變為密文,而解密則是通過解密演算法和解密金鑰將密文恢復為明文。它的核心是密碼學。資料加密仍是計算機系統對資訊進行保護的一種最可靠的辦法。它利用密碼技術對資訊進行加密,實現資訊隱蔽,從而起到保護資訊的安全的作用。

本文阿寶哥將介紹如何對資料進行混合加密,即使用對稱加密演算法與非對稱加密演算法對資料進行加密,從而進一步保證資料的安全性。閱讀完本文,你將瞭解以下內容:

  • 什麼是對稱加密、對稱加密的過程、對稱加密的優缺點及 AES 對稱加密演算法的使用;
  • 什麼是非對稱加密、非對稱加密的過程、非對稱加密的優缺點及 RSA 非對稱加密演算法的使用;
  • 什麼是混合加密、混合加密的過程及如何實現混合加密。

在最後的 阿寶哥有話說 環節,阿寶哥還將簡單介紹一下什麼是訊息摘要演算法和什麼是 MD5 演算法及其用途與缺陷。好的,現在讓我們步入正題。為了讓剛接觸混合加密的小夥伴更好地瞭解並掌握混合加密,阿寶哥將乘坐 “時光機” 帶大家來到某個發版的夜晚...

那一晚我們團隊的小夥伴正在等服務端資料升級,為了讓大家 “忘記” 這個漫漫的升級過程,阿寶哥就立馬組織了一場關於混合加密的技術分享會。在阿寶哥 “威逼利誘” 之下,團隊的小夥伴們很快就到齊了,之後阿寶哥以以下對話拉開了分享會的序幕:

幾分鐘過後,小哥講完了,基本關鍵點都有回答上來,但還遺漏了一些內容。為了讓小夥伴們更好地理解對稱加密,阿寶哥對小哥表述的內容進行了重新梳理,下面讓我們來一起認識一下對稱加密。

一、對稱加密

1.1 什麼是對稱加密

對稱金鑰演算法(英語:Symmetric-key algorithm)又稱為對稱加密、私鑰加密、共享金鑰加密,是密碼學中的一類加密演算法。這類演算法在加密和解密時使用相同的金鑰,或是使用兩個可以簡單地相互推算的金鑰。

1.2 對稱加密的優點

演算法公開、計算量小、加密速度快、加密效率高,適合對大量資料進行加密的場景。 比如 HLS(HTTP Live Streaming)普通加密場景中,一般會使用 AES-128 對稱加密演算法對 TS 切片進行加密,以保證多媒體資源安全。

1.3 對稱加密的過程

傳送方使用金鑰將明文資料加密成密文,然後傳送出去,接收方收到密文後,使用同一個金鑰將密文解密成明文讀取。

1.4 對稱加密的使用示例

常見的對稱加密演算法有 AES、ChaCha20、3DES、Salsa20、DES、Blowfish、IDEA、RC5、RC6、Camellia。這裡我們以常見的 AES 演算法為例,來介紹一下 AES(Advanced Encryption Standard)對稱加密與解密的過程。

下面阿寶哥將使用 crypto-js 這個庫來介紹 AES 演算法的加密與解密,該庫提供了 CryptoJS.AES.encrypt() 方法用於實現 AES 加密,而 AES 解密對應的方法是 CryptoJS.AES.decrypt()

基於上述兩個方法阿寶哥進一步封裝了 aesEncrypt()aesDecrypt() 這兩個方法,它們分別用於 AES 加密與解密,其具體實現如下所示:

1.4.1 AES 加密方法
// AES加密
function aesEncrypt(content) {
  let text = CryptoJS.enc.Utf8.parse(JSON.stringify(content));
  let encrypted = CryptoJS.AES.encrypt(text, key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7,
  });
  return encrypted.toString();
}
1.4.2 AES 解密方法
// AES解密
function aesDecrypt(content) {
  let decrypt = CryptoJS.AES.decrypt(content, key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7,
  });
  return decrypt.toString(CryptoJS.enc.Utf8);
}
1.4.3 AES 加密與解密示例

在以上示例中,我們在頁面上建立了 3 個 textarea,分別用於存放明文、加密後的密文和解密後的明文。當使用者點選 加密 按鈕時,會對使用者輸入的明文進行 AES 加密,完成加密後,會把密文顯示在密文對應的 textarea 中,當使用者點選 解密 按鈕時,會對密文進行 AES 解密,完成解密後,會把解密後的明文顯示在對應的 textarea 中。

以上示例對應的完整程式碼如下所示:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>AES 對稱加密與解密示例</title>
    <style>
      .block {
        flex: 1;
      }
    </style>
  </head>
  <body>
    <h3>阿寶哥:AES 對稱加密與解密示例(CBC 模式)</h3>
    <div style="display: flex;">
      <div class="block">
        <p>①明文加密 => <button onclick="encrypt()">加密</button></p>
        <textarea id="plaintext" rows="5" cols="15"></textarea>
      </div>
      <div class="block">
        <p>②密文解密 => <button onclick="decrypt()">解密</button></p>
        <textarea id="ciphertext" rows="5" cols="15"></textarea>
      </div>
      <div class="block">
        <p>③解密後的明文</p>
        <textarea id="decryptedCiphertext" rows="5" cols="15"></textarea>
      </div>
    </div>
    <!-- 引入 CDN Crypto.js AES加密 -->
    <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/core.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/enc-base64.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/cipher-core.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/aes.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/pad-pkcs7.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/enc-utf8.min.js"></script>
    <!-- 引入 CDN Crypto.js 結束 -->
    <script>
      const key = CryptoJS.enc.Utf8.parse("0123456789abcdef"); // 金鑰
      const iv = CryptoJS.enc.Utf8.parse("abcdef0123456789"); // 初始向量
      const plaintextEle = document.querySelector("#plaintext");
      const ciphertextEle = document.querySelector("#ciphertext");
      const decryptedCiphertextEle = document.querySelector(
        "#decryptedCiphertext"
      );

      function encrypt() {
        let plaintext = plaintextEle.value;
        ciphertextEle.value = aesEncrypt(plaintext);
      }

      function decrypt() {
        let ciphertext = ciphertextEle.value;
        decryptedCiphertextEle.value = aesDecrypt(ciphertext).replace(/\"/g,'');
      }

      // AES加密
      function aesEncrypt(content) {
        let text = CryptoJS.enc.Utf8.parse(JSON.stringify(content));
        let encrypted = CryptoJS.AES.encrypt(text, key, {
          iv: iv,
          mode: CryptoJS.mode.CBC,
          padding: CryptoJS.pad.Pkcs7,
        });
        return encrypted.toString();
      }

      // AES解密
      function aesDecrypt(content) {
        let decrypt = CryptoJS.AES.decrypt(content, key, {
          iv: iv,
          mode: CryptoJS.mode.CBC,
          padding: CryptoJS.pad.Pkcs7,
        });
        return decrypt.toString(CryptoJS.enc.Utf8);
      }
    </script>
  </body>
</html>

在上面的示例中,我們通過 AES 對稱加密演算法,對 “我是阿寶哥” 明文進行加密,從而實現資訊隱蔽。

那麼使用對稱加密演算法就可以解決我們前面的問題麼?答案是否定,這是因為對稱加密存在一些的缺點。

1.5 對稱加密的缺點

通過使用對稱加密演算法,我們已經把明文加密成密文。雖然這解決了資料的安全性,但同時也帶來了另一個新的問題。因為對稱加密演算法,加密和解密時使用的是同一個金鑰,所以對稱加密的安全性就不僅僅取決於加密演算法本身的強度,更取決於金鑰是否被安全的傳輸或保管。

另外對於實際應用場景,為了避免單一的金鑰被攻破,從而導致所有的加密資料被破解,對於不同的資料,我們一般會使用不同的金鑰進行加密,這樣雖然提高了安全性,但也增加了金鑰管理的難度。

由於對稱加密存在以上的問題,因此它並不是一種好的解決方案。為了找到更好的方案,阿寶哥開始了另一輪新的對話。

二、非對稱加密

2.1 什麼是非對稱加密

非對稱加密演算法需要兩個金鑰:公開金鑰(publickey:簡稱公鑰)和私有金鑰(privatekey:簡稱私鑰)。公鑰與私鑰是一對,如果用公鑰對資料進行加密,只有用對應的私鑰才能解密。 因為加密和解密使用的是兩個不同的金鑰,所以這種演算法叫作非對稱加密演算法。

2.2 非對稱加密的優點

安全性更高,公鑰是公開的,私鑰是自己儲存的,不需要將私鑰提供給別人。

2.3 非對稱加密的過程

2.4 非對稱加密的使用示例

常見的非對稱加密演算法有 RSA、Elgamal、揹包演算法、Rabin、D-H、ECC(橢圓曲線加密演算法)。這裡我們以常見的 RSA 演算法為例,來介紹一下 RSA 非對稱加密與解密的過程。

RSA 是 1977 年由羅納德·李維斯特(Ron Rivest)、阿迪·薩莫爾(Adi Shamir)和倫納德·阿德曼(Leonard Adleman)一起提出的。當時他們三人都在麻省理工學院工作。RSA 就是他們三人姓氏開頭字母拼在一起組成的。

下面阿寶哥將使用 jsencrypt 這個庫來介紹 RSA 演算法的加密與解密,該庫提供了 encrypt() 方法用於實現 RSA 加密,而 RSA 解密對應的方法是 decrypt()

2.4.1 建立公私鑰

使用 jsencrypt 這個庫之前,我們需要先生成公鑰和私鑰。接下來阿寶哥以 macOS 系統為例,來介紹一下如何生成公私鑰。

首先我們先來生成私鑰,在命令列輸入以下命令:

$ openssl genrsa -out rsa_1024_priv.pem 1024

在該命令成功執行之後,在當前目錄下會生成一個 rsa_1024_priv.pem 檔案,該檔案的內容如下:

-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDocWYwnJ4DYur0BjxFjJkLv4QRJpTJnwjiwxkuJZe1HTIIuLbu
/yHyHLhc2MAHKL0Ob+8tcKXKsL1oxs467+q0jA+glOUtBXFcUnutWBbnf9qIDkKP
...
bKkRJNJ2PpfWA45Vdq6u+izrn9e2TabKjWIfTfT/ZQ==
-----END RSA PRIVATE KEY-----

然後我們來生成公鑰,同樣在命令列輸入以下命令:

$ openssl rsa -pubout -in rsa_1024_priv.pem -out rsa_1024_pub.pem

在該命令成功執行之後,在當前目錄下會生成一個 rsa_1024_pub.pem 檔案,該檔案的內容如下:

-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDocWYwnJ4DYur0BjxFjJkLv4QR
JpTJnwjiwxkuJZe1HTIIuLbu/yHyHLhc2MAHKL0Ob+8tcKXKsL1oxs467+q0jA+g
lOUtBXFcUnutWBbnf9qIDkKP2uoDdZ//LUeW7jibVrVJbXU2hxB8bQpBkltZf/xs
cyhRIeiXxs13vlSHVwIDAQAB
-----END PUBLIC KEY-----
2.4.2 建立 RSA 加密器和解密器

建立完公私鑰之後,我們就可以進一步建立 RSA 加密器和解密器,具體程式碼如下:

const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDocWYwnJ4DYur0BjxFjJkLv4QR
...
cyhRIeiXxs13vlSHVwIDAQAB
-----END PUBLIC KEY-----`;

const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDocWYwnJ4DYur0BjxFjJkLv4QRJpTJnwjiwxkuJZe1HTIIuLbu
...
bKkRJNJ2PpfWA45Vdq6u+izrn9e2TabKjWIfTfT/ZQ==
-----END RSA PRIVATE KEY-----`;

const encryptor = new JSEncrypt(); // RSA加密器
encryptor.setPublicKey(PUBLIC_KEY);

const decryptor = new JSEncrypt(); // RSA解密器
decryptor.setPrivateKey(PRIVATE_KEY);
2.4.3 RSA 加密與解密示例

在以上示例中,我們在頁面上建立了 3 個 textarea,分別用於存放明文、加密後的密文和解密後的明文。當使用者點選 加密 按鈕時,會對使用者輸入的明文進行 RSA 加密,完成加密後,會把密文顯示在密文對應的 textarea 中,當使用者點選 解密 按鈕時,會對密文進行 RSA 解密,完成解密後,會把解密後的明文顯示在對應的 textarea 中。

以上示例對應的完整程式碼如下所示:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>RSA 對稱加密與解密示例</title>
    <style>
      .block {
        flex: 1;
      }
    </style>
  </head>
  <body>
    <h3>阿寶哥:RSA 對稱加密與解密示例</h3>
    <div style="display: flex;">
      <div class="block">
        <p>①明文加密 => <button onclick="encrypt()">加密</button></p>
        <textarea id="plaintext" rows="5" cols="15"></textarea>
      </div>
      <div class="block">
        <p>②密文解密 => <button onclick="decrypt()">解密</button></p>
        <textarea id="ciphertext" rows="5" cols="15"></textarea>
      </div>
      <div class="block">
        <p>③解密後的明文</p>
        <textarea id="decryptedCiphertext" rows="5" cols="15"></textarea>
      </div>
    </div>
    <script src="https://cdn.bootcdn.net/ajax/libs/jsencrypt/2.3.1/jsencrypt.min.js"></script>
    <script>
      const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDocWYwnJ4DYur0BjxFjJkLv4QR
JpTJnwjiwxkuJZe1HTIIuLbu/yHyHLhc2MAHKL0Ob+8tcKXKsL1oxs467+q0jA+g
lOUtBXFcUnutWBbnf9qIDkKP2uoDdZ//LUeW7jibVrVJbXU2hxB8bQpBkltZf/xs
cyhRIeiXxs13vlSHVwIDAQAB
-----END PUBLIC KEY-----`;
      const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDocWYwnJ4DYur0BjxFjJkLv4QRJpTJnwjiwxkuJZe1HTIIuLbu
/yHyHLhc2MAHKL0Ob+8tcKXKsL1oxs467+q0jA+glOUtBXFcUnutWBbnf9qIDkKP
2uoDdZ//LUeW7jibVrVJbXU2hxB8bQpBkltZf/xscyhRIeiXxs13vlSHVwIDAQAB
AoGAKOarYKpuc5IYXdArEuHmnFaa2pm7XK8LVTuXVrNuuoPkpfw61Fs4ke3T0yKg
x6G3gq7Xm1tTEROAgMtaxqwo1D5n1H0nkyDFggLB0K9Ws0frp7HENtSQwdNSry1A
iD8TLxkhoWo7BS0VViLT1gKOfnw4YeMJP+CcOQ+DQjCsUMECQQD0Nc0vKBTlK6GT
28gcIMVoQy2KicjiH222A9/TLCNAQ9DEeZDYEptuTfrlbggfWdgQ3nc6CyvGf6c5
6uBPi/+5AkEA86oqqZPi7ekkUVHx0VSkp0mTlD1tAPhDE8cLX8vyImGExS+tTznz
ROyzm3T1M1PisdQIU8Wd5rqvHP6dB0enjwJAauhKpMQ1MYYCPApQ9g9anCQcgbOD
34nGq5HSoE2IOQ/3Cqv1PsIWjRlSJrIemCrqrafWJfDR/xnPCUnLXMd68QJAPNwG
1d4zMvslcA5ImOFMUuBEtST2geSAVINFqwK0krPKxrmWzxAJW/DHF5AJ4m0UVRhB
kDLusn90V4iczgGurwJAZUz6w01OeoLhsOuWNvkbTq+IV0NQ5GAEGA721Ck5zp86
bKkRJNJ2PpfWA45Vdq6u+izrn9e2TabKjWIfTfT/ZQ==
-----END RSA PRIVATE KEY-----`;

      const encryptor = new JSEncrypt(); // RSA加密器
      encryptor.setPublicKey(PUBLIC_KEY);

      const decryptor = new JSEncrypt(); // RSA解密器
      decryptor.setPrivateKey(PRIVATE_KEY);

      const plaintextEle = document.querySelector("#plaintext");
      const ciphertextEle = document.querySelector("#ciphertext");
      const decryptedCiphertextEle = document.querySelector(
        "#decryptedCiphertext"
      );

      function encrypt() {
        let plaintext = plaintextEle.value;
        ciphertextEle.value = encryptor.encrypt(plaintext);
      }

      function decrypt() {
        let ciphertext = ciphertextEle.value;
        decryptedCiphertextEle.value = decryptor.decrypt(ciphertext);
      }
    </script>
  </body>
</html>

在上面的示例中,我們通過 RSA 非對稱加密演算法,對 “我是阿寶哥” 明文進行加密,從而實現資訊隱蔽。

那麼使用非對稱加密演算法就可以解決我們前面的問題麼?答案是否定,這是因為非對稱加密也存在一些的缺點。

2.5 非對稱加密的缺點

非對稱加密演算法加密和解密花費時間長、速度慢,只適合對少量資料進行加密。因為我們要提供的是通用的解決方案,即要同時考慮到少量資料和大量資料的情況,所以非對稱加密也不是一個好的解決方案。為了解決問題,阿寶哥又重新開啟了一輪新的對話。

三、混合加密

3.1 什麼是混合加密

混合加密是結合 對稱加密非對稱加密 各自優點的一種加密方式。其具體的實現思路是先使用 對稱加密演算法 對資料進行加密,然後使用非對稱加密演算法對 對稱加密的金鑰 進行非對稱加密,之後再把加密後的金鑰和加密後的資料傳送給接收方。

為了讓小夥伴們更加直觀理解上述的過程,阿寶哥花了點心思畫了一張圖,用來進一步說明混合加密的過程,下面我們就一起來看圖吧。

3.2 混合加密的過程

3.3 混合加密的實現

瞭解完 “混合加密資料傳輸流程”,阿寶哥跟小夥伴一起來實現一下上述的混合加密流程。這裡我們會基於前面介紹過的對稱加密和非對稱加密的示例進行開發,即以下示例會直接利用前面非對稱加密示例中用到的公私鑰。

3.3.1 建立生成隨機 AES 金鑰的函式
function getRandomAESKey() {
  return (
    Math.random().toString(36).substring(2, 10) +
    Math.random().toString(36).substring(2, 10)
  );
}
3.3.2 建立 AES 加密和解密函式
// AES加密
function aesEncrypt(key, iv, content) {
  let text = CryptoJS.enc.Utf8.parse(JSON.stringify(content));
  let encrypted = CryptoJS.AES.encrypt(text, key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7,
  });
  return encrypted.toString();
}

// AES解密
function aesDecrypt(key, iv, content) {
  let decrypt = CryptoJS.AES.decrypt(content, key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7,
  });
  return decrypt.toString(CryptoJS.enc.Utf8);
}
3.3.3 建立 RSA 加密器和解密器
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDocWYwnJ4DYur0BjxFjJkLv4QR
...
cyhRIeiXxs13vlSHVwIDAQAB
-----END PUBLIC KEY-----`;

const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDocWYwnJ4DYur0BjxFjJkLv4QRJpTJnwjiwxkuJZe1HTIIuLbu
...
bKkRJNJ2PpfWA45Vdq6u+izrn9e2TabKjWIfTfT/ZQ==
-----END RSA PRIVATE KEY-----`;

const rsaEncryptor = new JSEncrypt(); // RSA加密器
rsaEncryptor.setPublicKey(PUBLIC_KEY);

const rsaDecryptor = new JSEncrypt(); // RSA解密器
rsaDecryptor.setPrivateKey(PRIVATE_KEY);
3.3.4 建立混合加密加密和解密函式
function hybirdEncrypt(data) {
  const iv = getRandomAESKey();
  const key = getRandomAESKey();
  const encryptedData = aesEncrypt(key, iv, data);
  const encryptedIv = rsaEncryptor.encrypt(iv);
  const encryptedKey = rsaEncryptor.encrypt(key);
  return {
    iv: encryptedIv,
    key: encryptedKey,
    data: encryptedData,
   };
}

function hybirdDecrypt(encryptedResult) {
  const iv = rsaDecryptor.decrypt(encryptedResult.iv);
  const key = rsaDecryptor.decrypt(encryptedResult.key);
  const data = encryptedResult.data;
  return aesDecrypt(key, iv, data);
}
3.3.5 混合加密與解密示例

以上步驟完成之後,我們基本已經完成了混合加密的功能,在看完整程式碼之前,我們先來看一下實際的執行效果:

備註:密文解密下方對應的 textarea 文字框中,除了加密的資料之外,還會包含使用 RSA 加密過的 AES CBC 模式中的 iv 和 key。

在以上示例中,我們在頁面上建立了 3 個 textarea,分別用於存放明文、加密後的資料和解密後的明文。當使用者點選 加密 按鈕時,會對使用者輸入的明文進行混合加密,完成加密後,會把加密的資料顯示在密文對應的 textarea 中,當使用者點選 解密 按鈕時,會對密文進行 混合解密,即先使用 RSA 私鑰解密 AES 的 key 和 iv,然後再使用它們對 AES 加密過的密文進行 AES 解密,完成解密後,會把解密後的明文顯示在對應的 textarea 中。

以上示例對應的完整程式碼如下所示:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>混合加密與解密示例</title>
    <style>
      .block {
        flex: 1;
      }
    </style>
  </head>
  <body>
    <h3>阿寶哥:混合加密與解密示例</h3>
    <div style="display: flex;">
      <div class="block">
        <p>①明文加密 => <button onclick="encrypt()">加密</button></p>
        <textarea id="plaintext" rows="5" cols="15"></textarea>
      </div>
      <div class="block">
        <p>②密文解密 => <button onclick="decrypt()">解密</button></p>
        <textarea id="ciphertext" rows="5" cols="15"></textarea>
      </div>
      <div class="block">
        <p>③解密後的明文</p>
        <textarea id="decryptedCiphertext" rows="5" cols="15"></textarea>
      </div>
    </div>
    <!-- 引入 CDN Crypto.js AES加密 -->
    <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/core.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/enc-base64.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/md5.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/evpkdf.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/cipher-core.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/aes.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/pad-pkcs7.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/enc-utf8.min.js"></script>
    <!-- 引入 CDN Crypto.js 結束 -->
    <script src="https://cdn.bootcdn.net/ajax/libs/jsencrypt/2.3.1/jsencrypt.min.js"></script>
    <script>
      const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDocWYwnJ4DYur0BjxFjJkLv4QR
JpTJnwjiwxkuJZe1HTIIuLbu/yHyHLhc2MAHKL0Ob+8tcKXKsL1oxs467+q0jA+g
lOUtBXFcUnutWBbnf9qIDkKP2uoDdZ//LUeW7jibVrVJbXU2hxB8bQpBkltZf/xs
cyhRIeiXxs13vlSHVwIDAQAB
-----END PUBLIC KEY-----`;
      const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDocWYwnJ4DYur0BjxFjJkLv4QRJpTJnwjiwxkuJZe1HTIIuLbu
/yHyHLhc2MAHKL0Ob+8tcKXKsL1oxs467+q0jA+glOUtBXFcUnutWBbnf9qIDkKP
2uoDdZ//LUeW7jibVrVJbXU2hxB8bQpBkltZf/xscyhRIeiXxs13vlSHVwIDAQAB
AoGAKOarYKpuc5IYXdArEuHmnFaa2pm7XK8LVTuXVrNuuoPkpfw61Fs4ke3T0yKg
x6G3gq7Xm1tTEROAgMtaxqwo1D5n1H0nkyDFggLB0K9Ws0frp7HENtSQwdNSry1A
iD8TLxkhoWo7BS0VViLT1gKOfnw4YeMJP+CcOQ+DQjCsUMECQQD0Nc0vKBTlK6GT
28gcIMVoQy2KicjiH222A9/TLCNAQ9DEeZDYEptuTfrlbggfWdgQ3nc6CyvGf6c5
6uBPi/+5AkEA86oqqZPi7ekkUVHx0VSkp0mTlD1tAPhDE8cLX8vyImGExS+tTznz
ROyzm3T1M1PisdQIU8Wd5rqvHP6dB0enjwJAauhKpMQ1MYYCPApQ9g9anCQcgbOD
34nGq5HSoE2IOQ/3Cqv1PsIWjRlSJrIemCrqrafWJfDR/xnPCUnLXMd68QJAPNwG
1d4zMvslcA5ImOFMUuBEtST2geSAVINFqwK0krPKxrmWzxAJW/DHF5AJ4m0UVRhB
kDLusn90V4iczgGurwJAZUz6w01OeoLhsOuWNvkbTq+IV0NQ5GAEGA721Ck5zp86
bKkRJNJ2PpfWA45Vdq6u+izrn9e2TabKjWIfTfT/ZQ==
-----END RSA PRIVATE KEY-----`;

      const rsaEncryptor = new JSEncrypt(); // RSA加密器
      rsaEncryptor.setPublicKey(PUBLIC_KEY);

      const rsaDecryptor = new JSEncrypt(); // RSA解密器
      rsaDecryptor.setPrivateKey(PRIVATE_KEY);

      const plaintextEle = document.querySelector("#plaintext");
      const ciphertextEle = document.querySelector("#ciphertext");
      const decryptedCiphertextEle = document.querySelector(
        "#decryptedCiphertext"
      );

      function getRandomAESKey() {
        return (
          Math.random().toString(36).substring(2, 10) +
          Math.random().toString(36).substring(2, 10)
        );
      }

      // AES加密
      function aesEncrypt(key, iv, content) {
        let text = CryptoJS.enc.Utf8.parse(JSON.stringify(content));
        let encrypted = CryptoJS.AES.encrypt(text, key, {
          iv: iv,
          mode: CryptoJS.mode.CBC,
          padding: CryptoJS.pad.Pkcs7,
        });
        return encrypted.toString();
      }

      // AES解密
      function aesDecrypt(key, iv, content) {
        let decrypt = CryptoJS.AES.decrypt(content, key, {
          iv: iv,
          mode: CryptoJS.mode.CBC,
          padding: CryptoJS.pad.Pkcs7,
        });
        return decrypt.toString(CryptoJS.enc.Utf8);
      }

      function hybirdEncrypt(data) {
        const iv = getRandomAESKey();
        const key = getRandomAESKey();
        const encryptedData = aesEncrypt(key, iv, data);
        const encryptedIv = rsaEncryptor.encrypt(iv);
        const encryptedKey = rsaEncryptor.encrypt(key);
        return {
          iv: encryptedIv,
          key: encryptedKey,
          data: encryptedData,
        };
      }

      function hybirdDecrypt(encryptedResult) {
        const iv = rsaDecryptor.decrypt(encryptedResult.iv);
        const key = rsaDecryptor.decrypt(encryptedResult.key);
        const data = encryptedResult.data;
        return aesDecrypt(key, iv, data);
      }

      function encrypt() {
        let plaintext = plaintextEle.value;
        const encryptedResult = hybirdEncrypt(plaintext);
        ciphertextEle.value = JSON.stringify(encryptedResult);
      }

      function decrypt() {
        let ciphertext = ciphertextEle.value;
        const encryptedResult = JSON.parse(ciphertext);
        decryptedCiphertextEle.value = hybirdDecrypt(encryptedResult).replace(/\"/g,'');
      }
    </script>
  </body>
</html>

3.4 混合加密方案分析

通過這個示例,相信大家對混合加密已經有了一定的瞭解。但在實際 Web 專案中,我們一般不會在客戶端進行資料解密,而是會把資料提交到服務端,然後由服務端進行資料解密和資料處理。

HTTP 協議對大多數 Web 開發者來說,都不會陌生。HTTP 協議是基於請求和響應,具體如下圖所示:

在對資料安全要求較高的場景或傳輸敏感資料時,我們就可以考慮利用前面的混合加密方案對提交到服務端的資料進行混合加密,當服務端接收到對應的加密資料時,再使用對應的解密演算法對加密的資料進行解密,從而進一步進行資料處理。

但是如果服務端也要返回敏感資料時,應該怎麼辦呢?這裡阿寶哥給大家介紹一種方案,該方案只需使用一對公私鑰。當然該方案僅供大家參考,如果你有好的方案,歡迎給阿寶哥留言或跟阿寶哥交流喲。

下面我們來看一下該方案的具體操作流程:

① 生成一個唯一的 reqId(請求 ID),用於標識當前請求;

② 分別生成一個隨機的 AES Key 和 AES IV(採用 AES CBC 模式);

③ 採用 RSA 非對稱加密演算法,分別對 AES Key 和 AES IV 進行 RSA 非對稱加密;

④ 採用隨機生成的 AES Key 和 AES IV 對敏感資料進行 AES 對稱加密;

⑤ 把 reqId 作為 key,AES Key 和 AES IV 組成的物件作為 value 儲存到 Map 或 {} 物件中;

⑥ 把 reqId、加密後的 AES Key、AES IV 和加密後的資料儲存到物件中提交到服務端;

⑦ 當服務端接收到資料後,對接收的資料進行解密,然後使用客戶端傳過來的解密後的 AES Key 和 AES IV 對響應資料進行 AES 對稱加密;

⑧ 服務端在完成資料加密後,把 reqId 和加密後的資料包裝成響應物件,返回給客戶端;

⑨ 當客戶端成功接收服務端的響應後,先獲取 reqId,進而從儲存 AES Key 和 IV 的 Map 獲取該 reqId 對應的 AES 加密資訊;

⑩ 客戶端使用當前 reqId 對應的加密資訊,對服務端返回的資料進行解密,當完成解密之後,從 Map 或 {} 物件中刪除已有記錄。

現在我們來對上述流程做個簡單分析,首先 AES 加密資訊都是隨機生成的且根據每個請求獨立地儲存到記憶體中,把 AES 加密資訊中的 Key 和 IV 提交到服務端的時候都會使用 RSA 非對稱加密演算法進行加密。

在服務端返回資料的時候,會使用當前請求對應的 AES 加密資訊對返回的結果進行加密,同時返回當前請求對應的 reqId(請求 ID)。即服務端不需要再生成新的 AES 加密資訊,來對響應資料進行加密,這樣就不需要在響應物件中傳遞 AES 加密資訊。

該方案看似挺完美的,由於我們加密的資訊還是存在記憶體中,如果使用開發者工具對 Web 應用進行除錯時,那麼還是可以看到每個請求對應的加密資訊。那麼這個問題該如何解決呢?能不能防止使用開發者工具對我們的 Web 應用進行除錯,答案是有的。

不過這裡阿寶哥就不繼續展開了,後面可能會單獨寫一篇文章來介紹如何防止使用開發者工具除錯 Web 應用,感興趣的小夥伴可以給我留言喲。

四、阿寶哥有話說

4.1 什麼是訊息摘要演算法

其實在日常工作中,除了對稱加密和非對稱加密演算法之外。還有一種用得比較廣的訊息摘要演算法。訊息摘要演算法是密碼學演算法中非常重要的一個分支,它通過對所有資料提取指紋資訊以實現資料簽名、資料完整性校驗等功能,由於其不可逆性,有時候會被用做敏感資訊的加密。訊息摘要演算法也被稱為雜湊(Hash)演算法或雜湊演算法。

任何訊息經過雜湊函式處理後,都會獲得唯一的雜湊值,這一過程稱為 “訊息摘要”,其雜湊值稱為 “數字指紋”,其演算法自然就是 “訊息摘要演算法”了。 換句話說,如果其數字指紋一致,就說明其訊息是一致的。

(圖片來源 —— https://zh.wikipedia.org/wiki...

訊息摘要演算法的主要特徵是加密過程不需要金鑰,並且經過加密的資料無法被解密,目前可以解密逆向的只有 CRC32 演算法,只有輸入相同的明文資料經過相同的訊息摘要演算法才能得到相同的密文。 訊息摘要演算法不存在金鑰的管理與分發問題,適合於分散式網路上使用。訊息摘要演算法主要應用在 “數字簽名” 領域,作為對明文的摘要演算法。著名的摘要演算法有 RSA 公司的 MD5 演算法和 SHA-1 演算法及其大量的變體。

訊息摘要演算法擁有以下特點:

  • 無論輸入的訊息有多長,計算出來的訊息摘要的長度總是固定的。 例如應用 MD5 演算法摘要的訊息有 128 個位元位,用 SHA-1 演算法摘要的訊息最終有 160 個位元位的輸出,SHA-1的變體可以產生 192 個位元位和 256 個位元位的訊息摘要。一般認為,摘要的最終輸出越長,該摘要演算法就越安全。
  • 訊息摘要看起來是 “隨機的”。 這些位元看上去是胡亂的雜湊在一起的,可以用大量的輸入來檢驗其輸出是否相同,一般,不同的輸入會有不同的輸出,而且輸出的摘要訊息可以通過隨機性檢驗。一般地,只要輸入的訊息不同,對其進行摘要以後產生的摘要訊息也必不相同;但相同的輸入必會產生相同的輸出。
  • 訊息摘要函式是單向函式,即只能進行正向的資訊摘要,而無法從摘要中恢復出任何的訊息,甚至根本就找不到任何與原資訊相關的資訊。
  • 好的摘要演算法,沒有人能從中找到 “碰撞” 或者說極度難找到,雖然 “碰撞” 是肯定存在的(碰撞即不同的內容產生相同的摘要)。

4.2 什麼是 MD5 演算法

MD5(Message Digest Algorithm 5,訊息摘要演算法版本5),它由 MD2、MD3、MD4 發展而來,由 Ron Rivest(RSA 公司)在 1992 年提出,目前被廣泛應用於資料完整性校驗、資料(訊息)摘要、資料簽名等。MD2、MD4、MD5 都產生 16 位元組(128 位)的校驗值,一般用 32 位十六進位制數表示。MD2 的演算法較慢但相對安全,MD4 速度很快,但安全性下降,MD5 比 MD4 更安全、速度更快。

隨著計算機技術的發展和計算水平的不斷提高,MD5 演算法暴露出來的漏洞也越來越多。1996 年後被證實存在弱點,可以被加以破解,對於需要高度安全性的資料,專家一般建議改用其他演算法,如 SHA-2。2004 年,證實 MD5 演算法無法防止碰撞(collision),因此不適用於安全性認證,如 SSL 公開金鑰認證或是數字簽名等用途。

4.2.1 MD5 特點
  • 穩定、運算速度快。
  • 壓縮性:輸入任意長度的資料,輸出長度固定(128 位元位)。
  • 運算不可逆:已知運算結果的情況下,無法通過通過逆運算得到原始字串。
  • 高度離散:輸入的微小變化,可導致運算結果差異巨大。
4.2.2 MD5 雜湊

128 位的 MD5 雜湊在大多數情況下會被表示為 32 位十六進位制數字。以下是一個 43 位長的僅 ASCII 字母列的MD5 雜湊:

MD5("The quick brown fox jumps over the lazy dog")
= 9e107d9d372bb6826bd81d3542a419d6

即使在原文中作一個小變化(比如把 dog 改為 cog,只改變一個字元)其雜湊也會發生巨大的變化:

MD5("The quick brown fox jumps over the lazy cog")
= 1055d3e698d289f2af8663725127bd4b

接著我們再來舉幾個 MD5 雜湊的例子:

         MD5("") -> d41d8cd98f00b204e9800998ecf8427e 
MD5("semlinker") -> 688881f1c8aa6ffd3fcec471e0391e4d
   MD5("kakuqo") -> e18c3c4dd05aef020946e6afbf9e04ef
4.2.3 MD5 演算法的用途

檔案分發防篡改

在網際網路上分發軟體安裝包時,出於安全性考慮,為了防止軟體被篡改,比如在軟體安裝程式中新增木馬程式。軟體開發者通常會使用訊息摘要演算法,比如 MD5 演算法產生一個與檔案匹配的數字指紋,這樣接收者在接收到檔案後,就可以利用一些現成的工具來檢查檔案完整性。

訊息傳輸防篡改

假設在網路上你需要傳送電子文件給你的朋友,在檔案傳送前,先對文件的內容進行 MD5 運算,得出該電子文件的 “數字指紋”,並把該 “數字指紋” 隨電子文件一同傳送給對方。當對方接收到電子文件之後,也使用 MD5 演算法對文件的內容進行雜湊運算,在運算完成後也會得到一個對應 “數字指紋”,當該指紋與你所傳送文件的 “數字指紋” 一致時,表示文件在傳輸過程中未被篡改。

4.2.4 MD5 演算法的缺陷

雜湊碰撞是指不同的輸入卻產生了相同的輸出,好的雜湊演算法,應該沒有人能從中找到 “碰撞” 或者說極度難找到,雖然 “碰撞” 是肯定存在的。

2005 年山東大學的王小云教授釋出演算法可以輕易構造 MD5 碰撞例項,此後 2007 年,有國外學者在王小云教授演算法的基礎上,提出了更進一步的 MD5 字首碰撞構造演算法 “chosen prefix collision”,此後還有專家陸續提供了MD5 碰撞構造的開源的庫。

2009 年,中國科學院的謝濤和馮登國僅用了 220.96 的碰撞演算法複雜度,破解了 MD5 的碰撞抵抗,該攻擊在普通計算機上執行只需要數秒鐘。

MD5 碰撞很容易構造,基於 MD5 來驗證資料完整性已不可靠,考慮到近期谷歌已成功構造了 SHA-1(英語:Secure Hash Algorithm 1,中文名:安全雜湊演算法1)的碰撞例項,對於資料完整性,應使用 SHA256 或更強的演算法代替。

其實 MD5 的相關知識還有挺多,比如 MD5 密文反向查詢、密碼加鹽和實現內容資源防盜鏈等。這裡阿寶哥就不繼續展開了,感興趣的小夥伴可以閱讀阿寶哥之前寫的 ”一文讀懂 MD5 演算法“ 這篇文章。

五、參考資源