knxultimate
Version:
KNX IP protocol implementation for Node. This is the ENGINE of Node-Red KNX-Ultimate node.
377 lines • 15.1 kB
JavaScript
"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