如何在浏览器中加密文件,听着像是一个伪需求,但细心想想,并不是没有地方用得到。尽管浏览器中的代码是直接暴露给用户的,但是只要处理得当,存储的安全性自然是更上一层。

使用场景

  • 在线应用

    • 如果你只需要保存一个网址就可以随时随地加密你想加密的文件,那么何乐而不为呢?
  • 增加后台文件存储强度

    • 当用户上传文件之后,后台拿到文件可以进行加密,将密钥和密文分离存储,那么即使不小心泄露了其中一份数据,也无关紧要。而后端加密的这部分工作,可以转移到浏览器来做,这对后端的压力可以降低不少。

准备

需要一个浏览器中加密的库,我选用的是 crypto-js

实现

首先,我们可以在文件选择空间的 change 事件中拿到 File 对象。

1
2
3
4
5
6
const filePicker = document.body.querySelector('your-file-picker');
filePicker.addEventListener('change', event => {
  const file = event.target.files[0];

  // Read file
}); 

File 只是一个浏览器与真正文件交互的接口,接下来我们需要利用 FileReader 来读取文件的内容到内存中。

FileReader 有四个读取文件的接口,分别是

1
2
3
4
FileReader.readAsArrayBuffer()        // 读取二进制数据到 ArrayBuffer
FileReader.readAsBinaryString()       // 不推荐
FileReader.readAsDataURL()            // data:URL格式的字符串(base64编码)
FileReader.readAsText()               // 按文本方式读取文件

加密文件是根据二进制数据来的,这里我们使用 readAsArrayBufferArrayBuffer 不能直接被操作,我们需要将它转成类型数组对象使用。

1
2
3
4
5
6
7
8
9
// 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 进行加密。

1
2
3
4
5
6
7
8
9
// 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 对象下载的支持,至此,整个加密过程结束。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 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);

最终,处理的代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
}); 

优化

上面的例子只是展示了从浏览器到加密再到浏览器的一个过程。处理方式很粗糙,如果实际使用,需要重新构思细节。比如加密方式更改,大文件切片加密等等。