1. 程式人生 > >【重寫 CryptoJS】二、WordArray 與位操作

【重寫 CryptoJS】二、WordArray 與位操作

原始碼地址:entronad/crypto-es

 

【重寫 CryptoJS】一、ECMAScript 類與繼承

我們常見的各種編碼、雜湊、加密演算法,其基礎都是位操作。

不管是對哪種資料型別,位操作物件的本質都是一段連續的位元序列。從效能的角度講,位操作最好是能直接操作連續的記憶體位。很多語言提供了直接操作連續記憶體位的操作,比如 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 打字機