跳转到内容
HexaTransfer
返回博客
技术深度解析

JavaScript文件加密:分步教程

用JavaScript在浏览器中加密文件。实用教程涵盖AES加密、密钥派生和安全的文件处理。

《个人信息保护法》(PIPL)第五十一条要求数据处理者采用"加密"等技术措施保护个人信息。浏览器端加密是最彻底的实现方式——文件在设备上加密完成后才传输,服务器仅存储密文,从源头满足法律要求。JavaScript 可通过 Web Crypto API 完全在浏览器内加密文件,无需服务器介入。

标准流程:将文件读取为 ArrayBuffer,通过 PBKDF2-SHA256(600,000 次迭代)从密码派生 256 位 AES-GCM 密钥,使用随机 12 字节 IV 加密,将盐值、IV 和密文打包为可下载的 Blob。每个主流浏览器都通过 window.crypto.subtle 原生支持这一流程,2–3 GB 以内的文件在现代笔记本上 10 秒内即可完成,无需任何第三方库。

算法选择的关键决策

选 AES-GCM,不选 AES-CBC。GCM 一次通过提供认证加密,用 128 位标签检测篡改,而 CBC 需要单独的 HMAC 步骤,大多数教程对此处理有误。选 256 位密钥——与 128 位相比性能差距可忽略,有 AES-NI 硬件加速时尤其如此。选 PBKDF2-SHA256 进行基于密码的密钥派生,除非可以通过 WebAssembly 提供更强的 Argon2id(但会增加约 50 KB 下载体积)。

避免:ECB 模式(根本上已破损)、手工实现的填充(十年的 CBC 填充预言机攻击史)、MD5 或 SHA-1(碰撞已破解)以及默认用 CBC 配合 PKCS7 的 crypto-js 包。

派生密钥

绝不将原始密码传入 encrypt。先派生密钥:

async function deriveKey(password, salt) {
  const enc = new TextEncoder();
  const material = await crypto.subtle.importKey(
    'raw', enc.encode(password), { name: 'PBKDF2' }, false, ['deriveKey']
  );
  return crypto.subtle.deriveKey(
    { name: 'PBKDF2', salt, iterations: 600000, hash: 'SHA-256' },
    material,
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt', 'decrypt']
  );
}

crypto.getRandomValues(new Uint8Array(16)) 为每个文件生成全新的 16 字节盐值,与密文一起存储。跨文件复用盐值会使 PBKDF2 失去意义。OWASP 2026 年 12 月密码存储备忘单目前推荐 PBKDF2-SHA256 使用 600,000 次迭代,在中端手机上约需 500 ms 的密钥派生时间。

加密文件

有了密钥,加密是对每个缓冲区的一次 subtle.encrypt 调用:

async function encryptBuffer(key, plaintext) {
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const ciphertext = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv, tagLength: 128 },
    key,
    plaintext
  );
  return { iv, ciphertext };
}

GCM 在复用 (iv, key) 对时会灾难性失败——密钥流碰撞会同时泄露两段明文。96 位随机 IV 对单个密钥提供约 2^48 次安全加密,足以用于文件加密。若用同一密钥加密多个块,从计数器加随机 32 位前缀派生 IV。

打包输出

解密方需要盐值、IV 和密文。将它们打包为带小型头部的单个 Blob:

function pack(salt, iv, ciphertext) {
  const magic = new TextEncoder().encode('ENC1');
  return new Blob([magic, salt, iv, new Uint8Array(ciphertext)]);
}

对头部进行版本化(ENC1ENC2……),以便以后迁移算法而不破坏旧文件。通过创建临时 URL 触发下载:

const url = URL.createObjectURL(packed);
const a = document.createElement('a');
a.href = url; a.download = `${file.name}.enc`; a.click();
URL.revokeObjectURL(url);

解密文件

解密反转流程,密码错误或文件被篡改时抛出 OperationError

async function decryptFile(blob, password) {
  const buf = await blob.arrayBuffer();
  const view = new Uint8Array(buf);
  const magic = new TextDecoder().decode(view.slice(0, 4));
  if (magic !== 'ENC1') throw new Error('Unknown format');
  const salt = view.slice(4, 20);
  const iv = view.slice(20, 32);
  const ct = view.slice(32);
  const key = await deriveKey(password, salt);
  const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ct);
  return new Blob([pt]);
}

显示单一错误信息——"无法解密,密码错误或文件已损坏"——而非区分标签验证失败和结构失败。这消除了向攻击者泄露信息的类填充预言机风险。

不耗尽内存地加密大文件

超过 500 MB 的文件,切勿对整个文件调用 arrayBuffer()。使用唯一 IV 独立加密块:

function ivForChunk(baseIV, index) {
  const iv = new Uint8Array(baseIV);
  const view = new DataView(iv.buffer);
  view.setUint32(8, index, false);
  return iv;
}

将文件通过 TransformStream 管道传输,加密每个 4 MB 块,并通过 File System Access API 将结果写入指向磁盘的 WritableStream。即使对 20 GB 文件,峰值内存也控制在约 10 MB。分块格式需要在头部记录块大小和块数量,以便解密方正确重组。

生产环境中出现的常见错误

真实代码审查中反复出现三种模式:

第一,将原始密钥存储在 localStorage 中图方便。localStorage 是同步的、同源作用域的,任何 XSS 都可读取。改用 IndexedDB 中的不可提取 CryptoKey

第二,使用 Math.random() 生成 IV 或盐值。Math.random() 是可预测的;始终使用 crypto.getRandomValues

第三,假设 subtle.encrypt 是恒定时间的。在原生浏览器实现中它是,但任何围绕它的 JavaScript 包装几乎肯定不是。让自己的代码远离热路径。

HexaTransfer 应用这套完整流水线——PBKDF2 600k 次迭代,AES-256-GCM,流式分块——服务器只存储不透明密文。立即体验:https://hexatransfer.com——免费,无需注册,最大支持 10 GB。

测试实现

编写测试套件对随机 10 MB Blob 进行往返测试,改变一个字节,断言解密抛出异常。针对格式错误的头部和截断密文进行模糊测试——常见 Bug 是 slice() 缺少边界检查,导致崩溃而非拒绝。在 iPhone SE、中端 Android 和 Chromebook 上进行基准测试;任何密钥派生超过 2 秒的情况对移动用户来说都太慢。上线前进行第二人审核——加密代码看起来简单,却以测试难以发现的微妙方式失效。

通过端到端加密安全发送大文件

通过端到端加密免费传输最大10GB的文件。无需注册账户。文件在上传前在浏览器中加密,其他人无法读取。

发送文件