UNPKG

zbtk

Version:

ZigBee Toolkit for Node.js

261 lines (240 loc) 10.4 kB
import crypto from 'crypto'; import { env } from 'node:process'; import { Buffer } from 'node:buffer'; import { fromHex, toHex } from './utils.js'; const alg = 'aes-128-ccm'; // pre-configured keys export const pks = (!env.ZBTK_CRYPTO_WELL_KNOWN_PKS ? [] : [ fromHex('D0:D1:D2:D3:D4:D5:D6:D7:D8:D9:DA:DB:DC:DD:DE:DF'), // Uncertified fromHex('5A:69:67:42:65:65:41:6C:6C:69:61:6E:63:65:30:39') // ZigBeeAlliance09 ]).concat(!env.ZBTK_CRYPTO_PKS ? [] : env.ZBTK_CRYPTO_PKS.split('[,; ]').map(fromHex)); /** * Pre-configure a network key to be used for automatic decryption. * * @param {(Buffer|string)} key the key to pre-configure * @returns {boolean} true if the key was added, false if it was already present */ export function pk(key) { if (!Buffer.isBuffer(key)) { key = fromHex(key); } return !!(!pks.some(c => c.equals(key)) && pks.push(key)); } function prepare(src64, fc, scf, aad) { if (!env.ZBTK_CRYPTO_NO_WIRE_WORKAROUND) { // the security control field is not filled in correctly in the header, // so it is necessary to patch it up to contain ZBEE_SEC_ENC_MIC32 == 5. // not sure why, but Wireshark does it. also patch it in the AAD header. // the aad is 22 bytes in length, or 30 / 38 in case of a ext src+/dst. let idx; if (aad.length == 15) { // APS header idx = 2; } else { // NWK header idx = 8 + (aad.length - 22); } // just a safety-safe: check if the SCF is at the position we expect it to be if (aad[idx] !== scf[0]) { throw new Error(`Mismatch in AAD crypto. data for Wireshark workaround, expected SCF ${toHex(scf[0])} at position ${idx} of AAD, but got ${toHex(aad[idx])}`); } aad[idx] = scf[0] = (scf[0] & ~0x07) | 0x05; } if (typeof fc === 'number') { const fcb = Buffer.allocUnsafe(4); fcb.writeUInt32LE(fc); fc = fcb; } return { // according to the ZigBee Specification the iv for the encryption is a concatenation of the // src64 (extended source), fc (frame counter) and scf (security control field) nonce: Buffer.concat([src64, fc, scf]) }; } /** * Encrypts the given data using the Network Key (nk) and parameters. * * With help of https://github.com/osresearch/ZbPy/blob/master/zbdecode * https://lucidar.me/en/zigbee/zigbee-frame-encryption-with-aes-128-ccm/ * https://lucidar.me/en/zigbee/autopsy-of-a-zigbee-frame/#nwk-payload * * @param {Buffer} data the data to encrypt * @param {Buffer} nk Network Key, e.g. the temporary Link Key (lk) based on the Install Code (ic), the * Transport Key as captured during Device Association or the well-known default * Link Key aka ZigBee Transport Key aka ZigBeeAlliance09 (5A:69:67:62:65:65:41:6C:6C:69:61:6E:63:65:30:39) * @param {Buffer} src64 extended IEEE address of sender (8 bytes) from the ZigBee Security Header * @param {(Buffer|number)} fc Frame Counter (4 bytes) from the ZigBee Security Header * @param {Buffer} scf Security Control byte aka Security Control Field (1 byte) first byte in the ZigBee Security Header * @param {Buffer} aad Additional Authenticated Data, this includes the full ZigBee Network Layer Data Header (NwkHeader) + * the first part of the ZigBee Security Header aka the auxiliary frame header, more specifically * NwkHeader (Frame Control Field, Destination Address, Source Address, Radius and Sequence Number) + * AuxiliaryHeader (Security Control Field, Frame Counter, Extended Source Address, Key Sequence Number) * @param {number} miclen Message Integrity Code from the end of the ZigBee Network Layer Data packet / Security Header * @returns {{ data: Buffer, mic: Buffer }} encrypted data and Message Integrity Code (mic) */ export function encrypt(data, nk, src64, fc, scf, aad, miclen = 4) { const { nonce } = prepare(src64, fc, scf = Buffer.from(scf), aad = Buffer.from(aad)); const cipher = crypto.createCipheriv(alg, nk, nonce, { authTagLength: miclen }); cipher.setAAD(aad, { plaintextLength: data.length }); return { data: Buffer.concat([cipher.update(data), cipher.final()]), mic: cipher.getAuthTag() }; } /** * Decrypts the given data using the Network Key (nk) and parameters. * * With help of https://github.com/osresearch/ZbPy/blob/master/zbdecode * https://lucidar.me/en/zigbee/zigbee-frame-encryption-with-aes-128-ccm/ * https://lucidar.me/en/zigbee/autopsy-of-a-zigbee-frame/#nwk-payload * * @param {Buffer} data the data to decrypt * @param {Buffer} nk Network Key, e.g. the temporary Link Key (lk) based on the Install Code (ic), the * Transport Key as captured during Device Association or the well-known default * Link Key aka ZigBee Transport Key aka ZigBeeAlliance09 (5A:69:67:62:65:65:41:6C:6C:69:61:6E:63:65:30:39) * @param {Buffer} src64 extended IEEE address of sender (8 bytes) from the ZigBee Security Header * @param {(Buffer|number)} fc Frame Counter (4 bytes) from the ZigBee Security Header * @param {Buffer} scf Security Control byte aka Security Control Field (1 byte) first byte in the ZigBee Security Header * @param {Buffer} aad Additional Authenticated Data, this includes the full ZigBee Network Layer Data Header (NwkHeader) + * the first part of the ZigBee Security Header aka the auxiliary frame header, more specifically * NwkHeader (Frame Control Field, Destination Address, Source Address, Radius and Sequence Number) + * AuxiliaryHeader (Security Control Field, Frame Counter, Extended Source Address, Key Sequence Number) * @param {Buffer} mic Message Integrity Code to verify the integrity of the data * @returns {Buffer} the decrypted data */ export function decrypt(data, nk, src64, fc, scf, aad, mic) { const { nonce } = prepare(src64, fc, scf = Buffer.from(scf), aad = Buffer.from(aad)); const decipher = crypto.createDecipheriv(alg, nk, nonce, { authTagLength: mic.length }); decipher.setAuthTag(mic); decipher.setAAD(aad, { plaintextLength: data.length }); return Buffer.concat([ decipher.update(data), decipher.final() ]); } import { stdinMiddleware } from './utils.js'; export const commands = [ { command: 'encrypt [data]', desc: 'Encrypt Packet', builder: yargs => stdinMiddleware(yargs .option('network-key', { alias: 'nk', desc: 'Network Key (i.e. temp. Link Key)', type: 'string', demandOption: true }) .option('ext-address', { alias: 'src64', desc: 'Extended IEEE Sender Address (8 bytes)', type: 'string', demandOption: true }) .option('frame-counter', { alias: 'fc', desc: 'Frame Counter (4 bytes)', type: 'string', demandOption: true }) .option('sec-ctrl-field', { alias: 'scf', desc: 'Security Control Field (1 byte)', type: 'string', demandOption: true }) .option('add-auth-data', { alias: 'aad', desc: 'Additional Authenticated Data', type: 'string', demandOption: true }) .option('mic-length', { alias: 'mic', desc: 'Message Integrity Code Length', type: 'number', default: 4 }), { desc: 'Data to encrypt' }) .middleware(argv => { if (argv.help) { return; } argv.nk = Buffer.from(argv.nk, 'hex'); argv.src64 = Buffer.from(argv.src64, 'hex'); argv.fc = Buffer.from(argv.fc, 'hex'); argv.scf = Buffer.from(argv.scf, 'hex'); argv.aad = Buffer.from(argv.aad, 'hex'); }) .example('$0 encrypt --nk 52f0fe8052ebb35907daa243c95a2ff4 --src64 0db123feffa7db28 --fc 148a0700 --scf 28 --aad 48220000777f1e2028148a07000db123feffa7db2800 40020102040101ef0c2112100a014029a806', 'Encrypt the given data') .example('echo -n 40020102040101ef0c2112100a014029a806 | $0 encrypt --nk 52f0fe8052ebb35907daa243c95a2ff4 --src64 0db123feffa7db28 --fc 148a0700 --scf 28 --aad 48220000777f1e2028148a07000db123feffa7db2800', 'Decrypt the given data') .version(false) .help(), handler: argv => { const { data, mic } = encrypt(argv.data, argv.nk, argv.src64, argv.fc, argv.scf, argv.aad, argv.mic); console.log(`${data.toString('hex')}${mic.toString('hex')}`); } }, { command: 'decrypt [data]', desc: 'Decrypt Packet', builder: yargs => stdinMiddleware(yargs .option('network-key', { alias: 'nk', desc: 'Network Key (i.e. temp. Link Key)', type: 'string', demandOption: true }) .option('ext-address', { alias: 'src64', desc: 'Extended IEEE Sender Address (8 bytes)', type: 'string', demandOption: true }) .option('frame-counter', { alias: 'fc', desc: 'Frame Counter (4 bytes)', type: 'string', demandOption: true }) .option('sec-ctrl-field', { alias: 'scf', desc: 'Security Control Field (1 byte)', type: 'string', demandOption: true }) .option('add-auth-data', { alias: 'aad', desc: 'Additional Authenticated Data', type: 'string', demandOption: true }) .option('msg-int-code', { alias: 'mic', desc: 'Message Integrity Code Length', type: 'string' }), { desc: 'Data to decrypt' }) .middleware(argv => { if (argv.help) { return; } argv.nk = Buffer.from(argv.nk, 'hex'); argv.src64 = Buffer.from(argv.src64, 'hex'); argv.fc = Buffer.from(argv.fc, 'hex'); argv.scf = Buffer.from(argv.scf, 'hex'); argv.aad = Buffer.from(argv.aad, 'hex'); argv.mic = Buffer.from(argv.mic, 'hex'); }) .example('$0 decrypt --nk 52f0fe8052ebb35907daa243c95a2ff4 --src64 0db123feffa7db28 --fc 148a0700 --scf 28 --aad 48220000777f1e2028148a07000db123feffa7db2800 --mic 1d37730e 4235bf415d82f5f46c205476a2e6e3d23bfa') .example('echo -n 4235bf415d82f5f46c205476a2e6e3d23bfa | $0 decrypt --nk 52f0fe8052ebb35907daa243c95a2ff4 --src64 0db123feffa7db28 --fc 148a0700 --scf 28 --aad 48220000777f1e2028148a07000db123feffa7db2800 --mic 1d37730e') .version(false) .help(), handler: argv => { console.log(decrypt(argv.data, argv.nk, argv.src64, argv.fc, argv.scf, argv.aad, argv.mic).toString('hex')); } } ];