HDL-Buspro智能设备控制概要分析

HDL-Buspro智能设备控制概要分析

Denvo 树犹如此,人何以堪

前言

最近对HDL(河东科技)的智能家居系统比较感兴趣,故对HDL在控制家居方面浅浅研究一下。在此记录一下我的研究过程。

对HDL Home抓包

HDL为用户提供了一个名为HDL Home的软件,用户可以通过这个App随时控制家中的各种设备。所以这次打算拿这个App开刀。

抓包操作步骤

先从官网上下载并安装Charles,并通过下面按钮指向的网站激活。

破解工具

激活后,打开Charles,并点击菜单栏中的Help/SSL proxying/Install Charles Root Certificate。此操作会弹出来一个证书,选择安装证书,安装到本地计算机 -> 将所有的证书都放入下列存储 -> 受信任的根证书颁发机构

Charles默认的代理地址是电脑ip:8888。在手机上修改WLAN连接,设置代理为手动,并在IP处填写电脑的IP,端口填写8888。

手机连接代理后,Charles会弹出来是否允许的提示。请选择允许。如果选择了不允许,重新打开Charles即可弹出页面。

然后在手机上访问http://ssl.charles/(旧版是http://chls.pro/sslhttp://charlesproxy.com/getssl,然而官方后面改地址了,但是对应的提示文本没有改)下载证书,并安装到手机上。

接下来在Charles的菜单栏中点击Proxy/SSL proxying settings...,打开的窗口中有一个Include区域。把Host: *, Port: 443这条数据添加到Include区域,再点击Done按钮。

如果后面抓包出来一直是乱码,并且上面的步骤操作无误,可以尝试在Charles安装目录/Charles.ini文件(如果是默认安装,则在C:\Program Files (x86)\Charles\Charles.ini)中仿照里面存在的格式添加一条vmarg的配置,配置内容为-Dfile.encoding=UTF-8。(如果和我一样使用Charles 5.0.3版本,那么就是在文件的第15行后面新增一行:vmarg.11=-Dfile.encoding=UTF-8

点击右上角的图标就可以启动/停止抓包了。

我只抓了从打开软件到打开特定房间的一盏灯这一串操作的包。基本上所有的API的鉴权都只需要一个加一个header:Authorization = Bearer + 你的token。如果没有特殊说明,下面的API都是这样的。

同时,这些API基本都是POST,且body内容都是JSON,且这些JSON的最后三个字段一定是appKeytimestampsign。返回的JSON的格式也比较固定,除了data字段外的都是一样的。所以在此先描述一下json的共同部分,下面API描述时就会省略这些共同的字段。

要获取这些特殊字段的具体内容,建议自己亲自POST查看结果。

发送的json:

1
2
3
4
5
{
"appKey": "CXZMMOCF",
"timestamp": "string字符串", // 时间戳貌似并不会严格检查,只要sign是正确的,这个时间戳乱填都没问题
"sign": "string字符串"
}

接受的json:

1
2
3
4
5
6
7
8
9
10
11
{
"code": 0, // 正常情况应该是0,如果token需要更新则为10001
"data": "这个字段会不同,可能是一个数组,也可能是一个json或其他东西",
"requestId": "string字符串",
"timestamp": "string字符串",
"isSuccess": true, // 未成功则为false,token过期也为false

// 下面两个正常是不会出现的,当token需要更新时才出现
"defExec": true,
"message": "会话超时,请更新token"
}

获取用户信息

API:https://china-gateway.hdlcontrol.com/smart-footstone/member/memberInfo/getMemberInfo。可以通过当前用户的token获取当前用户的memberId、手机号前缀(比如+86)、邮箱、语言、地区、注册时间、上次登录时间等数据。

发送的json没有其他特殊数据。

返回的json如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
"data": {
"memberId": "string字符串",
"memberPhonePrefix": "86",
"memberEmail": "string字符串",
"languageType": "CHINESE",
"region": "string字符串",
"createTime": "string字符串",
"appCode": "CXZMMOCF",
"lastLoginTime": "string字符串"
}
}

获取家的环境信息

API:https://china-gateway.hdlcontrol.com/basis-footstone/app/weather/getWeatherNowByHouse。通过houseId获取这个家的温度、体感温度、当日气温(最高和最低)、NO2、SO2、CO、PM2.5、PM10、空气质量、天气、风向、风速、气压等信息。

发送的json如下:

1
2
3
{
"houseId": "string字符串"
}

返回的json如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"data": {
"temp": "string字符串",
"feelsLike": "string字符串",
"dayTempMin": "string字符串",
"dayTempMax": "string字符串",
"no2": "string字符串",
"so2": "string字符串",
"co": "string字符串",
"pm25": "string字符串",
"pm10": "string字符串",
"aqi": "string字符串",
"level": "string字符串",
"category": "string字符串",
"icon": "string字符串",
"text": "string字符串",
"wind360": "string字符串",
"windDir": "string字符串",
"windScale": "string字符串",
"windSpeed": "string字符串",
"humidity": "string字符串",
"precip": "string字符串",
"pressure": "string字符串",
"vis": "string字符串",
"cloud": "string字符串",
"dew": "string字符串"
}
}

获取家的位置信息

API:https://china-gateway.hdlcontrol.com/home-wisdom/app/home/info。能获取家的名字、经纬度、网关、位置等信息。

发送的json如下:

1
2
3
{
"homeId": "string字符串"
}

返回的json如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
"data": {
"id": "string字符串,同homeId",
"homeId": "string字符串",
"homeRegionId": "string字符串",
"userId": "string字符串",
"userRegionId": "string字符串",
"homeName": "string字符串",
"longitude": "double",
"latitude": "double",
"secretKey": "string字符串",
"deliverStatus": "string字符串",
"homeType": "string字符串",
"regionUrl": "https://china-gateway.hdlcontrol.com",
"emqUrl": "string表示的url,带https前缀",
"isBindGateway": "bool",
"isOtherShare": "bool",
"accountType": "string字符串",
"location": {
"nationCode": "string字符串",
"provinceCode": "string字符串",
"cityCode": "string字符串",
"nationName": "string字符串",
"provinceName": "string字符串",
"cityName": "string字符串"
},
"homeAddress": "string字符串",
"modifyTime": "string字符串",
"deliverUrl": "https://china-gateway.hdlcontrol.com/home-wisdom/app/home/deliver?homeId={homeId}&password={secretKey}",
"debugStaffUserId": "string字符串",
"debugPerm": "bool",
"createTime": "string字符串",
"localSecret": "string字符串",
"communityCode": "string字符串",
"communityId": "string字符串",
"gatewayId": "string字符串",
"deliverTime": "string字符串",
"debugStatus": "string字符串",
"tenantId": "string字符串",
"projectType": "string字符串",
"isAppDeleted": "bool"
}
}

获取网关数据

API:https://china-gateway.hdlcontrol.com/home-wisdom/app/gateway/getGatewayList。可以获取家中的网关id、AES密钥、设备id、oid、是否加密、子网id、网关状态、上次心跳包时间等信息。

发送的json如下:

1
2
3
{
"homeId": "string字符串"
}

返回的json如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"data": [{
"gatewayId": "string字符串",
"aesKey": "string字符串",
"deviceId": "int",
"deviceModel": "string字符串",
"oid": "string字符串",
"encryptionType": "int",
"gatewayType": "string字符串",
"groupName": "string字符串",
"homeId": "string字符串",
"mac": "string字符串",
"primaryKey": "string字符串,同aesKey",
"projectName": "string字符串",
"subnetId": "int",
"userName": "string字符串",
"gatewayStatus": "string字符串",
"modifyTime": "string字符串",
"localSecret": "string字符串",
"lastHeartbeatTime": "string字符串",
"slaveDevices": [{
"protocolType": "string字符串",
"deviceName": "string字符串",
"oid": "string字符串",
"addresses": "string字符串",
"deviceModel": "string字符串,同deviceName",
"mac": "string字符串"
}],
"isSupportGroupControl": "bool",
"linkDriverVersion": "string字符串"
},
{
// 有可能会有多个网关,所以使用数组
}]
}

获取家中设备MQTT广播信息

API:https://china-gateway.hdlcontrol.com/home-wisdom/app/mqtt/getRemoteInfo。能获取目标设备MQTT广播的URL、用户名和密码。其中密码会频繁更新。

发送的json如下:

1
2
3
4
5
{
"homeType": "string字符串",
"attachClientId": "string字符串",
"deviceUuid": "string字符串"
}

返回的json如下:

1
2
3
4
5
6
7
8
9
10
11
{
"data": {
"url": "tcp://china-mqtt.hdlcontrol.com:1883",
"wsUrl": "ws://china-mqtt.hdlcontrol.com:8083",
"wssUrl": "wss://china-mqtt.hdlcontrol.com:8084",
"wxsUrl": "wxs://china-mqtt.hdlcontrol.com:8084",
"clientId": "string字符串",
"userName": "string字符串",
"passWord": "string字符串"
}
}

获取新的推送消息

API:https://china-gateway.hdlcontrol.com/basis-footstone/app/pushMessage/homeHot。能获取新的推送消息(比如开门消息等)。

发送的json如下:

1
2
3
4
5
{
"homeId": "string字符串",
"channel": "HDL_HOME",
"messageLevels": [1, 2, 3, 4]
}

返回的json如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
"data": {
"id": "string字符串",
"messageTitle": "string字符串",
"messageContent": "string字符串",
"messageExpand": "
{
\"content\":\"string字符串,同messageContent\",
\"expantContent\":\"{
\\\"currentTime\\\":\\\"string字符串\\\",
\\\"picUrl\\\":\\\"string字符串\\\",
\\\"devSerial\\\":\\\"string字符串\\\",
\\\"interphoneTypeEnum\\\":\\\"string字符串\\\",
\\\"subToken\\\":\\\"string字符串\\\",
\\\"deviceSid\\\":\\\"string字符串\\\",
\\\"msgId\\\":\\\"string字符串\\\",
\\\"type\\\":\\\"string字符串\\\",
\\\"pushTime\\\":\\\"string字符串\\\",
\\\"extDevId\\\":\\\"string字符串\\\",
\\\"deviceId\\\":\\\"string字符串\\\",
\\\"spk\\\":\\\"string字符串\\\"
}\",
\"homeId\":string字符串,
\"messageLevel\":int,
\"messageType\":\"Prompt\"
}",
"isRead": "bool",
"messageLevel": "int",
"createTime": "string字符串"
}
}

获取新的token

API:https://china-gateway.hdlcontrol.com/smart-footstone/member/oauth/login。此API不需要Authorization鉴权。通过此接口更新token。

发送的json如下:

1
2
3
4
{
"refreshToken": "string字符串", // 貌似是固定的
"grantType": "refresh_token"
}

返回的json如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"data": {
"tokenUuid": "string字符串",
"accessToken": "string字符串", // 一般的请求鉴权使用此token
"tokenType": "access_token",
"headerPrefix": "Bearer ",
"expiresIn": "7200",
"expiration": "long",
"refreshToken": "string字符串",
"refreshExpiresIn": "2592000",
"refreshExpiration": "long",
"userId": "string字符串",
"userType": "string字符串",
"role": "string字符串",
"tenantId": "string字符串",
"region": "string字符串"
}
}

获取家的用电情况

API:https://china-gateway.hdlcontrol.com/home-wisdom/app/device/inverter/allInfo

发送的json如下:

1
2
3
{
"homeId": "string字符串"
}

返回的json如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"data": {
"workMode": "string字符串",
"totalElectricityPvToday": "string字符串",
"totalElectricityPvMonth": "string字符串",
"totalElectricityPvYear": "string字符串",
"totalElectricityPv": "string字符串",
"powerPvNow": "string字符串",
"powerRNow": "string字符串",
"batteryPowerNow": "string字符串",
"powerLoadNow": "string字符串",
"batterySoc": "string字符串",
"systemStatus": "string字符串",
"earningsToday": "string字符串"
}
}

获取家的设备信息

API:https://china-gateway.hdlcontrol.com/home-wisdom/app/device/list。可以获取家/房间中的设备信息。

发送的json如下:

1
2
3
4
5
6
7
8
{
"homeId": "string字符串",
"pageSize": "string字符串",
"pageNo": "string字符串",

// 可选,加上roomId即可获取此房间内的设备信息,不加则默认获取整个家的设备信息
"roomId": "string字符串"
}

返回的json如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
{
"data": {
"list": [{
"deviceId": "string字符串",
"homeId": "string字符串",
"roomIds": ["string字符串"],
"uids": ["string字符串"],
"roomInfos": [{
"floorId": "string字符串",
"floorUid": "string字符串",
"floorName": "string字符串",
"roomId": "string字符串",
"roomUid": "string字符串",
"roomName": "string字符串"
}],
"gatewayId": "string字符串",
"name": "string字符串",
"sid": "string字符串",
"spk": "string字符串",
"omodel": "string字符串",
"collect": "bool",
"online": "bool",
"controlCounter": "string字符串",
"createTime": "string字符串",
"modifyTime": "string字符串",
"productBrand": "string字符串",
"show": "int",
"productName": "string字符串",
"deviceIotId": "string字符串",

// 以下属性只有河东的设备才有
"deviceMac": "string字符串",
"oid": "string字符串",
"attributes": [{
"key": "string字符串",
"data_type": "string字符串",
"value": [
"string字符串",
// 属性的枚举值可能不止一个。如果此属性的值为数字的话则这里为空数组

//...
],
"max": "int",
"min": "int",
"sort": "int"
}, {
// 一个设备可能不止一个属性

//...
}],
"status": [{
"key": "string字符串",
"value": "string字符串"
}, {
// 设备的状态属性可能不止一个

//...
}],
"bus": {
"addresses": "0904",
"loopId": "001E"
},
"extend": "string字符串",
"productPic": "string字符串",

// 以下属性只有使用Buspro协议的设备才有
"bus": {
"addresses": "string字符串",
"loopId": "string字符串"
},

// 以下附加属性只有部分设备才有
"extDevId": "string字符串",
"localImageType": "string字符串",
"localImageIndex": "string字符串"
},{
// 可能不止一个设备

//...
}]
}
}

获取家的场景信息

API:https://china-gateway.hdlcontrol.com/home-wisdom/app/scene/list。可以获取家/房间中设置的场景信息。

发送的json如下:

1
2
3
4
5
6
7
{
"homeId": "string字符串",

// 下面都是可选的,如果添加roomId则仅返回特定房间的场景
"collect": "string字符串",
"roomId": "string字符串"
}

返回的json如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
{
"data": [{
"userSceneId": "string字符串",
"sid": "string字符串",
"name": "string字符串",
"delay": "string字符串",
"group": "string字符串",
"sceneType": "int",
"collect": "bool",
"gatewayId": "string字符串",
"modifyTime": "string字符串",
"createTime": "string字符串",
"uids": ["string字符串"],
"roomIds": ["string字符串"],
"roomInfos": [{
"floorId": "string字符串",
"floorUid": "string字符串",
"floorName": "string字符串",
"roomId": "string字符串",
"roomUid": "string字符串",
"roomName": "string字符串"
}],
"userId": "string字符串",
"can_delete": "string字符串",
"local": "string字符串",
"controlCounter": "string字符串",
"functions": [{
"sid": "string字符串",
"delay": "string字符串",
"status": [{
"key": "string字符串",
"value": "string字符串"
}, {
// 对目标设备的操作可能不止一个属性

//...
}]
}, {
// 操作可能不止一个

//...
}],
"ssdc": "bool",
"execType": "string字符串"
}, {
// 场景可能不止一个

//...
}]
}

获取家中的房间信息

API:https://china-gateway.hdlcontrol.com/home-wisdom/app/room/list。可以获取家中的房间信息。

发送的json如下:

1
2
3
4
5
6
7
8
{
"homeId": "string字符串",
"pageSize": "string字符串",
"pageNo": "string字符串",

// 可选
"roomType": "string字符串"
}

返回的json如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"data": {
"list": [{
"roomId": "string字符串",
"roomName": "string字符串",
"uid": "string字符串",
"roomType": "string字符串",
"parentId": "string字符串",
"homeId": "string字符串",
"createTime": "string字符串",
"modifyTime": "string字符串",
"floorRoomName": "string字符串",
"roomOrder": "string字符串"
}, {
// 可能不止一个房间

//...
}],
"totalCount": "string字符串",
"totalPage": "string字符串",
"pageNo": "string字符串",
"pageSize": "string字符串"
}
}

获取家中的房间详细信息

API:https://china-gateway.hdlcontrol.com/home-wisdom/app/room/listData。说是获取详细信息,其实也就多了一个roomImage的键值对。

发送的json如下:

1
2
3
4
{
"homeId": "string字符串",
"orderType": "string字符串"
}

返回的json如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"data": [{
"roomId": "string字符串",
"roomName": "string字符串",
"uid": "string字符串",
"roomType": "string字符串",
"parentId": "string字符串",
"homeId": "string字符串",
"createTime": "string字符串",
"modifyTime": "string字符串",
"floorRoomName": "string字符串",
"roomOrder": "string字符串",

// 就多了一个这个
"roomImage": "string字符串"
}, {
// 可能不止一个房间

//...
}]
}

控制设备

API:https://china-gateway.hdlcontrol.com/home-wisdom/app/device/control。可以通过此API来远程(非局域网)控制设备。

发送的json如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"homeId": "string字符串",
"gatewayId": "string字符串",
"actions": [{
"attributes": [{
"key": "on_off",
"value": "off"
},{
// 可能不止一个属性需要被修改

//...
}],
"deviceId": "string字符串",
"spk": "string字符串"
}]
}

返回的json如下:

1
2
3
{
"data": "bool"
}

通过逆向进一步获取信息

使用JADX对软件的apk逆向,惊喜地发现这个App竟然没有经过代码混淆!既然如此,我就能较为方便地分析一下了。

MQTT路径

根据com.hdl.onproandroid.utils.mqtt.MqttRecvClient这个类的描述:(有小幅度修改,以优化可读性)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
package com.hdl.onproandroid.utils.mqtt;

// 省略了部分import

import android.content.Context;
import android.util.Log;
import cn.hutool.core.thread.ThreadUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.hdl.cloud.bean.MqttAddIrDeviceInfo;
import com.hdl.onproandroid.bean.eventbus.EventAddIrDeviceChangeInfo;
import com.hdl.onproandroid.bean.eventbus.EventNotifyDunjiaLockBindSuccessInfo;
import com.hdl.onproandroid.bean.eventbus.EventNotifyIrModuleBindSuccessInfo;
import com.hdl.onproandroid.config.SPKey;
import com.hdl.onproandroid.manager.HDLDataManager;
import com.hdl.onproandroid.utils.SPUtils;
import com.hdl.onproandroid.utils.control.dunjialock.DunjiaLockType;
import com.hdl.onproandroid.utils.control.electrical.fan.ElectricalFanType;
import com.hdl.onproandroid.utils.control.hvac.HvacType;
import com.hdl.onproandroid.utils.control.ir.IrType;
import com.hdl.onproandroid.utils.control.mmw.MillimeterWaveType;
import org.eclipse.paho.client.mqttv3.IMqttActionListener;

import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
import org.eclipse.paho.client.mqttv3.MqttCallbackExtended;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.greenrobot.eventbus.EventBus;

public class MqttRecvClient {

// 省略了部分方法、字段和方法中的代码

private static String[] lastTopicFilters = new String[0];
private static String mBroker;
private static String mClientId;
private static String mPassWord;
private static String mUserName;
private static MqttRecvClient mqttRecvClient;
private static String[] topicFilters;
private MqttAsyncClient sampleClient;
private MemoryPersistence persistence = new MemoryPersistence();
private MqttConnectOptions connOpts = new MqttConnectOptions();

public static void init(Context context, String broker, String clientId, String username, String password) {
mClientId = clientId;
mBroker = broker;
mUserName = username;
mPassWord = password;

// homeId和memberId实际上等于字符串形式的homeId和memberId,是从远程服务器上得到的
String homeId = SPUtils.getString(SPKey.HOME_ID_KEY);
String memberId = HDLDataManager.getInstance().getMemberInfo().getMemberId();

topicFilters = getTopic(homeId, memberId);
if (lastTopicFilters.length == 0) {
lastTopicFilters = getTopic(homeId, memberId);
}
if (mqttRecvClient == null) {
mqttRecvClient = new MqttRecvClient();
}
}

private static String[] getTopic(String homeId, String memberId) {
return new String[] {
"/user/" + homeId + "/app/thing/property/send",
"/user/" + homeId + "/app/thing/topo/found",
"/user/" + homeId + "/app/thing/event/irCodeStudyDone/up",
"/user/" + homeId + "/app/thing/event/appHomeRefresh/up",
"/user/" + memberId + "/app/third/bind/send",
"/user/" + homeId + "/app/thing/event/intrude_alarm_event/up",
"/user/" + homeId + "/app/thing/event/tumble_alarm_event/up",
"/user/" + homeId + "/app/thing/event/stay_alarm_event/up",
"/user/" + homeId + "/app/thing/event/posture_calibration_event/up",
"/user/" + homeId + "/app/ota/device/progress/up",
"/user/" + homeId + "/app/son/session/online"
};
}

public void connect() throws InterruptedException {
try {
MqttAsyncClient mqttAsyncClient = this.sampleClient;
if (mqttAsyncClient != null) {
mqttAsyncClient.close();
}
this.sampleClient = new MqttAsyncClient(mBroker, mClientId, this.persistence);
this.connOpts.setUserName(mUserName);
this.connOpts.setPassword(mPassWord.toCharArray());
this.connOpts.setCleanSession(true);
this.connOpts.setKeepAliveInterval(10);
this.connOpts.setAutomaticReconnect(true);
this.connOpts.setConnectionTimeout(10);
this.connOpts.setMqttVersion(4);
this.sampleClient.setCallback(new MqttCallbackExtended() {

// 省略需要重写的connectComplete、connectionLost、deliveryComplete方法,这里只需要看重写的messageArrived方法

@Override
public void messageArrived(String str, MqttMessage mqttMessage) throws Exception {
Log.i("MQTTService", "------------mqttMessage topic=" + str);
if (str.endsWith("/app/third/bind/send")) {

// 这段就是在记录第三方登录的event事件,说明/user/{memberId}/app/third/bind/send这个topic用于接收第三方登录和绑定事件
if (mqttMessage.toString().contains("true")) {
EventBus.getDefault().post(new EventNotifyThirdAuthInfo(true));
return;
} else {
EventBus.getDefault().post(new EventNotifyThirdAuthInfo(false));
return;
}

}
try {

// 这段是在记录红外设备状态更新的事件,说明/user/{homeId}/app/thing/topo/found这个topic用于接收红外设备的状态更新事件。不过从MQTT收到的消息需要AesUtil这个类来解密。这个类在下面会提到
if (str.endsWith("/app/thing/topo/found")) {
EventAddIrDeviceChangeInfo eventAddIrDeviceChangeInfo = (EventAddIrDeviceChangeInfo) JSONObject.parseObject(AesUtil.jiemi(mqttMessage.getPayload(), SPUtils.getString(SPKey.HOME_ID_KEY)), EventAddIrDeviceChangeInfo.class);
List<MqttAddIrDeviceInfo> objects = eventAddIrDeviceChangeInfo.getObjects();
if (objects.size() > 0) {
String spk = objects.get(0).getSpk();
if (HvacType.HVAC_IR_AC_TYPE.equals(spk) ||
IrType.IR_TV_TYPE.equals(spk) ||
ElectricalFanType.IR_FAN.equals(spk) ||
IrType.IR_STB_TYPE.equals(spk) ||
IrType.IR_DVD_TYPE.equals(spk) ||
IrType.IR_PJT_TYPE.equals(spk) ||
"ir.custom".equals(spk) ||
IrType.IR_LEARN_TYPE.equals(spk)) {

eventAddIrDeviceChangeInfo.setTopic(str);
EventBus.getDefault().post(eventAddIrDeviceChangeInfo);
} else if ("ir.module".equals(spk) || MillimeterWaveType.SENSOR_HDL_MMW_POSE.equals(spk)) {
EventBus.getDefault().post(new EventNotifyIrModuleBindSuccessInfo());
} else if (DunjiaLockType.SECURITY_LOCK.equals(spk) ||
DunjiaLockType.SECURITY_FACELOCK.equals(spk) ||
DunjiaLockType.SECURITY_VIDEOLOCK.equals(spk)) {
EventBus.getDefault().post(new EventNotifyDunjiaLockBindSuccessInfo());
}
}
} else if (str.endsWith("/app/thing/event/irCodeStudyDone/up")) {
// 与上面的类似,说明/user/{homeId}/app/thing/event/irCodeStudyDone/up用于红外设备码学习完成的事件,无需解密,直接是json数据

// 代码都差不多的,都是上报事件,只是没有调用AesUtil.jiemi()方法解密,所以此处省略

//...
} else if (str.endsWith("/app/thing/event/appHomeRefresh/up")) {
// 刷新事件,无需解密

//...
} else if (str.endsWith("/app/thing/event/intrude_alarm_event/up") ||
str.endsWith("/app/thing/event/tumble_alarm_event/up") ||
str.endsWith("/app/thing/event/stay_alarm_event/up")) {
// 毫米波Alarm改变事件,需要解密

//...
} else if (str.endsWith("/app/thing/event/posture_calibration_event/up")) {
// 毫米波Posture改变事件,需要解密

//...
} else if (str.endsWith("/app/ota/device/progress/up")) {
// 毫米波Progress改变事件,需要解密

//...
} else if (str.endsWith("/app/son/session/online")) {
// 设备离在线状态更新事件,需要解密

// ...
} else {
// 收到来自topic(/user/{homeId}/app/thing/property/send)的设备改变信息并上报事件(需要解密)

//...
}
} catch (Exception unused) {}
}
});
this.sampleClient.connect(this.connOpts);
int i = 0;
while (!this.sampleClient.isConnected()) {
Thread.sleep(1000L);
i++;
if (i >= 10 || this.sampleClient.isConnected()) {
break;
}
}
if (this.sampleClient.isConnected()) {
ThreadUtil.sleep(1000L);
subscribeAllTopics();
}
} catch (Exception e) {
e.printStackTrace();
}
}

public void subscribeAllTopics() {
MqttAsyncClient mqttAsyncClient = this.sampleClient;
if (mqttAsyncClient == null) {
return;
}
if ((mqttAsyncClient == null || mqttAsyncClient.isConnected()) && this.sampleClient.isConnected()) {
try {
Log.i("MQTTService", "------------topicFilters==" + JSON.toJSONString(topicFilters));
this.sampleClient.unsubscribe(lastTopicFilters);
ArrayList arrayList = new ArrayList();
for (int i = 0; i < topicFilters.length; i++) {
arrayList.add(0);
}
int[] iArr = new int[arrayList.size()];
for (int i2 = 0; i2 < arrayList.size(); i2++) {
iArr[i2] = ((Integer) arrayList.get(i2)).intValue();
}
this.sampleClient.subscribe(topicFilters, iArr, (Object) null, new IMqttActionListener() {
// 里面Override的方法内部全是空的
});
lastTopicFilters = topicFilters;
} catch (MqttException e) {
e.printStackTrace();
}
}
}
}

简单来看就是通过这个类的init方法传入broker、clientId、username、password参数并初始化MQTT,getTopic方法记载了所有的topic路径,connect方法记载了这些topic具体的作用,同时也告诉了解密方法存放的位置。总体而言,有如下收获:

topic列表topic内容是否需解密
/user/{homeId}/app/thing/property/send设备改变信息事件
/user/{homeId}/app/thing/topo/found红外设备状态更新事件
/user/{homeId}/app/thing/event/irCodeStudyDone/up红外设备码学习完成事件
/user/{homeId}/app/thing/event/appHomeRefresh/up刷新事件
/user/{memberId}/app/third/bind/send第三方登录和绑定事件
/user/{homeId}/app/thing/event/intrude_alarm_event/up毫米波Alarm改变事件
/user/{homeId}/app/thing/event/tumble_alarm_event/up毫米波Alarm改变事件
/user/{homeId}/app/thing/event/stay_alarm_event/up毫米波Alarm改变事件
/user/{homeId}/app/thing/event/posture_calibration_event/up毫米波Posture改变事件
/user/{homeId}/app/ota/device/progress/up毫米波Progress改变事件
/user/{homeId}/app/son/session/online设备离在线状态更新事件

MQTT数据解密

根据上面的描述,从MQTT传输的消息大部分都是经过加密的,所以需要解密来读取信息。根据上面,可以定位到解密算法所在的类com.hdl.onproandroid.utils.mqtt.AesUtil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.hdl.onproandroid.utils.mqtt;

// 省略了import

public class AesUtil {

// 省略了部分方法、字段和方法中的代码

public static String jiemi(byte[] bytes, String homeId) {
return new String(decrypt(bytes, HouseIdSecretUtil.getSecret(homeId).getBytes(), "AES/CBC/PKCS7Padding", true, null););
}

public static byte[] decrypt(byte[] bArr, byte[] bArr2, String str, Boolean bool, byte[] bArr3) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException {
try {
SecretKeySpec secretKeySpec = new SecretKeySpec(bArr2, "AES");
Cipher cipher = Cipher.getInstance(str);
if (bool != null && bool.booleanValue()) {
if (bArr3 != null) {
bArr2 = bArr3;
}
cipher.init(2, secretKeySpec, new IvParameterSpec(bArr2));
} else {
cipher.init(2, secretKeySpec);
}
return cipher.doFinal(bArr);
} catch (Exception e) {
// 处理异常,记录日志,返回null

//...
}
}
}

已经确定解密使用AES/CBC/PKCS7Padding,且IV与Secret相同。接下来需要确定HouseIdSecretUtil.getSecret()的密钥怎么算的了。发现这个HouseIdSecretUtil类和这个类其实在同一个包中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.hdl.onproandroid.utils.mqtt;

public class HouseIdSecretUtil {
public static String getSecret(String str) {
String string = new StringBuffer().append(str).reverse().toString();
if (string.length() > 16) {
return string.substring(0, 16);
}
for (int length = string.length(); length < 16; length++) {
string = string + "0";
}
return string;
}

public static void main(String[] strArr) {
System.out.println(getSecret("1363358800782790658"));
}
}

现在知道HouseIdSecretUtil.getSecret()实际上是把输入的字符串前后翻转,然后取前16位(如果长度大于16)或在后面补0(如果小于16位),直到最后有16位为止。这样就明白MQTT中传输的数据应该如何解密了。至于HouseIdSecretUtil类中出现了一个main方法,初步推测可能是测试的时候没有删除干净。

现在尝试解密一下MQTT的频道吧。连接到mqtt://china-mqtt.hdlcontrol.com:1883并填入上面获取的clientId、用户名和密码,然后订阅topic/user/{userId}/app/thing/property/send。你会收到很多消息(QoS=0),而这些消息都是二进制数据,使用上面的解密方法解密后会得到如下json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"id": "string字符串",
"objects": [
{
"sid": "string字符串",
"status": [
{
"key": "string字符串",
"value": "string字符串"
},
{
// 可能不止一个状态

//...
}
]
},{
// 可能不止一个设备

//...
}
],
"time_stamp": "string字符串"
}

查验后端URL

com.hdl.cloud.HdlCloudApi类中记录了后端各个API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
package com.hdl.cloud;

public class HdlCloudApi {
public static final String ADD_CHILD_ACCOUNT = "/home-wisdom/app/child/account/add";
public static final String ADD_DEVICE_COLLECT = "/home-wisdom/app/device/collect";
public static final String ADD_DUNJIALOCK_TEMPPWD = "/home-wisdom/app/device/lock/temp/pwd/add";
public static final String ADD_LOGIC = "/home-wisdom/app/logic/add";
public static final String ADD_ROOM = "/home-wisdom/app/room/add";
public static final String ADD_SCENE = "/home-wisdom/app/scene/add";
public static final String ADD_SECURITY = "/home-wisdom/app/security/add";
public static final String ADD_SHARE = "/home-wisdom/app/share/add";
public static final String ASSIGN_DEVICE_TO_HOUSE = "/home-wisdom/app/open/assignDeviceToHouse";
public static final String BIND_CHILD_ACCOUNT = "/home-wisdom/app/child/account/accountBind";
public static final String BIND_HOME = "/home-wisdom/source/screen/home/bind";
public static final String BIND_WITH_ACCOUNT = "/smart-footstone/member/memberInfo/bindWithAccount";
public static final String CANCEL_COLLECT_SCENE = "/home-wisdom/app/scene/cancelCollect";
public static final String CANCEL_DEVICE_COLLECT = "/home-wisdom/app/device/cancelCollect";
public static final String CHECK_APP_VERSION_URL = "/basis-footstone/app/appVersion/check";
public static final String CHECK_MESSAGE = "/smart-footstone/verification/message/check";
public static final String CLEAR_ALL_MESSAGE = "/smart-footstone/app/message/clear";
public static final String CLEAR_MMW_INTRUDE_ALARM = "/home-wisdom/app/device/mmw/intrude/alarmClear";
public static final String CLEAR_MMW_STAY_ALARM = "/home-wisdom/app/device/mmw/stay/alarmClear";
public static final String CLEAR_MMW_TUMBLE_ALARM = "/home-wisdom/app/device/mmw/tumble/alarmClear";
public static final String CODE_TEST = "/home-wisdom/app/device/ir/codeTest";
public static final String COLLECT_SCENE = "/home-wisdom/app/scene/collect";
public static final String CONTROL_DEVICE = "/home-wisdom/app/device/control";
public static final String DELETE_CHILD_ACCOUNT = "/home-wisdom/app/child/account/delete";
public static final String DELETE_DEVICE = "/home-wisdom/app/device/remove";
public static final String DELETE_DUNJIALOCK_KEY = "/home-wisdom/app/device/lock/key/delete";
public static final String DELETE_DUNJIALOCK_TEMPPWD = "/home-wisdom/app/device/lock/temp/pwd/delete";
public static final String DELETE_LOGIC = "/home-wisdom/app/logic/delete";
public static final String DELETE_ROOM = "/home-wisdom/app/room/delete";
public static final String DELETE_ROOM_BYHOME = "/home-wisdom/app/room/deleteByHome";
public static final String DELETE_SCENE = "/home-wisdom/app/scene/delete";
public static final String DELETE_SECURITY = "/home-wisdom/app/security/delete";
public static final String DELETE_SHARE = "/home-wisdom/app/share/delete";
public static final String DELETE_YINSHI_DEVICE = "/home-wisdom/platform/yingshi/child/deleteDevice";
public static final String DOOR_PWD_CONFIRM = "/home-wisdom/app/device/door/pwdConfirm";
public static final String EDIT_DEVICE_INFO = "/home-wisdom/app/device/edit";
public static final String EDIT_DEVICE_LOCAL_IMAGE = "/home-wisdom/app/device/edit/local/image";
public static final String EDIT_DUNJIALOCK_TEMPPWD = "/home-wisdom/app/device/lock/temp/pwd/edit";
public static final String EDIT_LOGIC = "/home-wisdom/app/logic/update";
public static final String EDIT_SCENE = "/home-wisdom/app/scene/update";
public static final String EDIT_SECURITY = "/home-wisdom/app/security/edit";
public static final String EXECUTE_SCENE = "/home-wisdom/app/scene/execute";
public static final String FIND_ALL_ACCOUNT = "/home-wisdom/app/child/account/findAll";
public static final String FIND_CITY_URL = "https://developer.hdlcontrol.com/Weather/Weather/FindCity/";
public static final String FORGET_PASSWORD = "/smart-footstone/member/oauth/forgetPwd";
public static final String FORMAT_LOCK_SDCARD = "/home-wisdom/platform/yingshi/lock/sdCard/format";
public static final String GET_AIRQUALITY_AND_WEATHER_URL = "https://developer.hdlcontrol.com/Weather/Weather/GetAirQualityAndWeather/";
public static final String GET_AISUPPORT_LIST = "/home-wisdom/platform/yingshi/getAiSupportList";
public static final String GET_ALARM_LIST = "/community-wisdom/mgmt/statistics/alarmEvent";
public static final String GET_ALARM_RECORDS_URL = "/home-wisdom/app/device/alarmRecords/listByPage";
public static final String GET_AUTH_PRODUCT_BRAND_DEVICE_LIST = "/home-wisdom/app/open/extDeviceList";
public static final String GET_AUTH_PRODUCT_BRAND_LIST = "/smart-open/platform/auth/brand/list";
public static final String GET_COLLECTLIST_BYTYPE = "/home-wisdom/app/home/homePage/collectListByType";
public static final String GET_CUSTOMER_DETAIL = "/basis-user/app/customer/detail";
public static final String GET_DAHUA_CHILD_ROKEN = "/home-wisdom/imou/openapi/getSubToken";
public static final String GET_DEFAULT_PIC = "/home-wisdom/app/images/getDefaultPictures";
public static final String GET_DEVICE_HOURWEEKMONTH_DATA = "/home-wisdom/app/statistics/device/hourWeekMonth";
public static final String GET_DEVICE_LIST = "/home-wisdom/app/device/list";
public static final String GET_DEVICE_MESSAGERULESSET = "/home-wisdom/app/device/getDeviceMessageRulesSet";
public static final String GET_DEVICE_MESSAGE_LSIT = "/home-wisdom/app/device/message";
public static final String GET_DISCOVERY_GET = "/website-footstone/website/browse/content/page/app";
public static final String GET_DOORBELL_TONE = "/home-wisdom/platform/yingshi/lock/getDoorBellTone";
public static final String GET_DOOR_HISTORY = "/home-wisdom/app/device/door/logs";
public static final String GET_DUNJIALOCK_KEY_LIST = "/home-wisdom/app/device/lock/key/list";
public static final String GET_DUNJIALOCK_TEMPPWD_LIST = "/home-wisdom/app/device/lock/temp/pwd/list";
public static final String GET_FACE_RECOGNITION_UNLOCKCFG = "/home-wisdom/platform/yingshi/lock/getFaceRecognitionUnlockCfg";
public static final String GET_FACE_UNLOCK_ENABLE = "/home-wisdom/app/device/lock/face/unlock/enable/get";
public static final String GET_FACE_UNLOCK_TRIGGER = "/home-wisdom/app/device/lock/face/unlock/trigger/get";
public static final String GET_FULLDAY_RECORD_STATE = "/home-wisdom/platform/yingshi/getFulldayRecordStatus";
public static final String GET_GATEWAY_LIST = "/home-wisdom/app/gateway/getGatewayList";
public static final String GET_GROUPCONTROL_ADD = "/home-wisdom/app/device/groupcontrol/add";
public static final String GET_GROUPCONTROL_DELETE = "/home-wisdom/app/device/groupcontrol/deleted";
public static final String GET_GROUPCONTROL_EXCUTE = "/home-wisdom/app/device/groupcontrol/controlDown";
public static final String GET_GROUPCONTROL_INFO = "/home-wisdom/app/device/groupcontrol/infos";
public static final String GET_GROUPCONTROL_LIST = "/home-wisdom/app/device/groupcontrol/list";
public static final String GET_GROUPCONTROL_UPDATE = "/home-wisdom/app/device/groupcontrol/update";
public static final String GET_GROUPLIST_URL = "/home-wisdom/app/wise/music/groupList";
public static final String GET_GROUP_PLAYERLIST_URL = "/home-wisdom/app/wise/music/groupPlayerList";
public static final String GET_HOMEPAGE_INFO = "/home-wisdom/app/home/homePage/info";
public static final String GET_HOME_INFO = "/home-wisdom/source/screen/home/spaceInfo";
public static final String GET_HOME_INFO_DETAIL = "/home-wisdom/app/home/info";
public static final String GET_HOME_LIST = "/home-wisdom/app/home/list";
public static final String GET_HOME_MESSAGE = "/basis-footstone/app/pushMessage/homeHot";
public static final String GET_HOME_URL = "/home-wisdom/app/home/obtainDeliveryUrl";
public static final String GET_IMAGE_URL = "/home-wisdom/app/images/get_image_url";
public static final String GET_IMOU_LIST_VISUAL_SPEAKS_URL = "/home-wisdom/platform/imou/listVisualSpeaks";
public static final String GET_INVERTER_ALLINFO = "/home-wisdom/app/device/inverter/allInfo";
public static final String GET_IR_LIST = "/home-wisdom/app/device/ir/list";
public static final String GET_LOCK_BATTERY_MODE = "/home-wisdom/platform/yingshi/lock/battery/mode";
public static final String GET_LOCK_OPENRECORD = "/home-wisdom/platform/yingshi/lock/open/recoding";
public static final String GET_LOCK_REMOTECONFIG = "/home-wisdom/platform/yingshi/lock/remoteConfig/get";
public static final String GET_LOCK_SENSITIVITY = "/home-wisdom/app/device/lock/rader/sensiticity/get";
public static final String GET_LOCK_SYSTEMSOUND = "/home-wisdom/platform/yingshi/lock/getDoorLockSystemSound";
public static final String GET_LOGIC_DETAIL = "/home-wisdom/app/logic/info";
public static final String GET_LOGIC_LIST = "/home-wisdom/app/logic/list";
public static final String GET_MEMBERINFO_BY_ACCOUNT = "/smart-footstone/member/memberInfo/getMemberInfoByAccount";
public static final String GET_MEMBER_INFO = "/smart-footstone/member/memberInfo/getMemberInfo";
public static final String GET_MESSAGE_LIST = "/smart-footstone/app/message/page";
public static final String GET_MESSAGE_LIST_URL = "/smart-footstone/app/message/page";
public static final String GET_MESSAGE_SUBSCRIBE_CONFIGINFOS = "/basis-footstone/app/message/subscribe/configInfos";
public static final String GET_MESSAGE_UNREAD_STATUS = "/basis-footstone/app/pushMessage/categoryUnread";
public static final String GET_MMW_CONFIGINFO = "/home-wisdom/app/device/mmw/postureCalibration/configInfo";
public static final String GET_MMW_DEVICEDRIVERS = "/home-wisdom/app/device/ota/getDeviceDrivers";
public static final String GET_MMW_DEVICE_FIND = "/home-wisdom/app/device/mmw/deviceFind";
public static final String GET_MMW_DRIVERVERSIONNEW = "/home-wisdom/app/device/ota/getDriverVersionNew";
public static final String GET_MMW_INTRUDE_CONFIGINFO = "/home-wisdom/app/device/mmw/intrude/configInfo";
public static final String GET_MMW_SPACE = "/home-wisdom/app/device/mmw/subSpace/configList";
public static final String GET_MMW_STAY_CONFIGINFO = "/home-wisdom/app/device/mmw/stay/configInfo";
public static final String GET_MMW_TUMBLE_CONFIGINFO = "/home-wisdom/app/device/mmw/tumble/configInfo";
public static final String GET_NOTIFY_SOUNDCFG = "/home-wisdom/platform/yingshi/lock/loitering/notify/cfg";
public static final String GET_OID_LIST = "/home-wisdom/app/device/oid/list";
public static final String GET_OWNER_QRCODE_URL = "/community-wisdom/doorDevice/getOwnerQRCode";
public static final String GET_PLAYERLIST_URL = "/home-wisdom/app/wise/music/playerList";
public static final String GET_PRODUCT_BRAND_LIST = "/home-wisdom/app/product/brand/list";
public static final String GET_PRODUCT_LIST = "/home-wisdom/app/product/list";
public static final String GET_REMOTE_OPENLOCK_ENABLE = "/home-wisdom/app/device/lock/remote/open/enable/get";
public static final String GET_ROOM_LIST = "/home-wisdom/app/room/list";
public static final String GET_ROOM_LISTWITHDATA = "/home-wisdom/app/room/listData";
public static final String GET_ROOM_LIST_ORDER = "/home-wisdom/app/room/list";
public static final String GET_SCENE_DETAIL = "/home-wisdom/app/scene/info";
public static final String GET_SCENE_LIST = "/home-wisdom/app/scene/list";
public static final String GET_SCREENSAVER_GET = "/home-wisdom/source/screen/screensaver/info";
public static final String GET_SCREENSAVER_SET = "/home-wisdom/source/screen/screensaver/set";
public static final String GET_SDCARD_STATE = "/home-wisdom/platform/yingshi/lock/sdCard/state/get";
public static final String GET_SECURITY_INFO = "/home-wisdom/app/security/info";
public static final String GET_SECURITY_LIST = "/home-wisdom/app/security/list";
public static final String GET_SECURITY_SOS_INFO = "/home-wisdom/app/security/sos/info";
public static final String GET_SECURITY_STATUS = "/home-wisdom/app/security/statusRead";
public static final String GET_SHARE_LIST = "/home-wisdom/app/share/list";
public static final String GET_SIP_ACCOUNT_URL = "/home-wisdom/app/home/getSipAccount";
public static final String GET_TANGE_TOKEN = "/home-wisdom/app/tange/token";
public static final String GET_UNREGISTER_MEMBER = "/basis-footstone/member/unregister";
public static final String GET_WEATHER = "/basis-footstone/app/weather/getWeatherNowByHouse";
public static final String GET_YINSHI_CHILD_ROKEN = "/home-wisdom/platform/yingshi/child/token";
public static final String GET_YINSHI_LOCK_BATTERY_DETAILS = "/home-wisdom/platform/yingshi/lock/battery/details";
public static final String GET_YINSHI_LOCK_MODELS = "/home-wisdom/platform/yingshi/lock/models";
public static final String GET_YINSHI_LOCK_STATUS = "/home-wisdom/platform/yingshi/lock/status";
public static final String HASBIND_TANGE = "/home-wisdom/app/tange/hasBind";
public static final String HOME_TRANSFER = "/home-wisdom/app/home/transfer";
public static final String IMOU_CALL_ALLREJECTION_URL = "/home-wisdom/platform/imou/callAllRejection";
public static final String IMOU_OPEN_DOORBELL_URL = "/home-wisdom/platform/imou/openDoorbell";
public static final String IMOU_UPDATE_CALLSTATUS_URL = "/home-wisdom/platform/imou/updateCallStatus";
public static final String INCRADD_DEVICE_OID = "/home-wisdom/app/device/oid/incrAdd";
public static final String INPUT_USERFACE_HOUSE = "/community-wisdom/app/doorDevice/inputUserFaceHouse";
public static final String JPUSH_LOGOUT_URL = "/smart-footstone/app/push-information/unBindPushToken";
public static final String JPUSH_REGISTER_URL = "/smart-footstone/app/push-information/addPushToken";
public static final String LOGIN = "/smart-footstone/member/oauth/login";
public static final String MQTT_INFO_URL = "/home-wisdom/app/mqtt/getRemoteInfo";
public static final String OWNER_CONVERT = "/home-wisdom/app/home/ownerConvert";
public static final String POST_ADD_IR = "/home-wisdom/app/device/ir/add";
public static final String POST_DEVICE_INFO = "/home-wisdom/app/device/info";
public static final String POST_DEVICE_INFO_BY_SID = "/home-wisdom/app/device/infoBySid";
public static final String POST_ELECTRICITY_CHOOSE_DEVICE_LIST = "/home-wisdom/app/electricity/meter/device/listBySelect";
public static final String POST_ELECTRICITY_DEVICE_LIST = "/home-wisdom/app/electricity/meter/device/list";
public static final String POST_ELECTRICITY_SET_CHOOSE_DEVICE_LIST = "/home-wisdom/app/electricity/meter/device/add";
public static final String POST_GET_ELECTRICITY_COST = "/home-wisdom/app/statistics/electricity/meter/cost";
public static final String POST_GET_ELECTROVALENCE = "/home-wisdom/app/electrovalence/get";
public static final String POST_GET_IR_DEVICEBRANDLIST = "/smart-footstone/app/ir/brand/list";
public static final String POST_GET_IR_DEVICECODELIST = "/smart-footstone/app/ir/code/list";
public static final String POST_GET_IR_DEVICETYPELIST = "/smart-footstone/app/ir/device-type/list";
public static final String POST_GET_PHASE_STATISTICS = "/home-wisdom/app/statistics/electricity/meter/phase";
public static final String POST_GET_PHASE_STATISTICS_ATTIME = "/home-wisdom/app/statistics/electricity/meter/phase/info";
public static final String POST_GET_PHASE_STATISTICS_TOTAL = "/home-wisdom/app/statistics/electricity/meter/phase/total";
public static final String POST_GET_STATISTICS = "/home-wisdom/app/statistics/electricity/meter/energy";
public static final String POST_GET_STATISTICS_DETAIL = "/home-wisdom/app/statistics/electricity/meter/details";
public static final String POST_INDEPENT_REGISTER = "/home-wisdom/app/device/independentRegister";
public static final String POST_IR_CODEREMOVE = "/home-wisdom/app/device/ir/codeRemove";
public static final String POST_IR_CODESORT = "/home-wisdom/app/device/ir/codeSortByKey";
public static final String POST_IR_CODESTUDY = "/home-wisdom/app/device/ir/codeStudy";
public static final String POST_LINK_JOIN = "/home-wisdom/app/gateway/linkJoin";
public static final String POST_SET_ELECTROVALENCE = "/home-wisdom/app/electrovalence/set";
public static final String POST_SOUND_DEL = "/smart-footstone/app/token/delete";
public static final String POST_SOUND_DEVICE_BYROOM = "/home-wisdom/app/device/listByGroupRoom";
public static final String POST_SOUND_LIST = "/smart-footstone/app/token/list";
public static final String POST_SOUND_RELATION_LIST = "/home-wisdom/app/tokenRelation/list";
public static final String POST_SOUND_RELATION_SAVE = "/home-wisdom/app/tokenRelation/save";
public static final String POST_SOUND_SCENE_BYROOM = "/home-wisdom/app/scene/listByGroupRoom";
public static final String POST_SOUND_UPDATE = "/smart-footstone/app/token/update";
public static final String READ_ALL_MESSAGE_URL = "/smart-footstone/app/message/read_all";
public static final String REGION_BY_ACCOUNT = "/smart-footstone/region/regionByAccount";
public static final String REGISTER = "/smart-footstone/member/oauth/register";
public static final String REMOTE_LOCK_WAKEUP = "/home-wisdom/app/device/lock/remote/wakeup";
public static final String REMOTE_OPEN_DOOR = "/home-wisdom/app/device/door/remoteOpen";
public static final String REMOTE_OPEN_LOCK = "/home-wisdom/app/device/lock/remote/open";
public static final String REMOVE_CHILD_ACCOUNT_FACE = "/home-wisdom/app/child/account/removeFace";
public static final String RENAME_DEVICE_INFO = "/home-wisdom/app/device/rename";
public static final String ROOM_DATA_SORT = "/home-wisdom/app/room/dataSort";
public static final String SEND_MESSAGE = "/smart-footstone/verification/message/send";
public static final String SET_AISUPPORT_LIST = "/home-wisdom/platform/yingshi/setAiSupport";
public static final String SET_COLLECT_CANCEL = "/home-wisdom/app/home/homePage/collectCancel";
public static final String SET_COLLECT_EDIT = "/home-wisdom/app/home/homePage/collectEdit";
public static final String SET_DEVICE_EXT = "/home-wisdom/app/device/deviceExtSet";
public static final String SET_DEVICE_MESSAGERULES = "/home-wisdom/app/device/deviceMessageRulesSet";
public static final String SET_DOORBELL_TONE = "/home-wisdom/platform/yingshi/lock/setDoorBellTone";
public static final String SET_FACE_RECOGNITION_UNLOCKCFG = "/home-wisdom/platform/yingshi/lock/setFaceRecognitionUnlockCfg";
public static final String SET_FACE_UNLOCK_ENABLE = "/home-wisdom/app/device/lock/face/unlock/enable/set";
public static final String SET_FACE_UNLOCK_TRIGGER = "/home-wisdom/app/device/lock/face/unlock/trigger/set";
public static final String SET_FULLDAY_RECORD_STATE = "/home-wisdom/platform/yingshi/setFulldayRecordStatus";
public static final String SET_LOCK_BATTERY_MODE = "/home-wisdom/platform/yingshi/lock/battery/mode/set";
public static final String SET_LOCK_OPENRECORD = "/home-wisdom/platform/yingshi/lock/open/recoding/set";
public static final String SET_LOCK_REMOTECONFIG = "/home-wisdom/platform/yingshi/lock/remoteConfig";
public static final String SET_LOCK_SENSITIVITY = "/home-wisdom/app/device/lock/rader/sensiticity/set";
public static final String SET_LOCK_SYSTEMSOUND = "/home-wisdom/platform/yingshi/lock/setDoorLockSystemSound";
public static final String SET_LOGIC_ENABLE = "/home-wisdom/app/logic/enable";
public static final String SET_MESSAGE_READ_BY_TYPE = "/smart-footstone/app/message/read_all";
public static final String SET_MESSAGE_SUBSCRIBE_CONFIGINFOS = "/basis-footstone/app/message/subscribe/config";
public static final String SET_MMW_CONFIGEDIT = "/home-wisdom/app/device/mmw/postureCalibration/configEdit";
public static final String SET_MMW_DEVICEDRIVER_UPGRADE = "/home-wisdom/app/device/ota/deviceDriverUpgrade";
public static final String SET_MMW_INTRUDE_CONFIGINFO = "/home-wisdom/app/device/mmw/intrude/configEdit";
public static final String SET_MMW_SPACE = "/home-wisdom/app/device/mmw/subSpace/configEdit";
public static final String SET_MMW_STAY_CONFIGINFO = "/home-wisdom/app/device/mmw/stay/configEdit";
public static final String SET_MMW_TUMBLE_CONFIGINFO = "/home-wisdom/app/device/mmw/tumble/configEdit";
public static final String SET_NOTIFY_SOUNDCFG = "/home-wisdom/platform/yingshi/lock/loitering/notify/cfg/set";
public static final String SET_REMOTE_OPENLOCK_ENABLE = "/home-wisdom/app/device/lock/remote/open/enable/set";
public static final String SET_SECURITY_SOS_TRIGGER = "/home-wisdom/app/security/sos/trigger";
public static final String SET_SECURITY_SOS_UPDATE = "/home-wisdom/app/security/sos/update";
public static final String SET_SECURITY_STATUS = "/home-wisdom/app/security/statusSet";
public static final String SET_WRAPPER = "/home-wisdom/app/home/homePage/wallpaperUpdate";
public static final String UNBIND_PRODUCT_BRAND_LIST = "/smart-open/open-platform/tripartite/userUnbind";
public static final String UNBIND_WITH_ACCOUNT = "/smart-footstone/member/memberInfo/unbindWithAccount";
public static final String UPDATA_CUSTOMER_FACE_CLOSE = "/community-wisdom/app/doorDevice/updateCustomerFaceClose";
public static final String UPDATE_CHILD_ACCOUNT = "/home-wisdom/app/child/account/update";
public static final String UPDATE_CHILD_ACCOUNT_FACE = "/home-wisdom/app/child/account/updateFace";
public static final String UPDATE_DEBUG_STATUS = "/home-wisdom/app/home/updateDebugPerm";
public static final String UPDATE_DUNJIALOCK_KEY = "/home-wisdom/app/device/lock/key/update";
public static final String UPDATE_HOME = "/home-wisdom/app/home/update";
public static final String UPDATE_MEMBER_INFO = "/smart-footstone/member/memberInfo/updateMemberInfo";
public static final String UPDATE_PASSWORD = "/smart-footstone/member/memberInfo/updatePwd";
public static final String UPDATE_ROOM = "/home-wisdom/app/room/update";
public static final String UPLOAD_IMAGE_URL = "/basis-cosmos/file/upload";

public static String getRequestUrl(String str) {
return HdlCloudLink.getInstance().getCloudUrl() + str;
}
}

至于主机名,直接使用抓包时看到的主机名即可。实际上,HdlCloudLink.getInstance().getCloudUrl()就是在获取主机名。中国区的主机名一般是https://china-gateway.hdlcontrol.com

客户端发送json时携带的sign的生成算法

App向后端发送json消息时,大部分都会在最后附带appKeytimestampsign三个字段。appKey好说,因为这是一个被写死了的字符串字段CXZMMOCF(通过抓包就能知道是一个不变的字段。这个字段定义在com.hdl.onproandroid.buildConfig类中,这个类也全是各种public static final字段)。时间戳也好说,获取当前时间就行(实际上乱填都没问题,只要sign计算正确)。那么问题就在这个sign字段上。

我打算先从控制设备的类(com.hdl.cloud.controller.DeviceController类)开始下手:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.hdl.cloud.controller;

import com.google.gson.JsonObject;
import com.hdl.cloud.HdlCloudApi;
import com.hdl.cloud.callback.CallBackListener;
import com.hdl.cloud.exception.HDLException;
import com.hdl.cloud.response.HDLResponse;
import com.hdl.cloud.utils.GsonUtils;
import io.reactivex.rxjava3.disposables.Disposable;

public class DeviceController {
public static Disposable controlDevice(String homeId, String gatewayId, ControlDeviceDb controlDeviceDb, final CallBackListener callBackListener) {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("homeId", homeId);
jsonObject.addProperty("gatewayId", gatewayId);
ArrayList arrayList = new ArrayList();

// 这个controlDeviceDb实际上是一个bean,记录了目标设备的id,spk和一些操作属性。下面会说
arrayList.add(controlDeviceDb);

// GsonUtils.getGson()实际上是获取了一个Gson对象
jsonObject.add("actions", GsonUtils.getGson().toJsonTree(arrayList));

return (Disposable) HxHttp.builder()
.url(HdlCloudApi.getRequestUrl(HdlCloudApi.CONTROL_DEVICE)) // 这个HdlCloudApi在上面提过了
.raw(jsonObject.toString())
.build().post().subscribeWith(new HDLResponse<String>() {

@Override // com.hdl.cloud.response.HDLResponse
public void onResponse(String str3) {
CallBackListener callBackListener2 = callBackListener;
if (callBackListener2 != null) {
callBackListener2.onSuccess(str3);
}
}

@Override // com.hdl.cloud.response.HDLResponse
public void onFailure(HDLException hDLException) {
CallBackListener callBackListener2 = callBackListener;
if (callBackListener2 != null) {
callBackListener2.onError(hDLException);
}
}
});
}
}

发现在这里post时并没有传入类似的参数。那么说明是在此处调用post函数的过程中动态注入了sign参数。所以需要查看HxHttp.builder().url().raw().build().post()这一串究竟在干什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.hdl.hdlhttp;

import android.view.View;
import androidx.lifecycle.LifecycleOwner;
import com.hdl.hdlhttp.client.RetrofitCreator;
import com.trello.lifecycle4.android.lifecycle.AndroidLifecycle;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.FlowableTransformer;
import io.reactivex.rxjava3.schedulers.Schedulers;
import okhttp3.RequestBody;
import org.reactivestreams.Publisher;

public class HxHttp {
private final RequestBody BODY;
private final File FILE;
private final String FILE_NAME;
private final WeakHashMap<String, Object> HEADERS;
private final WeakHashMap<String, Object> PARAMS;
private final String URL;
private WeakReference<LifecycleOwner> bindOwner;
private WeakReference<View> bindView;

public HxHttp(WeakHashMap<String, Object> PARAMS, WeakHashMap<String, Object> HEADERS, String URL, RequestBody BODY, File FILE, String FILE_NAME) {
this.PARAMS = PARAMS;
this.HEADERS = HEADERS;
this.URL = URL;
this.BODY = BODY;
this.FILE = FILE;
this.FILE_NAME = FILE_NAME;
}

public static HxHttpBuilder builder() {
return new HxHttpBuilder();
}

public final Flowable<String> post() {
// RetrofitCreator类在下面
return wrapBind(RetrofitCreator.getRequest().postRaw(this.HEADERS, this.URL, this.BODY).onBackpressureLatest()).compose(io());
}

protected <T> Flowable<T> wrapBind(Flowable<T> flowable) {
WeakReference<LifecycleOwner> weakReference = this.bindOwner;
if (weakReference != null && weakReference.get() != null) {
return (Flowable<T>) flowable.compose(AndroidLifecycle.createLifecycleProvider(this.bindOwner.get()).bindToLifecycle());
}
WeakReference<View> weakReference2 = this.bindView;
return (weakReference2 == null || weakReference2.get() == null) ? flowable : (Flowable<T>) flowable.compose(RxLifecycleAndroid.bindView(this.bindView.get()));
}

public static <T> FlowableTransformer<T, T> io() {
return new FlowableTransformer<T, T>() {
@Override // io.reactivex.rxjava3.core.FlowableTransformer
public Publisher<T> apply(Flowable<T> upstream) {
return upstream.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
}
};
}
}

上面是HxHttp类,下面是HxHttpBuilder类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.hdl.hdlhttp;

import com.alibaba.fastjson.support.spring.FastJsonJsonView;
import okhttp3.MediaType;
import okhttp3.RequestBody;

public final class HxHttpBuilder {
private final WeakHashMap<String, Object> PARAMS = new WeakHashMap<>();
private final WeakHashMap<String, Object> HEADERS = new WeakHashMap<>();
private String mUrl = null;
private RequestBody mBody = null;
private File mFile = null;
private String mFileName = null;

public final HxHttpBuilder url(String url) {
this.mUrl = url;
return this;
}

public final HxHttpBuilder raw(String raw) {
this.mBody = RequestBody.create(raw, MediaType.parse(FastJsonJsonView.DEFAULT_CONTENT_TYPE));
return this;
}

public HxHttp build() {
return new HxHttp(this.PARAMS, this.HEADERS, this.mUrl, this.mBody, this.mFile, this.mFileName);
}
}

由以上可以判断出这个sign过程应该发生于RetrofitCreator.getRequest().postRaw(this.HEADERS, this.URL, this.BODY)这段代码之中。接下来分析RetrofitCreator类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.hdl.hdlhttp.client;

import com.hdl.hdlhttp.HxHttpConfig;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory;
import retrofit2.converter.scalars.ScalarsConverterFactory;

public final class RetrofitCreator {
// 这个类在下面
private static RequestService service;

public static RequestService getRequest() {
if (service == null) {
synchronized (RequestService.class) {
if (service == null) {

// HxHttpConfig类在下面
service = (RequestService) new Retrofit.Builder()
.baseUrl(HxHttpConfig.getInstance().getBaseUrl())
.client(HxHttpConfig.getInstance().getClient())
.addConverterFactory(ScalarsConverterFactory.create())
.addCallAdapterFactory(RxJava3CallAdapterFactory.createSynchronous())
.build().create(RequestService.class);
}
}
}
return service;
}
}

RequestService类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.hdl.hdlhttp.client;

import io.reactivex.rxjava3.core.Flowable;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import retrofit2.Response;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.FieldMap;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.HEAD;
import retrofit2.http.HeaderMap;
import retrofit2.http.Multipart;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Part;
import retrofit2.http.PartMap;
import retrofit2.http.QueryMap;
import retrofit2.http.Streaming;
import retrofit2.http.Url;

public interface RequestService {
@DELETE
Flowable<String> delete(@HeaderMap WeakHashMap<String, Object> headers, @Url String url, @QueryMap WeakHashMap<String, Object> params);

@Streaming
@GET
Flowable<ResponseBody> download(@HeaderMap WeakHashMap<String, Object> headers, @Url String url, @QueryMap WeakHashMap<String, Object> params);

@GET
Flowable<String> get(@HeaderMap WeakHashMap<String, Object> headers, @Url String url, @QueryMap WeakHashMap<String, Object> params);

@HEAD
Flowable<Response<Void>> head(@HeaderMap WeakHashMap<String, Object> headers, @Url String url, @QueryMap WeakHashMap<String, Object> params);

@FormUrlEncoded
@POST
Flowable<String> post(@HeaderMap WeakHashMap<String, Object> headers, @Url String url, @FieldMap WeakHashMap<String, Object> params);

@POST
Flowable<String> postRaw(@HeaderMap WeakHashMap<String, Object> headers, @Url String url, @Body RequestBody body);

@FormUrlEncoded
@PUT
Flowable<String> put(@HeaderMap WeakHashMap<String, Object> headers, @Url String url, @FieldMap WeakHashMap<String, Object> params);

@PUT
Flowable<String> putRaw(@HeaderMap WeakHashMap<String, Object> headers, @Url String url, @Body RequestBody body);

@POST
@Multipart
Flowable<String> upload(@HeaderMap WeakHashMap<String, Object> headers, @Url String url, @PartMap WeakHashMap<String, String> params, @Part MultipartBody.Part file);
}

RequestService只描述了关于如何处理http数据的接口。那么,问题可能就在new Retrofit.Builder().baseUrl(HxHttpConfig.getInstance().getBaseUrl()).client(HxHttpConfig.getInstance().getClient())之中了。其中有一个类HxHttpConfig:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package com.hdl.hdlhttp;

import android.content.Context;
import com.hdl.hdlhttp.ssl.HxSSLSocketFactory;
import com.hdl.hdlhttp.ssl.HxTrustManager;
import okhttp3.OkHttpClient;


public class HxHttpConfig {
private OkHttpClient.Builder BUILDER;
private OkHttpClient client;
private String mBaseUrl;

private HxHttpConfig() {}

private static final class SingletonInstance {
private static final HxHttpConfig INSTANCE = new HxHttpConfig();

private SingletonInstance() {}
}

public static HxHttpConfig getInstance() {
return SingletonInstance.INSTANCE;
}

public final HxHttpConfig init(Context context, String url) {
this.mBaseUrl = url;
this.context = context.getApplicationContext();
return this;
}

public final String getBaseUrl() {
return this.mBaseUrl;
}

public final HxHttpConfig ignoreSSL() {
OkHttpClient.Builder builderHostnameVerifier = getBuilder().hostnameVerifier(new HostnameVerifier() {
@Override // javax.net.ssl.HostnameVerifier
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
SSLSocketFactory ignoreSocketFactory = HxSSLSocketFactory.getIgnoreSocketFactory();
Objects.requireNonNull(ignoreSocketFactory);
builderHostnameVerifier.sslSocketFactory(ignoreSocketFactory, new HxTrustManager());
return this;
}

public final HxHttpConfig addInterceptor(Interceptor... interceptors) {
for (Interceptor interceptor : interceptors) {
getBuilder().addInterceptor(interceptor);
}
return this;
}

public final OkHttpClient.Builder getBuilder() {
if (this.BUILDER == null) {
synchronized (HxHttpConfig.class) {
if (this.BUILDER == null) {
this.BUILDER = new OkHttpClient.Builder();
}
}
}
return this.BUILDER;
}

public final OkHttpClient getClient() {
if (this.client == null) {
this.client = getBuilder().build();
}
return this.client;
}
}

这个类里面有个关键函数addInterceptor,说明这是一个AOP切面编程的逻辑,那就解释得通了。毕竟发送的json里面基本都有appKey、timestamp和sign字段,像这样通过拦截器统一处理确实更加方便。既然如此,我们只需要找到这个函数的调用者和调用方式就能明白这个sign的逻辑了。根据这样的思路,很快就能定位到整个过程。首先是Android的启动类com.hdl.onproandroid.App

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.hdl.onproandroid;

import android.app.Application;
import android.content.Context;
import android.text.TextUtils;
import com.hdl.cloud.HdlCloudLink;
import com.hdl.cloud.interceptor.LanguageInterceptor;
import com.hdl.hdlhttp.HxHttpConfig;
import com.hdl.onproandroid.config.SPKey;
import com.hdl.onproandroid.utils.SPUtils;
import okhttp3.logging.HttpLoggingInterceptor;

public class App extends Application {
public static boolean isOnline = true;
public static boolean isRelease = true;

@Override // android.app.Application
public void onCreate() throws IllegalAccessException, ClassNotFoundException, IllegalArgumentException, InvocationTargetException {
super.onCreate();

//...

// 其他的代码都不重要,只需要知道这里调用了initCloud方法
initCloud(this);

//...
}

private void initCloud(Context context) {
String str = Locale.ENGLISH.getLanguage().equals(context.getResources().getConfiguration().locale.getLanguage()) ? "en" : "cn";
String string = SPUtils.getString(SPKey.CHANGE_ENV);
if (TextUtils.isEmpty(string)) {
string = isOnline ? "2" : "1";
SPUtils.put(SPKey.CHANGE_ENV, string);
}

//以下的代码无论如何都调用了HdlCloudLink.getInstance().init()方法,这里具体写明了到底传了什么参数进去。BuildConfig类中全是写死的配置String等信息,下面会写。
if (!isRelease) {
if ("1".equals(string)) {
HdlCloudLink.getInstance().init(context, "CXTORGSN", "CXTORGTDCXTORGTT", "https://test-gz.hdlcontrol.com", "1706179306583613442", str);
} else if ("2".equals(string) || isOnline) {
HdlCloudLink.getInstance().init(context, BuildConfig.APP_KEY, BuildConfig.APP_SECRET, "https://nearest.hdlcontrol.com", BuildConfig.CHECK_UPDATE_APP_CODE, str);
} else {
HdlCloudLink.getInstance().init(context, "CXTORGSN", "CXTORGTDCXTORGTT", "https://test-gz.hdlcontrol.com", "1706179306583613442", str);
}
} else if (isOnline) {
HdlCloudLink.getInstance().init(context, BuildConfig.APP_KEY, BuildConfig.APP_SECRET, "https://nearest.hdlcontrol.com", BuildConfig.CHECK_UPDATE_APP_CODE, str);
} else {
HdlCloudLink.getInstance().init(context, "CXTORGSN", "CXTORGTDCXTORGTT", "https://test-gz.hdlcontrol.com", "1706179306583613442", str);
}

HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor();
httpLoggingInterceptor.level(HttpLoggingInterceptor.Level.BODY);

//这里使用了addIntercepter方法
HxHttpConfig.getInstance().addInterceptor(new LanguageInterceptor()).addInterceptor(httpLoggingInterceptor).ignoreSSL();
}
}

BuildConfig类定义了一些配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.hdl.onproandroid;

public final class BuildConfig {
public static final String APPLICATION_ID = "com.hdl.homepro";
public static final String APP_KEY = "CXZMMOCF";
public static final String APP_SECRET = "CXZMMOCVCXZMMODL";
public static final String BASE_URL = "https://nearest.hdlcontrol.com";
public static final String BUILD_TYPE = "release";
public static final String CHECK_UPDATE_APP_CODE = "1706178588764487682";
public static final boolean DEBUG = false;
public static final int VERSION_CODE = 31;
public static final String VERSION_NAME = "1.9.0";
}

App启动后会调用initCloud函数,而initCloud函数使用了HdlCloudLink.getInstance().init()方法进行初始化,然后调用了HxHttpConfig的addInterceptor方法添加一个关于语言的header。与sign有关的Interceptor就在HdlCloudLink.getInstance().init()方法内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.hdl.cloud;

import android.content.Context;
import com.hdl.hdlhttp.HxHttpConfig;

public class HdlCloudLink {
private static final String DEF_TOKEN_HEADER_PREFIX = "Bearer ";
private String appCode;
private String appKey;
private String appSecret;
private String cloudUrl;
private Context context;
private String tokenHeaderPrefix;

private HdlCloudLink() {
this.cloudUrl = "";
this.tokenHeaderPrefix = DEF_TOKEN_HEADER_PREFIX;
}

private static class SingletonInstance {
private static HdlCloudLink INSTANCE = new HdlCloudLink();

private SingletonInstance() {
}
}

public static HdlCloudLink getInstance() {
return SingletonInstance.INSTANCE;
}

public void init(Context context, String appKey, String appSecret, String cloudUrl, String appCode, String language) {
this.context = context;
HxHttpConfig.getInstance().init(context, cloudUrl).addInterceptor(new HdlLoginInterceptor(), new EncryptInterceptor(), new SmartHeaderInterceptor());
this.cloudUrl = cloudUrl;
this.appKey = appKey;
this.appSecret = appSecret;
this.appCode = appCode;
setLanguage(language);
}
}

我们发现在这个init方法内就添加了3个拦截器。其中EncryptInterceptor向我们揭示了sign是怎么被计算出来的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package com.hdl.cloud.interceptor;

import android.net.Uri;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.support.spring.FastJsonJsonView;
import com.google.gson.JsonObject;
import com.hdl.cloud.HdlCloudLink;
import com.hdl.cloud.utils.MD5Utils;
import com.umeng.umcrash.UMCrash;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

public class EncryptInterceptor implements Interceptor {
@Override // okhttp3.Interceptor
public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
if (TextUtils.isEmpty(request.headers().get("IgnoreSignHeader"))) {
return chain.proceed(encrypt(request));
}
return chain.proceed(request);
}

private Request encrypt(Request request) {
String strValueOf = String.valueOf(System.currentTimeMillis());

// HdlCloudLink类在上面
String appKey = HdlCloudLink.getInstance().getAppKey();
String appSecret = HdlCloudLink.getInstance().getAppSecret();

JsonObject bodyJson = getBodyJson(request);
if (bodyJson == null) {
return request;
}

// 在这里添加了appKey、timestamp、sign字段进最终的json。其中UMCrash.SP_KEY_TIMESTAMP是个public static final String,值为"timestamp"
bodyJson.addProperty("appKey", appKey);
bodyJson.addProperty(UMCrash.SP_KEY_TIMESTAMP, strValueOf);
bodyJson.addProperty("sign", getSign(bodyJson, appSecret));

return request.newBuilder().post(RequestBody.create(bodyJson.toString(), MediaType.parse(FastJsonJsonView.DEFAULT_CONTENT_TYPE))).build();
}

private JsonObject getBodyJson(Request request) {
RequestBody requestBodyBody = request.body();

// 将获取到的body(一般都是json)转换为JsonObject对象

//...
}

// 计算sign的具体函数
private String getSign(JsonObject jsonObject, String appSecret) {
return MD5Utils.encodeMD5(jsonToUrlParameter(jsonObject) + appSecret);
}

private String jsonToUrlParameter(JsonObject jsonObject) {
Uri.Builder builder = new Uri.Builder();
try {

// 相当于把Gson库中的json转移到fastjson库来处理
JSONObject object = JSON.parseObject(jsonObject.toString());

ArrayList<String> arrayList = new ArrayList(object.keySet());

// 把json中的字段按字母升序(A-Z)排列
Collections.sort(arrayList);

for (String str : arrayList) {
Object obj = object.get(str);

// 使用isSignValue函数判断这个参数是否需要参与sign的计算
if (isSignValue(obj)) {

// 会把所有需要参与sign计算的字段做成url参数字符串。如{"a"=1,"b"=2,"c"=3}会被转换为a=1&b=2&c=3。特殊字符和中文会被URL编码!
builder.appendQueryParameter(str, obj.toString());

}
}
} catch (Exception e) {
e.printStackTrace();
}
return builder.build().getQuery();
}

private static boolean isSignValue(Object obj) {

/*
当obj属于以下任何之一且不为null时返回true
1. byte、short、int、long、float、double、boolean、char的包装类
2. 非空字符串
*/

//...
}
}

MD5Utils.encodeMD5()的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.hdl.cloud.utils;

public final class MD5Utils {
public static String encodeMD5(String str) throws NoSuchAlgorithmException {
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
messageDigest.update(str.getBytes("UTF-8"));
return toHexString(messageDigest.digest());
} catch (UnsupportedEncodingException | NoSuchAlgorithmException e) {
e.printStackTrace();
return "";
}
}

private static String toHexString(byte[] bytes) {
if (bytes == null) {
return null;
}
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
String string = Integer.toString(b & 255, 16);
if (string.length() == 1) {
string = "0" + string;
}
sb.append(string);
}
return sb.toString();
}
}

也就是说,它会把准备发送的json中的所有数字类(比如int、double)、布尔类、char类、字符串类的字段收集起来,利用字段名进行字母升序(A-Z)排序,然后把这些字段转换成用URL参数表达的形式,再在得到的字符串后面跟一串appSecret(在App类或BuildConfig类中保存),计算得到的字符串的md5并以16进制表示。sign即为保存其16进制表示的字符串(长度为32)。

以UDP二进制流的方式控制

HDL的设备同时支持一个叫Buspro的协议,这个协议采用UDP方式(端口默认6000),一般会采用广播的方式来发送和接收消息。home_assistant_buspro是一个为HDL添加Home Assistant支持的插件,这个仓库中描述了UDP中传递的消息格式。由于Buspro一般只在局域网内运行,消息格式中不存在特殊的验证(CRC校验位除外),故我们可以通过直接构造特定的消息格式并转发到HDL网关的6000端口来控制设备。

为了更加详细的确定消息格式,可以通过Wireshark过滤udp.port == 6000的包,逐个测试具体的消息格式。

Wireshark需要Npcap这个软件。安装Wireshark时会提示安装,但如果使用便携版Wireshark时,则需要自己手动安装Npcap。

举出如下一个例子:C0 A8 01 03 48 44 4C 4D 49 52 41 43 4C 45 AA AA 16 09 00 11 F7 E4 5C 09 04 1E 64 FE 00 00 02 0D AC 00 00 00 57 30

分段:

  1. C0 A8 01 03 Buspro网关的地址。这里是192.168.1.3192对应16进制C0168对应16进制A81对应16进制013对应16进制03
  2. 48 44 4C 4D 49 52 41 43 4C 45 固定标识符HDLMIRACLE的16进制表示形式
  3. AA AA 同步字节,也是固定的
  4. 16 数据包长度,在这里是指从16(含)到末尾(含)一共有多少个字节(16进制表示)
  5. 09 00 发送数据的设备的子网和设备号,这里的发送数据的设备的子网是9,设备号是0。仓库中发送数据的设备的子网和设备号的默认缺省值均为200
  6. 11 F7 发送数据的设备的类型
  7. E4 5C 操作码
  8. 09 04 目标设备的子网和设备号,这里的目标设备的子网是9,设备号是4
  9. 1E 64 FE 00 00 02 0D AC 00 00 00 负载数据
  10. 57 30 CRC校验位,使用CRC-16/XMODEM算法(多项式:$x^{16} + x^{12} + x^5 + 1$,初始值:0,输入/输出反转:false,结果异或:0,多项式Poly:1021,宽度位数:16)计算,只计算从表示数据包长度的字节(包含)(这里是16)到倒数第三个字节(包含)(这里是00)这一段的数据。

其中,发送数据的设备的类型以及操作码可以参阅仓库中的枚举值,负载数据可以参阅仓库中的控制代码,而解析收到的负载数据可以参阅仓库中这个包中各类设备的代码。

甚至,可以根据仓库中的枚举值获取局域网中可用的Buspro设备:C0 A8 01 03 48 44 4C 4D 49 52 41 43 4C 45 AA AA 0B C8 C8 FF FC 00 33 01 4A 6F 77

后记

只是简单研究了以下整个系统的运作过程,且写的很粗糙,对于具体精细控制某一设备所需的信息并未写出,真的肝不动了qwq。仅供学习交流使用。

  • 标题: HDL-Buspro智能设备控制概要分析
  • 作者: Denvo
  • 创建于 : 2026-02-16 22:50:29
  • 更新于 : 2026-02-16 23:04:50
  • 链接: https://www.denvoshome.xyz/posts/HDL-Buspro-Control-Expandation/
  • 版权声明: 本文章采用 CC BY-SA 4.0 进行许可。
评论