Files
mgm-daylight-mcrm-mobile-ios/common/cfgSettingCrypto.js
T
2026-09-08 17:20:27 +08:00

188 lines
5.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 与后端 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;
}
}