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