這裡介紹如何使用 AES-JS 這個 JavaScript 的 AES 對稱式加密工具,讓有機密性的重要資料加上密碼保護。
AES 進階加密標準(Advanced Encryption Standard)是一種對稱式的加密演算法,此標準替代了原先的 DES 加密演算法,目前被各界廣泛使用。
AES 加密演算法的區塊長度固定為 128 位元,金鑰長度則可以是 128、192 或 256 位元,在設計結構及密鑰的長度上俱已到達保護機密資訊的標準,也是美國政府官方認可的加密演算法之一。
安裝 AES-JS 函式庫
在 JavaScript 中若要使用 AES 演算法來加密資料,可以使用 AES-JS 這個開放原始碼的 JavaScript 函式庫,它同時可以在瀏覽器(網頁)以及 Node.js 環境下使用。
網址:GitHub
在網頁中使用的話,要引入 AES-JS 函式庫的 JavaScript 檔案。
<script type="text/javascript" src="https://cdn.rawgit.com/ricmoo/aes-js/e27b99df/index.js"></script>
若是在 Node.js 環境中,則可使用 npm
安裝 aes-js
這個套件:
npm install aes-js
然後在 Node.js 指令稿中引入:
var aesjs = require('aes-js');
AES-JS 在使用上來說,不管是在瀏覽器中還是 Node.js 環境下,語法都相同,以下是用 AES-JS 加密與解密資料的步驟與範例。
定義金鑰
在使用 AES 之前,要先定義加密與解密用的金鑰,也就是俗稱的密碼。AES 所使用的金鑰長度有三種,分別是 128 位元、192 位元與 256 位元:
// 128 位元、192 位元與 256 位元的金鑰 var key_128 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; var key_192 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]; var key_256 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31];
每個數字代表一個位元組,所以它的值必須在 0
到 255
之間,使用者可以自行選擇要使用哪一種長度的金鑰,當然理論上越長的金鑰安全性越高。
我們也可以使用 Uint8Array
來儲存金鑰:
var key_128_array = new Uint8Array(key_128); var key_192_array = new Uint8Array(key_192); var key_258_array = new Uint8Array(key_256);
若在 Node.js 環境下,也可以使用 Buffer
來儲存金鑰:
var key_128_buffer = new Buffer(key_128); var key_192_buffer = new Buffer(key_192); var key_258_buffer = new Buffer(key_256);
初始向量
固定的明文資料在經過固定的加密演算法計算之後,會產生一模一樣的密文資料,這樣就有可能造成機密資料外洩。
初始向量(initialization vector,簡稱 IV)的用途在於將明文資料加入隨機性,讓相同的明文資料再加密之後不會產生相同的密文資料。
資料加密與解密
使用 AES 加密時,需要自己選擇要用哪一種 block cipher mode 進行加密,這部份牽涉到一些加密的理論,請參考維基百科的說明,中文的說明可參考寫程式是良心事業。
block cipher mode 有好多種可以選擇,比較建議使用的是 CTR(counter)與 CBC(cipher-block chaining)這兩種。
ECB(electronic codebook)沒有使用任何 IV,會讓同樣的資料 block 產生相同的加密輸出,所以比較不安全,應盡量避免使用。
CTR(Counter)
推薦使用的方法之一,其使用一連串不重複的整序序列(即 counter)結合 nonce(即 IV),使用金鑰加密後,產生一連串不同的密文區塊(cipher block),在用這一連串不同的密文區塊對資料進行 XOR 運算,得到最後的加密結果,確保同樣的明文資料不會產生相同的密文資料。
使用 CTR 加密的資料,不需要做補齊(padding)的動作,也就是說可以對任意長度的資料直接加密與解密。
// 定義 128 位元的金鑰(16 bytes * 8 bits/byte = 128 bits) var key = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 ]; // 將文字轉換為位元組 var text = 'Text may be any length you wish, no padding is required.'; var textBytes = aesjs.utils.utf8.toBytes(text); // Counter 可省略,若省略則從 1 開始 var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5)); var encryptedBytes = aesCtr.encrypt(textBytes); // 加密過後的資料是二進位資料,若要輸出可轉為十六進位格式 var encryptedHex = aesjs.utils.hex.fromBytes(encryptedBytes); console.log(encryptedHex); // 將十六進位的資料轉回二進位 var encryptedBytes = aesjs.utils.hex.toBytes(encryptedHex); // 解密時要建立另一個 Counter 實體 var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5)); var decryptedBytes = aesCtr.decrypt(encryptedBytes); // 將二進位資料轉換回文字 var decryptedText = aesjs.utils.utf8.fromBytes(decryptedBytes); console.log(decryptedText);
CBC(Cipher-Block Chaining)
推薦使用的方法之一,在第一個資料區塊加密時,指定一個 IV 然後結合金鑰進行加密,接著將加密後的密文作為第二個資料區塊加密時的 IV,以此類推,直到所有資料區塊都被加密完成。
// 定義 128 位元的金鑰 var key = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 ]; // 定義初始向量(16 bytes) var iv = [ 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36 ]; // 將文字轉換為位元組(資料長度必須為 16 bytes 的倍數) var text = '1234567890123456'; var textBytes = aesjs.utils.utf8.toBytes(text); // 建立 CBC 串鏈 var aesCbc = new aesjs.ModeOfOperation.cbc(key, iv); var encryptedBytes = aesCbc.encrypt(textBytes); // 加密過後的資料是二進位資料,若要輸出可轉為十六進位格式 var encryptedHex = aesjs.utils.hex.fromBytes(encryptedBytes); console.log(encryptedHex); // 將十六進位的資料轉回二進位 var encryptedBytes = aesjs.utils.hex.toBytes(encryptedHex); // 由於舊的 CBC 串鏈會儲存一些內部的狀態, // 所以解密時要重新建立一個新的 CBC 串鍊 var aesCbc = new aesjs.ModeOfOperation.cbc(key, iv); var decryptedBytes = aesCbc.decrypt(encryptedBytes); // 將二進位資料轉換回文字 var decryptedText = aesjs.utils.utf8.fromBytes(decryptedBytes); console.log(decryptedText);
CFB(Cipher Feedback)
CFB 與 CBC 很類似,概念上大同小異,都是拿前一個加密後的密文,來為下一次的加密加上隨機性。
// 定義 128 位元的金鑰 var key = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 ]; // 定義初始向量(16 bytes) var iv = [ 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36 ]; // 將文字轉換為位元組(長度必須是 segment 大小的倍數) var text = 'TextMustBeAMultipleOfSegmentSize'; var textBytes = aesjs.utils.utf8.toBytes(text); // segment 大小,預設為 1 var segmentSize = 8; // CFB 加密 var aesCfb = new aesjs.ModeOfOperation.cfb(key, iv, segmentSize); var encryptedBytes = aesCfb.encrypt(textBytes); // 轉為十六進位格式 var encryptedHex = aesjs.utils.hex.fromBytes(encryptedBytes); console.log(encryptedHex); // 將十六進位的資料轉回二進位 var encryptedBytes = aesjs.utils.hex.toBytes(encryptedHex); // CFB 加密時會存有內部的狀態資訊,解密時要另外建立一個實體 var aesCfb = new aesjs.ModeOfOperation.cfb(key, iv, segmentSize); var decryptedBytes = aesCfb.decrypt(encryptedBytes); // 將二進位資料轉換回文字 var decryptedText = aesjs.utils.utf8.fromBytes(decryptedBytes); console.log(decryptedText);
OFB(Output Feedback)
OFB 與 CFB 的結構幾乎相同,不過他是拿 IV 與金鑰加密之後的輸出來當作下一次加密的 IV。
OFB 可以對任意長度的資料直接加密與解密。
// 定義 128 位元的金鑰 var key = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 ]; // 定義初始向量(16 bytes) var iv = [ 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34,35, 36 ]; // 將文字轉換為位元組 var text = 'Text may be any length you wish, no padding is required.'; var textBytes = aesjs.utils.utf8.toBytes(text); // OFB 加密 var aesOfb = new aesjs.ModeOfOperation.ofb(key, iv); var encryptedBytes = aesOfb.encrypt(textBytes); // 轉為十六進位格式 var encryptedHex = aesjs.utils.hex.fromBytes(encryptedBytes); console.log(encryptedHex); // 將十六進位的資料轉回二進位 var encryptedBytes = aesjs.utils.hex.toBytes(encryptedHex); // OFB 加密時會存有內部的狀態資訊,解密時要另外建立一個實體 var aesOfb = new aesjs.ModeOfOperation.ofb(key, iv); var decryptedBytes = aesOfb.decrypt(encryptedBytes); // 將二進位資料轉換回文字 var decryptedText = aesjs.utils.utf8.fromBytes(decryptedBytes); console.log(decryptedText);
ECB(Electronic Codebook)
ECB 就是很簡單的使用金鑰直接加密與解密,沒有使用 IV,所以同樣的明文會產生同樣的密文,不建議使用!
// 定義 128 位元的金鑰 var key = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 ]; // 將文字轉換為位元組 var text = 'TextMustBe16Byte'; var textBytes = aesjs.utils.utf8.toBytes(text); // ECB 加密 var aesEcb = new aesjs.ModeOfOperation.ecb(key); var encryptedBytes = aesEcb.encrypt(textBytes); // 轉為十六進位格式 var encryptedHex = aesjs.utils.hex.fromBytes(encryptedBytes); console.log(encryptedHex); // 將十六進位的資料轉回二進位 var encryptedBytes = aesjs.utils.hex.toBytes(encryptedHex); // ECB 並不會儲存任何內部資訊,解密時可以使用同一個實體 //var aesEcb = new aesjs.ModeOfOperation.ecb(key); var decryptedBytes = aesEcb.decrypt(encryptedBytes); // 將二進位資料轉換回文字 var decryptedText = aesjs.utils.utf8.fromBytes(decryptedBytes); console.log(decryptedText);
金鑰與密碼長度
在 AES 加密演算法中,金鑰的角色就是密碼,但是 AES 限制金鑰的長度必須為 128 位元(16 位元組)、192 位元(24 位元組)或 256 位元(32 位元組),這樣的限制在使用上並不方便,若想要讓 AES 接受任意長度的密碼,可以使用類似 PBKDF 或 SHA256 之類的轉換,把任意長度的文字密碼轉換為指定長度的 AES 金鑰:
var pbkdf2 = require('pbkdf2'); var key_128 = pbkdf2.pbkdf2Sync('password', 'salt', 1, 128 / 8, 'sha512'); var key_192 = pbkdf2.pbkdf2Sync('password', 'salt', 1, 192 / 8, 'sha512'); var key_256 = pbkdf2.pbkdf2Sync('password', 'salt', 1, 256 / 8, 'sha512');
本文的 block cipher mode 圖片都是從維基百科上面的圖來修改的,需要的人可以下載 SVG 原始檔回去使用。