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)