241 lines
9.3 KiB
JavaScript
241 lines
9.3 KiB
JavaScript
/* codec.js —— 编码/加解密纯前端算法库
|
||
* 提供:UTF-8、Base64、Base32、URL、Unicode、SHA 系列(Web Crypto 原生)
|
||
* MD5 / SHA-224 依赖 CryptoJS(由页面懒加载,双 CDN fallback)
|
||
*/
|
||
(function (global) {
|
||
'use strict';
|
||
|
||
/* ---------- UTF-8 工具 ---------- */
|
||
function utf8Encode(str) {
|
||
if (typeof TextEncoder !== 'undefined') return new TextEncoder().encode(str);
|
||
// 降级实现
|
||
var out = [], ch, i, code;
|
||
for (i = 0; i < str.length; i++) {
|
||
code = str.charCodeAt(i);
|
||
if (code < 0x80) out.push(code);
|
||
else if (code < 0x800) {
|
||
out.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
|
||
} else if (code < 0xd800 || code >= 0xe000) {
|
||
out.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
|
||
} else {
|
||
// surrogate pair
|
||
ch = (code - 0xd800) * 0x400 + (str.charCodeAt(++i) - 0xdc00) + 0x10000;
|
||
out.push(0xf0 | (ch >> 18), 0x80 | ((ch >> 12) & 0x3f), 0x80 | ((ch >> 6) & 0x3f), 0x80 | (ch & 0x3f));
|
||
}
|
||
}
|
||
return new Uint8Array(out);
|
||
}
|
||
|
||
function utf8Decode(bytes) {
|
||
if (typeof TextDecoder !== 'undefined') return new TextDecoder('utf-8').decode(bytes);
|
||
var out = '', i = 0, b1, b2, b3, b4, code;
|
||
while (i < bytes.length) {
|
||
b1 = bytes[i++];
|
||
if (b1 < 0x80) { out += String.fromCharCode(b1); }
|
||
else if (b1 < 0xe0) { b2 = bytes[i++]; out += String.fromCharCode(((b1 & 0x1f) << 6) | (b2 & 0x3f)); }
|
||
else if (b1 < 0xf0) {
|
||
b2 = bytes[i++]; b3 = bytes[i++];
|
||
out += String.fromCharCode(((b1 & 0x0f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f));
|
||
} else {
|
||
b2 = bytes[i++]; b3 = bytes[i++]; b4 = bytes[i++];
|
||
code = ((b1 & 0x07) << 18) | ((b2 & 0x3f) << 12) | ((b3 & 0x3f) << 6) | (b4 & 0x3f);
|
||
code -= 0x10000;
|
||
out += String.fromCharCode(0xd800 + (code >> 10), 0xdc00 + (code & 0x3ff));
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function bytesToBin(bytes) {
|
||
var bin = '', i;
|
||
for (i = 0; i < bytes.length; i++) {
|
||
bin += String.fromCharCode(bytes[i]);
|
||
}
|
||
return bin;
|
||
}
|
||
|
||
function binToBytes(bin) {
|
||
var out = new Uint8Array(bin.length), i;
|
||
for (i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
||
return out;
|
||
}
|
||
|
||
/* ---------- Base64(UTF-8 安全) ---------- */
|
||
function b64Encode(str) {
|
||
var bytes = utf8Encode(str);
|
||
// 浏览器环境用 btoa
|
||
if (typeof btoa !== 'undefined') {
|
||
var bin = '';
|
||
for (var i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
||
return btoa(bin);
|
||
}
|
||
return global.Base64 && global.Base64.encode(bytesToBin(bytes)) ? global.Base64.encode(bytesToBin(bytes)) : '';
|
||
}
|
||
|
||
function b64Decode(b64) {
|
||
b64 = String(b64 || '').replace(/\s+/g, '');
|
||
var bin;
|
||
if (typeof atob !== 'undefined') {
|
||
bin = atob(b64);
|
||
} else if (global.Base64) {
|
||
bin = global.Base64.decode(b64);
|
||
} else {
|
||
throw new Error('当前环境不支持 Base64 解码');
|
||
}
|
||
return utf8Decode(binToBytes(bin));
|
||
}
|
||
|
||
/* ---------- Base32(RFC 4648,UTF-8 安全) ---------- */
|
||
var B32_ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||
|
||
function base32Encode(str) {
|
||
var bytes = utf8Encode(str), out = '', bits = 0, value = 0, i;
|
||
for (i = 0; i < bytes.length; i++) {
|
||
value = (value << 8) | bytes[i];
|
||
bits += 8;
|
||
while (bits >= 5) {
|
||
out += B32_ALPHA[(value >>> (bits - 5)) & 31];
|
||
bits -= 5;
|
||
}
|
||
}
|
||
if (bits > 0) out += B32_ALPHA[(value << (5 - bits)) & 31];
|
||
while (out.length % 8 !== 0) out += '=';
|
||
return out;
|
||
}
|
||
|
||
function base32Decode(b32) {
|
||
b32 = String(b32 || '').toUpperCase().replace(/[=\s]/g, '');
|
||
if (!b32) return '';
|
||
var bits = 0, value = 0, out = [], i, idx, b;
|
||
for (i = 0; i < b32.length; i++) {
|
||
idx = B32_ALPHA.indexOf(b32[i]);
|
||
if (idx === -1) throw new Error('Base32 字符串包含非法字符: ' + b32[i]);
|
||
value = (value << 5) | idx;
|
||
bits += 5;
|
||
if (bits >= 8) {
|
||
b = (value >>> (bits - 8)) & 0xff;
|
||
out.push(b);
|
||
bits -= 8;
|
||
}
|
||
}
|
||
return utf8Decode(new Uint8Array(out));
|
||
}
|
||
|
||
/* ---------- URL 编解码 ---------- */
|
||
function urlEncode(str) { return encodeURIComponent(String(str)); }
|
||
function urlDecode(str) { return decodeURIComponent(String(str).replace(/\+/g, ' ')); }
|
||
|
||
/* ---------- Unicode 编解码(支持中文、emoji) ---------- */
|
||
function unicodeEncode(str) {
|
||
str = String(str);
|
||
var out = [], i, code;
|
||
for (i = 0; i < str.length; i++) {
|
||
code = str.charCodeAt(i);
|
||
if (code >= 0xd800 && code <= 0xdbff && i + 1 < str.length) {
|
||
// 代理对:按两个 \uXXXX 输出,JS 自动组合
|
||
out.push('\\u' + code.toString(16).toUpperCase().padStart(4, '0'));
|
||
out.push('\\u' + str.charCodeAt(++i).toString(16).toUpperCase().padStart(4, '0'));
|
||
} else {
|
||
out.push('\\u' + code.toString(16).toUpperCase().padStart(4, '0'));
|
||
}
|
||
}
|
||
return out.join('');
|
||
}
|
||
|
||
function unicodeDecode(str) {
|
||
str = String(str);
|
||
// 兼容 \u{1F600} 与 \uXXXX 两种写法
|
||
str = str.replace(/\\u\{([0-9a-fA-F]{1,6})\}/g, function (m, h) {
|
||
var cp = parseInt(h, 16);
|
||
if (cp > 0xffff) {
|
||
cp -= 0x10000;
|
||
return String.fromCharCode(0xd800 + (cp >> 10), 0xdc00 + (cp & 0x3ff));
|
||
}
|
||
return String.fromCharCode(cp);
|
||
});
|
||
str = str.replace(/\\u([0-9a-fA-F]{4})/g, function (m, h) {
|
||
return String.fromCharCode(parseInt(h, 16));
|
||
});
|
||
return str;
|
||
}
|
||
|
||
/* ---------- SHA 系列(Web Crypto 原生) ---------- */
|
||
var HASHES = { 'SHA-1': 'SHA-1', 'SHA-256': 'SHA-256', 'SHA-384': 'SHA-384', 'SHA-512': 'SHA-512' };
|
||
|
||
function hexFromBuffer(buf) {
|
||
var bytes = new Uint8Array(buf), out = '', i;
|
||
for (i = 0; i < bytes.length; i++) out += bytes[i].toString(16).padStart(2, '0');
|
||
return out;
|
||
}
|
||
|
||
function shaDigest(algorithm, text) {
|
||
var bytes = utf8Encode(text);
|
||
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
||
return crypto.subtle.digest(algorithm, bytes).then(hexFromBuffer);
|
||
}
|
||
// 非安全上下文(如普通 http)无 Web Crypto:回退 crypto-js
|
||
return cryptoJsDigest(algorithm, text);
|
||
}
|
||
|
||
/* ---------- CryptoJS 懒加载(MD5 / SHA-224 等需要) ---------- */
|
||
var CDN_CRYPTO_JS = [
|
||
'https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js',
|
||
'https://cdn.jsdelivr.net/npm/crypto-js@4.2.0/crypto-js.min.js'
|
||
];
|
||
|
||
var cryptoJSPromise = null;
|
||
function loadCryptoJS() {
|
||
if (global.CryptoJS) return Promise.resolve(global.CryptoJS);
|
||
if (cryptoJSPromise) return cryptoJSPromise;
|
||
cryptoJSPromise = new Promise(function (resolve) {
|
||
var load = function (idx) {
|
||
if (global.CryptoJS) { resolve(global.CryptoJS); return; }
|
||
if (idx >= CDN_CRYPTO_JS.length) { resolve(null); return; }
|
||
var s = document.createElement('script');
|
||
s.src = CDN_CRYPTO_JS[idx];
|
||
s.onload = function () { resolve(global.CryptoJS || null); };
|
||
s.onerror = function () { load(idx + 1); };
|
||
document.head.appendChild(s);
|
||
};
|
||
load(0);
|
||
});
|
||
return cryptoJSPromise;
|
||
}
|
||
|
||
var CRYPTO_JS_METHODS = {
|
||
'MD5': 'MD5', 'SHA-1': 'SHA1', 'SHA-224': 'SHA224',
|
||
'SHA-256': 'SHA256', 'SHA-384': 'SHA384', 'SHA-512': 'SHA512'
|
||
};
|
||
|
||
function cryptoJsDigest(algo, text) {
|
||
return loadCryptoJS().then(function (C) {
|
||
if (!C) throw new Error('计算 ' + algo + ' 需要联网加载加密组件(crypto-js)失败,请检查网络');
|
||
var method = CRYPTO_JS_METHODS[algo];
|
||
if (!method || typeof C[method] !== 'function') throw new Error('不支持的算法: ' + algo);
|
||
return C[method](text).toString(C.enc.Hex);
|
||
});
|
||
}
|
||
|
||
function hashText(algo, text) {
|
||
if (HASHES[algo]) return shaDigest(HASHES[algo], text);
|
||
if (algo === 'MD5' || algo === 'SHA-224') return cryptoJsDigest(algo, text);
|
||
return Promise.reject(new Error('未知算法: ' + algo));
|
||
}
|
||
|
||
/* ---------- 导出 ---------- */
|
||
global.CodecLib = {
|
||
utf8Encode: utf8Encode,
|
||
utf8Decode: utf8Decode,
|
||
b64Encode: b64Encode,
|
||
b64Decode: b64Decode,
|
||
base32Encode: base32Encode,
|
||
base32Decode: base32Decode,
|
||
urlEncode: urlEncode,
|
||
urlDecode: urlDecode,
|
||
unicodeEncode: unicodeEncode,
|
||
unicodeDecode: unicodeDecode,
|
||
hashText: hashText,
|
||
hashes: ['MD5', 'SHA-1', 'SHA-224', 'SHA-256', 'SHA-384', 'SHA-512']
|
||
};
|
||
})(window);
|