From d17af2c36c2b0a1f7fae9988b02ea5378b48023c Mon Sep 17 00:00:00 2001 From: Karim Kanso Date: Thu, 19 Dec 2024 19:48:42 +0000 Subject: [PATCH] feat: gbcs mac verification --- src/context.ts | 4 + src/crypto.ts | 54 ++++++-- src/dlms.ts | 10 +- src/parser.ts | 93 ++++++++++++-- src/utrn.ts | 1 + test/crypto.test.ts | 137 +++++++++++++++------ test/integration.test.ts | 25 +++- test/parse.test.ts | 130 ++++++++++++++++++- test/rtds/keystore/123456789abcdef0-ka.key | 5 + test/rtds/keystore/abababababababab-ka.key | 5 + test/rtds/keystore/fffffffffffffffe-ka.pem | 4 + 11 files changed, 399 insertions(+), 69 deletions(-) create mode 100644 test/rtds/keystore/123456789abcdef0-ka.key create mode 100644 test/rtds/keystore/abababababababab-ka.key create mode 100644 test/rtds/keystore/fffffffffffffffe-ka.pem diff --git a/src/context.ts b/src/context.ts index 3f79b46..e8ddd72 100644 --- a/src/context.ts +++ b/src/context.ts @@ -32,6 +32,9 @@ export interface CipherInfo { origCounter: Uint8Array origSysTitle: Uint8Array recipSysTitle: Uint8Array + supplimentryRemotePartyId?: Uint8Array + supplimentryOriginatorCounter?: Uint8Array + cra: 'command' | 'response' | 'alert' } export interface ParsedItem { @@ -62,6 +65,7 @@ export type DecryptCB = (cipherInfo: CipherInfo, aesKey: KeyObject) => void export interface Context { lookupKey: KeyStore + acbEui?: string | Uint8Array output: ParsedMessage current: ( | ParsedBlock diff --git a/src/crypto.ts b/src/crypto.ts index aac08d2..c3dd36b 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -72,6 +72,35 @@ export function gcm( return { cipherText, tag } } +/** + * standard gcm decrypt for use with GBCS - sets the cipher size and fixes the iv + * + * @param cipherInfo - originator/target/counter from grouping header + * @param cipherText - text to decrypt - set as empty buffer if none + * @param aad - additional auth data - set as empty buffer if none + * @param aesKey - output from createSecretKey + * @param tag - auth tag - default is 12 + * @returns plainText or throws error in case of auth fail + */ +export function ungcm( + cipherInfo: CipherInfo, + cipherText: Uint8Array, + aad: Uint8Array, + aesKey: KeyObject, + tag: Uint8Array, +): Uint8Array { + const iv = new Uint8Array(12) + iv.set(cipherInfo.origSysTitle, 0) + iv.set([0, 0, 0, 0], 8) + + const decipher = createDecipheriv('aes-128-gcm', aesKey, iv) + decipher.setAAD(aad) + decipher.setAuthTag(tag) + const plainText = decipher.update(cipherText) + decipher.final() + return plainText +} + export function decryptPayloadWithKey( cipherInfo: CipherInfo, ciphertextTag: Uint8Array, @@ -108,7 +137,7 @@ export function deriveKeyFromPair( privkey: string | KeyObject, pubkey: string | KeyObject, cipherInfo: CipherInfo, - mode?: 'command' | 'response' | 'alert' | 'encryption', + mode: 'command' | 'response' | 'alert' | 'encryption', ) { if (typeof privkey === 'string') { privkey = createPrivateKey({ key: privkey, format: 'pem' }) @@ -118,10 +147,6 @@ export function deriveKeyFromPair( pubkey = createPublicKey({ key: pubkey, format: 'pem' }) } - if (!mode) { - mode = 'encryption' - } - assert(privkey.asymmetricKeyType === 'ec', 'expected ec private key') assert(pubkey.asymmetricKeyType === 'ec', 'expected ec public key') @@ -152,13 +177,18 @@ export function deriveKeyFromPair( otherInfo.set([0x04], 7 + 8 + 1) break } - otherInfo.set(cipherInfo.origCounter, 7 + 8 + 2) - otherInfo.set(cipherInfo.recipSysTitle, 7 + 8 + 2 + 8) - - const data = new Uint8Array(4 + secret.byteLength + otherInfo.byteLength) - data.set([0, 0, 0, 1], 0) - data.set(secret, 4) - data.set(otherInfo, 4 + secret.byteLength) + otherInfo.set( + mode === 'encryption' + ? (cipherInfo.supplimentryOriginatorCounter ?? cipherInfo.origCounter) + : cipherInfo.origCounter, + 7 + 8 + 2, + ) + otherInfo.set( + mode === 'encryption' + ? (cipherInfo.supplimentryRemotePartyId ?? cipherInfo.recipSysTitle) + : cipherInfo.recipSysTitle, + 7 + 8 + 2 + 8, + ) const sha256 = createHash('sha256') sha256.update(new Uint8Array([0, 0, 0, 1])) diff --git a/src/dlms.ts b/src/dlms.ts index 8690c11..1c0dedd 100644 --- a/src/dlms.ts +++ b/src/dlms.ts @@ -34,7 +34,7 @@ import { parseMessageCode, parseCraFlag, } from './common' -import { CipherInfo, Context, putBytes, putUnparsedBytes } from './context' +import { Context, putBytes, putUnparsedBytes } from './context' import { decryptGbcsData } from './crypto' import { daysInWeek, @@ -253,11 +253,6 @@ function parseProtectionParameters(ctx: Context, x: Slice, indent: string) { 2: 'Authentication and Encryption', }) putBytes(ctx, `${indent} Protection Options`, getBytes(x, 2)) - const cipherInfo: CipherInfo = { - origCounter: x.input.buffer.subarray(x.index + 3, x.index + 11), - origSysTitle: x.input.buffer.subarray(x.index + 13, x.index + 21), - recipSysTitle: x.input.buffer.subarray(x.index + 23, x.index + 31), - } putBytes(ctx, `${indent} Transaction Id`, getBytes(x, 11)) putBytes(ctx, `${indent} Originator System Title`, getBytes(x, 10)) putBytes(ctx, `${indent} Recipient System Title`, getBytes(x, 10)) @@ -272,7 +267,6 @@ function parseProtectionParameters(ctx: Context, x: Slice, indent: string) { 'C(0e, 2s ECC CDH)', ) putBytes(ctx, `${indent} Key Ciphered Data`, getBytes(x, 2)) - return cipherInfo } function parseDlmsProtectedAttributesResponse( @@ -292,7 +286,7 @@ function parseDlmsProtectedAttributesResponse( function parseDlmsProtectedData(ctx: Context, x: Slice, indent: string) { indent = indent + ' ' - /*const cipherInfo = */ parseProtectionParameters(ctx, x, indent) + parseProtectionParameters(ctx, x, indent) const lenSz = parseLength(x, 1) const len = lenSz.length const off = lenSz.size + 1 diff --git a/src/parser.ts b/src/parser.ts index 289450d..ef93cf7 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -25,7 +25,7 @@ * along with this program. If not, see . */ -import { verify } from 'crypto' +import { KeyObject, verify } from 'crypto' import { CipherInfo, Context, @@ -35,7 +35,7 @@ import { putSeparator, putUnparsedBytes, } from './context' -import { deriveKeyFromPair } from './crypto' +import { deriveKeyFromPair, ungcm } from './crypto' import { parseCounter, parseCraFlag, @@ -56,9 +56,73 @@ async function parseGeneralCiphering( const len = parseEncodedLength(ctx, x, 'Ciphered Service Length') const y = getBytes(x, len) putBytes(ctx, 'Security Header', getBytes(y, 5)) + const macDataStart = y.index const cipherInfo = await parseGeneralSigning(ctx, getBytes(y, len - 5 - 12)) + const macDataEnd = y.index + putSeparator(ctx, 'MAC') - putBytes(ctx, 'MAC', getBytes(y, 12)) + const mac = 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) { + putBytes(ctx, 'MAC', mac, 'unknown acb') + } else { + let pubKey: KeyObject | undefined = undefined + let prvKey: KeyObject | undefined = 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 = deriveKeyFromPair( + prvKey, + pubKey, + cipherInfo, + cipherInfo.cra, + ) + try { + ungcm( + cipherInfo, + new Uint8Array(0), + aad, + aesKey, + mac.input.buffer.subarray(mac.index, mac.end), + ) + putBytes(ctx, 'MAC', mac, 'valid') + } catch { + putBytes(ctx, 'MAC', mac, 'invalid') + } + } else { + putBytes(ctx, 'MAC', mac, 'unknown') + } + } return cipherInfo } @@ -74,6 +138,7 @@ async function parseGeneralSigning( 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', } parseCounter(ctx, 'Originator Counter', x) putBytes(ctx, 'Originator System Title', getBytes(x, 9)) @@ -83,7 +148,7 @@ async function parseGeneralSigning( const otherInfo = getBytes(x, otherInfoLen) const messageCode = parseMessageCode(ctx, ' Message Code', otherInfo) if (otherInfoLen >= 10) { - cipherInfo.recipSysTitle = otherInfo.input.buffer.subarray( + cipherInfo.supplimentryRemotePartyId = otherInfo.input.buffer.subarray( otherInfo.index, otherInfo.index + 8, ) @@ -91,10 +156,8 @@ async function parseGeneralSigning( if (otherInfoLen >= 18) { parseCounter(ctx, ' Supplementary Remote Party Counter', otherInfo) if (otherInfoLen === 26) { - cipherInfo.origCounter = otherInfo.input.buffer.subarray( - otherInfo.index, - otherInfo.index + 8, - ) + cipherInfo.supplimentryOriginatorCounter = + otherInfo.input.buffer.subarray(otherInfo.index, otherInfo.index + 8) parseCounter(ctx, ' Supplementary Originator Counter', otherInfo) } else if (otherInfoLen > 26) { asn1.parseCertificate( @@ -294,9 +357,11 @@ function parseEncodedLength(ctx: Context, x: Slice, name: string) { export async function parseGbcsMessage( text: string, lookupKey: KeyStore, + acbEui?: string | Uint8Array, ): Promise { const ctx: Context = { lookupKey, + acbEui, output: {}, current: [], decryptionList: [], @@ -336,11 +401,15 @@ async function handleDecryptGbcsData( lookupKey: KeyStore, ): Promise { const pubKey = await lookupKey(cipherInfo.origSysTitle, 'KA', {}) - const prvKey = await lookupKey(cipherInfo.recipSysTitle, 'KA', { - privateKey: true, - }) + const prvKey = await lookupKey( + cipherInfo.supplimentryRemotePartyId ?? cipherInfo.recipSysTitle, + 'KA', + { + privateKey: true, + }, + ) - const aesKey = deriveKeyFromPair(prvKey, pubKey, cipherInfo) + const aesKey = deriveKeyFromPair(prvKey, pubKey, cipherInfo, 'encryption') for (let i = 0; i < ctx.decryptionList.length; i++) { putSeparator(ctx, `Decrypted Payload ${i}`) diff --git a/src/utrn.ts b/src/utrn.ts index fe18dc1..4f3a702 100644 --- a/src/utrn.ts +++ b/src/utrn.ts @@ -102,6 +102,7 @@ async function ptut(options: PtutOptions): Promise { BigInt(options.counter).toString(16).padStart(16, '0').slice(-16), 'hex', ), + cra: 'command', } /* retrieve device public ka key */ const pubKey = await options.lookupKey(cipherInfo.recipSysTitle, 'KA', {}) diff --git a/test/crypto.test.ts b/test/crypto.test.ts index 19454c0..fcf441e 100644 --- a/test/crypto.test.ts +++ b/test/crypto.test.ts @@ -110,6 +110,7 @@ describe('deriveKeyFromPair', () => { origCounter: Buffer.from('0000000100000000', 'hex'), origSysTitle: Buffer.from('90b3d51f30010000', 'hex'), recipSysTitle: Buffer.from('00db1234567890a0', 'hex'), + cra: 'command', } expect( crypto @@ -117,9 +118,9 @@ describe('deriveKeyFromPair', () => { createPrivateKey(org_90b3d51f30000002_ka_key), createPublicKey(device_00db1234567890a0_ka_cert), cipherInfo, - 'command' + 'command', ) - .export() + .export(), ).toStrictEqual(Buffer.from('6A93E360717394015DF93E031218C9A6', 'hex')) }) @@ -128,6 +129,7 @@ describe('deriveKeyFromPair', () => { origCounter: Buffer.from('0000000100000000', 'hex'), origSysTitle: Buffer.from('90b3d51f30010000', 'hex'), recipSysTitle: Buffer.from('00db1234567890a0', 'hex'), + cra: 'command', } expect( crypto @@ -135,9 +137,9 @@ describe('deriveKeyFromPair', () => { createPrivateKey(device_00db1234567890a0_ka_key), createPublicKey(org_90b3d51f30000002_ka_cert), cipherInfo, - 'command' + 'command', ) - .export() + .export(), ).toStrictEqual(Buffer.from('6A93E360717394015DF93E031218C9A6', 'hex')) }) @@ -146,6 +148,7 @@ describe('deriveKeyFromPair', () => { origCounter: Buffer.from('0000000100000000', 'hex'), origSysTitle: Buffer.from('00db1234567890a0', 'hex'), recipSysTitle: Buffer.from('90b3d51f30010000', 'hex'), + cra: 'command', } expect( crypto @@ -153,9 +156,9 @@ describe('deriveKeyFromPair', () => { createPrivateKey(org_90b3d51f30010000_ka_key), createPublicKey(device_00db1234567890a0_ka_cert), cipherInfo, - 'response' + 'response', ) - .export() + .export(), ).toStrictEqual(Buffer.from('03FD30815577C49EB7E3834B63EF8302', 'hex')) }) @@ -164,6 +167,7 @@ describe('deriveKeyFromPair', () => { origCounter: Buffer.from('0000000100000000', 'hex'), origSysTitle: Buffer.from('00db1234567890a0', 'hex'), recipSysTitle: Buffer.from('90b3d51f30010000', 'hex'), + cra: 'command', } expect( crypto @@ -171,9 +175,9 @@ describe('deriveKeyFromPair', () => { createPrivateKey(device_00db1234567890a0_ka_key), createPublicKey(org_90b3d51f30010000_ka_cert), cipherInfo, - 'response' + 'response', ) - .export() + .export(), ).toStrictEqual(Buffer.from('03FD30815577C49EB7E3834B63EF8302', 'hex')) }) @@ -182,6 +186,7 @@ describe('deriveKeyFromPair', () => { origCounter: Buffer.from('00000000000007D1', 'hex'), origSysTitle: Buffer.from('00db1234567890a0', 'hex'), recipSysTitle: Buffer.from('90b3d51f30010000', 'hex'), + cra: 'command', } expect( crypto @@ -189,9 +194,9 @@ describe('deriveKeyFromPair', () => { createPrivateKey(org_90b3d51f30010000_ka_key), createPublicKey(device_00db1234567890a0_ka_cert), cipherInfo, - 'alert' + 'alert', ) - .export() + .export(), ).toStrictEqual(Buffer.from('558FB1FA6660AF0D9E8365C47804B8FA', 'hex')) }) @@ -200,6 +205,7 @@ describe('deriveKeyFromPair', () => { origCounter: Buffer.from('00000000000007D1', 'hex'), origSysTitle: Buffer.from('00db1234567890a0', 'hex'), recipSysTitle: Buffer.from('90b3d51f30010000', 'hex'), + cra: 'command', } expect( crypto @@ -207,9 +213,9 @@ describe('deriveKeyFromPair', () => { createPrivateKey(device_00db1234567890a0_ka_key), createPublicKey(org_90b3d51f30010000_ka_cert), cipherInfo, - 'alert' + 'alert', ) - .export() + .export(), ).toStrictEqual(Buffer.from('558FB1FA6660AF0D9E8365C47804B8FA', 'hex')) }) @@ -218,6 +224,7 @@ describe('deriveKeyFromPair', () => { origCounter: Buffer.from('00000000000007D1', 'hex'), origSysTitle: Buffer.from('00db1234567890a0', 'hex'), recipSysTitle: Buffer.from('90b3d51f30010000', 'hex'), + cra: 'command', } expect( crypto @@ -225,9 +232,9 @@ describe('deriveKeyFromPair', () => { device_00db1234567890a0_ka_key, org_90b3d51f30010000_ka_cert, cipherInfo, - 'alert' + 'alert', ) - .export() + .export(), ).toStrictEqual(Buffer.from('558FB1FA6660AF0D9E8365C47804B8FA', 'hex')) }) }) @@ -244,11 +251,12 @@ describe('gcm', () => { origCounter: Buffer.from('0102030405060708', 'hex'), origSysTitle: Buffer.from('90b3d51f30030000', 'hex'), recipSysTitle: Buffer.from('00db1234567890a1', 'hex'), + cra: 'command', }, Buffer.from([]), Buffer.from('helloworld', 'ascii'), - createSecretKey('000102030405060708090a0b0c0d0e0f', 'hex') - ) + createSecretKey('000102030405060708090a0b0c0d0e0f', 'hex'), + ), ).toStrictEqual({ cipherText: Buffer.from([]), tag: Buffer.from('fb3afed43b78508864f00da3', 'hex'), @@ -262,12 +270,13 @@ describe('gcm', () => { origCounter: Buffer.from('0102030405060708', 'hex'), origSysTitle: Buffer.from('90b3d51f30030000', 'hex'), recipSysTitle: Buffer.from('00db1234567890a1', 'hex'), + cra: 'command', }, Buffer.from([]), Buffer.from('helloworld', 'ascii'), createSecretKey('000102030405060708090a0b0c0d0e0f', 'hex'), - 4 - ) + 4, + ), ).toStrictEqual({ cipherText: Buffer.from([]), tag: Buffer.from('fb3afed4', 'hex'), @@ -281,11 +290,12 @@ describe('gcm', () => { origCounter: Buffer.from('0102030405060708', 'hex'), origSysTitle: Buffer.from('90b3d51f30030000', 'hex'), recipSysTitle: Buffer.from('00db1234567890a1', 'hex'), + cra: 'command', }, Buffer.from('one two three', 'ascii'), Buffer.from('helloworld', 'ascii'), - createSecretKey('000102030405060708090a0b0c0d0e0f', 'hex') - ) + createSecretKey('000102030405060708090a0b0c0d0e0f', 'hex'), + ), ).toStrictEqual({ cipherText: Buffer.from('584fa806b24491178829d46c38', 'hex'), tag: Buffer.from('6b33bdcb1e223242ec20b957', 'hex'), @@ -293,6 +303,63 @@ describe('gcm', () => { }) }) +describe('ungcm', () => { + test('is defined', () => { + expect(crypto.ungcm).toBeDefined() + }) + + test('nominal-aad-only', () => { + expect( + crypto.ungcm( + { + origCounter: Buffer.from('0102030405060708', 'hex'), + origSysTitle: Buffer.from('90b3d51f30030000', 'hex'), + recipSysTitle: Buffer.from('00db1234567890a1', 'hex'), + cra: 'command', + }, + Buffer.from([]), + Buffer.from('helloworld', 'ascii'), + createSecretKey('000102030405060708090a0b0c0d0e0f', 'hex'), + Buffer.from('fb3afed43b78508864f00da3', 'hex'), + ), + ).toStrictEqual(Buffer.from([])) + }) + + test('nominal', () => { + expect( + crypto.ungcm( + { + origCounter: Buffer.from('0102030405060708', 'hex'), + origSysTitle: Buffer.from('90b3d51f30030000', 'hex'), + recipSysTitle: Buffer.from('00db1234567890a1', 'hex'), + cra: 'command', + }, + Buffer.from('584fa806b24491178829d46c38', 'hex'), + Buffer.from('helloworld', 'ascii'), + createSecretKey('000102030405060708090a0b0c0d0e0f', 'hex'), + Buffer.from('6b33bdcb1e223242ec20b957', 'hex'), + ), + ).toStrictEqual(Buffer.from('one two three', 'ascii')) + }) + + test('invalid-mac', () => { + expect(() => + crypto.ungcm( + { + origCounter: Buffer.from('0102030405060708', 'hex'), + origSysTitle: Buffer.from('90b3d51f30030000', 'hex'), + recipSysTitle: Buffer.from('00db1234567890a1', 'hex'), + cra: 'command', + }, + Buffer.from([]), + Buffer.from('helloworld', 'ascii'), + createSecretKey('000102030405060708090a0b0c0d0e0f', 'hex'), + Buffer.from('fb3afed43b78508864f00da4', 'hex'), + ), + ).toThrow(/unable to authenticate/) + }) +}) + describe('signGroupingHeader', () => { test('is defined', () => { expect(crypto.signGroupingHeader).toBeDefined() @@ -304,8 +371,8 @@ describe('signGroupingHeader', () => { crypto.signGroupingHeader( '90B3D51F30010000', message.toString('base64'), - keyStore - ) + keyStore, + ), ).rejects.toThrow() }) @@ -315,8 +382,8 @@ describe('signGroupingHeader', () => { crypto.signGroupingHeader( '90B3D51F30010000', message.toString('base64'), - keyStore - ) + keyStore, + ), ).rejects.toThrow() }) @@ -332,12 +399,12 @@ describe('signGroupingHeader', () => { 09 0C FF FF FF FF FF FF FF FF FF 80 00 FF 09 0C 07 DE 0C 1F FF 17 3B 0A 00 80 00 FF 09 0C 07 DF 01 01 FF 00 00 0A 00 80 00 FF 0F 00 00 00`.replace(/[ \n\t]/g, ''), - 'hex' + 'hex', ) const signed = await crypto.signGroupingHeader( '90B3D51F30010000', message.toString('base64'), - keyStore + keyStore, ) const signedBuffer = Buffer.from(signed, 'base64') expect(signedBuffer.length).toBe(message.length + 1 + 64) @@ -352,8 +419,8 @@ describe('signGroupingHeader', () => { key: createPublicKey(org_90b3d51f30010000_ds_cert), dsaEncoding: 'ieee-p1363', }, - signature - ) + signature, + ), ).toBeTruthy() }) @@ -369,12 +436,12 @@ describe('signGroupingHeader', () => { 09 0C FF FF FF FF FF FF FF FF FF 80 00 FF 09 0C 07 DE 0C 1F FF 17 3B 0A 00 80 00 FF 09 0C 07 DF 01 01 FF 00 00 0A 00 80 00 FF 0F 00 00 00 00`.replace(/[ \n\t]/g, ''), - 'hex' + 'hex', ) const signed = await crypto.signGroupingHeader( '90B3D51F30010000', message.toString('base64'), - keyStore + keyStore, ) const signedBuffer = Buffer.from(signed, 'base64') expect(signedBuffer.length).toBe(message.length + 64) @@ -389,8 +456,8 @@ describe('signGroupingHeader', () => { key: createPublicKey(org_90b3d51f30010000_ds_cert), dsaEncoding: 'ieee-p1363', }, - signature - ) + signature, + ), ).toBeTruthy() }) @@ -404,14 +471,14 @@ describe('signGroupingHeader', () => { 96 0c 02 5b 1a 90 1f 8f 21 7d d7 eb 02 0b cf 50 62 0c 3a 9c 51 bd 3f a5 0b ae 67 ec df 34 3d 3e 02 73 1d 9c c9 b2 41 c2 b2`.replace(/[ \n\t]/g, ''), - 'hex' + 'hex', ) await expect( crypto.signGroupingHeader( '90B3D51F30010000', message.toString('base64'), - keyStore - ) + keyStore, + ), ).rejects.toThrow('already signed') }) }) diff --git a/test/integration.test.ts b/test/integration.test.ts index 7f17cae..8deb9c0 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -10,13 +10,36 @@ describe('rtds', () => { const name = basename(file) test.concurrent(name, async () => { const message = readFileSync(file, 'utf-8') - const output = await parseGbcsMessage(message, keyStore) + const output = await parseGbcsMessage( + message, + keyStore, + '90 B3 D5 1F 30 00 00 02', + ) expect('Grouping Header' in output).toBeTruthy() + expect('CRA Flag' in output['Grouping Header'].children).toBeTruthy() + expect( + 'Other Information Length' in output['Grouping Header'].children, + ).toBeTruthy() + expect( + 'Message Code' in + (output['Grouping Header']?.children['Other Information Length'] + ?.children ?? {}), + ).toBeTruthy() expect('Payload' in output).toBeTruthy() if (file.search(/PRECOMMAND/) >= 0) { expect('Signature' in output).toBeFalsy() } else { expect('Signature' in output).toBeTruthy() + if ( + 'MAC' in output && + !String( + output['Grouping Header']?.children?.['Other Information Length'] + ?.children?.['Message Code']?.notes, + ).startsWith('PCS') + ) { + expect('MAC' in output.MAC.children).toBeTruthy() + expect(output.MAC.children.MAC.notes).toStrictEqual('valid') + } } }) }) diff --git a/test/parse.test.ts b/test/parse.test.ts index d0de548..bcc21b1 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -100,11 +100,17 @@ describe('parse', () => { test('double-long', async () => { const message = '3QAAAAAAAEQRAAAAAN8JAgAAAYw0vVLYCBzBG7EAAB0CCJCz1R8wAQAAAAIAaRDaIL1S2AAAAQX+evxTAQEAAEGfoFG78UF8nO+4bw==' - const output = await parseGbcsMessage(message, keyStore) + const output = await parseGbcsMessage( + message, + keyStore, + '90 B3 D5 1F 30 00 00 02', + ) expect('MAC Header' in output).toBeTruthy() expect('Grouping Header' in output).toBeTruthy() + expect('CRA Flag' in output['Grouping Header'].children).toBeTruthy() expect('Payload' in output).toBeTruthy() expect('Signature' in output).toBeTruthy() + expect('MAC' in output).toBeTruthy() expect('DLMS Access Response' in output.Payload.children).toBeTruthy() expect( output.Payload.children['DLMS Access Response'].children?.[ @@ -126,6 +132,11 @@ describe('parse', () => { 'List of Access Response Data' ].children?.['[0] Double Long'].notes, ).toBe('-25494445') + expect('MAC' in output.MAC.children).toBeTruthy() + expect(output.MAC.children.MAC.hex).toBe( + '41 9F A0 51 BB F1 41 7C 9C EF B8 6F', + ) + expect(output.MAC.children.MAC.notes).toBe('unknown') }) describe('cs02a', () => { @@ -210,4 +221,121 @@ describe('parse', () => { }) }) }) + + describe('mac-validation', () => { + describe('gbcs-sme.c.nc', () => { + test('command', async () => { + const message = ` + DD:00:00:00:00:00:00:54:11:00:00:00:00:DF:09:01: + 00:00:00:00:00:00:00:02:08:12:34:56:78:9A:BC:DE: + F0:08:FF:FF:FF:FF:FF:FF:FF:FE:00:02:00:22:20:D9: + 20:00:00:02:00:01:02:00:01:00:00:5E:2C:03:02:02: + 01:09:0C:07:DF:01:05:FF:00:00:00:00:80:00:FF:00: + 0F:1D:D0:0D:67:45:EB:D8:E0:A6:63:A4 + ` + + const output = await parseGbcsMessage( + message, + keyStore, + 'abababababababab', + ) + expect('Grouping Header' in output).toBeTruthy() + expect('Payload' in output).toBeTruthy() + expect('Signature' in output).toBeTruthy() + expect(output['Signature'].children['Signature Length'].hex).toBe('00') + expect('MAC' in output).toBeTruthy() + expect('MAC' in output.MAC.children).toBeTruthy() + expect(output.MAC.children.MAC.hex).toBe( + '0F 1D D0 0D 67 45 EB D8 E0 A6 63 A4', + ) + expect(output.MAC.children.MAC.notes).toBe('valid') + }) + + test('response', async () => { + const message = ` + DD:00:00:00:00:00:00:40:11:00:00:00:00:DF:09:02: + 00:00:00:00:00:00:00:02:08:FF:FF:FF:FF:FF:FF:FF: + FE:08:12:34:56:78:9A:BC:DE:F0:00:02:00:22:0C:DA: + 20:00:00:02:00:00:01:00:01:02:00:00:0B:3C:1B:31: + 2C:EA:E9:C1:30:06:0E:29 + ` + + const output = await parseGbcsMessage( + message, + keyStore, + 'abababababababab', + ) + expect('Grouping Header' in output).toBeTruthy() + expect('Payload' in output).toBeTruthy() + expect('Signature' in output).toBeTruthy() + expect(output['Signature'].children['Signature Length'].hex).toBe('00') + expect('MAC' in output).toBeTruthy() + expect('MAC' in output.MAC.children).toBeTruthy() + expect(output.MAC.children.MAC.hex).toBe( + '0B 3C 1B 31 2C EA E9 C1 30 06 0E 29', + ) + expect(output.MAC.children.MAC.notes).toBe('valid') + }) + }) + + test('gfi-ecs01a', async () => { + const message = ` + DD 00 00 00 00 00 00 82 04 F4 11 00 00 00 00 DF 09 01 00 00 00 00 00 00 03 E8 08 90 B3 D5 1F 30 + 01 00 00 08 00 DB 12 34 56 78 90 A0 00 02 00 19 82 04 7E D9 20 00 03 E8 00 15 02 00 14 00 00 0D + 00 00 FF 07 02 00 14 00 00 0D 00 00 FF 08 02 00 14 00 00 0D 00 00 FF 09 02 00 0B 00 01 0B 00 00 + FF 02 02 00 15 00 00 10 01 0B FF 02 02 00 15 00 00 10 01 0C FF 02 02 00 15 00 00 10 01 0D FF 02 + 02 00 15 00 00 10 01 0E FF 02 02 00 15 00 00 10 01 0F FF 02 02 00 15 00 00 10 01 10 FF 02 02 00 + 15 00 00 10 01 11 FF 02 02 00 15 00 00 10 01 12 FF 02 02 23 28 00 00 5E 2C 02 00 04 02 00 71 00 + 00 13 14 04 FF 06 02 00 71 00 00 13 14 00 FF 06 02 00 14 00 00 0D 00 00 FF 0A 02 23 28 00 00 5E + 2C 80 1D 06 02 23 28 00 00 5E 2C 02 00 06 02 00 71 00 00 13 14 04 FF 07 02 00 71 00 00 13 14 00 + FF 07 02 23 28 00 00 3F 01 01 FF 06 15 01 01 02 03 09 06 73 70 72 69 6E 67 09 0C 07 DF 01 0F FF + 00 00 00 00 80 00 FF 09 01 01 01 01 02 08 09 01 01 11 01 11 01 11 01 11 01 11 01 11 01 11 01 01 + 01 02 02 11 01 01 01 02 03 09 04 03 0B 2B 00 09 06 00 00 0A 00 64 FF 12 00 01 01 01 02 03 12 00 + 01 09 05 07 DF 01 1E FF 11 01 01 01 06 FF FF FF FF 01 01 06 FF FF FF FF 01 01 06 FF FF FF FF 01 + 01 06 FF FF FF FF 01 01 06 FF FF FF FF 01 01 06 FF FF FF FF 01 01 06 FF FF FF FF 01 01 06 FF FF + FF FF 03 00 02 03 02 02 0F 00 0F FC 02 03 12 00 00 09 06 00 00 00 00 00 00 0F 00 01 01 02 02 09 + 00 10 03 E8 02 03 02 02 0F 03 0F FC 02 03 12 00 03 09 06 01 00 01 08 00 FF 0F 02 01 50 02 02 09 + 01 01 10 39 AF 02 02 09 01 02 10 00 00 02 02 09 01 03 10 00 00 02 02 09 01 04 10 00 00 02 02 09 + 01 05 10 00 00 02 02 09 01 06 10 00 00 02 02 09 01 07 10 00 00 02 02 09 01 08 10 00 00 02 02 09 + 01 09 10 00 00 02 02 09 01 0A 10 00 00 02 02 09 01 0B 10 00 00 02 02 09 01 0C 10 00 00 02 02 09 + 01 0D 10 00 00 02 02 09 01 0E 10 00 00 02 02 09 01 0F 10 00 00 02 02 09 01 10 10 00 00 02 02 09 + 01 11 10 00 00 02 02 09 01 12 10 00 00 02 02 09 01 13 10 00 00 02 02 09 01 14 10 00 00 02 02 09 + 01 15 10 00 00 02 02 09 01 16 10 00 00 02 02 09 01 17 10 00 00 02 02 09 01 18 10 00 00 02 02 09 + 01 19 10 00 00 02 02 09 01 1A 10 00 00 02 02 09 01 1B 10 00 00 02 02 09 01 1C 10 00 00 02 02 09 + 01 1D 10 00 00 02 02 09 01 1E 10 00 00 02 02 09 01 1F 10 00 00 02 02 09 01 20 10 00 00 02 02 09 + 01 21 10 00 00 02 02 09 01 22 10 00 00 02 02 09 01 23 10 00 00 02 02 09 01 24 10 00 00 02 02 09 + 01 25 10 00 00 02 02 09 01 26 10 00 00 02 02 09 01 27 10 00 00 02 02 09 01 28 10 00 00 02 02 09 + 01 29 10 00 00 02 02 09 01 2A 10 00 00 02 02 09 01 2B 10 00 00 02 02 09 01 2C 10 00 00 02 02 09 + 01 2D 10 00 00 02 02 09 01 2E 10 00 00 02 02 09 01 2F 10 00 00 02 02 09 01 30 10 00 00 02 02 09 + 01 A1 10 00 00 02 02 09 01 A2 10 00 00 02 02 09 01 A3 10 00 00 02 02 09 01 A4 10 00 00 02 02 09 + 01 A5 10 00 00 02 02 09 01 A6 10 00 00 02 02 09 01 A7 10 00 00 02 02 09 01 A8 10 00 00 02 02 09 + 01 B1 10 00 00 02 02 09 01 B2 10 00 00 02 02 09 01 B3 10 00 00 02 02 09 01 B4 10 00 00 02 02 09 + 01 B5 10 00 00 02 02 09 01 B6 10 00 00 02 02 09 01 B7 10 00 00 02 02 09 01 B8 10 00 00 02 02 09 + 01 C1 10 00 00 02 02 09 01 C2 10 00 00 02 02 09 01 C3 10 00 00 02 02 09 01 C4 10 00 00 02 02 09 + 01 C5 10 00 00 02 02 09 01 C6 10 00 00 02 02 09 01 C7 10 00 00 02 02 09 01 C8 10 00 00 02 02 09 + 01 D1 10 00 00 02 02 09 01 D2 10 00 00 02 02 09 01 D3 10 00 00 02 02 09 01 D4 10 00 00 02 02 09 + 01 D5 10 00 00 02 02 09 01 D6 10 00 00 02 02 09 01 D7 10 00 00 02 02 09 01 D8 10 00 00 09 0C 00 + 00 00 00 00 00 00 00 00 80 00 FF 09 0C 00 00 00 00 00 00 00 00 00 80 00 FF 09 0C 00 00 00 00 00 + 00 00 00 00 80 00 FF 09 0C 00 00 00 00 00 00 00 00 00 80 00 FF 09 0C 00 00 00 00 00 00 00 00 00 + 80 00 FF 09 0C 00 00 00 00 00 00 00 00 00 80 00 FF 40 87 6C 14 FE 27 B4 51 FB 48 9D 84 75 D3 52 + 5D DC DA AE 46 B6 5A B7 04 3A BB 1C 7D 82 0A C4 F4 66 3F 35 E8 4A 9C 1E 66 E6 73 73 49 36 B1 91 + 94 65 78 6D E7 F3 AC 17 F9 6B C3 06 00 36 9E 96 34 87 40 74 C0 81 51 C6 16 1E 0E 20 31 99 + ` + const output = await parseGbcsMessage( + message, + keyStore, + '90 B3 D5 1F 30 00 00 02', + ) + expect('Grouping Header' in output).toBeTruthy() + expect('Payload' in output).toBeTruthy() + expect('Signature' in output).toBeTruthy() + expect(output['Signature'].children['Signature Length'].hex).toBe('40') + expect('MAC' in output).toBeTruthy() + expect('MAC' in output.MAC.children).toBeTruthy() + expect(output.MAC.children.MAC.hex).toBe( + '40 74 C0 81 51 C6 16 1E 0E 20 31 99', + ) + expect(output.MAC.children.MAC.notes).toBe('valid') + }) + }) }) diff --git a/test/rtds/keystore/123456789abcdef0-ka.key b/test/rtds/keystore/123456789abcdef0-ka.key new file mode 100644 index 0000000..ebe92d6 --- /dev/null +++ b/test/rtds/keystore/123456789abcdef0-ka.key @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPZ37My601tYG10cY +VT5eYbOSsPxMkM5qpM7agX6AEbGhRANCAATv8h1d1nTuxuCHQHA7UiVSy7dP/KEV +NsU3w8gG5BQ8j7Lnyj5zBstG2+S9WZzEox94jC+3qbm8l76YyB7xghow +-----END PRIVATE KEY----- diff --git a/test/rtds/keystore/abababababababab-ka.key b/test/rtds/keystore/abababababababab-ka.key new file mode 100644 index 0000000..162bb29 --- /dev/null +++ b/test/rtds/keystore/abababababababab-ka.key @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg5KbPtDFHHPyuSR/V +ZtGchwgs+fp3Itf6JLKz9WadvvuhRANCAAQpL5f+wbMMOEm4BtkERuSgN9bReAGX +luduUlW9w6CONG+fbm5+j2pNVZYtLy0OFs/ye/P5Jfp9uv0VqLHcaViU +-----END PRIVATE KEY----- diff --git a/test/rtds/keystore/fffffffffffffffe-ka.pem b/test/rtds/keystore/fffffffffffffffe-ka.pem new file mode 100644 index 0000000..eb49787 --- /dev/null +++ b/test/rtds/keystore/fffffffffffffffe-ka.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELbRaPyGIlDi0LI9GTHUpK6z1/dtd +oLSSUBspnL/pLY/bkPyP9AJhKYOLG8rRQCyuR/59gITkCaQa/OFtY1ecXw== +-----END PUBLIC KEY-----