博客搬遷至https://blog.wangjiegulu.com
RSS訂閱:https://blog.wangjiegulu.com/feed.xml
原文鏈接:https://blog.wangjiegulu.com/2018/09/26/private-smart-life-cloud-a--hack-tuya-smart-plug/
構建自己的 Smart Life 私有雲(一)-> 破解塗鴉智能插座
本系列文章的目標是通過自己搭建的私有雲、IFTTT、Slack、Google Assistant、塗鴉智能、圖靈機器人等等第三方服務,構建自己的“智能生活”基本的框架。
前段時間我在京東上購買了一個塗鴉智能的插座。塗鴉智能是一個把產品智能化的一個平台,從軟件和硬件和雲端上面提供給廠商一個一站式人工智能物聯網的解決方案(確實不是塗鴉智能的廣告- -.)。所以嚴格來說應該是“鵲起”基於塗鴉解決方案所開發出來的一個產品,基於塗鴉提供的一切規范,所有軟硬雲端的開發規范都可以在官網無權限限制地查看到(https://docs.tuya.com/cn/)
初試插座
我首先下載了塗鴉的官方app:Smart Life,注冊登錄、智能配對插座,通過手機控制插座開關,一切順利,還支持設置定時開關和 Schedule,而且手機控制到插座的反應十分靈敏(看樣子應該使用了長鏈接)。
破解插座
當然使用官方 app 顯然是無法滿足我們需求,所以查看官方文檔,發現文檔中的 tuya.m.device.dp.publish 接口正好可以滿足我們的需求。
通過接入指南,我們可以知道接入方式提供了兩種:
- MQTT,果然有使用長鏈接(app端默認用的應該就是長鏈接了)
- https,這個目前比較適合我們,暫時不管延遲怎么樣,先選擇用 https 來驗證控制的可行性
下面有列出通用的參數:
| 參數名稱 | 參數類型 | 是否必須 | 是否簽名 | 參數描述 |
|---|---|---|---|---|
| a | String | 是 | 是 | API名稱 |
| v | String | 是 | 是 | API接口版本 |
| sid | String | 否 | 是 | 用戶登錄授權成功后,ATOP頒發給應用的用戶session |
| time | String | 是 | 是 | 時間戳,格式為數字,大小到秒非毫秒,時區為標准時區,例如:1458010495。API服務端允許客戶端請求最大時間誤差為540分鍾。 |
| sign | String | 是 | 否 | API輸入參數簽名結果,簽名算法參照下面的介紹 |
| clientId | String | 是 | 是 | 用戶的APPID(注各平台不一樣如:ios、android、雲雲對接的id都不一樣) |
| lang | String | 否 | 是 | APP的語言,如"en",“zh_cn”,錯誤信息根據語言自動翻譯 |
| ttid | String | 否 | 是 | APP渠道或雲端渠道,如公司名,用於數據分析跟蹤 |
| os | String | 是 | 是 | 手機操作系統,如"Android",“ios”,雲端可以寫linux或寫公司名 |
上述我們無法拿到的是哪些呢?
- sid:登錄后自然就能拿到了
- sign:所有參數都有了,那sign自然能算出來(文檔下面有sign算法)
- clientId:這玩意就比較麻煩了,相當於一個apiKey
- ttid:這個非必須,暫時可以不管
所以只要拿到 clientId,我們就能
- 通過 tuya.m.user.email.token.get 接口拿到登錄用的 token
- 通過 tuya.m.user.mobile.passwd.login 接口登錄
- 然后通過 tuya.m.device.dp.publish 接口下發指令
那怎么去拿到 clientId 呢?反編譯試試(大家應該都知道怎么做),class.dex 有點多,最后拿到了
- client_id:
8qp5cfk*******3mpmc3(這個打碼了) - app_secret:
g75ktcvsae8**********e95j738tawg(同樣打碼)
拿到登錄 token
打開 Postman,按照文檔 POST countryCode 和 mobile 可以得到 token:
{
"api":"tuya.m.user.mobile.token.get",
"result":{
"exponent":"3",
"pExponent":"...",
"publicKey":"...",
"token":"..."
},
"status":"ok",
"success":true
}
通過 token 進行登錄
{
"countryCode":"86",
"mobile":"11745678923",
"passwd":"根據獲取token接口返回的公鑰對 md5(明文密碼) 進行rsa加密",
"token":"126bb7570dcae343980b0607e6b35084",
"ifencrypt":1
}
根據登錄接口的文檔,密碼需要使用 gettoken 接口返回的公鑰對 md5之后的密碼進行 rsa 加密,apk 反編譯之后代碼可以完全看到,直接拷貝即可
登錄完之后,就可以拿到具體的 sid (sessionId)了,有了 sessionId,就可以去對插座進行下發指令。
插座下發指令
同樣根據下發指令接口文檔:
{
"devId": "002yt001sf000000sfV3",
"dps": {
"1":1,
"2":5
}
}
可以看到,POST 的數據除了 sid,還需要 devId 和 dps
dps 可以參考這里的文檔:https://fchelp.cloud.alipay.com/queryArticleContent.htm?tntInstId=WRNQWLCN&articleId=89429815&helpCode=SCE_00000019
我們要控制插座開關的話,那就使用 {"1": true} / {"1": false} 即可
那 devId 呢?它代表我添加設備 id,那我怎么知道我剛給在 Smart Life 上添加的插座 id 呢?抓包試試(大家應該都知道),通過抓包,我們可以很容易拿到所有你綁定的 devId。
至此,我們可以通過 http 來控制插座的開關了。
搭建自己的私有雲項目
我為自己的項目取名為
Angelia,安革利亞,古希臘神話人物之一,為“消息女神”。
搭建 web 項目,新建 AngeliaController:
@RestController
@RequestMapping("/angelia")
@Configuration
class AngeliaController {
// ...
}
增加 Tuya 的配置文件:tuyaclient.properties
tuya.client.client_id=xxxxxx
tuya.client.app_secret=xxxxxx
tuya.client.ttid=xxxxxx
tuya.client.v=1.0
tuya.client.base_url=https://a1.tuyacn.com/api.json
# smart life app 登錄手機號
tuya.client.user_mobile=18511111111
tuya.client.user_country_code=86
tuya.client.user_rsa_encrypted_passwd=xxx
# 設備信息
tuya.client.lang=en
tuya.client.os=Android
tuya.client.os_system=9
tuya.client.time_zone_id=Asia/Shanghai
tuya.client.platform=Pixel
tuya.client.sdk_version=2.6.4
tuya.client.app_version=3.4.3
tuya.client.app_rn_version=5.6
# 設備 id,模擬手機的 deviceId
tuya.client.device_id=e507510a0288accd7******
tuya.client.imei=xxxxxx
tuya.client.imsi=xxxxxx
# 智能設備,可以多個:別名|id|dpid,別名|id|dpid,別名|id|dpid
# dpid: 1. 開關;4.rgb;5. 檔位;6. 溫度;15. 紅外數據
tuya.client.dev_ids=[\
{\
"aliasList": ["plug a", "plug 1", "插座a", "洗手間總電源"],\
"devId": "111222333",\
"dpId": "1"\
},\
{\
"aliasList": ["plug b", "plug 2", "插座b", "卧室總電源"],\
"devId": "12341234",\
"dpId": "1"\
},\
// ...
]
以上,除了剛剛的那些必要的參數之類,在這個配置文件里面還增加了對所有設備的配置,每個設備對應它的 dpId,devId,還有別名(控制的時候不可能直接說 “devId 為 a1d2f32a1d2f32 的插座關掉”,而是說 “關掉插座1” / “關掉洗手間總電源”等等),每個設備別名可以有多個。
好了,回到 AngeliaController,新增一個接口:
/**
* 插座控制接口
*/
@Autowired
lateinit var tuyaClientService: TuyaClientService
@Autowired
lateinit var tuyaClientProperties: TuyaClientProperties
@PostMapping("/control/plug")
fun controlPlug(@RequestBody request: PlugRequestVo): JSONObject {
val dev = tuyaClientProperties.findDev(request.alias)
return try {
if (null == dev) {
JsonResult.error("Device named ${request.alias} is not found")
} else {
tuyaClientService.controlPlug(dev.devId, request.turnOn)
JsonResult.success()
}
} catch (e: Exception) {
JsonResult.error(e.message)
}
}
data class PlugRequestVo(
val alias: String,
val turnOn: Boolean
)
以上,該接口接收別名(alias)和開閉狀態(turnOn)兩個入參。首先,通過請求的別名去 properties 查找設備,如果找到,則通過 tuyaClientService 的 controlPlug 方法來下發指令,controlPlug 的實現就是剛剛上面說的 獲取登錄接口-登錄-下發指令幾步。
構建、部署,通過 Postman 訪問 http://localhost:xxxxx/angelia/control/plug, body 設置為 {"alias": "卧室總電源", "turnOn": true},插座即可打開。
