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