Package Exports
- @chrisfh/secure-otp
Readme
@chrisfh/secure-otp
A modern, 2025-compliant MFA/OTP library implementing TOTP (RFC 6238) with advanced security features.
๐ Features
Core Security (2025 Standards)
- โ Modern Cryptography: HMAC-SHA512 and HMAC-SHA256 (no SHA1)
- โ Strong Entropy: Minimum 160-bit (20-byte) secrets
- โ Encrypted Storage: AES-256-GCM encryption for secrets using WebCrypto API
- โ
Timing Attack Protection: Constant-time comparison using
crypto.timingSafeEqual - โ Rate Limiting: Built-in protection against brute-force attacks
TOTP Features
- โ 6-digit and 8-digit code support
- โ 30-second time step (configurable)
- โ ยฑ1 time-step drift tolerance (configurable)
- โ RFC 6238 compliant
Developer Experience
- โ Full TypeScript support with type definitions
- โ ESM-only (modern Node.js)
- โ Comprehensive test coverage
- โ QR code generation for easy provisioning
- โ Compatible with Google Authenticator, Authy, and other TOTP apps
๐ฆ Installation
npm install @chrisfh/secure-otpRequirements:
- Node.js >= 20.0.0 (for WebCrypto API support)
๐ Quick Start
import {
generateSecret,
generateTOTP,
verifyTOTP,
generateQRCodeForSecret
} from '@chrisfh/secure-otp';
// 1. Generate a secret for a new user
const secret = generateSecret();
// 2. Generate a QR code for user to scan
const { url, qrCode } = await generateQRCodeForSecret(secret, {
account: 'user@example.com',
issuer: 'MyApp'
});
// Display the QR code to user (qrCode is a Data URL)
console.log('Scan this QR code:', qrCode);
// 3. Verify user's OTP code during login
const userCode = '123456'; // Code from user's authenticator app
const isValid = await verifyTOTP(userCode, secret);
if (isValid) {
console.log('โ Authentication successful!');
} else {
console.log('โ Invalid code');
}๐ API Documentation
Secret Generation
generateSecret(options?)
Generate a cryptographically secure random secret.
import { generateSecret } from '@chrisfh/secure-otp';
// Default: 20 bytes (160 bits)
const secret = generateSecret();
// Custom length (minimum 20 bytes enforced)
const longSecret = generateSecret({ length: 32 });Options:
length?: number- Length in bytes (default: 20, minimum: 20)
Returns: string - Base32-encoded secret
TOTP Generation
generateTOTP(secret, options?)
Generate a TOTP code for the current time.
import { generateTOTP } from '@chrisfh/secure-otp';
// Generate 6-digit code with SHA-512
const code = await generateTOTP(secret);
// Generate 8-digit code with SHA-256
const code8 = await generateTOTP(secret, {
digits: 8,
algorithm: 'SHA-256'
});Options:
period?: number- Time step in seconds (default: 30)digits?: 6 | 8- Number of digits (default: 6)algorithm?: 'SHA-256' | 'SHA-512'- HMAC algorithm (default: 'SHA-512')
Returns: Promise<string> - Zero-padded OTP code
TOTP Verification
verifyTOTP(token, secret, options?)
Verify a TOTP code with time drift tolerance.
import { verifyTOTP } from '@chrisfh/secure-otp';
const userCode = '123456';
const isValid = await verifyTOTP(userCode, secret, {
window: 1, // Allow ยฑ1 time step (ยฑ30 seconds)
algorithm: 'SHA-512'
});Options:
period?: number- Time step in seconds (default: 30)digits?: 6 | 8- Number of digits (default: 6)algorithm?: 'SHA-256' | 'SHA-512'- HMAC algorithm (default: 'SHA-512')window?: number- Drift tolerance in time steps (default: 1)
Returns: Promise<boolean> - true if valid, false otherwise
QR Code Generation
generateQRCodeForSecret(secret, options)
Generate an otpauth:// URL and QR code for easy provisioning.
import { generateQRCodeForSecret } from '@chrisfh/secure-otp';
const { url, qrCode } = await generateQRCodeForSecret(secret, {
account: 'user@example.com',
issuer: 'MyApp',
digits: 6,
algorithm: 'SHA512'
});
// Display QR code to user
console.log('OTPAuth URL:', url);
console.log('QR Code Data URL:', qrCode);Options:
account: string- User's email or username (required)issuer: string- App or company name (required)period?: number- Time step in seconds (default: 30)digits?: 6 | 8- Number of digits (default: 6)algorithm?: 'SHA256' | 'SHA512'- Algorithm name (default: 'SHA512')
Returns: Promise<{ url: string, qrCode: string }> - URL and Data URL for QR code
Secure Storage
encryptSecret(secret, options)
Encrypt a secret using AES-256-GCM.
import { encryptSecret, generateMasterKey } from '@chrisfh/secure-otp';
const masterKey = generateMasterKey();
const encrypted = await encryptSecret(secret, { masterKey });
// Store encrypted.ciphertext, encrypted.iv, and encrypted.authTag in databaseOptions:
masterKey: Buffer- 32-byte encryption key (required)
Returns: Promise<EncryptedSecret> - Object with ciphertext, iv, and authTag
decryptSecret(encrypted, options)
Decrypt a previously encrypted secret.
import { decryptSecret } from '@chrisfh/secure-otp';
const decrypted = await decryptSecret(encrypted, { masterKey });
// Use decrypted secret for TOTP operationsOptions:
masterKey: Buffer- Same 32-byte key used for encryption (required)
Returns: Promise<string> - Decrypted Base32-encoded secret
generateMasterKey()
Generate a secure 32-byte master key for encryption.
import { generateMasterKey } from '@chrisfh/secure-otp';
const masterKey = generateMasterKey();
// Store this securely (e.g., environment variable, key management service)Returns: Buffer - 32-byte master key
Rate Limiting
RateLimiter
Prevent brute-force attacks with built-in rate limiting.
import { RateLimiter, createDefaultRateLimiter } from '@chrisfh/secure-otp';
// Default: 5 attempts per minute
const rateLimiter = createDefaultRateLimiter();
// Or custom configuration
const customLimiter = new RateLimiter({
maxAttempts: 3,
windowMs: 60000 // 1 minute
});
// Check if request is allowed
const userId = 'user@example.com';
if (rateLimiter.check(userId)) {
// Verify OTP
const isValid = await verifyTOTP(code, secret);
if (isValid) {
rateLimiter.reset(userId); // Reset on success
}
} else {
console.log('Rate limit exceeded');
}Methods:
check(identifier: string): boolean- Check if request is allowedreset(identifier: string): void- Reset rate limit for an identifiergetRemainingAttempts(identifier: string): number- Get remaining attemptscleanup(): void- Remove expired rate limit states
๐ Security Best Practices
1. Secret Storage
Never store secrets in plain text! Always encrypt them:
import { generateSecret, encryptSecret, generateMasterKey } from '@chrisfh/secure-otp';
// Generate and encrypt
const secret = generateSecret({ length: 32 }); // 256 bits
const masterKey = generateMasterKey(); // Store in environment variable
const encrypted = await encryptSecret(secret, { masterKey });
// Store encrypted.ciphertext, encrypted.iv, encrypted.authTag in database2. Rate Limiting
Always implement rate limiting to prevent brute-force attacks:
import { createDefaultRateLimiter, verifyTOTP } from '@chrisfh/secure-otp';
const rateLimiter = createDefaultRateLimiter();
app.post('/verify-otp', async (req, res) => {
const { userId, code } = req.body;
if (!rateLimiter.check(userId)) {
return res.status(429).json({ error: 'Too many attempts' });
}
const isValid = await verifyTOTP(code, secret);
if (isValid) {
rateLimiter.reset(userId);
return res.json({ success: true });
} else {
return res.status(401).json({ error: 'Invalid code' });
}
});3. Use Strong Algorithms
Always use SHA-256 or SHA-512 (never SHA-1):
// โ Good
await generateTOTP(secret, { algorithm: 'SHA-512' });
// โ Bad (not supported)
// await generateTOTP(secret, { algorithm: 'SHA-1' });4. Time Drift Tolerance
Use a reasonable window (default is ยฑ1 time step = ยฑ30 seconds):
// โ Balanced security and usability
await verifyTOTP(code, secret, { window: 1 });
// โ Less secure but more tolerant
await verifyTOTP(code, secret, { window: 2 });
// โ More secure but less tolerant
await verifyTOTP(code, secret, { window: 0 });๐งช Testing
Run the test suite:
npm testRun tests with coverage:
npm run test -- --coverage๐ Examples
See the examples/demo.js file for complete usage examples including:
- New user setup with QR code
- Login verification with rate limiting
- Secure secret encryption/decryption
- 8-digit codes with SHA-256
- Enterprise scenarios
Run the demo:
npm run build
node examples/demo.js๐๏ธ Architecture
This library uses:
- Node.js WebCrypto API (
crypto.subtle) for HMAC and AES-GCM - Base32 encoding (RFC 4648) for secret representation
- TOTP algorithm (RFC 6238) with modern hash functions
- Constant-time comparison to prevent timing attacks
๐ License
MIT License - see LICENSE file for details.
๐ค Contributing
Contributions are welcome! Please ensure:
- All tests pass
- Code coverage remains high
- TypeScript types are properly defined
- Security best practices are followed
๐ Resources
โ ๏ธ Changelog
1.0.0 (2025)
- Initial release
- TOTP with SHA-256 and SHA-512
- AES-256-GCM encryption
- QR code generation
- Rate limiting
- Full TypeScript support
- Comprehensive test coverage
Built with โค๏ธ for security in 2025