348 lines
15 KiB
JavaScript
348 lines
15 KiB
JavaScript
/* navicat.js —— Navicat 数据库连接密码 本地解密 / 加密(纯前端,不上传服务器)
|
||
* 支持两种算法(依据 HyperSine《how-does-navicat-encrypt-password》实现):
|
||
* 版本 11 及以前:Blowfish/ECB/NoPadding + 自定义链式
|
||
* 密钥 = SHA1("3DC5CA39"),IV = E(FFFFFFFFFFFFFFFF) = d9c7c3c8870d64bd(无填充)
|
||
* 版本 12 及以后:AES-128-CBC/PKCS7
|
||
* 密钥 = "libcckeylibcckey",IV = "libcciv libcciv "
|
||
* 组件依赖(优先本地 assets/js/,缺失回退公共 CDN;仅加载代码,不含你的内容):
|
||
* crypto-js 4.2.0 —— 版本 12 的 AES-128-CBC
|
||
* egoroof-blowfish 2.2.2 —— 版本 11 的 Blowfish/ECB 单块
|
||
*/
|
||
(function (global) {
|
||
'use strict';
|
||
|
||
var AES_KEY = 'libcckeylibcckey';
|
||
var AES_IV = 'libcciv libcciv ';
|
||
var BLOW_KEY_HEX = '42ceb271a5e458b74aea93947922354391873340'; // SHA1("3DC5CA39") 的 20 字节密钥
|
||
var BLOW_IV_HEX = 'd9c7c3c8870d64bd'; // Blowfish E(FFFFFFFFFFFFFFFF)
|
||
|
||
var LIBS = {
|
||
cryptojs: {
|
||
key: 'CryptoJS',
|
||
urls: [
|
||
'../assets/js/crypto-js.min.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'
|
||
]
|
||
},
|
||
blowfish: {
|
||
key: 'Blowfish',
|
||
urls: [
|
||
'../assets/js/blowfish.js',
|
||
'https://cdn.jsdelivr.net/npm/egoroof-blowfish@2.2.2/dist/blowfish.js',
|
||
'https://unpkg.com/egoroof-blowfish@2.2.2/dist/blowfish.js'
|
||
]
|
||
}
|
||
};
|
||
var loadCache = {};
|
||
|
||
/* 懒加载指定组件(首个可用地址即可,返回 Promise<库对象|null>) */
|
||
function loadLib(name) {
|
||
var spec = LIBS[name];
|
||
if (global[spec.key]) return Promise.resolve(global[spec.key]);
|
||
if (loadCache[name]) return loadCache[name];
|
||
loadCache[name] = new Promise(function (resolve) {
|
||
var go = function (idx) {
|
||
if (global[spec.key]) { resolve(global[spec.key]); return; }
|
||
if (idx >= spec.urls.length) { resolve(null); return; }
|
||
var s = document.createElement('script');
|
||
s.src = spec.urls[idx];
|
||
s.async = true;
|
||
s.onload = function () { resolve(global[spec.key] || null); };
|
||
s.onerror = function () { go(idx + 1); };
|
||
document.head.appendChild(s);
|
||
};
|
||
go(0);
|
||
});
|
||
return loadCache[name];
|
||
}
|
||
|
||
/* ================= 字节 / 编码工具 ================= */
|
||
function cleanHex(s) { return String(s == null ? '' : s).replace(/[^0-9a-fA-F]/g, ''); }
|
||
|
||
function hexToBytes(hex) {
|
||
var h = cleanHex(hex);
|
||
if (h.length % 2 !== 0) throw new Error('十六进制长度必须为偶数');
|
||
var out = new Uint8Array(h.length / 2);
|
||
for (var i = 0; i < out.length; i++) out[i] = parseInt(h.substr(i * 2, 2), 16);
|
||
return out;
|
||
}
|
||
|
||
function bytesToHex(bytes) {
|
||
var s = '';
|
||
for (var i = 0; i < bytes.length; i++) {
|
||
var b = bytes[i] & 0xff;
|
||
s += (b < 16 ? '0' : '') + b.toString(16);
|
||
}
|
||
return s;
|
||
}
|
||
|
||
function utf8Bytes(str) {
|
||
str = String(str);
|
||
if (typeof TextEncoder !== 'undefined') return new TextEncoder().encode(str);
|
||
var out = [], i, c;
|
||
for (i = 0; i < str.length; i++) {
|
||
c = str.charCodeAt(i);
|
||
if (c < 0x80) out.push(c);
|
||
else if (c < 0x800) out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f));
|
||
else if (c < 0xd800 || c >= 0xe000) out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
|
||
else {
|
||
var ch = (c - 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 utf8Str(bytes) {
|
||
if (typeof TextDecoder !== 'undefined') return new TextDecoder('utf-8').decode(bytes);
|
||
var out = '', i = 0, b1, b2, b3, b4, c;
|
||
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++];
|
||
c = (((b1 & 0x07) << 18) | ((b2 & 0x3f) << 12) | ((b3 & 0x3f) << 6) | (b4 & 0x3f)) - 0x10000;
|
||
out += String.fromCharCode(0xd800 + (c >> 10), 0xdc00 + (c & 0x3ff));
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function xorBytes(a, b) {
|
||
var n = Math.min(a.length, b.length), out = new Uint8Array(n);
|
||
for (var i = 0; i < n; i++) out[i] = a[i] ^ b[i];
|
||
return out;
|
||
}
|
||
|
||
function concatBytes(list) {
|
||
var len = 0, i;
|
||
for (i = 0; i < list.length; i++) len += list[i].length;
|
||
var out = new Uint8Array(len), off = 0;
|
||
for (i = 0; i < list.length; i++) { out.set(list[i], off); off += list[i].length; }
|
||
return out;
|
||
}
|
||
|
||
function ab(u8) { return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength); }
|
||
|
||
function asU8(x) {
|
||
if (x instanceof Uint8Array) return x;
|
||
if (typeof ArrayBuffer !== 'undefined' && x instanceof ArrayBuffer) return new Uint8Array(x);
|
||
if (Array.isArray(x)) return new Uint8Array(x);
|
||
if (x && typeof x.length === 'number') return new Uint8Array(x);
|
||
throw new Error('无法识别的字节数据');
|
||
}
|
||
|
||
/* ================= 版本 11:Blowfish 自定义链式(无填充) ================= */
|
||
function newBlowfish(Blowfish) {
|
||
return new Blowfish(ab(hexToBytes(BLOW_KEY_HEX)), Blowfish.MODE.ECB, Blowfish.PADDING.NULL);
|
||
}
|
||
function bfEnc(bf, block) { return asU8(bf.encode(ab(block))).slice(0, 8); }
|
||
function bfDec(bf, block, Blowfish) { return asU8(bf.decode(ab(block), Blowfish.TYPE.UINT8_ARRAY)).slice(0, 8); }
|
||
|
||
function encrypt11(Blowfish, text) {
|
||
var bf = newBlowfish(Blowfish);
|
||
var v = hexToBytes(BLOW_IV_HEX);
|
||
var data = utf8Bytes(text);
|
||
var rounds = Math.floor(data.length / 8);
|
||
var left = data.length % 8;
|
||
var out = '';
|
||
for (var i = 0; i < rounds; i++) {
|
||
var block = data.slice(i * 8, i * 8 + 8);
|
||
var temp = bfEnc(bf, xorBytes(block, v));
|
||
v = xorBytes(v, temp);
|
||
out += bytesToHex(temp).toUpperCase();
|
||
}
|
||
if (left > 0) {
|
||
v = bfEnc(bf, v);
|
||
out += bytesToHex(xorBytes(data.slice(rounds * 8), v)).toUpperCase();
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function decrypt11(Blowfish, hex) {
|
||
var bf = newBlowfish(Blowfish);
|
||
var v = hexToBytes(BLOW_IV_HEX);
|
||
var data = hexToBytes(hex);
|
||
var rounds = Math.floor(data.length / 8);
|
||
var left = data.length % 8;
|
||
var parts = [];
|
||
for (var i = 0; i < rounds; i++) {
|
||
var block = data.slice(i * 8, i * 8 + 8);
|
||
parts.push(xorBytes(bfDec(bf, block, Blowfish), v));
|
||
v = xorBytes(v, block);
|
||
}
|
||
if (left > 0) {
|
||
v = bfEnc(bf, v);
|
||
parts.push(xorBytes(data.slice(rounds * 8), v));
|
||
}
|
||
return utf8Str(concatBytes(parts));
|
||
}
|
||
|
||
/* ================= 版本 12:AES-128-CBC / PKCS7 ================= */
|
||
function pkcs7Pad(u8, block) {
|
||
var pad = block - (u8.length % block);
|
||
var out = new Uint8Array(u8.length + pad);
|
||
out.set(u8);
|
||
for (var i = 0; i < pad; i++) out[u8.length + i] = pad;
|
||
return out;
|
||
}
|
||
function pkcs7Unpad(u8, block) {
|
||
if (!u8.length || u8.length % block) return u8;
|
||
var pad = u8[u8.length - 1];
|
||
if (pad < 1 || pad > block || pad > u8.length) return u8; // 非合法填充:按原文返回
|
||
for (var i = u8.length - pad; i < u8.length; i++) {
|
||
if (u8[i] !== pad) return u8;
|
||
}
|
||
return u8.slice(0, u8.length - pad);
|
||
}
|
||
|
||
function u8ToWa(CryptoJS, u8) {
|
||
var words = [];
|
||
for (var i = 0; i < u8.length; i++) words[i >>> 2] = (words[i >>> 2] || 0) | (u8[i] << (24 - (i % 4) * 8));
|
||
return CryptoJS.lib.WordArray.create(words, u8.length);
|
||
}
|
||
function waToU8(wa) {
|
||
var words = wa.words, n = wa.sigBytes, out = new Uint8Array(n);
|
||
for (var i = 0; i < n; i++) out[i] = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
|
||
return out;
|
||
}
|
||
|
||
function encrypt12(CryptoJS, text) {
|
||
var key = CryptoJS.enc.Utf8.parse(AES_KEY);
|
||
var iv = CryptoJS.enc.Utf8.parse(AES_IV);
|
||
var data = u8ToWa(CryptoJS, pkcs7Pad(utf8Bytes(text), 16));
|
||
var enc = CryptoJS.AES.encrypt(data, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.NoPadding });
|
||
return enc.ciphertext.toString(CryptoJS.enc.Hex).toUpperCase();
|
||
}
|
||
|
||
function decrypt12(CryptoJS, hex) {
|
||
var key = CryptoJS.enc.Utf8.parse(AES_KEY);
|
||
var iv = CryptoJS.enc.Utf8.parse(AES_IV);
|
||
var ct = CryptoJS.enc.Hex.parse(cleanHex(hex).toLowerCase());
|
||
var dec = CryptoJS.AES.decrypt({ ciphertext: ct }, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.NoPadding });
|
||
return utf8Str(pkcs7Unpad(waToU8(dec), 16));
|
||
}
|
||
|
||
/* ================= .ncx 连接文件解析 ================= */
|
||
function decodeEntities(s) {
|
||
return String(s).replace(/&(amp|lt|gt|quot|apos|#\d+|#x[0-9a-f]+);/gi, function (m, g) {
|
||
var k = g.toLowerCase();
|
||
if (k === 'amp') return '&';
|
||
if (k === 'lt') return '<';
|
||
if (k === 'gt') return '>';
|
||
if (k === 'quot') return '"';
|
||
if (k === 'apos') return "'";
|
||
if (g.charAt(0) === '#') {
|
||
var code = g.charAt(1).toLowerCase() === 'x' ? parseInt(g.substr(2), 16) : parseInt(g.substr(1), 10);
|
||
return isNaN(code) ? m : String.fromCharCode(code);
|
||
}
|
||
return m;
|
||
});
|
||
}
|
||
|
||
/* 解析 .ncx(Navicat 导出的 XML 连接文件),返回连接数组 */
|
||
function parseNcx(xmlText) {
|
||
var text = String(xmlText == null ? '' : xmlText);
|
||
if (!/\S/.test(text)) throw new Error('文件内容为空');
|
||
var connRe = /<Connection\b[^>]*?\/?>/gi;
|
||
var attrRe = /([A-Za-z_][\w:.-]*)\s*=\s*"([^"]*)"/g;
|
||
var out = [], m;
|
||
while ((m = connRe.exec(text))) {
|
||
var attrs = {}, a;
|
||
attrRe.lastIndex = 0;
|
||
while ((a = attrRe.exec(m[0]))) attrs[a[1]] = decodeEntities(a[2]);
|
||
out.push({
|
||
name: attrs.ConnectionName || '',
|
||
type: attrs.ConnType || '',
|
||
host: attrs.Host || '',
|
||
port: attrs.Port || '',
|
||
user: attrs.UserName || '',
|
||
password: attrs.Password || attrs.Pwd || '',
|
||
attrs: attrs
|
||
});
|
||
}
|
||
if (!out.length) throw new Error('未找到 <Connection> 节点,请确认是 Navicat 导出的 .ncx 文件');
|
||
return out;
|
||
}
|
||
|
||
function formatReport(rows, version) {
|
||
var lines = ['共解析 ' + rows.length + ' 个连接(密码按 Navicat ' + version + ' 解密)'];
|
||
for (var i = 0; i < rows.length; i++) {
|
||
var r = rows[i];
|
||
lines.push('');
|
||
lines.push('【' + (i + 1) + '】' + (r.name || '(未命名)') + (r.type ? '(' + r.type + ')' : ''));
|
||
lines.push(' 主机:' + (r.host || '(空)'));
|
||
lines.push(' 端口:' + (r.port || '(空)'));
|
||
lines.push(' 用户名:' + (r.user || '(空)'));
|
||
lines.push(' 密码:' + (r.plain ? r.plain : '(空)') + (r.plainNote || ''));
|
||
if (r.password && !/^null$/i.test(r.password)) lines.push(' 密码密文:' + r.password);
|
||
}
|
||
return lines.join('\n') + '\n';
|
||
}
|
||
|
||
/* 解析文件并解密每个连接密码,返回规范格式文本(Promise<string>) */
|
||
function buildConnectionReport(xmlText, version) {
|
||
var ver = normalizeVersion(version);
|
||
var rows;
|
||
try { rows = parseNcx(xmlText); } catch (e) { return Promise.reject(e); }
|
||
return Promise.all(rows.map(function (c) {
|
||
var pwd = c.password;
|
||
var make = function (plain, note) {
|
||
return {
|
||
name: c.name, type: c.type, host: c.host, port: c.port, user: c.user,
|
||
password: pwd, plain: plain, plainNote: note || ''
|
||
};
|
||
};
|
||
if (!pwd || /^null$/i.test(pwd)) return Promise.resolve(make('', ''));
|
||
if (!/^[0-9a-fA-F]+$/.test(pwd) || pwd.length % 2 !== 0) {
|
||
return Promise.resolve(make(pwd, '(非十六进制密文,按原文显示)'));
|
||
}
|
||
return decrypt(pwd, ver).then(function (t) {
|
||
return make(t, t.indexOf('\uFFFD') >= 0 ? '(解密结果含乱码,请核对版本)' : '');
|
||
}).catch(function (e) {
|
||
return make(pwd, '(解密失败:' + (e && e.message ? e.message : e) + ')');
|
||
});
|
||
})).then(function (list) {
|
||
return formatReport(list, ver);
|
||
});
|
||
}
|
||
|
||
/* ================= 对外接口 ================= */
|
||
function normalizeVersion(v) { return String(v) === '11' ? 11 : 12; }
|
||
|
||
function decrypt(cipher, version) {
|
||
var ver = normalizeVersion(version);
|
||
if (!cleanHex(cipher).length) return Promise.reject(new Error('请输入十六进制密文'));
|
||
return loadLib(ver === 11 ? 'blowfish' : 'cryptojs').then(function (lib) {
|
||
if (!lib) throw new Error('加密组件加载失败,请检查网络后重试');
|
||
return ver === 11 ? decrypt11(lib, cipher) : decrypt12(lib, cipher);
|
||
});
|
||
}
|
||
|
||
function encrypt(text, version) {
|
||
var ver = normalizeVersion(version);
|
||
if (text === '' || text == null) return Promise.reject(new Error('请输入明文'));
|
||
return loadLib(ver === 11 ? 'blowfish' : 'cryptojs').then(function (lib) {
|
||
if (!lib) throw new Error('加密组件加载失败,请检查网络后重试');
|
||
return ver === 11 ? encrypt11(lib, text) : encrypt12(lib, text);
|
||
});
|
||
}
|
||
|
||
global.NavicatCipher = {
|
||
decrypt: decrypt,
|
||
encrypt: encrypt,
|
||
buildConnectionReport: buildConnectionReport,
|
||
_core: {
|
||
encrypt11: encrypt11, decrypt11: decrypt11, encrypt12: encrypt12, decrypt12: decrypt12,
|
||
parseNcx: parseNcx, formatReport: formatReport,
|
||
hexToBytes: hexToBytes, bytesToHex: bytesToHex, utf8Bytes: utf8Bytes, utf8Str: utf8Str,
|
||
pkcs7Pad: pkcs7Pad, pkcs7Unpad: pkcs7Unpad
|
||
}
|
||
};
|
||
})(typeof window !== 'undefined' ? window : globalThis);
|