|
|
@@ -0,0 +1,380 @@
|
|
|
+import CryptoJS from 'crypto-js'
|
|
|
+
|
|
|
+/**
|
|
|
+ * 增强版AES CFB加密解密工具
|
|
|
+ * 采用CFB模式,无需填充,解决CBC模式填充问题
|
|
|
+ */
|
|
|
+
|
|
|
+// 配置接口
|
|
|
+export interface AESConfig {
|
|
|
+ key: string
|
|
|
+ iv: string
|
|
|
+ mode: typeof CryptoJS.mode.CFB
|
|
|
+ padding: typeof CryptoJS.pad.NoPadding
|
|
|
+}
|
|
|
+
|
|
|
+// 加密结果接口
|
|
|
+export interface EncryptResult {
|
|
|
+ encryptedData: string
|
|
|
+ iv: string
|
|
|
+ timestamp: number
|
|
|
+ version: string
|
|
|
+}
|
|
|
+
|
|
|
+// 后端使用的十六进制密钥和IV(与后端完全匹配)
|
|
|
+const BACKEND_AES_KEY = "e36fa8396463df288532c5f1adc4c9fb960ec8decff0d2856cd94ad340638e6f"
|
|
|
+const BACKEND_AES_IV = "1c16606009b8e3630ddbaeb06c831099"
|
|
|
+
|
|
|
+// 默认配置
|
|
|
+const DEFAULT_CONFIG: AESConfig = {
|
|
|
+ key: BACKEND_AES_KEY,
|
|
|
+ iv: BACKEND_AES_IV,
|
|
|
+ mode: CryptoJS.mode.CFB,
|
|
|
+ padding: CryptoJS.pad.NoPadding
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 密钥和IV处理函数
|
|
|
+ * 后端使用十六进制格式,需要将十六进制字符串转换为字节数组
|
|
|
+ */
|
|
|
+function processKeyAndIV(config: AESConfig): { key: CryptoJS.WordArray; iv: CryptoJS.WordArray } {
|
|
|
+ // 检查是否为十六进制字符串(后端格式)
|
|
|
+ const isHexKey = /^[0-9a-fA-F]+$/.test(config.key)
|
|
|
+ const isHexIV = /^[0-9a-fA-F]+$/.test(config.iv)
|
|
|
+
|
|
|
+ if (isHexKey && isHexIV) {
|
|
|
+ // 后端格式:十六进制字符串,直接解析为字节数组
|
|
|
+ return {
|
|
|
+ key: CryptoJS.enc.Hex.parse(config.key),
|
|
|
+ iv: CryptoJS.enc.Hex.parse(config.iv)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 前端格式:字符串,使用UTF-8编码
|
|
|
+ // 密钥处理:确保32字节长度
|
|
|
+ const processedKey = config.key.padEnd(32, '0').substring(0, 32)
|
|
|
+
|
|
|
+ // IV处理:确保16字节长度
|
|
|
+ const processedIV = config.iv.padEnd(16, '0').substring(0, 16)
|
|
|
+
|
|
|
+ return {
|
|
|
+ key: CryptoJS.enc.Utf8.parse(processedKey),
|
|
|
+ iv: CryptoJS.enc.Utf8.parse(processedIV)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 验证配置有效性
|
|
|
+ */
|
|
|
+function validateConfig(config: AESConfig): void {
|
|
|
+ if (!config.key || config.key.trim().length === 0) {
|
|
|
+ throw new Error('AES密钥不能为空')
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!config.iv || config.iv.trim().length === 0) {
|
|
|
+ throw new Error('AES IV不能为空')
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否为十六进制格式
|
|
|
+ const isHexKey = /^[0-9a-fA-F]+$/.test(config.key)
|
|
|
+ const isHexIV = /^[0-9a-fA-F]+$/.test(config.iv)
|
|
|
+
|
|
|
+ if (isHexKey && isHexIV) {
|
|
|
+ // 十六进制格式:密钥应为64字符(32字节),IV应为32字符(16字节)
|
|
|
+ if (config.key.length !== 64) {
|
|
|
+ throw new Error('十六进制AES密钥长度必须为64字符(32字节)')
|
|
|
+ }
|
|
|
+
|
|
|
+ if (config.iv.length !== 32) {
|
|
|
+ throw new Error('十六进制AES IV长度必须为32字符(16字节)')
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 字符串格式:长度要求
|
|
|
+ if (config.key.length < 16) {
|
|
|
+ throw new Error('AES密钥长度至少16位')
|
|
|
+ }
|
|
|
+
|
|
|
+ if (config.iv.length < 16) {
|
|
|
+ throw new Error('AES IV长度至少16位')
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 增强版AES加密函数
|
|
|
+ * @param data 需要加密的数据
|
|
|
+ * @param customConfig 自定义配置(可选)
|
|
|
+ * @returns 加密结果对象
|
|
|
+ */
|
|
|
+export function enhancedEncrypt(data: string | object, customConfig?: Partial<AESConfig>): EncryptResult {
|
|
|
+ try {
|
|
|
+ if (!data) {
|
|
|
+ throw new Error('加密数据不能为空')
|
|
|
+ }
|
|
|
+
|
|
|
+ // 合并配置
|
|
|
+ const config: AESConfig = { ...DEFAULT_CONFIG, ...customConfig }
|
|
|
+
|
|
|
+ // 验证配置
|
|
|
+ validateConfig(config)
|
|
|
+
|
|
|
+ // 处理密钥和IV
|
|
|
+ const { key, iv } = processKeyAndIV(config)
|
|
|
+
|
|
|
+ // 准备明文数据
|
|
|
+ const plaintext = typeof data === 'object' ? JSON.stringify(data) : String(data)
|
|
|
+
|
|
|
+ // 对数据进行UTF-8编码
|
|
|
+ const srcs = CryptoJS.enc.Utf8.parse(plaintext)
|
|
|
+
|
|
|
+ // 执行AES CFB加密
|
|
|
+ const encrypted = CryptoJS.AES.encrypt(srcs, key, {
|
|
|
+ iv: iv,
|
|
|
+ mode: config.mode,
|
|
|
+ padding: config.padding
|
|
|
+ })
|
|
|
+
|
|
|
+ // 返回完整的加密结果
|
|
|
+ return {
|
|
|
+ encryptedData: CryptoJS.enc.Base64.stringify(encrypted.ciphertext),
|
|
|
+ iv: CryptoJS.enc.Base64.stringify(iv),
|
|
|
+ timestamp: Date.now(),
|
|
|
+ version: '3.0' // 更新版本号以标识CFB模式
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('AES加密失败:', error)
|
|
|
+ throw new Error(`加密失败: ${error instanceof Error ? error.message : '未知错误'}`)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 增强版AES解密函数
|
|
|
+ * @param encryptedData 加密的Base64字符串
|
|
|
+ * @param iv IV的Base64字符串
|
|
|
+ * @param customConfig 自定义配置(可选)
|
|
|
+ * @returns 解密后的原始数据
|
|
|
+ */
|
|
|
+export function enhancedDecrypt(encryptedData: string, iv: string, customConfig?: Partial<AESConfig>): string | object {
|
|
|
+ try {
|
|
|
+ if (!encryptedData || !iv) {
|
|
|
+ throw new Error('解密数据或IV不能为空')
|
|
|
+ }
|
|
|
+
|
|
|
+ // 合并配置
|
|
|
+ const config: AESConfig = { ...DEFAULT_CONFIG, ...customConfig }
|
|
|
+
|
|
|
+ // 验证配置
|
|
|
+ validateConfig(config)
|
|
|
+
|
|
|
+ // 处理密钥
|
|
|
+ const { key } = processKeyAndIV(config)
|
|
|
+
|
|
|
+ // 解析IV
|
|
|
+ const aesIv = CryptoJS.enc.Base64.parse(iv)
|
|
|
+
|
|
|
+ // 执行AES CFB解密
|
|
|
+ const decrypt = CryptoJS.AES.decrypt(encryptedData, key, {
|
|
|
+ iv: aesIv,
|
|
|
+ mode: config.mode,
|
|
|
+ padding: config.padding
|
|
|
+ })
|
|
|
+
|
|
|
+ // 转换为UTF-8字符串
|
|
|
+ const decryptedStr = decrypt.toString(CryptoJS.enc.Utf8)
|
|
|
+
|
|
|
+ if (!decryptedStr) {
|
|
|
+ throw new Error('解密失败,可能是密钥错误或数据损坏')
|
|
|
+ }
|
|
|
+
|
|
|
+ // 尝试解析为JSON对象,如果不是JSON则返回字符串
|
|
|
+ try {
|
|
|
+ return JSON.parse(decryptedStr)
|
|
|
+ } catch {
|
|
|
+ return decryptedStr
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('AES解密失败:', error)
|
|
|
+ throw new Error(`解密失败: ${error instanceof Error ? error.message : '未知错误'}`)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 加密敏感数据(业务专用)
|
|
|
+ * @param sensitiveData 敏感数据对象
|
|
|
+ * @returns 加密结果
|
|
|
+ */
|
|
|
+export function encryptSensitiveData(sensitiveData: object): EncryptResult {
|
|
|
+ const dataToEncrypt = {
|
|
|
+ ...sensitiveData,
|
|
|
+ encryptedAt: new Date().toISOString(),
|
|
|
+ version: '3.0',
|
|
|
+ securityLevel: 'high'
|
|
|
+ }
|
|
|
+
|
|
|
+ return enhancedEncrypt(dataToEncrypt)
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 与后端完全匹配的加密函数
|
|
|
+ * 使用后端相同的十六进制密钥和IV,确保加密解密完全匹配
|
|
|
+ * @param plaintext 明文数据
|
|
|
+ * @returns 加密后的Base64字符串
|
|
|
+ */
|
|
|
+export function encryptWithBackendConfig(plaintext: string): string {
|
|
|
+ try {
|
|
|
+ if (!plaintext) {
|
|
|
+ throw new Error('加密数据不能为空')
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用后端配置
|
|
|
+ const config: AESConfig = { ...DEFAULT_CONFIG }
|
|
|
+
|
|
|
+ // 验证配置
|
|
|
+ validateConfig(config)
|
|
|
+
|
|
|
+ // 处理密钥和IV(十六进制格式)
|
|
|
+ const { key, iv } = processKeyAndIV(config)
|
|
|
+
|
|
|
+ // 对数据进行UTF-8编码
|
|
|
+ const srcs = CryptoJS.enc.Utf8.parse(plaintext)
|
|
|
+
|
|
|
+ // 执行AES CFB加密
|
|
|
+ const encrypted = CryptoJS.AES.encrypt(srcs, key, {
|
|
|
+ iv: iv,
|
|
|
+ mode: config.mode,
|
|
|
+ padding: config.padding
|
|
|
+ })
|
|
|
+
|
|
|
+ // 返回Base64编码的加密数据
|
|
|
+ return CryptoJS.enc.Base64.stringify(encrypted.ciphertext)
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('后端匹配加密失败:', error)
|
|
|
+ throw new Error(`加密失败: ${error instanceof Error ? error.message : '未知错误'}`)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 与后端完全匹配的解密函数
|
|
|
+ * @param encryptedData 加密的Base64字符串
|
|
|
+ * @returns 解密后的原始数据
|
|
|
+ */
|
|
|
+export function decryptWithBackendConfig(encryptedData: string): string {
|
|
|
+ try {
|
|
|
+ if (!encryptedData) {
|
|
|
+ throw new Error('解密数据不能为空')
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用后端配置
|
|
|
+ const config: AESConfig = { ...DEFAULT_CONFIG }
|
|
|
+
|
|
|
+ // 验证配置
|
|
|
+ validateConfig(config)
|
|
|
+
|
|
|
+ // 处理密钥
|
|
|
+ const { key, iv } = processKeyAndIV(config)
|
|
|
+
|
|
|
+ // 执行AES CFB解密
|
|
|
+ const decrypt = CryptoJS.AES.decrypt(encryptedData, key, {
|
|
|
+ iv: iv,
|
|
|
+ mode: config.mode,
|
|
|
+ padding: config.padding
|
|
|
+ })
|
|
|
+
|
|
|
+ // 转换为UTF-8字符串
|
|
|
+ const decryptedStr = decrypt.toString(CryptoJS.enc.Utf8)
|
|
|
+
|
|
|
+ if (!decryptedStr) {
|
|
|
+ throw new Error('解密失败,可能是密钥错误或数据损坏')
|
|
|
+ }
|
|
|
+
|
|
|
+ return decryptedStr
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('后端匹配解密失败:', error)
|
|
|
+ throw new Error(`解密失败: ${error instanceof Error ? error.message : '未知错误'}`)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 解密登录数据
|
|
|
+ * @param loginData 登录数据对象
|
|
|
+ * @returns 解密后的登录信息
|
|
|
+ */
|
|
|
+export function decryptLoginData(loginData: { aesPassword: string; saltValue: string }): any {
|
|
|
+ try {
|
|
|
+ // 使用saltValue作为IV进行解密
|
|
|
+ return enhancedDecrypt(loginData.aesPassword, loginData.saltValue)
|
|
|
+ } catch (error) {
|
|
|
+ console.error('登录数据解密失败:', error)
|
|
|
+ throw error
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 验证加密数据完整性
|
|
|
+ * @param encryptResult 加密结果
|
|
|
+ * @returns 是否有效
|
|
|
+ */
|
|
|
+export function validateEncryptedData(encryptResult: EncryptResult): boolean {
|
|
|
+ try {
|
|
|
+ if (!encryptResult.encryptedData || !encryptResult.iv) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查时间戳是否合理(不超过24小时)
|
|
|
+ const now = Date.now()
|
|
|
+ const timeDiff = now - encryptResult.timestamp
|
|
|
+ if (timeDiff > 24 * 60 * 60 * 1000) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查版本号(支持2.0和3.0版本)
|
|
|
+ if (!encryptResult.version || (encryptResult.version !== '2.0' && encryptResult.version !== '3.0')) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+ } catch {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 生成安全的随机密钥
|
|
|
+ * @param length 密钥长度
|
|
|
+ * @returns 随机密钥
|
|
|
+ */
|
|
|
+export function generateSecureKey(length: number = 32): string {
|
|
|
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?'
|
|
|
+ const crypto = window.crypto || (window as any).msCrypto
|
|
|
+
|
|
|
+ if (crypto && crypto.getRandomValues) {
|
|
|
+ // 使用Web Crypto API生成更安全的随机数
|
|
|
+ const array = new Uint32Array(length)
|
|
|
+ crypto.getRandomValues(array)
|
|
|
+ let result = ''
|
|
|
+ for (let i = 0; i < length; i++) {
|
|
|
+ result += chars[array[i] % chars.length]
|
|
|
+ }
|
|
|
+ return result
|
|
|
+ } else {
|
|
|
+ // 回退方案
|
|
|
+ let result = ''
|
|
|
+ for (let i = 0; i < length; i++) {
|
|
|
+ result += chars.charAt(Math.floor(Math.random() * chars.length))
|
|
|
+ }
|
|
|
+ return result
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export default {
|
|
|
+ enhancedEncrypt,
|
|
|
+ enhancedDecrypt,
|
|
|
+ encryptSensitiveData,
|
|
|
+ decryptLoginData,
|
|
|
+ validateEncryptedData,
|
|
|
+ generateSecureKey
|
|
|
+}
|