對稱加密與非對稱加密

餘騰靖發表於2019-09-22

加密在程式設計中的應用的是非常廣泛的,尤其是在各種網路協議之中,對稱/非對稱加密則是經常被提及的兩種加密方式。

對稱加密

我們平時碰到的絕大多數加密就是對稱加密,比如:指紋解鎖,PIN 碼鎖,保險箱密碼鎖,賬號密碼等都是使用了對稱加密。

對稱加密:加密和解密用的是同一個密碼或者同一套邏輯的加密方式。

這個密碼也叫對稱祕鑰,其實這個對稱和不對稱指的就是加密和解密用的祕鑰是不是同一個

我在上大學的時候做過一個命令列版的圖書館管理系統作為 C 語言課設。登入系統時需要輸入賬號密碼,當然,校驗使用者輸入的密碼本身就是一種對稱加密,使用者必須輸入的密碼必須和你之前設定的賬號密碼相同

當時我選擇將賬號密碼存放在本地檔案中,但是如果這個檔案被竊取了,而且沒有對密碼本身進行加密的話密碼就洩露了。那麼如何對儲存在檔案中的密碼進行加密呢?我當時採取的方式(也可以理解為一種加密演算法)是:將使用者設定的賬號密碼的每一個字元取它的碼值然後加上一個固定的值比如 6,然後儲存的時候儲存計算後的碼值字串。用 js 來模擬一下:

// 加密使用者密碼過程
const sourcePassword = 'abcdef'; // 使用者賬號密碼
// 加密後的儲存在本地檔案的賬號密碼
const encryptedPassword = [...sourcePassword].map((char) => char.codePointAt(0) + 6).join();
console.log(`加密後的賬號密碼是:${encryptedPassword}`) // => 加密後的賬號密碼是:103,104,105,106,107,108

// 解密過程
const decryptedPassword = encryptedPassword.split(',').map((codePoint) => String.fromCodePoint(codePoint - 6)).join('');
console.log(`解密後的賬號密碼是:${decryptedPassword}`); // => 解密後的賬號密碼是:abcdef

複製程式碼

上面對使用者賬號密碼加密的過程雖然沒有明顯涉及到用於加密的密碼,但是加密解密用的都是同一套邏輯:取字元的 ascii 碼值加 6,其實這也是對稱加密。其實也可以理解為加密密碼就是加密演算法本身,知道了加密演算法就能解碼出賬號密碼。

有些軟體支援對資料進行加密如 rar 加密的時候需要你輸入密碼,然後解密的時候需要我們輸入設定的加密密碼。其實回到到我上面說的那個很簡單的加密演算法,系統可以設計成讓使用者可以設定一個用於加密賬號密碼的加密密碼。這裡簡化為一個數字,然後系統在儲存使用者賬號密碼的時候就不是像之前那樣每個使用者都是加上固定的 6 了,而是加上使用者設定的這個數字。現在你這個加密邏輯就可以像 rar 公開壓縮演算法那樣公開,即便是被攻擊者知道了加密演算法和加密後的賬號密碼,如果不知道加密密碼還是無法獲取使用者賬號密碼。這裡的加密密碼和解密密碼用的都是同一個密碼,對應到上面那個系統就是 6,所以也是對稱加密。

使用 nodejs 來進行對稱加密

nodejs 的 crypto 模組是一個專門用於各種加密的模組,可以用來取摘要(hash),加鹽摘要(hmac),對稱加密,非對稱加密等。使用 crypto 進行對稱加密很簡單,crypto 模組提供了 Cipher 類用於加密資料,Decipher 用於解密。

常見的對稱加密演算法有DES3DESAESBlowfishIDEARC5RC6,這裡演示下使用 AES 演算法進行對稱加密。

const crypto = require('crypto');

/**
 * 對稱加密字串
 * @param {String} password 用於對稱加密的祕鑰
 * @param {String} string 被加密的資料
 * @return {String} encryptedString 加密後的字串
 */
const encrypt = (password, string) => {
  // 使用的對稱加密演算法是:aes-192-cbc
  const algorithm = 'aes-192-cbc';
  // 生成對稱加密祕鑰,salt 用於生成祕鑰,24 指定祕鑰長度是 24 位
  const key = crypto.scryptSync(password, 'salt', 24);
  console.log('key:', key); // => key: <Buffer f0 ca 6c ac 39 3c b3 f9 77 13 0d d9 bc cb dd 9d 86 f7 96 e0 75 53 7f 8a>
  console.log(`祕鑰長度: ${key.length}`); // => 祕鑰長度: 24

  // 初始化向量
  const iv = Buffer.alloc(16, 0);
  // 獲得 Cipher 加密類
  const cipher = crypto.createCipheriv(algorithm, key, iv);

  // utf-8 指定被加密的資料字元編碼,hex 指定輸出的字元編碼
  let encryptedString = cipher.update(string, 'utf8', 'hex');
  encryptedString += cipher.final('hex');

  return encryptedString;
};

const PASSWORD = 'lyreal666';
const encryptedString = encrypt(PASSWORD, '天分不夠努力來湊');
console.log(`加密後的資料是:${encryptedString}`); // => 加密後的資料是:1546756bb4e530fc1fbae7fd2cf9aeac0368631b54581a39e5c53ee3172638de 

/**
 * 解密字串
 * @param {String} password 加密密碼
 * @param {String} encryptedString 加密後的字串
 * @return {String} decryptedString 解密後的字串
 */
const decrypt = (password, encryptedString) => {
  const algorithm = 'aes-192-cbc';
  // 採用相同的演算法生成相同的祕鑰
  const key = crypto.scryptSync(password, 'salt', 24);
  const iv = Buffer.alloc(16, 0);
  // 生成 Decipher 解密類
  const decipher = crypto.createDecipheriv(algorithm, key, iv);

  let decryptedString = decipher.update(encryptedString, 'hex', 'utf8');
  decryptedString += decipher.final('utf8');

  return decryptedString;
};

const decryptedString = decrypt(PASSWORD, encryptedString);
console.log(`解密後的資料時:${decryptedString}`); // => 解密後的資料時:天分不夠努力來湊
複製程式碼

非對稱加密

非對稱加密用的是一對祕鑰,分別叫做公鑰(public key)和私鑰(private key),也叫非對稱祕鑰。非對稱祕鑰既可以用於加密還可以用於認證,我們先聊加密。

加密有一個密碼就行了,為啥要整個非對稱加密要兩個密碼呢?

黑人問號.jpg

我相信肯定會有人和我一樣有過這樣的想法。其實對稱加密只要保證加密的密碼長度足夠長的話,被加密的資料在拿不到密碼本身的情況下一般是安全的。但是有個問題就是在實際應用中比如加密網路資料,因為加密和解密使用的是同一個祕鑰,所以,伺服器和客戶端必然是要交換祕鑰的,而正是因為非對稱祕鑰由於有一個交換祕鑰這一過程可能會被中間人竊取祕鑰,一旦對稱加密祕鑰被竊取,而且被分析出加密演算法的話,那麼傳輸的資料對於中間人來說就是透明的。所以對稱加密的致命性缺點就是無法保證祕鑰的安全性

那麼非對稱加密就能保證祕鑰的安全性了嗎?是的,祕鑰可以大膽的公開,被公開的祕鑰就叫公鑰。非對稱加密的祕鑰由加密演算法計算得出,是成對的,可以被公開的那個祕鑰稱之為公鑰不能公開的那個私有的祕鑰叫私鑰

在使用 github 等 git 倉庫託管平臺的時候,我們一般都會配置 ssh 公鑰,生成一對祕鑰我們可以採用下面的命令:

rsa public/private key pair

上面使用 ssh-keygen 程式指定加密演算法為 rsa,在當前目錄生成一對祕鑰 keykey.pubkey 是私鑰,key.pub 是公鑰,字尾 pub 全拼明顯是 public 嘛。來看一下生成的具體內容:

public/private key content

這個 key.pub 是個公鑰,公鑰就是被設計來可以隨意公開的。我們把這個公鑰配置到託管平臺,就可以不用每次和 github 通訊都要輸入密碼了。

這對公私鑰有一個特點,同時也是非對稱加密為什麼安全的關鍵就是:使用祕鑰對中的一個祕鑰加密,加密後的資料只能通過另一個祕鑰解密。也就是說使用一對祕鑰中的公鑰加密資料,只能通過另一個私鑰解密出資料。或者反過來,使用一對祕鑰中的私鑰進行加密的資料,只能通過另一個公鑰解密出來。由此可見,從加密的角度來看,公鑰和私鑰其實作用是等同的,都可以用於加密或解密,只不過當我們使用非對稱祕鑰用於加密資料時往往是用公鑰進行加密。

在 https 的加密中,加密傳輸的資料本身使用的是對稱加密,加密對稱祕鑰時使用的非對稱加密。整個過程是這樣的:server 端先生成一對非對稱祕鑰,將可以公開的公鑰傳送給 client 端,client 端也決定此次資料傳輸使用的對稱加密演算法和對稱祕鑰,然後利用 server 端給的公鑰,對對稱祕鑰進行加密傳輸。server 端接受到 client 端傳送的對稱加密演算法和祕鑰後,server 端和 client 端的資料傳輸都使用這個對稱祕鑰和演算法進行對稱加密。整個過程中即便 server 端的公鑰被中間人知道了內容,但是沒有儲存在 server 端的私鑰,你是無法破譯使用公鑰加密的對稱祕鑰的。公鑰原本就是可以被隨意公開的,拿到也沒用,解密需要的是私鑰。非對稱加密或者說公鑰加密之所以能保證加密安全就是因為私鑰是保密不公開的,攻擊者沒有私鑰無法破譯

可能會有人有疑問:為什麼需要使用非對稱加密對對稱祕鑰加密呢?那是因為交換對稱祕鑰時可能被第三方竊取,對稱祕鑰被竊取了那對稱加密就沒意義了。還有為什麼不直接使用非對稱加密來加密傳輸內容而只是加密對稱祕鑰?非對稱加密不是對稱加密更安全嗎?這就和對稱加密與非對稱加密的特點有關係了。

非對稱加密和對稱加密對比

  1. 對稱加密是一個祕鑰,非對稱加密是一對,兩個祕鑰
  2. 非對稱加密比起對稱加密更安全,因為不存在祕鑰洩露問題,公鑰即便被知道也沒關係
  3. 由於使用非對稱加密在計算上特別複雜,所以一般來說對稱加密的加密解密的速度相對於非對稱加密快很多
  4. 非對稱祕鑰還可以用於認證

由於以上第三條,所以在 https 中傳輸資料時不會使用非對稱加密加密傳輸資料,傳輸資料時有可能資料本身很大,那樣的話非對稱加密更耗時了,所以傳輸資料時不會使用非對稱加密的方式加密。

使用 nodejs 演示非對稱加密

常見的非對稱加密有 RSA、ECC(橢圓曲線加密演算法)、Diffie-Hellman、El Gamal、DSA(數字簽名用),這裡演示一下 RSA 加密。

const crypto = require('crypto');

// 祕鑰加密短語
const passphrase = 'lyreal666';

// rsa 指定非對稱祕鑰演算法為 rsa
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
  modulusLength: 4096, // 指定祕鑰長度
  publicKeyEncoding: {
    type: 'spki', // 公鑰編碼格式
    format: 'pem',
  },
  privateKeyEncoding: {
    type: 'pkcs8', // 私鑰編碼格式
    format: 'pem',
    cipher: 'aes-256-cbc',
    passphrase,
  },
});

/**
 * 使用公鑰加密
 * @param {String} publicKey 用於對稱加密的祕鑰
 * @param {String} string 被加密的資料
 * @return {String} encryptedString 加密後的字串
 */
const encrypt = (publicKey, string) => {
  // 使用公鑰加密
  return crypto.publicEncrypt({ key: publicKey, passphrase } , Buffer.from(string)).toString('hex');
};

/**
 * 使用私鑰解密字串
 * @param {String} privateKey 私鑰
 * @param {String} encryptedString 加密後的字串
 * @return {String} decryptedString 解密後的字串
 */
const decrypt = (privateKey, encryptedString) => {
    return crypto.privateDecrypt({ key: privateKey, passphrase } , Buffer.from(encryptedString, 'hex'));
}

const string = '說好不哭,不愛我就拉倒ヽ(`⌒´)ノ';
const encryptedString = encrypt(publicKey, string);
console.log(`公鑰加密後的結果:${encryptedString}`); // => 公鑰加密後的結果:caf7535c46146f5...
const decryptedString = decrypt(privateKey, encryptedString);
console.log(`私鑰解密後的結果:${decryptedString}`); // => 私鑰解密後的結果:說好不哭,不愛我就拉倒ヽ(`⌒´)ノ 
複製程式碼

非對稱金鑰認證

非對稱加密有時也叫公鑰加密,而非對稱祕鑰認證也被稱為私鑰認證。我們說使用非對稱祕鑰對資料進行認證其實就是說確認一個資料是否有沒有被篡改過。非對稱祕鑰除了用於加密資料,用於認證也是非常廣泛的,比如手機 apk 的簽名, https 中的證照。

原理很簡單:比如現在我要認證一個 apk 的程式碼是否被串改過,首先準備一對非對稱祕鑰,一般來自權威機構。官方在打包 apk 時不但包含應用程式碼,還帶上一個簽名,這個簽名這裡簡單理解為使用私鑰對應用程式碼的 hash 值加密後的資料。在安裝 apk 時,android 系統會提取 apk 中的簽名,使用公鑰解密簽名得到原始應用程式碼的 hash,然後和原始應用程式碼的 hash 進行比對,如果內容相同,那麼 apk 沒有被篡改過。如果 apk 的應用程式碼被第三方修改了,那麼從簽名中解密出來的 hash 和應用程式碼的 hash 肯定是不同的。所以可以起到確保應用程式碼沒有篡改,也就是認證

認證的關鍵其實是因為簽名的存在,簽名必須保證能拿到 apk 原始應用程式碼的 hash。至於如何保證簽名沒有被篡改不在本文討論範圍。

可能有人看了上面對 apk 認證的過程會有這麼一個疑問:使用私鑰對內容加密可以達到認證的目的,那能不能使用公鑰加密來認證呢?

答案肯定是不能的,如果你使用公鑰對內容進行加密,那中間人要篡改你的內容,偽造簽名超簡單,直接使用公鑰對偽造後的內容的 hash 加密就可以了。所以使用非對稱祕鑰可以用於認證的另一個關鍵就是私鑰是不公開的,中間人沒法獲取私鑰,也就沒法偽造簽名。

幾個疑問

hash 算是加密嗎?

我覺得不算,hash 是不可逆的,加密應該是可以根據加密後的資料還原的。

base 64 算是加密嗎?

是對稱加密,對稱祕鑰就是 base 64 字元碼錶。

非對稱加密絕對安全嗎?

沒有什麼加密是絕對安全的,非對稱加密存在交換公鑰時公鑰被篡改的問題。

由於我並沒有專門學過密碼學,以上內容皆由我通過總結以往所學所瞭解的知識所寫,難免會有偏頗之處,甚至說帶有很明顯的個人觀點。歡迎讀者在評論處指出文中錯誤和討論。

感謝您的閱讀,如果對你有所幫助不妨加個關注,點個贊支援一下O(∩_∩)O。

本文為原創內容,首發於個人部落格,轉載請註明出處。

相關文章