前言
最近对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/ssl和http://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的最后三个字段一定是appKey、timestamp、sign。返回的JSON的格式也比较固定,除了data字段外的都是一样的。所以在此先描述一下json的共同部分,下面API描述时就会省略这些共同的字段。
要获取这些特殊字段的具体内容,建议自己亲自POST查看结果。
发送的json:
1 2 3 4 5
| { "appKey": "CXZMMOCF", "timestamp": "string字符串", "sign": "string字符串" }
|
接受的json:
1 2 3 4 5 6 7 8 9 10 11
| { "code": 0, "data": "这个字段会不同,可能是一个数组,也可能是一个json或其他东西", "requestId": "string字符串", "timestamp": "string字符串", "isSuccess": true,
"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字符串", "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": "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字符串",
"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字符串",
"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如下:
通过逆向进一步获取信息
使用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 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;
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() {
@Override public void messageArrived(String str, MqttMessage mqttMessage) throws Exception { Log.i("MQTTService", "------------mqttMessage topic=" + str); if (str.endsWith("/app/third/bind/send")) {
if (mqttMessage.toString().contains("true")) { EventBus.getDefault().post(new EventNotifyThirdAuthInfo(true)); return; } else { EventBus.getDefault().post(new EventNotifyThirdAuthInfo(false)); return; }
} try {
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")) {
} 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")) {
} else if (str.endsWith("/app/thing/event/posture_calibration_event/up")) { } else if (str.endsWith("/app/ota/device/progress/up")) { } else if (str.endsWith("/app/son/session/online")) { } else {
} } 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() { }); 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;
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) {
} } }
|
已经确定解密使用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消息时,大部分都会在最后附带appKey、timestamp、sign三个字段。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();
arrayList.add(controlDeviceDb);
jsonObject.add("actions", GsonUtils.getGson().toJsonTree(arrayList));
return (Disposable) HxHttp.builder() .url(HdlCloudApi.getRequestUrl(HdlCloudApi.CONTROL_DEVICE)) .raw(jsonObject.toString()) .build().post().subscribeWith(new HDLResponse<String>() {
@Override public void onResponse(String str3) { CallBackListener callBackListener2 = callBackListener; if (callBackListener2 != null) { callBackListener2.onSuccess(str3); } }
@Override 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() { 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 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) {
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 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 public void onCreate() throws IllegalAccessException, ClassNotFoundException, IllegalArgumentException, InvocationTargetException { super.onCreate();
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); }
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);
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 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());
String appKey = HdlCloudLink.getInstance().getAppKey(); String appSecret = HdlCloudLink.getInstance().getAppSecret();
JsonObject bodyJson = getBodyJson(request); if (bodyJson == null) { return request; }
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();
}
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 {
JSONObject object = JSON.parseObject(jsonObject.toString());
ArrayList<String> arrayList = new ArrayList(object.keySet());
Collections.sort(arrayList);
for (String str : arrayList) { Object obj = object.get(str);
if (isSignValue(obj)) {
builder.appendQueryParameter(str, obj.toString());
} } } catch (Exception e) { e.printStackTrace(); } return builder.build().getQuery(); }
private static boolean isSignValue(Object obj) {
} }
|
而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
分段:
C0 A8 01 03 Buspro网关的地址。这里是192.168.1.3(192对应16进制C0,168对应16进制A8,1对应16进制01,3对应16进制03)48 44 4C 4D 49 52 41 43 4C 45 固定标识符HDLMIRACLE的16进制表示形式AA AA 同步字节,也是固定的16 数据包长度,在这里是指从16(含)到末尾(含)一共有多少个字节(16进制表示)09 00 发送数据的设备的子网和设备号,这里的发送数据的设备的子网是9,设备号是0。仓库中发送数据的设备的子网和设备号的默认缺省值均为20011 F7 发送数据的设备的类型E4 5C 操作码09 04 目标设备的子网和设备号,这里的目标设备的子网是9,设备号是41E 64 FE 00 00 02 0D AC 00 00 00 负载数据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。仅供学习交流使用。