1. 程式人生 > 實用技巧 >在瀏覽器端如何進行檔案簽名的計算

在瀏覽器端如何進行檔案簽名的計算

在瀏覽器端如何進行檔案簽名的計算

現在有這樣一個需求:我們需要對一個任意檔案在瀏覽器端進行一個簽名的計算,以供後一步的比對校驗使用。

要求:簽名演算法包括但不限於MD5、SHA-1、SHA-256、SHA-384、SHA-512等演算法。

經驗:因為之前有簡單調研過 Web Cryptography API 介面下的功能和底層實現,所以可以確保Web Cryptography擁有在計算簽名方面的能力。

思路:在 WebCryptoAPIREC推薦標準MDN Crypto介面介面支援情況查詢後,初步判定我們可以使用Crypto API下的SubtleCrypto interface下的digest方法

用於SHA系列演算法的實現,MD5演算法無法從Web Cryptography下獲得需要另行找實現庫或自己造輪子。

介面支援情況

SubtleCrypto interface的實現情況:

Chrome Edge Firefox Internet Explorer Opera Safari WebView Android Chrome Android Firefox Android Opera Android iOS Safari Samsung Internet
subtleExperimental Full support37 Full support12 Full support34Open Partial support11 Full support24 Full support10.1Open Full support37 Full support37 Full support34Open Full support24 Full support10.3Open

Web Crypto介面下的演算法及功能表,來自REC文件

Algorithm name encrypt decrypt sign verify digest generateKey deriveKey deriveBits importKey exportKey wrapKey unwrapKey
RSASSA-PKCS1-v1_5
RSA-PSS
RSA-OAEP
ECDSA
ECDH
AES-CTR
AES-CBC
AES-GCM
AES-KW
HMAC
SHA-1
SHA-256
SHA-384
SHA-512
HKDF
PBKDF2

我們的需求實現是在一個>=Gecko44.0版本的嵌入式裝置上,滿足我們需求的實現。

原型實現

我們需要使用digest方法,檢視其方法描述:

14.3.5. The digest method
The digest method returns a new Promise object that will digest data using the specified AlgorithmIdentifier. It must act as follows:

Let algorithm be the algorithm parameter passed to the digest method.

Let data be the result of getting a copy of the bytes held by the data parameter passed to the digest method.

Let normalizedAlgorithm be the result of normalizing an algorithm, with alg set to algorithm and op set to "digest".

If an error occurred, return a Promise rejected with normalizedAlgorithm.

Let promise be a new Promise.

Return promise and asynchronously perform the remaining steps.

If the following steps or referenced procedures say to throw an error, reject promise with the returned error and then terminate the algorithm.

Let result be the result of performing the digest operation specified by normalizedAlgorithm using algorithm, with data as message.

Resolve promise with result.

簡單的說就是:輸入生成摘要的演算法型別資料,輸出一個包含資料摘要Promise物件

語法:

const digest = crypto.subtle.digest(algorithm, data);

簡繪我們需要的流程圖:

檔案 -> Uint8Array資料 -> 摘要演算法 -> buffer摘要 -> 轉Uint8Array並轉成16進位制串

程式碼:

var response = new Response(file);

// 也可使用FileReader WebAPI或其他的BOM提供的處理檔案的API
response.arrayBuffer().then((arrayBuffer) => {
  
  // buffer -> uint8array
  var uint8Array = new Uint8Array(arrayBuffer);
  
  // digest()方法處理 -> 得到摘要buffer
  // type可選:SHA-1、SHA-256、SHA-384、SHA-512
  window.crypto.subtle.digest('SHA-512', uint8Array).then(hashBuffer => {
    
    // hashBuffer轉普通陣列
    var hashArray = Array.from(new Uint8Array(hashBuffer));
    
    // 摘要結果轉16進位制字串表示
    // 其中padStart用於將字元'0-F'轉為'00-0F',不然摘要長度將發生變化
    var hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    
    console.log(hashHex);
  })
});

其中在我們的目標平臺上需要相容padStart方法,獲取polyfill方式:

// padStart()方法用另一個字串填充當前字串(如果需要的話,會重複多次),以便產生的字串達到給定的長度。從當前字串的左側開始填充。
// https://github.com/uxitten/polyfill/blob/master/string.polyfill.js
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
/**
 * String.padStart()
 * version 1.0.1
 * Feature	        Chrome  Firefox Internet Explorer   Opera	Safari	Edge
 * Basic support	57   	51      (No)	            44   	10      15
 * -------------------------------------------------------------------------------
 */
if (!String.prototype.padStart) {
  String.prototype.padStart = function padStart(targetLength, padString) {
    targetLength = targetLength >> 0; //floor if number or convert non-number to 0;
    padString = String(typeof padString !== 'undefined' ? padString : ' ');
    if (this.length > targetLength) {
      return String(this);
    } else {
      targetLength = targetLength - this.length;
      if (targetLength > padString.length) {
        padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed
      }
      return padString.slice(0, targetLength) + String(this);
    }
  };
}

測試

  1. vim編輯文字檔案test:'hello!' 並儲存退出,使用sha512sum test命令計算得到:

dd80bc1a06f03059682d2bb1327a0c3b3eee84c46fe7d973e804fe76d433738209ecc8bc49f8bae345fef1eb865dbc92f17fd83ac584f9760cf62bd92af5075c

  1. 在瀏覽器端敲入以下內容,因為編輯器vim自動添加了換行符,故此處需補換行符\n
var response = new Response(new Blob(['hello!\n']));
response.arrayBuffer().then((arrayBuffer) => {
  var uint8Array = new Uint8Array(arrayBuffer);
  window.crypto.subtle.digest('SHA-512', uint8Array).then(hashBuffer => {
    var hashArray = Array.from(new Uint8Array(hashBuffer));
    var hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    console.log(hashHex);
  })
});
//  dd80bc1a06f03059682d2bb1327a0c3b3eee84c46fe7d973e804fe76d433738209ecc8bc49f8bae345fef1eb865dbc92f17fd83ac584f9760cf62bd92af5075c

步驟1,2結果呈一致性。也可實際在瀏覽器端使用input輸入檔案或者輸入伺服器端的檔案進行測試。

擴充套件-MD5

Github上基於MD5摘要演算法實現的庫有非常多,可輕鬆進行擴充套件。筆者目前所使用的 browser-md5-file

它的API描述:

browserMD5File(file, (err, md5) => {})

注意

因為MD5摘要演算法為上層(JS庫)實現,所以速度是沒有SHA-系列摘要演算法(底層使用的C實現的OpenSSL)速度快的。並且由於MD5的可碰撞性,不推薦作為目前的日常使用。

同理,使用Web Crypto下的API去進行對檔案的加解密工作、簽名和驗證簽名工作都是可以實現的。