first commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user