UNPKG

knxultimate

Version:

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

368 lines 15.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const KnxLog_1 = require("./KnxLog"); const xml2js_1 = __importDefault(require("xml2js")); const crypto_js_1 = __importDefault(require("crypto-js")); const utils_1 = require("./utils"); const keyringSalt = '1.keyring.ets.knx.org'; const _retJson = {}; const logger = (0, KnxLog_1.module)('keyring'); let signature = ''; let createdHash = ''; let passwordHash = ''; let jSonXMLKeyringFile = {}; let XMLKeyringFileString = ''; async function xml2json(_sXML) { return new Promise((resolve, reject) => { try { const parser = new xml2js_1.default.Parser({ explicitArray: false }); parser.parseString(_sXML, function (err, json) { if (err) { reject(err); } else { resolve(json); } }); } catch (error) { reject(error); } }); } const keyring = (function () { async function pbkdf2WithHmacSha256(password, salt) { const iterations = 65536; const keyLength = 4; if (password == null || password.length === 0) { password = '\0'; } try { const secretKey = crypto_js_1.default.PBKDF2(password, salt, { keySize: keyLength, iterations, hasher: crypto_js_1.default.algo.SHA256, }); return secretKey.toString(crypto_js_1.default.enc.Base64); } catch (error) { logger.error(`pbkdf2WithHmacSha256 ${error.message}`); throw error; } } async function sha256(_input) { return new Promise((resolve, reject) => { try { const hash = crypto_js_1.default.SHA256(_input); let buffer = Buffer.from(hash.toString(crypto_js_1.default.enc.Hex), 'hex'); buffer = buffer.subarray(0, 16); resolve(buffer.toString('base64')); } catch (error) { reject(error); } }); } async function hashKeyringPwd(keyringPwd) { try { return await pbkdf2WithHmacSha256(keyringPwd, keyringSalt); } catch (error) { logger.error(`hashKeyringPwd ${error.message}`); throw error; } } async function aes128Cbc(_inputBase64, _pwdKeyringHashBase64, _createdHashBase64) { return new Promise((resolve, reject) => { try { const decrypted = crypto_js_1.default.AES.decrypt(_inputBase64, crypto_js_1.default.enc.Base64.parse(_pwdKeyringHashBase64), { iv: crypto_js_1.default.enc.Base64.parse(_createdHashBase64), mode: crypto_js_1.default.mode.CBC, padding: crypto_js_1.default.pad.ZeroPadding, }); resolve(crypto_js_1.default.enc.Hex.stringify(decrypted)); } catch (error) { logger.error(`aes128Cbc ${error}`); reject(error); } }); } async function verifySignature(_passwordHash) { const sRows = XMLKeyringFileString.split('>'); const aFiltered = []; try { for (let index = 0; index < sRows.length; index++) { let sRow = `${sRows[index]}>`; let bEndElement = false; sRow = sRow.trim(); if (sRow !== '' && !sRow.startsWith('<?xml')) { if (sRow.startsWith('</') || sRow.endsWith('/>')) { bEndElement = true; } if (sRow.startsWith('<') && !sRow.startsWith('</')) { aFiltered.push(Uint8Array.from([1])); } if (!sRow.startsWith('</')) { sRow = sRow.replace(/</g, ''); sRow = sRow.replace(/\/>/g, ''); sRow = sRow.replace(/>/g, ''); sRow = sRow.trim(); let sTag = ''; let sTemp = ''; let sAttribute = ''; let sValue = ''; for (let i = 0; i < sRow.length; i++) { const _char = sRow[i]; if (_char !== ' ') { sTemp += _char; } else { sTag = sTemp; break; } } if (sTag === '') sTag = sTemp; sTemp = ''; sRow = sRow.substring(sTag.length + 1); const aAttribs = []; do { sAttribute = sRow.substring(0, sRow.indexOf('=')); sRow = sRow.substring(sRow.indexOf('=') + 1).trim(); sValue = sRow.substring(0, sRow.indexOf('"', 1) + 1); sRow = sRow.substring(sValue.length + 1).trim(); sValue = sValue.replace(/"/g, ''); if (sAttribute !== 'xmlns' && sAttribute !== 'Signature') { aAttribs.push({ attlen: sAttribute.length, att: sAttribute, vallen: sValue.length, val: sValue, }); } sAttribute = ''; sValue = ''; } while (sRow.length > 0); if (sTag.length > 0) { aFiltered.push(Uint8Array.from([sTag.length])); aFiltered.push(new TextEncoder().encode(sTag)); } const aAttribsSorted = aAttribs.sort((a, b) => a.att > b.att ? 1 : b.att > a.att ? -1 : 0); for (let i = 0; i < aAttribsSorted.length; i++) { const element = aAttribsSorted[i]; if (element.attlen > 0) { aFiltered.push(Uint8Array.from([element.attlen])); aFiltered.push(new TextEncoder().encode(element.att)); aFiltered.push(Uint8Array.from([element.vallen])); aFiltered.push(new TextEncoder().encode(element.val)); } } if (bEndElement) { aFiltered.push(Uint8Array.from([2])); bEndElement = false; } } else { aFiltered.push(Uint8Array.from([2])); } } } aFiltered.push(Uint8Array.from([_passwordHash.length])); aFiltered.push(new TextEncoder().encode(_passwordHash)); const buffKeyringFileForHashing = Buffer.concat(aFiltered); const keyringFileForHashing = Buffer.from(buffKeyringFileForHashing).toString(); const outputHash = await sha256(keyringFileForHashing); if (outputHash === signature) { return true; } throw new Error('verifySignature failed'); } catch (error) { throw new Error('verifySignature ') + error.message; } } async function decryptKey(_inputBase64, _pwdKeyringHashBase64, _createdHashBase64) { try { return await aes128Cbc(_inputBase64, _pwdKeyringHashBase64, _createdHashBase64); } catch (error) { throw new Error(`decryptKey ${error.message}`); } finally { } } async function decryptPassword(_inputBase64, _pwdKeyringHashBase64, _createdHashBase64) { try { const pwdData = await extractPassword(await decryptKey(_inputBase64, _pwdKeyringHashBase64, _createdHashBase64)); let ret = ''; for (let index = 0; index < pwdData.length; index++) { const element = pwdData[index]; ret += String.fromCharCode(pwdData[index] & 0xff); } return ret; } catch (error) { throw new Error(`Error while decrypting password data: ${error.message}`); } } async function extractPassword(data) { data = Buffer.from(data.toString('hex'), 'hex'); const b = data[data.length - 1] & 0xff; const range = await copyOfRange(data, 8, data.length - b); return range; } async function copyOfRange(_arr, _start, _to) { const ret = []; for (let index = _start; index < _to; index++) { try { ret.push(_arr[index]); } catch (error) { ret.push(0); } } return ret; } async function load(_sXML, _keyringPassword) { if (_keyringPassword === undefined) _keyringPassword = ''; if (_sXML === undefined) _sXML = ''; let created; try { jSonXMLKeyringFile = await xml2json(_sXML); XMLKeyringFileString = _sXML; _retJson.ETSProjectName = jSonXMLKeyringFile.Keyring.$.Project; const createdBy = jSonXMLKeyringFile.Keyring.$.CreatedBy; _retJson.ETSCreatedBy = createdBy; created = jSonXMLKeyringFile.Keyring.$.Created; _retJson.ETSCreated = created; } catch (error) { logger.error(`load ${error.message}`); throw error; } try { passwordHash = await hashKeyringPwd(_keyringPassword); _retJson.HASHkeyringPasswordBase64 = passwordHash; } catch (error) { logger.error(`passwordHash ${error.message}`); throw new Error(`passwordHash ${error.message}`); } createdHash = await sha256(created); _retJson.HASHCreatedBase64 = createdHash; logger.debug(`createdHash ${createdHash}`); signature = jSonXMLKeyringFile.Keyring.$.Signature.toString('base64'); logger.debug(`signature ${signature}`); if (_keyringPassword.length > 0) { try { await verifySignature(passwordHash); logger.debug('verifySignature OK'); } catch (error) { logger.error(`signature verification failed for keyring ${_keyringPassword}`); throw new Error('The password is wrong'); } } try { _retJson.backbone = { multicastAddress: jSonXMLKeyringFile.Keyring.Backbone.$.MulticastAddress, latency: jSonXMLKeyringFile.Keyring.Backbone.$.Latency, key: await decryptKey(jSonXMLKeyringFile.Keyring.Backbone.$.Key, passwordHash, createdHash), }; } catch (error) { logger.error(`KNX-Secure: Backbone details ${error.message}`); throw new Error(`KNX-Secure: Backbone details ${error.message}`); } try { _retJson.interfaces = []; if ((0, utils_1.hasProp)(jSonXMLKeyringFile.Keyring, 'Interface')) { for (let index = 0; index < jSonXMLKeyringFile.Keyring.Interface.length; index++) { const element = jSonXMLKeyringFile.Keyring.Interface[index]; _retJson.interfaces.push({ individualAddress: element.$.IndividualAddress, type: element.$.Type, host: element.$.Host, userID: element.$.UserID, managementPassword: (0, utils_1.hasProp)(element.$, 'Password') ? await decryptPassword(element.$.Password, passwordHash, createdHash) : null, authenticationPassword: (0, utils_1.hasProp)(element.$, 'Authentication') ? await decryptPassword(element.$.Authentication, passwordHash, createdHash) : null, }); } } } catch (error) { logger.error(`KNX-Secure: Interfaces details ${error.message}`); throw new Error(`KNX-Secure: Interfaces details ${error.message}`); } try { _retJson.groupAddresses = []; if ((0, utils_1.hasProp)(jSonXMLKeyringFile.Keyring, 'GroupAddresses')) { for (let index = 0; index < jSonXMLKeyringFile.Keyring.GroupAddresses.Group.length; index++) { const element = jSonXMLKeyringFile.Keyring.GroupAddresses.Group[index]; _retJson.groupAddresses.push({ address: await getKNXAddressfromXML(element.$.Address), key: (0, utils_1.hasProp)(element.$, 'Key') ? await decryptKey(element.$.Key, passwordHash, createdHash) : null, }); } } } catch (error) { logger.error(`KNX-Secure: GroupAddres details ${error.message}`); throw new Error(`KNX-Secure: GroupAddres details ${error.message}`); } async function getKNXAddressfromXML(_rawAddress) { const digits = []; if (_rawAddress > 0x7ff) { digits.push((_rawAddress >> 11) & 0x1f); } digits.push((_rawAddress >> 8) & 0x07); digits.push(_rawAddress & 0xff); return digits.join('/'); } try { _retJson.Devices = []; if ((0, utils_1.hasProp)(jSonXMLKeyringFile.Keyring, 'Devices')) { for (let index = 0; index < jSonXMLKeyringFile.Keyring.Devices.Device.length; index++) { const element = jSonXMLKeyringFile.Keyring.Devices.Device[index]; _retJson.Devices.push({ individualAddress: element.$.IndividualAddress, sequenceNumber: element.$.SequenceNumber, toolKey: (0, utils_1.hasProp)(element.$, 'ToolKey') ? await decryptKey(element.$.ToolKey, passwordHash, createdHash) : null, managementPassword: (0, utils_1.hasProp)(element.$, 'ManagementPassword') ? await decryptPassword(element.$.ManagementPassword, passwordHash, createdHash) : null, authenticationPassword: (0, utils_1.hasProp)(element.$, 'Authentication') ? await decryptPassword(element.$.Authentication, passwordHash, createdHash) : null, }); } } } catch (error) { logger.error(`KNX-Secure: Devices details ${error.message}`); throw new Error(`KNX-Secure: Devices details ${error.message}`); } return _retJson; } return { load, }; })(); exports.default = keyring; //# sourceMappingURL=KNXsecureKeyring.js.map