From ceac9cb7f82af9d84d1eae2692d3093a655a9180 Mon Sep 17 00:00:00 2001 From: Li Zhineng Date: Wed, 16 Jul 2025 22:45:24 +0800 Subject: validate signatures --- packages/airmx/src/airmx.test.ts | 36 +++++++++++++++++++++++++++++++++++- packages/airmx/src/airmx.ts | 26 +++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/packages/airmx/src/airmx.test.ts b/packages/airmx/src/airmx.test.ts index e0c7048..80e70a0 100644 --- a/packages/airmx/src/airmx.test.ts +++ b/packages/airmx/src/airmx.test.ts @@ -78,11 +78,45 @@ describe('airmx', () => { }) it('should subscribe to the topic when the client connects', () => { - new Airmx({ mqtt: mockMqttClient }) + new Airmx({ mqtt: mockMqttClient, devices: [] }) const connectHandler = mockMqttClient.on.mock.calls.find( ([event]) => event === 'connect' )?.[1] as (() => void) | undefined; connectHandler?.() expect(mockMqttClient.subscribe).toHaveBeenCalledWith(TOPIC_STATUS); }) + + describe('message validation', () => { + const testDevice = { id: 1, key: 'f0eb21fe346c88e1d1ac73546022cd5d' } + let messageHandler: ((topic: string, message: Buffer) => void) | undefined + + beforeEach(() => { + new Airmx({ mqtt: mockMqttClient, devices: [testDevice] }) + messageHandler = mockMqttClient.on.mock.calls.find( + ([event]) => event === 'message' + )?.[1] as ((topic: string, message: Buffer) => void) | undefined + expect(messageHandler).toBeDefined() + }) + + const validMessage = '{"cmdId": 210,"name":"eagleStatus","time":1752675701,"from":2,"data":{"version":"10.00.17","power":1,"heatStatus":0,"mode":0,"cadr":47,"prm":1320,"g4Percent": 100,"hepaPercent":100,"carbonId":"031","g4Id":"041","hepaId":"021","carbonPercent":17,"diffPressure1":99999,"diffPressure2":99999,"t0":35,"status":0,"denoise":1},"sig":"b8796682da77e8c929dddf7e6461afec"}' + const invalidMessage = '{"cmdId": 210,"name":"eagleStatus","time":1752675701,"from":2,"data":{"version":"10.00.17","power":1,"heatStatus":0,"mode":0,"cadr":47,"prm":1320,"g4Percent": 100,"hepaPercent":100,"carbonId":"031","g4Id":"041","hepaId":"021","carbonPercent":17,"diffPressure1":99999,"diffPressure2":99999,"t0":35,"status":0,"denoise":1},"sig":"invalid"}' + + it('should validate message signatures when receiving messages', () => { + expect(() => { + messageHandler?.('airmx/01/0/1/1/1/1', Buffer.from(validMessage)) + }).not.toThrow() + }) + + it('should throw errors when message signatures are invalid', () => { + expect(() => { + messageHandler?.('airmx/01/0/1/1/1/1', Buffer.from(invalidMessage)) + }).toThrow('Failed to validate the message.') + }) + + it('should throw errors when device does not exist', () => { + expect(() => { + messageHandler?.('airmx/01/0/1/1/1/99999', Buffer.from(validMessage)) + }).toThrow('Could not find the device with ID 99999.') + }) + }) }) diff --git a/packages/airmx/src/airmx.ts b/packages/airmx/src/airmx.ts index 6ece32e..e3950b4 100644 --- a/packages/airmx/src/airmx.ts +++ b/packages/airmx/src/airmx.ts @@ -1,3 +1,4 @@ +import crypto from 'crypto' import { MqttClient } from 'mqtt' import { EagleStatus } from './eagle' import { SnowStatus } from './snow' @@ -7,6 +8,12 @@ type TEagleListener = (status: EagleStatus) => void export interface TConfig { mqtt: MqttClient + devices: Device[] +} + +export interface Device { + id: number + key: string } export enum Command { @@ -122,7 +129,9 @@ export class Airmx { return } - const data = JSON.parse(message.toString()) + const str = message.toString() + const data = JSON.parse(str) + this.#validateMessage(t.deviceId, str, data.sig) switch (data.cmdId) { case Command.SnowStatus: @@ -141,4 +150,19 @@ export class Airmx { #notifyEagle(status: EagleStatus) { this.#listeners.eagle.forEach((listener) => listener(status)) } + + #validateMessage(deviceId: number, message: string, sig: string) { + const device = this.config.devices.find((device) => device.id === deviceId) + if (device === undefined) { + throw new Error(`Could not find the device with ID ${deviceId}.`) + } + const plainText = message.slice(1, message.lastIndexOf('"sig"')) + const calculated = crypto.createHash('md5') + .update(plainText) + .update(device.key) + .digest('hex') + if (calculated !== sig) { + throw new Error('Failed to validate the message.') + } + } } -- cgit v1.2.3