150 lines
No EOL
4.6 KiB
JavaScript
150 lines
No EOL
4.6 KiB
JavaScript
/**
|
||
* 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
|
||
}; |