UNPKG

secure-2fa

Version:

A secure, developer-friendly Node.js package for email-based OTP (2FA) with strong security controls

115 lines 3.88 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.OtpGenerator = void 0; const crypto_1 = require("crypto"); const bcrypt_1 = __importDefault(require("bcrypt")); class OtpGenerator { constructor(serverSecret) { if (!serverSecret || serverSecret.length < 32) { throw new Error('Server secret must be at least 32 characters long'); } this.serverSecret = serverSecret; } /** * Generate a secure OTP with specified length */ generateOtp(length = 6) { if (length < 4 || length > 10) { throw new Error('OTP length must be between 4 and 10 digits'); } // Generate cryptographically secure random bytes const bytes = (0, crypto_1.randomBytes)(length); let otp = ''; // Convert to numeric OTP for (let i = 0; i < length; i++) { otp += (bytes[i] % 10).toString(); } return otp; } /** * Create HMAC for OTP to prevent tampering */ createHmac(otp, context, sessionId) { const data = `${otp}:${context}:${sessionId}`; const hmac = (0, crypto_1.createHmac)('sha256', this.serverSecret); hmac.update(data); return hmac.digest('hex'); } /** * Verify HMAC for OTP using constant-time comparison */ verifyHmac(otp, context, sessionId, expectedHmac) { const calculatedHmac = this.createHmac(otp, context, sessionId); // Use constant-time comparison to prevent timing attacks if (calculatedHmac.length !== expectedHmac.length) { return false; } let result = 0; for (let i = 0; i < calculatedHmac.length; i++) { result |= calculatedHmac.charCodeAt(i) ^ expectedHmac.charCodeAt(i); } return result === 0; } /** * Hash OTP for secure storage (using bcrypt) */ async hashOtp(otp) { const saltRounds = 12; return bcrypt_1.default.hash(otp, saltRounds); } /** * Verify OTP hash */ async verifyOtpHash(otp, hash) { // Validate inputs are strings if (typeof otp !== 'string' || typeof hash !== 'string') { throw new Error('OTP and hash must be strings'); } // Validate inputs are not empty if (!otp || !hash) { throw new Error('OTP and hash cannot be empty'); } return bcrypt_1.default.compare(otp, hash); } /** * Generate session ID (UUID v4 with timestamp) */ generateSessionId() { const bytes = (0, crypto_1.randomBytes)(16); // Set version (4) and variant bits bytes[6] = (bytes[6] & 0x0f) | 0x40; bytes[8] = (bytes[8] & 0x3f) | 0x80; const hex = bytes.toString('hex'); const uuid = [ hex.slice(0, 8), hex.slice(8, 12), hex.slice(12, 16), hex.slice(16, 20), hex.slice(20, 32) ].join('-'); // Add timestamp to make it even more unique const timestamp = Date.now().toString(36); return `${uuid}-${timestamp}`; } /** * Create a hash of request metadata for context binding */ hashRequestMeta(requestMeta) { const data = JSON.stringify(requestMeta); const hash = (0, crypto_1.createHash)('sha256'); hash.update(data); return hash.digest('hex'); } /** * Verify request metadata hash */ verifyRequestMeta(requestMeta, expectedHash) { const calculatedHash = this.hashRequestMeta(requestMeta); return calculatedHash === expectedHash; } } exports.OtpGenerator = OtpGenerator; //# sourceMappingURL=otp-generator.js.map