【重寫 CryptoJS】二、WordArray 與位操作
原始碼地址:entronad/crypto-es
我們常見的各種編碼、雜湊、加密演算法,其基礎都是位操作。
不管是對哪種資料型別,位操作物件的本質都是一段連續的位元序列。從效能的角度講,位操作最好是能直接操作連續的記憶體位。很多語言提供了直接操作連續記憶體位的操作,比如 C++ 中的陣列與指標,ECMAScript 6 中的 ArrayBuffer 。 JavaScript 最初是作為瀏覽器的指令碼語言設計的,並沒有直接操作記憶體的特性,但還是有辦法獲取到位元序列的抽象,那就是通過二進位制位操作符( Binary Bitwise Operators )。
根據標準,在含有位操作符的運算中,不管是什麼型別的運算元,都通過 ToInt32() 轉換為 32 位有符號整數,然後將其當做 32 位的位元序列進行位運算,運算結果返回也為 32 位有符號整數。因此,通過拼接 32 位有符號整數,就可以實現“對一段連續的位元序列進行位操作”的功能了。
正是基於這樣的原理, CryptoJs 實現了名為 WordArray 的類,作為“一段連續位元序列”的抽象進行各種位操作。 WordArray 是 CryptoJs 中最核心的一個類,所有主要演算法的實際操作物件都是 WordArray 物件。理解 WordArray 是理解 CryptoJs 各演算法的基礎,也為今後使用 ArrayBuffer 重寫的前提。
WordArray 的定義位於 core.js 中:
注:以下所有程式碼為 entronad/crypto-es 中的重寫程式碼
export class WordArray extends Base {
constructor(words = [], sigBytes = words.length * 4) {
super();
this.words = words;
this.sigBytes = sigBytes;
}
...
}
它直接繼承自 Base ,有 words 和 sigBytes 兩個成員變數。words 為 32 位有符號整數構成的陣列,通過按順序拼接陣列中的數,就組成了位元序列。 JavaScript 中 32 位有符號整數是通過補碼轉換為二進位制的,不過在這裡我們不需要關注這點,因為這個整數的值是沒有意義的,實際使用中,位元序列更多的是用位元組作單位,或用 16 進位制數表示,因此我們只需要知道 32 位等價於 4 個位元組,等價 於 8個 16 進位制數。
編碼演算法的物件是字元,因此實際位元序列長度都是整位元組的,即 8 的倍數,但不一定是 32 的倍數,因此僅通過 words 陣列是不能反映位元序列實際長度的,最後可能有多餘位,因此 WordArray 有第二個成員變數 sigBytes ,表示實際有效位元組數( significant bytes )。
可通過直接傳入這兩個欄位構建例項:
const wordArray = CryptoES.lib.WordArray.create([0x00010203, 0x04050607], 6);
為方便 sigBytes 對 words 陣列的控制, WordArray 中定義了一個特別的方法 clamp :
clamp() {
// Shortcuts
const { words, sigBytes } = this;
// Clamp
words[sigBytes >>> 2] &= 0xffffffff << (32 - (sigBytes % 4) * 8);
words.length = Math.ceil(sigBytes / 4);
}
clamp 意指壓縮,作用是移除 words 中不是有效的位元組( insignificant bits )。前段全是有效位元組的 word 直接保留,末段完全沒有有效位元組的 word 通過 words.length = Math.ceil(sigBytes / 4)
移除。
比較麻煩的是中間不全是有效位元組的一個分界 word 。首先算出要去除的位數: (32 - (sigBytes % 4) * 8)
,對 0xffffffff
左移該位數獲得一個 32 位的掩碼,然後通過 sigBytes >>> 2
(相當於整除 4 )找到分界 word 下標,通過與掩碼取與將無效位元組置 0 。
這種右移定位下標和掩碼與或計算在 CryptoJS 中非常普遍。
與 clamp 類似,拼接兩個 WordArray 的 concat 方法主要麻煩之處也在處理分界 word :
concat(wordArray) {
// Shortcuts
const thisWords = this.words;
const thatWords = wordArray.words;
const thisSigBytes = this.sigBytes;
const thatSigBytes = wordArray.sigBytes;
// Clamp excess bits
this.clamp();
// Concat
if (thisSigBytes % 4) {
// Copy one byte at a time
for (let i = 0; i < thatSigBytes; i += 1) {
const thatByte = (thatWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
thisWords[(thisSigBytes + i) >>> 2] |= thatByte << (24 - ((thisSigBytes + i) % 4) * 8);
}
} else {
// Copy one word at a time
for (let i = 0; i < thatSigBytes; i += 4) {
thisWords[(thisSigBytes + i) >>> 2] = thatWords[i >>> 2];
}
}
this.sigBytes += thatSigBytes;
// Chainable
return this;
}
在 CryptoJs 內部 WordArray 是各演算法主要的操作物件和結果,不過外部使用者想要的結果還是指定編碼方式的字串,因此 WordArray 有重寫的 toString 方法:
toString(encoder = Hex) {
return encoder.stringify(this);
}
由於 words 陣列是引用型別,因此 clone 方法需要重寫一下,通過 slice 複製一份拷貝:
clone() {
const clone = super.clone.call(this);
clone._data = this._data.clone();
return clone;
}
除了建構函式,還有一個靜態函式生成指定位元組長度的隨機 WordArray 。由於 Math.random() 提供的不是安全的隨機數,且型別為 64 位浮點數,所以生成過程中進行了一些處理:
static random(nBytes) {
const words = [];
const r = (m_w) => {
let _m_w = m_w;
let _m_z = 0x3ade68b1;
const mask = 0xffffffff;
return () => {
_m_z = (0x9069 * (_m_z & 0xFFFF) + (_m_z >> 0x10)) & mask;
_m_w = (0x4650 * (_m_w & 0xFFFF) + (_m_w >> 0x10)) & mask;
let result = ((_m_z << 0x10) + _m_w) & mask;
result /= 0x100000000;
result += 0.5;
return result * (Math.random() > 0.5 ? 1 : -1);
};
};
for (let i = 0, rcache; i < nBytes; i += 4) {
const _r = r((rcache || Math.random()) * 0x100000000);
rcache = _r() * 0x3ade67b7;
words.push((_r() * 0x100000000) | 0);
}
return new WordArray(words, nBytes);
}
題圖: Royal 打字機