summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorLi Zhineng <[email protected]>2025-07-16 22:45:24 +0800
committerLi Zhineng <[email protected]>2025-07-16 22:45:24 +0800
commitceac9cb7f82af9d84d1eae2692d3093a655a9180 (patch)
tree9f65144e65d8a8533a101ca0788e9c17f463c545 /packages
parentda9a75299b5505be80039712c57274cee54d0b8b (diff)
downloadairmx-ceac9cb7f82af9d84d1eae2692d3093a655a9180.tar.gz
airmx-ceac9cb7f82af9d84d1eae2692d3093a655a9180.zip
validate signatures
Diffstat (limited to 'packages')
-rw-r--r--packages/airmx/src/airmx.test.ts36
-rw-r--r--packages/airmx/src/airmx.ts26
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.')
+ }
+ }
}