/** * 与后端 EncryptUtil.aesEncryptToBase64(AES-GCM,12 字节 IV,16 字节 tag)一致。 * App 端 WebView 往往无 crypto.subtle,需用纯 JS 解密。 */ import { gcm } from '@noble/ciphers/aes.js'; import { sha256 } from '@noble/hashes/sha2.js'; // Polyfill for TextEncoder/TextDecoder in Weex environment class TextEncoderPolyfill { encode(text) { const arr = new Uint8Array(text.length); for (let i = 0; i < text.length; i++) { arr[i] = text.charCodeAt(i) & 0xff; } return arr; } } class TextDecoderPolyfill { decode(uint8Array) { let str = ''; let i = 0; while (i < uint8Array.length) { const byte1 = uint8Array[i]; if (byte1 < 0x80) { // 单字节 ASCII str += String.fromCharCode(byte1); i++; } else if ((byte1 & 0xe0) === 0xc0) { // 双字节 UTF-8 const byte2 = uint8Array[i + 1]; const code = ((byte1 & 0x1f) << 6) | (byte2 & 0x3f); str += String.fromCharCode(code); i += 2; } else if ((byte1 & 0xf0) === 0xe0) { // 三字节 UTF-8 const byte2 = uint8Array[i + 1]; const byte3 = uint8Array[i + 2]; const code = ((byte1 & 0x0f) << 12) | ((byte2 & 0x3f) << 6) | (byte3 & 0x3f); str += String.fromCharCode(code); i += 3; } else if ((byte1 & 0xf8) === 0xf0) { // 四字节 UTF-8(需要代理对) const byte2 = uint8Array[i + 1]; const byte3 = uint8Array[i + 2]; const byte4 = uint8Array[i + 3]; let code = ((byte1 & 0x07) << 18) | ((byte2 & 0x3f) << 12) | ((byte3 & 0x3f) << 6) | (byte4 & 0x3f); // 转换为 UTF-16 代理对 code -= 0x10000; const high = 0xd800 + (code >> 10); const low = 0xdc00 + (code & 0x3ff); str += String.fromCharCode(high, low); i += 4; } else { // 非法字节,跳过 str += String.fromCharCode(byte1); i++; } } return str; } } const TextEncoder = typeof globalThis !== 'undefined' && globalThis.TextEncoder ? globalThis.TextEncoder : TextEncoderPolyfill; const TextDecoder = typeof globalThis !== 'undefined' && globalThis.TextDecoder ? globalThis.TextDecoder : TextDecoderPolyfill; /** 与后端 EncryptUtil 一致:去掉 BOM/空白,避免与 SHA-256(UTF-8) 密钥不一致 */ function normalizeSecret(secret) { if (secret == null || typeof secret !== 'string') { return secret; } let s = secret.trim(); if (s.length > 0 && s.charCodeAt(0) === 0xfeff) { s = s.slice(1); } return s; } /** * x-www-form-urlencoded /部分网关会把 Base64 里的 "+" 变成空格;JSON 一般不受影响。 */ function normalizeBase64Segment(seg) { if (seg == null || seg === '') { return seg; } let s = String(seg).trim().replace(/\s/g, '+'); s = s.replace(/-/g, '+').replace(/_/g, '/'); const pad = s.length % 4; if (pad !== 0) { s += '='.repeat(4 - pad); } return s; } function base64ToUint8Array(b64) { const normalized = normalizeBase64Segment(b64); if (typeof atob === 'function') { const bin = atob(normalized); const out = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) { out[i] = bin.charCodeAt(i) & 0xff; } return out; } if (typeof uni !== 'undefined' && uni.base64ToArrayBuffer) { return new Uint8Array(uni.base64ToArrayBuffer(normalized)); } throw new Error('cfgSettingCrypto: no base64 decoder'); } function splitIvAndCipher(encryptedText) { const idx = encryptedText.indexOf(':'); if (idx <= 0 || idx === encryptedText.length - 1) { return null; } return { ivB64: encryptedText.slice(0, idx), cipherB64: encryptedText.slice(idx + 1), }; } async function decryptWithSubtle(encryptedText, secret) { const parts = splitIvAndCipher(encryptedText); if (!parts) { throw new Error('cfgSettingCrypto: invalid iv:cipher format'); } const ivBytes = base64ToUint8Array(parts.ivB64); const cipherBytes = base64ToUint8Array(parts.cipherB64); const keyRaw = new TextEncoder().encode(normalizeSecret(secret)); const keyHash = await globalThis.crypto.subtle.digest('SHA-256', keyRaw); const aesKey = await globalThis.crypto.subtle.importKey( 'raw', keyHash, { name: 'AES-GCM' }, false, ['decrypt'] ); const decrypted = await globalThis.crypto.subtle.decrypt( { name: 'AES-GCM', iv: ivBytes, tagLength: 128 }, aesKey, cipherBytes ); return new TextDecoder().decode(decrypted); } function decryptWithNoble(encryptedText, secret) { const parts = splitIvAndCipher(encryptedText); if (!parts) { throw new Error('cfgSettingCrypto: invalid iv:cipher format'); } const ivBytes = base64ToUint8Array(parts.ivB64); const cipherBytes = base64ToUint8Array(parts.cipherB64); const keyBytes = sha256(new TextEncoder().encode(normalizeSecret(secret))); const decipher = gcm(keyBytes, ivBytes); const plainBytes = decipher.decrypt(cipherBytes); return new TextDecoder().decode(plainBytes); } /** * @param {string} encryptedText 格式 base64(iv):base64(ciphertext||tag) * @param {string} secret 与后端 allow_key 一致 * @returns {Promise} */ export async function decryptCfgSettingValue(encryptedText, secret) { if (!encryptedText || !secret || encryptedText.indexOf(':') === -1) { return encryptedText; } if (!splitIvAndCipher(encryptedText)) { return encryptedText; } try { if (globalThis.crypto && globalThis.crypto.subtle) { const plain = await decryptWithSubtle(encryptedText, secret); if (plain) { return plain; } } } catch (e) { console.warn('[cfgSettingCrypto] WebCrypto 解密失败,改用 @noble/ciphers', e); } try { return decryptWithNoble(encryptedText, secret); } catch (e) { console.error('[cfgSettingCrypto]解密失败', e); return encryptedText; } }