這裡介紹如何使用 AES-JS 這個 JavaScript 的 AES 對稱式加密工具,讓有機密性的重要資料加上密碼保護。

AES 進階加密標準(Advanced Encryption Standard)是一種對稱式的加密演算法,此標準替代了原先的 DES 加密演算法,目前被各界廣泛使用。

AES 加密演算法的區塊長度固定為 128 位元,金鑰長度則可以是 128、192 或 256 位元,在設計結構及密鑰的長度上俱已到達保護機密資訊的標準,也是美國政府官方認可的加密演算法之一。

安裝 AES-JS 函式庫

在 JavaScript 中若要使用 AES 演算法來加密資料,可以使用 AES-JS 這個開放原始碼的 JavaScript 函式庫,它同時可以在瀏覽器(網頁)以及 Node.js 環境下使用。

名稱:AES-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];

每個數字代表一個位元組,所以它的值必須在 0255 之間,使用者可以自行選擇要使用哪一種長度的金鑰,當然理論上越長的金鑰安全性越高。

我們也可以使用 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 原始檔回去使用。

參考資料:寫程式是良心事業CodeDatajsaes