TP-Link Tapo P100

Interesting, where are you running DIBR, on your router?
Wireshark live or pcap playback?

Stuart

Actually was running DIBR from a Kali VM.
Wireshark findings were live.
Any more suggestions on new approaches?

Hi guys,

Also just bought one of these because it was cheap and smaller than some of the alternatives. It’s on a fairly non-critical item of my home (old TV in the basement). My temporarily (possibly permanent) hack is to use hook the thing up to Google Assistant, then use Assistant Relay to send a a message like “Turn on basement TV Plug” and “Turn off basement TV Plug”. This seems to work most o the time, provided you don’t try and toggle things too quickly. The issue of course is that I can’t see current state. :frowning: Ah well, it sort of works. Thought you guys might be interested for a temporary hack.

Wireshark can interpret Bluetooth captures, so my next avenue is to pair a new device, logging the Bluetooth packets for analysis using wireshark.

Stuart

@hsiboy Let us now how it went then :slight_smile:

I have posted some information about accessing the Tapo C200 camera on the local network, in another topic. You might want to have a look and try it with the Tapo P100.

@hsiboy, @pnmoliveira, @tachikoma1373, @tnmendes, @fishbigger, @lorenzor95, @iain

2 Likes

Ok, I’ve got a P100 plug as well and I can see, that the plug uses a completely different method for the communication. It uses RSA key pairs and AES encryption. @tnmendes’ posts above are really useful.

@hsiboy, I see that it does the initial pairing by Bluetooth. The cameras have an AP where the app joins, and passes the Wifi network’s name and password to the camera. Probably exactly the same happens when the app connects by Bluetooth to the plug as well.

Indeed the communication is different after, the plug uses pure HTTP, but encrypts all the messages.

As I can see, there are a few things what the app does. It does a UDP broadcast with an RSA public key and waits for replies from the devices on the network. I guess this is just a discovery function.

Then when the app knows the IP address of the device on the network, then it does a handshake.

Like this:

POST /app HTTP/1.1
Host: 192.168.X.XXX
Referer: http://192.168.X.XXX:80
Accept: application/json
Accept-Encoding: gzip
User-Agent: okhttp/3.12.2
Connection: Keep-Alive
requestByApp: true
Content-Type: application/json; charset=UTF-8
Host: 192.168.8.XXX
Cookie: TP_SESSIONID=4C22EA2D8FDDD4DA6EAAC3A916994898

{"method":"handshake","params":{"key":"-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCNIZEwdOB78kqRRLdh7WRnEa6ubxgYhYDXrb7vkQjGqY7DuzVVGXt05XjT8KB5SovHzXzhMbKLZK1vgNDBbAXtfxkhifYZ6sOw+1GwlZWTEOlme9bfTNyqJ9gJxBjzJ+IsLVKoNi65QM3c3Ft1yBqXULibC7GkNA7ZFa3i5TJiEwIDAQAB\n-----END PUBLIC KEY-----\n"},"requestTimeMils":0}

In return the plug replies:

{

    "error_code": 0,

    "result": {

        "key": "HpL/IhOrGg9VU7Fx2M3/3/asHSKEdeerUVWoA2nlfpbH7KnskW8j+Kwtgk6In5+b95wTYY0TFQQFX3M3dSZ8ncz+j1/IOI7EyTJ+u0k9QLo8eNcdgDDyq/cTeBuq+/tIQcrFpea1sS0I90NndkiXPhFigVGdYZQcb0GU43iz7JU="

    }

}

The TP_SESSIONID is used along the communication further on, the validity of the ID is 24 hours. In the next step the app post another messages to the plug, which I believe confirms the handshake, and the plug replies with a token (of course all encrypted). I haven’t managed to mimic those yet, even I haven’t managed to decrypt the reply from the plug yet. But here are some hints from the decompiled app, what is really happening:

    public void mo35029c() {
        KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
        instance.initialize(1024, new SecureRandom());
        KeyPair generateKeyPair = instance.generateKeyPair();
        String str = new String(Base64.encode(((RSAPublicKey) generateKeyPair.getPublic()).getEncoded(), 0));
        String str2 = new String(Base64.encode(((RSAPrivateKey) generateKeyPair.getPrivate()).getEncoded(), 0));
        this.f20965b.put(0, str);
        this.f20965b.put(1, str2);
    }

    /* renamed from: d */
    public String mo35030d() {
        if (TextUtils.isEmpty(this.f20965b.get(0))) {
            mo35029c();
        }
        return "-----BEGIN PUBLIC KEY-----\n" + this.f20965b.get(0) + "-----END PUBLIC KEY-----\n";
    }

    /* renamed from: a */
    public void mo35024a(String str) {
        byte[] decode = Base64.decode(str.getBytes("UTF-8"), 0);
        byte[] decode2 = Base64.decode(this.f20965b.get(1), 0);
        Cipher instance = Cipher.getInstance("RSA/NONE/PKCS1Padding");
        instance.init(2, (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decode2)));
        byte[] doFinal = instance.doFinal(decode);
        byte[] bArr = new byte[16];
        byte[] bArr2 = new byte[16];
        System.arraycopy(doFinal, 0, bArr, 0, 16);
        System.arraycopy(doFinal, 16, bArr2, 0, 16);
        String encodeToString = Base64.encodeToString(bArr, 0);
        String encodeToString2 = Base64.encodeToString(bArr2, 0);
        this.f20966c = new C6586a(bArr, bArr2);
        mo35025a(encodeToString, encodeToString2);
    }

This confirms that the RSA key is an 1024bit key, and the encryption method is RSA/NONE/PKCS1Padding. But unfortunately it is not that straight forward. There is a second part of the code which handles bArr and bArr2 and it turns out that they are the AES Secret Key and IV:

public C6586a(byte[] bArr, byte[] bArr2) {
        try {
            SecretKeySpec secretKeySpec = new SecretKeySpec(bArr, "AES");
            IvParameterSpec ivParameterSpec = new IvParameterSpec(bArr2);
            this.f21776a = Cipher.getInstance("AES/CBC/PKCS7Padding");
            this.f21776a.init(1, secretKeySpec, ivParameterSpec);
            this.f21777b = Cipher.getInstance("AES/CBC/PKCS7Padding");
            this.f21777b.init(2, secretKeySpec, ivParameterSpec);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (NoSuchPaddingException e2) {
            e2.printStackTrace();
        } catch (InvalidKeyException e3) {
            e3.printStackTrace();
        } catch (InvalidAlgorithmParameterException e4) {
            e4.printStackTrace();
        }
    }

I believe further on this AES encryption is used to transmit information between the app and device, but that is a hard guess. And I have no idea is it hard coded to the device or generated every time.

Anyhow, some pieces to start reverse engineering.

@JurajNyiri, @Dpavey are you fancy to crack this nut and make the integration for the Tapo plugs as well?

PS.: I used Postman to send the handshake request, and manually generated the RSA key. Initially I used HttpCanary on Android to capture some communication between the app and the plug. (and fortunately managed to catch the handshake, without CA Pinning, might be it is due to the pure HTTP use)

4 Likes

Nice research again @GSzabados!

Unfortunately I do not own tapo plugs, so I am not able to work on this. I stick with the well supported tplink hs110 with api available.

1 Like

Getting API reverse engineered is the best option but I’m using the following combination:

Pair Tapo with Google Assistant, Install assistant-relay (execute google home commands via api), hass.io making requests to assistant-relay.

1 Like

I’ve watched what tapo app is sending and after a while I’m able to generate valid TPIoTRequest which are sent to the p100. (p100 answers with error_code:0 so it’s valid)

Now I will try to decrypt the answer from the p100.

edit:
I’m able to decrypt answer from the p100! So when sending login_device request, p100 sends {"error_code":-1501} back… I can communicate with p100 but my username and password are not correct.

Bingo! Username & Password needed to be base64 encoded + username needs to be a SHA-digest.
P100 answers: {"error_code":0,"result":{"token":"887E98FF98EB90AB1......9D0881A98"}}

6 Likes

Sounds very promising! Great work. :ok_hand:

Do you think this opens the door for a functional integration?

I think I should be able to send on/off requests

1 Like

After that point the P100 communicates by using the token the way how it is in this post:

Thanks for picking up this work and carrying further!

3 Likes

More progress!, I’ve patched the android app with my pre-made key pair, so I can decrypt all information the app is sending/receiving to/from p100. (It’s easier to reproduce pure HTTP requests than searching through decompiled code for requests)

App sends this request to change the state of the plug:

{"method":"set_device_info","params":{"device_on":false},"requestTimeMils":1602840338865,"terminalUUID":"88-54-DE-AD-52-E1"}

I can now craft it dynamically, so I can change the plug state (turn on/off) with my own code!

Edit: Now, it’s time to document it and create repository with an PoC.

7 Likes

Great work to all of you guys!
Really incredible what you achieved so far!

1 Like

Definitely easier, great work!

1 Like

Ah, nice technique.

So the plug uses the key pair from the app? Interesting!

I’ll take a look at the Bluetooth packets tomorrow :+1:

1 Like

Yup, I’ve written a post about the connection here: https://k4czp3r.xyz/posts/reverse-engineering-tp-link-tapo/

6 Likes

Great write up!

So, last night i paired a new p100 and i had packet capture enabled, but it failed to capture any packets.
However, while i was looking for the cached firmware, i found what looks like the AES key.

/mnt/sdcard/Android/data/com.tplink.iot/files/model_cache/deco_model/deco_model

its a txt file with two key:value pairs.

{
"DECO_TPSERIALIZEUTILS_AES_KEY":"skf6gYCmI25E1mcE9WvDWG5wxdExTTAtOeNrFN5aui1SMEHK6Udx9XEgsaDL1ifJEPcDce9gHbSB\nnqfAeSdcTvt+JNP7KBxTdGgU64irT2i8pt+uBxhODNmN\/PX5g2PorsnjsUs6op8us8v4xFxWsSp2\ng4jQLdEgbEkluTz8\/I+htZrgUvaV8EUWVy6oz0nSdkG5b8DJutna\/iqJOO75P2AaqSCf8fnLZet6\ntK1zrSa3+GBifnK+J98gjXn6I6kxkPPUWbRA0JTZhLS+JvLEAa4W34+ZggdFo6soeK7Ga\/gGUy1M\nls2xMSihnt9UR7MO6+P3cLKxu6COZzLbO7bPVQ==\n",

"DECO_TPSERIALIZEUTILS_AES_VECTOR":"IDoSrxMmme+YrIwVJ0tv2tI+ZcCYOPMgprpqwPdgsCu\/ncM4WY6BXNN272KaAHkrAlu+nbNHqa51\nxufCO2DsJz6eQjkuq9IUYw9mzeHsgU4\/lsHJmFd2thfdppaHyIGrFlYA5O\/mfC2gMBMSpXKxXq4G\nyhaxKHN7jynghLrHO6i9d3TvoFXPNbHc0+Koo7COIBMHNn6EbNMSlzrSWi7TEoX7NBwPf\/5FZcnL\ntV+d1IrShHeACrmF2D6ufJkXvaPuA3SIZ7lecdzlTn9ixrBWbJ4cZY3V\/2f7d2vTfm9QxDxkorbQ\n0h8DQtQ\/ub59rdcvjEmRfE9tHZwpLUNHmYOBgQ==\n"
}
3 Likes

@K4CZP3R, will you make a custom integration?

2 Likes