Skip to content

TPAP Implementation - #1592

Open
ZeliardM wants to merge 73 commits into
python-kasa:masterfrom
ZeliardM:feature/tpap
Open

ZeliardM wants to merge 73 commits into
python-kasa:masterfrom
ZeliardM:feature/tpap

Conversation

@ZeliardM

Copy link
Copy Markdown
Collaborator

Discovered with new firmware for devices, TP-Link is implementing a new Encryption Type, TPAP. This is an initial implementation to see if the coding works for the handshake. Testing of the code coverage still has to be worked on. The initial implementation includes the new transport, changes to the device_factory to allow devices to select the new transport, and a change to the project to include ecdsa as a new dependency along with cryptography for the new tpaptransport.py.

Copilot AI review requested due to automatic review settings October 18, 2025 19:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Implements the new TPAP (TP-Link Adaptive Protocol) encryption type with SPAKE2+ HTTPS transport for TP-Link devices. This is an initial implementation to test handshake functionality with new firmware that uses TPAP encryption.

  • Added complete TPAP transport implementation using SPAKE2+ P-256 handshake and AEAD data channel
  • Updated device factory to support TPAP encryption type routing
  • Added ecdsa dependency for elliptic curve operations

Reviewed Changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pyproject.toml Added ecdsa dependency and mypy overrides for the new package
kasa/transports/tpaptransport.py New TPAP transport implementation with SPAKE2+ handshake and secure channel
kasa/transports/init.py Added TpapTransport to module exports
kasa/deviceconfig.py Added Tpap enum value to DeviceEncryptionType
kasa/device_factory.py Added SMART.TPAP.HTTPS protocol mapping and fixed typo

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment thread kasa/transports/tpaptransport.py Outdated
Comment thread kasa/transports/tpaptransport.py Outdated
Comment thread kasa/transports/tpaptransport.py Outdated
Comment thread kasa/transports/tpaptransport.py Fixed
Comment thread kasa/transports/tpaptransport.py Fixed
Comment thread kasa/transports/tpaptransport.py Fixed
Comment thread kasa/transports/tpaptransport.py Fixed
@codecov

codecov Bot commented Oct 18, 2025 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.73%. Comparing base (88e1c27) to head (e708447).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1592      +/-   ##
==========================================
+ Coverage   93.21%   93.73%   +0.52%     
==========================================
  Files         157      158       +1     
  Lines        9819    10637     +818     
  Branches     1005     1110     +105     
==========================================
+ Hits         9153     9971     +818     
  Misses        472      472              
  Partials      194      194              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread kasa/transports/tpaptransport.py Fixed
Comment thread kasa/transports/tpaptransport.py Fixed
Comment thread tests/transports/test_tpaptransport.py Fixed
Comment thread kasa/transports/tpaptransport.py Fixed
Comment thread kasa/transports/tpaptransport.py Fixed
Comment thread tests/transports/test_tpaptransport.py Fixed
@ZeliardM

Copy link
Copy Markdown
Collaborator Author

@rytilahti Ok, I think this is a good first pass. I just need someone with a RV device to test it. The CodeQL Security flags, from my understanding, will always come up with md5 and sha1 hashing in the code, but it's required for the device communication, just like with the other transports.

@danieyal

Copy link
Copy Markdown

Sanitized discovery, logs, and TLS observations for the RV30 Max Plus(EU)-Firmware:1.3.0 Build 250909 Rel.135514 using TPAP. Personally identifying values are redacted. Hope this help.

  • Discovery advertises TPAP with tls=2 and PAKE suite [2]; HTTPS on 4433.
  • TLS1.3 fails immediately; TLS1.2 handshake succeeds with ECDHE-ECDSA-CHACHA20-POLY1305; no ALPN.
  • AES “/app” login on this device returns text/html “200 OK” (likely wrong endpoint for this family).
  • TPAP transport attempts hit TLS alert 40 initially; after constraining to TLS1.2, device still doesn’t accept our HTTP request format (suspect different login paths/headers/body or a pre-HTTP step).

uv run kasa --username '' --password '' --debug --host 192.168.68.63 discover config

DEBUG    Trying to connect with SmartProtocol                                                            discover.py:681
DEBUG    Using SmartDevice for SMART.TAPOROBOVAC                                                   device_factory.py:180
DEBUG    Initializing 192.168.68.63 of type <class 'kasa.smart.smartdevice.SmartDevice'>                   device.py:214
DEBUG    192.168.68.63 multi-request-batch-1-of-1 >>                                                smartprotocol.py:241
         '{"method":"multipleRequest","request_time_milis":1760838809450,"terminal_uuid":"qHsKLwgAV
         HlHIO3ZttV4Mw==","params":{"requests":[{"method":"component_nego"},{"method":"get_device_i
         nfo"},{"method":"get_connect_cloud_state"}]}}'
DEBUG    Posting to https://192.168.68.63:4433/                                                         httpclient.py:88
DEBUG    Device 192.168.68.63 received an os error, enabling sequential request delay: Cannot connect  httpclient.py:136
         to host 192.168.68.63:4433 ssl:<ssl.SSLContext object at 0x000001B6D6A4F7D0> [[SSL:
         SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:1000)]
DEBUG    Device 192.168.68.63 got a connection error, will retry 3 times: ('Device connection       smartprotocol.py:145
         error: 192.168.68.63: Cannot connect to host 192.168.68.63:4433 ssl:<ssl.SSLContext object
         at 0x000001B6D6A4F7D0> [[SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure
         (_ssl.c:1000)]', ClientConnectorSSLError(ConnectionKey(host='192.168.68.63', port=4433,
         is_ssl=True, ssl=<ssl.SSLContext object at 0x000001B6D6A4F7D0>, proxy=None,
         proxy_auth=None, proxy_headers_hash=None), SSLError(1, '[SSL:
         SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:1000)')))
DEBUG    192.168.68.63 multi-request-batch-1-of-1 >>                                                smartprotocol.py:241
         '{"method":"multipleRequest","request_time_milis":1760838809471,"terminal_uuid":"qHsKLwgAV
         HlHIO3ZttV4Mw==","params":{"requests":[{"method":"component_nego"},{"method":"get_device_i
         nfo"},{"method":"get_connect_cloud_state"}]}}'
DEBUG    Device 192.168.68.63 waiting 0.25 seconds to send request                                      httpclient.py:81
DEBUG    Posting to https://192.168.68.63:4433/                                                         httpclient.py:88
DEBUG    192.168.68.63 multi-request-batch-1-of-1 >>                                                smartprotocol.py:241
         '{"method":"multipleRequest","request_time_milis":1760838809754,"terminal_uuid":"qHsKLwgAV
         HlHIO3ZttV4Mw==","params":{"requests":[{"method":"component_nego"},{"method":"get_device_i
         nfo"},{"method":"get_connect_cloud_state"}]}}'
DEBUG    Device 192.168.68.63 waiting 0.25 seconds to send request                                      httpclient.py:81
DEBUG    Posting to https://192.168.68.63:4433/                                                         httpclient.py:88
DEBUG    192.168.68.63 multi-request-batch-1-of-1 >>                                                smartprotocol.py:241
         '{"method":"multipleRequest","request_time_milis":1760838810228,"terminal_uuid":"qHsKLwgAV
         HlHIO3ZttV4Mw==","params":{"requests":[{"method":"component_nego"},{"method":"get_device_i
         nfo"},{"method":"get_connect_cloud_state"}]}}'
DEBUG    Device 192.168.68.63 waiting 0.25 seconds to send request                                      httpclient.py:81
DEBUG    Posting to https://192.168.68.63:4433/                                                         httpclient.py:88
DEBUG    Giving up on 192.168.68.63 after 3 retries                                                 smartprotocol.py:152
DEBUG    Unable to connect with SmartProtocol: ('Device connection error: 192.168.68.63: Cannot connect  discover.py:684
         to host 192.168.68.63:4433 ssl:<ssl.SSLContext object at 0x000001B6D6A4F7D0> [[SSL:
         SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:1000)]',
         ClientConnectorSSLError(ConnectionKey(host='192.168.68.63', port=4433, is_ssl=True,
         ssl=<ssl.SSLContext object at 0x000001B6D6A4F7D0>, proxy=None, proxy_auth=None,
         proxy_headers_hash=None), SSLError(1, '[SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert
         handshake failure (_ssl.c:1000)')))
Attempt to connect to 192.168.68.63 with SmartProtocol + TpapTransport + SmartDevice + https failed
Unable to connect to 192.168.68.63

uv run kasa --username '' --password '' --debug --host 192.168.68.63 discover raw

Discovering device 192.168.68.63 for 10 seconds
DEBUG    [DISCOVERY] 192.168.68.63 >> {'system': {'get_sysinfo': {}}}                                    discover.py:326
DEBUG    Waiting a total of 10 seconds for responses...                                                  discover.py:587
{
  "discovery_response": {
    "result": {
      "device_id": "REDACTED_f47bb068b8aacda06dec54e",
      "owner": "REDACTED_96B05E0B002C122F61EC032",
      "device_type": "SMART.TAPOROBOVAC",
      "device_model": "RV30 Max Plus(EU)",
      "ip": "192.168.68.63",
      "mac": "BC-07-1D-00-00-00",
      "is_support_iot_cloud": true,
      "tpap": {
        "tls": 2,
        "dac": 1,
        "noc": 1,
        "pake": [
          2
        ],
        "port": 4433
      },
      "obd_src": "tplink",
      "protocol_version": 1,
      "factory_default": false,
      "mgt_encrypt_schm": {
        "is_support_https": true,
        "http_port": 4433,
        "encrypt_type": "TPAP",
        "lv": 2
      }
    },
    "error_code": 0
  },
  "meta": {
    "ip": "192.168.68.63",
    "port": 20002
  }
}
DEBUG    Using SmartDevice for SMART.TAPOROBOVAC                                                   device_factory.py:180
DEBUG    Finding protocol for 192.168.68.63                                                        device_factory.py:195
DEBUG    Finding protocol for DeviceFamily.SmartTapoRobovac                                        device_factory.py:198
DEBUG    Finding transport for SMART.TPAP.HTTPS                                                    device_factory.py:227
DEBUG    [DISCOVERY] 192.168.68.63 << {'error_code': 0,                                                  discover.py:924
          'result': {'device_id': 'REDACTED_f47bb068b8aacda06dec54e',
                     'device_model': 'RV30 Max Plus(EU)',
                     'device_type': 'SMART.TAPOROBOVAC',
                     'factory_default': False,
                     'ip': '192.168.68.63',
                     'is_support_iot_cloud': True,
                     'mac': 'BC-07-1D-00-00-00',
                     'mgt_encrypt_schm': {'encrypt_type': 'TPAP',
                                          'http_port': 4433,
                                          'is_support_https': True,
                                          'lv': 2},
                     'obd_src': 'tplink',
                     'owner': 'REDACTED_96B05E0B002C122F61EC032',
                     'protocol_version': 1,
                     'tpap': {'dac': 1, 'noc': 1, 'pake': [2], 'port': 4433, 'tls': 2}}}
DEBUG    Initializing 192.168.68.63 of type <class 'kasa.smart.smartdevice.SmartDevice'>                   device.py:214

SSL:

root@pve:~# openssl s_client -connect 192.168.68.63:4433 -tls1_3 -alpn h2
Connecting to 192.168.68.63
CONNECTED(00000003)
40D767A1D0750000:error:0A000410:SSL routines:ssl3_read_bytes:ssl/tls alert handshake failure:../ssl/record/rec_layer_s3.c:916:SSL alert number 40
---
no peer certificate available
---
No client certificate CA names sent
Negotiated TLS1.3 group: <NULL>
---
SSL handshake has read 7 bytes and written 1469 bytes
Verification: OK
---
New, (NONE), Cipher is (NONE)
Protocol: TLSv1.3
This TLS version forbids renegotiation.
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 0 (ok)
---
root@pve:~# openssl s_client -connect 192.168.68.63:4433 -tls1_2 -alpn h2
Connecting to 192.168.68.63
CONNECTED(00000003)
Can't use SSL_get_servername
depth=1 CN=SMART.TAPOROBOVAC CA, O=TP-LINK SYSTEMS INC., L=Irvine, ST=California, C=US
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 CN=RV30 Max Plus(EU), OU=SMART.TAPOHUB
verify return:1
---
Certificate chain
 0 s:CN=RV30 Max Plus(EU), OU=SMART.TAPOHUB
   i:CN=SMART.TAPOROBOVAC CA, O=TP-LINK SYSTEMS INC., L=Irvine, ST=California, C=US
   a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA256
   v:NotBefore: Sep 19 20:43:33 2025 GMT; NotAfter: Sep 19 20:43:33 2030 GMT
 1 s:CN=SMART.TAPOROBOVAC CA, O=TP-LINK SYSTEMS INC., L=Irvine, ST=California, C=US
   i:CN=TP-LINK SYSTEMS DEVICE ROOT CA, O=TP-LINK SYSTEMS INC., L=Irvine, ST=California, C=US
   a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA256
   v:NotBefore: Nov 22 04:07:54 2024 GMT; NotAfter: Nov 20 04:07:54 2034 GMT
---
Server certificate
-----BEGIN CERTIFICATE-----
<REDACTED>
-----END CERTIFICATE-----
subject=CN=RV30 Max Plus(EU), OU=SMART.TAPOHUB
issuer=CN=SMART.TAPOROBOVAC CA, O=TP-LINK SYSTEMS INC., L=Irvine, ST=California, C=US
---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: ecdsa_secp256r1_sha256
Peer Temp Key: ECDH, secp384r1, 384 bits
---
SSL handshake has read 1476 bytes and written 350 bytes
Verification error: unable to get local issuer certificate
---
New, TLSv1.2, Cipher is ECDHE-ECDSA-CHACHA20-POLY1305
Protocol: TLSv1.2
Server public key is 256 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-ECDSA-CHACHA20-POLY1305
    Session-ID: <REDACTED>
    Session-ID-ctx:
    Master-Key: <REDACTED>
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    Start Time: 1760847175
    Timeout   : 7200 (sec)
    Verify return code: 20 (unable to get local issuer certificate)
    Extended master secret: yes
---

Wireshark:

Frame 221: Packet, 234 bytes on wire (1872 bits), 234 bytes captured (1872 bits) on interface \Device\NPF_{5B7CFEBB-96D4-4EA5-A9EB-AEB90614BE7A}, id 0
    Section number: 1
    Interface id: 0 (\Device\NPF_{5B7CFEBB-96D4-4EA5-A9EB-AEB90614BE7A})
        Interface name: \Device\NPF_{5B7CFEBB-96D4-4EA5-A9EB-AEB90614BE7A}
        Interface description: Ethernet
    Encapsulation type: Ethernet (1)
    Arrival Time: Oct 19, 2025 11:35:25.271976000 Malay Peninsula Standard Time
    UTC Arrival Time: Oct 19, 2025 03:35:25.271976000 UTC
    Epoch Arrival Time: 1760844925.271976000
    [Time shift for this packet: 0.000000000 seconds]
    [Time delta from previous captured frame: 1.073000 milliseconds]
    [Time delta from previous displayed frame: 1.073000 milliseconds]
    [Time since reference or first frame: 3 minutes, 2.003639000 seconds]
    Frame Number: 221
    Frame Length: 234 bytes (1872 bits)
    Capture Length: 234 bytes (1872 bits)
    [Frame is marked: False]
    [Frame is ignored: False]
    [Protocols in frame: eth:ethertype:ip:tcp:tls]
    Character encoding: ASCII (0)
    [Coloring Rule Name: TCP]
    [Coloring Rule String: tcp]
Ethernet II, Src: CompalInform_02:ed:ec (38:a7:46:02:ed:ec), Dst: TPLink_77:0e:7d (bc:07:1d:77:0e:7d)
    Destination: TPLink_77:0e:7d (bc:07:1d:77:0e:7d)
        .... ..0. .... .... .... .... = LG bit: Globally unique address (factory default)
        .... ...0 .... .... .... .... = IG bit: Individual address (unicast)
    Source: CompalInform_02:ed:ec (38:a7:46:02:ed:ec)
        .... ..0. .... .... .... .... = LG bit: Globally unique address (factory default)
        .... ...0 .... .... .... .... = IG bit: Individual address (unicast)
    Type: IPv4 (0x0800)
    [Stream index: 0]
Internet Protocol Version 4, Src: 192.168.68.91, Dst: 192.168.68.63
    0100 .... = Version: 4
    .... 0101 = Header Length: 20 bytes (5)
    Differentiated Services Field: 0x00 (DSCP: CS0, ECN: Not-ECT)
        0000 00.. = Differentiated Services Codepoint: Default (0)
        .... ..00 = Explicit Congestion Notification: Not ECN-Capable Transport (0)
    Total Length: 220
    Identification: 0x2625 (9765)
    010. .... = Flags: 0x2, Don't fragment
        0... .... = Reserved bit: Not set
        .1.. .... = Don't fragment: Set
        ..0. .... = More fragments: Not set
    ...0 0000 0000 0000 = Fragment Offset: 0
    Time to Live: 128
    Protocol: TCP (6)
    Header Checksum: 0x0000 [validation disabled]
    [Header checksum status: Unverified]
    Source Address: 192.168.68.91
    Destination Address: 192.168.68.63
    [Stream index: 0]
Transmission Control Protocol, Src Port: 46387, Dst Port: 4433, Seq: 1, Ack: 1, Len: 180
    Source Port: 46387
    Destination Port: 4433
    [Stream index: 5]
    [Stream Packet Number: 4]
    [Conversation completeness: Incomplete, DATA (15)]
        ..0. .... = RST: Absent
        ...0 .... = FIN: Absent
        .... 1... = Data: Present
        .... .1.. = ACK: Present
        .... ..1. = SYN-ACK: Present
        .... ...1 = SYN: Present
        [Completeness Flags: ··DASS]
    [TCP Segment Len: 180]
    Sequence Number: 1    (relative sequence number)
    Sequence Number (raw): 2124410112
    [Next Sequence Number: 181    (relative sequence number)]
    Acknowledgment Number: 1    (relative ack number)
    Acknowledgment number (raw): 1100174539
    0101 .... = Header Length: 20 bytes (5)
    Flags: 0x018 (PSH, ACK)
        000. .... .... = Reserved: Not set
        ...0 .... .... = Accurate ECN: Not set
        .... 0... .... = Congestion Window Reduced: Not set
        .... .0.. .... = ECN-Echo: Not set
        .... ..0. .... = Urgent: Not set
        .... ...1 .... = Acknowledgment: Set
        .... .... 1... = Push: Set
        .... .... .0.. = Reset: Not set
        .... .... ..0. = Syn: Not set
        .... .... ...0 = Fin: Not set
        [TCP Flags: ·······AP···]
    Window: 255
    [Calculated window size: 65280]
    [Window size scaling factor: 256]
    Checksum: 0x0aba [unverified]
    [Checksum Status: Unverified]
    Urgent Pointer: 0
    [Timestamps]
        [Time since first frame in this TCP stream: 8.459000 milliseconds]
        [Time since previous frame in this TCP stream: 1.073000 milliseconds]
    [SEQ/ACK analysis]
        [iRTT: 7.386000 milliseconds]
        [Bytes in flight: 180]
        [Bytes sent since last PSH flag: 180]
    [Client Contiguous Streams: 1]
    [Server Contiguous Streams: 1]
    TCP payload (180 bytes)
Transport Layer Security
    [Stream index: 2]
    TLSv1.2 Record Layer: Handshake Protocol: Client Hello
        Content Type: Handshake (22)
        Version: TLS 1.0 (0x0301)
        Length: 175
        Handshake Protocol: Client Hello
            Handshake Type: Client Hello (1)
            Length: 171
            Version: TLS 1.2 (0x0303)
            Random: 3cbe0eae6e3be4e450edfdf4d611302985ecb513dc5d147633b7e16465777bf3
                GMT Unix Time: Apr 18, 2002 08:09:18.000000000 Malay Peninsula Standard Time
                Random Bytes: 6e3be4e450edfdf4d611302985ecb513dc5d147633b7e16465777bf3
            Session ID Length: 32
            Session ID: 1b04dc59c6d238c56aff3701dc937ef2e4a14eed2c195aa26d2a880c4b9c8af1
            Cipher Suites Length: 16
            Cipher Suites (8 suites)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (0xc02c)
                Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f)
                Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xc009)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (0xc00a)
                Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xc013)
                Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xc014)
            Compression Methods Length: 1
            Compression Methods (1 method)
                Compression Method: null (0)
            Extensions Length: 82
            Extension: renegotiation_info (len=1)
                Type: renegotiation_info (65281)
                Length: 1
                Renegotiation Info extension
                    Renegotiation info extension length: 0
            Extension: extended_master_secret (len=0)
                Type: extended_master_secret (23)
                Length: 0
            Extension: session_ticket (len=0)
                Type: session_ticket (35)
                Length: 0
                Session Ticket: <MISSING>
            Extension: signature_algorithms (len=20)
                Type: signature_algorithms (13)
                Length: 20
                Signature Hash Algorithms Length: 18
                Signature Hash Algorithms (9 algorithms)
                    Signature Algorithm: ecdsa_secp256r1_sha256 (0x0403)
                        Signature Hash Algorithm Hash: SHA256 (4)
                        Signature Hash Algorithm Signature: ECDSA (3)
                    Signature Algorithm: rsa_pss_rsae_sha256 (0x0804)
                        Signature Hash Algorithm Hash: Unknown (8)
                        Signature Hash Algorithm Signature: Unknown (4)
                    Signature Algorithm: rsa_pkcs1_sha256 (0x0401)
                        Signature Hash Algorithm Hash: SHA256 (4)
                        Signature Hash Algorithm Signature: RSA (1)
                    Signature Algorithm: ecdsa_secp384r1_sha384 (0x0503)
                        Signature Hash Algorithm Hash: SHA384 (5)
                        Signature Hash Algorithm Signature: ECDSA (3)
                    Signature Algorithm: rsa_pss_rsae_sha384 (0x0805)
                        Signature Hash Algorithm Hash: Unknown (8)
                        Signature Hash Algorithm Signature: Unknown (5)
                    Signature Algorithm: rsa_pkcs1_sha384 (0x0501)
                        Signature Hash Algorithm Hash: SHA384 (5)
                        Signature Hash Algorithm Signature: RSA (1)
                    Signature Algorithm: rsa_pss_rsae_sha512 (0x0806)
                        Signature Hash Algorithm Hash: Unknown (8)
                        Signature Hash Algorithm Signature: Unknown (6)
                    Signature Algorithm: rsa_pkcs1_sha512 (0x0601)
                        Signature Hash Algorithm Hash: SHA512 (6)
                        Signature Hash Algorithm Signature: RSA (1)
                    Signature Algorithm: rsa_pkcs1_sha1 (0x0201)
                        Signature Hash Algorithm Hash: SHA1 (2)
                        Signature Hash Algorithm Signature: RSA (1)
            Extension: status_request (len=5)
                Type: status_request (5)
                Length: 5
                Certificate Status Type: OCSP (1)
                Responder ID list Length: 0
                Request Extensions Length: 0
            Extension: application_layer_protocol_negotiation (len=14)
                Type: application_layer_protocol_negotiation (16)
                Length: 14
                ALPN Extension Length: 12
                ALPN Protocol
                    ALPN string length: 2
                    ALPN Next Protocol: h2
                    ALPN string length: 8
                    ALPN Next Protocol: http/1.1
            Extension: ec_point_formats (len=2)
                Type: ec_point_formats (11)
                Length: 2
                EC point formats Length: 1
                Elliptic curves point formats (1)
                    EC point format: uncompressed (0)
            Extension: supported_groups (len=8)
                Type: supported_groups (10)
                Length: 8
                Supported Groups List Length: 6
                Supported Groups (3 groups)
                    Supported Group: x25519 (0x001d)
                    Supported Group: secp256r1 (0x0017)
                    Supported Group: secp384r1 (0x0018)

Frame 224: Packet, 63 bytes on wire (504 bits), 63 bytes captured (504 bits) on interface \Device\NPF_{5B7CFEBB-96D4-4EA5-A9EB-AEB90614BE7A}, id 0
    Section number: 1
    Interface id: 0 (\Device\NPF_{5B7CFEBB-96D4-4EA5-A9EB-AEB90614BE7A})
        Interface name: \Device\NPF_{5B7CFEBB-96D4-4EA5-A9EB-AEB90614BE7A}
        Interface description: Ethernet
    Encapsulation type: Ethernet (1)
    Arrival Time: Oct 19, 2025 11:35:25.281849000 Malay Peninsula Standard Time
    UTC Arrival Time: Oct 19, 2025 03:35:25.281849000 UTC
    Epoch Arrival Time: 1760844925.281849000
    [Time shift for this packet: 0.000000000 seconds]
    [Time delta from previous captured frame: 4.004000 milliseconds]
    [Time delta from previous displayed frame: 4.004000 milliseconds]
    [Time since reference or first frame: 3 minutes, 2.013512000 seconds]
    Frame Number: 224
    Frame Length: 63 bytes (504 bits)
    Capture Length: 63 bytes (504 bits)
    [Frame is marked: False]
    [Frame is ignored: False]
    [Protocols in frame: eth:ethertype:ip:tcp:tls]
    Character encoding: ASCII (0)
    [Coloring Rule Name: TCP]
    [Coloring Rule String: tcp]
Ethernet II, Src: TPLink_77:0e:7d (bc:07:1d:77:0e:7d), Dst: CompalInform_02:ed:ec (38:a7:46:02:ed:ec)
    Destination: CompalInform_02:ed:ec (38:a7:46:02:ed:ec)
        .... ..0. .... .... .... .... = LG bit: Globally unique address (factory default)
        .... ...0 .... .... .... .... = IG bit: Individual address (unicast)
    Source: TPLink_77:0e:7d (bc:07:1d:77:0e:7d)
        .... ..0. .... .... .... .... = LG bit: Globally unique address (factory default)
        .... ...0 .... .... .... .... = IG bit: Individual address (unicast)
    Type: IPv4 (0x0800)
    [Stream index: 0]
Internet Protocol Version 4, Src: 192.168.68.63, Dst: 192.168.68.91
    0100 .... = Version: 4
    .... 0101 = Header Length: 20 bytes (5)
    Differentiated Services Field: 0x00 (DSCP: CS0, ECN: Not-ECT)
        0000 00.. = Differentiated Services Codepoint: Default (0)
        .... ..00 = Explicit Congestion Notification: Not ECN-Capable Transport (0)
    Total Length: 49
    Identification: 0x2213 (8723)
    010. .... = Flags: 0x2, Don't fragment
        0... .... = Reserved bit: Not set
        .1.. .... = Don't fragment: Set
        ..0. .... = More fragments: Not set
    ...0 0000 0000 0000 = Fragment Offset: 0
    Time to Live: 64
    Protocol: TCP (6)
    Header Checksum: 0x0ec9 [validation disabled]
    [Header checksum status: Unverified]
    Source Address: 192.168.68.63
    Destination Address: 192.168.68.91
    [Stream index: 0]
Transmission Control Protocol, Src Port: 4433, Dst Port: 46386, Seq: 1426, Ack: 181, Len: 9
    Source Port: 4433
    Destination Port: 46386
    [Stream index: 4]
    [Stream Packet Number: 12]
    [Conversation completeness: Incomplete, DATA (15)]
        ..0. .... = RST: Absent
        ...0 .... = FIN: Absent
        .... 1... = Data: Present
        .... .1.. = ACK: Present
        .... ..1. = SYN-ACK: Present
        .... ...1 = SYN: Present
        [Completeness Flags: ··DASS]
    [TCP Segment Len: 9]
    Sequence Number: 1426    (relative sequence number)
    Sequence Number (raw): 1085180748
    [Next Sequence Number: 1435    (relative sequence number)]
    Acknowledgment Number: 181    (relative ack number)
    Acknowledgment number (raw): 241256951
    0101 .... = Header Length: 20 bytes (5)
    Flags: 0x018 (PSH, ACK)
        000. .... .... = Reserved: Not set
        ...0 .... .... = Accurate ECN: Not set
        .... 0... .... = Congestion Window Reduced: Not set
        .... .0.. .... = ECN-Echo: Not set
        .... ..0. .... = Urgent: Not set
        .... ...1 .... = Acknowledgment: Set
        .... .... 1... = Push: Set
        .... .... .0.. = Reset: Not set
        .... .... ..0. = Syn: Not set
        .... .... ...0 = Fin: Not set
        [TCP Flags: ·······AP···]
    Window: 1892
    [Calculated window size: 30272]
    [Window size scaling factor: 16]
    Checksum: 0x968c [unverified]
    [Checksum Status: Unverified]
    Urgent Pointer: 0
    [Timestamps]
        [Time since first frame in this TCP stream: 216.772000 milliseconds]
        [Time since previous frame in this TCP stream: 4.204000 milliseconds]
    [SEQ/ACK analysis]
        [iRTT: 11.499000 milliseconds]
        [Bytes in flight: 9]
        [Bytes sent since last PSH flag: 9]
    [Client Contiguous Streams: 1]
    [Server Contiguous Streams: 1]
    TCP payload (9 bytes)
Transport Layer Security
    [Stream index: 1]
    TLSv1.2 Record Layer: Handshake Protocol: Server Hello Done
        Content Type: Handshake (22)
        Version: TLS 1.2 (0x0303)
        Length: 4
        Handshake Protocol: Server Hello Done
            Handshake Type: Server Hello Done (14)
            Length: 0
Frame 225: Packet, 212 bytes on wire (1696 bits), 212 bytes captured (1696 bits) on interface \Device\NPF_{5B7CFEBB-96D4-4EA5-A9EB-AEB90614BE7A}, id 0
    Section number: 1
    Interface id: 0 (\Device\NPF_{5B7CFEBB-96D4-4EA5-A9EB-AEB90614BE7A})
        Interface name: \Device\NPF_{5B7CFEBB-96D4-4EA5-A9EB-AEB90614BE7A}
        Interface description: Ethernet
    Encapsulation type: Ethernet (1)
    Arrival Time: Oct 19, 2025 11:35:25.284112000 Malay Peninsula Standard Time
    UTC Arrival Time: Oct 19, 2025 03:35:25.284112000 UTC
    Epoch Arrival Time: 1760844925.284112000
    [Time shift for this packet: 0.000000000 seconds]
    [Time delta from previous captured frame: 2.263000 milliseconds]
    [Time delta from previous displayed frame: 2.263000 milliseconds]
    [Time since reference or first frame: 3 minutes, 2.015775000 seconds]
    Frame Number: 225
    Frame Length: 212 bytes (1696 bits)
    Capture Length: 212 bytes (1696 bits)
    [Frame is marked: False]
    [Frame is ignored: False]
    [Protocols in frame: eth:ethertype:ip:tcp:tls]
    Character encoding: ASCII (0)
    [Coloring Rule Name: TCP]
    [Coloring Rule String: tcp]
Ethernet II, Src: CompalInform_02:ed:ec (38:a7:46:02:ed:ec), Dst: TPLink_77:0e:7d (bc:07:1d:77:0e:7d)
    Destination: TPLink_77:0e:7d (bc:07:1d:77:0e:7d)
        .... ..0. .... .... .... .... = LG bit: Globally unique address (factory default)
        .... ...0 .... .... .... .... = IG bit: Individual address (unicast)
    Source: CompalInform_02:ed:ec (38:a7:46:02:ed:ec)
        .... ..0. .... .... .... .... = LG bit: Globally unique address (factory default)
        .... ...0 .... .... .... .... = IG bit: Individual address (unicast)
    Type: IPv4 (0x0800)
    [Stream index: 0]
Internet Protocol Version 4, Src: 192.168.68.91, Dst: 192.168.68.63
    0100 .... = Version: 4
    .... 0101 = Header Length: 20 bytes (5)
    Differentiated Services Field: 0x00 (DSCP: CS0, ECN: Not-ECT)
        0000 00.. = Differentiated Services Codepoint: Default (0)
        .... ..00 = Explicit Congestion Notification: Not ECN-Capable Transport (0)
    Total Length: 198
    Identification: 0x2627 (9767)
    010. .... = Flags: 0x2, Don't fragment
        0... .... = Reserved bit: Not set
        .1.. .... = Don't fragment: Set
        ..0. .... = More fragments: Not set
    ...0 0000 0000 0000 = Fragment Offset: 0
    Time to Live: 128
    Protocol: TCP (6)
    Header Checksum: 0x0000 [validation disabled]
    [Header checksum status: Unverified]
    Source Address: 192.168.68.91
    Destination Address: 192.168.68.63
    [Stream index: 0]
Transmission Control Protocol, Src Port: 46386, Dst Port: 4433, Seq: 181, Ack: 1435, Len: 158
    Source Port: 46386
    Destination Port: 4433
    [Stream index: 4]
    [Stream Packet Number: 13]
    [Conversation completeness: Incomplete, DATA (15)]
        ..0. .... = RST: Absent
        ...0 .... = FIN: Absent
        .... 1... = Data: Present
        .... .1.. = ACK: Present
        .... ..1. = SYN-ACK: Present
        .... ...1 = SYN: Present
        [Completeness Flags: ··DASS]
    [TCP Segment Len: 158]
    Sequence Number: 181    (relative sequence number)
    Sequence Number (raw): 241256951
    [Next Sequence Number: 339    (relative sequence number)]
    Acknowledgment Number: 1435    (relative ack number)
    Acknowledgment number (raw): 1085180757
    0101 .... = Header Length: 20 bytes (5)
    Flags: 0x018 (PSH, ACK)
        000. .... .... = Reserved: Not set
        ...0 .... .... = Accurate ECN: Not set
        .... 0... .... = Congestion Window Reduced: Not set
        .... .0.. .... = ECN-Echo: Not set
        .... ..0. .... = Urgent: Not set
        .... ...1 .... = Acknowledgment: Set
        .... .... 1... = Push: Set
        .... .... .0.. = Reset: Not set
        .... .... ..0. = Syn: Not set
        .... .... ...0 = Fin: Not set
        [TCP Flags: ·······AP···]
    Window: 250
    [Calculated window size: 64000]
    [Window size scaling factor: 256]
    Checksum: 0x0aa4 [unverified]
    [Checksum Status: Unverified]
    Urgent Pointer: 0
    [Timestamps]
        [Time since first frame in this TCP stream: 219.035000 milliseconds]
        [Time since previous frame in this TCP stream: 2.263000 milliseconds]
    [SEQ/ACK analysis]
        [This is an ACK to the segment in frame: 224]
        [The RTT to ACK the segment was: 2.263000 milliseconds]
        [iRTT: 11.499000 milliseconds]
        [Bytes in flight: 158]
        [Bytes sent since last PSH flag: 158]
    [Client Contiguous Streams: 1]
    [Server Contiguous Streams: 1]
    TCP payload (158 bytes)
Transport Layer Security
    [Stream index: 1]
    TLSv1.2 Record Layer: Handshake Protocol: Client Key Exchange
        Content Type: Handshake (22)
        Version: TLS 1.2 (0x0303)
        Length: 102
        Handshake Protocol: Client Key Exchange
            Handshake Type: Client Key Exchange (16)
            Length: 98
            EC Diffie-Hellman Client Params
                Pubkey Length: 97
                Pubkey: 0495574c7aa82261d993d4498805cc1090590eb703e0f85f77a68b4ca4f6dc148b8d401ff25e0359f99a06473e8fa771956ecbcf6512de82961734bc2584546b0ffc4ebcd2d787aa39402881fb51297e8dab3dc0f887e863494fa8b8fba336c0c6
    TLSv1.2 Record Layer: Change Cipher Spec Protocol: Change Cipher Spec
        Content Type: Change Cipher Spec (20)
        Version: TLS 1.2 (0x0303)
        Length: 1
        Change Cipher Spec Message
    TLSv1.2 Record Layer: Handshake Protocol: Encrypted Handshake Message
        Content Type: Handshake (22)
        Version: TLS 1.2 (0x0303)
        Length: 40
        Handshake Protocol: Encrypted Handshake Message

@ZeliardM

Copy link
Copy Markdown
Collaborator Author

@danieyal Pull the latest commit and try giving it a shot again.

@danieyal

Copy link
Copy Markdown

unfortunately, still the same error.

DEBUG    Trying to connect with SmartProtocol                                                             discover.py:681
DEBUG    Using SmartDevice for SMART.TAPOROBOVAC                                                    device_factory.py:180
DEBUG    Initializing 192.168.68.63 of type <class 'kasa.smart.smartdevice.SmartDevice'>                    device.py:214
DEBUG    192.168.68.63 multi-request-batch-1-of-1 >>                                                 smartprotocol.py:241
         '{"method":"multipleRequest","request_time_milis":1760887822371,"terminal_uuid":"mRY0IrNONh
         G5kZRhIPcYrg==","params":{"requests":[{"method":"component_nego"},{"method":"get_device_inf
         o"},{"method":"get_connect_cloud_state"}]}}'
DEBUG    Posting to https://192.168.68.63:4433/                                                          httpclient.py:88
DEBUG    Device 192.168.68.63 received an os error, enabling sequential request delay: Cannot connect   httpclient.py:157
         to host 192.168.68.63:4433 ssl:<ssl.SSLContext object at 0x0000025A3D7D7550> [[SSL:
         SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:1000)]
DEBUG    Device 192.168.68.63 got a connection error, will retry 3 times: ('Device connection error: smartprotocol.py:145
         192.168.68.63: Cannot connect to host 192.168.68.63:4433 ssl:<ssl.SSLContext object at
         0x0000025A3D7D7550> [[SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure
         (_ssl.c:1000)]', ClientConnectorSSLError(ConnectionKey(host='192.168.68.63', port=4433,
         is_ssl=True, ssl=<ssl.SSLContext object at 0x0000025A3D7D7550>, proxy=None,
         proxy_auth=None, proxy_headers_hash=None), SSLError(1, '[SSL:
         SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:1000)')))
DEBUG    192.168.68.63 multi-request-batch-1-of-1 >>                                                 smartprotocol.py:241
         '{"method":"multipleRequest","request_time_milis":1760887822496,"terminal_uuid":"mRY0IrNONh
         G5kZRhIPcYrg==","params":{"requests":[{"method":"component_nego"},{"method":"get_device_inf
         o"},{"method":"get_connect_cloud_state"}]}}'
DEBUG    Device 192.168.68.63 waiting 0.25 seconds to send request                                       httpclient.py:81
DEBUG    Posting to https://192.168.68.63:4433/                                                          httpclient.py:88
DEBUG    192.168.68.63 multi-request-batch-1-of-1 >>                                                 smartprotocol.py:241
         '{"method":"multipleRequest","request_time_milis":1760887823014,"terminal_uuid":"mRY0IrNONh
         G5kZRhIPcYrg==","params":{"requests":[{"method":"component_nego"},{"method":"get_device_inf
         o"},{"method":"get_connect_cloud_state"}]}}'
DEBUG    Device 192.168.68.63 waiting 0.25 seconds to send request                                       httpclient.py:81
DEBUG    Posting to https://192.168.68.63:4433/                                                          httpclient.py:88
DEBUG    192.168.68.63 multi-request-batch-1-of-1 >>                                                 smartprotocol.py:241
         '{"method":"multipleRequest","request_time_milis":1760887823298,"terminal_uuid":"mRY0IrNONh
         G5kZRhIPcYrg==","params":{"requests":[{"method":"component_nego"},{"method":"get_device_inf
         o"},{"method":"get_connect_cloud_state"}]}}'
DEBUG    Device 192.168.68.63 waiting 0.25 seconds to send request                                       httpclient.py:81
DEBUG    Posting to https://192.168.68.63:4433/                                                          httpclient.py:88
DEBUG    Giving up on 192.168.68.63 after 3 retries                                                  smartprotocol.py:152
DEBUG    Unable to connect with SmartProtocol: ('Device connection error: 192.168.68.63: Cannot connect   discover.py:684
         to host 192.168.68.63:4433 ssl:<ssl.SSLContext object at 0x0000025A3D7D7550> [[SSL:
         SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:1000)]',
         ClientConnectorSSLError(ConnectionKey(host='192.168.68.63', port=4433, is_ssl=True,
         ssl=<ssl.SSLContext object at 0x0000025A3D7D7550>, proxy=None, proxy_auth=None,
         proxy_headers_hash=None), SSLError(1, '[SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert
         handshake failure (_ssl.c:1000)')))
Attempt to connect to 192.168.68.63 with SmartProtocol + TpapTransport + SmartDevice + https failed
Unable to connect to 192.168.68.63

@danieyal

Copy link
Copy Markdown

I think i have got the TLS working now but stuck on the authentication now.

  • TLS 1.2, cipher ECDHE-ECDSA-AES-256-GCM-SHA384
  • No SNI (IP direct)
  • ALPN offered: h2, http/1.1
  • Server certificate chain: TP-LINK SYSTEMS DEVICE ROOT CA → SMART.TAPOROBOVAC CA → device leaf (CN RV30 Max Plus(EU), OU SMART.TAPOHUB)
  • OCSP AIA: http://ocsp-tss.tplinkcloud.com
  • No CertificateRequest observed → not mTLS
CIPHERS = ":".join(
        [
            "ECDHE-ECDSA-AES256-GCM-SHA384",
            "ECDHE-ECDSA-CHACHA20-POLY1305",
            "ECDHE-ECDSA-AES128-GCM-SHA256",
            "AES256-GCM-SHA384",
            "AES256-SHA256",
            "AES128-GCM-SHA256",
            "AES128-SHA256",
            "AES256-SHA",
        ]
    )

The device is returning a JSON response with 'error_code': -2402 along with authentication failure details like failedAttempts and remainAttempts. The device is rejecting the authentication attempt at the pake_register stage returning error code -2402. My theory is the device is actively rejecting our credentials/authentication attempt before we even get to the SPAKE2+ cryptographic exchange. This suggests the device needs something we're not providing, most likely DAC support.

DEBUG    TPAP register(empty) response: status=200, data_type=dict, data_len=3,
         data={'error_info': {'lockedMinute': 0, 'failedAttempts': 4, 'remainAttempts': 11},
         'result': {'sub_method': 'pake_register'}, 'error_code': -2402}

@ZeliardM

Copy link
Copy Markdown
Collaborator Author

I have been working on this today without much movement, still trying to figure out the authentication pieces. Looks like I will have to implement NOC but having issues getting the information and URLs for the certificate registration with the Tapo cloud. There is an API rate limit which causes problems as well. So, I'm still working on this, but nothing yet.

@ZeliardM

Copy link
Copy Markdown
Collaborator Author

@danieyal Are you able to use the Tapo app on your computer with Wireshark? I'm trying to reverse engineer the url for the certificates and the requests are not working. I am looking for something to do with:
nbu.cvm-server-v2

This is what points to where to apply for the certificates, but I can't get the communication to work correctly on my end. I'm trying to get the signature correct with the app to the cloud so I can pull the url, but I can't get that either. Until I have the URL to work with the serviceId: nbu.cvm-server-v2, then I can't get the library to handle the noc certificates.

@ZeliardM

Copy link
Copy Markdown
Collaborator Author

@danieyal try pulling again and let me know. I updated the ciphers like you found and I also corrected the last error you posted, it went to the stok field in the register parameters, it needed to be sessionId instead. Let me know what you get with this.

@danieyal

danieyal commented Oct 22, 2025 •

Copy link
Copy Markdown

@ZeliardM Thanks for the updates, and to clarify my side:

  • I can’t use Wireshark alone due to TLS/Cert pinning; I’m using Frida. I’ve posted sanitized findings (TLS, DS wrapper, discovery, logs) in my report and can share more privately if needed.
  • RV30 appears to commission via cloud and only then uses local DS:
    • Local DS: POST https://<ip>:4433/stok=<REDACTED>/ds with headers Accept: application/octet-stream, Connection: keep-alive, body wrapper:
      {"inputParams":{"requestData":{<SMART JSON>}},"serviceId":"passthrough"}
    • Plain JSON to /ds returns {"error_code": -2402} on this firmware, implying DS expects the SPAKE-protected binary frames tied to the cloud-issued session.
  • TLS (local): TLS 1.2 only, ECDHE‑ECDSA (AES‑256‑GCM/CHACHA20), no SNI, ALPN offered h2/http/1.1, device ECDSA leaf via SMART.TAPOROBOVAC CA.

Re: NOC/nbu.cvm-server-v2:

  • I haven’t seen any local pake_register at all; the app drives commissioning over cloud (…/v1/things/<thing-id>/services-sync) and then hits local DS.
  • I’ll keep trying to capture the exact services-sync request/response for serviceId: nbu.cvm-server-v2 with Frida (and sanitize), but so far, I have not seen it at all. If someone already has the signed payload/URL shape, that would unblock wiring NOC into the library much faster.
  • I cannot directly see DAC/NOC (Matter) details locally since those are invoked against the cloud endpoints.

I also pulled the latest branch:

  • Cipher updates look good.
  • Current failure is still -2402 at register.

Frida Log for device commissioning, I reset my device to see if pake_register appear at all, but it seems like nope, it just communicates over cloud regardless, other Tapo devices (plug, bulb, hub, camera) that I own still communicates locally (so probably not local network issue), I can 'see' the local endpoint for other devices but only vacuum just straight to cloud endpoint. Sorry, if it takes too long, setting up Frida took longer than expected.

frida -U -f com.tplink.iot -l .\unpin.js -l .\okhttp_log.js -l .\mac_sniff.js -l .\json_watch.js -l .\native_sock_hook.js -l .\hook_socket_write.js -l .\hook_socket_java.js -l .\okhttp_resp_tap.js
     ____
    / _  |   Frida 17.4.0 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://frida.re/docs/home/
Waiting for USB device to appear...
   . . . .
   . . . .   Connected to Redmi Note 5 (id=adb-21f3ca07-4K2z0L._adb-tls-connect._tcp)
Spawned `com.tplink.iot`. Resuming main thread!

---- OKHTTP REQUEST ----
URL: https://n-aps1-wap.i.tplinkcloud.com/api/v2/account/checkPassword
Method: POST
Headers:
signature-required: true
token-required: false

Body:
{"appType":"TP-Link_Tapo_Android","cloudPassword":"<REDACTED>","cloudUserName":"<REDACTED>"}
------------------------
[KeySpec] algo=HmacSHA1 key=54,101,100,55,100,57,55,102,51,101,55,51,52,54,55,......
[Mac] getInstance: HmacSHA1
---- OKHTTP REQUEST ----
URL: https://nbu.iot-app-server.app-v2/v1/families/thing-onboarding
Method: POST
Headers:

Body:
{"familyId":"default","roomId":"2sVeb2pm","thingNames":["<REDACTED>"]}
------------------------
[Socket.connect] addr=aps1-app-server.iot.i.tplinkcloud.com/54.254.71.245:443 timeout=30000
---- OKHTTP REQUEST ----
URL: https://192.168.68.63:4433/stok=<REDACTED>/ds
Method: POST
Headers:
Connection: keep-alive
Accept: application/octet-stream

Body:
  fDɣ   S5*:  3  @ 2 ԧ  k JٗhC W x~  I   p 
------------------------
[Socket.connect] addr=/192.168.68.63:4433 timeout=30000
---- OKHTTP REQUEST ----
URL: https://n-aps1-wap.i.tplinkcloud.com/api/v2/common/getDeviceListByPage
Method: POST
Headers:
signature-required: true

Body:
{"deviceTypeList":["SMART.TAPOPLUG","SMART.TAPOBULB","SMART.IPCAMERA","SMART.TAPOHUB","SMART.TAPOSENSOR","SMART.TAPOSWITCH","SMART.TAPOROBOVAC","SMART.TAPODOORBELL","SMART.TAPOLOCK","SMART.TAPOREMOTE","SMART.TAPOCHIME","SMART.KASAPLUG","SMART.KASASWITCH","SMART.KASAHUB","SMART.KASAENERGY","IOT.IPCAMERA"],"index":0,"limit":20}
------------------------
[KeySpec] algo=HmacSHA1 key=54,101,100,55,100,57,55,102,51,101,55,51,52,54,55,......
[Mac] getInstance: HmacSHA1
[Socket.connect] addr=n-aps1-wap.i.tplinkcloud.com/47.130.43.98:443 timeout=30000
---- OKHTTP REQUEST ----
URL: https://nbu.iot-app-server.app-v2/v2/things?page=0&pageSize=100&deviceTypes=SMART.TAPOPLUG%2CSMART.TAPOBULB%2CSMART.IPCAMERA%2CSMART.TAPOHUB%2CSMART.TAPOSENSOR%2CSMART.TAPOSWITCH%2CSMART.TAPOROBOVAC%2CSMART.TAPODOORBELL%2CSMART.TAPOLOCK%2CSMART.TAPOREMOTE%2CSMART.TAPOCHIME%2CIOT.SMARTPLUGSWITCH%2CIOT.SMARTBULB%2CIOT.IPCAMERA%2CIOT.HUB%2CIOT.RANGEEXTENDER%2CIOT.RANGEEXTENDER.SMARTPLUG%2CIOT.ROUTER%2CSMART.KASAPLUG%2CSMART.KASASWITCH%2CSMART.KASAHUB%2CSMART.KASAENERGY%2CSMART.MATTERPLUG%2CSMART.MATTERBULB&includePcDevice=false&includeKasaShareDevices=true&includeMatterDevice=false
Method: GET
Headers:

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/rt-info
Method: GET
Headers:

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://192.168.68.63:4433/stok=<REDACTED>/ds
Method: POST
Headers:
Connection: keep-alive
Accept: application/octet-stream

Body:
  g  6B
  Y >   Q!Tf ^  FsD  NHL
------------------------
[Socket.connect] addr=/192.168.68.63:4433 timeout=30000
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/details
Method: GET
Headers:

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/settings
Method: GET
Headers:

logReq err: TypeError: not a function
[Socket.connect] addr=aps1-app-server.iot.i.tplinkcloud.com/54.254.71.245:443 timeout=30000
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/components
Method: GET
Headers:

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/shadows?thingNames=<REDACTED>
Method: GET
Headers:
isEdgeRequest: false

logReq err: TypeError: not a function
[Socket.connect] addr=aps1-app-server.iot.i.tplinkcloud.com/54.254.71.245:443 timeout=30000
[Socket.connect] addr=aps1-app-server.iot.i.tplinkcloud.com/54.254.71.245:443 timeout=30000
[Socket.connect] addr=aps1-app-server.iot.i.tplinkcloud.com/54.254.71.245:443 timeout=30000
---- OKHTTP REQUEST ----
URL: https://n-aps1-wap.i.tplinkcloud.com/api/v2/common/getIntlFwList
Method: POST
Headers:
signature-required: true

Body:
{"devFwCurrentVer":"1.3.0","deviceId":"<REDACTED>","fwId":"00000000000000000000000000000000","hwId":"FE727A169352FE69<REDACTED>","locale":"en_US","oemId":"E24B0568952B79<REDACTED>"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"setQuickSetupExtraInfo","params":{"nickname":"Um9ib3QgVmFjdXVt"}}]}}},"serviceId":"passthrough"}
------------------------
[KeySpec] algo=HmacSHA1 key=54,101,100,55,100,57,55,102,51,101,55,51,52,54,55,......
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"account_sync","params":{"cloud_account":{"password":"<REDACTED>","username":"<REDACTED>,"serviceId":"passthrough"}
------------------------
[Mac] getInstance: HmacSHA1
[Socket.connect] addr=n-aps1-wap.i.tplinkcloud.com/47.130.43.98:443 timeout=30000
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/components
Method: GET
Headers:

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/voice-packages?deviceModel=RV30%20Max%20Plus
Method: GET
Headers:

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://192.168.68.63:4433/stok=<REDACTED>/ds
Method: POST
Headers:
Connection: keep-alive
Accept: application/octet-stream

Body:
      _Tt3 v LB Q[iw LaG    @ T 24 ؏ 
------------------------
[Socket.connect] addr=/192.168.68.63:4433 timeout=30000
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/components
Method: GET
Headers:

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/features/autoUpdateMode
Method: PATCH
Headers:

Body:
{"enable":true,"random_range":120,"time":180}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getCurrentVoiceLanguage"}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/voice-packages?deviceModel=RV30
Method: GET
Headers:

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://192.168.68.63:4433/stok=<REDACTED>/ds
Method: POST
Headers:
Connection: keep-alive
Accept: application/octet-stream

Body:
  Ô{ F ZR  =F    |ZK0K  @   A  8    v;|Z  e 
------------------------
[Socket.connect] addr=/192.168.68.63:4433 timeout=30000
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/components
Method: GET
Headers:

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/voice-packages/signed-url?voicePackageId=2
Method: GET
Headers:

logReq err: TypeError: not a function
[Socket.connect] addr=aps1-app-server.iot.i.tplinkcloud.com/54.254.71.245:443 timeout=30000
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"setVoiceLanguage","params":{"md5":"055ba03d9973ac5dd","name":"bb053ca2c56f3902b","url":"https://prd-iot-as-aps1.s3.ap-southeast-1.amazonaws.com/%2FvoicePackages/voicePackages2/2_English_Female_American.tar.gz?X-Amz-Security-Token=IQoJb3JpZ2luX2VjEHYaDmFwLXNvdXRoZWFzdC0xIkgwRgIhAM3vt3Fd2jRBA8aYB71rzzu1Woohe1O%2BvnvOlH4wLz3CAiEAzvdnJc4HuMUxH%2Fdq0hUmUUXVrYvkG3jP%2Ba%2FB6Vj3jI8qnQUILxADGgw3NDYxNjY3NTYyNjUiDFr71TsbnRtbNUX4ACr6BPGn9lOCsTm5n34KJtuhT33sBzsxQes0I2AamY%2B73JvPJMc08a%2FQ%2FqNM%2BtdxjDcxILd%2F%2BGgSeAJVl5WpSuyeG1gs3cpOkcQY6yvsm8PoCHlb5EyugCp2oDmm6HgMj%2Bkuf4IXODgZmohgtRNculKigHe2c9i7A5w4V2psD4YYdjBQBZFApSaKd%2BhloX3ctD7m0KGaeKxJeJzjD1zN5wBzmGh8UAY1a6iLSont%2BHnljSQsZu9U7iDe99%2FSxcbWZL2nEjvjyVENN%2B94m1a3yLeWg8WpaLpcPj%2FUeBHA&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20251022T132901Z&X-Amz-SignedHeaders=host&X-Amz-Expires=3599&X-Amz-Credential=ASIA23OYBEOUZHYDRAAJ%2F20251022%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Signature=b165c66043b59fac66d120144f8e51f","version":2}}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/shadows?thingNames=<REDACTED>
Method: GET
Headers:
isEdgeRequest: false

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getCleanAttr","params":{"type":"global"}}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getMapInfo"}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/details
Method: GET
Headers:

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/settings
Method: GET
Headers:

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/shadows?thingNames=<REDACTED>
Method: GET
Headers:
isEdgeRequest: false

logReq err: TypeError: not a function
[Socket.connect] addr=aps1-app-server.iot.i.tplinkcloud.com/54.254.71.245:443 timeout=30000
[Socket.connect] addr=aps1-app-server.iot.i.tplinkcloud.com/54.254.71.245:443 timeout=30000
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getCleanAttr","params":{"type":"running"}},{"method":"getAreaUnit"},{"method":"getCleanRecords"},{"method":"getDoNotDisturb"},{"method":"getConsumablesInfo"},{"method":"getMopState"},{"method":"getDeviceTime"},{"method":"getCurrentVoiceLanguage"},{"method":"getCustomizationRulesInfo"}]}}},"serviceId":"passthrough"}
------------------------
[Socket.connect] addr=aps1-app-server.iot.i.tplinkcloud.com/54.254.71.245:443 timeout=30000
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/voice-packages?deviceModel=RV30%20Max%20Plus
Method: GET
Headers:

logReq err: TypeError: not a function
[Socket.connect] addr=aps1-app-server.iot.i.tplinkcloud.com/54.254.71.245:443 timeout=30000
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getMapData","params":{}},{"method":"getPathData","params":{"start_pos":0}}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/rt-info
Method: GET
Headers:

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getCleanAttr","params":{"type":"global"}}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/firmwares/latest
Method: GET
Headers:

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"startUpdateMapData","params":{"map_upload_start":true}}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getMapData","params":{}},{"method":"getPathData","params":{"start_pos":0}}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"startUpdateMapData","params":{"map_upload_start":true}}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getMapInfo"}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getMapData","params":{"map_id":0}},{"method":"getPathData","params":{"start_pos":0}}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getPathData","params":{"start_pos":0}}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getPathData","params":{"start_pos":0}}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getCleanRecords"}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getCleanRecords"}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getPathData","params":{"start_pos":0}}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getMapData","params":{}},{"method":"getPathData","params":{"start_pos":0}}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/shadows?thingNames=<REDACTED>
Method: GET
Headers:
isEdgeRequest: false

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getPathData","params":{"start_pos":0}}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/rules?ruleType=schedule&startIndex=0
Method: GET
Headers:

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getPathData","params":{"start_pos":0}}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getPathData","params":{"start_pos":0}}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getMapData","params":{}},{"method":"getPathData","params":{"start_pos":0}}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/firmwares/latest
Method: GET
Headers:

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getDeviceTime"}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/shadows?thingNames=<REDACTED>
Method: GET
Headers:
isEdgeRequest: false

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/settings
Method: GET
Headers:

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/details
Method: GET
Headers:

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getCarpetClean"},{"method":"getChildLockInfo"},{"method":"getDoNotDisturb"},{"method":"getCleanAttr","params":{"type":"pose"}},{"method":"getConsumablesInfo"},{"method":"getDustCollectionInfo"}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getMapInfo"}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/rt-info
Method: GET
Headers:

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getMapData","params":{"map_id":0}},{"method":"getPathData","params":{"start_pos":0}}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getPathData","params":{"start_pos":0}}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/shadows?thingNames=<REDACTED>
Method: GET
Headers:
isEdgeRequest: false

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getMapInfo"}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/details
Method: GET
Headers:

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/shadows?thingNames=<REDACTED>
Method: GET
Headers:
isEdgeRequest: false

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/settings
Method: GET
Headers:

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/voice-packages?deviceModel=RV30%20Max%20Plus
Method: GET
Headers:

logReq err: TypeError: not a function
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getMapData","params":{"map_id":0}},{"method":"getPathData","params":{"start_pos":0}}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/services-sync
Method: POST
Headers:
isEdgeRequest: false

Body:
{"inputParams":{"requestData":{"method":"multipleRequest","params":{"requests":[{"method":"getCleanAttr","params":{"type":"running"}},{"method":"getAreaUnit"},{"method":"getCleanRecords"},{"method":"getDoNotDisturb"},{"method":"getConsumablesInfo"},{"method":"getMopState"},{"method":"getDeviceTime"},{"method":"getCurrentVoiceLanguage"},{"method":"getCustomizationRulesInfo"}]}}},"serviceId":"passthrough"}
------------------------
---- OKHTTP REQUEST ----
URL: https://aps1-app-server.iot.i.tplinkcloud.com/v1/things/<REDACTED>/rt-info
Method: GET
Headers

@ZeliardM

Copy link
Copy Markdown
Collaborator Author

@danieyal That's great, I do see something in your logs about nbu, that may help point me where I need to go.

Yea, Frida is a pain. I've used it in the past but lost my devices to do so, so it's been rough lately.

I appreciate your work so far. I am going through the encryption pieces again. Yea, the pake_register phase is essentially handshake1 and I still need to see what is going on.

@ZeliardM

Copy link
Copy Markdown
Collaborator Author

@danieyal Ok, I've spent most of the day going over things, and I think I'm at an impasse. I can't get the URLs to communicate with the cloud for the NOC certificates. I've gotten close, but what happens is that the APK uses a call, gets a cloud token, then pulls the URLs from the cloud. I can get the cloud token, but the I cannot get the signature matches for the calls out that have 'signature-required' set to true. I need to see if there is a way to reverse engineer these signatures so we can get them to match from my code based on how everything is supposed to be. I have some scripts and code that I've got for testing if you would be able to take it along with some of the Frida work you've done and possibly see if you can get the URL? I can send them via Discord if that works?

@danieyal

Copy link
Copy Markdown

@ZeliardM yeah sure, I will try but since I am working during the day, I might not have enough time to dedicate to it, but I'll try. Discord works for me.

@ZeliardM

Copy link
Copy Markdown
Collaborator Author

@danieyal All good, give it a shot and let me know and we will go from there. Thanks!

@ZeliardM

Copy link
Copy Markdown
Collaborator Author

@danieyal What I have been able to pull apart today is that the checkpassword call that is used in the frida logs you posted earlier, this has the same signature requirements that I am looking for. If you can get me some wireshark pulls of this actual communication so I can see the actual headers and packet information, then I might be able to reverse engineer this from there. If you want, we can keep going on Discord, my username is the same there.

@ZeliardM

Copy link
Copy Markdown
Collaborator Author

@danieyal nevermind, I finally got the matching signature and figured out how to get the right url for getting the certificates!

@ZeliardM

ZeliardM commented Oct 24, 2025 •

Copy link
Copy Markdown
Collaborator Author

Ok, now that I have all of this, my plan is as follows:

  1. Add certificate handling and a certificate store to the python-kasa API and CLI. This will be both as callable functions and storage. python-kasa itself is stateless and does not hold information, so these will be configured to store data in predefined areas based on Operating Systems.

  2. Update the TPAP Transport to handle NOC Authentication with SPAKE2+ and DAC Authentication. This will use the NOC data from the new modules to handle everything hopefully seamlessly, at least that's how I'm going to try to get it configured.

It's going to take quite some time for me to get through all of this, I want to put it together cleanly.

I'm headed out of town with my family for the weekend so I may not work on it much right now, but will keep everyone updated as I keep working on it.

@ZeliardM

Copy link
Copy Markdown
Collaborator Author

I am still slowly working on this. I have the NOC capabilities set up, now I'm working on testing the code. Having issues with the CSR formatting so I'll keep plugging away at it and keep you in the loop.

@rhanekom

Copy link
Copy Markdown

Ok, a couple more credential handling things as well were just pushed. Please test the changes and let me know how things work everyone!

@ZeliardM credentials recalled on reboots, and P110 plugs working perfectly. Thanks!

@Pacha-Icefree

Copy link
Copy Markdown

RV30 Max validation update for PR #1592

Tested against the current feature/tpap branch:

e708447 Retry TPAP sessions on stat access errors

Device under test:

Tapo RV30 Max / SMART.TAPOROBOVAC / TPAP over HTTPS

Static checks

Check Result
Clean checkout at e708447 OK
Local RV30 test scripts: bash -n OK
Local RV30 test scripts: shellcheck OK
Destructive command scan in local RV30 scripts OK
Relevant feature IDs present in code OK
Key TPAP / clean / mop modules compile OK
pytest tests/smart/modules/test_clean.py tests/smart/modules/test_mop.py OK, 44 passed

Relevant feature IDs confirmed in the PR code:

vacuum_status
vacuum_error
vacuum_fan_speed
vacuum_start
vacuum_pause
vacuum_return_home
clean_count
carpet_boost
mop_attached
mop_waterlevel

Runtime results on RV30 Max

Area Result Notes
Read-only state/features OK state, feature, individual feature reads all succeeded
Retry/session reads OK 3 delayed vacuum_status reads succeeded after 120s waits
Discovery list without credentials OK Device detected as RV30 Max / SMART.TAPOROBOVAC / TPAP / HTTPS / login version 2
Discovery list with credentials OK Authenticated discovery succeeded
Discovery detail with credentials OK Full detail completed and found 1 device
Cleaning records OK vacuum records list succeeded
Reversible writes OK Settings changed and restored successfully
Motion commands Partial Robot executes the commands, but CLI returns an error after execution
Matter module Partial get_matter_setup_info returns UNKNOWN_METHOD_ERROR; TPAP operation is unaffected

Reversible write details

Feature Result Roundtrip
vacuum_fan_speed OK Turbo -> Standard -> Turbo
mop_waterlevel OK Medium -> Disable -> Medium
clean_count OK 2 -> 1 -> 2
carpet_boost OK True -> False -> True

Motion command details

The robot accepted and executed the motion commands, confirmed by status reads:

Action Status after action Result
Initial state 6 OK
vacuum_start 1 OK at device level
vacuum_pause 7 OK at device level
Resume via vacuum_start 1 OK at device level
vacuum_return_home 4, later 5 OK at device level

However, each action command returned this CLI error after the robot had already accepted the command:

Raised error: expected string or bytes-like object, got 'dict'

This looks like a CLI formatting issue for action responses, not a TPAP transport/device-control failure. The likely code path is kasa/cli/feature.py, where action features call echo(response) and the response is a dict.

Conclusion

For RV30 Max, the current PR works for the practical TPAP paths:

  • Discovery
  • Authenticated communication
  • State and feature reads
  • Cleaning records
  • Delayed/repeated reads for retry/session behavior
  • Reversible setting writes
  • Start, pause, resume and return-home at the device level

The only issue found is the CLI action-response formatting error above. I would not consider that a blocker for the TPAP implementation itself; it can be handled as a small CLI follow-up fix.

@regan-young

regan-young commented Aug 1, 2026 •

Copy link
Copy Markdown

Tested and seems to work, ty - AI comment below:

L900-5(EU) with dac: 0 now authenticates on e7084472 — read and write both working

Following up on @awood-ops' report of an L900-5(EU) failing pake_share with -100000, and @ZeliardM's request for dac: 0 retests after the 9 Jul commits: it works now.

Tested against PR head e7084472972f08f2e2235b342f3964aedbfaeb3b in a clean python:3.12-slim container.

Device

model:    L900-5(EU)
firmware: 1.4.3 Build 260402 Rel.010106
discover: {"tpap_preferred": true,
           "tpap": {"tls": 0, "dac": 0, "noc": 0, "pake": [0], "port": 80},
           "mgt_encrypt_schm": {"is_support_https": false, "http_port": 80,
                                "encrypt_type": "TPAP", "lv": 2}}

Result — forcing DeviceEncryptionType.Tpap via DeviceConfig, the handshake completes and a full component_nego / get_device_info / get_device_usage multi-request returns cleanly:

>>> TPAP SUCCESS
    alias : Tapo L900
    model : L900
    fw    : 1.4.3 Build 260402 Rel.010106
    is_on : True

Write path verified too, via Light.set_brightness() — set to 30, read back 30, restored to 100. All features present: brightness, hsv, color_temperature, light_effect, light_preset, smooth_transition_on/off, overheated, rssi.

One difference worth flagging. @awood-ops' failing L900-5 reported "pake": [2]; mine reports "pake": [0]. Same model, same dac: 0/noc: 0/tls: 0. So either the 9 Jul changes fixed the non-DAC pake_share path outright, or pake: [0] and pake: [2] take different routes and only the former is fixed. Worth a retest on a pake: [2] unit before assuming the whole dac: 0 class is covered.

Rest of the fleet on the same branch, all authenticating:

Model Firmware Transport Result
L900-5(EU) 1.4.3 Build 260402 TPAP OK
P110M(AU) ×2 1.4.3 Build 260526 TPAP OK
L535E(EU) 1.4.2 Build 260203 TPAP OK
L430C(EU) 1.0.9 Build 250718 KLAP OK (no regression)

Also worth noting for anyone else with an L900 — with Third-Party Compatibility on, this strip advertises KLAP but its /app/handshake1 returns 500 Server Internal Error with an empty body, so it is completely unreachable on master. Turning the setting off puts it back on TPAP, where this branch handles it. That inverts the usual advice in these threads.

Happy to run further tests or capture full debug logs on request — I have the hardware and a reproducible setup.

@jimboca

jimboca commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Small FreeBSD/gmpy2 follow-up for this TPAP work: ecdsa can return mpz from Point.x()/y(), which breaks EllipticCurvePublicNumbers.

PR against feature/tpap: ZeliardM#7

Verified on KP125M(US) with encrypt_type=TPAP.

@peterfich

Copy link
Copy Markdown

P304M(UK) confirmed working on feature/tpap

model:    P304M(UK)
hardware: 1.0
firmware: 1.4.2 Build 260513 Rel.024250
discover: SMART.TAPOPLUG / TPAP / no-https / port 80

Tested against PR head via:

uv run kasa --username '...' --password '...' --host <ip> \
  --device-family SMART.TAPOPLUG --encrypt-type TPAP --no-https state

Result: authenticates and reads full state correctly — all 4 child sockets (relay state, voltage/current/power, energy monitoring, auto-off config, power protection threshold) all report as expected. No regressions observed.

@chris-eckinger

Copy link
Copy Markdown

P100 (non-M), hw 2.0, fw 1.4.6 (Jun 2026) — TPAP handshake reaches pake_share, fails with STAT_ACCESS_ERROR(-2203)

Reporting a device family and a newer firmware than the successful reports so far (those were P125M / P110M on 1.4.2 Build 251204, and RV30). This is a plain P100, no Matter support.

Environment

  • Device: Tapo P100, hardware version 2.0, firmware 1.4.6 Build 260617 Rel.064743
  • Branch: feature/tpap (installed via pip install git+https://github.com/ZeliardM/python-kasa.git@feature/tpap), Python 3.13 on Windows, clean venv
  • Cleartext credentials (account owner, not a shared device), lowercase email

Discovery reports (from the stock integration's error): EncryptionScheme(is_support_https=False, encrypt_type='TPAP', http_port=80, lv=2)

What I found on connection parameters

  • --https --port 4433 → ConnectionRefused (nothing listening on 4433 on this device)
  • --https --port 80 → SSL: WRONG_VERSION_NUMBER (device speaks plain HTTP on 80)
  • --port 80 without --https → gets furthest; this is the working transport combination for this device

discover config with credentials does not find a working path on its own; all ten attempted combinations fail. Only explicitly forcing -df SMART.TAPOPLUG -e TPAP --port 80 (no --https) gets the handshake started.

Failure

kasa --host <ip> -df SMART.TAPOPLUG -e TPAP --port 80 --timeout 20 --username "<email>" --password "<pw>" state --debug

kasa.exceptions._RetryableError: TPAP pake_share failed for <ip>: STAT_ACCESS_ERROR(-2203) (error_code=STAT_ACCESS_ERROR)
  File "kasa/transports/tpaptransport.py", line 338, in _perform_auth_handshake
    share_result = await self._login(share_params, step_name="pake_share")

So pake_register succeeds and it fails one step later at pake_share.

Note on -2203: earlier in this thread -2203 was associated with using a stored credentials_hash instead of cleartext. That's not the case here — cleartext account credentials are supplied and the account owns the device. I also ruled out a stale cloud association: logged out and back in in the Tapo app, power-cycled the plug, waited for it to come back online, retried — identical error code.

Possible firmware regression? Note this firmware (Build 260617, June 2026) is roughly six months newer than the builds in the working reports above (1.4.2 Build 251204). If TP-Link tightened the PAKE/NOC path in the meantime, -2203 here may not be a P100-specific issue but a newer-firmware one — worth checking whether anyone with a working P125M has since auto-updated past 1.4.x and lost it again.

Happy to run any additional diagnostics or patched builds against this P100 if that's useful — I have the device available for testing.

@heythisisnate

Copy link
Copy Markdown

I just came across this while troubleshooting some of my Tapo switches that auto-updated firmware and now won't connect to HA. How close are we to getting this merged? Do we still need help testing?

Lmk what I can do to help this along.

@nopoz

nopoz commented Aug 24, 2026 •

Copy link
Copy Markdown

Got this branch authenticating against a real device. Tested e708447 against a Tapo L920-10 LED strip (hw 1.0, fw 1.4.4 Build 260408): connect, handshake and update all work, and it's been running in Home Assistant with the branch patched over 0.10.2.

The reason this one works when the RV30 doesn't is in discovery:

{"tpap":{"tls":0,"dac":0,"noc":0,"pake":[2],"port":80}}

No TLS, no attestation, plain HTTP on 80. The SPAKE2+ code here is already correct for devices that don't ask for DAC/NOC, which makes that a separate axis rather than a blocker.

So the main suggestion: land TPAP for the non-attestation case now, and keep the NOC signature work on its own track. That covers at least the bulb and strip family, and those devices need nothing that isn't already in this branch.

Three specific changes I'd make:

1. Move -2203 out of SMART_RETRYABLE_ERRORS and treat it as an authentication error. It's a pake_register credential rejection carrying a lockout counter:

{"error_info":{"lockedMinute":0,"failedAttempts":1,"remainAttempts":14},"error_code":-2203}

The device allows around 15 failures before locking. SmartProtocol._query retries _RetryableError three times with a fresh handshake each, so a wrong password burns four attempts per poll and locks the device out within two cycles. At that point it presents as a hang rather than an auth failure.

2. Add -2402 to SmartErrorCode as a no-session error. The device returns it for any non-login method issued before the handshake completes, and it currently renders as INTERNAL_UNKNOWN_ERROR(-100000). Related: -2402 is what the RV30 was returning during pake_register earlier in this thread. On this device -2402 and -2203 are clearly distinct conditions, with -2402 meaning the request never reached an authenticated context, so that may be worth re-reading.

3. Treat HTTP 401 on the ds endpoint as a retryable live-session error in send(). After a device reboot, a request to /stok=<old>/ds returns 401 with no body. That's a status rather than an error code, so it never reaches _should_retry_live_session() and becomes a generic KasaException. It does recover, but only because _query's final handler resets the transport before re-raising:

query 1 after session loss: FAILED, status 401
query 2: OK
query 3: OK

So one poll always drops.

Minor, while I was in there: sslaestransport.py's _md5_hash uppercases and tpaptransport.py doesn't. Lowercase is what this device wants, but the two disagreeing looks accidental.

One debugging note that may save time on the devices still failing. pake_register is a username oracle: it only returns the salt and device share, and the password isn't proven until pake_share, so it succeeds on a valid username with no password at all.

$ ./probe.py register admin
username='admin' -> 21232f297a57a5a743894a0e4a801fc3
{"result":{"sub_method":"pake_register","extra_crypt":{"params":{"passwd_id":2},
 "type":"password_shadow"},...,"cipher_suites":1,"iterations":3000},"error_code":0}

One request tells you whether a username is right, independently of credentials, which is how I confirmed admin here. discover needs no credentials and doesn't increment the failure counter either, so between the two you can triage a device without having working credentials for it.

One limit on that: discover is not exempt from the lockout. Once the ~15 failed logins are gone the device answers it with ACCOUNT_ERROR(-2101) too until lockedMinute expires, so it won't get you into a device you have already locked out.

A related trap: this repo's pytest --ip harness reconnects for every parametrisation, so a test parametrised across six fixtures fires 42 full handshakes in under a minute and trips that lockout, after which every test errors at setup with -2101. I haven't pinned down whether each connect is genuinely failing auth or the device throttles rapid handshakes and counts them as failures. Either way it is worth knowing before pointing --ip at a TPAP device.

I have the device on hand and a local harness against it, so send patches or trace requests my way. I can also generate a fixture for the test suite.

@kartikay-bagla

Copy link
Copy Markdown

Tested this locally against a real device today (L530B(EU) bulb, firmware 1.4.4).

Test setup:

  • Checked out this branch, uv sync --all-extras (pulled in ecdsa and passlib cleanly)
  • tests/transports/test_tpaptransport.py: 116/116 passed
  • kasa discover correctly identified the bulb as Encrypt Type: TPAP
  • kasa --host -e tpap state — full auth handshake + state read succeeded using TP-Link cloud credentials
  • kasa --host -e tpap brightness 50 — write command succeeded and persisted (verified via a follow-up state read), then reverted back

Also tested live in Home Assistant (Docker, linuxserver/homeassistant, Python 3.14): force-reinstalled python-kasa from this branch into the running container (pip install git+https://github.com/ZeliardM/python-kasa.git@feature/tpap — note a plain pip install won't actually replace the package since this branch reports the same version, 0.10.2, as what's already installed; needed --force-reinstall). Restarted HA, the tplink integration loaded with no errors, and the bulb entity worked normally through the dashboard.

Thanks for raising the PR!

If I can be of any further help, do let me know. I have a L900 lightstrip that is still using the old KLAP encryption but has a firmware update waiting for it. I haven't updated it yet, but can update to test if needed.

@tieguy

tieguy commented Aug 28, 2026 •

Copy link
Copy Markdown

I am running this live against two KP125M smart power switches and it seems to work. One of the switches, is on old firmware, which doesn’t require TPAP, and a second is on new firmware which does. Code so far seems to work fine on both of them. Feel free to ask if you need more details.

@heythisisnate

Copy link
Copy Markdown

I finally got a chance to test this. I re-installed python-kasa from this branch into my live HA. I have about 15 TP-link switches and smart plugs on various firmware versions that have slowly been updating on their own. Happy to report that they're all authenticating correctly after the update! Below are my device models and firmware versions confirmed working with this branch.

# Model Firmware
1 S505D 1.4.0 Build 260611 Rel.011905
2 S505D 1.4.0 Build 260611 Rel.011905
3 S505D 1.2.3 Build 240821 Rel.094036
4 S505D 1.4.0 Build 260611 Rel.011905
5 S505D 1.4.0 Build 260611 Rel.011905
6 TP25 1.1.3 Build 240604 Rel.150844
7 S505 1.4.1 Build 260713 Rel.041836
8 P400M 1.4.0 Build 260731 Rel.213056
9 TP25 1.1.3 Build 240604 Rel.150844
10 TP25 1.1.3 Build 240604 Rel.150844
11 P400M 1.4.0 Build 260731 Rel.213056
12 P400M 1.4.0 Build 260731 Rel.213056
13 S505D 1.2.3 Build 240821 Rel.094036
14 S505D 1.2.3 Build 240821 Rel.094036
15 S505D 1.2.3 Build 240821 Rel.094036
16 S505 1.4.1 Build 260713 Rel.041836
17 S505 1.4.1 Build 260713 Rel.041836

@nopoz

nopoz commented Aug 31, 2026

Copy link
Copy Markdown

Following up with something about the Third-Party Compatibility workaround rather than the crypto on this branch, since it is being recommended here and in #1590.

That toggle can cost the user their stored credentials. The L920 I tested against reverted from TPAP to KLAP, and discovery went from the profile in my earlier comment to:

{"result":{"sub_method":"discover","tpap_preferred":false},"error_code":0}

Firmware was byte identical to when TPAP worked (1.4.4 Build 260408 Rel.054818) and the alias, region and timezone all survived, so this was not an update or a reset.

Discovery handles the change correctly and rewrites the encryption type. credentials_hash does not survive it: this branch writes the plaintext un / pwd form, klaptransport does a bare b64decode of whatever it is handed, and the result fails handshake1 as a wrong password. Home Assistant acts on that by deleting the stored hash, which on a device with no separate credential store is the only copy of the credentials.

I have opened #1749 against master with the details and a fix. It is reachable without TPAP on any aes or klap change so it stands alone there, but it covers this branch's hash format too, so a TPAP device that reverts to KLAP reconnects without a reauth.

Two things from diagnosing it that are worth knowing here:

The failure does not name TPAP. The warning comes from klaptransport and reads as a bad password, so grepping for a TPAP fault finds nothing. The TPAP side symptom is separate and never reaches the UI: TPAP discover response missing tpap object logged by the coordinator. I had 73,417 of those before a restart surfaced anything, and the restart then looked like the cause when it was only the first thing to run the reauth path.

_discover() raises a bare KasaException when the tpap object is missing. Separating tpap_preferred: false, which means the device has deliberately left TPAP and the stored config is stale, from a genuinely malformed response would make this much easier to diagnose. Right now they are the same error.

nopoz added a commit to nopoz/python-kasa that referenced this pull request Aug 31, 2026
sslaestransport and the tpap transport on python-kasa#1592 store a credentials_hash
that is base64 json of the plaintext credentials, so the credentials can be
read back out of it. klap and aes hashes are one way and cannot.

When a device changes its encryption type the stored hash is the one the
old transport wrote. The previous commit stops that being mistaken for a
bad password, but the connection still has no credentials to offer and the
caller has to prompt for them again. Home Assistant stores the hash as the
only copy of the credentials for a device, so that prompt loses them.

Read the credentials back out when the hash is one of the plaintext forms
and let the transport derive its own. A tpap device that reverts to klap
because Third-Party Compatibility was turned on then reconnects without a
reauth. This only works in that direction.
@RayWangyangMa

Copy link
Copy Markdown

Adding a KP125M(US) data point, since this thread has KP125M reported working but no firmware detail or fixture behind it.

KP125M(US) hw 1.0, fw 1.4.1 Build 260721 — discovery:

"tpap_preferred": true,
"tpap": { "tls": 0, "dac": 1, "noc": 1, "pake": [2], "port": 80 },
"mgt_encrypt_schm": { "is_support_https": false, "http_port": 80, "encrypt_type": "TPAP", "lv": 2 }

Note dac: 1, noc: 1. @nopoz called DAC/NOC a separate axis from the L920 (dac: 0, noc: 0) rather than a blocker — this is a data point on the other side of that axis, and this branch handles it unmodified: discovery resolves to SmartProtocol + TpapTransport + SmartDevice + http, handshake over plain HTTP :80, full update() (device info, usage, energy monitoring, emeter batches), turn_on()/turn_off() round-trip, and repeated sessions/reconnects all work. KLAP is fully off on this firmware — handshake1 returns 403 — so this branch is the only local-control path left for it.

I opened ZeliardM#8 against feature/tpap with the dumped fixture (KP125M(US)_1.0_1.4.1.json; upstream fixtures for this model stop at 1.2.3, pre-TPAP) plus one small transport fix the fixture surfaced: TpapTransport did not fall back to blank Credentials() when neither credentials nor a hash were supplied, so the credential-less CLI tests raised before the transport was exercised. KlapTransport already does this; the four-line change mirrors it and leaves real-device behavior unchanged.

The plug is permanently on my LAN, so happy to run anything targeted against this firmware.

@Austin519

Copy link
Copy Markdown

Confirming this branch works on KP125M(US) — the port-80 / plaintext-HTTP
SMART.KASAPLUG TPAP variant, which I hadn't seen validated in the thread yet.

Device / firmware

  • KP125M(US), firmware 1.2.5 Build 241213 Rel.172504
  • Discovery advertises: mgt_encrypt_schm = {encrypt_type: "TPAP", is_support_https: false, http_port: 80, lv: 2}, owner set (cloud-account-bound), obd_src: "tplink". So this is the cloud-bound / SPAKE2+-with-account-credentials path over plaintext HTTP:80, not the HTTPS:4433 vacuum variant.

Result — all working against 4 physical plugs (2× dishwasher, 2× air purifier):

  • Discovery → TpapTransport selected via the non-HTTPS SMART.TPAP key (port 80).
  • Discover.discover_single(host, credentials=Credentials(<tp-link account>, <pw>)) + update() succeeds.
  • State/control: is_on, alias, model all correct.
  • Energy monitoring works over TPAP: live current_consumption (e.g. 55.2 W on an active air purifier, 0.0 W on idle), plus voltage (~121 V). (Worth noting since KP125M energy is not exposed over Matter — the local TPAP path restores it.)

Environment

  • Home Assistant 2026.9.1, cryptography==48.0.1 (this branch's TpapTransport runs fine on 48 — no newer-cryptography requirement in practice). Only genuinely new deps pulled were ecdsa and passlib, both pure-Python.

Migration note for existing entries (KLAP → TPAP after a firmware bump):
Devices that were previously KLAP kept a stale connection_parameters.encryption_type = KLAP + a KLAP credentials_hash in their config entry, so HA kept trying KLAP → 403 to handshake1. Flipping the stored encryption_type to TPAP and clearing the stale credentials_hash/aes_keys, then completing the reauth flow (which supplies the TP-Link account credentials SPAKE2+ needs, since the entry only held a KLAP hash), got all four onto TPAP cleanly. Might be worth a migration path so a firmware-triggered scheme change reauths automatically rather than sitting in setup_retry.

Happy to capture packet traces or test specific commands on this hardware if useful.

@Austin519

Copy link
Copy Markdown

Following up on my KP125M(US) confirmation above with a security note, since I had the
hardware to check it: this transport reuses the AEAD (key, nonce) pair between each
request and its response.

Captured 7 consecutive request/response transactions from a real KP125M(US)
(fw 1.2.5 Build 241213) on the port-80 path, instrumenting TpapEncryptionSession:
every response carried response_seq == request_seq, under a single session key and
single base_nonce. Since nonce = base_nonce[:-4] + struct.pack(">I", seq) and the
seq is identical for the request and its response, the outbound request ciphertext and
the inbound response ciphertext are produced under the identical (key, nonce).

For an AEAD (AES-CCM or ChaCha20-Poly1305), nonce reuse under one key with two different
plaintexts is a catastrophic break: C1 ⊕ C2 = P1 ⊕ P2 (keystream cancels), and the
plaintexts here are low-entropy JSON — and on the is_support_https:false / port-80
variant it's all observable on the wire with no TLS. For ChaCha20-Poly1305 it also
enables Poly1305 one-time-key recovery → tag forgery.

This looks protocol-level (the device dictates the seq echo; decrypt() already
just logs and proceeds when response_seq != request_seq), so it isn't something the
client can fix unilaterally without breaking interop with real firmware. Flagging so it
can be documented in the PR rather than merged as a fully-solved encryption path, and so
someone with a TP-Link security contact can consider reporting it.

Capture method (reproducible by anyone with a device): patch TpapEncryptionSession.decrypt
to log (request_seq, response_seq) and run one update(); or tcpdump -A on the LAN
since it's plaintext HTTP. Happy to share the exact script.

@freeKC

freeKC commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Hi, this may help with the part where /stok=.../ds keeps returning errors.

I got the SPAKE2+ login and the /ds channel fully working against a Tapo C510W camera (fw 1.3.4, the plain userpw path, no NOC involved on that model). Write-up, small Python client and offline tests are here: https://github.com/freeKC/tapo-v4-protocol

The things that blocked me for a long time, in case it's the same for you:

  • Over HTTP the body is only uint32_be(seq) + ciphertext + tag. The 24 byte TSLP header with the CRC (class dm1/a in the app) is for the TCP transport. The HTTP path is hm1/a0: it does getAndIncrement() on the seq and writes a 4 byte big endian prefix, that's all. If you send the TSLP header over HTTP, the device reads 0x01020200 as the sequence number and answers -40401 no matter what follows.
  • The first seq is start_seq itself, not start_seq + 1.
  • On cameras the plaintext has to be a multipleRequest envelope, a bare method is refused with a plaintext -40209.
  • Any refused /ds request kills the session on the device side, so when you try variants you need a fresh login for each one. That one made my test results meaningless for a while.

Session key and nonce derivation, the full field list of pake_register / pake_share and the app's other variants (hashed usernames, the encryption list, stok reuse, the DAC/NOC branches) are in sections 4 and 5 of PROTOCOL.md. I can't test the RV30 -2402 case since I don't own one, but the data channel part should be the same code in the app.

trafcombg pushed a commit to trafcombg/ham-radio-remote that referenced this pull request Sep 20, 2026
…sa@feature/tpap)

Upstream python-kasa doesn't support TPAP yet (the encryption scheme our P100 actually uses) — see python-kasa/python-kasa#1590 (open since 2025-10) and the fix PR python-kasa/python-kasa#1592 (not yet merged). Switch back to the plain PyPI package once that PR ships in a release.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Austin519

Copy link
Copy Markdown

Tested this branch against a KP125M (US, fw 1.4.1) that advertises encrypt_type=TPAP in the UDP broadcast: the TPAP transport reaches the device but aborts at discovery with TPAP discover response missing tpap object. Its direct discover returns:

{"result":{"sub_method":"discover","tpap_preferred":false},"error_code":0}

So it advertises TPAP capability but prefers KLAP — and it connects and reads energy fine over plain KLAP (login_version 2, http port 80, https false). Could _discover() fall back to KLAP when tpap_preferred is false / the tpap object is absent, rather than raising KasaException("TPAP discover response missing tpap object")? That would cover these "TPAP-advertised but KLAP-preferring" KP125M units (see #1733). Happy to test any change against the device.

@tobixen

tobixen commented Sep 23, 2026 •

Copy link
Copy Markdown

Disclaimer: AI-generated text below the line, generated by Claude Opus 5.5 (Claude Code), from the prompts:

Please see if it's possible to get connected to the vacuum. Do not start any cleaning operations now.

The real value I provide here is the hardware to reproduce and test on. It's late night here, I haven't even bothered reading through the text below - perhaps I'll do more research tomorrow and edit this comment.


RV50 Pro Omni(EU): in-band discover says tls: 0, transport downgrades to http:// on the TLS-only port 4433

Tested e708447 against a Tapo RV50 Pro Omni(EU), hw 1.0, fw 1.2.8 Build 260811 Rel.030349.

UDP discovery advertises TLS:

"tpap": {"tls": 2, "dac": 1, "noc": 1, "pake": [2], "port": 4433},
"mgt_encrypt_schm": {"is_support_https": true, "http_port": 4433, "encrypt_type": "TPAP", "lv": 2}

But the in-band login/discover, sent over HTTPS to that same port, answers tls: 0:

$ curl -sk -X POST -H 'Content-Type: application/json' \
    -d '{"method":"login","params":{"sub_method":"discover"}}' https://<ip>:4433/
{"result":{"sub_method":"discover","tpap_preferred":true,"mac":"...","tpap":{"tls":0,"dac":1,"noc":1,"pake":[2],"port":4433}},"error_code":0}

TpapEncryptionSession._discover() stores that 0, _update_transport_url() rebuilds the app URL as http://<ip>:4433, and the device drops the plaintext connection:

DEBUG:kasa.transports.tpaptransport:TPAP: starting handshake with <ip>
DEBUG:kasa.httpclient:Posting to https://<ip>:4433/
DEBUG:kasa.httpclient:Posting to http://<ip>:4433/
DEBUG:kasa.httpclient:Device <ip> received an os error, enabling sequential request delay: Server disconnected
...
Raised error: ('Device connection error: <ip>: Server disconnected', ServerDisconnectedError('Server disconnected'))

Same result with and without discovery (--port 4433 --https -e tpap -df SMART.TAPOROBOVAC).

My reading is that this firmware answers tls in-band as "additional TLS needed on top of this channel", which is 0 because the request already arrived over TLS. The RV30 reports in this thread presumably get 2 back in-band, which is why they are not affected.

Workaround, verified: don't downgrade a session that is already on HTTPS. In _discover(), right after self._tpap_tls is parsed:

if self._tpap_tls == 0 and self._transport._app_url.scheme == "https":
    self._tpap_tls = 2

With that, the full handshake (DAC proof included) and kasa state work: component_nego, device info, battery, consumables, clean records, and so on. The device certificate chain validates against the embedded TPAP_ROOT_CA_PEM (checked with openssl s_client -CAfile), so forcing 2 here keeps certificate verification on rather than falling back to CERT_NONE.

A cleaner fix might be to never let the in-band value downgrade the scheme or port that UDP discovery or the config established. I'll leave that call to you.

Two smaller observations on this model, read-only, nothing written:

  • getAutoDustCollection and get_matter_setup_info return UNKNOWN_METHOD_ERROR.
  • component_nego lists goto_point and direction_control (both ver_code 1). These cover the app's "go to spot" and manual-drive features, which no integration implements yet.

Edit (2026-09-24): Proposed as a PR against feature/tpap, with a unit test: ZeliardM#10. A correction to the above: with tls forced to 2, the DAC proof is not part of the handshake, because _use_dac_certification() requires tls == 0. TLS verified against the TP-Link root CA takes its place.

@freeKC

freeKC commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

Camera data point, since none had been posted yet: with three small changes this branch logs in and runs a full update() on a Tapo C510W 2.0 (fw 1.3.4), the kind of camera behind the -40211 reports on the Home Assistant side. The changes are in ZeliardM#11:

  • the camera refuses the in-band discover with -40209, so the session falls back to what the UDP discovery advertises (tpap: {pake: [2], tls: 1, port: 443});
  • the https default port is 443 for cameras, not 4433;
  • discovery still says AES for these cameras, so _connect switches to TPAP when the AES login answers -40211.

Handshake takes about 5 s, then detection settings, LED, pan/tilt and RSSI all read fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.