fastbee/yack-mini/miniprogram/utils/crypto.js
2026-09-27 15:17:17 +08:00

174 lines
5.0 KiB
JavaScript
Raw Permalink 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.

/**
* 加密工具
* 用于加密存储敏感信息(如产品密钥)
*
* 注意:这是一个简单的加密实现,适用于小程序场景。
* 生产环境建议使用更安全的加密算法或从服务端动态获取密钥。
*/
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,
};