@smartdcc/gbcs-parser
Version:
GBCS parser based on henrygiraldo.github.io
423 lines • 15.9 kB
JavaScript
;
/*
*
* Original copyright holders for the GBCS message parser tool:
*
* Copyright (c) 2019 Andre B. Oliveira
* 2019 Enrique Giraldo
* 2019 Cristóbal Borrero
*
* Copyright for subsequent changes, including porting to NodeJS,
* updating for TypeScript and refactor to support unit testing:
*
* Copyright (c) 2022 Smart DCC Limited
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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.parseGbcsMessage = parseGbcsMessage;
const crypto_1 = require("crypto");
const context_1 = require("./context");
const crypto_2 = require("./crypto");
const common_1 = require("./common");
const util_1 = require("./util");
const dlms = __importStar(require("./dlms"));
const gbz = __importStar(require("./zigbee"));
const asn1 = __importStar(require("./asn1"));
async function parseGeneralCiphering(ctx, x) {
(0, context_1.putSeparator)(ctx, 'MAC Header');
(0, context_1.putBytes)(ctx, 'General Ciphering', (0, util_1.getBytes)(x, 7));
const len = parseEncodedLength(ctx, x, 'Ciphered Service Length');
const y = (0, util_1.getBytes)(x, len);
(0, context_1.putBytes)(ctx, 'Security Header', (0, util_1.getBytes)(y, 5));
const macDataStart = y.index;
const cipherInfo = await parseGeneralSigning(ctx, (0, util_1.getBytes)(y, len - 5 - 12));
const macDataEnd = y.index;
(0, context_1.putSeparator)(ctx, 'MAC');
const mac = (0, util_1.getBytes)(y, 12);
const aad = new Uint8Array(6 + (macDataEnd - macDataStart));
aad.set([0x11, 0, 0, 0, 0, 0], 0);
aad.set(y.input.buffer.subarray(macDataStart, macDataEnd), 6);
if (cipherInfo.cra === 'command' && ctx.acbEui === undefined) {
(0, context_1.putBytes)(ctx, 'MAC', mac, 'unknown acb');
}
else {
let pubKey = undefined;
let prvKey = undefined;
try {
pubKey = await ctx.lookupKey(cipherInfo.cra === 'command' && ctx.acbEui !== undefined
? ctx.acbEui
: cipherInfo.origSysTitle, 'KA', {});
prvKey = await ctx.lookupKey(cipherInfo.recipSysTitle, 'KA', {
privateKey: true,
});
}
catch {
try {
prvKey = await ctx.lookupKey(cipherInfo.cra === 'command' && ctx.acbEui !== undefined
? ctx.acbEui
: cipherInfo.origSysTitle, 'KA', {
privateKey: true,
});
pubKey = await ctx.lookupKey(cipherInfo.recipSysTitle, 'KA', {});
}
catch {
pubKey = undefined;
prvKey = undefined;
}
}
if (pubKey !== undefined && prvKey !== undefined) {
const aesKey = (0, crypto_2.deriveKeyFromPair)(prvKey, pubKey, cipherInfo, cipherInfo.cra);
try {
(0, crypto_2.ungcm)(cipherInfo, new Uint8Array(0), aad, aesKey, mac.input.buffer.subarray(mac.index, mac.end));
(0, context_1.putBytes)(ctx, 'MAC', mac, 'valid');
}
catch {
(0, context_1.putBytes)(ctx, 'MAC', mac, 'invalid');
}
}
else {
(0, context_1.putBytes)(ctx, 'MAC', mac, 'unknown');
}
}
return cipherInfo;
}
async function parseGeneralSigning(ctx, x) {
(0, context_1.putSeparator)(ctx, 'Grouping Header');
const signedDataStart = x.index + 1;
(0, context_1.putBytes)(ctx, 'General Signing', (0, util_1.getBytes)(x, 2));
const craFlag = (0, common_1.parseCraFlag)(ctx, x, '');
const cipherInfo = {
origCounter: x.input.buffer.subarray(x.index, x.index + 8),
origSysTitle: x.input.buffer.subarray(x.index + 9, x.index + 17),
recipSysTitle: x.input.buffer.subarray(x.index + 18, x.index + 26),
cra: craFlag === 2 ? 'response' : craFlag === 3 ? 'alert' : 'command',
};
(0, common_1.parseCounter)(ctx, 'Originator Counter', x);
(0, context_1.putBytes)(ctx, 'Originator System Title', (0, util_1.getBytes)(x, 9));
(0, context_1.putBytes)(ctx, 'Recipient System Title', (0, util_1.getBytes)(x, 9));
dlms.parseDlmsOctetString(ctx, x, 'Date Time', false);
const otherInfoLen = parseEncodedLength(ctx, x, 'Other Information Length');
const otherInfo = (0, util_1.getBytes)(x, otherInfoLen);
const messageCode = (0, common_1.parseMessageCode)(ctx, ' Message Code', otherInfo);
if (otherInfoLen >= 10) {
cipherInfo.supplimentryRemotePartyId = otherInfo.input.buffer.subarray(otherInfo.index, otherInfo.index + 8);
(0, context_1.putBytes)(ctx, ' Supplementary Remote Party ID', (0, util_1.getBytes)(otherInfo, 8));
if (otherInfoLen >= 18) {
(0, common_1.parseCounter)(ctx, ' Supplementary Remote Party Counter', otherInfo);
if (otherInfoLen === 26) {
cipherInfo.supplimentryOriginatorCounter =
otherInfo.input.buffer.subarray(otherInfo.index, otherInfo.index + 8);
(0, common_1.parseCounter)(ctx, ' Supplementary Originator Counter', otherInfo);
}
else if (otherInfoLen > 26) {
asn1.parseCertificate(ctx, otherInfo, ' Supplementary Remote Party Key Agreement Certificate');
}
}
}
const contentLen = parseEncodedLength(ctx, x, 'Content Length');
parsePayload(ctx, (0, util_1.getBytes)(x, contentLen), messageCode, craFlag);
const signedDataEnd = x.index;
if (x.index !== x.end) {
(0, context_1.putSeparator)(ctx, 'Signature');
const signatureLen = parseEncodedLength(ctx, x, 'Signature Length');
if (signatureLen > 0) {
const s = (0, util_1.getBytes)(x, signatureLen);
const dataToSign = x.input.buffer.subarray(signedDataStart, signedDataEnd);
const signature = s.input.buffer.subarray(s.index, s.end);
(0, context_1.putBytes)(ctx, 'Signature', s);
const pubKey = await ctx.lookupKey(cipherInfo.origSysTitle, 'DS', {});
const valid = (0, crypto_1.verify)('SHA256', dataToSign, { key: pubKey, dsaEncoding: 'ieee-p1363' }, signature);
(0, context_1.putBytes)(ctx, 'Valid', (0, util_1.getBytes)(x, 0), valid ? 'true' : 'false');
}
}
return cipherInfo;
}
function parsePayload(ctx, x, messageCode, craFlag) {
(0, context_1.putSeparator)(ctx, 'Payload');
if (messageCode === 0x0008) {
// CS02a
if (craFlag === 1) {
asn1.parseProvideSecurityCredentialDetailsCommand(ctx, x);
}
else {
asn1.parseProvideSecurityCredentialDetailsResponse(ctx, x);
}
}
else if (messageCode >= 0x0100 && messageCode <= 0x0109) {
// CS02b
if (craFlag === 1) {
asn1.parseUpdateSecurityCredentialsCommand(ctx, x);
}
else {
asn1.parseUpdateSecurityCredentialsResponse(ctx, x);
}
}
else if (messageCode === 0x00cb) {
// CS02b alert
asn1.parseUpdateSecurityCredentialsAlert(ctx, x);
}
else if (messageCode === 0x000a) {
// CS02c
if (craFlag === 1) {
asn1.parseIssueSecurityCredentialsCommand(ctx, x);
}
else {
asn1.parseIssueSecurityCredentialsResponse(ctx, x);
}
}
else if (messageCode === 0x000b) {
// CS02d
if (craFlag === 1) {
asn1.parseUpdateDeviceCertificateCommand(ctx, x);
}
else {
asn1.parseUpdateDeviceCertificateResponse(ctx, x);
}
}
else if (messageCode === 0x000c) {
// CS02e
if (craFlag === 1) {
asn1.parseProvideDeviceCertificateCommand(ctx, x);
}
else {
asn1.parseProvideDeviceCertificateResponse(ctx, x);
}
}
else if (messageCode === 0x000d ||
messageCode === 0x00ab ||
messageCode === 0x000e ||
messageCode === 0x00af) {
// CS03A1 || CS03A2 || CS03B || CS03C
if (craFlag === 1) {
asn1.parseJoinDeviceCommand(ctx, x);
}
else {
asn1.parseJoindDeviceResponse(ctx, x);
}
}
else if (messageCode === 0x000f || messageCode === 0x0010) {
// CS04AC || CS04B
if (craFlag === 1) {
asn1.parseUnjoinDeviceCommand(ctx, x);
}
else {
asn1.parseUnjoindDeviceResponse(ctx, x);
}
}
else if (messageCode === 0x0012) {
// CS06
if (craFlag === 1) {
asn1.parseActivateFirmwareCommand(ctx, x);
}
else {
asn1.parseActivateFirmwareResponse(ctx, x);
}
}
else if (messageCode === 0x00ca) {
// CS06 alert
asn1.parseActivateFirmwareAlert(ctx, x);
}
else if (messageCode === 0x0013) {
// CS07
if (craFlag === 1) {
asn1.parseReadDeviceJoinDetailsCommand(ctx, x);
}
else {
asn1.parseReadDeviceJoinDetailsResponse(ctx, x);
}
}
else if (messageCode === 0x007f) {
// GCS28
if (craFlag === 1) {
asn1.parseSetTimeCommand(ctx, x);
}
else {
asn1.parseSetTimeResponse(ctx, x);
}
}
else if (messageCode === 0x008b) {
// GCS53
gbz.parseGbzGcs53AlertPayload(ctx, x);
}
else if (messageCode === 0x008c) {
// GCS59
if (craFlag === 1) {
asn1.parseGpfDeviceLogRestoreCommand(ctx, x);
}
else {
asn1.parseGpfDeviceLogRestoreResponse(ctx, x);
}
}
else if (messageCode === 0x00b2) {
// GCS62
asn1.parseGpfDeviceLogBackupAlert(ctx, x);
}
else if (messageCode === 0x00cc) {
// Future Dated DLMS
dlms.parseDlmsFutureDatedAlert(ctx, x);
}
else if (messageCode === 0x00cd) {
// Future Dated GBZ
gbz.parseGbzFutureDatedAlertPayload(ctx, x);
}
else if (messageCode === 0x00ce) {
// FW Distribution Receipt Alert ESME
dlms.parseDlmsFirmwareDistributionReceiptAlert(ctx, x);
}
else if (messageCode === 0x00cf) {
// FW Distribution Receipt Alert GSME
gbz.parseGbzFirmwareDistributionReceiptAlert(ctx, x);
}
else if (messageCode === 0x0061) {
// ECS68
dlms.parseDlmsBillingDataLogAlert(ctx, x);
}
else if (messageCode === 0x00d5) {
// 8F84 Alert
dlms.parseFailureToDeliverRemotePartyToEsme(ctx, x);
}
else if (messageCode === 0x00f0) {
// 81A0 Alert ESME
dlms.parseDlmsMeterIntegrityIssueWarningAlert(ctx, x);
}
else if (messageCode === 0x00f2) {
// 81A0 Alert GSME
gbz.parseGbzMeterIntegrityIssueWarningAlert(ctx, x);
}
else if (x.input.byte(x.index) === 1 && x.input.byte(x.index + 1) === 9) {
if (craFlag === 3) {
gbz.parseGbzAlertPayload(ctx, x);
}
else {
gbz.parseGbzPayload(ctx, x);
}
}
else if (x.input.byte(x.index) === 0xd9) {
dlms.parseDlmsAccessRequest(ctx, x);
}
else if (x.input.byte(x.index) === 0xda) {
dlms.parseDlmsAccessResponse(ctx, x, messageCode);
}
else if (x.input.byte(x.index) === 0x0f) {
dlms.parseDlmsDataNotificationGbcsAlert(ctx, x);
}
else if (messageCode === 0x0128) {
// CCS08 alert
asn1.parseFirmwareTransferAlert(ctx, x);
}
else if (messageCode === 0x0129) {
// CS08 Read PPMID/HCALCS Firmware Version
if (craFlag === 3) {
asn1.parseReadPPMIDHCALCSFirmwareVersionAlert(ctx, x);
}
else if (craFlag === 2) {
asn1.parseReadPPMIDHCALCSFirmwareVersionResponse(ctx, x);
}
else {
asn1.parseReadPPMIDHCALCSFirmwareVersionCommand(ctx, x);
}
}
else {
(0, context_1.putBytes)(ctx, 'Payload', x);
}
(0, context_1.putUnparsedBytes)(x);
}
function parseEncodedLength(ctx, x, name) {
const lenSz = (0, common_1.parseLength)(x, 0);
(0, context_1.putBytes)(ctx, name, (0, util_1.getBytes)(x, lenSz.size), String(lenSz.length));
return lenSz.length;
}
async function parseGbcsMessage(text, lookupKey, acbEui) {
const ctx = {
lookupKey,
acbEui,
output: {},
current: [],
decryptionList: [],
};
let x = (0, util_1.parseHexString)(text);
if (x.input.byte(0) !== 0xdd && x.input.byte(0) !== 0xdf) {
// input could be base64 encoded
const y = (0, util_1.parseBase64String)(text);
if (y.input.byte(0) === 0xdd || y.input.byte(0) === 0xdf) {
// assume base64 encoding if it starts with a known tag
x = y;
}
}
let cipherInfo;
if (x.input.byte(0) === 0xdd) {
if (x.input.byte(1) === 0x00) {
cipherInfo = await parseGeneralCiphering(ctx, x);
}
else {
throw new Error('GBT not supported');
}
}
else if (x.input.byte(0) === 0xdf) {
cipherInfo = await parseGeneralSigning(ctx, x);
}
else {
throw new Error('unknown frame format');
}
if (ctx.decryptionList.length > 0) {
await handleDecryptGbcsData(ctx, cipherInfo, lookupKey);
}
return ctx.output;
}
async function handleDecryptGbcsData(ctx, cipherInfo, lookupKey) {
const pubKey = await lookupKey(cipherInfo.origSysTitle, 'KA', {});
const prvKey = await lookupKey(cipherInfo.supplimentryRemotePartyId ?? cipherInfo.recipSysTitle, 'KA', {
privateKey: true,
});
const aesKey = (0, crypto_2.deriveKeyFromPair)(prvKey, pubKey, cipherInfo, 'encryption');
for (let i = 0; i < ctx.decryptionList.length; i++) {
(0, context_1.putSeparator)(ctx, `Decrypted Payload ${i}`);
ctx.decryptionList[i](cipherInfo, aesKey);
}
}
//# sourceMappingURL=parser.js.map