Skip to content

Commit

Permalink
feat: gbcs mac verification
Browse files Browse the repository at this point in the history
  • Loading branch information
kazkansouh committed Dec 19, 2024
1 parent 5176035 commit d99f531
Show file tree
Hide file tree
Showing 11 changed files with 399 additions and 69 deletions.
4 changes: 4 additions & 0 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export interface CipherInfo {
origCounter: Uint8Array
origSysTitle: Uint8Array
recipSysTitle: Uint8Array
supplimentryRemotePartyId?: Uint8Array
supplimentryOriginatorCounter?: Uint8Array
cra: 'command' | 'response' | 'alert'
}

export interface ParsedItem {
Expand Down Expand Up @@ -62,6 +65,7 @@ export type DecryptCB = (cipherInfo: CipherInfo, aesKey: KeyObject) => void

export interface Context {
lookupKey: KeyStore
acbEui?: string | Uint8Array
output: ParsedMessage
current: (
| ParsedBlock
Expand Down
54 changes: 42 additions & 12 deletions src/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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' })
Expand All @@ -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')

Expand Down Expand Up @@ -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]))
Expand Down
10 changes: 2 additions & 8 deletions src/dlms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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))
Expand All @@ -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(
Expand All @@ -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
Expand Down
93 changes: 81 additions & 12 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { verify } from 'crypto'
import { KeyObject, verify } from 'crypto'
import {
CipherInfo,
Context,
Expand All @@ -35,7 +35,7 @@ import {
putSeparator,
putUnparsedBytes,
} from './context'
import { deriveKeyFromPair } from './crypto'
import { deriveKeyFromPair, ungcm } from './crypto'
import {
parseCounter,
parseCraFlag,
Expand All @@ -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
}

Expand All @@ -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))
Expand All @@ -83,18 +148,16 @@ 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,
)
putBytes(ctx, ' Supplementary Remote Party ID', getBytes(otherInfo, 8))
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(
Expand Down Expand Up @@ -294,9 +357,11 @@ function parseEncodedLength(ctx: Context, x: Slice, name: string) {
export async function parseGbcsMessage(
text: string,
lookupKey: KeyStore,
acbEui?: string | Uint8Array,
): Promise<ParsedMessage> {
const ctx: Context = {
lookupKey,
acbEui,
output: {},
current: [],
decryptionList: [],
Expand Down Expand Up @@ -336,11 +401,15 @@ async function handleDecryptGbcsData(
lookupKey: KeyStore,
): Promise<void> {
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}`)
Expand Down
1 change: 1 addition & 0 deletions src/utrn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ async function ptut(options: PtutOptions): Promise<bigint> {
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', {})
Expand Down
Loading

0 comments on commit d99f531

Please sign in to comment.