Vue 3 与 Web Crypto API

概述

Web Crypto API 是一个提供强大加密功能的 Web 标准 API,允许 Web 应用程序执行各种加密操作,如生成密钥、加密解密数据、数字签名和验证等。在 Vue 3 应用中集成 Web Crypto API 可以保护敏感数据,实现安全的通信和存储,增强应用的安全性。

核心知识

1. Web Crypto API 基本概念

  • 作用:提供加密、解密、数字签名、密钥生成等安全功能
  • 支持的算法
    • 对称加密:AES-CBC、AES-GCM、AES-CTR 等
    • 非对称加密:RSA-OAEP、ECDSA、Ed25519 等
    • 哈希函数:SHA-1、SHA-256、SHA-384、SHA-512 等
    • 密钥派生:PBKDF2、HKDF 等
    • 随机数生成:Crypto.getRandomValues()
  • 安全上下文:只能在 HTTPS 环境中使用

2. Web Crypto API 核心接口

  • Crypto:提供基本的加密功能,如随机数生成
  • CryptoKey:表示加密密钥
  • CryptoKeyPair:表示非对称加密的密钥对(公钥和私钥)

3. Web Crypto API 工作流程

3.1 对称加密流程

  1. 生成对称密钥
  2. 准备要加密的数据
  3. 使用密钥加密数据
  4. 存储或传输加密数据
  5. 使用相同密钥解密数据

3.2 非对称加密流程

  1. 生成密钥对(公钥和私钥)
  2. 使用公钥加密数据
  3. 存储或传输加密数据
  4. 使用私钥解密数据

3.3 数字签名流程

  1. 生成密钥对
  2. 准备要签名的数据
  3. 使用私钥对数据进行签名
  4. 存储或传输数据和签名
  5. 使用公钥验证签名

4. 前端实现(Vue 3)

4.1 随机数生成

<template>
  <div>
    <h2>生成随机数</h2>
    <button @click="generateRandomNumber">生成随机数</button>
    <div v-if="randomNumber">随机数:{{ randomNumber }}</div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const randomNumber = ref(null);

const generateRandomNumber = () => {
  const array = new Uint32Array(1);
  crypto.getRandomValues(array);
  randomNumber.value = array[0];
};
</script>

4.2 哈希函数(SHA-256)

<template>
  <div>
    <h2>计算哈希值</h2>
    <input v-model="inputText" placeholder="输入要哈希的文本" />
    <button @click="calculateHash" :disabled="!inputText">计算 SHA-256 哈希</button>
    <div v-if="hashResult">哈希值:{{ hashResult }}</div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const inputText = ref('');
const hashResult = ref('');

const calculateHash = async () => {
  try {
    // 将文本转换为 ArrayBuffer
    const encoder = new TextEncoder();
    const data = encoder.encode(inputText.value);
    
    // 计算哈希值
    const hashBuffer = await crypto.subtle.digest('SHA-256', data);
    
    // 将 ArrayBuffer 转换为十六进制字符串
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    
    hashResult.value = hashHex;
  } catch (error) {
    console.error('计算哈希失败:', error);
  }
};
</script>

4.3 对称加密(AES-GCM)

<template>
  <div>
    <h2>对称加密(AES-GCM)</h2>
    <input v-model="plaintext" placeholder="要加密的文本" />
    <button @click="encryptData" :disabled="!plaintext">加密</button>
    <button @click="decryptData" :disabled="!encryptedData">解密</button>
    
    <div v-if="encryptedData">
      <h3>加密后:</h3>
      <p>密文:{{ encryptedData.ciphertext }}</p>
      <p>IV:{{ encryptedData.iv }}</p>
      <p>Tag:{{ encryptedData.tag }}</p>
    </div>
    
    <div v-if="decryptedText">
      <h3>解密后:</h3>
      <p>{{ decryptedText }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const plaintext = ref('');
const encryptedData = ref(null);
const decryptedText = ref('');
let encryptionKey = null;

// 生成对称密钥
const generateKey = async () => {
  return await crypto.subtle.generateKey(
    {
      name: 'AES-GCM',
      length: 256
    },
    true, // 是否可提取
    ['encrypt', 'decrypt'] // 密钥用途
  );
};

// 加密数据
const encryptData = async () => {
  try {
    if (!encryptionKey) {
      encryptionKey = await generateKey();
    }
    
    // 创建随机 IV(初始化向量)
    const iv = crypto.getRandomValues(new Uint8Array(12));
    
    // 将文本转换为 ArrayBuffer
    const encoder = new TextEncoder();
    const data = encoder.encode(plaintext.value);
    
    // 加密数据
    const encrypted = await crypto.subtle.encrypt(
      {
        name: 'AES-GCM',
        iv: iv,
        tagLength: 128 // 标签长度(bits)
      },
      encryptionKey,
      data
    );
    
    // 提取认证标签(GCM 模式会生成标签)
    const encryptedArray = new Uint8Array(encrypted);
    const tag = encryptedArray.slice(-16); // 最后 16 字节是标签
    const ciphertext = encryptedArray.slice(0, -16); // 前面的是密文
    
    // 转换为十六进制字符串以便显示和传输
    encryptedData.value = {
      ciphertext: arrayBufferToHex(ciphertext),
      iv: arrayBufferToHex(iv),
      tag: arrayBufferToHex(tag)
    };
    
    decryptedText.value = null;
  } catch (error) {
    console.error('加密失败:', error);
  }
};

// 解密数据
const decryptData = async () => {
  try {
    if (!encryptionKey || !encryptedData.value) return;
    
    // 将十六进制字符串转换为 ArrayBuffer
    const ciphertext = hexToArrayBuffer(encryptedData.value.ciphertext);
    const iv = hexToArrayBuffer(encryptedData.value.iv);
    const tag = hexToArrayBuffer(encryptedData.value.tag);
    
    // 合并密文和标签
    const combined = new Uint8Array(ciphertext.byteLength + tag.byteLength);
    combined.set(new Uint8Array(ciphertext), 0);
    combined.set(new Uint8Array(tag), ciphertext.byteLength);
    
    // 解密数据
    const decrypted = await crypto.subtle.decrypt(
      {
        name: 'AES-GCM',
        iv: iv,
        tagLength: 128
      },
      encryptionKey,
      combined
    );
    
    // 将 ArrayBuffer 转换为文本
    const decoder = new TextDecoder();
    decryptedText.value = decoder.decode(decrypted);
  } catch (error) {
    console.error('解密失败:', error);
  }
};

// 辅助函数:ArrayBuffer 转十六进制字符串
const arrayBufferToHex = (buffer) => {
  return Array.from(new Uint8Array(buffer))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
};

// 辅助函数:十六进制字符串转 ArrayBuffer
const hexToArrayBuffer = (hex) => {
  const bytes = new Uint8Array(hex.length / 2);
  for (let i = 0; i < hex.length; i += 2) {
    bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
  }
  return bytes.buffer;
};
</script>

4.3 非对称加密(RSA-OAEP)

<template>
  <div>
    <h2>非对称加密(RSA-OAEP)</h2>
    <input v-model="plaintext" placeholder="输入要加密的文本" />
    <button @click="generateKeyPair" :disabled="keyPair">生成密钥对</button>
    <button @click="encryptWithPublicKey" :disabled="!plaintext || !keyPair">使用公钥加密</button>
    <button @click="decryptWithPrivateKey" :disabled="!encryptedText || !keyPair">使用私钥解密</button>
    
    <div v-if="keyPair">
      <h3>密钥对已生成</h3>
    </div>
    
    <div v-if="encryptedText">
      <h3>加密后:</h3>
      <p>{{ encryptedText }}</p>
    </div>
    
    <div v-if="decryptedText">
      <h3>解密后:</h3>
      <p>{{ decryptedText }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const plaintext = ref('');
const encryptedText = ref('');
const decryptedText = ref('');
const keyPair = ref(null);

// 生成 RSA 密钥对
const generateKeyPair = async () => {
  try {
    keyPair.value = await crypto.subtle.generateKey(
      {
        name: 'RSA-OAEP',
        modulusLength: 2048,
        publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
        hash: 'SHA-256'
      },
      true,
      ['encrypt', 'decrypt']
    );
  } catch (error) {
    console.error('生成密钥对失败:', error);
  }
};

// 使用公钥加密
const encryptWithPublicKey = async () => {
  try {
    const encoder = new TextEncoder();
    const data = encoder.encode(plaintext.value);
    
    const encrypted = await crypto.subtle.encrypt(
      {
        name: 'RSA-OAEP'
      },
      keyPair.value.publicKey,
      data
    );
    
    encryptedText.value = arrayBufferToBase64(encrypted);
    decryptedText.value = null;
  } catch (error) {
    console.error('加密失败:', error);
  }
};

// 使用私钥解密
const decryptWithPrivateKey = async () => {
  try {
    const encrypted = base64ToArrayBuffer(encryptedText.value);
    
    const decrypted = await crypto.subtle.decrypt(
      {
        name: 'RSA-OAEP'
      },
      keyPair.value.privateKey,
      encrypted
    );
    
    const decoder = new TextDecoder();
    decryptedText.value = decoder.decode(decrypted);
  } catch (error) {
    console.error('解密失败:', error);
  }
};

// 辅助函数:ArrayBuffer 转 Base64
const arrayBufferToBase64 = (buffer) => {
  const bytes = new Uint8Array(buffer);
  let binary = '';
  for (let i = 0; i < bytes.byteLength; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  return window.btoa(binary);
};

// 辅助函数:Base64 转 ArrayBuffer
const base64ToArrayBuffer = (base64) => {
  const binary = window.atob(base64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return bytes.buffer;
};
</script>

5. 创建可复用的 Web Crypto Composable

// composables/useCrypto.js
import { ref } from 'vue';

export function useCrypto() {
  // 辅助函数:ArrayBuffer 转十六进制字符串
  const arrayBufferToHex = (buffer) => {
    return Array.from(new Uint8Array(buffer))
      .map(b => b.toString(16).padStart(2, '0'))
      .join('');
  };

  // 辅助函数:十六进制字符串转 ArrayBuffer
  const hexToArrayBuffer = (hex) => {
    const bytes = new Uint8Array(hex.length / 2);
    for (let i = 0; i < hex.length; i += 2) {
      bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
    }
    return bytes.buffer;
  };

  // 辅助函数:ArrayBuffer 转 Base64
  const arrayBufferToBase64 = (buffer) => {
    const bytes = new Uint8Array(buffer);
    let binary = '';
    for (let i = 0; i < bytes.byteLength; i++) {
      binary += String.fromCharCode(bytes[i]);
    }
    return window.btoa(binary);
  };

  // 辅助函数:Base64 转 ArrayBuffer
  const base64ToArrayBuffer = (base64) => {
    const binary = window.atob(base64);
    const bytes = new Uint8Array(binary.length);
    for (let i = 0; i < binary.length; i++) {
      bytes[i] = binary.charCodeAt(i);
    }
    return bytes.buffer;
  };

  // 生成随机数
  const generateRandomNumber = (bits = 32) => {
    const bytes = Math.ceil(bits / 8);
    const array = new Uint8Array(bytes);
    crypto.getRandomValues(array);
    return parseInt(arrayBufferToHex(array), 16);
  };

  // 生成随机字符串
  const generateRandomString = (length = 16) => {
    const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    let result = '';
    const array = new Uint32Array(length);
    crypto.getRandomValues(array);
    
    for (let i = 0; i < length; i++) {
      result += charset[array[i] % charset.length];
    }
    
    return result;
  };

  // 计算哈希值
  const computeHash = async (data, algorithm = 'SHA-256') => {
    const encoder = new TextEncoder();
    const buffer = typeof data === 'string' ? encoder.encode(data) : data;
    const hash = await crypto.subtle.digest(algorithm, buffer);
    return arrayBufferToHex(hash);
  };

  // 对称加密(AES-GCM)
  const encryptAESGCM = async (data, key, iv = null) => {
    if (!iv) {
      iv = crypto.getRandomValues(new Uint8Array(12));
    }
    
    const encoder = new TextEncoder();
    const buffer = typeof data === 'string' ? encoder.encode(data) : data;
    
    const encrypted = await crypto.subtle.encrypt(
      {
        name: 'AES-GCM',
        iv: iv,
        tagLength: 128
      },
      key,
      buffer
    );
    
    const encryptedArray = new Uint8Array(encrypted);
    const tag = encryptedArray.slice(-16);
    const ciphertext = encryptedArray.slice(0, -16);
    
    return {
      ciphertext: arrayBufferToBase64(ciphertext),
      iv: arrayBufferToBase64(iv),
      tag: arrayBufferToBase64(tag)
    };
  };

  // 对称解密(AES-GCM)
  const decryptAESGCM = async (encryptedData, key) => {
    const ciphertext = base64ToArrayBuffer(encryptedData.ciphertext);
    const iv = base64ToArrayBuffer(encryptedData.iv);
    const tag = base64ToArrayBuffer(encryptedData.tag);
    
    const combined = new Uint8Array(ciphertext.byteLength + tag.byteLength);
    combined.set(new Uint8Array(ciphertext), 0);
    combined.set(new Uint8Array(tag), ciphertext.byteLength);
    
    const decrypted = await crypto.subtle.decrypt(
      {
        name: 'AES-GCM',
        iv: iv,
        tagLength: 128
      },
      key,
      combined
    );
    
    const decoder = new TextDecoder();
    return decoder.decode(decrypted);
  };

  // 生成 AES 密钥
  const generateAESKey = async (length = 256) => {
    return await crypto.subtle.generateKey(
      {
        name: 'AES-GCM',
        length: length
      },
      true,
      ['encrypt', 'decrypt']
    );
  };

  // 生成 RSA 密钥对
  const generateRSAKeyPair = async (modulusLength = 2048) => {
    return await crypto.subtle.generateKey(
      {
        name: 'RSA-OAEP',
        modulusLength: modulusLength,
        publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
        hash: 'SHA-256'
      },
      true,
      ['encrypt', 'decrypt']
    );
  };

  // 导出密钥(用于存储或传输)
  const exportKey = async (key, format = 'jwk') => {
    return await crypto.subtle.exportKey(format, key);
  };

  // 导入密钥
  const importKey = async (keyData, algorithm, usages, format = 'jwk') => {
    return await crypto.subtle.importKey(
      format,
      keyData,
      algorithm,
      true,
      usages
    );
  };

  return {
    generateRandomNumber,
    generateRandomString,
    computeHash,
    encryptAESGCM,
    decryptAESGCM,
    generateAESKey,
    generateRSAKeyPair,
    exportKey,
    importKey,
    arrayBufferToHex,
    hexToArrayBuffer,
    arrayBufferToBase64,
    base64ToArrayBuffer
  };
}

最佳实践

1. 安全考虑

  • 始终使用 HTTPS:Web Crypto API 只能在安全上下文(HTTPS)中使用
  • 保护密钥:不要在客户端代码中硬编码密钥
  • 使用强算法:优先使用 AES-GCM、RSA-OAEP 等现代算法,避免使用过时的 DES、RC4 等
  • 安全存储密钥:考虑使用 Web Crypto API 的密钥存储功能,或加密后存储在 IndexedDB 中
  • 使用适当的密钥长度:AES 至少使用 128 位,RSA 至少使用 2048 位

2. 性能优化

  • 重用密钥:避免频繁生成新密钥,特别是非对称密钥
  • 合理使用加密:只加密敏感数据,不要加密所有数据
  • 考虑异步操作:Web Crypto API 的大部分方法都是异步的,使用 async/await 处理
  • 优化数据转换:减少 ArrayBuffer、Base64、十六进制之间的转换次数

3. 错误处理

  • 捕获和处理所有错误:加密操作可能会抛出各种错误,如不支持的算法、无效的密钥等
  • 提供友好的错误信息:向用户显示清晰的错误信息,而不是原始错误
  • 记录错误:在开发环境中记录详细的错误信息,便于调试

4. 跨浏览器兼容性

  • 检查浏览器支持:在使用前检查 Web Crypto API 是否可用
  • 使用标准算法:优先使用广泛支持的算法
  • 测试多种浏览器:在不同浏览器上测试加密功能

常见问题与解决方案

1. 加密数据过大导致失败

  • 原因:RSA 加密有最大数据大小限制(取决于密钥长度)
  • 解决方案
    • 对于大文件,使用对称加密(AES)
    • 或使用混合加密:生成随机对称密钥,用对称密钥加密数据,用 RSA 加密对称密钥

2. 密钥无法导出

  • 原因:生成密钥时 extractable 参数设置为 false
  • 解决方案:生成密钥时将 extractable 参数设置为 true

3. 解密失败,报错 "OperationError"

  • 原因
    • 密钥不匹配
    • IV(初始化向量)不正确
    • 认证标签(GCM 模式)不匹配
    • 密文被篡改
  • 解决方案
    • 检查密钥是否正确
    • 确保 IV 和认证标签与加密时使用的相同
    • 验证密文完整性

4. 浏览器不支持某些算法

  • 原因:不同浏览器对 Web Crypto API 算法的支持程度不同
  • 解决方案
    • 检查浏览器支持的算法
    • 提供备选算法
    • 使用特性检测

5. 密钥存储安全问题

  • 原因:客户端存储密钥存在安全风险
  • 解决方案
    • 考虑使用 Web Crypto API 的密钥存储功能
    • 加密后存储在 IndexedDB 中
    • 对于敏感数据,考虑在服务器端加密

高级学习资源

1. 官方文档

2. 深度教程

3. 相关库和工具

  • CryptoJS:JavaScript 加密库
  • forge:JavaScript 加密库,支持多种算法
  • jsrsasign:JavaScript RSA 和 ECDSA 库

4. 视频教程

实践练习

1. 基础练习:实现简单的加密功能

  • 创建 Vue 3 应用,实现随机数生成
  • 实现哈希函数计算
  • 测试不同的哈希算法(SHA-1、SHA-256、SHA-512)

2. 进阶练习:对称加密应用

  • 实现 AES-GCM 对称加密和解密
  • 创建密钥生成和导出功能
  • 实现加密数据的存储和读取

3. 高级练习:非对称加密应用

  • 实现 RSA-OAEP 非对称加密和解密
  • 实现数字签名和验证
  • 测试密钥对生成和管理

4. 综合练习:密码管理器

  • 创建简单的密码管理器应用
  • 使用 Web Crypto API 加密存储密码
  • 实现密码生成、加密、解密功能
  • 添加用户认证

5. 挑战练习:安全聊天应用

  • 创建基于 WebSocket 的聊天应用
  • 使用 Web Crypto API 加密消息
  • 实现端到端加密
  • 支持密钥协商和管理

总结

Web Crypto API 为 Vue 3 应用提供了强大的加密功能,可以显著增强应用的安全性。通过合理使用 Web Crypto API 的各种功能,如对称加密、非对称加密、数字签名等,可以保护用户的敏感数据,实现安全的通信和存储。掌握 Web Crypto API 的实现原理和最佳实践,对于构建现代化、安全的 Vue 3 应用至关重要。

« 上一篇 Vue 3与Web Authentication API - 实现无密码安全认证的核心技术 下一篇 » Vue 3与Web Speech API - 实现语音交互功能的核心技术