/*
 * decrypt_images.c — WeChat V2 image batch decryptor (multi-key)
 *
 * Decrypts all V2 encrypted .dat files in the WeChat image cache.
 * Supports multiple keys via image_keys.json (CT block → AES key mapping).
 *
 * V2 format:
 *   [15B header] [AES-128-ECB ciphertext] [XOR encrypted tail]
 *   Header: \x07\x08V2\x08\x07 (6B) + aes_size:u32LE + xor_size:u32LE + 1B pad
 *   AES region: ceil(aes_size/16)*16 bytes of AES-128-ECB ciphertext
 *   XOR tail: xor_size bytes, each XOR'd with a single-byte key
 *
 * Build:
 *   cc -O3 -o decrypt_images decrypt_images.c -framework Security
 *
 * Usage:
 *   ./decrypt_images                                   # auto from config + image_keys.json
 *   ./decrypt_images <key_hex> <image_dir> <out_dir>   # single-key manual
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <dirent.h>
#include <sys/stat.h>
#include <errno.h>
#include <CommonCrypto/CommonCryptor.h>

#define MAX_PATH    4096
#define V2_MAGIC    "\x07\x08V2\x08\x07"
#define V2_MAGIC_LEN 6
#define HEADER_SIZE 15
#define MAX_KEYS    4096

/* ---- Key mapping: CT block hex → AES key ---- */
typedef struct {
    unsigned char ct[16];   /* CT block 0 pattern */
    unsigned char key[16];  /* AES key for this pattern */
} key_map_t;

static key_map_t key_map[MAX_KEYS];
static int       n_keys = 0;

/* ---- Utility ---- */

static int hex2bytes(const char *hex, unsigned char *out, int maxlen) {
    int len = 0;
    while (*hex && *(hex + 1) && len < maxlen) {
        unsigned int b;
        if (sscanf(hex, "%2x", &b) != 1) break;
        out[len++] = (unsigned char)b;
        hex += 2;
    }
    return len;
}

/* Minimal JSON string extractor (for simple unescaped string values only). */
static int json_get_string(const char *json, const char *key,
                           char *value, int maxlen) {
    char pattern[256];
    snprintf(pattern, sizeof(pattern), "\"%s\"", key);
    const char *p = strstr(json, pattern);
    if (!p) return 0;
    p = strchr(p + strlen(pattern), '"');
    if (!p) return 0;
    p++;
    const char *end = strchr(p, '"');
    if (!end) return 0;
    int len = (int)(end - p);
    if (len >= maxlen) len = maxlen - 1;
    memcpy(value, p, len);
    value[len] = '\0';
    return 1;
}

/* Load image_keys.json: { "ct_hex": "key_hex", ... } */
static int load_key_map(const char *path) {
    FILE *f = fopen(path, "r");
    if (!f) return 0;
    fseek(f, 0, SEEK_END);
    long sz = ftell(f);
    if (sz <= 0) { fclose(f); return 0; }
    fseek(f, 0, SEEK_SET);
    char *json = malloc((size_t)sz + 1);
    if (!json) { fclose(f); return 0; }
    size_t rd = fread(json, 1, (size_t)sz, f);
    if (rd != (size_t)sz) {
        fclose(f);
        free(json);
        return 0;
    }
    json[rd] = '\0';
    fclose(f);

    /* Simple parser: find all "32hex": "32hex" pairs */
    const char *p = json;
    int warned_capacity = 0;
    while ((p = strchr(p, '"')) != NULL) {
        if (n_keys >= MAX_KEYS) {
            if (!warned_capacity) {
                fprintf(stderr, "Warning: image_keys.json exceeds MAX_KEYS=%d, extra keys ignored\n",
                        MAX_KEYS);
                warned_capacity = 1;
            }
            break;
        }

        p++;
        const char *end = strchr(p, '"');
        if (!end) break;
        int klen = (int)(end - p);
        if (klen != 32) { p = end + 1; continue; }

        char ct_hex[33];
        memcpy(ct_hex, p, 32);
        ct_hex[32] = '\0';
        const char *colon = end + 1;
        while (*colon == ' ' || *colon == '\t' || *colon == '\r' || *colon == '\n')
            colon++;
        if (*colon != ':') { p = end + 1; continue; }
        p = colon + 1;

        /* Find next quoted string (the value) */
        p = strchr(p, '"');
        if (!p) break;
        p++;
        end = strchr(p, '"');
        if (!end) break;
        int vlen = (int)(end - p);
        if (vlen != 32) { p = end + 1; continue; }

        char key_hex[33];
        memcpy(key_hex, p, 32);
        key_hex[32] = '\0';
        p = end + 1;

        if (hex2bytes(ct_hex, key_map[n_keys].ct, 16) != 16 ||
            hex2bytes(key_hex, key_map[n_keys].key, 16) != 16) {
            continue;
        }
        n_keys++;
    }
    free(json);
    return n_keys;
}

/* Find AES key for a given CT block */
static const unsigned char *find_key_for_ct(const unsigned char *ct) {
    for (int i = 0; i < n_keys; i++)
        if (memcmp(key_map[i].ct, ct, 16) == 0) return key_map[i].key;
    return NULL;
}

/* Create directory and parents */
static void mkdirs(const char *path) {
    char tmp[MAX_PATH];
    snprintf(tmp, sizeof(tmp), "%s", path);
    for (char *p = tmp + 1; *p; p++) {
        if (*p == '/') {
            *p = '\0';
            mkdir(tmp, 0755);
            *p = '/';
        }
    }
    mkdir(tmp, 0755);
}

static int has_parent_segment(const char *path) {
    if (!path || !path[0]) return 1;
    if (path[0] == '/' || path[0] == '\\') return 1;

    const char *p = path;
    while (*p) {
        while (*p == '/' || *p == '\\') p++;
        if (!*p) break;
        const char *seg = p;
        while (*p && *p != '/' && *p != '\\') p++;
        if ((p - seg) == 2 && seg[0] == '.' && seg[1] == '.') return 1;
    }
    return 0;
}

/* Detect image type from magic bytes */
static const char *detect_ext(const unsigned char *data, size_t len) {
    if (len < 4) return ".bin";
    if (data[0] == 0xFF && data[1] == 0xD8) return ".jpg";
    if (data[0] == 0x89 && data[1] == 0x50 &&
        data[2] == 0x4E && data[3] == 0x47) return ".png";
    if (data[0] == 'G' && data[1] == 'I' &&
        data[2] == 'F' && data[3] == '8') return ".gif";
    if (data[0] == 'R' && data[1] == 'I' &&
        data[2] == 'F' && data[3] == 'F') return ".webp";
    if (data[0] == 0x00 && data[1] == 0x00 &&
        data[2] == 0x00 && (data[3] == 0x18 || data[3] == 0x1C ||
        data[3] == 0x20 || data[3] == 0x14)) return ".mp4";
    return ".bin";
}

/* Auto-detect XOR key */
static unsigned char detect_xor_key(const unsigned char *xor_data, size_t xor_size) {
    if (xor_size == 0) return 0;
    unsigned char candidates[] = {0x80, 0xDC, 0x00};
    for (int i = 0; i < (int)(sizeof(candidates)/sizeof(candidates[0])); i++) {
        /* We want a candidate that doesn't produce a leading NUL byte after XOR. */
        unsigned char test = xor_data[0] ^ candidates[i];
        if (test != 0x00 || candidates[i] == 0x00)
            return candidates[i];
    }
    return 0x80;
}

/* ---- Decrypt one V2 file ---- */

static int decrypt_v2_file(const char *input_path, const char *output_dir,
                           const char *rel_path, const unsigned char *aes_key,
                           unsigned char xor_key, int auto_xor,
                           int *out_xor_detected) {
    FILE *fin = fopen(input_path, "rb");
    if (!fin) return -1;

    unsigned char header[HEADER_SIZE];
    if (fread(header, 1, HEADER_SIZE, fin) != HEADER_SIZE) {
        fclose(fin); return -1;
    }
    if (memcmp(header, V2_MAGIC, V2_MAGIC_LEN) != 0) {
        fclose(fin); return -2;
    }

    uint32_t aes_size, xor_size;
    memcpy(&aes_size, header + 6, 4);
    memcpy(&xor_size, header + 10, 4);

    if ((uint64_t)aes_size > 100u * 1024u * 1024u ||
        (uint64_t)xor_size > 100u * 1024u * 1024u) {
        fclose(fin);
        return -6;
    }

    /* PKCS7: when aes_size is already 16-byte aligned, an extra 16-byte
     * padding block is present in the ciphertext */
    size_t aes_ct_size = (aes_size % 16 == 0)
        ? (size_t)aes_size + 16
        : ((size_t)aes_size + 15) / 16 * 16;

    /* Get total file size and validate header claims fit within it */
    long cur_pos = ftell(fin);
    fseek(fin, 0, SEEK_END);
    long file_size = ftell(fin);
    fseek(fin, cur_pos, SEEK_SET);

    if ((long)aes_ct_size + (long)xor_size > file_size - HEADER_SIZE) {
        fclose(fin);
        return -6;  /* header claims more data than file contains */
    }

    unsigned char *aes_ct = malloc(aes_ct_size);
    if (!aes_ct) { fclose(fin); return -1; }
    size_t rd = fread(aes_ct, 1, aes_ct_size, fin);
    if (rd != aes_ct_size) {
        free(aes_ct);
        fclose(fin);
        return -8;
    }

    /* V2 may have unencrypted raw_data between AES and XOR sections */
    long raw_data_size = file_size - HEADER_SIZE - (long)aes_ct_size - (long)xor_size;
    if (raw_data_size < 0) raw_data_size = 0;

    unsigned char *raw_data = NULL;
    if (raw_data_size > 0) {
        raw_data = malloc((size_t)raw_data_size);
        if (!raw_data) { free(aes_ct); fclose(fin); return -1; }
        rd = fread(raw_data, 1, (size_t)raw_data_size, fin);
        if (rd != (size_t)raw_data_size) {
            free(aes_ct); free(raw_data); fclose(fin); return -8;
        }
    }

    unsigned char *xor_data = NULL;
    if (xor_size > 0) {
        xor_data = malloc(xor_size);
        if (!xor_data) { free(aes_ct); free(raw_data); fclose(fin); return -1; }
        rd = fread(xor_data, 1, xor_size, fin);
        if (rd != xor_size) {
            free(aes_ct); free(raw_data); free(xor_data);
            fclose(fin); return -8;
        }
    }
    fclose(fin);

    /* Try multi-key lookup (image_keys.json) first, then fall back to provided key */
    if (aes_ct_size >= 16) {
        const unsigned char *mk = find_key_for_ct(aes_ct);
        if (mk) aes_key = mk;
    }
    if (!aes_key) { free(aes_ct); free(raw_data); free(xor_data); return -5; }

    unsigned char *aes_pt = malloc(aes_ct_size);
    if (!aes_pt) { free(aes_ct); free(raw_data); free(xor_data); return -1; }

    size_t moved = 0;
    CCCryptorStatus st = CCCrypt(
        kCCDecrypt, kCCAlgorithmAES128, kCCOptionECBMode,
        aes_key, 16, NULL,
        aes_ct, aes_ct_size, aes_pt, aes_ct_size, &moved);
    free(aes_ct);

    if (st != kCCSuccess) {
        free(aes_pt); free(raw_data); free(xor_data); return -3;
    }

    if (auto_xor && xor_data && xor_size > 0) {
        xor_key = detect_xor_key(xor_data, xor_size);
        if (out_xor_detected) *out_xor_detected = xor_key;
    }

    if (xor_data && xor_size > 0) {
        for (uint32_t i = 0; i < xor_size; i++)
            xor_data[i] ^= xor_key;
    }

    const char *ext = detect_ext(aes_pt, aes_size);

    /* Skip unrecognized formats — avoids writing garbage .bin files */
    if (strcmp(ext, ".bin") == 0) {
        free(aes_pt); free(raw_data); free(xor_data);
        return -9; /* unrecognized image type */
    }

    char out_path[MAX_PATH];
    char rel_noext[MAX_PATH];
    snprintf(rel_noext, sizeof(rel_noext), "%s", rel_path);
    char *dot = strrchr(rel_noext, '.');
    if (dot) *dot = '\0';
    if (has_parent_segment(rel_noext)) {
        free(aes_pt); free(raw_data); free(xor_data);
        return -7;
    }
    snprintf(out_path, sizeof(out_path), "%s/%s%s", output_dir, rel_noext, ext);

    /* Skip if already decrypted */
    struct stat st_out;
    if (stat(out_path, &st_out) == 0 && st_out.st_size > 0) {
        free(aes_pt); free(raw_data); free(xor_data);
        return 1; /* already exists */
    }

    char parent[MAX_PATH];
    snprintf(parent, sizeof(parent), "%s", out_path);
    char *last_slash = strrchr(parent, '/');
    if (last_slash) { *last_slash = '\0'; mkdirs(parent); }

    FILE *fout = fopen(out_path, "wb");
    if (!fout) { free(aes_pt); free(raw_data); free(xor_data); return -4; }

    fwrite(aes_pt, 1, aes_size, fout);
    if (raw_data && raw_data_size > 0) fwrite(raw_data, 1, (size_t)raw_data_size, fout);
    if (xor_data && xor_size > 0) fwrite(xor_data, 1, xor_size, fout);

    fclose(fout);
    free(aes_pt);
    free(raw_data);
    free(xor_data);
    return 0;
}

/* ---- Directory walking ---- */

typedef struct {
    const unsigned char *fallback_key; /* single key from config.json (or NULL) */
    int multi_key;                     /* 1 if using image_keys.json */
    unsigned char xor_key;
    int auto_xor;
    const char *output_dir;
    const char *base_dir;
    int success;
    int skipped;
    int existed;                       /* already decrypted */
    int no_key;                        /* V2 files with no matching key */
    int failed;
} walk_ctx;

static void walk_dir(const char *dir, walk_ctx *ctx) {
    DIR *d = opendir(dir);
    if (!d) return;

    struct dirent *ent;
    while ((ent = readdir(d))) {
        if (ent->d_name[0] == '.') continue;

        char path[MAX_PATH];
        snprintf(path, sizeof(path), "%s/%s", dir, ent->d_name);

        struct stat st;
        if (lstat(path, &st) != 0) continue;
        if (S_ISLNK(st.st_mode)) continue;

        if (S_ISDIR(st.st_mode)) {
            walk_dir(path, ctx);
        } else if (S_ISREG(st.st_mode)) {
            size_t nlen = strlen(ent->d_name);
            if (nlen < 5 || strcmp(ent->d_name + nlen - 4, ".dat") != 0)
                continue;

            const char *rel = path + strlen(ctx->base_dir);
            if (*rel == '/') rel++;

            int xor_detected = -1;
            /* In multi-key mode, pass fallback_key — decrypt_v2_file tries
             * image_keys.json lookup first, falls back to this key if provided */
            const unsigned char *key = ctx->fallback_key;
            int ret = decrypt_v2_file(path, ctx->output_dir, rel,
                                       key, ctx->xor_key,
                                       ctx->auto_xor, &xor_detected);

            if (ret == 0) {
                ctx->success++;
                if (ctx->auto_xor && xor_detected >= 0) {
                    ctx->xor_key = (unsigned char)xor_detected;
                    ctx->auto_xor = 0;
                    printf("  Auto-detected XOR key: 0x%02X\n", ctx->xor_key);
                }
                if (ctx->success <= 5 || ctx->success % 1000 == 0) {
                    printf("  [%d] %s\n", ctx->success, rel);
                }
            } else if (ret == 1) {
                ctx->existed++;
            } else if (ret == -2) {
                ctx->skipped++;
            } else if (ret == -5) {
                ctx->no_key++;
            } else {
                ctx->failed++;
                if (ctx->failed <= 5)
                    printf("  FAIL(%d): %s\n", ret, rel);
            }
        }
    }
    closedir(d);
}

/* ---- Main ---- */

int main(int argc, char *argv[]) {
    unsigned char aes_key[16];
    char image_dir[MAX_PATH] = "";
    char output_dir[MAX_PATH] = "";
    char key_hex[64] = "";
    int have_single_key = 0;

    printf("=== WeChat V2 Image Decryptor ===\n\n");

    /* Determine exe directory for config file lookup */
    char exe_dir[MAX_PATH] = ".";
    const char *last_slash = strrchr(argv[0], '/');
    if (last_slash) {
        int len = (int)(last_slash - argv[0]);
        snprintf(exe_dir, sizeof(exe_dir), "%.*s", len, argv[0]);
    }

    if (argc >= 4) {
        /* Manual single-key mode */
        strncpy(key_hex, argv[1], sizeof(key_hex) - 1);
        key_hex[sizeof(key_hex) - 1] = '\0';
        strncpy(image_dir, argv[2], sizeof(image_dir) - 1);
        image_dir[sizeof(image_dir) - 1] = '\0';
        strncpy(output_dir, argv[3], sizeof(output_dir) - 1);
        output_dir[sizeof(output_dir) - 1] = '\0';
        have_single_key = (key_hex[0] != '\0');
    } else {
        /* Load image_keys.json first (multi-key) */
        char keys_path[MAX_PATH];
        snprintf(keys_path, sizeof(keys_path), "%s/image_keys.json", exe_dir);
        int loaded = load_key_map(keys_path);
        if (loaded > 0)
            printf("Loaded %d key mappings from %s\n", loaded, keys_path);

        /* Read config.json for paths (and fallback single key) */
        char cfg_path[MAX_PATH];
        snprintf(cfg_path, sizeof(cfg_path), "%s/config.json", exe_dir);
        FILE *cf = fopen(cfg_path, "r");
        if (!cf) {
            fprintf(stderr, "ERROR: Cannot open %s\n", cfg_path);
            return 1;
        }

        fseek(cf, 0, SEEK_END);
        long sz = ftell(cf);
        if (sz <= 0) { fclose(cf); return 1; }
        fseek(cf, 0, SEEK_SET);
        char *json = malloc((size_t)sz + 1);
        if (!json) { fclose(cf); return 1; }
        size_t rd = fread(json, 1, (size_t)sz, cf);
        if (rd != (size_t)sz) {
            free(json);
            fclose(cf);
            return 1;
        }
        json[sz] = '\0';
        fclose(cf);

        if (json_get_string(json, "image_key", key_hex, sizeof(key_hex)) &&
            key_hex[0] != '\0')
            have_single_key = 1;
        else
            have_single_key = 0;

        char db_dir[MAX_PATH] = "";
        json_get_string(json, "db_dir", db_dir, sizeof(db_dir));

        char out_rel[MAX_PATH] = "decrypted_images";
        json_get_string(json, "decrypted_images_dir", out_rel, sizeof(out_rel));
        if (out_rel[0] == '/')
            strncpy(output_dir, out_rel, sizeof(output_dir) - 1);
        else
            snprintf(output_dir, sizeof(output_dir), "%s/%s", exe_dir, out_rel);
        output_dir[sizeof(output_dir) - 1] = '\0';

        if (db_dir[0]) {
            char *s = strrchr(db_dir, '/');
            if (!s) s = strrchr(db_dir, '\\');
            if (s) {
                int plen = (int)(s - db_dir);
                snprintf(image_dir, sizeof(image_dir),
                         "%.*s/msg", plen, db_dir);
            }
        }
        free(json);
    }

    /* Parse single key if available (used as fallback or sole key) */
    if (have_single_key && key_hex[0]) {
        if (hex2bytes(key_hex, aes_key, 16) == 16) {
            /* If no image_keys.json loaded, add single key to key_map
             * by discovering its CT block at runtime */
        } else {
            have_single_key = 0;
        }
    }

    if (n_keys == 0 && !have_single_key) {
        fprintf(stderr, "ERROR: No keys available.\n");
        fprintf(stderr, "Run find_image_key first, or set image_key in config.json\n");
        return 1;
    }

    /* Auto-detect: scan ~/Library/Containers/com.tencent.xinWeChat */
    if (image_dir[0] == '\0') {
        const char *home = getenv("HOME");
        if (!home) home = "/Users";
        char base[MAX_PATH];
        snprintf(base, sizeof(base),
                 "%s/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files",
                 home);
        DIR *d = opendir(base);
        if (d) {
            struct dirent *ent;
            while ((ent = readdir(d))) {
                if (ent->d_name[0] == '.') continue;
                char candidate[MAX_PATH];
                snprintf(candidate, sizeof(candidate), "%s/%s/msg", base, ent->d_name);
                struct stat st2;
                if (stat(candidate, &st2) == 0 && S_ISDIR(st2.st_mode)) {
                    strncpy(image_dir, candidate, sizeof(image_dir) - 1);
                    printf("Auto-detected image directory:\n  %s\n\n", image_dir);
                    break;
                }
            }
            closedir(d);
        }
    }

    if (image_dir[0] == '\0') {
        fprintf(stderr, "ERROR: Cannot determine image directory.\n");
        fprintf(stderr, "Tried: command line, config.json, auto-detect.\n");
        fprintf(stderr, "Set db_dir in config.json or pass image_dir as argument.\n");
        return 1;
    }

    printf("Mode:      %s\n", n_keys > 0 ? "multi-key" : "single-key");
    if (n_keys > 0) printf("Keys:      %d pattern→key mappings\n", n_keys);
    if (have_single_key) printf("Fallback:  %s\n", key_hex);
    printf("Image dir: %s\n", image_dir);
    printf("Output:    %s\n\n", output_dir);

    mkdirs(output_dir);

    walk_ctx ctx = {
        .fallback_key = have_single_key ? aes_key : NULL,
        .multi_key    = (n_keys > 0),
        .xor_key      = 0,
        .auto_xor     = 1,
        .output_dir   = output_dir,
        .base_dir     = image_dir,
        .success      = 0,
        .skipped      = 0,
        .existed      = 0,
        .no_key       = 0,
        .failed       = 0,
    };

    walk_dir(image_dir, &ctx);

    printf("\n==================================================\n");
    printf("Results:\n");
    printf("  Decrypted:  %d\n", ctx.success);
    printf("  Existed:    %d (already decrypted, skipped)\n", ctx.existed);
    printf("  No key:     %d (run find_image_key to discover more keys)\n", ctx.no_key);
    printf("  Skipped:    %d (non-V2)\n", ctx.skipped);
    printf("  Failed:     %d\n", ctx.failed);
    printf("Output: %s\n", output_dir);
    printf("==================================================\n");

    return (ctx.success > 0) ? 0 : 1;
}
