UNPKG

signalk-mosquitto

Version:

SignalK plugin for managing Mosquitto MQTT broker with bridge connections and security

557 lines 23.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.SecurityManagerImpl = void 0; const file_utils_1 = require("../utils/file-utils"); const validation_1 = require("../utils/validation"); const crypto = __importStar(require("crypto")); const forge = __importStar(require("node-forge")); const path = __importStar(require("path")); class SecurityManagerImpl { constructor(app, config) { this.app = app; this.config = config; this.dataDir = file_utils_1.FileUtils.getDataDir('signalk-mosquitto'); this.configDir = path.join(this.dataDir, 'config'); this.passwordFile = path.join(this.configDir, 'passwd'); this.aclFile = path.join(this.configDir, 'acl'); this.usersFile = path.join(this.dataDir, 'users.json'); this.aclsFile = path.join(this.dataDir, 'acls.json'); this.certsDir = path.join(this.dataDir, 'certs'); } async addUser(user) { try { const validation = validation_1.ValidationUtils.validateUser(user); if (validation.length > 0) { throw new Error(`User validation failed: ${validation.join(', ')}`); } const existingUsers = await this.getUsers(); const existingUser = existingUsers.find(u => u.username === user.username); if (existingUser) { throw new Error(`User '${user.username}' already exists`); } const hashedPassword = await this.hashPassword(user.password); const userWithHashedPassword = { ...user, password: hashedPassword }; existingUsers.push(userWithHashedPassword); await this.saveUsers(existingUsers); await this.generatePasswordFile(existingUsers); console.log(`User '${user.username}' added successfully`); } catch (error) { console.error(`Failed to add user: ${error.message}`); throw error; } } async removeUser(username) { try { const existingUsers = await this.getUsers(); const userIndex = existingUsers.findIndex(u => u.username === username); if (userIndex === -1) { throw new Error(`User '${username}' not found`); } existingUsers.splice(userIndex, 1); await this.saveUsers(existingUsers); await this.generatePasswordFile(existingUsers); console.log(`User '${username}' removed successfully`); } catch (error) { console.error(`Failed to remove user: ${error.message}`); throw error; } } async updateUser(username, user) { try { const validation = validation_1.ValidationUtils.validateUser(user); if (validation.length > 0) { throw new Error(`User validation failed: ${validation.join(', ')}`); } const existingUsers = await this.getUsers(); const userIndex = existingUsers.findIndex(u => u.username === username); if (userIndex === -1) { throw new Error(`User '${username}' not found`); } if (user.username !== username) { user.username = username; } const hashedPassword = await this.hashPassword(user.password); const userWithHashedPassword = { ...user, password: hashedPassword }; existingUsers[userIndex] = userWithHashedPassword; await this.saveUsers(existingUsers); await this.generatePasswordFile(existingUsers); console.log(`User '${username}' updated successfully`); } catch (error) { console.error(`Failed to update user: ${error.message}`); throw error; } } async getUsers() { try { if (!(await file_utils_1.FileUtils.fileExists(this.usersFile))) { return []; } const content = await file_utils_1.FileUtils.readFile(this.usersFile); return JSON.parse(content); } catch (error) { console.error(`Failed to load users: ${error.message}`); return []; } } async hashPassword(password) { try { const salt = crypto.randomBytes(12); const saltBase64 = salt.toString('base64'); const hash = crypto.pbkdf2Sync(password, salt, 10000, 32, 'sha256'); const hashBase64 = hash.toString('base64'); return `PBKDF2$sha256$10000$${saltBase64}$${hashBase64}`; } catch (error) { console.error(`Failed to hash password: ${error.message}`); throw error; } } async addAcl(acl) { try { const validation = validation_1.ValidationUtils.validateAcl(acl); if (validation.length > 0) { throw new Error(`ACL validation failed: ${validation.join(', ')}`); } const existingAcls = await this.getAcls(); const duplicateAcl = existingAcls.find(a => a.username === acl.username && a.clientid === acl.clientid && a.topic === acl.topic && a.access === acl.access); if (duplicateAcl) { throw new Error('Identical ACL rule already exists'); } existingAcls.push(acl); await this.saveAcls(existingAcls); await this.generateAclFile(existingAcls); console.log('ACL rule added successfully'); } catch (error) { console.error(`Failed to add ACL: ${error.message}`); throw error; } } async removeAcl(acl) { try { const existingAcls = await this.getAcls(); const aclIndex = existingAcls.findIndex(a => a.username === acl.username && a.clientid === acl.clientid && a.topic === acl.topic && a.access === acl.access); if (aclIndex === -1) { throw new Error('ACL rule not found'); } existingAcls.splice(aclIndex, 1); await this.saveAcls(existingAcls); await this.generateAclFile(existingAcls); console.log('ACL rule removed successfully'); } catch (error) { console.error(`Failed to remove ACL: ${error.message}`); throw error; } } async getAcls() { try { if (!(await file_utils_1.FileUtils.fileExists(this.aclsFile))) { return []; } const content = await file_utils_1.FileUtils.readFile(this.aclsFile); return JSON.parse(content); } catch (error) { console.error(`Failed to load ACLs: ${error.message}`); return []; } } async generateCertificates() { try { await file_utils_1.FileUtils.ensureDir(this.certsDir); const caKeyPath = path.join(this.certsDir, 'ca-key.pem'); const caCertPath = path.join(this.certsDir, 'ca-cert.pem'); const serverKeyPath = path.join(this.certsDir, 'server-key.pem'); const serverCertPath = path.join(this.certsDir, 'server-cert.pem'); if ((await file_utils_1.FileUtils.fileExists(caCertPath)) && (await file_utils_1.FileUtils.fileExists(serverCertPath))) { console.log('Certificates already exist, skipping generation'); return; } console.log('Generating TLS certificates...'); const caKeys = forge.pki.rsa.generateKeyPair(2048); const caCert = forge.pki.createCertificate(); caCert.publicKey = caKeys.publicKey; caCert.serialNumber = '01'; caCert.validity.notBefore = new Date(); caCert.validity.notAfter = new Date(); caCert.validity.notAfter.setFullYear(caCert.validity.notBefore.getFullYear() + 10); const caAttrs = [ { name: 'commonName', value: 'SignalK Mosquitto CA', }, { name: 'countryName', value: 'US', }, { name: 'organizationName', value: 'SignalK', }, ]; caCert.setSubject(caAttrs); caCert.setIssuer(caAttrs); caCert.setExtensions([ { name: 'basicConstraints', cA: true, }, { name: 'keyUsage', keyCertSign: true, digitalSignature: true, keyEncipherment: true, }, ]); caCert.sign(caKeys.privateKey); const serverKeys = forge.pki.rsa.generateKeyPair(2048); const serverCert = forge.pki.createCertificate(); serverCert.publicKey = serverKeys.publicKey; serverCert.serialNumber = '02'; serverCert.validity.notBefore = new Date(); serverCert.validity.notAfter = new Date(); serverCert.validity.notAfter.setFullYear(serverCert.validity.notBefore.getFullYear() + 5); const serverAttrs = [ { name: 'commonName', value: 'localhost', }, { name: 'countryName', value: 'US', }, { name: 'organizationName', value: 'SignalK', }, ]; serverCert.setSubject(serverAttrs); serverCert.setIssuer(caAttrs); serverCert.setExtensions([ { name: 'basicConstraints', cA: false, }, { name: 'keyUsage', digitalSignature: true, keyEncipherment: true, }, { name: 'subjectAltName', altNames: [ { type: 2, value: 'localhost', }, { type: 7, ip: '127.0.0.1', }, ], }, ]); serverCert.sign(caKeys.privateKey); const caPem = forge.pki.certificateToPem(caCert); const caKeyPem = forge.pki.privateKeyToPem(caKeys.privateKey); const serverCertPem = forge.pki.certificateToPem(serverCert); const serverKeyPem = forge.pki.privateKeyToPem(serverKeys.privateKey); await file_utils_1.FileUtils.writeFile(caCertPath, caPem); await file_utils_1.FileUtils.writeFile(caKeyPath, caKeyPem); await file_utils_1.FileUtils.writeFile(serverCertPath, serverCertPem); await file_utils_1.FileUtils.writeFile(serverKeyPath, serverKeyPem); await file_utils_1.FileUtils.chmod(caKeyPath, '600'); await file_utils_1.FileUtils.chmod(serverKeyPath, '600'); this.config.tlsCertPath = serverCertPath; this.config.tlsKeyPath = serverKeyPath; this.config.tlsCaPath = caCertPath; console.log('TLS certificates generated successfully'); } catch (error) { console.error(`Failed to generate certificates: ${error.message}`); throw error; } } async validateCertificates() { try { if (!this.config.tlsCertPath || !this.config.tlsKeyPath) { return false; } const certExists = await file_utils_1.FileUtils.fileExists(this.config.tlsCertPath); const keyExists = await file_utils_1.FileUtils.fileExists(this.config.tlsKeyPath); if (!certExists || !keyExists) { return false; } const certContent = await file_utils_1.FileUtils.readFile(this.config.tlsCertPath); const keyContent = await file_utils_1.FileUtils.readFile(this.config.tlsKeyPath); try { const cert = forge.pki.certificateFromPem(certContent); forge.pki.privateKeyFromPem(keyContent); // Validate private key format const now = new Date(); if (cert.validity.notAfter <= now) { console.log('Certificate has expired'); return false; } if (cert.validity.notBefore > now) { console.log('Certificate is not yet valid'); return false; } return true; } catch (parseError) { console.log(`Certificate parsing error: ${parseError.message}`); return false; } } catch (error) { console.error(`Certificate validation error: ${error.message}`); return false; } } async saveUsers(users) { await file_utils_1.FileUtils.ensureDir(this.dataDir); await file_utils_1.FileUtils.writeFile(this.usersFile, JSON.stringify(users, null, 2)); } async saveAcls(acls) { await file_utils_1.FileUtils.ensureDir(this.dataDir); await file_utils_1.FileUtils.writeFile(this.aclsFile, JSON.stringify(acls, null, 2)); } async generatePasswordFile(users) { try { await file_utils_1.FileUtils.ensureDir(this.configDir); const lines = []; for (const user of users) { if (user.enabled) { lines.push(`${user.username}:${user.password}`); } } await file_utils_1.FileUtils.writeFile(this.passwordFile, lines.join('\n') + '\n'); await file_utils_1.FileUtils.chmod(this.passwordFile, '600'); console.log(`Password file generated with ${lines.length} users`); } catch (error) { console.error(`Failed to generate password file: ${error.message}`); throw error; } } async generateAclFile(acls) { try { await file_utils_1.FileUtils.ensureDir(this.configDir); const lines = []; lines.push('# ACL file generated by SignalK Mosquitto plugin'); lines.push('# Do not edit manually - changes will be overwritten'); lines.push(''); const userGroups = new Map(); const clientGroups = new Map(); const globalAcls = []; for (const acl of acls) { if (acl.username) { if (!userGroups.has(acl.username)) { userGroups.set(acl.username, []); } userGroups.get(acl.username).push(acl); } else if (acl.clientid) { if (!clientGroups.has(acl.clientid)) { clientGroups.set(acl.clientid, []); } clientGroups.get(acl.clientid).push(acl); } else { globalAcls.push(acl); } } if (globalAcls.length > 0) { lines.push('# Global ACLs'); for (const acl of globalAcls) { lines.push(`topic ${this.formatAclAccess(acl.access)} ${acl.topic}`); } lines.push(''); } for (const [username, userAcls] of userGroups) { lines.push(`user ${username}`); for (const acl of userAcls) { lines.push(`topic ${this.formatAclAccess(acl.access)} ${acl.topic}`); } lines.push(''); } for (const [clientid, clientAcls] of clientGroups) { lines.push(`clientid ${clientid}`); for (const acl of clientAcls) { lines.push(`topic ${this.formatAclAccess(acl.access)} ${acl.topic}`); } lines.push(''); } await file_utils_1.FileUtils.writeFile(this.aclFile, lines.join('\n')); await file_utils_1.FileUtils.chmod(this.aclFile, '644'); console.log(`ACL file generated with ${acls.length} rules`); } catch (error) { console.error(`Failed to generate ACL file: ${error.message}`); throw error; } } formatAclAccess(access) { switch (access) { case 'read': return 'read'; case 'write': return 'write'; case 'readwrite': return 'readwrite'; default: return 'read'; } } async enableUser(username) { const users = await this.getUsers(); const user = users.find(u => u.username === username); if (!user) { throw new Error(`User '${username}' not found`); } user.enabled = true; await this.updateUser(username, user); } async disableUser(username) { const users = await this.getUsers(); const user = users.find(u => u.username === username); if (!user) { throw new Error(`User '${username}' not found`); } user.enabled = false; await this.updateUser(username, user); } async changePassword(username, newPassword) { const users = await this.getUsers(); const user = users.find(u => u.username === username); if (!user) { throw new Error(`User '${username}' not found`); } user.password = newPassword; await this.updateUser(username, user); } async exportSecurityConfig() { const users = await this.getUsers(); const acls = await this.getAcls(); return JSON.stringify({ users: users.map(u => ({ ...u, password: '[REDACTED]' })), acls, }, null, 2); } async importSecurityConfig(configJson, overwrite = false) { try { const importedConfig = JSON.parse(configJson); let importedUsers = 0; let importedAcls = 0; if (importedConfig.users && Array.isArray(importedConfig.users)) { const existingUsers = await this.getUsers(); for (const user of importedConfig.users) { if (user.password === '[REDACTED]') { console.log(`Skipping user '${user.username}' with redacted password`); continue; } const validation = validation_1.ValidationUtils.validateUser(user); if (validation.length > 0) { console.log(`Skipping invalid user '${user.username}': ${validation.join(', ')}`); continue; } const existingUserIndex = existingUsers.findIndex(u => u.username === user.username); if (existingUserIndex >= 0) { if (overwrite) { existingUsers[existingUserIndex] = user; importedUsers++; } } else { existingUsers.push(user); importedUsers++; } } if (importedUsers > 0) { await this.saveUsers(existingUsers); await this.generatePasswordFile(existingUsers); } } if (importedConfig.acls && Array.isArray(importedConfig.acls)) { const existingAcls = await this.getAcls(); for (const acl of importedConfig.acls) { const validation = validation_1.ValidationUtils.validateAcl(acl); if (validation.length > 0) { console.log(`Skipping invalid ACL: ${validation.join(', ')}`); continue; } const duplicateAcl = existingAcls.find(a => a.username === acl.username && a.clientid === acl.clientid && a.topic === acl.topic && a.access === acl.access); if (!duplicateAcl || overwrite) { if (duplicateAcl && overwrite) { const index = existingAcls.indexOf(duplicateAcl); existingAcls[index] = acl; } else if (!duplicateAcl) { existingAcls.push(acl); } importedAcls++; } } if (importedAcls > 0) { await this.saveAcls(existingAcls); await this.generateAclFile(existingAcls); } } return { users: importedUsers, acls: importedAcls }; } catch (error) { console.error(`Failed to import security config: ${error.message}`); throw error; } } } exports.SecurityManagerImpl = SecurityManagerImpl; //# sourceMappingURL=security-manager.js.map