如何在浏览器中加密文件,听着像是一个伪需求,但细心想想,并不是没有地方用得到。尽管浏览器中的代码是直接暴露给用户的,但是只要处理得当,存储的安全性自然是更上一层。
使用场景
-
在线应用
- 如果你只需要保存一个网址就可以随时随地加密你想加密的文件,那么何乐而不为呢?
-
增加后台文件存储强度
- 当用户上传文件之后,后台拿到文件可以进行加密,将密钥和密文分离存储,那么即使不小心泄露了其中一份数据,也无关紧要。而后端加密的这部分工作,可以转移到浏览器来做,这对后端的压力可以降低不少。
准备
需要一个浏览器中加密的库,我选用的是 crypto-js
实现
首先,我们可以在文件选择空间的 change
事件中拿到 File
对象。
const filePicker = document.body.querySelector('your-file-picker');
filePicker.addEventListener('change', event => {
const file = event.target.files[0];
// Read file
});
File
只是一个浏览器与真正文件交互的接口,接下来我们需要利用 FileReader
来读取文件的内容到内存中。
FileReader
有四个读取文件的接口,分别是
FileReader.readAsArrayBuffer() // 读取二进制数据到 ArrayBuffer
FileReader.readAsBinaryString() // 不推荐
FileReader.readAsDataURL() // data:URL格式的字符串(base64编码)
FileReader.readAsText() // 按文本方式读取文件
加密文件是根据二进制数据来的,这里我们使用 readAsArrayBuffer
。ArrayBuffer
不能直接被操作,我们需要将它转成类型数组对象使用。
// Read file
const reader = new FileReader();
reader.onload = event => {
const uint8View = new Uint8Array(event.target.result)
// Process binary data
}
reader.readAsArrayBuffer(file);
到这里,我们已经获取到无符号整型 8 位的二进制数据了,接下来就进行加密。为了方便演示,我们使用 aes-256-ecb
的加密方式。
crypto-js
可处理的数据是它定义的 WordArray
的格式。而 WordArray
有几种转换方式,可以在 https://cryptojs.gitbook.io/docs/#encoders 查阅。其中一种是通过十六进制字符串获得,因此我们需要将 uint8View
转换成十六进制字符串,然后再转换成 WordArray
进行加密。
// Process binary data
const hexResult = uint8View.reduce((hex, current) => hex + current.toString(16), '');
const message = CryptoJS.enc.Hex.parse(hexResult);
const pass = 'your-password-here';
const result = CryptoJS.AES.encrypt(message, key512Bits1000Iterations, {
mode: CryptoJS.mode.ECB,
});
// Save to file
result
就是加密后的内容了。不过既然是文件加密,自然最终结果也要是文件。因此,我们还要将 result
中的内容写入文件才算结束。
写入的过程和加密的过程基本一致。result.toString()
返回的是 base64 字符串,因此需要先 decode base64,然后将十六进制字符串转换成 TypedArray
,也就是无符号整型8位数组,写入 File
对象。
最后提供对 File
对象下载的支持,至此,整个加密过程结束。
// Save to file
const e64 = CryptoJS.enc.Base64.parse(result.toString());
const hex = e64.toString(CryptoJS.enc.Hex);
const view = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
const d = hex.slice(i, i + 2);
view[i / 2] = parseInt(d, 16);
}
const file = new File([view], 'yourfile.ecb');
const link = document.createElement('a');
link.setAttribute('href', objectUrl);
link.innerText = 'Download';
link.download = 'yourfile.ecb';
document.body.append(link);
最终,处理的代码如下
const filePicker = document.body.querySelector('your-file-picker');
filePicker.addEventListener('change', event => {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = event => {
const uint8View = new Uint8Array(event.target.result)
const hexResult = uint8View.reduce((hex, current) => hex + current.toString(16), '');
const message = CryptoJS.enc.Hex.parse(hexResult);
const pass = 'your-password-here';
const result = CryptoJS.AES.encrypt(message, pass, {
mode: CryptoJS.mode.ECB,
});
const e64 = CryptoJS.enc.Base64.parse(result.toString());
const hex = e64.toString(CryptoJS.enc.Hex);
const view = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
const d = hex.slice(i, i + 2);
view[i / 2] = parseInt(d, 16);
}
const file = new File([view], 'yourfile.ecb');
const link = document.createElement('a');
link.setAttribute('href', objectUrl);
link.innerText = 'Download';
link.download = 'yourfile.ecb';
document.body.append(link);
}
reader.readAsArrayBuffer(file);
// Read file
});
优化
上面的例子只是展示了从浏览器到加密再到浏览器的一个过程。处理方式很粗糙,如果实际使用,需要重新构思细节。比如加密方式更改,大文件切片加密等等。