TP-Link TAPO reverse engineeren
Platte HTTP en een eigen encryptiemethode.
De TAPO-app communiceert via twee methoden: Bluetooth en HTTP. Bluetooth wordt gebruikt om verbinding te maken met niet-gekoppelde apparaten (uitwisselen van wifi-ssid & psk enzovoort). HTTP wordt gebruikt voor elk ander verzoek na het initiële koppelingsproces (status en instellingen van de plug ophalen/instellen, firmware updaten, enzovoort).
Mijn doel
Mijn plug aan/uit kunnen zetten met een HTTP-verzoek.
HTTP-verzoeken
De TAPO-app stuurt alle verzoeken naar http://<ip-of-the-tapo-device>/app.
Handshake
De app stuurt bij het opstarten twee verzoeken (sessie-initialisatie); het eerste is het handshake-verzoek.
POST http://192.168.137.203/app HTTP/1.1
{
"method": "handshake",
"params": {
"key": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCiHkY5laTugGN1Hf/sBHiiw6mnnkohmvVHHHGJqwRx59RjQaL/SPBoLpeNRgN3B/uykzYTLUVMpTcWSZHsS6FfhdoOkJ1B6nit6nheIfltbP99uJduP1JQ44S9dqUr73w++Lpl6TKrzK3KOc5z/vc9xmqiKK6PYbFZu2evCsL19wIDAQAB-----END PUBLIC KEY-----\n"
},
"requestTimeMils": 0
}De velden spreken voor zich: dit verzoek stuurt een door de app gegenereerde publieke sleutel naar het TAPO-apparaat.
TAPO-app-implementatie - handshake-generatie
Na het decompileren van de Android-TAPO-app kunnen we zien hoe deze publieke sleutel wordt gegenereerd:
public String getPublicKey() {
/* Not relevant code, omitted */
return "-----BEGIN PUBLIC KEY-----\n" + this.f20965b.get(0) + "-----END PUBLIC KEY-----\n";
}f20965b is een HashMap met de publieke en de private sleutel (i=0 is de publieke sleutel en i=1 is de private sleutel). Verder onderzoek bracht me bij de functie waar de sleutels werden gegenereerd:
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);
}Het is een RSA 1024 bit-sleutelpaar dat Base64-gecodeerd is. Nadat ik deze functie in java had (her)geïmplementeerd, kan ik mijn eigen (geldige) sleutelpaar genereren.
Reponse
Oké, terug naar het handshake-verzoek. Nadat we het naar het TAPO-apparaat hebben gestuurd, krijgen we (als alles goed gaat) de volgende response-body:
{
"error_code":0,
"result":{
"key":"ZvHUZ2EZ1LLkrh9YG0ShBINL59Rna1++j8iW2r44klFseH17A6C8HH2TqN8UkNpi+MHxFgQ4Jvs/nvz8QoNPgVxWCsgVBI01GTtDdwHtaRXRNh2VuIp6NDUJ0/1NSydiMfeUs1AZT2vwxSg7/cI1DVHFzL7jNr1WNHEsDiYtm48="
}
}en een Set-Cookie-header met de TP_SESSIONID: TP_SESSIONID=D31BB81A0B0A3...EF0A790A150AD60A;TIMEOUT=1440 (Die nodig is voor de volgende verzoeken)
Zoals je ziet is het handshake-verzoek 24 uur geldig.
TAPO-app-implementatie - ontsleuteling van de sleutel
De TAPO-app ontsleutelt de sleutel van het apparaat in deze functie; ik heb hem als volgt (her)geïmplementeerd:
public void mo35024a(String tapokey) {
byte[] decode = KspB64.decode(key.getBytes("UTF-8"));
byte[] decode2 = KspB64.decode(keyPair.getPrivateKey());
Cipher instance = Cipher.getInstance("RSA/ECB/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);
return new C658a(bArr, bArr2);
}Dit codeblok genereert een Cipher met RSA/NONE/PKCS1Padding en voert vervolgens meer methoden uit en manipuleert de data.
Op de laatste regel van deze functie wordt een C6586a-object gedefinieerd:
public class C658a {
Cipher f21776a_enc;
Cipher f21777b_dec;
public C658a(byte[] bArr, byte[] bArr2) throws Exception {
SecretKeySpec secretKeySpec = new SecretKeySpec(bArr, "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(bArr2);
f21776a_enc = Cipher.getInstance("AES/CBC/PKCS7Padding");
f21776a_enc.init(1, secretKeySpec, ivParameterSpec);
f21777b_dec = Cipher.getInstance("AES/CBC/PKCS7Padding");
f21777b_dec.init(2, secretKeySpec, ivParameterSpec);
}
/* more code */
}Hier zien we dat bArr een secret key spec is voor AES. En bArr2 is de iv.
Na deze constructor zijn er twee functies die de encryptie en de decryptie van de param-inhoud afhandelen:
public String mo38009b_enc(String str) throws Exception {
byte[] doFinal;
doFinal = this.f21776a_enc.doFinal(str.getBytes());
String encrypted = KspB64.encodeToString(doFinal);
return encrypted.replace("\r\n","");
}
public String mo38006a_dec(String str) throws Exception{
byte[] doFinal;
doFinal = this.f21777b_dec.doFinal(KspB64.decode(str.getBytes("utf-8")));
return new String(doFinal);
}Met deze twee functies kunnen we de verzoeken die we naar het TAPO-apparaat sturen versleutelen en de responses ontsleutelen!
Eerste securePassthrough
Het tweede verzoek dat naar het apparaat wordt gestuurd, is een universeel verzoek met de method securePassthrough. Het wordt later gebruikt voor elk ander verzoek, zoals het wijzigen van de status van de plug, info ophalen, enzovoort.
De request-body van de TAPO-app ziet er als volgt uit:
{
"method": "securePassthrough",
"params": {
"request": "vQewGPIlmr3G2l8uL0O3Yjnxc6dKUAMBzOA4xGwJe81N4iYrFzEEoLxY2Jxr5qxQ5uE84gMgQVHJ\nT174Z6z1/lDglp0FOtcFdXw6lUsvj5hcgjpHjaD+6CxcA5z1XF4xyfDJIIBcb5eJ+ZCyiw9wO+WN\nNnBg5SH6Lmq06+AzbP8I6R6X8SgrEt2OUjclJWnuYjJlxffwFD243VU30fKhjMthzGo0+UU+bXgA\nEE/LITY=\n"
}
}Zoals je ziet is alles leesbaar behalve params.request. Zoals we weten is dat versleuteld met mo38009b_enc uit het C658a-object dat een stap eerder is aangemaakt. De platte params.request ziet er als volgt uit:
{
"method": "login_device",
"params": {
"password": "ITcyNjU....",
"username": "MzhhNTk2NT..."
},
"requestTimeMils": 0
}We kunnen dus zien dat het tweede verzoek de autorisatiepoging is met de TP-Link-inloggegevens.
TAPO-app-implementatie - codering van de inloggegevens
Zoals je ziet staan het wachtwoord en de gebruikersnaam niet in platte tekst. Ze zijn gecodeerd, allebei op een andere manier.
Het wachtwoord is gecodeerd met B64-encoding en de gebruikersnaam gebruikt een MessageDigest van SHA1:
public static String shaDigestUsername(String str) throws NoSuchAlgorithmException {
byte[] bArr = str.getBytes();
byte[] digest = MessageDigest.getInstance("SHA1").digest(bArr);
StringBuilder sb = new StringBuilder();
for(byte b : digest){
String hexString = Integer.toHexString(b & 255);
if(hexString.length() == 1){
sb.append("0");
sb.append(hexString);
} else {
sb.append(hexString);
}
}
return sb.toString();
}En daarna wordt deze gebruikersnaam ook nog B64-gecodeerd.
Reponse
{
"error_code": 0,
"result": {
"response": "zQMfnu0DQcB9xaJ9srqWVqbxC/2vuKnDT4jyFVqKyCb4GBas06djUCchwdwbp8iFr9Z5gFtrMmy/SHVjKl3eruAqe+vzVtgQtWUjeVrhSyE="
}
}Deze response kan worden ontsleuteld en na ontsleuteling zien we:
{
"error_code": 0,
"result": {
"token": "E0AA81A79277AA712...BF127322B523"
}
}En daar is hij: het volgende token dat nodig is voor de volgende verzoeken. Op dit punt zijn we geauthenticeerd om andere verzoeken uit te voeren.
Implementatie van mijn doel
Nadat we de eerste twee verzoeken hebben gesnift, kunnen we het laatste sniffen: het verzoek voor de status van het lampje. Ontsleuteld ziet het er zo uit:
{
"method": "set_device_info",
"params": {
"device_on": false
},
"requestTimeMils": 1602840338865,
"terminalUUID": "88-54-DE-AD-52-E1"
}De meeste velden spreken voor zich; terminalUUID is het MAC-adres van de plug.
Deze params.request kan worden ingevoegd in een verzoek met de method securePassthrough.
Samenvatting
Met deze informatie is het dus mogelijk om een library te maken om TP-Link TAPO-apparaten te besturen.
Voorlopig heb ik alleen set_device_info geïmplementeerd, maar op dit punt kan ik gewoon elk ander verzoek dat de app maakt sniffen, het ontsleutelen en het uiteindelijk implementeren.
Ik plaats later een PoC geschreven in Java.
PS
Om onbekende verzoeken te ontsleutelen (zonder reverse engineering) moet je mo35029c patchen, zodat hij jouw eigen private en publieke sleutel gebruikt.