Open majkrzak opened 2 years ago
Correct call:
return self.performRequest(
{"method": "get", "device_info": {"name": ["basic_info"]}}
)
-40106 returned for both:
return self.performRequest(
{"method": "get", "device_infoBAD": {"name": ["basic_info"]}}
)
return self.performRequest(
{"method": "get", "device_info": {"name": ["basic_infoBAD"]}}
)
Following https://md.depau.eu/s/r1Ys_oWoP#All-command-types and above structure:
self.performRequest(
{"method": "get", "on_boarding": {"name": ["get_connect_status"]}}
)
should work but it still returns -40106. I did test this on already provisioned camera though. Same happens with
self.performRequest(
{"method": "get", "hard_disk_manage": {"name": ["hard_disk_info"]}}
)
But for example below works:
print(self.performRequest({"method": "get", "system": {"name": ["sys"]}}))
print(self.performRequest({"method": "get", "system": {"name": ["basic"]}}))
BUT, user_id no longer works and returns 40106.
print(self.performRequest({"method": "get", "system": {"name": ["user_id"]}}))
@majkrzak Here's a bit of documentation that may help, it required quite a bit of reverse engineering of the Android App to fully understand that authentication is not required, but it's now possible (with a few unauthenticated API requests) to provision a camera without going through the Tapo app !
In this mode, the camera's LED will alternate between green and red.
The camera creates an unsecured WiFi network such as Tapo_Cam_87D1
(87D1
being the last 4 characters of the camera's MAC address)
Once connected to this WiFi network, the camera provides a dynamic IP in the 192.168.191.0/24
subnet and the camera is accessible via 192.168.191.1
Note: It is possible to log in without any parameters or using admin:admin
, but it doesn't really matter since authentication is not required for the onboarding requests.
POST https://192.168.191.1/
{
"method": "scanApList",
"params": {
"onboarding": {
"scan": "null"
}
}
}
{
"error_code": 0,
"result": {
"onboarding": {
"scan": {
"wpa3_supported": "false",
"ap_list": [
{
"ssid": "IOT-ROUTEUR-5246",
"bssid": "A5-6E-51-61-54-53",
"auth": 3,
"encryption": 2,
"rssi": 2
},
{
"ssid": "SFR WiFi Mobile",
"bssid": "72-CE-7D-D9-D8-8D",
"auth": 5,
"encryption": 2,
"rssi": 0
},
{
"ssid": "SFR WiFi FON",
"bssid": "72-CE-7D-D9-D8-8F",
"auth": 0,
"encryption": 0,
"rssi": 0
},
{
"ssid": "FreeWifi_secure",
"bssid": "00-24-D4-58-CB-45",
"auth": 5,
"encryption": 3,
"rssi": 0
}
]
}
}
}
}
auth
:
0
: None1
: WEP2
: WPA23
: ?4
: ?5
: WPA3POST https://192.168.191.1/
{
"method": "connectAp",
"params": {
"onboarding": {
"connect": {
"ssid": "IOT-ROUTEUR-5246",
"bssid": "A5-6E-51-61-54-53",
"auth": 3,
"encryption": 2,
"rssi": 2,
"password": "JfHLxfTdB+evKq5Pvlcuth2MgVlpw2Z++yEG/O0mk/Otcydno5ujFump0NNa+/dxOiN6D9m6JvpkRhqVY9VafrTkTIOFLesVwRbnzgczl90yS9gjjlyevkCrhIdM+x8OHERMB/NSeZUB6hLLY3IGwcAjVi3Ort13fxV/LovZ1qs="
}
}
}
}
You may be wondering where that password comes from, it's encrypted with a 1024 bit RSA public key found in the decompiled Tapo APK (represented as Java RSAPublicKeySpec):
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4D6i0oD/Ga5qb//RfSe8MrPVI
rMIGecCxkcGWGj9kxxk74qQNq8XUuXoy2PczQ30BpiRHrlkbtBEPeWLpq85tfubT
UjhBz1NPNvWrC88uaYVGvzNpgzZOqDC35961uPTuvdUa8vztcUQjEZy16WbmetRj
URFIiWJgFCmemyYVbQIDAQAB
So using a free online tool (such as https://www.devglan.com/online-tools/rsa-encryption-decryption) you should be able to encrypt for example MyPassword
and obtain JcRz1lFfpeaa2FPo+tcCVZ6jtpEiLiXCdrb6vk85UXUEfUuVS7Zbh6PzDiWUtvCEMGZm7UCfIpYptnPT5y5jRsKr+C3v8t+jDfGwzW3+1DcnP+Jd/eo1Omfk+tkV0MlLKV+ur16/rzbWfAwDqGnEpwWQJ/u4T8VSJjyZGM+xt3o=
{
"error_code": 0,
"result": {
"onboarding": {
"connect": {
"connect_time": 60000,
"mac": "1c-61-b4-d5-87-d1",
"support_ap": "true"
}
}
}
}
POST https://192.168.191.1/
{
"method": "getConnectStatus",
"params": {
"onboarding": {
"get_connect_status": "null"
}
}
}
{
"error_code": 0,
"result": {
"onboarding": {
"get_connect_status": {
"status": 0
}
}
}
}
Once onboarding is performed and the camera is connected to the desired WiFi network, the onboarding methods will be unavailable and return the error code -40101
.
Once the camera is configured, the only way to change WiFi network appears to require a hard reset, returning the camera to factory settings and back into onboarding mode.
I've been able to get the onboarding functions to work, and the cam connects to wifi. In this mode, you can also set the "third user" and admin/cloud passwords. There seems to be another message that gets sent to the camera that gets it out of "onboarding" mode, but I haven't been able to figure it out. Knowing this, we should be able to get these cameras provisioned without the app.
Hi @ahoy
Sorry, what do you mean by getting it out of onboarding mode?
Does the device not exit onboarding once connectAP
is successful?
The following are the steps I have used to set up the device for RTSP streaming without requiring the app or internet, listing the method
names for brevity, but let me know if you need additional detail on the params
and I can provide those:
scanApList
setLanguage
changeAdminPassword
setMediaEncrypt
setTimezone
setRecordPlan
connectAp
Then once connected to the same network again:setDeviceAlias
changeThirdAccount
This is not the minimal set of steps (e.g. setRecordPlan
can be ignored), but at this stage I can then connect via VLC using the third account details I set in the last step.
@jp-0 providing examples will be very useful. Currently, with commands from @liamjack I managed to force camera to connect my wifi, I'm able to connect to it with pytapo
regardless credentials I provide, but then trying to use the Home assistant Tapo integration or the onvif it fails. I guess, I'm missing the changeAdminPassword
or changeThirdAccount
part.
hey @majkrzak, see below some detail on the two requests you mentioned.
changeThirdAccount
can also be used at a later stage to update the password but after it is initially set, it will require an additional authentication at which point the steps then should be verifyThirdAccount
before changeThirdAccount
. The first time only changeThirdAccount
is required.
The below are all POST to /{stok}/ds
(e.g. /829dd315d803fee540f127ad0e86f54c/ds
)
"changeAdminPassword"
"user_management": {
"change_admin_password": {
"secname": "root",
"passwd": password_md5, # uppercase of the md5 hash of your password
"old_passwd": password,
"ciphertext": password_rsa, # using the same public key extracted from the app, as mentioned in another comment
"username": "admin"
}
}
"changeThirdAccount"
"user_management": {
"change_third_account": {
"secname": "third_account",
"passwd": new_third_account_password_md5, # uppercase of the md5 hash of your password
"old_passwd": "",
"ciphertext": new_third_account_password_rsa, # using the same public key extracted from the app, as mentioned in another comment
"username": new_username,
}
}
"verifyThirdAccount"
"user_management": {
"verify_third_account": {
"secname": "third_account",
"passwd": current_third_account_password_md5, # uppercase of the md5 hash of your password
"old_passwd": "",
"ciphertext": current_third_account_password_rsa, # using the same public key extracted from the app, as mentioned in another comment
"username": current_username,
}
}
It may be you need to go via the multipleRequest
, I've noticed sometimes a portion of the commands fail otherwise - although not tested that fully.
{"method": "multipleRequest", "params": {"requests": [{"method": method, "params": params}]}}
@jp-0 @ahoy I've already successfully run connectAp
but I am having difficulty running changeAdminPassword
, with this request being sent:
{
"method": "changeAdminPassword",
"params": {
"user_management": {
"change_admin_password": {
"secname": "root",
"passwd": "...",
"old_passwd": "slprealtek",
"ciphertext": "...",
"username": "admin"
}
}
}
}
I am getting these response:
{
"error_code": -40401,
"result": {
"data": {
"code": -40405,
"encrypt_type": [
"1",
"2"
],
"key": "...",
"nonce": "..."
}
}
}
Is there something I am doing wrong? Also, is there also some reference for what the error codes -40401
and -40405
mean?
How to set the IP if my AP doesn't have DHCP? I only see getDeviceIpAddress
in the command list 🤔
Also, where do I find info how to run the commands mentioned above? What is the respective payload? @jp-0
Edit: I'm also not sure how to set a username password. I'm also not sure what the current username/password is
I have received my C220 now, but I'm not sure how to change the password. For now, I've set up a custom DHCP server for the camera. I'd still like to know how to give it a static network config.
Because I don't know the current username/password, I can't use my camera at all, because I'm unable to access any video stream.
(Pan/Tilting the camera works fine, though, and any password is accepted via pytapo as long as username is admin
)
I don't want to use the mobile app / bind my device to the cloud, so I'd prefer it someone can document what those different accounts are used for (how does the app use them / which account should be used for routine tasks).
I also couldn't figure out how to do the steps in https://github.com/JurajNyiri/pytapo/issues/24#issuecomment-1575639582:
scanApList
setLanguage
- Not sure what options or what parameterschangeAdminPassword
- While some fields are documented, the full structure / POST payload hasn't been shown yet and I can't seem to make it work. I also don't know the current password and I'm not sure if each fields contain the old or the new passwordsetMediaEncrypt
setTimezone
- I'm not sure what options or what parameterssetRecordPlan
- Not sure what options or what parametersconnectAp
setDeviceAlias
- Not sure what options or what parameterschangeThirdAccount
- Not sure what options or what parameters (also not sure what this account is used for)Regardless I went ahead with a firmware upgrade as documented in https://github.com/DrmnSamoLiu/Tapo_Camera_Firmware/issues/9
Finally got it working
Also, is there also some reference for what the error codes -40401 and -40405 mean?
I've actually reverse-engineered this now. I believe 40xxx are basically HTTP status codes, so 401 = unauthorized and 405 = not allowed. The 60xxx codes are for each command. I also got some and only through RE I was able to find out some restrictions on certin arguments (example: username must be at least 6 symbols long).
Some observations:
zMiVw8Kw0oxKXL0
(https://nvd.nist.gov/vuln/detail/CVE-2018-11482) but from what I can tell there's also a flag which remembers if this was ever changed; this means once changed you might never be able to get back (!)changeAdminPassword
/ds
instead of the /stok=.../ds
, you can likely use that to recover from a bad password, but I wouldn't bet on itconnectAp
POST it does not work for /ds
; I'm still not sure why (as it fails even with SSL verification disabled), so I had to use pytapo with some custom codeExcuse the poor following code style, I've been trying to figure this out for way too long and need some sleep now. I've used this set of steps after resetting the camera and joining its network:
import base64
from hashlib import md5
import json
from pytapo import Tapo
user = "admin"
password = "admin" # This password only works if you never used `changeAdminPassword` before
host = "192.168.191.1"
tapo = Tapo(host, user, password, printDebugInformation=True, redactConfidentialInformation=False)
def encryptPassphrase(passphrase):
public_pem_data = b"""-----BEGIN RSA PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4D6i0oD/Ga5qb//RfSe8MrPVI
rMIGecCxkcGWGj9kxxk74qQNq8XUuXoy2PczQ30BpiRHrlkbtBEPeWLpq85tfubT
UjhBz1NPNvWrC88uaYVGvzNpgzZOqDC35961uPTuvdUa8vztcUQjEZy16WbmetRj
URFIiWJgFCmemyYVbQIDAQAB
-----END RSA PUBLIC KEY-----"""
public_key = load_pem_public_key(public_pem_data)
ciphertext = public_key.encrypt(passphrase, padding.PKCS1v15())
return ciphertext
def multipleRequests(requests):
return {
"method": "multipleRequest",
"params": {
"requests": requests
}
}
# Work around login not working
def anonPerformRequest(req):
url = tapo.getHostURL()
res = tapo.request(
"POST",
url,
data=json.dumps(req),
headers=tapo.headers,
verify=False,
)
return res.json()
# Target values
third_account_username = "tapousername" # must be at least 6 symbols (!)
third_account_password = "tapopassword"
admin_password = "adminpassword"
# Change third user account
if True:
passwd = third_account_password
passwd_md5 = md5(passwd.encode('utf-8')).hexdigest().upper()
passwd_rsa = base64.b64encode(encryptPassphrase(passwd.encode('utf-8'))).decode('utf-8')
changeThirdAccount = {
"method": "changeThirdAccount",
"params": {
"user_management": {
"change_third_account": {
"secname": "third_account",
"passwd": passwd_md5, # uppercase of the md5 hash of your password
"old_passwd": "",
"ciphertext": passwd_rsa, # using the same public key extracted from the app, as mentioned in another comment
"username": third_account_username,
}
}
}
}
res = anonPerformRequest(multipleRequests([changeThirdAccount]))
print('changeThirdAccount', res)
verifyThirdAccount = {
"method": "verifyThirdAccount",
"params": {
"user_management": {
"verify_third_account": {
"secname": "third_account",
"passwd": passwd_md5, # uppercase of the md5 hash of your password
"old_passwd": "",
"ciphertext": passwd_rsa, # using the same public key extracted from the app, as mentioned in another comment
"username": third_account_username,
}
}
}
}
# Change admin password (the above must have ran before)
if False:
# Factory password (!)
passwd = admin_password
passwd_md5 = md5(passwd.encode('utf-8')).hexdigest().upper()
passwd_rsa = base64.b64encode(encryptPassphrase(passwd.encode('utf-8'))).decode('utf-8')
changeAdminPassword = {
"method": "changeAdminPassword",
"params": {
"user_management": {
"change_admin_password": {
"secname": "root",
"passwd": passwd_md5,
"old_passwd": md5(password.encode('utf-8')).hexdigest().upper(), # old password as md5 encoded
"ciphertext": passwd_rsa,
"username": "admin" # must be "admin" (?)
}
}
}
}
res = anonPerformRequest(multipleRequests([verifyThirdAccount, changeAdminPassword]))
print('changeAdminPassword', res)
After that, I did the connectAp
.
Following that, I did:
ffplay 'rtsp://tapousername:tapopassword@192.168.179.104/stream1' -rtsp_transport tcp
Connecting through admin:adminpassword
did not work.
The UDP rtsp was really unstable for me but that might also be related to my bridge which blocks the camera from the internet. I still have to figure out how to do two-way audio though.
I have a pretty good understanding about all involved tools (based on my RE of the C200 firmware, even though I own a C220):
/ds
requests and sends some of them to cloud-client)I plan to document my findings as I've also found some potential security issues (I plan responsible disclosure here if tplink has that). There are also a bunch of yet-to-be-documented commands. It should also be possible to upgrade the firmware without cloud-access, too - the endpoint for that is also exposed via uhttpd.
I also made my own client for the tplink cloud API (device-facing, not user-facing like other projects) but it still has some issues. My plan is to discover firmware URLs.
That way I should be able to get the camera completely cloudless, including the option to update.
hi @JayFoxRox
The steps I mention are from a traffic capture between the android application and camera on setup. Some of the steps probably not strictly necessary, but it covers what the application would do.
The third account details are what I use for rtsp
connections.
Below some hastily copied excerpts I have which may help you on the items you mentioned - although you have addressed some in later post. Unfortunately not much time at the moment for this project, but I have enumerated all the paths in the application (for my camera at least) so have a lot more traffic / request captures to review whenever I get some more time (e.g. setting the 'active zones' via getLinecrossingDetectionConfig
, disabling the OSD setOsd
, etc).
I also couldn't figure out how to do the steps in #24 (comment):
- ~
scanApList
~ - Done, trivial and documented abovesetLanguage
- Not sure what options or what parameterschangeAdminPassword
- While some fields are documented, the full structure / POST payload hasn't been shown yet and I can't seem to make it work. I also don't know the current password and I'm not sure if each fields contain the old or the new password- ~
setMediaEncrypt
~ - Why is this part of the setup? I've done this thoughsetTimezone
- I'm not sure what options or what parameterssetRecordPlan
- Not sure what options or what parameters- ~
connectAp
~ - Done, trivial and documented abovesetDeviceAlias
- Not sure what options or what parameterschangeThirdAccount
- Not sure what options or what parameters (also not sure what this account is used for)
setLanguage
:
{
"method": "setLanguage",
"params": {
"language": "EN"
}
}
setTimezone
:
{
"method": "setTimezone",
"params": {
"system": {
"basic": {
"timing_mode": "ntp",
"timezone": "UTC-00:00",
"zone_id": "UTC",
}
}
},
},
setRecordPlan
:
{
"method": "setRecordPlan",
"params": {
"record_plan": {
"chn1_channel": {
"enabled": "on",
"friday": '["0000-2400:2"]',
"monday": '["0000-2400:2"]',
"saturday": '["0000-2400:2"]',
"sunday": '["0000-2400:2"]',
"thursday": '["0000-2400:2"]',
"tuesday": '["0000-2400:2"]',
"wednesday": '["0000-2400:2"]',
}
}
},
},
setDeviceAlias
:
{
"method": "setDeviceAlias",
"params": {"system": {"sys": {"dev_alias": "mycamera"}}},
}
changeThirdAccount
:
{
"method": "changeThirdAccount",
"params": {
"user_management": {
"change_third_account": {
"secname": "third_account",
"passwd": new_third_account_password_md5,
"old_passwd": "",
"ciphertext": new_third_account_password_rsa,
"username": new_username,
}
}
},
}
verifyThirdAccount
:
{
"method": "verifyThirdAccount",
"params": {
"user_management": {
"verify_third_account": {
"secname": "third_account",
"passwd": current_third_account_password_md5,
"old_passwd": "",
"ciphertext": current_third_account_password_rsa,
"username": current_username,
}
}
},
}
@JayFoxRox
have made some of my own stuff available here: https://github.com/jp-0/tapo-camera (I guess be careful with the provisioning as it does run changeAdminPassword
)
You should be able to use the provision-camera
command
Also have a few other interactions with the camera detailed in cam.py
. There are plenty yet that I have not got to, however.
I'm currently struggling with process of provisioning the Tapo C100 camera.
When camera is in factory state it sets the unprotected AP named
Tapo_Camera_XXXX
whereXXXX
is some hex value. After connecting the dhcp server on camera assign the 192.168.191.100 ip address, while reporting the gateway to be 192.168.191.1.I assume it hosts the same API which is partially implemented in this library, as when querying the https server on this address it returns very similar responses.
Calling the
login
no matter the params, returns some stok result:I found out that there should be something like
onboarding
module, witchscan
,connect
andget_connect_status
.By comparing this to other methods implemented in this library, I assume it should be called somehow like:
Sadly this seems not to work.
Sadly I got stuck at this point, If you got any suggestions or question please ask.
I'll try to push it somehow forward and extend this library with provisioning methods.