This commit is contained in:
“xHuPo” 2025-06-09 13:35:15 +08:00
commit 2b8870a40e
51 changed files with 5845 additions and 0 deletions

150
utils/otp.js Normal file
View file

@ -0,0 +1,150 @@
/**
* OTP (One-Time Password) 工具集
* 包含TOTP和HOTP的实现以及相关辅助功能
*/
const { generateTOTP: baseTOTP, getRemainingSeconds: baseGetRemainingSeconds } = require('./totp');
const { generateHOTP } = require('./hotp');
const { parseURL } = require('./format');
/**
* 获取当前时间戳
* 确保在同一时间窗口内使用相同的时间戳
* @returns {number} 当前时间戳
*/
function getCurrentTimestamp() {
return Math.floor(Date.now() / 1000);
}
/**
* 生成OTP值
*
* @param {string} type - OTP类型'totp''hotp'
* @param {string} secret - Base32编码的密钥
* @param {Object} options - 配置选项
* @param {number} [options.counter] - 计数器值仅HOTP需要
* @param {number} [options.timestamp] - 用于TOTP的时间戳
* @param {boolean} [options._forceRefresh] - 是否强制刷新不使用缓存
* @returns {Promise<string>} 生成的OTP值
* @throws {Error} 参数无效时抛出错误
*/
async function generateOTP(type, secret, options = {}) {
// 处理otpauth URI
if (type === 'otpauth') {
const parsed = parseURL(secret);
if (!parsed) {
throw new Error('Invalid otpauth URI format');
}
// 使用解析出的类型和参数
return await generateOTP(parsed.type, parsed.secret, {
...options,
...(parsed.type === 'totp' ? { period: parsed.period } : {}),
...(parsed.type === 'hotp' ? { counter: parsed.counter } : {}),
algorithm: parsed.algo,
digits: parsed.digits
});
}
if (type === 'totp') {
const totpOptions = {
...options,
timestamp: options.timestamp || getCurrentTimestamp(),
_forceRefresh: !!options._forceRefresh
};
return await baseTOTP(secret, totpOptions);
} else if (type === 'hotp') {
if (options.counter === undefined) {
throw new Error('Counter is required for HOTP');
}
return await generateHOTP(secret, options.counter, options);
} else {
throw new Error(`Unsupported OTP type: ${type}`);
}
}
/**
* 验证OTP值
*
* @param {string} token - 要验证的OTP值
* @param {string} type - OTP类型'totp''hotp'
* @param {string} secret - Base32编码的密钥
* @param {Object} options - 配置选项
* @param {number} [options.counter] - 当前计数器值仅HOTP需要
* @returns {Promise<boolean>} 验证结果
* @throws {Error} 参数无效时抛出错误
*/
async function verifyOTP(token, type, secret, options = {}) {
// 处理otpauth URI
if (type === 'otpauth') {
const parsed = parseURL(secret);
if (!parsed) {
throw new Error('Invalid otpauth URI format');
}
// 使用解析出的类型和参数
return await verifyOTP(token, parsed.type, parsed.secret, {
...options,
...(parsed.type === 'totp' ? { period: parsed.period } : {}),
...(parsed.type === 'hotp' ? { counter: parsed.counter } : {}),
algorithm: parsed.algo,
digits: parsed.digits
});
}
if (type === 'totp') {
// 始终使用基础版本TOTP确保一致性
const generatedToken = await baseTOTP(secret, {
...options,
timestamp: options.timestamp || getCurrentTimestamp()
});
return generatedToken === token;
} else if (type === 'hotp') {
if (options.counter === undefined) {
throw new Error('Counter is required for HOTP');
}
const generatedToken = await generateHOTP(secret, options.counter, options);
return generatedToken === token;
} else {
throw new Error(`Unsupported OTP type: ${type}`);
}
}
/**
* 获取TOTP剩余秒数
* @param {Object} options - 配置选项
* @param {number} [options.period=30] - TOTP周期
* @param {number} [options.timestamp] - 指定时间戳
* @returns {number} 剩余秒数
*/
function getRemainingSeconds(options = {}) {
// 始终使用基础版本,确保一致性
// 确保传递正确的参数
const period = options.period || 30;
const timestamp = options.timestamp || getCurrentTimestamp();
const remaining = baseGetRemainingSeconds({
...options,
period,
timestamp
});
return remaining;
}
// 导出统一接口
module.exports = {
now: async (type, secret, counter) => {
if (type === 'totp') {
// 始终使用基础版本TOTP确保一致性
return await baseTOTP(secret, {
timestamp: getCurrentTimestamp(),
_forceRefresh: true
});
} else if (type === 'hotp') {
return await generateHOTP(secret, counter);
} else {
throw new Error(`Unsupported OTP type: ${type}`);
}
},
generate: generateOTP,
verify: verifyOTP,
getRemainingSeconds,
getCurrentTimestamp
};