totp-native
Version:
High-performance TOTP library using native Web Crypto API
319 lines (318 loc) • 10.6 kB
JavaScript
/**
* TOTP Native - High-performance TOTP library using Web Crypto API
* RFC 6238 compliant implementation with TypeScript support
*/
export Totp = export TotpGenerator = export TotpError = void 0;
class TotpError extends Error {
constructor(message, code) {
super(message);
this.code = code;
this.name = 'TotpError';
}
}
export TotpError = TotpError;
/**
* Base32 alphabet as defined in RFC 4648
*/
const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
/**
* Decode Base32 string to Uint8Array
*/
function base32Decode(encoded) {
// Remove padding and convert to uppercase
const cleanInput = encoded.replace(/=+$/, '').toUpperCase();
let bits = '';
// Convert each character to 5-bit binary
for (let i = 0; i < cleanInput.length; i++) {
const char = cleanInput[i];
const index = BASE32_ALPHABET.indexOf(char);
if (index === -1) {
throw new TotpError(`Invalid Base32 character: ${char}`, 'INVALID_BASE32');
}
bits += index.toString(2).padStart(5, '0');
}
// Convert bits to bytes
const bytes = [];
for (let i = 0; i < bits.length; i += 8) {
const byte = bits.substring(i, i + 8);
if (byte.length === 8) {
bytes.push(parseInt(byte, 2));
}
}
return new Uint8Array(bytes);
}
/**
* Encode Uint8Array to Base32 string
*/
function base32Encode(data) {
let result = '';
let bits = '';
// Convert bytes to bits
for (const byte of data) {
bits += byte.toString(2).padStart(8, '0');
}
// Convert 5-bit groups to Base32 characters
for (let i = 0; i < bits.length; i += 5) {
const chunk = bits.substring(i, i + 5);
if (chunk.length === 5) {
result += BASE32_ALPHABET[parseInt(chunk, 2)];
}
}
return result;
}
/**
* Generate HMAC using Web Crypto API
*/
async function generateHmac(key, message, algorithm) {
const keyBuffer = new Uint8Array(key);
const cryptoKey = await crypto.subtle.importKey('raw', keyBuffer, { name: 'HMAC', hash: algorithm }, false, ['sign']);
const signature = await crypto.subtle.sign('HMAC', cryptoKey, message);
return new Uint8Array(signature);
}
/**
* Generate cryptographically secure random bytes
*/
function getRandomBytes(length) {
const bytes = new Uint8Array(length);
crypto.getRandomValues(bytes);
return bytes;
}
/**
* Convert number to 8-byte big-endian array
*/
function numberToBytes(num) {
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setUint32(4, num, false); // Big-endian, store in lower 32 bits
return buffer;
}
/**
* Validate TOTP configuration
*/
function validateConfig(config) {
if (!config.secret) {
throw new TotpError('Secret is required', 'MISSING_SECRET');
}
if (config.digits !== undefined && (config.digits < 4 || config.digits > 8)) {
throw new TotpError('Digits must be between 4 and 8', 'INVALID_DIGITS');
}
if (config.period !== undefined && config.period <= 0) {
throw new TotpError('Period must be greater than 0', 'INVALID_PERIOD');
}
if (config.skew !== undefined && config.skew < 0) {
throw new TotpError('Skew must be non-negative', 'INVALID_SKEW');
}
}
/**
* TOTP Generator class for repeated use with the same configuration
*/
class TotpGenerator {
constructor(config) {
validateConfig(config);
// Set defaults
this.config = {
secret: config.secret,
digits: config.digits ?? 6,
period: config.period ?? 30,
algorithm: config.algorithm ?? 'SHA1',
skew: config.skew ?? 1,
explicitZeroPad: config.explicitZeroPad ?? true,
timestamp: config.timestamp ?? Date.now()
};
}
/**
* Generate TOTP token for current time
*/
async generate() {
const timestamp = Math.floor(Date.now() / 1000);
return this.generateAt(timestamp);
}
/**
* Generate TOTP token for specific timestamp
*/
async generateAt(timestamp) {
try {
// Decode secret
const key = base32Decode(this.config.secret);
// Calculate time step
const timeStep = Math.floor(timestamp / this.config.period);
// Convert time step to 8-byte array
const timeBytes = numberToBytes(timeStep);
// Generate HMAC - map algorithm name for crypto.subtle
const algorithmMap = {
'SHA1': 'SHA-1',
'SHA256': 'SHA-256',
'SHA512': 'SHA-512'
};
const cryptoAlgorithm = algorithmMap[this.config.algorithm] || this.config.algorithm;
const hmac = await generateHmac(key, timeBytes, cryptoAlgorithm);
// Dynamic truncation (RFC 6238)
const offset = hmac[hmac.length - 1] & 0x0f;
const binary = ((hmac[offset] & 0x7f) << 24) |
((hmac[offset + 1] & 0xff) << 16) |
((hmac[offset + 2] & 0xff) << 8) |
(hmac[offset + 3] & 0xff);
// Generate OTP
const otp = binary % Math.pow(10, this.config.digits);
// Format with zero padding if enabled
return this.config.explicitZeroPad
? otp.toString().padStart(this.config.digits, '0')
: otp.toString();
}
catch (error) {
if (error instanceof TotpError) {
throw error;
}
throw new TotpError(`Failed to generate TOTP: ${error}`, 'GENERATION_ERROR');
}
}
/**
* Verify TOTP token against current time
*/
async verify(token) {
return this.verifyWithSkew(token, this.config.skew);
}
/**
* Verify TOTP token with custom skew tolerance
*/
async verifyWithSkew(token, skew) {
const timestamp = Math.floor(Date.now() / 1000);
const currentStep = Math.floor(timestamp / this.config.period);
// Check current time step and surrounding steps within skew
for (let i = 0; i <= skew; i++) {
const steps = i === 0 ? [currentStep] : [currentStep - i, currentStep + i];
for (const step of steps) {
if (step >= 0) { // Ensure we don't go negative
const testToken = await this.generateAt(step * this.config.period);
if (testToken === token) {
return true;
}
}
}
}
return false;
}
/**
* Generate Google Authenticator compatible URI
*/
generateUri(issuer, accountName) {
const params = new URLSearchParams({
secret: this.config.secret,
issuer: issuer,
algorithm: this.config.algorithm,
digits: this.config.digits.toString(),
period: this.config.period.toString()
});
const encodedIssuer = encodeURIComponent(issuer);
const encodedAccount = encodeURIComponent(accountName);
return `otpauth://totp/${encodedIssuer}:${encodedAccount}?${params.toString()}`;
}
/**
* Generate random Base32 secret
*/
static generateSecret(length = 32) {
const bytes = getRandomBytes(length);
return base32Encode(bytes);
}
/**
* Parse Google Authenticator URI
*/
static parseUri(uri) {
try {
const url = new URL(uri);
if (url.protocol !== 'otpauth:' || url.hostname !== 'totp') {
throw new TotpError('Invalid TOTP URI format', 'INVALID_URI');
}
const secret = url.searchParams.get('secret');
if (!secret) {
throw new TotpError('Missing secret in URI', 'MISSING_SECRET');
}
const config = { secret };
const digits = url.searchParams.get('digits');
if (digits)
config.digits = parseInt(digits, 10);
const period = url.searchParams.get('period');
if (period)
config.period = parseInt(period, 10);
const algorithm = url.searchParams.get('algorithm');
if (algorithm && ['SHA1', 'SHA256', 'SHA512'].includes(algorithm)) {
config.algorithm = algorithm;
}
return config;
}
catch (error) {
if (error instanceof TotpError) {
throw error;
}
throw new TotpError(`Failed to parse URI: ${error}`, 'PARSE_ERROR');
}
}
}
export TotpGenerator = TotpGenerator;
/**
* Static utility class for one-off TOTP operations
*/
class Totp {
/**
* Generate TOTP token directly from secret
*/
static async generate(secret, options = {}) {
const generator = new TotpGenerator({ secret, ...options });
return generator.generate();
}
/**
* Generate TOTP token for specific timestamp
*/
static async generateAt(secret, timestamp, options = {}) {
const generator = new TotpGenerator({ secret, ...options });
return generator.generateAt(timestamp);
}
/**
* Verify TOTP token directly
*/
static async verify(secret, token, options = {}) {
const generator = new TotpGenerator({ secret, ...options });
return generator.verify(token);
}
/**
* Verify TOTP token with custom skew
*/
static async verifyWithSkew(secret, token, skew, options = {}) {
const generator = new TotpGenerator({ secret, ...options });
return generator.verifyWithSkew(token, skew);
}
/**
* Generate random secret
*/
static generateSecret(length = 32) {
return TotpGenerator.generateSecret(length);
}
/**
* Parse URI to config
*/
static parseUri(uri) {
return TotpGenerator.parseUri(uri);
}
/**
* Create URI from config
*/
static createUri(secret, issuer, accountName, options = {}) {
const generator = new TotpGenerator({ secret, ...options });
return generator.generateUri(issuer, accountName);
}
/**
* Get remaining time in current period
*/
static getRemainingTime(period = 30) {
const now = Math.floor(Date.now() / 1000);
return period - (now % period);
}
}
export Totp = Totp;
// Export everything
export default = {
TotpGenerator,
Totp,
TotpError
};
//# sourceMappingURL=index.js.map