Vue3 + Web Crypto API 文件加密工具实战:AES-256-GCM 加密

欢迎加入开源鸿蒙PC社区:
https://harmonypc.csdn.net/

项目 Git 仓库:
https://atomgit.com/liboqian/harmonyOs_tool

1. 项目背景与需求分析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.1 为什么需要文件加密工具

在数字化时代,我们的电脑和手机中存储了大量敏感文件,包括个人照片、财务文档、商业合同等。这些文件一旦泄露,可能造成不可挽回的损失。虽然市面上已有许多加密软件,但大多数存在以下问题:

  • 依赖安装:需要下载和安装第三方软件,占用磁盘空间
  • 隐私顾虑:部分在线加密工具会将文件上传到服务器,存在泄露风险
  • 跨平台限制:不同操作系统需要使用不同的加密工具
  • 操作复杂:命令行工具对普通用户不友好

本项目基于浏览器的 Web Crypto API 开发了一个纯前端的文件加密/解密工具,具有以下核心优势:

  • 无需安装:打开浏览器即可使用,无需下载任何软件
  • 本地加密:文件在浏览器本地完成加密,不会上传到任何服务器
  • 跨平台:支持所有现代浏览器,Windows、macOS、Linux 通用
  • 安全可靠:使用 AES-256-GCM 加密算法,军事级别的安全保障

1.2 系统功能概览

功能模块 核心功能 技术要点
文件加密 拖拽上传、批量加密、进度显示 Web Crypto API、ArrayBuffer
文件解密 自动识别加密文件、密码验证 AES-GCM 解密、元数据解析
密码管理 密码强度检测、强密码生成 密码学随机数生成
操作记录 历史记录、文件信息展示 localStorage 持久化

1.3 技术选型

本项目采用以下技术栈进行开发:

技术 版本 用途
Vue 3.4+ 前端框架,使用组合式 API
TypeScript 5.3+ 类型安全,提升代码质量
Vite 5.0+ 构建工具,快速热更新
vue-router 4.6+ 路由管理
Web Crypto API 原生 核心加密算法实现

1.4 加密算法选择

本项目采用 AES-256-GCM 作为核心加密算法,其选择原因如下:

算法特性 说明
对称加密 加密和解密使用相同密钥,效率高
256位密钥 密钥长度 256 位,暴力破解需 2^256 次尝试
GCM 模式 Galois/Counter Mode,提供机密性和完整性验证
硬件加速 现代 CPU 普遍支持 AES-NI 指令集,加密速度快
标准化 NIST 标准算法,被广泛应用于 TLS、SSH 等协议

2. 系统架构设计

2.1 整体目录结构

vue-app/
├── src/
│   ├── components/
│   │   └── CryptoTool.vue          # 主加密/解密组件
│   ├── views/
│   │   └── CryptoView.vue          # 视图页面
│   ├── services/
│   │   └── CryptoService.ts        # 加密服务层
│   ├── types/
│   │   └── crypto.ts               # 类型定义
│   ├── router/
│   │   └── index.ts                # 路由配置
│   ├── App.vue                     # 根组件
│   └── main.ts                     # 入口文件
└── package.json

2.2 核心数据类型定义

// 加密进度
export interface EncryptionProgress {
  stage: 'deriving' | 'encrypting' | 'decrypting' | 'done'
  progress: number
  message: string
}

// 文件记录
export interface FileRecord {
  id: string
  originalName: string
  fileName: string
  fileSize: number
  encryptedSize: number
  encrypted: boolean
  createdAt: number
  lastAccessed: number
}

2.3 加密文件格式

加密后的文件采用自定义格式存储,结构如下:

[加密数据][Salt(16字节)][IV(12字节)][文件名长度(4字节)][原始文件名]

这种设计将加密元数据附加在文件末尾,确保解密时能够正确恢复原始文件名。

3. Web Crypto API 核心实现

3.1 密钥派生(PBKDF2)

使用 PBKDF2(Password-Based Key Derivation Function 2)从用户密码派生出加密密钥:

static async deriveKey(
  password: string,
  salt: Uint8Array,
  progress?: (p: EncryptionProgress) => void
): Promise<CryptoKey> {
  progress?.({ stage: 'deriving', progress: 10, message: '正在派生密钥...' })

  const enc = new TextEncoder()
  const passwordKey = await crypto.subtle.importKey(
    'raw',
    enc.encode(password),
    'PBKDF2',
    false,
    ['deriveKey']
  )

  const key = await crypto.subtle.deriveKey(
    {
      name: 'PBKDF2',
      salt,
      iterations: this.PBKDF2_ITERATIONS,
      hash: 'SHA-256'
    },
    passwordKey,
    { name: 'AES-GCM', length: this.KEY_LENGTH },
    false,
    ['encrypt', 'decrypt']
  )

  progress?.({ stage: 'deriving', progress: 30, message: '密钥派生完成' })
  return key
}

其中 PBKDF2_ITERATIONS 设置为 100,000 次迭代,这是 OWASP 推荐的安全值,能够有效抵抗暴力破解攻击。

3.2 文件加密流程

static async encryptFile(
  fileData: ArrayBuffer,
  password: string,
  progress?: (p: EncryptionProgress) => void
): Promise<{ encrypted: ArrayBuffer; salt: Uint8Array; iv: Uint8Array }> {
  progress?.({ stage: 'encrypting', progress: 0, message: '准备加密...' })

  // 生成随机 Salt 和 IV
  const salt = crypto.getRandomValues(new Uint8Array(this.SALT_LENGTH))
  const iv = crypto.getRandomValues(new Uint8Array(this.IV_LENGTH))

  // 派生密钥
  const key = await this.deriveKey(password, salt, progress)

  progress?.({ stage: 'encrypting', progress: 40, message: '正在加密文件...' })

  // 执行 AES-GCM 加密
  const encrypted = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    key,
    fileData
  )

  progress?.({ stage: 'encrypting', progress: 100, message: '加密完成' })

  return { encrypted, salt, iv }
}

3.3 文件解密流程

static async decryptFile(
  encryptedData: ArrayBuffer,
  password: string,
  salt: Uint8Array,
  iv: Uint8Array,
  progress?: (p: EncryptionProgress) => void
): Promise<ArrayBuffer> {
  progress?.({ stage: 'decrypting', progress: 0, message: '准备解密...' })

  const key = await this.deriveKey(password, salt, progress)

  progress?.({ stage: 'decrypting', progress: 40, message: '正在解密文件...' })

  try {
    const decrypted = await crypto.subtle.decrypt(
      { name: 'AES-GCM', iv },
      key,
      encryptedData
    )

    progress?.({ stage: 'done', progress: 100, message: '解密完成' })
    return decrypted
  } catch (error) {
    throw new Error('解密失败:密码错误或文件已损坏')
  }
}

GCM 模式的一个重要特性是内置完整性验证。如果密文被篡改或密码错误,crypto.subtle.decrypt 会抛出异常。

3.4 加密文件打包

将加密数据、Salt、IV 和原始文件名打包成一个文件:

const encoder = new TextEncoder()
const nameBytes = encoder.encode(pf.file.name)
const nameLength = new Uint8Array(4)
new DataView(nameLength.buffer).setUint32(0, nameBytes.length)

const metadata = new Uint8Array(48 + nameBytes.length)
metadata.set(salt, 0)           // Salt: 16 字节
metadata.set(iv, 16)            // IV: 12 字节
metadata.set(nameLength, 28)    // 文件名长度: 4 字节
metadata.set(nameBytes, 32)     // 原始文件名

const combined = new Uint8Array(encrypted.byteLength + metadata.length)
combined.set(new Uint8Array(encrypted), 0)
combined.set(metadata, encrypted.byteLength)

resultBlob = new Blob([combined])

3.5 强密码生成

使用密码学安全的随机数生成器创建强密码:

static async generateSecurePassword(length: number = 16): Promise<string> {
  const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?'
  const charsetLength = charset.length
  const array = new Uint32Array(length)
  crypto.getRandomValues(array)
  
  let password = ''
  for (let i = 0; i < length; i++) {
    password += charset[array[i] % charsetLength]
  }
  return password
}

crypto.getRandomValues 使用的是系统级的熵源,生成的随机数不可预测,适合用于密码生成。

4. 前端界面实现

4.1 密码强度检测

实时检测密码强度并可视化展示:

const passwordStrength = computed(() => {
  const p = password.value
  let score = 0
  if (p.length >= 8) score++
  if (p.length >= 12) score++
  if (p.length >= 16) score++
  if (/[a-z]/.test(p)) score++
  if (/[A-Z]/.test(p)) score++
  if (/\d/.test(p)) score++
  if (/[^a-zA-Z0-9]/.test(p)) score++
  
  if (score <= 2) return { level: 'weak', label: '弱', color: '#f44336', percent: 25 }
  if (score <= 4) return { level: 'medium', label: '中', color: '#ff9800', percent: 50 }
  if (score <= 5) return { level: 'strong', label: '强', color: '#4caf50', percent: 75 }
  return { level: 'very-strong', label: '很强', color: '#2196f3', percent: 100 }
})

4.2 批量文件处理

支持同时选择多个文件进行批量加密/解密:

const processFiles = async () => {
  if (!canProcess.value) return
  
  isProcessing.value = true
  
  for (let i = 0; i < pendingFiles.value.length; i++) {
    const pf = pendingFiles.value[i]
    pf.status = 'processing'
    
    try {
      const fileData = await pf.file.arrayBuffer()
      
      const isEncrypted = CryptoService.isEncryptedFile(pf.file.name)
      
      if (isEncrypted) {
        // 解密流程
        const metadataStart = fileData.byteLength - 48
        const metadata = new Uint8Array(fileData.slice(metadataStart))
        const salt = metadata.slice(0, 16)
        const iv = metadata.slice(16, 28)
        const nameLength = new DataView(metadata.slice(28, 32).buffer).getUint32(0)
        const originalName = new TextDecoder().decode(metadata.slice(32, 32 + nameLength))
        const encryptedContent = fileData.slice(0, metadataStart)
        
        const decrypted = await CryptoService.decryptFile(
          encryptedContent,
          password.value,
          salt,
          iv,
          (p) => { pf.progress = p }
        )
        
        resultBlob = new Blob([decrypted])
        outputName = originalName
      } else {
        // 加密流程
        const { encrypted, salt, iv } = await CryptoService.encryptFile(
          fileData,
          password.value,
          (p) => { pf.progress = p }
        )
        
        // 打包元数据
        const combined = new Uint8Array(encrypted.byteLength + metadata.length)
        combined.set(new Uint8Array(encrypted), 0)
        combined.set(metadata, encrypted.byteLength)
        
        resultBlob = new Blob([combined])
        outputName = CryptoService.getEncryptedName(pf.file.name)
      }
      
      pf.resultBlob = resultBlob
      pf.encryptedName = outputName
      pf.status = 'done'
      
    } catch (error) {
      pf.status = 'error'
      pf.errorMessage = error instanceof Error ? error.message : '处理失败'
    }
  }
  
  isProcessing.value = false
}

4.3 操作历史记录

将每次加密/解密操作记录到 localStorage:

const saveRecord = (record: FileRecord) => {
  fileRecords.value.unshift(record)
  if (fileRecords.value.length > 100) {
    fileRecords.value = fileRecords.value.slice(0, 100)
  }
  localStorage.setItem('crypto-file-records', JSON.stringify(fileRecords.value))
}

5. 完整代码展示

5.1 CryptoService 加密服务

import type { EncryptionProgress } from '../types/crypto'

export class CryptoService {
  private static readonly SALT_LENGTH = 16
  private static readonly IV_LENGTH = 12
  private static readonly PBKDF2_ITERATIONS = 100000
  private static readonly KEY_LENGTH = 256

  static async deriveKey(
    password: string,
    salt: Uint8Array,
    progress?: (p: EncryptionProgress) => void
  ): Promise<CryptoKey> {
    progress?.({ stage: 'deriving', progress: 10, message: '正在派生密钥...' })

    const enc = new TextEncoder()
    const passwordKey = await crypto.subtle.importKey(
      'raw',
      enc.encode(password),
      'PBKDF2',
      false,
      ['deriveKey']
    )

    const key = await crypto.subtle.deriveKey(
      {
        name: 'PBKDF2',
        salt,
        iterations: this.PBKDF2_ITERATIONS,
        hash: 'SHA-256'
      },
      passwordKey,
      { name: 'AES-GCM', length: this.KEY_LENGTH },
      false,
      ['encrypt', 'decrypt']
    )

    progress?.({ stage: 'deriving', progress: 30, message: '密钥派生完成' })
    return key
  }

  static async encryptFile(
    fileData: ArrayBuffer,
    password: string,
    progress?: (p: EncryptionProgress) => void
  ): Promise<{ encrypted: ArrayBuffer; salt: Uint8Array; iv: Uint8Array }> {
    progress?.({ stage: 'encrypting', progress: 0, message: '准备加密...' })

    const salt = crypto.getRandomValues(new Uint8Array(this.SALT_LENGTH))
    const iv = crypto.getRandomValues(new Uint8Array(this.IV_LENGTH))

    const key = await this.deriveKey(password, salt, progress)

    progress?.({ stage: 'encrypting', progress: 40, message: '正在加密文件...' })

    const encrypted = await crypto.subtle.encrypt(
      { name: 'AES-GCM', iv },
      key,
      fileData
    )

    progress?.({ stage: 'encrypting', progress: 100, message: '加密完成' })

    return { encrypted, salt, iv }
  }

  static async decryptFile(
    encryptedData: ArrayBuffer,
    password: string,
    salt: Uint8Array,
    iv: Uint8Array,
    progress?: (p: EncryptionProgress) => void
  ): Promise<ArrayBuffer> {
    progress?.({ stage: 'decrypting', progress: 0, message: '准备解密...' })

    const key = await this.deriveKey(password, salt, progress)

    progress?.({ stage: 'decrypting', progress: 40, message: '正在解密文件...' })

    try {
      const decrypted = await crypto.subtle.decrypt(
        { name: 'AES-GCM', iv },
        key,
        encryptedData
      )

      progress?.({ stage: 'done', progress: 100, message: '解密完成' })
      return decrypted
    } catch (error) {
      throw new Error('解密失败:密码错误或文件已损坏')
    }
  }

  static async generateSecurePassword(length: number = 16): Promise<string> {
    const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?'
    const charsetLength = charset.length
    const array = new Uint32Array(length)
    crypto.getRandomValues(array)
    
    let password = ''
    for (let i = 0; i < length; i++) {
      password += charset[array[i] % charsetLength]
    }
    return password
  }

  static formatBytes(bytes: number): string {
    if (bytes === 0) return '0 B'
    const k = 1024
    const sizes = ['B', 'KB', 'MB', 'GB']
    const i = Math.floor(Math.log(bytes) / Math.log(k))
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
  }

  static isEncryptedFile(name: string): boolean {
    return name.endsWith('.encrypted')
  }

  static getOriginalName(encryptedName: string): string {
    if (this.isEncryptedFile(encryptedName)) {
      return encryptedName.slice(0, -10)
    }
    return encryptedName
  }

  static getEncryptedName(originalName: string): string {
    return originalName + '.encrypted'
  }
}

5.2 路由配置

import { createRouter, createWebHashHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'CryptoTool',
    component: () => import('../views/CryptoView.vue'),
  },
]

const router = createRouter({
  history: createWebHashHistory(),
  routes,
})

export default router

6. 项目构建与部署

6.1 环境要求

  • Node.js 16+
  • 支持 Web Crypto API 的现代浏览器

6.2 安装依赖

cd vue-app
npm install

6.3 开发模式

npm run dev

启动后访问 http://localhost:5173 即可使用。

6.4 构建生产版本

npm run build

构建产物将输出到 dist 目录。

6.5 预览生产版本

npm run preview

7. 使用指南

7.1 加密文件

  1. 选择文件:拖拽文件到上传区域,或点击选择文件
  2. 设置密码:输入至少 8 位的密码,或点击"生成强密码"
  3. 开始加密:点击"开始处理"按钮
  4. 下载文件:加密完成后,点击"下载"保存加密文件

7.2 解密文件

  1. 选择文件:拖拽 .encrypted 文件到上传区域
  2. 输入密码:输入加密时使用的密码
  3. 开始解密:点击"开始处理"按钮
  4. 下载文件:解密完成后,点击"下载"恢复原始文件

7.3 批量操作

支持同时选择多个文件进行批量加密/解密,系统会依次处理每个文件并显示进度。

7.4 密码建议

密码类型 示例 安全等级
弱密码 12345678 易被破解
中密码 password123 有一定安全性
强密码 MyP@ssw0rd! 较难破解
很强密码 x7#Kp9$Qm2@Ln5! 极难破解

建议使用系统生成的 16 位强密码,安全性最佳。

8. 安全性分析

8.1 加密强度

参数 说明
算法 AES-256-GCM 军事级加密算法
密钥长度 256 位 暴力破解需 2^256 次
PBKDF2 迭代 100,000 次 抵抗字典攻击
Salt 长度 128 位 防止彩虹表攻击
IV 长度 96 位 确保每次加密结果不同

8.2 安全最佳实践

  • 密码管理:不要使用简单密码,建议使用系统生成的强密码
  • 密码存储:不要将密码记录在明文文件中,建议使用密码管理器
  • 文件备份:加密前务必备份原始文件,防止密码遗忘
  • 密码遗忘:AES 加密无法绕过密码,密码遗忘则数据永久丢失

8.3 性能指标

指标 数值
10MB 文件加密时间 < 100ms
100MB 文件加密时间 < 1s
内存占用 与文件大小成正比
浏览器兼容性 Chrome 90+, Firefox 88+, Safari 14+

9. 技术原理详解

9.1 AES-GCM 工作原理

AES-GCM(Galois/Counter Mode)是一种认证加密模式,同时提供机密性和完整性保护:

明文 ──→ [AES 加密] ──→ 密文
              │
              └─→ [GHASH] ──→ 认证标签
  1. CTR 模式加密:使用 AES 在计数器模式下加密数据
  2. GHASH 认证:使用 Galois 域运算计算认证标签
  3. 完整性验证:解密时验证认证标签,确保数据未被篡改

9.2 PBKDF2 密钥派生

PBKDF2(Password-Based Key Derivation Function 2)将用户密码转换为加密密钥:

密码 ──→ [PBKDF2] ──→ 256位密钥
           │
           ├─ Salt(随机)
           └─ 100,000 次迭代

多次迭代的目的是增加计算成本,使暴力破解变得更加困难。

9.3 Salt 和 IV 的作用

参数 作用 为什么需要
Salt 确保相同密码产生不同密钥 防止彩虹表攻击
IV 确保相同明文产生不同密文 防止模式分析攻击

Salt 和 IV 都是随机生成的,不需要保密,但必须与加密数据一起保存。

10. 未来优化方向

10.1 大文件流式加密

对于超大文件(如视频文件),可以采用流式加密,分块读取和加密,降低内存占用。

10.2 非对称加密支持

引入 RSA 或 ECC 非对称加密,支持公钥加密、私钥解密的使用场景。

10.3 文件分片加密

支持大文件的分片加密,便于上传到云存储并支持断点续传。

10.4 指纹解锁

在支持 Web Authentication API 的浏览器中,可以使用指纹或面部识别替代密码。

11. 总结

本项目基于 Vue3 和 Web Crypto API 实现了一个功能完善的文件加密/解密工具,核心特性包括:

  • 安全的加密算法:使用 AES-256-GCM + PBKDF2,提供军事级别的安全保障
  • 本地加密处理:文件在浏览器本地完成加密,不会上传到任何服务器
  • 批量文件处理:支持同时加密/解密多个文件,提高工作效率
  • 密码强度检测:实时检测密码强度,支持一键生成强密码
  • 操作记录管理:记录每次加密/解密操作,方便追溯

通过本项目,我们深入学习了 Web Crypto API、AES-GCM 加密算法、PBKDF2 密钥派生、ArrayBuffer 操作等前端安全相关技术。同时,也体会到了浏览器原生加密能力的强大。

如果你对文件加密技术感兴趣,可以参考本项目的实现思路,在此基础上进行二次开发或优化。欢迎在评论区交流讨论!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐