UNPKG

knxultimate

Version:

KNX IP protocol implementation for Node. This is the ENGINE of Node-Red KNX-Ultimate node.

377 lines 15.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.Keyring = exports.GroupAddress = exports.IndividualAddress = void 0; const crypto = __importStar(require("crypto")); const fs = __importStar(require("fs")); const xml2js = __importStar(require("xml2js")); const zlib = __importStar(require("zlib")); class IndividualAddress { constructor(address) { if (typeof address === 'string') { const parts = address.split('.'); if (parts.length !== 3) { throw new Error(`Invalid individual address format: ${address}`); } const area = parseInt(parts[0]); const line = parseInt(parts[1]); const device = parseInt(parts[2]); this.raw = (area << 12) | (line << 8) | device; } else { this.raw = address; } } toString() { const area = (this.raw >> 12) & 0xf; const line = (this.raw >> 8) & 0xf; const device = this.raw & 0xff; return `${area}.${line}.${device}`; } } exports.IndividualAddress = IndividualAddress; class GroupAddress { constructor(address) { if (typeof address === 'string') { if (!address.includes('/')) { this.raw = parseInt(address); return; } const parts = address.split('/'); if (parts.length === 3) { const main = parseInt(parts[0]); const middle = parseInt(parts[1]); const sub = parseInt(parts[2]); this.raw = (main << 11) | (middle << 8) | sub; } else if (parts.length === 2) { const main = parseInt(parts[0]); const sub = parseInt(parts[1]); this.raw = (main << 11) | sub; } else { throw new Error(`Invalid group address format: ${address}`); } } else { this.raw = address; } } toString() { const main = (this.raw >> 11) & 0x1f; const middle = (this.raw >> 8) & 0x7; const sub = this.raw & 0xff; return `${main}/${middle}/${sub}`; } } exports.GroupAddress = GroupAddress; class Keyring { constructor() { this.interfaces = new Map(); this.backbones = []; this.groupAddresses = new Map(); this.devices = new Map(); } async load(source, password) { let xmlContent; if (fs.existsSync(source)) { if (process.env.KNX_DEBUG === '1') console.log('🔐 Loading keyring file:', source); const zipContent = fs.readFileSync(source); xmlContent = await this.unzipKnxKeys(zipContent); } else { const trimmed = (source || '').trim(); if (trimmed.startsWith('<')) { xmlContent = trimmed; } else { try { const buf = Buffer.from(trimmed, 'base64'); xmlContent = await this.unzipKnxKeys(buf); } catch (e) { xmlContent = trimmed; } } } const parser = new xml2js.Parser(); const result = await parser.parseStringPromise(xmlContent); this.passwordHash = this.hashKeyringPassword(password); if (process.env.KNX_DEBUG === '1') console.log('Password hash:', this.passwordHash.toString('hex')); await this.parseKeyring(result); } async loadFromString(content, password) { return this.load(content, password); } getCreatedBy() { return this.createdBy; } getCreated() { return this.created; } hashKeyringPassword(password) { return crypto.pbkdf2Sync(Buffer.from(password, 'utf-8'), Buffer.from('1.keyring.ets.knx.org', 'utf-8'), 65536, 16, 'sha256'); } decryptAes128Cbc(encryptedData, key, iv) { const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv); decipher.setAutoPadding(false); return Buffer.concat([decipher.update(encryptedData), decipher.final()]); } extractPassword(data) { if (!data || data.length === 0) return ''; const pad = data[data.length - 1]; const padLen = pad >= 1 && pad <= 16 ? pad : 0; const end = data.length - padLen; if (end <= 8) return ''; const payload = data.slice(8, end); return payload.toString('utf-8'); } async unzipKnxKeys(zipContent) { return new Promise((resolve, reject) => { zlib.unzip(zipContent.slice(30), (err, buffer) => { if (err) { const xmlStart = zipContent.indexOf('<?xml'); if (xmlStart !== -1) { const xmlEnd = zipContent.indexOf('</Keyring>') + 10; resolve(zipContent .slice(xmlStart, xmlEnd) .toString('utf-8')); } else { reject(err); } } else { resolve(buffer.toString('utf-8')); } }); }); } async parseKeyring(data) { const keyring = data.Keyring; if (!keyring) { throw new Error('Invalid keyring format'); } this.createdBy = keyring.$?.CreatedBy; this.created = keyring.$?.Created; if (this.created) { const createdHash = crypto .createHash('sha256') .update(Buffer.from(this.created, 'utf-8')) .digest(); this.iv = createdHash.slice(0, 16); } if (process.env.KNX_DEBUG === '1') console.log(`Keyring created by: ${this.createdBy} on ${this.created}`); if (keyring.Interface) { const interfaces = Array.isArray(keyring.Interface) ? keyring.Interface : [keyring.Interface]; for (const iface of interfaces) { this.parseInterface(iface); } } if (keyring.Backbone) { const backbones = Array.isArray(keyring.Backbone) ? keyring.Backbone : [keyring.Backbone]; for (const backbone of backbones) { this.parseBackbone(backbone); } } if (keyring.GroupAddresses?.[0]?.Group) { const groups = Array.isArray(keyring.GroupAddresses[0].Group) ? keyring.GroupAddresses[0].Group : [keyring.GroupAddresses[0].Group]; for (const group of groups) { this.parseGroupAddress(group); } } if (keyring.Devices?.[0]?.Device) { const devices = Array.isArray(keyring.Devices[0].Device) ? keyring.Devices[0].Device : [keyring.Devices[0].Device]; for (const device of devices) { this.parseDevice(device); } } } parseInterface(data) { const attrs = data.$; if (!attrs) return; const iface = { type: attrs.Type, individualAddress: new IndividualAddress(attrs.IndividualAddress), host: attrs.Host ? new IndividualAddress(attrs.Host) : undefined, userId: attrs.UserID ? parseInt(attrs.UserID) : undefined, password: attrs.Password, authentication: attrs.Authentication, groupAddresses: new Map(), }; if (iface.password && this.passwordHash) { const encrypted = Buffer.from(iface.password, 'base64'); const iv = this.iv ?? Buffer.alloc(16, 0); const decrypted = this.decryptAes128Cbc(encrypted, this.passwordHash, iv); if (process.env.KNX_DEBUG === '1') console.log(`Interface ${iface.individualAddress} password raw:`, decrypted.toString('hex')); iface.decryptedPassword = this.extractPassword(decrypted); if (process.env.KNX_DEBUG === '1') console.log(`Interface ${iface.individualAddress} password:`, iface.decryptedPassword); } if (iface.authentication && this.passwordHash) { const encrypted = Buffer.from(iface.authentication, 'base64'); const iv = this.iv ?? Buffer.alloc(16, 0); const decrypted = this.decryptAes128Cbc(encrypted, this.passwordHash, iv); if (process.env.KNX_DEBUG === '1') console.log(`Interface ${iface.individualAddress} auth raw:`, decrypted.toString('hex')); iface.decryptedAuthentication = this.extractPassword(decrypted); if (process.env.KNX_DEBUG === '1') console.log(`Interface ${iface.individualAddress} auth:`, iface.decryptedAuthentication); } if (data.Group) { const groups = Array.isArray(data.Group) ? data.Group : [data.Group]; for (const group of groups) { const groupAddr = new GroupAddress(group.$.Address); const senders = group.$.Senders ? group.$.Senders.split(' ').map((s) => new IndividualAddress(s)) : []; iface.groupAddresses.set(groupAddr.toString(), senders); } } this.interfaces.set(iface.individualAddress.toString(), iface); } parseBackbone(data) { const attrs = data.$; if (!attrs) return; const backbone = { key: attrs.Key, latency: attrs.Latency ? parseInt(attrs.Latency) : undefined, multicastAddress: attrs.MulticastAddress, }; if (backbone.key && this.passwordHash) { const encrypted = Buffer.from(backbone.key, 'base64'); const iv = this.iv ?? Buffer.alloc(16, 0); backbone.decryptedKey = this.decryptAes128Cbc(encrypted, this.passwordHash, iv); if (process.env.KNX_DEBUG === '1') console.log('Backbone key:', backbone.decryptedKey?.toString('hex')); } this.backbones.push(backbone); } parseGroupAddress(data) { const attrs = data.$; if (!attrs || !attrs.Address || !attrs.Key) return; const group = { address: new GroupAddress(attrs.Address), key: attrs.Key, }; if (this.passwordHash) { const encrypted = Buffer.from(group.key, 'base64'); const iv = this.iv ?? Buffer.alloc(16, 0); group.decryptedKey = this.decryptAes128Cbc(encrypted, this.passwordHash, iv); if (process.env.KNX_DEBUG === '1') console.log(`Group ${group.address} key:`, group.decryptedKey?.toString('hex')); } this.groupAddresses.set(group.address.toString(), group); } parseDevice(data) { const attrs = data.$; if (!attrs) return; const device = { individualAddress: new IndividualAddress(attrs.IndividualAddress), toolKey: attrs.ToolKey, managementPassword: attrs.ManagementPassword, authentication: attrs.Authentication, sequenceNumber: attrs.SequenceNumber ? parseInt(attrs.SequenceNumber) : undefined, serialNumber: attrs.SerialNumber, }; if (device.toolKey && this.passwordHash) { const encrypted = Buffer.from(device.toolKey, 'base64'); const iv = this.iv ?? Buffer.alloc(16, 0); device.decryptedToolKey = this.decryptAes128Cbc(encrypted, this.passwordHash, iv); if (process.env.KNX_DEBUG === '1') console.log(`Device ${device.individualAddress} tool key:`, device.decryptedToolKey?.toString('hex')); } if (device.managementPassword && this.passwordHash) { const encrypted = Buffer.from(device.managementPassword, 'base64'); const iv = this.iv ?? Buffer.alloc(16, 0); const decrypted = this.decryptAes128Cbc(encrypted, this.passwordHash, iv); if (process.env.KNX_DEBUG === '1') console.log(`Device ${device.individualAddress} mgmt raw:`, decrypted.toString('hex')); device.decryptedManagementPassword = this.extractPassword(decrypted); } if (device.authentication && this.passwordHash) { const encrypted = Buffer.from(device.authentication, 'base64'); const iv = this.iv ?? Buffer.alloc(16, 0); const decrypted = this.decryptAes128Cbc(encrypted, this.passwordHash, iv); if (process.env.KNX_DEBUG === '1') console.log(`Device ${device.individualAddress} auth raw:`, decrypted.toString('hex')); device.decryptedAuthentication = this.extractPassword(decrypted); } this.devices.set(device.individualAddress.toString(), device); } getInterfaces() { return this.interfaces; } getInterface(address) { return this.interfaces.get(address); } getBackbones() { return this.backbones; } getGroupAddresses() { return this.groupAddresses; } getGroupAddress(address) { return this.groupAddresses.get(address); } getDevices() { return this.devices; } getDevice(address) { return this.devices.get(address); } } exports.Keyring = Keyring; //# sourceMappingURL=keyring.js.map