bropat / eufy-security-client

This shared library allows to control Eufy security devices by connecting to the Eufy cloud servers and local/remote stations over p2p.
MIT License
507 stars 75 forks source link

[Question]: Cover_Path returns local file / Cloud images are encrypted #273

Closed martijnpoppen closed 1 year ago

martijnpoppen commented 1 year ago

Ask your question

Hey @bropat , Seems like Eufy patched more in their API and now the cover_path is moved from a CDN url to T8410REDACTED~/media/mmcblk0p1/Camera00/20221210230725n.jpg

Any idea if it's possible to fetch an image from the station itself?

Jo-Jo1987 commented 1 year ago

same

PhilippEngler commented 1 year ago

Hi martijnpoppen,

this seem to be implemented by the firmware version 3.2.5.1. My HomeBase E gives another link to the image (T8002REDACTED~/media/mmcblk0p1/video/20221210235159_c00.jpg). My cams on the HomeBase 2 have not detect any motion since the firmware update.

I think, we have two options: We can use the stationDownload function to download the image (i will try that tonight). Or, if this does not work, we have to implement function like bropat did it in his ioBroker plugin (onStationDownloadStart function in bropat/ioBroker.eusec). There he is downloading the video and create a thumbnail from the downloaded video. But this will add new dependencies (mainly ffmpeg).

fuatakgun commented 1 year ago

Can we download the image as if we download the video?

PhilippEngler commented 1 year ago

Can we download the image as if we download the video?

I am not sure. I will try it later this day.

martijnpoppen commented 1 year ago

@PhilippEngler yeah that's the Issue for my app. (https://homey.app/en-us/app/com.eufylife.security/Eufy-Security/)

The Homey hardware is not powerful enough to run ffmpeg + no way to install it :/

Beer17HWAM commented 1 year ago

Can we download the image as if we download the video?

I am not familiar with downloading video, can you explain me how to do it with Home Assistant or Node-red?

PhilippEngler commented 1 year ago

Downloading the image does not work. Eventually there is a new CommandType for downloading images. I could not find anything regarding this in the android app (v4.5.1), maybe someone else finds something.

For all recordings done with older firmware, the lifetime of the CDN-Links was reduced from 24 to 1 hour in my case.

bropat commented 1 year ago

I will check this soon...

bropat commented 1 year ago

I checked my Eufy Cam 2 and the notification image is still coming from the cloud, except that the image file is encrypted. The first 41 bytes are a custom header from Eufy that contains info to decrypt the beginning of the image data. Only the first 256 bytes of the image data are encrypted with AES.

PhilippEngler commented 1 year ago

Thanks for your checking @bropat. May I ask, what firmware version your homebase is running on? The problem here occurred after the homebase was updated to 3.2.5.1(h) and only for events after the firmware update. Very strange...

DEBUG: Normalized Properties {  deviceSN: 'T8112N2X',  properties: { motionDetected: false, personDetected: false, personName: '', name: 'Wäscheplatz Tor', model: 'T8112', serialNumber: 'T8112N2X', type: 1, hardwareVersion: 'HAIYI-IMX323', softwareVersion: '1.9.3', stationSerialNumber: 'T8002N2X', pictureUrl: 'T8002N2X~/media/mmcblk0p1/video/20221210235159_c00.jpg', recordingClipLength: 0, ...

martijnpoppen commented 1 year ago

@PhilippEngler thats the cover_path from the device response The one @bropat talks about is the cloud image from the notification

martijnpoppen commented 1 year ago

@bropat is the decryption something you want to put in this library? I was thinking that it might be nice to provide a decryption method which can be used by the clients using the library ?

Beer17HWAM commented 1 year ago

I wish you all success. I hope you all can find a way to retrieve the last motion image (thumbnail). I always used it for sending to my telegram with (home assistant / node-red) because I did this for all my cams. It's fast and much better than opening all kinds of apps.

PhilippEngler commented 1 year ago

Thank you for clarification, @martijnpoppen, I have misunderstood that point, sorry.

ajvdw commented 1 year ago

I think the android app (apk) will show the way to decode the thumbnail ...

martijnpoppen commented 1 year ago

@bropat i tried several decryption methods based on the info you gave (first 41 bytes etc) but unfortunately i end up with corrupted images. Hopefully you can come up with a nice solution :D

bropat commented 1 year ago

@martijnpoppen A few days ago I successfully decrypted the image with the correct keys ;) But I still need to explore the generation of the correct key. Unfortunately I had a very stressful month at work and could not spend much time...

martijnpoppen commented 1 year ago

Thats moe than i achieved :p

No worries! Take your time and have nice holidays! 🍾🎄

Beer17HWAM commented 1 year ago

Thank you all for the effort. Have a nice Christmas and lucky new year!

bropat commented 1 year ago

Thank you, you too

Jo-Jo1987 commented 1 year ago

I am struggling with the 2fa again. I have to reconfigure it, to see if i get it working again.,

Beer17HWAM commented 1 year ago

I will ask it again because I never got an answer. Is it possible to retrieve any video from the base station? What can we do with the eufy-security-ws library at this moment other than streaming start/stop and motion and ring detection? Can we retrieve any kind of video from the past?

fuatakgun commented 1 year ago

@Beer17HWAM did you check commands and events of the repository and couldn't find your answer?

Beer17HWAM commented 1 year ago

@Beer17HWAM did you check commands and events of the repository and couldn't find your answer?

I'am only able to start video streaming. I don't know if I can do more. I like to hear it from someone who knows. Maybe with examples in HA or Node-red. I can't find it myself.

fuatakgun commented 1 year ago

You didn't answer my question? Did you go through the getting started documentation?

Beer17HWAM commented 1 year ago

You didn't answer my question? Did you go through the getting started documentation?

Yes here: https://bropat.github.io/eufy-security-ws/#/api_cmds

But I don't know how to do use the commands and I hope someone can show me how to retrieve old videos.

fuatakgun commented 1 year ago

So, your question had changed a lot. So you know now it is capable of doing it but you don't know how to do it.

You can more info below or different previously created issues.

https://github.com/bropat/eufy-security-ws/issues/31

Get video events Get cipher and path from events Send download video command with cipher and path You will receive bytes of video and audio with video data and audio data events

Beer17HWAM commented 1 year ago

Thanks for the info. I was hoping for a step by step instruction manual and an example. I am familiar with scripting but this is a bit to complex for me.

fuatakgun commented 1 year ago

it is not an easy thing, agree. Let me put more detailed example here, I am planning to add this feature into Home Assistant integration soon (get the latest recorded video)

send_message - {'messageId': '1', 'command': 'driver.get_video_events', 'maxResults': 1}

 _on_message - {'type': 'result', 'success': True, 'messageId': '1', 'result': {'events': [{'monitor_id': 114484245, 'transfer_monitor_id': 0, 'station_sn': 'T8010N2320460480', 'device_sn': 'T8113N63205014E2', 'storage_type': 1, 'storage_path': '/media/mmcblk0p1/Camera01/20221230205117.dat', 'hevc_storage_path': '', 'cloud_path': '', 'frame_num': 149, 'thumb_path': 'T8010N2320460480~/media/mmcblk0p1/video/20221230205117_c01.jpg', 'thumb_data': '', 'start_time': 1672429877877, 'end_time': 1672429887784, 'cipher_id': 191, 'cipher_user_id': 'd0da85d5ba183f277c3d7eca940c4cb880c43651', 'has_human': 0, 'volume': 'Anker_B_JICiGmd', 'vision': 1, 'device_name': 'Entrance', 'device_type': 8, 'video_type': 1002, 'extra': 'eyJhdXRvbWF0aW9uX2lkIjowLCJoYXNfbWRldGVjdCI6MSwicHVzaF9tb2RlIjoyLCJtaWNfc3RhdHVzIjoxLCJyZWNvcmRfZm9ybWF0IjoxLCJwaWNrX3RpbWUiOjAsImRlbGl2ZXJfdGltZSI6MH0=', 'user_range_id': 387702, 'viewed': False, 'create_time': 1672429888, 'update_time': 1672429888, 'status': 1, 'station_name': '', 'p2p_did': 'HXEUCAM-135914-HKXPL', 'push_did': 'HXEUCAM-135914-HKXPL', 'p2p_license': 'RSEUZH', 'push_license': '', 'ndt_did': 'HXEUCAM-135914-HKXPL', 'ndt_license': '', 'wakeup_flag': 1, 'p2p_conn': '', 'app_conn': '', 'query_server_did': '', 'prefix': 'ZXCAMAA', 'wakeup_key': '', 'ai_faces': None, 'is_favorite': False, 'storage_alias': 1}]}}

send_message - call.data: {'message': {'messageId': '1', 'command': 'device.start_download', 'serialNumber': 'T8113N63205014E2', 'cipherId': 191, 'path': '/media/mmcblk0p1/Camera01/20221230205117.dat'}}

_on_message - {'type': 'result', 'success': True, 'messageId': '1', 'result': {'async': True}}

_on_message - {'type': 'event', 'event': {'source': 'device', 'event': 'command result', 'serialNumber': 'T8113N63205014E2', 'command': 'start_download', 'returnCode': 0, 'returnCodeName': 'ERROR_PPCS_SUCCESSFUL', 'customData': {'command': {'name': 'deviceStartDownload', 'value': {'path': '/media/mmcblk0p1/Camera01/20221230205117.dat', 'cipher_id': 191}}}}}

_on_message - {'type': 'event', 'event': {'source': 'device', 'event': 'download started', 'serialNumber': 'T8113N63205014E2'}}

_on_message - {'type': 'event', 'event': {'source': 'device', 'event': 'download video data', 'serialNumber': 'T8113N63205014E2', 'buffer': {'type': 'Buffer', 'data': [VIDEO BYTES ARE HERE]}, 'metadata': {'videoCodec': 'H264', 'videoFPS': 15, 'videoHeight': 1080, 'videoWidth': 1920}}}

 _on_message - {'type': 'event', 'event': {'source': 'device', 'event': 'download video data', 'serialNumber': 'T8113N63205014E2', 'buffer': {'type': 'Buffer', 'data': [AUDIO BYTES ARE HERE]}, 'metadata': {'videoCodec': 'H264', 'videoFPS': 15, 'videoHeight': 1080, 'videoWidth': 1920}}}

Now, you need to merge video and audio bytes into one video file, save it somewhere so you can watch it later on. Or you can live stream these.

PhilippEngler commented 1 year ago

If I am right, the image path in the notification event is only contained when the extended push notification is enabled.

I have taken a deeper look at the app and the p2p communication: The app use p2p to fetch the preview image from the HomeBase. Therefor the "cover_path" is used. The "~" is to split the path in station serial and the path to the image.

For now, I was able to build the p2p request, sending it to the station and receiving data from the station. What is missing is parsing the data and store the image (and last but not least the confirmation, that the data contains the image).

You can take a look here: Link to PhilippEngler/eufy-security-client/commit/4bed085f413df9fb04ee35e8a71cac59d45011f1

martijnpoppen commented 1 year ago

Nice @PhilippEngler I'll have a look at that too! 👀

I guess if you can download the image this way it would also work for the notification images. As every notification has a thumb_path. Then there's no need to decrypt the cloud image :)

bropat commented 1 year ago

fyi: The image data you receive via the p2p command is encrypted in the same way as the images of the cloud push notification.

I think I am close to the result.

The reverse engineering of .so files is much more complicated (libenc.so)... ;)

cratoo commented 1 year ago

@bropat can we support in any way to help in the reverse engineering? Missing the pictures of the camera a lot :)

Beer17HWAM commented 1 year ago

See my post fuatakgun/eufy_security#685

I start the Eufy Doorbell Stream and grab a part of the stream. I grab directly from the rtsp Eufy stream, not the go2rtc stream. It's working fine but indeed ffmpeg is slow to start, you miss the first seconds. But it works, you can send a few seconds to Telegram. I have it up and running now:

Start streaming

Grab piece of stream: ffmpeg -y -i rtsp://192.168.1.8:8554/T8210P0020330C10 -c copy -map 0 -t 6 ffmpeg-snapshot.mp4

Send mp4 to Telegram

Repeat this as long there is motion and keep it running for a half a minute or so when motion stops.

It works great. I tried to Grab one jpg frame but that is not working because the first part is indeed not "full". It will be a distorted jpg because of the delay of a keyframe. The mp4 is perfect. I grab 6 seconds.

I hope you can do something with this now. Good luck.

bropat commented 1 year ago

Sorry guys had a lot to do and so couldn't invest much time here. Anyone who wants to help is of course welcome. :)

As you know eufy has encrypted the images you receive through the push notification. The same encryption is also used when you try to download the images via P2P (see commit above from @PhilippEngler).

Example Image data:

eufysecurity:T8010P2XXXXXXXX8:0110972768:<256 bytes encrypted image header><unencrypted image data>

The first 41 bytes contain part of the required decryption information. 3 pieces of information are supplied (separated by :):

A fourth piece of information is needed and this is fetched directly from the cloud from the properties of the station, namely p2p_did.

The following implementation is found in the code used for image decoding:

public byte[] decryptImage(byte[] image_data) {
        int length = image_data.length - 41;
        byte[] bImageData = new byte[length];
        byte[] bSerialnumber= new byte[16];
        byte[] bSecret = new byte[10];
        // Station serialnumber
        System.arraycopy(image_data, 13, bSerialnumber, 0, 16);
        // Secret
        System.arraycopy(image_data, 30, bSecret, 0, 10);
        // Effective image data
        System.arraycopy(image_data, 41, bImageData, 0, length);
        String serialnumber = new String(bSerialnumber, StandardCharsets.UTF_8);
        String secret = new String(bSecret, StandardCharsets.UTF_8);
        QueryStationData queryStationData = StationDataManager.a().e(serialnumber);
        if (e == null) {
            return null;
        }
        // Get AES key passing station serialnumber, p2p_did and secret to the jndi function in libenc.so
        String aeskey = Encoder.genCheckCode(serialnumber, queryStationData.p2p_did, secret).substring(0, 16);
        byte[] bEncryptedImageHeader = new byte[256];
        System.arraycopy(bImageData, 0, bEncryptedImageHeader, 0, 256);
        // Decrypt first 256 image bytes using the derivated AES key
        System.arraycopy(decrypt(bEncryptedImageHeader, aeskey), 0, bImageData, 0, 256);
        return bImageData;
}

public byte[] decrypt(byte[] bEncryptedData, String aeskey) {
        try {
            SecretKeySpec secretKeySpec = new SecretKeySpec(aeskey.getBytes(StandardCharsets.UTF_8), "AES");
            Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
            cipher.init(2, secretKeySpec);
            return cipher.doFinal(bEncryptedData);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
...

The following is the class that loads the native external library libenc.so:

public class Encoder {
    public static native String genCheckCode(String serialnumber, String p2p_did, String secret);

    static {
        System.loadLibrary("enc");
    }
}

This library contains the knowledge of how to derive the AES key to decrypt the image header. For this reason, we need to understand how this works through reverse engineering. I use ghidra for that. So I load the library libenc.so into ghidra and have it analysed. From this I get pseudo C code. Here you still have to correct the data types and sizes. Of course, you can also give the variables meaningful names, etc. Behind the function genCheckCode you will find the following procedures in the native library:

    gen_pic_base_code(p_serialnumber,p_len_serialnumber,p_p2p_did,res_basecode);
    gen_rand_seed(p_p2p_did,p_numericstr,res_seed);
    gen_check_code_v1(res_basecode,res_seed,out_result);

I have tried to understand these procedures and translated them into the following functions in typescript:

import md5 from "crypto-js/md5";
import enc_hex from "crypto-js/enc-hex";
import sha256 from "crypto-js/sha256";

export const getIdSuffix = function(p2pDid: string): number {
    let result = 0;
    const match = p2pDid.match(/^[A-Z]+-(\d+)-[A-Z]+$/);
    if (match?.length == 2) {
        /*const num1 = Number.parseInt(match[1]);
        const num2 = Number.parseInt(match[1].substring(1));
        const num3 = Number.parseInt(match[1].substring(3));
        const num4 = Number.parseInt(match[1].substring(5));*/
        const num1 = Number.parseInt(match[1][0]);
        const num2 = Number.parseInt(match[1][1]);
        const num3 = Number.parseInt(match[1][3]);
        const num4 = Number.parseInt(match[1][5]);

        result = num1 + num2 + num3;
        if (num3 < 5) {
          result = result + num3;
        }
        result = result + num4;
    }
    return result;
};

export const getImageBaseCode = function(serialnumber: string, p2pDid: string): string {
    let nr = 0;
    try {
        nr = Number.parseInt(`0x${serialnumber[serialnumber.length - 1]}`);
    } catch (error) {
    }
    nr = (nr  + 10) % 10;
    const base = serialnumber.substring(nr);
    return `${base}${getIdSuffix(p2pDid)}`;
};

export const getImageSeed = function(p2pDid: string, secret: string): string {
    try {
        const nsecret = Number.parseInt(secret.substring(2));
        const prefix = 1000 - getIdSuffix(p2pDid);
        return md5(`${prefix}${nsecret}`).toString(enc_hex).toUpperCase();
    } catch(error) {
        //TODO: raise custom exception
    }
    return ``;
};

export const getImageKey = function(serialnummer: string, p2pDid: string, secret: string): string {
    const basecode = getImageBaseCode(serialnummer, p2pDid);
    const seed = getImageSeed(p2pDid, secret);
    const data = `10${basecode}${seed}`;
    const hash = sha256(data);
    const hashBytes = [...Buffer.from(hash.toString(enc_hex), "hex")];
    for(let i = 0; i < 32; i++) {
        const byte = hashBytes[i];
        let fixed_byte = hashBytes[10];
        if (i < 31) {
            fixed_byte = hashBytes[i + 1];
        }
        if ((i == 31) || ((i & 1) != 0)) {
            if (i != 31) {
                hashBytes[10] = fixed_byte;
            }
            if ((126 < byte) || (126 < hashBytes[10])) {
                if (byte < hashBytes[10] || (byte - hashBytes[10]) == 0) {
                    hashBytes[i] = hashBytes[10] - byte;
                } else {
                    hashBytes[i] = byte - hashBytes[10];
                }
            }
        } else if ((byte < 125) || (fixed_byte < 125)) {
            hashBytes[i] = fixed_byte + byte;
        }
    }

    return `${Buffer.from(hashBytes.slice(16)).toString("hex").toUpperCase()}`;
};

Unfortunately, I do not get the expected result. I think the error is in the second half of the getImageKey function... To understand where this effectively is, you have to debug the native library at runtime (e.g. with gdb). So I wrote a simple Java console program that implements and calls the native class Encoder so that I can test the native library better.

Encoder.java

package com.example.enc;

public class Encoder {

    public static native String genCheckCode(String str, String str2, String str3);

    static {
        System.loadLibrary("enc");
    }

}

HelloWorld.java

package com.example.enc;

import com.example.enc.Encoder;
import java.util.Scanner;

public class HelloWorld {

    public static void main(String[] args) {
        System.out.println("Start...");
    String serialnumber = "0123456789000003";
    String p2p_did = "ZXCAMAA-000000-HCUHU";
    String numericstr = "0000000000";
    if (args.length > 0) {
        serialnumber = args[0];
    }
    if (args.length > 1) {
        p2p_did = args[1];
    }
    if (args.length > 2) {
        numericstr = args[2];
    }
    System.out.println("Serialnumber: " + serialnumber);
    System.out.println("p2p_did: " + p2p_did);
    System.out.println("numericstr: " + numericstr);
        String result = Encoder.genCheckCode(serialnumber, p2p_did, numericstr);
        System.out.println("1 Result length: " + result.length());
        System.out.println("1 Result: " + result);

    Scanner sc = new Scanner(System.in);
    sc.nextLine();

        result = Encoder.genCheckCode(serialnumber, p2p_did, numericstr);
        System.out.println("2 Result length: " + result.length());
        System.out.println("2 Result: " + result);

    }

}

The line with sc.nextLine(); was implemented so that I can easily debug the process with gdb and be sure that the native library has already been loaded by the JVM (dalvikVM).

I compiled the Java code and created a jar file from the class files. I then converted this into a dex file using the Android SDK Build Tools (v. 33.0.0) with d8:

.../Android/sdk/build-tools/33.0.0/d8 --output dex helloworld.jar

I then loaded the dex file by adb push onto a rooted device or emulator including the libenc.so file (in the correct architecture of the device or emulator):

adb push classes.dex /data/local/tmp
adb push libenc.so /data/local/tmp

Then I started the program on the device or emulator as follows:

adb shell
su - 
cd /data/local/tmp
export LD_LIBRARY_PATH=/data/local/tmp
dalvikvm64 -cp classes.dex com.example.HelloWorld 0123456789000003 ZXCAMAA-000000-HCUHU 0000000000

Output of the above command:

Start...
Serialnumber: 0123456789000003
p2p_did: ZXCAMAA-000000-HCUHU
numericstr: 0000000000
output:: ts:0000000000, check code: D174D8169229AD39F55E6C3D801E4169
1 Result length: 32
1 Result: D174D8169229AD39F55E6C3D801E4169

<ENTER>

output:: ts:0000000000, check code: D174D8169229AD39F55E6C3D801E4169
2 Result length: 32
2 Result: D174D8169229AD39F55E6C3D801E4169

If you pass the correct input values here, you will get the AES key to decrypt the image header.

Attention: Only the first 16 bytes are used as AES key (see Java code at the beginning).

I have now fetched the gdbserver for the correct architecture of the device or emulator from the Android NDK (older version still supplied with gdb), loaded it with adb push and then started it.

  1. start the HelloWorld Java console program as shown above and do not give an enter command yet.
  2. Find the process id of launched java program:
adb push gdbserver /data/local/tmp
adb shell
su - 
ps -edf | grep dalvikvm
  1. Launch gdbserver as follow from another session:
adb push gdbserver /data/local/tmp
adb shell
su - 
cd /data/local/tmp
./gdbserver --attach :<port> <pid>
# Example: ./gdbserver --attach :5055 1234

Now you can forward the previously selected port with adb so that you can debug from the PC:

adb forward tcp:5055 tcp:5055
  1. Now you can use gdb or ghidra or radare to connect to the remote gdbserver process via localhost:5055 and start debugging.

So far, I have not been able to read the correct registers or memory areas to understand what was misinterpreted in the typescript code above. I assume that there is an error in the generated pseudo C code of ghidra. Here you could also check the assembler code using the official documentation from ARM to understand what ghidra has interpreted wrong... Another challenge... ;)

The challenge is now to get the correct result with the following input values and thus solve the mystery ;)

serialnumber = "0123456789000003";
p2pDid = "ZXCAMAA-000000-HCUHU";
code = "0000000000";

Expected outcome: D174D8169229AD39F55E6C3D801E4169

If someone can be helpful here would be great :)

bropat commented 1 year ago

Annex:

I have compiled the Java code with the following commands:

javac -source 1.7 -target 1.7 -d bin src/com/example/enc/Encoder.java
javac -source 1.7 -target 1.7 -d bin -cp bin/com/example/enc src/com/example/enc/HelloWorld.java
martijnpoppen commented 1 year ago

I'll have a look soon :)

martijnpoppen commented 1 year ago

So small update from my side

I spend some long evenings on this. Unluckily I'm not really helpful...

Java and reverse engineering APK's are not my expertise. Certainly not reading Peudo Code C files 😅

I did take another approach at getting the genCheckCode result. I hooked up frida to my emulator and ofcourse I also got D174D8169229AD39F55E6C3D801E4169 back.

Next to that I tried to dissasemble the libhome_security.so (same as libenc.so?) with RetDec and IDA Pro and Hopper. The IDA Pro file is better readable than the RetDec variant, but still I couldn't manage to get anything useful out of it.

If it helps I can share you the IDA Pro functions.

Also trying another approach: I downloaded the ROM of my indoorcam. Hoping there would be a encrypt function somewhere which would help us decrypting it. No luck yet, but will dig further. Some info on the ROM here

Might be interesting...: it seems like the Homebase 3 isn't encrypting it's images..

PhilippEngler commented 1 year ago

I have also spend some time on this. Yes, @martijnpoppen, the functions from libenc.so were moved to libhome_security.so in v4.5.4 (and the libhome_security.so should also includes the new p2p messages types).

I have also tried to understand the code. From my side, I have a slightly different implementation of the for-statement of @bropat's getImageKey-function. But the result is the same, so that I believe, that the problem is in one of the three functions before. I will have a look on this.

One question I have regarding @bropat's implementation: Does ${Buffer.from(hashBytes.slice(16)).toString("hex").toUpperCase()} will return us the really the first 16 bytes? I have understand the slice-function so, that in this case we will get the last 16 bytes. ${Buffer.from(hashBytes.slice(0,16)).toString("hex").toUpperCase()} will return the first 16 bytes? But maybe (surely) I am completely wrong.

The non-encrypting thing with the HB3 is interesting... All in all I have the impression, that they are being under time pressure. The app versions v4.5.4 and v4.5.5 for example does not show the mode changing events from HomeBase 2 or E while they are on firmware version below 3.2.6.7. And the app v4.5.3 does not show the mode changing events from HomeBase firmware 3.2.6.7.

martijnpoppen commented 1 year ago

Ah thanks @PhilippEngler that makes sense. Saw that GenCheckCode is there since app V2.

I do think you're right about the 3 first functions, also tried some different implementations but got the same result everytime.

Next to that I did just find something in the firmware 😄 There's home_security file there too with functions save_the_enc_pic, gen_rand_seed, gen_check_code_v1, gen_base_code Might be useful ?

PhilippEngler commented 1 year ago

Thank you @martijnpoppen for the information about the GenCheckCode occurrence in the app.

I do think you're right about the 3 first functions, also tried some different implementations but got the same result everytime.

That sounds quite good. @bropat has done great work with the implementation.

Next to that I did just find something in the firmware 😄 There's home_security file there too with functions save_the_enc_pic, gen_rand_seed, gen_check_code_v1, gen_base_code Might be useful ?

Yes, this could maybe help. The results should be the same. Is there a possibility to send me the IDA Pro result of the three functions, I want to check if there are differences to Ghidra (I think we should not share the disassembled code here in the comments).

I hope that you have success on the firmware-way.

martijnpoppen commented 1 year ago

Yes i can send you the file. Anywhere we can reach each other? 😄 And also the firmware .so file if you're interested in that. Decompiling that one is a bit harder

PhilippEngler commented 1 year ago

Yes i can send you the file. Anywhere we can reach each other? 😄 And also the firmware .so file if you're interested in that. Decompiling that one is a bit harder

From my side, the best way is by email: I would ask you, if you could take a look here, there you can find the address (it's the package.json of my project): PhilippEngler/eufy-security-hm/package.json in the author key. It lead to my private mail, so no other person will receive this email. Many thanks in advance.

I will take what I can get and try my best :smile:.

bropat commented 1 year ago

One question I have regarding @bropat's implementation: Does ${Buffer.from(hashBytes.slice(16)).toString("hex").toUpperCase()} will return us the really the first 16 bytes? I have understand the slice-function so, that in this case we will get the last 16 bytes. ${Buffer.from(hashBytes.slice(0,16)).toString("hex").toUpperCase()} will return the first 16 bytes? But maybe (surely) I am completely wrong.

The gen_check_code_v1 function produces a SHA256 hash at the end, whereby only the last 16 bytes (of the total 32 bytes) are converted into uppercase hex string (32 characters long) and returned as the result. From this result, the first 16 characters of the string are then used as the AES key.

bropat commented 1 year ago

@martijnpoppen can you send me the IDA functions too? :)

martijnpoppen commented 1 year ago

@bropat @PhilippEngler Sent! :D

martijnpoppen commented 1 year ago

The IDA files only contain the relevant functions. Someone else decompiled it for me, as I don't have the pro version

bropat commented 1 year ago

Thank you very much. I quickly skimmed the code and there is still 1 relevant function j_cal_ppcs_id_suffix missing. Can you ask for these as well? :)

martijnpoppen commented 1 year ago

It's in your mail @bropat

bropat commented 1 year ago

@martijnpoppen

I need the following file <IDA installation folder>\plugins\defs.h from the used IDA version.

Thanks again :)