diff mbox series

[firmware-utils] add dlink-sge-image for D-Link devices by SGE

Message ID 20230623120600.16663-1-openwrt@sebastianschaper.net
State New
Headers show
Series [firmware-utils] add dlink-sge-image for D-Link devices by SGE | expand

Commit Message

Sebastian Schaper June 23, 2023, 12:06 p.m. UTC
This tool will encrypt/decrypt factory images requiring the "SHRS" header
e.g. COVR-C1200, COVR-P2500, COVR-X1860, DIR-878, DIR-882, ...

Encryption is loosely based on a series of blogposts by ricksanchez [1]
and the provided code [2], as well as patches to qca-uboot found in the
GPL tarball released for D-Link COVR-P2500 Rev. A1 [3].

Further scripts (e.g. /lib/upgrade/) and keys were found in the GPL tarball
and/or rootfs of COVR-C1200 (the devices are based on OpenWrt Chaos Calmer
and failsafe can be entered by pressing 'f' on the serial console during
boot, allowing to access the file system of the running device).

For newer devices like COVR-X1860 and DIR-X3260, an updated method of
vendor key derivation is implemented based on enk.txt from the GPL release.

[1] https://0x00sec.org/t/breaking-the-d-link-dir3060-firmware-encryption-recon-part-1/21943
[2] https://github.com/0xricksanchez/dlink-decrypt
[3] https://tsd.dlink.com.tw/GPL.asp

Signed-off-by: Sebastian Schaper <openwrt@sebastianschaper.net>
Tested-By: Alan Luck <luckyhome2008@gmail.com>
---

This patch is based on previous work on PR #4174, migrated to OpenSSL EVP API
and extended to include support for newer devices like COVR-X1860.

The intermediate development stages are tracked at
https://github.com/s-2/firmware-utils/commits/dlink-sge-image
and the forum thread
https://forum.openwrt.org/t/add-support-for-d-link-covr-x1860/161862


 CMakeLists.txt        |   1 +
 src/dlink-sge-image.c | 510 ++++++++++++++++++++++++++++++++++++++++++
 src/dlink-sge-image.h | 349 +++++++++++++++++++++++++++++
 3 files changed, 860 insertions(+)
 create mode 100644 src/dlink-sge-image.c
 create mode 100644 src/dlink-sge-image.h

Comments

Paul Spooren Nov. 2, 2023, 5:46 p.m. UTC | #1
Hello,

> On Jun 23, 2023, at 14:06, Sebastian Schaper <openwrt@sebastianschaper.net> wrote:
> 
> This tool will encrypt/decrypt factory images requiring the "SHRS" header
> e.g. COVR-C1200, COVR-P2500, COVR-X1860, DIR-878, DIR-882, ...
> 
> Encryption is loosely based on a series of blogposts by ricksanchez [1]
> and the provided code [2], as well as patches to qca-uboot found in the
> GPL tarball released for D-Link COVR-P2500 Rev. A1 [3].
> 
> Further scripts (e.g. /lib/upgrade/) and keys were found in the GPL tarball
> and/or rootfs of COVR-C1200 (the devices are based on OpenWrt Chaos Calmer
> and failsafe can be entered by pressing 'f' on the serial console during
> boot, allowing to access the file system of the running device).
> 
> For newer devices like COVR-X1860 and DIR-X3260, an updated method of
> vendor key derivation is implemented based on enk.txt from the GPL release.
> 
> [1] https://0x00sec.org/t/breaking-the-d-link-dir3060-firmware-encryption-recon-part-1/21943
> [2] https://github.com/0xricksanchez/dlink-decrypt
> [3] https://tsd.dlink.com.tw/GPL.asp
> 
> Signed-off-by: Sebastian Schaper <openwrt@sebastianschaper.net>
> Tested-By: Alan Luck <luckyhome2008@gmail.com>
> ---
> 
> This patch is based on previous work on PR #4174, migrated to OpenSSL EVP API
> and extended to include support for newer devices like COVR-X1860.
> 
> The intermediate development stages are tracked at
> https://github.com/s-2/firmware-utils/commits/dlink-sge-image
> and the forum thread
> https://forum.openwrt.org/t/add-support-for-d-link-covr-x1860/161862
> 

— %< —

> +/*
> +  key.pem as found in GPL tarball, e.g.:
> +  COVRP2500A1_FW101/COVRP2500_GPL_Release/package/tw-prog.priv/imgcrypt/key.pem
> +  encrypted with passphrase: "12345678"
> +*/
> +const unsigned char key_legacy_pem[] = R"(
> +-----BEGIN RSA PRIVATE KEY-----
> +Proc-Type: 4,ENCRYPTED
> +DEK-Info: AES-256-CBC,EAA1FD02CDBC7AA7E62AC821D47823F2
> +
> +1LiXFElARVvVJnikiVqZxC5FS7silmaqZ1yBOfzWuYNLaiEuvoUOylwiT0JYna94
> +nevCGjdU27GOUBsLnhGVulVuD8aiZCGBaZES5BAFtOEz0rrmpJLHxD3txLBh49rM
> +zLfn77/bMfubuhUFw+TPQ9J6SlrwK12IaMBTBTCFf06h+dkY500A0GAESgSA4Cab
> +retvT5/xtnl5jtT7zBYPbDGPDZ0fFoDSa7IqkqJ+chGz4w02UezAuKPPlNTTxD4l

— >% —

Could you please change the usage of raw strings (R”) with the less pretty `\n\` after each line? This would allow compilation on macOS which uses clang.

Apart from that I tried to get the device running based one your work. Via the web installer an upload and “successful” flash was possible, however it doesn’t boot afterwards. I'll solder a serial connection later to figure out where things go wrong.

Thanks for your work and patience!

Best,
Paul
diff mbox series

Patch

diff --git a/CMakeLists.txt b/CMakeLists.txt
index c1750d478c..d7d4ed2a7b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -40,6 +40,7 @@  FW_UTIL(buffalo-tftp src/buffalo-lib.c "" "")
 FW_UTIL(cros-vbutil "" "" "${OPENSSL_CRYPTO_LIBRARIES}")
 FW_UTIL(dgfirmware "" "" "")
 FW_UTIL(dgn3500sum "" "" "")
+FW_UTIL(dlink-sge-image "" "" "${OPENSSL_CRYPTO_LIBRARIES}")
 FW_UTIL(dns313-header "" "" "")
 FW_UTIL(edimax_fw_header "" "" "")
 FW_UTIL(encode_crc "" "" "")
diff --git a/src/dlink-sge-image.c b/src/dlink-sge-image.c
new file mode 100644
index 0000000000..9b567f43ff
--- /dev/null
+++ b/src/dlink-sge-image.c
@@ -0,0 +1,510 @@ 
+// SPDX-License-Identifier: GPL-2.0-or-later OR MIT
+/*
+ * Copyright (C) 2023 Sebastian Schaper <openwrt@sebastianschaper.net>
+ *
+ * This tool encrypts factory images for certain D-Link Devices
+ * manufactured by SGE / T&W, e.g. COVR-C1200, COVR-P2500, DIR-882, ...
+ *
+ * Usage:
+ *   ./dlink-sge-image DEVICE_MODEL infile outfile [-d: decrypt]
+ *
+ */
+
+#include "dlink-sge-image.h"
+
+#include <openssl/evp.h>
+#include <openssl/pem.h>
+
+#include <arpa/inet.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define BUFSIZE		4096
+
+#define HEAD_MAGIC		"SHRS"
+#define HEAD_MAGIC_LEN	4
+#define SHA512_DIGEST_LENGTH	64
+#define RSA_KEY_LENGTH_BYTES	512
+#define AES_BLOCK_SIZE	16
+#define HEADER_LEN		1756
+
+unsigned char aes_iv[AES_BLOCK_SIZE];
+
+unsigned char readbuf[BUFSIZE];
+unsigned char encbuf[BUFSIZE];
+
+unsigned int read_bytes;
+unsigned long read_total;
+unsigned int i;
+
+unsigned char vendor_key[AES_BLOCK_SIZE];
+BIO *rsa_private_bio;
+const EVP_CIPHER *aes128;
+EVP_CIPHER_CTX *aes_ctx;
+
+FILE *input_file;
+FILE *output_file;
+
+int pass_cb(char *buf, int size, int rwflag, void *u)
+{
+    char *tmp = "12345678";
+    size_t len = strlen(tmp);
+
+    if (len > size)
+        len = size;
+    memcpy(buf, tmp, len);
+    return len;
+}
+
+void image_encrypt(void)
+{
+	char buf[HEADER_LEN];
+	const EVP_MD *sha512;
+	EVP_MD_CTX *digest_before;
+	EVP_MD_CTX *digest_post;
+	EVP_MD_CTX *digest_vendor;
+	EVP_PKEY *signing_key;
+	EVP_PKEY_CTX *rsa_ctx;
+	uint32_t payload_length_before, pad_len, sizebuf;
+	unsigned char md_before[SHA512_DIGEST_LENGTH];
+	unsigned char md_post[SHA512_DIGEST_LENGTH];
+	unsigned char md_vendor[SHA512_DIGEST_LENGTH];
+	unsigned char sigret[RSA_KEY_LENGTH_BYTES];
+	size_t siglen;
+	char footer[] = {0x00, 0x00, 0x00, 0x00, 0x30};
+
+	// seek to position 1756 (begin of AES-encrypted data),
+	// write image headers later
+	memset(buf, 0, HEADER_LEN);
+	fwrite(&buf, 1, HEADER_LEN, output_file);
+	digest_before = EVP_MD_CTX_new();
+	digest_post = EVP_MD_CTX_new();
+	digest_vendor = EVP_MD_CTX_new();
+	sha512 = EVP_sha512();
+	EVP_DigestInit_ex(digest_before, sha512, NULL);
+	EVP_DigestInit_ex(digest_post, sha512, NULL);
+	EVP_DigestInit_ex(digest_vendor, sha512, NULL);
+
+	signing_key = PEM_read_bio_PrivateKey(rsa_private_bio, NULL, pass_cb, NULL);
+	rsa_ctx = EVP_PKEY_CTX_new(signing_key, NULL);
+
+	EVP_PKEY_sign_init(rsa_ctx);
+	EVP_PKEY_CTX_set_signature_md(rsa_ctx, sha512);
+
+	memcpy(&aes_iv, &salt, AES_BLOCK_SIZE);
+	aes_ctx = EVP_CIPHER_CTX_new();
+	EVP_EncryptInit_ex(aes_ctx, aes128, NULL, &vendor_key[0], aes_iv);
+	EVP_CIPHER_CTX_set_padding(aes_ctx, 0);
+	int outlen;
+
+	while ((read_bytes = fread(&readbuf, 1, BUFSIZE, input_file)) == BUFSIZE) {
+		EVP_DigestUpdate(digest_before, &readbuf[0], read_bytes);
+		read_total += read_bytes;
+
+		EVP_EncryptUpdate(aes_ctx, encbuf, &outlen, &readbuf[0], BUFSIZE);
+		fwrite(&encbuf, 1, BUFSIZE, output_file);
+
+		EVP_DigestUpdate(digest_post, &encbuf[0], BUFSIZE);
+	}
+
+	// handle last block of data (read_bytes < BUFSIZE)
+	EVP_DigestUpdate(digest_before, &readbuf[0], read_bytes);
+	read_total += read_bytes;
+
+	pad_len = AES_BLOCK_SIZE - (read_total % AES_BLOCK_SIZE);
+	if (pad_len == 0)
+		pad_len = AES_BLOCK_SIZE;
+	memset(&readbuf[read_bytes], 0, pad_len);
+
+	EVP_EncryptUpdate(aes_ctx, encbuf, &outlen, &readbuf[0], read_bytes + pad_len);
+	EVP_CIPHER_CTX_free(aes_ctx);
+	fwrite(&encbuf, 1, read_bytes + pad_len, output_file);
+
+	EVP_DigestUpdate(digest_post, &encbuf[0], read_bytes + pad_len);
+
+	fclose(input_file);
+	payload_length_before = read_total;
+	printf("\npayload_length_before: %li\n", read_total);
+
+	// copy digest state, since we need another one with vendor key appended
+	EVP_MD_CTX_copy_ex(digest_vendor, digest_before);
+
+	EVP_DigestFinal_ex(digest_before, &md_before[0], NULL);
+	EVP_MD_CTX_free(digest_before);
+
+	printf("\ndigest_before: ");
+	for (i = 0; i < SHA512_DIGEST_LENGTH; i++)
+		printf("%02x", md_before[i]);
+
+	EVP_DigestUpdate(digest_vendor, &vendor_key[0], AES_BLOCK_SIZE);
+	EVP_DigestFinal_ex(digest_vendor, &md_vendor[0], NULL);
+	EVP_MD_CTX_free(digest_vendor);
+
+	printf("\ndigest_vendor: ");
+	for (i = 0; i < SHA512_DIGEST_LENGTH; i++)
+		printf("%02x", md_vendor[i]);
+
+	EVP_DigestFinal_ex(digest_post, &md_post[0], NULL);
+	EVP_MD_CTX_free(digest_post);
+
+	printf("\ndigest_post: ");
+	for (i = 0; i < SHA512_DIGEST_LENGTH; i++)
+		printf("%02x", md_post[i]);
+
+	fwrite(&footer, 1, 5, output_file);
+
+	// go back to file header and write all the digests and signatures
+	fseek(output_file, 0, SEEK_SET);
+
+	fwrite(HEAD_MAGIC, 1, HEAD_MAGIC_LEN, output_file);
+
+	// write payload length before
+	sizebuf = htonl(payload_length_before);
+	fwrite((char *) &sizebuf, 1, 4, output_file);
+
+	// write payload length post
+	payload_length_before += pad_len;
+	sizebuf = htonl(payload_length_before);
+	fwrite((char *) &sizebuf, 1, 4, output_file);
+
+	// write salt and digests
+	fwrite(salt, 1, AES_BLOCK_SIZE, output_file);
+	fwrite(&md_vendor[0], 1, SHA512_DIGEST_LENGTH, output_file);
+	fwrite(&md_before[0], 1, SHA512_DIGEST_LENGTH, output_file);
+	fwrite(&md_post[0],   1, SHA512_DIGEST_LENGTH, output_file);
+
+	// zero-fill rsa_pub field, unused in header
+	memset(sigret, 0, RSA_KEY_LENGTH_BYTES);
+	fwrite(&sigret[0], 1, RSA_KEY_LENGTH_BYTES, output_file);
+
+	// sign md_before
+	EVP_PKEY_sign(rsa_ctx, &sigret[0], &siglen, &md_before[0], SHA512_DIGEST_LENGTH);
+	printf("\nsigned before:\n");
+	for (i = 0; i < RSA_KEY_LENGTH_BYTES; i++)
+		printf("%02x", sigret[i]);
+	fwrite(&sigret[0], 1, RSA_KEY_LENGTH_BYTES, output_file);
+
+	// sign md_post
+	EVP_PKEY_sign(rsa_ctx, &sigret[0], &siglen, &md_post[0], SHA512_DIGEST_LENGTH);
+	printf("\nsigned post:\n");
+	for (i = 0; i < RSA_KEY_LENGTH_BYTES; i++)
+		printf("%02x", sigret[i]);
+	fwrite(&sigret[0], 1, RSA_KEY_LENGTH_BYTES, output_file);
+
+	printf("\n");
+
+	fclose(output_file);
+}
+
+void image_decrypt(void)
+{
+	char magic[4];
+	uint32_t payload_length_before, payload_length_post, pad_len;
+	char salt[AES_BLOCK_SIZE];
+	char md_vendor[SHA512_DIGEST_LENGTH];
+	char md_before[SHA512_DIGEST_LENGTH];
+	char md_post[SHA512_DIGEST_LENGTH];
+	EVP_PKEY *signing_key;
+	EVP_PKEY_CTX *rsa_ctx;
+	unsigned char rsa_sign_before[RSA_KEY_LENGTH_BYTES];
+	unsigned char rsa_sign_post[RSA_KEY_LENGTH_BYTES];
+	unsigned char md_post_actual[SHA512_DIGEST_LENGTH];
+	unsigned char md_before_actual[SHA512_DIGEST_LENGTH];
+	unsigned char md_vendor_actual[SHA512_DIGEST_LENGTH];
+	const EVP_MD *sha512;
+	EVP_MD_CTX *digest_before;
+	EVP_MD_CTX *digest_post;
+	EVP_MD_CTX *digest_vendor;
+
+	printf("\ndecrypt mode\n");
+
+	if (fread(&magic, 1, HEAD_MAGIC_LEN, input_file) == 0)
+		goto error_read;
+	if (strncmp(magic, HEAD_MAGIC, HEAD_MAGIC_LEN) != 0) {
+		fprintf(stderr, "Input File header magic does not match '%s'.\n"
+			"Maybe this file is not encrypted?\n", HEAD_MAGIC);
+		goto error;
+	}
+
+	if (fread((char *) &payload_length_before, 1, 4, input_file) == 0)
+		goto error_read;
+	if (fread((char *) &payload_length_post, 1, 4, input_file) == 0)
+		goto error_read;
+	payload_length_before = ntohl(payload_length_before);
+	payload_length_post   = ntohl(payload_length_post);
+
+	if (fread(salt, 1, AES_BLOCK_SIZE, input_file) == 0)
+		goto error_read;
+	if (fread(md_vendor, 1, SHA512_DIGEST_LENGTH, input_file) == 0)
+		goto error_read;
+	if (fread(md_before, 1, SHA512_DIGEST_LENGTH, input_file) == 0)
+		goto error_read;
+	if (fread(md_post, 1, SHA512_DIGEST_LENGTH, input_file) == 0)
+		goto error_read;
+
+	// skip rsa_pub
+	if (fread(readbuf, 1, RSA_KEY_LENGTH_BYTES, input_file) == 0)
+		goto error_read;
+
+	if (fread(rsa_sign_before, 1, RSA_KEY_LENGTH_BYTES, input_file) == 0)
+		goto error_read;
+	if (fread(rsa_sign_post, 1, RSA_KEY_LENGTH_BYTES, input_file) == 0)
+		goto error_read;
+
+	// file should be at position HEADER_LEN now, start AES decryption
+	digest_before = EVP_MD_CTX_new();
+	digest_post = EVP_MD_CTX_new();
+	digest_vendor = EVP_MD_CTX_new();
+	sha512 = EVP_sha512();
+	EVP_DigestInit_ex(digest_before, sha512, NULL);
+	EVP_DigestInit_ex(digest_post, sha512, NULL);
+	EVP_DigestInit_ex(digest_vendor, sha512, NULL);
+
+	memcpy(&aes_iv, &salt, AES_BLOCK_SIZE);
+	aes_ctx = EVP_CIPHER_CTX_new();
+	EVP_DecryptInit_ex(aes_ctx, aes128, NULL, &vendor_key[0], aes_iv);
+	EVP_CIPHER_CTX_set_padding(aes_ctx, 0);
+	int outlen;
+	pad_len = payload_length_post - payload_length_before;
+
+	while (read_total < payload_length_post) {
+		if (read_total + BUFSIZE <= payload_length_post)
+			read_bytes = fread(&readbuf, 1, BUFSIZE, input_file);
+		else
+			read_bytes = fread(&readbuf, 1, payload_length_post - read_total, \
+				input_file);
+
+		read_total += read_bytes;
+
+		EVP_DigestUpdate(digest_post, &readbuf[0], read_bytes);
+
+		EVP_DecryptUpdate(aes_ctx, encbuf, &outlen, &readbuf[0], read_bytes);
+
+		// only update digest_before until payload_length_before,
+		// do not hash decrypted padding
+		if (read_total > payload_length_before) {
+			// only calc hash for data before padding
+			EVP_DigestUpdate(digest_before, &encbuf[0], read_bytes - pad_len);
+			fwrite(&encbuf[0], 1, read_bytes - pad_len, output_file);
+
+			// copy digest state, since we need another one with vendor key appended
+			EVP_MD_CTX_copy_ex(digest_vendor, digest_before);
+
+			// append vendor_key
+			EVP_DigestUpdate(digest_vendor, &vendor_key[0], AES_BLOCK_SIZE);
+		} else {
+			// calc hash for all of read_bytes
+			EVP_DigestUpdate(digest_before, &encbuf[0], read_bytes);
+			fwrite(&encbuf[0], 1, read_bytes, output_file);
+		}
+	}
+
+	fclose(input_file);
+	fclose(output_file);
+	EVP_CIPHER_CTX_free(aes_ctx);
+
+	EVP_DigestFinal_ex(digest_post, &md_post_actual[0], NULL);
+	EVP_MD_CTX_free(digest_post);
+
+	printf("\ndigest_post: ");
+	for (i = 0; i < SHA512_DIGEST_LENGTH; i++)
+		printf("%02x", md_post_actual[i]);
+
+	if (strncmp(md_post, (char *) md_post_actual, SHA512_DIGEST_LENGTH) != 0) {
+		fprintf(stderr, "SHA512 post does not match file contents.\n");
+		goto error;
+	}
+
+	EVP_DigestFinal_ex(digest_before, &md_before_actual[0], NULL);
+	EVP_MD_CTX_free(digest_before);
+
+	printf("\ndigest_before: ");
+	for (i = 0; i < SHA512_DIGEST_LENGTH; i++)
+		printf("%02x", md_before_actual[i]);
+
+	if (strncmp(md_before, (char *) md_before_actual, SHA512_DIGEST_LENGTH) != 0) {
+		fprintf(stderr, "SHA512 before does not match decrypted payload.\n");
+		goto error;
+	}
+
+	EVP_DigestFinal_ex(digest_vendor, &md_vendor_actual[0], NULL);
+	EVP_MD_CTX_free(digest_vendor);
+
+	printf("\ndigest_vendor: ");
+	for (i = 0; i < SHA512_DIGEST_LENGTH; i++)
+		printf("%02x", md_vendor_actual[i]);
+
+	if (strncmp(md_vendor, (char *) md_vendor_actual, SHA512_DIGEST_LENGTH) != 0) {
+		fprintf(stderr, "SHA512 vendor does not match decrypted payload padded" \
+			" with vendor key.\n");
+		goto error;
+	}
+
+	signing_key = PEM_read_bio_PrivateKey(rsa_private_bio, NULL, pass_cb, NULL);
+	rsa_ctx = EVP_PKEY_CTX_new(signing_key, NULL);
+	EVP_PKEY_verify_init(rsa_ctx);
+	EVP_PKEY_CTX_set_signature_md(rsa_ctx, sha512);
+
+	if (EVP_PKEY_verify(rsa_ctx, &rsa_sign_before[0], RSA_KEY_LENGTH_BYTES, \
+		&md_before_actual[0], SHA512_DIGEST_LENGTH)) {
+		printf("\nsignature before verification success");
+	} else {
+		fprintf(stderr, "Signature before verification failed.\nThe decrypted" \
+			" image file may however be flashable via bootloader recovery.\n");
+	}
+
+	if (EVP_PKEY_verify(rsa_ctx, &rsa_sign_post[0], RSA_KEY_LENGTH_BYTES, \
+		&md_post_actual[0], SHA512_DIGEST_LENGTH)) {
+		printf("\nsignature post verification success");
+	} else {
+		fprintf(stderr, "Signature post verification failed.\nThe decrypted" \
+			" image file may however be flashable via bootloader recovery.\n");
+	}
+
+	printf("\n");
+
+	return;
+
+error_read:
+	fprintf(stderr, "Error reading header fields from input file.\n");
+error:
+	fclose(input_file);
+	fclose(output_file);
+	exit(1);
+}
+
+/*
+  generate legacy vendor key for COVR-C1200, COVR-P2500, DIR-882, DIR-2660, ...
+  decrypt ciphertext key2 using aes128 with key1 and iv, write result to *vkey
+*/
+void generate_vendorkey_legacy(unsigned char *vkey)
+{
+	int outlen;
+	memcpy(&aes_iv, &iv, AES_BLOCK_SIZE);
+	aes_ctx = EVP_CIPHER_CTX_new();
+	EVP_DecryptInit_ex(aes_ctx, aes128, NULL, &key1[0], &aes_iv[0]);
+	EVP_CIPHER_CTX_set_padding(aes_ctx, 0);
+	EVP_DecryptUpdate(aes_ctx, vkey, &outlen, &key2[0], AES_BLOCK_SIZE);
+	EVP_CIPHER_CTX_free(aes_ctx);
+}
+
+/*
+  helper function for generate_vendorkey_dimgkey()
+  deinterleave input in chunks of 8 bytes according to pattern,
+  last block shorter than 8 bytes is appended in reverse order
+*/
+void deinterleave(unsigned char *enk, size_t len, unsigned char *vkey)
+{
+	unsigned char i, pattern = 0;
+
+	while (len >= INTERLEAVE_BLOCK_SIZE)
+	{
+		for (i = 0; i < INTERLEAVE_BLOCK_SIZE; i++)
+			*(vkey + i) = *(enk + interleaving_pattern[pattern][i]);
+
+		vkey += INTERLEAVE_BLOCK_SIZE;
+		enk += INTERLEAVE_BLOCK_SIZE;
+		len -= INTERLEAVE_BLOCK_SIZE;
+
+		if (pattern++ >= INTERLEAVE_BLOCK_SIZE)
+			pattern = 0;
+	}
+
+	for (i = 0; i < len; i++)
+		*(vkey + i) = *(enk + (len - i - 1));
+}
+
+/*
+  generate vendor key for COVR-X1860, DIR-X3260, ...
+  base64 decode enk, pass to deinterleave, result will be in *vkey
+*/
+void generate_vendorkey_dimgkey(const unsigned char *enk, size_t len, unsigned char *vkey)
+{
+	unsigned char *decode_buf = malloc(3 * (len / 4));
+	int outlen;
+	EVP_ENCODE_CTX *base64_ctx = EVP_ENCODE_CTX_new();
+	EVP_DecodeInit(base64_ctx);
+	EVP_DecodeUpdate(base64_ctx, decode_buf, &outlen, enk, len);
+	EVP_DecodeFinal(base64_ctx, decode_buf + outlen, &outlen);
+	EVP_ENCODE_CTX_free(base64_ctx);
+
+	// limit deinterleaving output to first 16 bytes
+	deinterleave(decode_buf, AES_BLOCK_SIZE, vkey);
+}
+
+int main(int argc, char **argv)
+{
+	if (argc < 3 || argc > 5) {
+		fprintf(stderr, "Usage:\n"
+			"\tdlink-sge-image DEVICE_MODEL infile outfile [-d: decrypt]\n\n"
+			"DEVICE_MODEL can be any of:\n"
+			"\tCOVR-C1200\n"
+			"\tCOVR-P2500\n"
+			"\tCOVR-X1860\n"
+			"\tDIR-853\n"
+			"\tDIR-867\n"
+			"\tDIR-878\n"
+			"\tDIR-882\n"
+			"\tDIR-1935\n"
+			"\tDIR-2150\n"
+			"\tDIR-X3260\n\n"
+			"Any other value will default to COVR-C1200/P2500/DIR-8xx keys\n"
+			"which may work to decrypt images for several further devices,\n"
+			"however there are currently no private keys known that would\n"
+			"allow for signing images to be used for flashing those devices.\n\n"
+			);
+		exit(1);
+	}
+
+	input_file = fopen(argv[2], "rb");
+	if (input_file == NULL) {
+		fprintf(stderr, "Input File %s could not be opened.\n", argv[2]);
+		exit(1);
+	}
+
+	output_file = fopen(argv[3], "wb");
+	if (input_file == NULL) {
+		fprintf(stderr, "Output File %s could not be opened.\n", argv[3]);
+		fclose(input_file);
+		exit(1);
+	}
+
+	aes128 = EVP_aes_128_cbc();
+
+	if (strncmp(argv[1], "COVR-X1860", 10) == 0)
+	{
+		generate_vendorkey_dimgkey(enk_covrx1860, sizeof(enk_covrx1860), &vendor_key[0]);
+		rsa_private_bio = BIO_new_mem_buf(key_covrx1860_pem, -1);
+	}
+	else if (strncmp(argv[1], "DIR-X3260", 9) == 0)
+	{
+		generate_vendorkey_dimgkey(enk_dirx3260, sizeof(enk_dirx3260), &vendor_key[0]);
+		rsa_private_bio = BIO_new_mem_buf(key_dirx3260_pem, -1);
+	}
+	else if (strncmp(argv[1], "DIR-1260", 8) == 0)
+	{
+		generate_vendorkey_legacy(&vendor_key[0]);
+		rsa_private_bio = BIO_new_mem_buf(key_dir1260_pem, -1);
+	}
+	else if (strncmp(argv[1], "DIR-2150", 8) == 0)
+	{
+		generate_vendorkey_legacy(&vendor_key[0]);
+		rsa_private_bio = BIO_new_mem_buf(key_dir2150_pem, -1);
+	}
+	else
+	{
+		/* COVR-C1200, COVR-P2500, DIR-853, DIR-867, DIR-878, DIR-882, DIR-1935 */
+		generate_vendorkey_legacy(&vendor_key[0]);
+		rsa_private_bio = BIO_new_mem_buf(key_legacy_pem, -1);
+	}
+
+	printf("\nvendor_key: ");
+	for (i = 0; i < AES_BLOCK_SIZE; i++)
+		printf("%02x", vendor_key[i]);
+
+	if (argc == 5 && strncmp(argv[4], "-d", 2) == 0)
+		image_decrypt();
+	else
+		image_encrypt();
+}
diff --git a/src/dlink-sge-image.h b/src/dlink-sge-image.h
new file mode 100644
index 0000000000..8966cc97b8
--- /dev/null
+++ b/src/dlink-sge-image.h
@@ -0,0 +1,349 @@ 
+/* SPDX-License-Identifier: GPL-2.0-only */
+
+const unsigned char key1[] =
+	"\x35\x87\x90\x03\x45\x19\xf8\xc8\x23\x5d\xb6\x49\x28\x39\xa7\x3f";
+
+const unsigned char key2[] =
+	"\xc8\xd3\x2f\x40\x9c\xac\xb3\x47\xc8\xd2\x6f\xdc\xb9\x09\x0b\x3c";
+
+const unsigned char iv[] =
+	"\x98\xc9\xd8\xf0\x13\x3d\x06\x95\xe2\xa7\x09\xc8\xb6\x96\x82\xd4";
+
+const unsigned char salt[] =
+	"\x67\xc6\x69\x73\x51\xff\x4a\xec\x29\xcd\xba\xab\xf2\xfb\xe3\x46";
+
+/*
+  enk.txt as found in GPL tarball:
+  COVR-X1860_GPL_Release/MTK7621_AX1800_BASE/vendors/COVR-X1860/imgkey/enk.txt
+*/
+const unsigned char enk_covrx1860[] =
+	"NE1oIS1lKzkkIzZkbX49KTMsMWFkJXEybjheJiN6KjIwNjgx";
+
+/*
+  enk.txt as found in GPL tarball:
+  DIR-X3260_GPL_Release/MTK7621_AX1800_BASE/vendors/DIR-X3260/imgkey/enk.txt
+*/
+const unsigned char enk_dirx3260[] =
+	"NF5yKy10JTl+bSkhNj1kTTIkI3FhIyUsJDU0czMyZmR6Jl4jMzI4KjA2Mg==";
+
+#define INTERLEAVE_BLOCK_SIZE	8
+const unsigned char interleaving_pattern[INTERLEAVE_BLOCK_SIZE][INTERLEAVE_BLOCK_SIZE] = {
+	{2, 5, 7, 4, 0, 6, 1, 3},
+	{7, 3, 2, 6, 4, 5, 1, 0},
+	{5, 1, 6, 7, 3, 0, 4, 2},
+	{0, 3, 7, 6, 5, 4, 2, 1},
+	{1, 5, 7, 0, 3, 2, 6, 4},
+	{3, 6, 2, 5, 4, 7, 1, 0},
+	{6, 0, 5, 1, 3, 4, 2, 7},
+	{4, 6, 7, 3, 2, 0, 1, 5}
+};
+
+/*
+  key.pem as found in GPL tarball, e.g.:
+  COVRP2500A1_FW101/COVRP2500_GPL_Release/package/tw-prog.priv/imgcrypt/key.pem
+  encrypted with passphrase: "12345678"
+*/
+const unsigned char key_legacy_pem[] = R"(
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-256-CBC,EAA1FD02CDBC7AA7E62AC821D47823F2
+
+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
+-----END RSA PRIVATE KEY-----
+)";
+
+/*
+  key.pem as found in GPL tarball:
+  DIR1260_GPL_Release/vendors/DIR-1260/imgkey/key.pem
+  encrypted with passphrase: "12345678"
+*/
+const unsigned char key_dir1260_pem[] = R"(
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-256-CBC,FFC890207BA9F13F6CE13F8EF7E69802
+
+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
+-----END RSA PRIVATE KEY-----
+)";
+
+/*
+  key.pem as found in GPL tarball:
+  DIR2150_GPL_Release/vendors/DIR-2150/imgkey/key.pem
+  encrypted with passphrase: "12345678"
+*/
+const unsigned char key_dir2150_pem[] = R"(
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-256-CBC,314D3C7FB69E7DDEAF084A0A7CA39069
+
+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
+-----END RSA PRIVATE KEY-----
+)";
+
+/*
+  key.pem as found in GPL tarball:
+  COVR-X1860_GPL_Release/MTK7621_AX1800_BASE/vendors/COVR-X1860/imgkey/key.pem
+  encrypted with passphrase: "12345678"
+*/
+const unsigned char key_covrx1860_pem[] = R"(
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-256-CBC,34CCF1AEF0C34EAC5FFAE6BCF81ABB8D
+
+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
+-----END RSA PRIVATE KEY-----
+)";
+
+/*
+  key.pem as found in GPL tarball, e.g.:
+  DIR-X3260_GPL_Release/MTK7621_AX1800_BASE/vendors/DIR-X3260/imgkey/key.pem
+  encrypted with passphrase: "12345678"
+*/
+const unsigned char key_dirx3260_pem[] = R"(
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-256-CBC,DDAC1FE060CB240242046BDFEEE17E44
+
+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
+-----END RSA PRIVATE KEY-----
+)";