first commit

This commit is contained in:
Mauve
2026-09-08 17:20:27 +08:00
commit a7f2e0ca01
752 changed files with 111259 additions and 0 deletions
+187
View File
@@ -0,0 +1,187 @@
/**
* 与后端 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<string>}
*/
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;
}
}