UNPKG

node-red-contrib-knx-ultimate

Version:

Control your KNX intallation via Node-Red! Single Node KNX IN/OUT with optional ETS group address importer. Easy to use and highly configurable.

569 lines (512 loc) • 22.5 kB
/** * KNX Secure protocol stack in pure Javascript (C) 2021 Supergiovane */ // // The function returns this JSON (with ETS Keyring password "banana"): // // retJson { // ETSProjectName: 'KNX Secure', // ETSCreated: '2021-11-17T07:43:08', // ETSCreatedBy: 'ETS 5.7.6 (Build 1398)', // HASHkeyringPasswordBase64: '08qj3lhCDI1zINbqanGlaQ==', // HASHCreatedBase64: 'bX2hbMK6AR9l/U9ATjbwlA==', // backbone: { // multicastAddress: '224.0.23.12', // latency: '2000', // key: '28bd8f6fb56881eb8b4b3e3aec960f13' // }, // interfaces: [ // { // individualAddress: '3.1.2', // type: 'Tunneling', // host: '3.1.1', // userID: '2', // managementPassword: '.!Pea332', // authenticationPassword: 'autenticazione' // }, // { // individualAddress: '3.1.3', // type: 'Tunneling', // host: '3.1.1', // userID: '3', // managementPassword: '6Y*xu2QN', // authenticationPassword: 'autenticazione' // }, // { // individualAddress: '3.1.4', // type: 'Tunneling', // host: '3.1.1', // userID: '4', // managementPassword: '7e#qfoGG', // authenticationPassword: 'autenticazione' // }, // { // individualAddress: '3.1.5', // type: 'Tunneling', // host: '3.1.1', // userID: '5', // managementPassword: 'WC@rJrl*', // authenticationPassword: 'autenticazione' // }, // { // individualAddress: '3.1.6', // type: 'Tunneling', // host: '3.1.1', // userID: '6', // managementPassword: '."M1Cmjr', // authenticationPassword: 'autenticazione' // }, // { // individualAddress: '3.1.7', // type: 'Tunneling', // host: '3.1.1', // userID: '7', // managementPassword: '+-Ikuj y', // authenticationPassword: 'autenticazione' // }, // { // individualAddress: '3.1.8', // type: 'Tunneling', // host: '3.1.1', // userID: '8', // managementPassword: '4NV@Xp=(', // authenticationPassword: 'autenticazione' // }, // { // individualAddress: '3.1.9', // type: 'Tunneling', // host: '3.1.1', // userID: '9', // managementPassword: '"3CDfNH3', // authenticationPassword: 'autenticazione' // } // ], // groupAddresses: [ // { address: '8/0/0', key: '313681939762ff36fdcd774efec56d1b' }, // { address: '8/0/1', key: 'e990d57b3630bc4940a40d0c7caa3698' }, // { address: '8/0/2', key: 'cc0e1a6204d5c4623626b1ccc1069b63' } // ], // Devices: [ // { // individualAddress: '3.1.1', // sequenceNumber: '121960556295', // toolKey: '51b52bef0f8d5b83f7975fb3c1d67f96', // managementPassword: 'commissione', // authenticationPassword: 'autenticazione' // }, // { // individualAddress: '3.1.10', // sequenceNumber: '121960675276', // toolKey: 'db580560e32dc040d062e4f91bbf1182', // managementPassword: null, // authenticationPassword: null // }, // { // individualAddress: '3.1.11', // sequenceNumber: '121960725775', // toolKey: 'f03ca86237705ed9d7014627fb16f88a', // managementPassword: null, // authenticationPassword: null // } // ] // } // https://support.knx.org/hc/it/articles/360001582259-Usare-keyring-al-di-fuori-di-ETS-Falcon-SDK const KnxLog = require('./KnxLog'); const xml2js = require("xml2js"); const CryptoJS = require('crypto-js'); const keyringSalt = "1.keyring.ets.knx.org"; // Class returned by the keyring function after the load var _retJson = {}; var signature = ""; var createdHash = ""; var passwordHash = ""; var jSonXMLKeyringFile = {};// Holds the Keyring XML in JSON Format var XMLKeyringFileString = ""; // Holds the Keyring XML in string Format async function xml2json(_sXML) { return new Promise((resolve, reject) => { try { xml2js.Parser({ explicitArray: false }).parseString(_sXML, function (err, json) { if (err) reject(err); else resolve(json); }); } catch (error) { reject(error) } }); } var keyring = (function () { // Return byte, input char[] password, byte[] salt async function pbkdf2WithHmacSha256(password, salt) { const iterations = 65536; const keyLength = 4; if ((password == null) || (password.length === 0)) { password = "\0"; } try { //password = CryptoJS.enc.Base64.parse(key); var secretKey = CryptoJS.PBKDF2(password, salt, { keySize: keyLength, iterations: iterations, hasher: CryptoJS.algo.SHA256, padding: CryptoJS.pad.NoPadding }); return secretKey.toString(CryptoJS.enc.Base64) } catch (error) { KnxLog.get().error("pbkdf2WithHmacSha256 " + error.message); throw error; } } // Return bytes async function sha256(_input) { return new Promise((resolve, reject) => { try { let hash = CryptoJS.SHA256(_input); let buffer = Buffer.from(hash.toString(CryptoJS.enc.Hex), 'hex'); buffer = buffer.slice(0, 16); resolve(buffer.toString("base64")); } catch (error) { reject(error); } }); } // Return byte, input char[] async function hashKeyringPwd(keyringPwd) { try { return pbkdf2WithHmacSha256(keyringPwd, keyringSalt); } catch (error) { KnxLog.get().error("hashKeyringPwd " + error.message); throw error; } } // private static byte[] aes128Cbc(final byte[] input, final byte[] key, final byte[] iv) // throws GeneralSecurityException { // final var cipher = Cipher.getInstance("AES/CBC/NoPadding"); // final var keySpec = new SecretKeySpec(key, "AES"); // final var params = new IvParameterSpec(iv); // cipher.init(Cipher.DECRYPT_MODE, keySpec, params); // return cipher.doFinal(input); // } async function aes128Cbc(_inputBase64, _pwdKeyringHashBase64, _createdHashBase64) { //var decrypted = CryptoJS.AES.decrypt(_input,_pwdKeyringHash,{iv:iv,padding:CryptoJS.pad.ZeroPadding}); return new Promise((resolve, reject) => { try { var decrypted = CryptoJS.AES.decrypt(_inputBase64, CryptoJS.enc.Base64.parse(_pwdKeyringHashBase64), { iv: CryptoJS.enc.Base64.parse(_createdHashBase64), mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.ZeroPadding }); //resolve(CryptoJS.enc.Base64.stringify(decrypted)); resolve(CryptoJS.enc.Hex.stringify(decrypted)); } catch (error) { KnxLog.get().error("aes128Cbc " + error) reject(error); } }); } async function verifySignature(passwordHash) { let sRows = XMLKeyringFileString.split(">") let aFiltered = []; try { for (let index = 0; index < sRows.length; index++) { let sRow = sRows[index] + ">"; let bEndElement = false; sRow = sRow.trim(); // Clean close tags if (sRow !== "" && !sRow.startsWith("<?xml")) { if (sRow.startsWith("<\/") || sRow.endsWith("\/>")) { bEndElement = true; } if (sRow.startsWith("<") && !sRow.startsWith("<\/")) { // START_ELEMENT aFiltered.push(Uint8Array.from([1])); } if (!sRow.startsWith("<\/")) { sRow = sRow.replace(/</g, ''); sRow = sRow.replace(/\/>/g, ''); sRow = sRow.replace(/>/g, ''); sRow = sRow.trim(); // sRow is something like this: Backbone MulticastAddress="224.0.23.12" Latency="1000" Key="VGnz2DbdiMqN5EE4I7tqLw==" // Get the TAG let sTag = ""; let sTemp = ""; let sAttribute = ""; let sValue = ""; // Find the TAG for (let index = 0; index < sRow.length; index++) { const _char = sRow[index]; if (_char !== " ") { sTemp += _char; } else { // Got the tag sTag = sTemp; break; } } if (sTag === "") sTag = sTemp; // In case of TAG without attributes sTemp = ""; sRow = sRow.substring(sTag.length + 1); // Purge TAG // Attributes and values MulticastAddress="224.0.23.12" Banana="22sdsf02312==" let aAttribs = []; do { sAttribute = sRow.substring(0, sRow.indexOf("=")); // Get attribute sRow = sRow.substring(sRow.indexOf("=") + 1).trim(); // Purge attribute sValue = sRow.substring(0, sRow.indexOf("\"", 1) + 1); // Get value sRow = sRow.substring(sValue.length + 1).trim(); // Purge value 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)); } // Order the attribute array let aAttribsSorted = aAttribs.sort((a, b) => (a.att > b.att) ? 1 : ((b.att > a.att) ? -1 : 0)) // Put all togheddder for (let index = 0; index < aAttribsSorted.length; index++) { const element = aAttribsSorted[index]; 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)); } } // Add the end element tag. if (bEndElement) { // END_ELEMENT aFiltered.push(Uint8Array.from([2])); bEndElement = false; } } else { // END_ELEMENT aFiltered.push(Uint8Array.from([2])); } } } // Add the has password aFiltered.push(Uint8Array.from([passwordHash.length])); aFiltered.push(new TextEncoder().encode(passwordHash)) let buffKeyringFileForHashing = Buffer.concat(aFiltered); let keyringFileForHashing = Buffer.from(buffKeyringFileForHashing).toString(); let outputHash = await sha256(keyringFileForHashing); if (outputHash === signature) { return true; } else { throw (new Error("verifySignature failed")); } } catch (error) { throw (new Error("verifySignature ") + error.message) } } /** * Decrypts a backbone key, tool key, or group address key using the keyring * password. * * @param _inputBase64 encrypted key * @param _pwdKeyringHashBase64 the password hash of this keyring * @param _createdHashBase64 the keyring created datetime hash * @return decrypted key base64 */ async function decryptKey(_inputBase64, _pwdKeyringHashBase64, _createdHashBase64) { try { return await aes128Cbc(_inputBase64, _pwdKeyringHashBase64, _createdHashBase64); } catch (error) { throw new Error("decryptKey " + error.message); } finally { //Arrays.fill(_pwdKeyringHash, (byte) 0); } } /** * Decrypts a user password, device authentication code, or commissioning password using the keyring password. * * @param _inputBase64 encrypted password * @param _pwdKeyringHashBase64 the password of this keyring * @param _createdHashBase64 the created hash * @return decrypted password in plain text */ async function decryptPassword(_inputBase64, _pwdKeyringHashBase64, _createdHashBase64) { try { let 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("decrypting password data", error); } } // 18/11/2021 Estraggo la password smanettando sul range. async function extractPassword(data) { data = Buffer.from(data.toString("hex"), "hex"); let b = data[data.length - 1] & 0xff; let range = await copyOfRange(data, 8, data.length - b); return range; } // 18/11/2021 Copy of array range. If _to is > _arr.length, append 0 async function copyOfRange(_arr, _start, _to) { let ret = []; for (let index = _start; index < _to; index++) { try { ret.push(_arr[index]); } catch (error) { ret.push(0); } } return ret; } // Read the XML text // Returns an object with all necessary info, or error if the keyring password is wrong or something is going bad async function load(_sXML, _keyringPassword) { if (_keyringPassword === undefined) _keyringPassword = ""; if (_sXML === undefined) _sXML = ""; // All returned key are in base64 per comodità. try { jSonXMLKeyringFile = await xml2json(_sXML); XMLKeyringFileString = _sXML; _retJson.ETSProjectName = jSonXMLKeyringFile.Keyring.$.Project; var createdBy = jSonXMLKeyringFile.Keyring.$.CreatedBy; _retJson.ETSCreatedBy = createdBy; var created = jSonXMLKeyringFile.Keyring.$.Created; _retJson.ETSCreated = created; } catch (error) { KnxLog.get().error("load " + error.message); throw (error); } try { // Get the hash from the keyring password passwordHash = await hashKeyringPwd(_keyringPassword); _retJson.HASHkeyringPasswordBase64 = passwordHash; } catch (error) { KnxLog.get().error("passwordHash " + error.message) throw (new Error("passwordHash " + error.message)); } // Get the hash from the created tac createdHash = await sha256(created); _retJson.HASHCreatedBase64 = createdHash; KnxLog.get().debug("createdHash " + createdHash); // OK !!! // Get the signature from the KEYRING attribute signature = jSonXMLKeyringFile.Keyring.$.Signature.toString("base64"); KnxLog.get().debug("signature " + signature); // OK !!! if (_keyringPassword.length > 0) { try { await verifySignature(passwordHash); KnxLog.get().debug("verifySignature OK"); } catch (error) { KnxLog.get().error("signature verification failed for keyring " + _keyringPassword); throw (new Error("The password is wrong")); } } // Get the BACKBONE details OK !!!!! try { _retJson.backbone = { multicastAddress: jSonXMLKeyringFile.Keyring.Backbone.$.MulticastAddress, latency: jSonXMLKeyringFile.Keyring.Backbone.$.Latency, key: await decryptKey(jSonXMLKeyringFile.Keyring.Backbone.$.Key, passwordHash, createdHash) } } catch (error) { KnxLog.get().error("KNX-Secure: Backbone details " + error.message); throw (new Error("KNX-Secure: Backbone details " + error.message)); } // Get the INTERFACES details // <Interface IndividualAddress="3.1.2" Type="Tunneling" Host="3.1.1" UserID="2" Password="gF8N8lKGU9cD3TNMLEvu50SbI48qI5EeC8WeciL53Zg=" Authentication="jHW6k+R/b+GOfdaNzXXildWI4BrqHkAoa6lUtWCGGDI=" /> // this.interfaces = [{ // individualAddress: "", // type: "", // host: "", // userID: "", // managementPassword: "", // authenticationPassword: "" // }]; try { _retJson.interfaces = []; if (jSonXMLKeyringFile.Keyring.hasOwnProperty("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: element.$.hasOwnProperty("Password") ? await decryptPassword(element.$.Password, passwordHash, createdHash) : null, authenticationPassword: element.$.hasOwnProperty("Authentication") ? await decryptPassword(element.$.Authentication, passwordHash, createdHash) : null }) } } } catch (error) { KnxLog.get().error("KNX-Secure: Interfaces details " + error.message); throw (new Error("KNX-Secure: Interfaces details " + error.message)); } // Get the GROUP ADDRESSES details // <Group Address="16384" Key="CreHKeXp+5U2qMLVU0XWxw==" /> // this.groupAddresses = [{ // address: "", // key: "" // }]; try { _retJson.groupAddresses = []; if (jSonXMLKeyringFile.Keyring.hasOwnProperty("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: element.$.hasOwnProperty("Key") ? await decryptKey(element.$.Key, passwordHash, createdHash) : null }) } } } catch (error) { KnxLog.get().error("KNX-Secure: GroupAddres details " + error.message); throw (new Error("KNX-Secure: GroupAddres details " + error.message)); } // 18/11/2021 Recupero il gruppo dall'XML 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('/'); } // Get the DEVICES details // <Device IndividualAddress="3.1.1" ToolKey="T770+Sebf2zpx3X3A0S64A==" ManagementPassword="6LPLJeu+XxuGpn6tOqt9fw4NuSa/jIQCYXzFVDwPUiU=" Authentication="rywptqDB0/UNF/5VmlTs5YnrIqO9FJ3YGGEIm08Z1UQ=" SequenceNumber="121960556295" /> // Devices: // this.devices = [{ // individualAddress: "", // sequenceNumber: "", // toolKey: "", // managementPassword: "", // authenticationPassword: "" // }]; try { _retJson.Devices = []; if (jSonXMLKeyringFile.Keyring.hasOwnProperty("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: element.$.hasOwnProperty("ToolKey") ? await decryptKey(element.$.ToolKey, passwordHash, createdHash) : null, managementPassword: element.$.hasOwnProperty("ManagementPassword") ? await decryptPassword(element.$.ManagementPassword, passwordHash, createdHash) : null, authenticationPassword: element.$.hasOwnProperty("Authentication") ? await decryptPassword(element.$.Authentication, passwordHash, createdHash) : null }) } } } catch (error) { KnxLog.get().error("KNX-Secure: Devices details " + error.message); throw (new Error("KNX-Secure: Devices details " + error.message)); } return _retJson; } return { load: load }; })(); exports.keyring = keyring;