## Major Enhancements ### 🏗️ Architecture & Infrastructure - Implement comprehensive Docker testing infrastructure with hermetic environment - Add Forgejo Actions CI/CD pipeline for automated deployments - Create Page Object Model (POM) testing architecture reducing test duplication by 90% - Establish security-first development patterns with input validation and output escaping ### 🧪 Testing Framework Modernization - Migrate 146+ tests from 80 duplicate files to centralized architecture - Add comprehensive E2E test suites for all user roles and workflows - Implement WordPress error detection with automatic site health monitoring - Create robust browser lifecycle management with proper cleanup ### 📚 Documentation & Guides - Add comprehensive development best practices guide - Create detailed administrator setup documentation - Establish user guides for trainers and master trainers - Document security incident reports and migration guides ### 🔧 Core Plugin Features - Enhance trainer profile management with certification system - Improve find trainer functionality with advanced filtering - Strengthen master trainer area with content management - Add comprehensive venue and organizer management ### 🛡️ Security & Reliability - Implement security-first patterns throughout codebase - Add comprehensive input validation and output escaping - Create secure credential management system - Establish proper WordPress role-based access control ### 🎯 WordPress Integration - Strengthen singleton pattern implementation across all classes - Enhance template hierarchy with proper WordPress integration - Improve page manager with hierarchical URL structure - Add comprehensive shortcode and menu system ### 🔍 Developer Experience - Add extensive debugging and troubleshooting tools - Create comprehensive test data seeding scripts - Implement proper error handling and logging - Establish consistent code patterns and standards ### 📊 Performance & Optimization - Optimize database queries and caching strategies - Improve asset loading and script management - Enhance template rendering performance - Streamline user experience across all interfaces 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
375 lines
No EOL
12 KiB
JavaScript
375 lines
No EOL
12 KiB
JavaScript
/**
|
|
* HVAC Testing Framework - Secure Credential Management
|
|
*
|
|
* Provides secure credential management with encryption, environment variable support,
|
|
* and WordPress role-based authentication for the HVAC testing framework.
|
|
*
|
|
* Security Features:
|
|
* - AES-256-GCM encryption for credential storage
|
|
* - Environment variable-based configuration
|
|
* - Session integrity verification
|
|
* - Automatic credential rotation support
|
|
* - Secure memory handling
|
|
*
|
|
* @author Claude Code - Emergency Security Response
|
|
* @version 1.0.0
|
|
* @security CRITICAL - Handles production authentication credentials
|
|
*/
|
|
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
require('dotenv').config();
|
|
|
|
class SecureCredentialManager {
|
|
constructor() {
|
|
this.encryptionKey = this.getEncryptionKey();
|
|
this.credentials = new Map();
|
|
this.sessionData = new Map();
|
|
this.auditLog = [];
|
|
|
|
// Initialize audit logging
|
|
this.auditLogger = this.createAuditLogger();
|
|
|
|
// Validate environment configuration
|
|
this.validateEnvironment();
|
|
}
|
|
|
|
/**
|
|
* Get or generate encryption key for credential storage
|
|
* @returns {Buffer} 256-bit encryption key
|
|
*/
|
|
getEncryptionKey() {
|
|
const keyHex = process.env.SESSION_ENCRYPTION_KEY;
|
|
if (!keyHex) {
|
|
throw new Error('SESSION_ENCRYPTION_KEY environment variable not set. Generate with: openssl rand -hex 32');
|
|
}
|
|
|
|
if (keyHex.length !== 64) { // 32 bytes = 64 hex characters
|
|
throw new Error('SESSION_ENCRYPTION_KEY must be 64 hex characters (32 bytes)');
|
|
}
|
|
|
|
return Buffer.from(keyHex, 'hex');
|
|
}
|
|
|
|
/**
|
|
* Validate required environment variables are present
|
|
*/
|
|
validateEnvironment() {
|
|
const required = [
|
|
'STAGING_BASE_URL',
|
|
'MASTER_TRAINER_USERNAME',
|
|
'MASTER_TRAINER_PASSWORD',
|
|
'REGULAR_TRAINER_USERNAME',
|
|
'REGULAR_TRAINER_PASSWORD',
|
|
'SESSION_ENCRYPTION_KEY',
|
|
'JWT_SECRET'
|
|
];
|
|
|
|
const missing = required.filter(key => !process.env[key]);
|
|
if (missing.length > 0) {
|
|
throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
|
|
}
|
|
|
|
this.auditLog.push({
|
|
timestamp: new Date().toISOString(),
|
|
event: 'ENVIRONMENT_VALIDATED',
|
|
details: 'All required environment variables present'
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get credentials for a specific user role
|
|
* @param {string} role - User role (master_trainer, master_trainer_alt, regular_trainer, admin)
|
|
* @returns {Object} Encrypted credential object
|
|
*/
|
|
getCredentials(role) {
|
|
const credentialMap = {
|
|
master_trainer: {
|
|
username: process.env.MASTER_TRAINER_USERNAME,
|
|
password: process.env.MASTER_TRAINER_PASSWORD,
|
|
email: process.env.MASTER_TRAINER_EMAIL,
|
|
role: 'hvac_master_trainer'
|
|
},
|
|
master_trainer_alt: {
|
|
username: process.env.MASTER_TRAINER_ALT_USERNAME,
|
|
password: process.env.MASTER_TRAINER_ALT_PASSWORD,
|
|
email: process.env.MASTER_TRAINER_ALT_EMAIL,
|
|
role: 'hvac_master_trainer'
|
|
},
|
|
regular_trainer: {
|
|
username: process.env.REGULAR_TRAINER_USERNAME,
|
|
password: process.env.REGULAR_TRAINER_PASSWORD,
|
|
email: process.env.REGULAR_TRAINER_EMAIL,
|
|
role: 'hvac_trainer'
|
|
},
|
|
admin: {
|
|
username: process.env.ADMIN_USERNAME,
|
|
password: process.env.ADMIN_PASSWORD,
|
|
email: process.env.ADMIN_EMAIL,
|
|
role: 'administrator'
|
|
}
|
|
};
|
|
|
|
const credentials = credentialMap[role];
|
|
if (!credentials) {
|
|
throw new Error(`Invalid role: ${role}`);
|
|
}
|
|
|
|
if (!credentials.username || !credentials.password) {
|
|
throw new Error(`Incomplete credentials for role: ${role}`);
|
|
}
|
|
|
|
this.auditLog.push({
|
|
timestamp: new Date().toISOString(),
|
|
event: 'CREDENTIALS_ACCESSED',
|
|
role: role,
|
|
username: credentials.username
|
|
});
|
|
|
|
return this.encryptCredentials(credentials);
|
|
}
|
|
|
|
/**
|
|
* Encrypt credentials using AES-256-GCM
|
|
* @param {Object} credentials - Plain text credentials
|
|
* @returns {Object} Encrypted credentials with metadata
|
|
*/
|
|
encryptCredentials(credentials) {
|
|
const iv = crypto.randomBytes(16);
|
|
const cipher = crypto.createCipher('aes-256-gcm', this.encryptionKey, iv);
|
|
|
|
const plaintext = JSON.stringify(credentials);
|
|
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
|
|
encrypted += cipher.final('hex');
|
|
|
|
const authTag = cipher.getAuthTag();
|
|
|
|
return {
|
|
encrypted,
|
|
iv: iv.toString('hex'),
|
|
authTag: authTag.toString('hex'),
|
|
algorithm: 'aes-256-gcm',
|
|
created: new Date().toISOString()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Decrypt credentials
|
|
* @param {Object} encryptedData - Encrypted credential object
|
|
* @returns {Object} Plain text credentials
|
|
*/
|
|
decryptCredentials(encryptedData) {
|
|
try {
|
|
const decipher = crypto.createDecipher(
|
|
'aes-256-gcm',
|
|
this.encryptionKey,
|
|
Buffer.from(encryptedData.iv, 'hex')
|
|
);
|
|
|
|
decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));
|
|
|
|
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
|
|
decrypted += decipher.final('utf8');
|
|
|
|
return JSON.parse(decrypted);
|
|
} catch (error) {
|
|
this.auditLog.push({
|
|
timestamp: new Date().toISOString(),
|
|
event: 'DECRYPTION_FAILED',
|
|
error: error.message
|
|
});
|
|
throw new Error('Failed to decrypt credentials');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create authenticated session with integrity verification
|
|
* @param {string} role - User role
|
|
* @returns {Object} Session object with encrypted credentials and metadata
|
|
*/
|
|
createSecureSession(role) {
|
|
const sessionId = crypto.randomUUID();
|
|
const credentials = this.getCredentials(role);
|
|
|
|
const session = {
|
|
sessionId,
|
|
role,
|
|
credentials,
|
|
created: new Date().toISOString(),
|
|
expires: new Date(Date.now() + (24 * 60 * 60 * 1000)).toISOString(), // 24 hours
|
|
integrity: this.generateSessionIntegrityHash(sessionId, role, credentials)
|
|
};
|
|
|
|
this.sessionData.set(sessionId, session);
|
|
|
|
this.auditLog.push({
|
|
timestamp: new Date().toISOString(),
|
|
event: 'SESSION_CREATED',
|
|
sessionId,
|
|
role,
|
|
expires: session.expires
|
|
});
|
|
|
|
return session;
|
|
}
|
|
|
|
/**
|
|
* Verify session integrity and return decrypted credentials
|
|
* @param {string} sessionId - Session identifier
|
|
* @returns {Object} Decrypted credentials if session is valid
|
|
*/
|
|
getSessionCredentials(sessionId) {
|
|
const session = this.sessionData.get(sessionId);
|
|
if (!session) {
|
|
throw new Error('Session not found');
|
|
}
|
|
|
|
// Check expiration
|
|
if (new Date() > new Date(session.expires)) {
|
|
this.sessionData.delete(sessionId);
|
|
throw new Error('Session expired');
|
|
}
|
|
|
|
// Verify integrity
|
|
const expectedHash = this.generateSessionIntegrityHash(
|
|
sessionId,
|
|
session.role,
|
|
session.credentials
|
|
);
|
|
|
|
if (session.integrity !== expectedHash) {
|
|
this.sessionData.delete(sessionId);
|
|
this.auditLog.push({
|
|
timestamp: new Date().toISOString(),
|
|
event: 'SESSION_INTEGRITY_VIOLATION',
|
|
sessionId
|
|
});
|
|
throw new Error('Session integrity violation detected');
|
|
}
|
|
|
|
return this.decryptCredentials(session.credentials);
|
|
}
|
|
|
|
/**
|
|
* Generate session integrity hash
|
|
* @param {string} sessionId
|
|
* @param {string} role
|
|
* @param {Object} encryptedCredentials
|
|
* @returns {string} HMAC-SHA256 hash
|
|
*/
|
|
generateSessionIntegrityHash(sessionId, role, encryptedCredentials) {
|
|
const data = JSON.stringify({ sessionId, role, encryptedCredentials });
|
|
return crypto.createHmac('sha256', this.encryptionKey)
|
|
.update(data)
|
|
.digest('hex');
|
|
}
|
|
|
|
/**
|
|
* Destroy session securely
|
|
* @param {string} sessionId
|
|
*/
|
|
destroySession(sessionId) {
|
|
if (this.sessionData.delete(sessionId)) {
|
|
this.auditLog.push({
|
|
timestamp: new Date().toISOString(),
|
|
event: 'SESSION_DESTROYED',
|
|
sessionId
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get base URL with SSL/TLS validation
|
|
* @returns {string} Validated base URL
|
|
*/
|
|
getBaseUrl() {
|
|
const baseUrl = process.env.STAGING_BASE_URL;
|
|
if (!baseUrl) {
|
|
throw new Error('STAGING_BASE_URL environment variable not set');
|
|
}
|
|
|
|
// Ensure HTTPS for production environments
|
|
if (!baseUrl.startsWith('https://')) {
|
|
throw new Error('Base URL must use HTTPS for security');
|
|
}
|
|
|
|
return baseUrl;
|
|
}
|
|
|
|
/**
|
|
* Get TLS validation mode
|
|
* @returns {string} Validation mode (strict|permissive)
|
|
*/
|
|
getTLSValidationMode() {
|
|
return process.env.TLS_VALIDATION_MODE || 'strict';
|
|
}
|
|
|
|
/**
|
|
* Create audit logger for security events
|
|
* @returns {Function} Audit logging function
|
|
*/
|
|
createAuditLogger() {
|
|
return async (event, details = {}) => {
|
|
const auditEntry = {
|
|
timestamp: new Date().toISOString(),
|
|
event,
|
|
...details
|
|
};
|
|
|
|
this.auditLog.push(auditEntry);
|
|
|
|
// Write to file if enabled
|
|
if (process.env.ENABLE_SECURITY_AUDIT === 'true') {
|
|
try {
|
|
const logFile = process.env.AUDIT_LOG_FILE || './security-audit.log';
|
|
await fs.appendFile(logFile, JSON.stringify(auditEntry) + '\n');
|
|
} catch (error) {
|
|
console.warn('Failed to write security audit log:', error.message);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Export audit log for security review
|
|
* @returns {Array} Complete audit log
|
|
*/
|
|
getAuditLog() {
|
|
return [...this.auditLog];
|
|
}
|
|
|
|
/**
|
|
* Clear sensitive data from memory (call on shutdown)
|
|
*/
|
|
secureCleanup() {
|
|
this.credentials.clear();
|
|
this.sessionData.clear();
|
|
|
|
// Overwrite sensitive data
|
|
if (this.encryptionKey) {
|
|
this.encryptionKey.fill(0);
|
|
}
|
|
|
|
this.auditLogger('SECURE_CLEANUP_COMPLETED');
|
|
}
|
|
}
|
|
|
|
// Singleton instance for global use
|
|
let credentialManagerInstance = null;
|
|
|
|
/**
|
|
* Get singleton instance of SecureCredentialManager
|
|
* @returns {SecureCredentialManager}
|
|
*/
|
|
function getCredentialManager() {
|
|
if (!credentialManagerInstance) {
|
|
credentialManagerInstance = new SecureCredentialManager();
|
|
}
|
|
return credentialManagerInstance;
|
|
}
|
|
|
|
module.exports = {
|
|
SecureCredentialManager,
|
|
getCredentialManager
|
|
}; |