Web端如何用SHA-256计算文件哈希值
背景:业务要求对需要上链的文件使用SHA-256计算哈希值,小文件都是可以直接通过 加密算法库 crypto-js 中的 SHA256方法 进行计算 ,但在文件大小超过一定的值(10-50Mb)会出现浏览器页面会 内存溢出 ,甚至页面崩溃。那如何在浏览器计算大文件的哈希值呢?
下面的代码有以下问题:
- 文件过大时会网页会内存溢出;
- 阻碍了主线程的文件上传
import CryptoJS from 'crypto-js'
//...
const files = e.target.files[0]
const fileReads = new FileReader()
fileReads.readAsArrayBuffer(files)
fileReads.onload = () =>{
const wordArray = CryptoJS.lib.WordArray.create(fileReads.result)
const sha256 = CryptoJS.SHA256(wordArray)
const hash = sha256.toString()//转16进制
}
网页为什么会内存溢出
是指网页中存在无法回收的内存或使用的内存过多,最终使得浏览器运行要用到的内存大于能提供的最大内存。此时浏览器就运行不了,浏览器会提示页面崩溃,有时候会自动关闭浏览器;
计算文件时内存溢出的原因
上面代码中用到了CryptoJS.lib .WordArray.create() ,这个方法短时间内会不断的开辟新的内存来保存文件的二进制数组,但长时间得不到释放,占用内存越来越多,从而导致内存溢出。
要使用 CryptoJS加密库对文件计算,需要转换文件暂存区的ArrayBuffer 转化为 CryptoJS需要的wordArray格式,拿就需要对这个数据for循环进行转换,一个14.3KB大小的文件都要创造出14732长度的数组,那10MB就有千万长度的数组了......,所以内存溢出是必然的。
阻碍主线程文件上传的原因就不说了,JS是单线程懂的都懂;
问题一:解决思路
首先想到的是文件切块,等分切块去计算文件。这样就不会有内存溢出了。但是问题立马就出现了,每一块文件都是独立的,得到的数据难道单独加密,那就得不到整个文件的哈希值了。那怎么办?难道计算每个小块的哈希再合并?NO,这里需要介绍SHA-256算法原理:
SHA-256简单介绍
算法基本描述:(详细请看文末-参考资料)
- 补位
- 补长度
- 使用的常量
- 使用的主要函数
- 计算消息摘要
算法基本流程:
- 哈希初值H(0)
const hash = [
0x6A09E667, 0xBB67AE85,
0x3C6EF372, 0xA54FF53A,
0x510E527F, 0x9B05688C,
0x1F83D9AB, 0x5BE0CD19
]
- 工作流程
- 将原始消息分为N个512bit的消息块。每个消息块分成16个32bit的字标记为M(i)0、M(i)1、M(i)2、…、M(i)15然后对这N个消息块依次进行主循环进行处理,从而影响哈希初始值
- 最后对N个消息块依次进行以上四步操作后将最终得到的H(N)0、H(N)1、H(N)2、…、H(N)7 串联起来即可得到最后的256bit消息摘要。详细的串联代码可以在文末的代码仓库或crypto-js库中的sha256.js源码中可以看到 。
基本流程
从上面我们得知可以在只需要在工作流程的第一届阶段,把文件分成小块传给主循环函数便可以,我们需要的是从算法库里面独立出来第一阶段和第二阶段,并对文件的每一块进行第一阶段的主循环,来影响H0-H7,最终在文件最后一块的时候在进行第二阶段的计算,最终得到我们想要的哈希值,也就是消息摘要。
文件切块(部分代码)
let bufferSize = 1024*1024
let block = {
fileSize: file.size,
start: 0,
end: bufferSize
}
let reader = new FileReader();
let blob = file.slice(block.start, block.end);
reader.readAsArrayBuffer(blob);
reader.onload = ()=>{
//将拿到的传给计算函数 之后继续调用 start=file.size file.size += bufferSize
};
计算逻辑(部分代码)
需要将文件缓存的数据转成可以操作的数据类型
let hash = [ 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19 ];
function sha256(m, H) {
var w = [],
a, b, c, d, e, f, g, h, i, j,
t1, t2;
for (var i = 0; i < m.length; i += 16) {
a = H[0];
b = H[1];
c = H[2];
d = H[3];
e = H[4];
f = H[5];
g = H[6];
h = H[7];
for (var j = 0; j < 64; j++) {
if (j < 16) w[j] = m[j + i];
else {
var gamma0x = w[j - 15],
gamma1x = w[j - 2],
gamma0 = ((gamma0x << 25) | (gamma0x >>> 7)) ^
((gamma0x << 14) | (gamma0x >>> 18)) ^
(gamma0x >>> 3),
gamma1 = ((gamma1x << 15) | (gamma1x >>> 17)) ^
((gamma1x << 13) | (gamma1x >>> 19)) ^
(gamma1x >>> 10);
w[j] = gamma0 + (w[j - 7] >>> 0) +
gamma1 + (w[j - 16] >>> 0);
}
var ch = e & f ^ ~e & g,
maj = a & b ^ a & c ^ b & c,
sigma0 = ((a << 30) | (a >>> 2)) ^
((a << 19) | (a >>> 13)) ^
((a << 10) | (a >>> 22)),
sigma1 = ((e << 26) | (e >>> 6)) ^
((e << 21) | (e >>> 11)) ^
((e << 7) | (e >>> 25));
t1 = (h >>> 0) + sigma1 + ch + (K[j]) + (w[j] >>> 0);
t2 = sigma0 + maj;
h = g;
g = f;
f = e;
e = (d + t1) >>> 0;
d = c;
c = b;
b = a;
a = (t1 + t2) >>> 0;
}
H[0] = (H[0] + a) | 0;
H[1] = (H[1] + b) | 0;
H[2] = (H[2] + c) | 0;
H[3] = (H[3] + d) | 0;
H[4] = (H[4] + e) | 0;
H[5] = (H[5] + f) | 0;
H[6] = (H[6] + g) | 0;
H[7] = (H[7] + h) | 0;
}
return H;
}
function computer(event) {//传过来的文件块
let uint8_array, message, block, nBitsTotal, output, nBitsLeft, nBitsTotalH, nBitsTotalL;
uint8_array = new Uint8Array(event.data.message);//转为10进制
message = bytesToWords(uint8_array);//10转为
block = event.data.block;
event = null;
uint8_array = null;
output = {
'block' : block
};
if (block.end === block.file_size) {
nBitsTotal = block.file_size * 8;
nBitsLeft = (block.end - block.start) * 8;
nBitsTotalH = Math.floor(nBitsTotal / 0x100000000);
nBitsTotalL = nBitsTotal & 0xFFFFFFFF;
message[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsTotal % 32);
message[((nBitsLeft + 64 >>> 9) << 4) + 14] = nBitsTotalH;
message[((nBitsLeft + 64 >>> 9) << 4) + 15] = nBitsTotalL;
hash = sha256(message, hash);
//打印结果
console.log(bytesToHex(wordsToBytes(hash)))
} else {
hash = sha256(message, self.hash);
}
}
function bytesToWords(t) {
for (var e = [], a = 0, n = 0; a < t.length; a++,
n += 8)
e[n >>> 5] |= t[a] << 24 - n % 32;
return e
}
function wordsToBytes(a){
for(var b=[],c=0;c<a.length*32;c+=8){
b.push(a[c>>>5]>>>24-c%32&255)
}
return b
}
function bytesToHex(a){
for(var b=[],c=0;c<a.length;c++){
b.push((a[c]>>>4).toString(16)),
b.push((a[c]&15).toString(16));
}
return b.join("")
}
对文件操作所要注意的
不管是对哪种数据类型,位操作对象的本质都是一段连续的比特序列。从性能的角度讲,位操作最好是能直接操作连续的内存位。那就是通过二进制位操作符。在含有位操作符的运算中,都得通过 ToInt32() 转换为 32 位有符号整数,然后将其当做 32 位的比特序列进行位运算,运算结果返回也为 32 位有符号整数。因此,通过拼接 32 位有符号整数,就可以实现“对一段连续的比特序列进行位操作”的功能了。
正是基于这样的原理, CryptoJs 实现了名为WordArray的类,作为“一段连续比特序列”的抽象进行各种位操作。 WordArray 是 CryptoJs 中最核心的一个类,所有主要算法的实际操作对象都是WordArray对象。理解WordArray是理解CryptoJs各算法的基础 words 为 32 位有符号整数构成的数组,通过按顺序拼接数组中的数,就组成了比特序列。 JavaScript 中 32 位有符号整数是通过补码转换为二进制的,不过在这里我们不需要关注这点,因为这个整数的值是没有意义的,实际使用中,比特序列更多的是用字节作单位,或用 16 进制数表示,因此我们只需要知道 32 位等价于 4 个字节,等价 于 8个 16 进制数。
问题二:解决思路
Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。
- 将计算相关逻辑存入js,将该js作为后台线程文件
let myWorker = new Worker('hash.js');
线程之间通信通过监听
你可以通过postMessage() 方法和onmessage事件处理函数触发workers的方法。当你想要向一个worker发送消息时,你只需要这样做(main.js):
function message(value) {
myWorker.postMessage([value]);
console.log('Message posted to worker');
}
在worker中接收到消息后,我们可以写这样一个事件处理函数代码作为响应(hash.js):
onmessage = function(e) {
console.log('Message received from main script');
var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
console.log('Posting message back to main script');
postMessage(workerResult);
}
Web Worker
Web Worker 有以下几个使用注意点。
(1)同源限制
分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。
(2)DOM 限制
Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用document、window、parent这些对象。但是,Worker 线程可以navigator对象和location对象。
(3)通信联系
Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。
(4)脚本限制
Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。
(5)文件限制
Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。
web worker使用注意
在webpack相关项目中需要特定的插件去编译web worker相关文件,不同版本坑还不同需要多多调试;