/** * 格式化相关工具函数 */ /** * 格式化数字为两位数字符串 * @param {number} n 数字 * @returns {string} 格式化后的字符串 */ const formatNumber = n => { n = n.toString(); return n[1] ? n : '0' + n; }; /** * 格式化时间 * @param {Date} date 日期对象 * @returns {string} 格式化后的时间字符串 */ const formatTime = date => { const year = date.getFullYear(); const month = date.getMonth() + 1; const day = date.getDate(); const hour = date.getHours(); const minute = date.getMinutes(); const second = date.getSeconds(); return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':'); }; /** * 解析OTP URL格式 * @param {string} url - OTP URL字符串 * @returns {Object|null} 解析后的令牌数据对象,如果解析失败返回null */ const parseURL = (url) => { try { // 基本格式验证 if (!url.startsWith('otpauth://')) { return null; } // 解析类型和路径 const protocolAndRest = url.split('://'); if (protocolAndRest.length !== 2) return null; const typeAndRest = protocolAndRest[1].split('/', 2); if (typeAndRest.length !== 2) return null; const type = typeAndRest[0].toLowerCase(); // 分离路径和查询参数 const pathAndQuery = typeAndRest[1].split('?'); if (pathAndQuery.length !== 2) return null; // 解析路径部分(issuer和account) const pathParts = decodeURIComponent(pathAndQuery[0]).split(':'); let issuer = pathParts[0]; const account = pathParts.length > 1 ? pathParts[1] : ''; // 解析查询参数 const params = {}; const queryParts = pathAndQuery[1].split('&'); for (const part of queryParts) { const [key, value] = part.split('='); if (key && value) { params[key.toLowerCase()] = decodeURIComponent(value); } } // 如果params中有issuer参数,优先使用它 if (params.issuer) { issuer = params.issuer; } // 验证必要参数 if (!params.secret) { throw new Error('缺少必要参数: secret'); } // 验证并规范化参数 const digits = parseInt(params.digits) || 6; if (digits < 6 || digits > 8) { throw new Error('验证码位数必须在6-8之间'); } const period = parseInt(params.period) || (type === 'totp' ? 30 : undefined); if (type === 'totp' && (period < 15 || period > 60)) { throw new Error('TOTP周期必须在15-60秒之间'); } const counter = parseInt(params.counter) || (type === 'hotp' ? 0 : undefined); if (type === 'hotp' && counter < 0) { throw new Error('HOTP计数器不能为负数'); } // 构建返回对象 return { type, issuer, account, secret: params.secret.toUpperCase(), // 统一转换为大写 algorithm: (params.algorithm || 'SHA1').toUpperCase(), digits, period, counter }; } catch (error) { console.error('解析OTP URL失败:', error); return null; } }; /** * 验证令牌数据的有效性 * @param {Object} tokenData - 令牌数据对象 * @returns {string[]|null} 错误信息数组,如果验证通过返回null */ const validateToken = (tokenData) => { const errors = []; // 验证必填字段 if (!tokenData.issuer || !tokenData.issuer.trim()) { errors.push('服务名称不能为空'); } if (!tokenData.secret || !tokenData.secret.trim()) { errors.push('密钥不能为空'); } // 验证算法 const validAlgorithms = ['SHA1', 'SHA256', 'SHA512']; if (!validAlgorithms.includes(tokenData.algorithm)) { errors.push(`不支持的算法: ${tokenData.algorithm}`); } // 验证位数 if (tokenData.digits < 6 || tokenData.digits > 8) { errors.push('位数必须在6-8之间'); } // 类型特定验证 if (tokenData.type === 'totp') { if (tokenData.period < 15 || tokenData.period > 300) { errors.push('更新周期必须在15-300秒之间'); } } else if (tokenData.type === 'hotp') { if (tokenData.counter < 0) { errors.push('计数器值不能为负数'); } } else { errors.push('无效的令牌类型'); } return errors.length > 0 ? errors : null; }; module.exports = { formatTime, formatNumber, parseURL, validateToken };