174 lines
5.0 KiB
JavaScript
174 lines
5.0 KiB
JavaScript
/**
|
||
* 加密工具
|
||
* 用于加密存储敏感信息(如产品密钥)
|
||
*
|
||
* 注意:这是一个简单的加密实现,适用于小程序场景。
|
||
* 生产环境建议使用更安全的加密算法或从服务端动态获取密钥。
|
||
*/
|
||
|
||
const { CRYPTO_KEY } = require('./constants.js');
|
||
|
||
/**
|
||
* UTF-16 字符串 → UTF-8 字节数组
|
||
*
|
||
* 加密必须落在**字节**上:早期实现按 UTF-16 charCode 做 XOR,再用
|
||
* `DataView.setUint8` 写缓冲区 —— setUint8 只保留低 8 位,中文这类
|
||
* >255 的码点(如 '中' 0x4E2D XOR key 后仍 >255)会被静默截断,
|
||
* 解密回来是乱码,密钥被永久损坏且没有任何报错。
|
||
* 代理对(emoji)按 CESU-8 拆成两个三字节序列,编解码对称,往返无损。
|
||
* @param {string} str
|
||
* @returns {Array<number>}
|
||
*/
|
||
function utf8Bytes(str) {
|
||
const bytes = [];
|
||
for (let i = 0; i < str.length; i++) {
|
||
const code = str.charCodeAt(i);
|
||
if (code < 0x80) {
|
||
bytes.push(code);
|
||
} else if (code < 0x800) {
|
||
bytes.push(0xC0 | (code >> 6), 0x80 | (code & 0x3F));
|
||
} else {
|
||
bytes.push(0xE0 | (code >> 12), 0x80 | ((code >> 6) & 0x3F), 0x80 | (code & 0x3F));
|
||
}
|
||
}
|
||
return bytes;
|
||
}
|
||
|
||
/**
|
||
* UTF-8 字节数组 → UTF-16 字符串(utf8Bytes 的逆运算)
|
||
* @param {Array<number>} bytes
|
||
* @returns {string}
|
||
*/
|
||
function bytesToUtf8(bytes) {
|
||
let out = '';
|
||
for (let i = 0; i < bytes.length;) {
|
||
const b = bytes[i++];
|
||
if (b < 0x80) {
|
||
out += String.fromCharCode(b);
|
||
} else if (b < 0xE0) {
|
||
out += String.fromCharCode(((b & 0x1F) << 6) | (bytes[i++] & 0x3F));
|
||
} else {
|
||
out += String.fromCharCode(
|
||
((b & 0x0F) << 12) | ((bytes[i++] & 0x3F) << 6) | (bytes[i++] & 0x3F)
|
||
);
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/**
|
||
* 字节数组 → Base64(兼容小程序环境)
|
||
* @param {Array<number>} bytes
|
||
* @returns {string} Base64 编码字符串;失败返回空串
|
||
*/
|
||
function base64Encode(bytes) {
|
||
try {
|
||
return wx.arrayBufferToBase64(new Uint8Array(bytes).buffer);
|
||
} catch (e) {
|
||
console.error('[Crypto] Base64 编码失败:', e);
|
||
return '';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Base64 → 字节数组(兼容小程序环境)
|
||
* @param {string} base64 - Base64 编码字符串
|
||
* @returns {Array<number>} 字节数组;失败返回空数组
|
||
*/
|
||
function base64Decode(base64) {
|
||
try {
|
||
const buffer = wx.base64ToArrayBuffer(base64);
|
||
return Array.from(new Uint8Array(buffer));
|
||
} catch (e) {
|
||
console.error('[Crypto] Base64 解码失败:', e);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 对称加密(XOR + Base64),按字节运算,任意字符均可无损往返
|
||
*
|
||
* 失败一律返回空串,**不 fail-open 返回明文** —— 把明文当密文落盘
|
||
* 比"加密失败"危险得多(后者是可感知的,前者只是悄悄不加密)。
|
||
* @param {string} text - 明文
|
||
* @param {string} key - 密钥
|
||
* @returns {string} 密文(Base64 编码)
|
||
*/
|
||
function encrypt(text, key = CRYPTO_KEY) {
|
||
if (!text) return '';
|
||
try {
|
||
const bytes = utf8Bytes(text);
|
||
const keyBytes = utf8Bytes(key);
|
||
if (keyBytes.length === 0) {
|
||
console.error('[Crypto] 密钥为空,拒绝加密');
|
||
return '';
|
||
}
|
||
const encrypted = bytes.map((b, i) => b ^ keyBytes[i % keyBytes.length]);
|
||
return base64Encode(encrypted);
|
||
} catch (e) {
|
||
console.error('[Crypto] 加密失败:', e);
|
||
return '';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解密
|
||
* @param {string} ciphertext - 密文(Base64 编码)
|
||
* @param {string} key - 密钥
|
||
* @returns {string} 明文;失败返回空串(同样不返回密文冒充明文)
|
||
*/
|
||
function decrypt(ciphertext, key = CRYPTO_KEY) {
|
||
if (!ciphertext) return '';
|
||
try {
|
||
const bytes = base64Decode(ciphertext);
|
||
const keyBytes = utf8Bytes(key);
|
||
if (bytes.length === 0 || keyBytes.length === 0) return '';
|
||
return bytesToUtf8(bytes.map((b, i) => b ^ keyBytes[i % keyBytes.length]));
|
||
} catch (e) {
|
||
console.error('[Crypto] 解密失败:', e);
|
||
return '';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 加密对象中的指定字段
|
||
* @param {Object} obj - 对象
|
||
* @param {Array<string>} fields - 需要加密的字段名数组
|
||
* @returns {Object} 加密后的对象
|
||
*/
|
||
function encryptFields(obj, fields) {
|
||
if (!obj || typeof obj !== 'object') return obj;
|
||
const result = { ...obj };
|
||
fields.forEach(field => {
|
||
if (result[field]) {
|
||
result[field] = encrypt(result[field]);
|
||
result[`_${field}_encrypted`] = true; // 标记已加密
|
||
}
|
||
});
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 解密对象中的指定字段
|
||
* @param {Object} obj - 对象
|
||
* @param {Array<string>} fields - 需要解密的字段名数组
|
||
* @returns {Object} 解密后的对象
|
||
*/
|
||
function decryptFields(obj, fields) {
|
||
if (!obj || typeof obj !== 'object') return obj;
|
||
const result = { ...obj };
|
||
fields.forEach(field => {
|
||
if (result[field] && result[`_${field}_encrypted`]) {
|
||
result[field] = decrypt(result[field]);
|
||
delete result[`_${field}_encrypted`]; // 移除标记
|
||
}
|
||
});
|
||
return result;
|
||
}
|
||
|
||
module.exports = {
|
||
encrypt,
|
||
decrypt,
|
||
encryptFields,
|
||
decryptFields,
|
||
}; |