380 lines
No EOL
9.5 KiB
JavaScript
380 lines
No EOL
9.5 KiB
JavaScript
/**
|
||
* 统一工具接口
|
||
* 所有外部模块只能通过此文件访问功能
|
||
*/
|
||
const otp = require('./otp');
|
||
const storage = require('./storage');
|
||
const format = require('./format');
|
||
const cloud = require('./cloud');
|
||
const ui = require('./ui');
|
||
const base32 = require('./base32');
|
||
const crypto = require('./crypto');
|
||
|
||
const eventManager = require('./eventManager');
|
||
|
||
/**
|
||
* 验证token是否在当前时间窗口内
|
||
* @param {Object} token - 令牌对象
|
||
* @param {number} timestamp - 当前时间戳(秒)
|
||
* @param {number} period - TOTP周期(秒)
|
||
* @returns {boolean} 是否在同一时间窗口
|
||
*/
|
||
function verifyTokenWindow(token, timestamp, period) {
|
||
if (!token || !token.timestamp) return false;
|
||
const currentCounter = Math.floor(timestamp / period);
|
||
const tokenCounter = Math.floor(token.timestamp / period);
|
||
return currentCounter === tokenCounter;
|
||
}
|
||
|
||
// ============ OTP相关功能 ============
|
||
|
||
/**
|
||
* 生成当前时间的验证码
|
||
* @param {Object} config - 配置对象
|
||
* @param {string} config.type - OTP类型 ('totp' 或 'hotp')
|
||
* @param {string} config.secret - Base32编码的密钥
|
||
* @param {string} [config.algorithm='SHA1'] - 使用的哈希算法
|
||
* @param {number} [config.period=30] - TOTP的时间周期(秒)
|
||
* @param {number} [config.digits=6] - OTP的位数
|
||
* @param {number} [config.counter] - HOTP的计数器值
|
||
* @param {boolean} [config._forceRefresh=false] - 是否强制刷新
|
||
* @returns {Promise<string>} 生成的验证码
|
||
*/
|
||
const generateCode = async (config) => {
|
||
try {
|
||
const options = {
|
||
algorithm: config.algorithm || 'SHA1',
|
||
digits: Number(config.digits) || 6,
|
||
_forceRefresh: !!config._forceRefresh,
|
||
timestamp: config.timestamp || otp.getCurrentTimestamp()
|
||
};
|
||
|
||
if (config.type === 'otpauth') {
|
||
// 智能处理otpauth类型
|
||
if (!config.secret) {
|
||
throw new Error('otpauth类型必须提供secret参数');
|
||
}
|
||
if (!config.secret.startsWith('otpauth://')) {
|
||
// 如果不是otpauth URI格式,自动转换为TOTP处理
|
||
config.type = 'totp';
|
||
}
|
||
}
|
||
|
||
if (config.type === 'totp') {
|
||
options.period = Number(config.period) || 30;
|
||
|
||
// 如果提供了token对象且不需要强制刷新,验证时间窗口
|
||
if (config.token && !options._forceRefresh) {
|
||
if (verifyTokenWindow(config.token, options.timestamp, options.period)) {
|
||
return config.token.code; // 仍在同一窗口,返回缓存的code
|
||
}
|
||
}
|
||
} else if (config.type === 'hotp') {
|
||
if (config.counter === undefined) {
|
||
throw new Error('HOTP需要计数器值');
|
||
}
|
||
options.counter = Number(config.counter);
|
||
} else {
|
||
throw new Error('不支持的OTP类型');
|
||
}
|
||
|
||
return await otp.generate(config.type, config.secret, options);
|
||
} catch (error) {
|
||
console.error('[Error] Failed to generate code:', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 验证OTP码
|
||
* @param {string} token - 要验证的OTP码
|
||
* @param {Object} config - 配置对象(与generateCode相同)
|
||
* @returns {Promise<boolean>} 验证结果
|
||
*/
|
||
const verifyCode = async (token, config) => {
|
||
try {
|
||
const options = {
|
||
algorithm: config.algorithm || 'SHA1',
|
||
digits: Number(config.digits) || 6,
|
||
timestamp: otp.getCurrentTimestamp() // 使用统一的时间戳获取方法
|
||
};
|
||
|
||
if (config.type === 'otpauth') {
|
||
// otpauth URI由底层otp.verify处理
|
||
} else if (config.type === 'totp') {
|
||
options.period = Number(config.period) || 30;
|
||
} else if (config.type === 'hotp') {
|
||
if (config.counter === undefined) {
|
||
throw new Error('HOTP需要计数器值');
|
||
}
|
||
options.counter = Number(config.counter);
|
||
} else {
|
||
throw new Error('不支持的OTP类型');
|
||
}
|
||
|
||
return await otp.verify(token, config.type, config.secret, options);
|
||
} catch (error) {
|
||
console.error('[Error] Failed to verify code:', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 获取TOTP剩余秒数
|
||
* @param {number} [period=30] - TOTP周期(秒)
|
||
* @returns {number} 剩余秒数
|
||
*/
|
||
const getRemainingSeconds = (period = 30) => {
|
||
try {
|
||
// 使用otp模块的getRemainingSeconds,它已经使用了getCurrentTimestamp
|
||
return otp.getRemainingSeconds({ period });
|
||
} catch (error) {
|
||
console.error('[Error] Failed to get remaining seconds:', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
// ============ 令牌管理相关功能 ============
|
||
|
||
/**
|
||
* 添加新令牌
|
||
* @param {Object} tokenData - 令牌数据
|
||
* @returns {Promise<void>}
|
||
*/
|
||
const addToken = async (tokenData) => {
|
||
// 验证令牌数据
|
||
const errors = format.validateToken(tokenData);
|
||
if (errors) {
|
||
throw new Error(errors.join('; '));
|
||
}
|
||
|
||
// 生成唯一ID
|
||
const id = `token_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||
const now = formatTime(new Date());
|
||
const token = {
|
||
...tokenData,
|
||
id,
|
||
createTime: now,
|
||
lastUpdate: now,
|
||
code: '' // 初始化为空字符串
|
||
};
|
||
|
||
// 对于HOTP类型,添加counter字段
|
||
if (tokenData.type && tokenData.type.toUpperCase() === 'HOTP') {
|
||
token.counter = 0; // HOTP类型需要counter >= 0
|
||
}
|
||
// 对于TOTP类型,不设置counter字段,让它在JSON序列化时被忽略
|
||
|
||
// 验证是否可以生成代码
|
||
try {
|
||
await generateCode(token);
|
||
} catch (error) {
|
||
throw new Error('无效的令牌配置: ' + error.message);
|
||
}
|
||
|
||
// 保存令牌
|
||
await storage.addToken(token);
|
||
};
|
||
|
||
/**
|
||
* 从URL添加令牌
|
||
* @param {string} url - OTP URL
|
||
* @returns {Promise<void>}
|
||
*/
|
||
const addTokenFromUrl = async (url) => {
|
||
const tokenData = format.parseURL(url);
|
||
if (!tokenData) {
|
||
throw new Error('无效的OTP URL');
|
||
}
|
||
await addToken(tokenData);
|
||
};
|
||
|
||
/**
|
||
* 获取所有令牌
|
||
* @returns {Promise<Array>}
|
||
*/
|
||
const getTokens = async () => {
|
||
return await storage.getTokens();
|
||
};
|
||
|
||
/**
|
||
* 更新令牌
|
||
* @param {string} tokenId - 令牌ID
|
||
* @param {Object} updates - 更新的字段
|
||
* @returns {Promise<void>}
|
||
*/
|
||
const updateToken = async (tokenId, updates) => {
|
||
await storage.updateToken(tokenId, updates);
|
||
};
|
||
|
||
/**
|
||
* 删除令牌
|
||
* @param {string} tokenId - 令牌ID
|
||
* @returns {Promise<void>}
|
||
*/
|
||
const deleteToken = async (tokenId) => {
|
||
await storage.deleteToken(tokenId);
|
||
};
|
||
|
||
// ============ 格式化相关功能 ============
|
||
|
||
/**
|
||
* 格式化时间
|
||
* @param {Date} date - 日期对象
|
||
* @returns {string} 格式化后的时间字符串
|
||
*/
|
||
const formatTime = (date) => {
|
||
return format.formatTime(date);
|
||
};
|
||
|
||
/**
|
||
* 格式化日期
|
||
* @param {Date} date - 日期对象
|
||
* @returns {string} 格式化后的日期字符串
|
||
*/
|
||
const formatDate = (date) => {
|
||
return format.formatDate(date);
|
||
};
|
||
|
||
// ============ 云同步相关功能 ============
|
||
|
||
/**
|
||
* 同步令牌到云端
|
||
* @param {Array} tokens - 令牌列表
|
||
* @returns {Promise<Array>} 同步后的令牌列表
|
||
*/
|
||
const syncTokens = async (tokens) => {
|
||
try {
|
||
// 上传本地令牌到云端
|
||
await cloud.uploadTokens(tokens);
|
||
|
||
// 获取云端最新数据
|
||
const cloudData = await cloud.fetchLatestTokens();
|
||
|
||
// 使用cloud.js中的mergeTokens函数合并本地和云端数据
|
||
const mergedTokens = cloud.mergeTokens(tokens, cloudData.tokens, { preferCloud: true });
|
||
|
||
// 更新所有令牌的时间戳
|
||
for (const token of mergedTokens) {
|
||
token.timestamp = cloudData.timestamp;
|
||
}
|
||
|
||
return mergedTokens;
|
||
} catch (error) {
|
||
console.error('同步令牌失败:', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 从云端获取令牌
|
||
* @returns {Promise<Array>} 云端的令牌列表
|
||
*/
|
||
const getCloudTokens = async () => {
|
||
try {
|
||
const cloudData = await cloud.fetchLatestTokens();
|
||
return cloudData.tokens;
|
||
} catch (error) {
|
||
// 如果是404错误(云端无数据),不打印错误日志
|
||
if (error.statusCode !== 404) {
|
||
console.error('获取云端令牌失败:', error);
|
||
}
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
// ============ UI相关功能 ============
|
||
|
||
/**
|
||
* 显示加载提示
|
||
* @param {string} [title='加载中'] - 提示文字
|
||
*/
|
||
const showLoading = (title = '加载中') => {
|
||
ui.showLoading(title);
|
||
};
|
||
|
||
/**
|
||
* 隐藏加载提示
|
||
*/
|
||
const hideLoading = () => {
|
||
ui.hideLoading();
|
||
};
|
||
|
||
/**
|
||
* 显示提示信息
|
||
* @param {string} title - 提示文字
|
||
* @param {string} [icon='none'] - 图标类型
|
||
*/
|
||
const showToast = (title, icon = 'none') => {
|
||
ui.showToast(title, icon);
|
||
};
|
||
|
||
// ============ 加密相关功能 ============
|
||
|
||
/**
|
||
* Base32编码
|
||
* @param {string|Uint8Array} input - 输入数据
|
||
* @returns {string} Base32编码字符串
|
||
*/
|
||
const encodeBase32 = (input) => {
|
||
return base32.encode(input);
|
||
};
|
||
|
||
/**
|
||
* Base32解码
|
||
* @param {string} input - Base32编码字符串
|
||
* @returns {Uint8Array} 解码后的数据
|
||
*/
|
||
const decodeBase32 = (input) => {
|
||
return base32.decode(input);
|
||
};
|
||
|
||
/**
|
||
* 生成随机密钥
|
||
* @param {number} [length=20] - 密钥长度(字节)
|
||
* @returns {string} Base32编码的随机密钥
|
||
*/
|
||
const generateSecret = (length = 20) => {
|
||
return crypto.generateSecret(length);
|
||
};
|
||
|
||
// 导出所有功能
|
||
// 从format模块重新导出验证和解析函数
|
||
const { validateToken, parseURL } = require('./format');
|
||
|
||
module.exports = {
|
||
// OTP相关
|
||
generateCode,
|
||
verifyCode,
|
||
getRemainingSeconds,
|
||
|
||
// 令牌管理
|
||
addToken,
|
||
addTokenFromUrl,
|
||
getTokens,
|
||
updateToken,
|
||
deleteToken,
|
||
|
||
// 格式化
|
||
validateToken, // 添加验证函数
|
||
parseURL, // 添加解析函数
|
||
formatTime,
|
||
formatDate,
|
||
|
||
// 云同步
|
||
syncTokens,
|
||
getCloudTokens,
|
||
|
||
// UI
|
||
showLoading,
|
||
hideLoading,
|
||
showToast,
|
||
|
||
// 加密
|
||
encodeBase32,
|
||
decodeBase32,
|
||
generateSecret,
|
||
|
||
// 事件管理
|
||
eventManager
|
||
}; |