先看一個在 Node.js 中使用 AES 對文件內(nèi)容進行加密的例子:
const fs = require('fs')
const { createCipheriv, randomBytes } = require('crypto')
const key = randomBytes(32)
const iv = randomBytes(16)
const cipher = createCipheriv('aes-256-cbc', key, iv)
// 加密文件
fs.createReadStream('plain.txt')
.pipe(cipher)
.pipe(fs.createWriteStream('aes-256-cbc.dat'))
// 輸出 key,iv
fs.writeFileSync('aes-256-cbc.key', key.toString('hex') + '|' + iv.toString('hex'))
輸出的 key 和 iv 分別為:
435157a4775085cef2aa2d44dd399eeea4a3cc304f85c0ef895770943e5ec4fc
294926351aad0cc4e8ce88c8356575b5
對加密后的文件進行解密:
const fs = require('fs')
const { createDecipheriv } = require('crypto')
const [key, iv] = fs.readFileSync('aes-256-cbc.key', {encoding: 'utf8'}).split('|')
const decipher = createDecipheriv('aes-256-cbc', Buffer.from(key, 'hex'), Buffer.from(iv, 'hex'))
fs.createReadStream('aes-256-cbc.dat')
.pipe(decipher)
.pipe(fs.createWriteStream('aes-256-cbc.txt'))
如果你感興趣,可以把代碼復(fù)制出來跑一下看看。
接下來我并不想繼續(xù)介紹 Node.js 中的加密、解密 API 如何,這些在 Node.js 官方文檔中都有:
在上面的例子,使用了算法 aes-256-cbc 進行加解密。AES 是一種對稱密碼算法,而后面的 256 和 cbc 是指什么?iv 又是什么呢?
如果你對這感興趣,那么本文正適合你。
AES 對稱密碼
AES 全稱為“Advanced Encryption Standard”,是美國政府于 2001 年通過公開競標征集的對稱密碼算法,用于替代已經(jīng)老舊的 DES 算法。
AES 有三種密鑰長度:128,192 和 256 比特。上面例子中的 256 就表示密鑰長度為 256 比特。
由于 AES 算法是公開的,所以決定加密數(shù)據(jù)的機密性的關(guān)鍵是 密鑰。密鑰越長,則可供選擇的組合數(shù)目越多,也就是密鑰空間越大,對于暴力破解來說難度也就越高。
AES 每次加密的數(shù)據(jù)塊大小是確定的 128 比特,如果數(shù)據(jù)超過這個大小,就需要進行拆分,分別進行加密后再組裝,解密的過程則相反。
看起來對數(shù)據(jù)進行分組加密再組合是很簡單的事情,最多額外約定下如果分組數(shù)據(jù)不夠 128 比特如何填補就好了。
分組工作模式
ECB 模式
前面提到的思路,就是 ECB(Electronic CodeBook)模式的實現(xiàn)方式:

解密過程相反:分解 - 解密 - 合并。
但是這種模式是不夠安全的,我們考慮數(shù)據(jù)的發(fā)送方(Alice)和接收方(Bob)之間有一個攻擊者(Mallory)的情況。
由于對稱密碼的存在,Mallory 并不能解密數(shù)據(jù)。但是 ECB 模式下,數(shù)據(jù)只是簡單分組加密再組裝,Mallory 如果改變密文分組的順序,Bob 解密得到的明文也就被改變了。
如果 Mallory 知曉通信報文的格式,就可以通過調(diào)整密文分組的順序達到特殊的目的。例如 Alice 發(fā)送轉(zhuǎn)賬數(shù)據(jù)給 Bob,Mallory 從中對調(diào)了付款人和收款人信息對應(yīng)的分組,就會導(dǎo)致 Bob 接收到錯誤數(shù)據(jù)。
能夠在不破譯密文的情況下操縱明文,這是 ECB 模式的一大弱點。
接下來,我們看下 CBC 模式是怎么應(yīng)對這種攻擊的。
CBC 模式
CBC 全稱 Cipher Block Chaining,也就是密文分組鏈接模式:

上一個分組加密結(jié)果先與下一個分組明文執(zhí)行 XOR(異或)運算,然后再加密。由于第一個明文分組沒有上一個密文分組可以使用,所以需要事先傳人一個數(shù)據(jù),也就是 IV(初始化向量),其長度與分組的大小相同。對于 AES 密碼算法,其分組大小為 128 bit,所以使用的 IV 也就是 128 比特。
采用 CBC 模式,密文中如果有一個分組缺失,后續(xù)所有分組都無法正確解密;如果一個分組數(shù)據(jù)被篡改或損壞,由于鏈接關(guān)系的存在,會影響下一個分組的解密(因為解密需要使用上一分組的密文進行 XOR 運算)。
如果 Mallory 對分組順序進行了調(diào)換,由于相鄰分組鏈接的關(guān)系,會導(dǎo)致無法正常解密。
當(dāng)然,確保數(shù)據(jù)在傳輸過程中不被篡改(完整性),使用消息認真碼(MAC,Message Authentication Code)是更好的方案。
根據(jù)前一分組信息介入的時機和方式的不同,還有其他幾種和 CBC 模式類似的模式。
CFB 模式
CFB,Cipher FeedBack,密文反饋模式:

看起來有點奇怪,第一個分組中,應(yīng)用 AES 進行加密的其實 IV,IV 的密文再和明文進行 XOR 運算得到分組最終密文。其實 XOR 也是一種加密運算,而 IV 的密文則可以看作是生成的 一次性密碼本。
OFB 模式
OFB,Output-FeedBack,輸出反饋模式:

和 CFB 模式非常類似,只是提供下一組的密鑰是密碼算法的輸出,而非最終的密文。
OFB 的優(yōu)勢是可以拋開明文,事先計算好每個分組需要的密鑰,所以生成密鑰流和對明文進行 XOR 運算是可以并行的。
CTR 模式
CTR,CounTer,計數(shù)器模式:

CTR 模式和 CBC 模式差別就更大了,CTR 模式是通過對逐次累加的計數(shù)器進行加密來生成密鑰流的 流密碼。
CTR 和 OFB 有點類似,真正對明文加密的過程,是基于密鑰流進行 XOR 運算。而 CTR 模式更進一步,由于是采用計數(shù)器的模式,任意分組的密鑰是可以提前計算的,不依賴前一分組,所以在支持并行計算的系統(tǒng)中,CTR 模式的速度會非??臁?/p>
小結(jié)
這里介紹的分組密碼模式有很多,一般而言,ECB 模式是不應(yīng)該使用的,而通常使用的是 CBC 模式和 CTR 模式。
認證加密模式
如果在加密過程中,同時還生成了有關(guān)數(shù)據(jù)的認證信息,則既可以確保數(shù)據(jù)的機密性,又能確保完整性。
認證加密的方法有:
- Encrypt-then-MAC (EtM):明文加密,密文生成 MAC
- Encrypt-and-MAC (E&M):明文加密,明文生成 MAC
- MAC-then-Encrypt (MtE):明文生成 MAC,明文和MAC一起加密
Node.js 文檔中給出的一個使用 aes-192-ccm 的例子,CCM 模式就是一個滿足認證加密要求的算法:
總結(jié)
回過頭,再看下本文開頭的算法 aes-256-cbc,我們知道:
-
AES:對稱密碼算法 -
256:選擇的 AES 密鑰長度 -
CBC:分組密碼模式
在使用該算法進行加密時,需要傳入兩個參數(shù):
-
key:AES 的密鑰,256 比特 -
iv:CBC 模式需要的初始化向量,與分組大小一致,AES 分組為固定的 128 比特
另外,密鑰 key 是重要的機密,要妥善保存,通信雙方如果需要傳輸該密鑰,需要額外通過其他機制(密鑰交換/協(xié)商機制)。而初始化向量 iv 則無需秘密保存,但要避免在反復(fù)使用相同的數(shù)據(jù),盡量每次創(chuàng)建隨機數(shù)據(jù)。