eufy-security-client: [Question]: Cover_Path returns local file / Cloud images are encrypted

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?

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 1
  • Comments: 85 (66 by maintainers)

Commits related to this issue

Most upvoted comments

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)… 😉

Today I finally had time to dedicate myself to this topic again and was able to decode it correctly 🎉.

I will now implement the remaining part in the library.

Stay tuned 😉

Many thanks to all who have helped.

Here is a foretaste of the correct functions:

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][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, code: string): string {
    try {
        const ncode = Number.parseInt(code.substring(2));
        const prefix = 1000 - getIdSuffix(p2pDid);
        return md5(`${prefix}${ncode}`).toString(enc_hex).toUpperCase();
    } catch(error) {
        //TODO: raise custom exception
    }
    return ``;
};

export const getImageKey = function(serialnumber: string, p2pDid: string, code: string): string {
    const basecode = getImageBaseCode(serialnumber, p2pDid);
    const seed = getImageSeed(p2pDid, code);
    const data = `01${basecode}${seed}`;
    const hash = sha256(data);
    const hashBytes = [...Buffer.from(hash.toString(enc_hex), "hex")];
    const startByte = hashBytes[10];
    for(let i = 0; i < 32; i++) {
        const byte = hashBytes[i];
        let fixed_byte = startByte;
        if (i < 31) {
            fixed_byte = hashBytes[i + 1];
        }
        if ((i == 31) || ((i & 1) != 0)) {
            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()}`;
};

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 :):

  • eufysecurity: If the first 12 bytes contain the string eufysecurity, the image data is encrypted, otherwise not.
  • T8010P2XXXXXXXX8: serialnumber of the station
  • 0110972768: Secret required to derive the effective encryption key (AES key)

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 😃

I will check this soon…

sorry for the interruption, the updated stuff is now added: previewImageHomeBase_v2-branch. Please keep in mind, that you will have to adapt the implementation in your euyfsecurity.ts (at the end of the file, that’s why I have commented it out) [I do historically some adaptations]. But the rest should work.

@bropat @PhilippEngler I did some debugging on this topic. I managed to attach Frida to the native library functions and call them with the provided args from bropat.

I got the following results:

gen_rand_seed('ZXCAMAA-000000-HCUHU', '0000000000') // B7A782741F667201B54880C925FAEC4B 

This is the same as bropat’s getImageSeed implementation 🎉


So i did a lot of debugging and found out that we have the following values:

Doorbell:

the GenCheckCode is called like

GenCheckCode('STATION_SN', 'HXUSCAM-430329-HWEPG', '0186410153')
 returns: "E33D0354E55F390AFE6B007EC93EC021"

inside the native library:

the gen_check_code_v1 is called with:

gen_check_code_v1('STATION_SN22', 'CBA63CB1FE86078F682FA42FB2FEE090'). 

argument1 represents the basecode argument2 represents the seed

the basecode is coming from gen_pic_base_code which returns the station_sn+suffix

you can validate this by running

getImageSeed('HXUSCAM-430329-HWEPG', '0186410153') // CBA63CB1FE86078F682FA42FB2FEE090 

Indoorcam:

the GenCheckCode is called like

GenCheckCode('STATION_SN', 'HXUSCAM-348024-PHMUP', '0136152141')
returns: "456DBA5721B2345ACF3802B0F9072A2B"

inside the native library:

the gen_check_code_v1 is called with:

gen_check_code_v1('STATION_SN11, '03BB6E12CF8481C99B02EBEEFB710C84')

argument1 represents the basecode argument2 represents the seed

the basecode is coming from gen_pic_base_code which returns the station_sn+suffix

you can validate this by running

getImageSeed('HXUSCAM-348024-PHMUP', '0136152141') // 03BB6E12CF8481C99B02EBEEFB710C84 

Theoretical device:

the GenCheckCode is called like

GenCheckCode('0123456789000003', 'ZXCAMAA-000000-HCUHU', '0000000000')
returns: "456DBA5721B2345ACF3802B0F9072A2B"

inside the native library:

the gen_check_code_v1 is called with:

gen_check_code_v1('01234567890000030', 'B7A782741F667201B54880C925FAEC4B')

argument1 represents the basecode argument2 represents the seed

the basecode is coming from gen_pic_base_code which returns the station_sn+suffix

you can validate this by running

getImageSeed('ZXCAMAA-000000-HCUHU', '0000000000') // B7A782741F667201B54880C925FAEC4B 

Concluding:

  • getImageSeed is ✅
  • getIdSuffix is ✅
  • getImageBaseCode should return: ${DEVICE_SN}${suffix}
  • getImageKey should be fixed, but really can’t figure it out. (tried a lot of chatGPT 😛 )

but if i follow the pattern from above for the keys bropat provide we should have:

basecode = 01234567890000030
seed = B7A782741F667201B54880C925FAEC4B

I hope this helps. I’ll have another look at this this week, but hopefully this is helpful 🤞🏼 I can share my device_sn’s in the mail if you like 😉

@fuatakgun I implemented it already for Homey.

You can take getPropertyValue(PropertyName.DevicePicture)

Then you get a data field which contains the image buffer which you can turn into a picture 😃

For what it’s worth, this is my integration in Homey: https://github.com/martijnpoppen/com.eufylife.security/blob/main/drivers/main-device.js#L497

If anyone is interested I found an interim solution on Android using Autonotification+Tasker to grab the snapshot and post in it on a Home Assistant camera using the Push Camera integration. Works flawlessly.

Same here indeed, had some other priorities last week. Will try to spend some time on it this week.

@PhilippEngler I’ll forward you the same mails I send to @bropat

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

@martijnpoppen can you send me the IDA functions too? 😃

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 ?

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…

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

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.

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.

As far as I understood it will download the cover_path and set that into the DevicePicture.

Correct, but the Indoor Cam PT (T8410) hasn’t the parameter cover_path 😉 In the next version of the library, the initial image will be fetched from the database (only if an sd card is inserted, formatted and working) for this device (already works):

https://github.com/bropat/eufy-security-client/commit/db5057289962d551af1fae3803fd422cd5fee75c

@martijnpoppen

Okay, complicated stuff, hard to distingiush what stuff is done in @bropat stuff and @fuatakgun parts 😃

Will wait for update on his side 😃

Great, appreciated @martijnpoppen

Hi all,

@bropat thanks again for great solution. So, do we need to download the latest image now, rather than relying on url?

@bropat thanks for the update. Works perfect!

One thing: the initial Image is not working. Somehow the command CMD_DATABASE_IMAGE doesn’t seem to work.

Thanks, @bropat and @martijnpoppen. Great work. I had finished the image download from homebase part, do you need it @bropat?

@bropat Nice work! 🎉 💪🏼

I just tried it in my script and works like a charm 😄 So the expected outputs are indeed as the ones I shared last week !

Thanks a lot! Your tasker profile was a good starter, though I had to do some adjustments as the curl call did not work on my Fire Tablet house dashboard (apparently no curl installed on these devices…), so I used the tasker http api. A native implementation decrypting the data in the library would of course be preferred, but at least I got pictures on my tablet again. Hope this works as flawless for me as it does for you. Thanks again! 😃

It’s in your mail @bropat

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.

I’ll have a look soon 😃

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.

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

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

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).