secure-2fa
Version:
A secure, developer-friendly Node.js package for email-based OTP (2FA) with strong security controls
115 lines • 3.88 kB
JavaScript
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
;