otp/utils/otp.js
2025-06-17 14:44:48 +08:00

150 lines
No EOL
4.6 KiB
JavaScript
Raw 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.

/**
* 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.algorithm,
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.algorithm,
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
};