/*
 * find_image_key.c — WeChat V2 image key continuous scanner (macOS)
 *
 * Discovers all unique V2 encryption patterns from the image cache,
 * then continuously scans WeChat process memory to find AES keys.
 * User just keeps browsing images in WeChat — the scanner catches
 * keys as they transiently appear in memory.
 *
 * Uses multi-block CCCrypt: one key setup decrypts ALL unsolved
 * patterns in a single call (~1.5 min per full scan with 20 patterns).
 *
 * Build:
 *   cc -O3 -o find_image_key find_image_key.c -framework Security
 *
 * Usage:
 *   sudo ./find_image_key              # auto-discover from config.json
 *   sudo ./find_image_key <image_dir>  # explicit image directory
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <signal.h>
#include <time.h>
#include <dirent.h>
#include <sys/stat.h>
#include <sys/sysctl.h>
#include <mach/mach.h>
#include <mach/mach_vm.h>
#include <unistd.h>
#include <CommonCrypto/CommonCryptor.h>

#define MAX_PATH    4096
#define MAX_PATTERNS 8192
#define V2_MAGIC    "\x07\x08V2\x08\x07"
#define V2_MAGIC_LEN 6
#define REGION_MAX  (200 * 1024 * 1024)
#define DEEP_PRIORITY_MAX 10  /* byte-by-byte scan for top N unsolved patterns */

/* ---- Strict image magic detection (16 bytes available from decrypted block) ---- */
static int is_image_magic(const unsigned char *pt) {
    if (pt[0] == 0xFF && pt[1] == 0xD8 && pt[2] == 0xFF &&
        pt[3] >= 0xC0 && pt[3] != 0xFF) {
        /* JFIF: verify "JF" at offset 6 */
        if (pt[3] == 0xE0) return (pt[6] == 'J' && pt[7] == 'F');
        /* EXIF: verify "Ex" at offset 6 */
        if (pt[3] == 0xE1) return (pt[6] == 'E' && pt[7] == 'x');
        /* Other markers: verify length field is sane (big-endian, 2..32767) */
        uint16_t len = ((uint16_t)pt[4] << 8) | pt[5];
        return (len >= 2 && len < 0x8000);
    }
    /* PNG: full 8-byte signature */
    if (pt[0]==0x89 && pt[1]==0x50 && pt[2]==0x4E && pt[3]==0x47 &&
        pt[4]==0x0D && pt[5]==0x0A && pt[6]==0x1A && pt[7]==0x0A) return 1;
    /* GIF: "GIF89a" or "GIF87a" */
    if (pt[0]=='G' && pt[1]=='I' && pt[2]=='F' && pt[3]=='8' &&
        (pt[4]=='9' || pt[4]=='7') && pt[5]=='a') return 1;
    /* WebP: "RIFF....WEBP" */
    if (pt[0]=='R' && pt[1]=='I' && pt[2]=='F' && pt[3]=='F' &&
        pt[8]=='W' && pt[9]=='E' && pt[10]=='B' && pt[11]=='P') return 1;
    return 0;
}

/* ---- Pattern tracking ---- */
typedef struct {
    unsigned char ct[16];           /* CT block 0 (first 16 encrypted bytes) */
    unsigned char key[16];          /* found AES key */
    int           solved;
    int           file_count;       /* how many .dat files use this pattern */
    char          sample_path[MAX_PATH];
} pattern_t;

static pattern_t patterns[MAX_PATTERNS];
static int       npatterns = 0;
static int       total_v2_files = 0;

/* ---- Rejected key blacklist (false positives) ---- */
#define MAX_REJECTED 256
static unsigned char rejected_keys[MAX_REJECTED][16];
static int n_rejected = 0;

static int is_rejected(const unsigned char *key) {
    for (int i = 0; i < n_rejected; i++)
        if (memcmp(rejected_keys[i], key, 16) == 0) return 1;
    return 0;
}
static void add_rejected(const unsigned char *key) {
    if (n_rejected < MAX_REJECTED && !is_rejected(key)) {
        memcpy(rejected_keys[n_rejected], key, 16);
        n_rejected++;
    }
}

/* ---- Global scan mode ---- */
static int g_deep_mode = 0;

/* ---- Graceful shutdown ---- */
static volatile sig_atomic_t stop_flag = 0;
static void sigint_handler(int sig) { (void)sig; stop_flag = 1; }

/* ---- Utility ---- */
static void bytes2hex(const unsigned char *d, int n, char *out) {
    for (int i = 0; i < n; i++) sprintf(out + i*2, "%02x", d[i]);
    out[n*2] = '\0';
}
static int hex2bytes(const char *h, unsigned char *o, int max) {
    int n = 0;
    while (n < max) {
        if (!h[0] || !h[1]) return 0;
        if (!((h[0] >= '0' && h[0] <= '9') || (h[0] >= 'a' && h[0] <= 'f') ||
              (h[0] >= 'A' && h[0] <= 'F'))) return 0;
        if (!((h[1] >= '0' && h[1] <= '9') || (h[1] >= 'a' && h[1] <= 'f') ||
              (h[1] >= 'A' && h[1] <= 'F'))) return 0;

        unsigned int b = 0;
        if (sscanf(h, "%2x", &b) != 1) return 0;
        o[n++] = (unsigned char)b; h += 2;
    }
    return n;
}

/* Minimal JSON string extractor */
static int json_get_string(const char *json, const char *key,
                           char *val, int maxlen) {
    char pat[256];
    snprintf(pat, sizeof(pat), "\"%s\"", key);
    const char *p = strstr(json, pat);
    if (!p) return 0;
    p = strchr(p + strlen(pat), '"');
    if (!p) return 0;
    p++;
    const char *end = strchr(p, '"');
    if (!end || (int)(end - p) >= maxlen) return 0;
    memcpy(val, p, end - p);
    val[end - p] = '\0';
    return 1;
}

/* ---- Pattern discovery ---- */
static int find_pattern_index(const unsigned char *ct) {
    for (int i = 0; i < npatterns; i++)
        if (memcmp(patterns[i].ct, ct, 16) == 0) return i;
    return -1;
}

static void discover_dir(const char *dir) {
    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)) {
            discover_dir(path);
            continue;
        }
        if (!S_ISREG(st.st_mode)) continue;
        size_t nlen = strlen(ent->d_name);
        if (nlen < 5 || strcmp(ent->d_name + nlen - 4, ".dat") != 0) continue;

        FILE *f = fopen(path, "rb");
        if (!f) continue;
        unsigned char hdr[31];
        size_t rd = fread(hdr, 1, 31, f);
        fclose(f);
        if (rd < 31 || memcmp(hdr, V2_MAGIC, V2_MAGIC_LEN) != 0) continue;

        unsigned char *ct = hdr + 15;
        total_v2_files++;
        int idx = find_pattern_index(ct);
        if (idx >= 0) {
            patterns[idx].file_count++;
        } else if (npatterns < MAX_PATTERNS) {
            memcpy(patterns[npatterns].ct, ct, 16);
            patterns[npatterns].file_count = 1;
            patterns[npatterns].solved = 0;
            strncpy(patterns[npatterns].sample_path, path,
                    sizeof(patterns[npatterns].sample_path) - 1);
            patterns[npatterns].sample_path[sizeof(patterns[npatterns].sample_path) - 1] = '\0';
            npatterns++;
        }
    }
    closedir(d);
}

/* Sort patterns by file_count descending */
static int cmp_patterns(const void *a, const void *b) {
    return ((pattern_t*)b)->file_count - ((pattern_t*)a)->file_count;
}

/* ---- Process discovery ---- */
static int get_wechat_pids(pid_t *pids, int max) {
    int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0};
    size_t sz = 0;
    if (sysctl(mib, 4, NULL, &sz, NULL, 0) != KERN_SUCCESS || sz == 0)
        return 0;

    size_t alloc_sz = sz + (sz >> 2);
    struct kinfo_proc *procs = malloc(alloc_sz);
    if (!procs) return 0;

    if (sysctl(mib, 4, procs, &alloc_sz, NULL, 0) != KERN_SUCCESS) {
        free(procs);
        return 0;
    }

    int n = (int)(alloc_sz / sizeof(struct kinfo_proc)), cnt = 0;
    for (int i = 0; i < n && cnt < max; i++)
        if (strstr(procs[i].kp_proc.p_comm, "WeChat"))
            pids[cnt++] = procs[i].kp_proc.p_pid;
    free(procs);
    return cnt;
}

/* ---- Verification: decrypt sample file, validate JPEG marker chain ---- */

/* Validate JPEG structure: check marker chain (SOI → markers → SOS/EOI) */
static int verify_jpeg_chain(const unsigned char *data, size_t len) {
    if (len < 4 || data[0] != 0xFF || data[1] != 0xD8) return 0;
    size_t pos = 2;
    int markers = 0;
    while (pos + 4 <= len) {
        if (data[pos] != 0xFF) return markers >= 2;
        unsigned char m = data[pos + 1];
        /* Skip fill bytes (FF FF...) */
        if (m == 0xFF) { pos++; continue; }
        if (m == 0x00) return 0; /* stuffed byte outside scan = invalid */
        if (m == 0xD9) return markers >= 1; /* EOI */
        if (m == 0xDA) return markers >= 1; /* SOS = scan data follows */
        if (m < 0xC0) return 0;
        uint16_t mlen = ((uint16_t)data[pos+2] << 8) | data[pos+3];
        if (mlen < 2) return 0;
        pos += 2 + mlen;
        markers++;
    }
    /* Ran out of data (first marker spans past AES region): accept if >= 1 valid marker */
    return markers >= 1;
}

/* Validate PNG: 8-byte sig + IHDR chunk */
static int verify_png_chain(const unsigned char *data, size_t len) {
    static const unsigned char sig[8] = {0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A};
    if (len < 24 || memcmp(data, sig, 8) != 0) return 0;
    /* IHDR chunk at offset 8: length(4) + "IHDR"(4) + data(13) + CRC(4) */
    return (data[12]=='I' && data[13]=='H' && data[14]=='D' && data[15]=='R');
}

static int verify_key(int pat_idx) {
    pattern_t *p = &patterns[pat_idx];
    FILE *f = fopen(p->sample_path, "rb");
    if (!f) return 1; /* can't verify, assume ok */

    unsigned char hdr[15];
    if (fread(hdr, 1, 15, f) != 15) { fclose(f); return 1; }
    uint32_t aes_size;
    memcpy(&aes_size, hdr + 6, 4);
    /* PKCS7: extra padding block when aes_size is 16-byte aligned */
    uint32_t ct_size = (aes_size % 16 == 0)
        ? aes_size + 16
        : ((aes_size + 15) / 16) * 16;
    if (ct_size > 10 * 1024 * 1024) { fclose(f); return 1; }

    unsigned char *ct = malloc(ct_size);
    size_t rd = fread(ct, 1, ct_size, f);
    fclose(f);
    if (rd < ct_size) { free(ct); return 1; }

    unsigned char *pt = malloc(ct_size);
    size_t moved;
    CCCryptorStatus st = CCCrypt(kCCDecrypt, kCCAlgorithmAES128,
        kCCOptionECBMode, p->key, 16, NULL,
        ct, ct_size, pt, ct_size, &moved);
    free(ct);

    if (st != kCCSuccess || moved < 16) { free(pt); return 0; }

    /* Deep validation based on image type */
    int ok = 0;
    if (pt[0] == 0xFF && pt[1] == 0xD8)
        ok = verify_jpeg_chain(pt, moved);
    else if (pt[0] == 0x89 && pt[1] == 0x50)
        ok = verify_png_chain(pt, moved);
    else if (pt[0] == 'G' && pt[1] == 'I' && pt[2] == 'F')
        ok = (moved >= 6 && pt[3] == '8' && (pt[4]=='9'||pt[4]=='7') && pt[5]=='a');
    else if (pt[0] == 'R' && pt[1] == 'I')
        ok = (moved >= 12 && pt[8]=='W' && pt[9]=='E' && pt[10]=='B' && pt[11]=='P');

    free(pt);
    return ok;
}

/* ---- Memory scanning ---- */

/*
 * Multi-block scan: for each candidate key, decrypt ALL unsolved
 * CT blocks in one CCCrypt call (ECB processes blocks independently).
 */
static int g_task_fail_warned = 0;

static int scan_pid(pid_t pid) {
    mach_port_t task;
    kern_return_t kr = task_for_pid(mach_task_self(), pid, &task);
    if (kr != KERN_SUCCESS) {
        if (!g_task_fail_warned) {
            g_task_fail_warned = 1;
            fprintf(stderr,
                "  WARNING: task_for_pid(%d) failed (kr=%d).\n"
                "  Cannot read WeChat memory. Checklist:\n"
                "    1. Run with sudo\n"
                "    2. Enable Developer Mode: Settings > Privacy & Security > Developer Mode\n"
                "    3. Grant Terminal Full Disk Access: Settings > Privacy & Security > Full Disk Access\n"
                "    4. If still failing, try: sudo DevToolsSecurity -enable\n"
                "    5. Last resort: disable SIP (boot to Recovery, run: csrutil disable)\n",
                pid, kr);
        }
        return 0;
    }

    /* Build batch CT buffer for unsolved patterns */
    int unsolved_idx[MAX_PATTERNS];
    int n_unsolved = 0;
    for (int i = 0; i < npatterns; i++)
        if (!patterns[i].solved) unsolved_idx[n_unsolved++] = i;
    if (n_unsolved == 0) {
        mach_port_deallocate(mach_task_self(), task);
        return 0;
    }

    unsigned char *batch_ct = malloc(n_unsolved * 16);
    unsigned char *batch_pt = malloc(n_unsolved * 16);
    if (!batch_ct || !batch_pt) {
        free(batch_ct);
        free(batch_pt);
        mach_port_deallocate(mach_task_self(), task);
        return 0;
    }
    for (int i = 0; i < n_unsolved; i++)
        memcpy(batch_ct + i*16, patterns[unsolved_idx[i]].ct, 16);

    mach_vm_address_t addr = 0;
    mach_vm_size_t rsize;
    vm_region_basic_info_data_64_t info;
    mach_msg_type_number_t count;
    mach_port_t obj = MACH_PORT_NULL;

    long regions = 0, found_this_pid = 0;
    long long total_bytes = 0, tests = 0;

    while (!stop_flag) {
        count = VM_REGION_BASIC_INFO_COUNT_64;
        kr = mach_vm_region(task, &addr, &rsize, VM_REGION_BASIC_INFO_64,
                            (vm_region_info_t)&info, &count, &obj);
        if (kr != KERN_SUCCESS) break;
        regions++;
        if (obj != MACH_PORT_NULL) {
            mach_port_deallocate(mach_task_self(), obj);
            obj = MACH_PORT_NULL;
        }

        if ((info.protection & VM_PROT_READ) && rsize > 0 && rsize < REGION_MAX) {
            vm_offset_t data;
            mach_msg_type_number_t data_cnt;
            kr = mach_vm_read(task, addr, rsize, &data, &data_cnt);
            if (kr == KERN_SUCCESS) {
                unsigned char *buf = (unsigned char *)data;
                total_bytes += data_cnt;

                /* Method 1: every 16-byte aligned position (raw binary keys) */
                for (mach_msg_type_number_t j = 0;
                     j + 16 <= data_cnt && !stop_flag; j += 16) {
                    tests++;
                    size_t moved;
                    CCCryptorStatus st = CCCrypt(
                        kCCDecrypt, kCCAlgorithmAES128, kCCOptionECBMode,
                        buf + j, 16, NULL,
                        batch_ct, n_unsolved * 16,
                        batch_pt, n_unsolved * 16, &moved);
                    if (st != kCCSuccess) continue;

                    for (int p = 0; p < n_unsolved; p++) {
                        if (is_image_magic(batch_pt + p*16)) {
                            if (is_rejected(buf + j)) continue;
                            int idx = unsolved_idx[p];
                            memcpy(patterns[idx].key, buf + j, 16);
                            patterns[idx].solved = 1;

                            char kh[33]; bytes2hex(buf + j, 16, kh);
                            char ch[33]; bytes2hex(patterns[idx].ct, 16, ch);
                            printf("\n  *** FOUND KEY: %s ***\n", kh);
                            printf("      Pattern: %s (%d files)\n",
                                   ch, patterns[idx].file_count);
                            printf("      PID %d, addr=0x%llx+0x%x\n",
                                   pid, addr, j);

                            /* Cross-check: does this key solve OTHER patterns? */
                            for (int q = 0; q < n_unsolved; q++) {
                                if (q == p || patterns[unsolved_idx[q]].solved)
                                    continue;
                                unsigned char tpt[16];
                                size_t tm;
                                CCCrypt(kCCDecrypt, kCCAlgorithmAES128,
                                    kCCOptionECBMode, buf + j, 16, NULL,
                                    patterns[unsolved_idx[q]].ct, 16,
                                    tpt, 16, &tm);
                                if (is_image_magic(tpt)) {
                                    int qi = unsolved_idx[q];
                                    memcpy(patterns[qi].key, buf + j, 16);
                                    patterns[qi].solved = 1;
                                    char qch[33];
                                    bytes2hex(patterns[qi].ct, 16, qch);
                                    printf("      Also solves: %s (%d files)\n",
                                           qch, patterns[qi].file_count);
                                }
                            }

                            found_this_pid++;
                            /* Rebuild batch for remaining unsolved */
                            n_unsolved = 0;
                            for (int i = 0; i < npatterns; i++)
                                if (!patterns[i].solved)
                                    unsolved_idx[n_unsolved++] = i;
                            for (int i = 0; i < n_unsolved; i++)
                                memcpy(batch_ct + i*16,
                                       patterns[unsolved_idx[i]].ct, 16);
                            if (n_unsolved == 0) goto done;
                            break; /* restart block check with new batch */
                        }
                    }
                }

                /* Method 2: hex string [0-9a-f]{16+} at unaligned positions.
                 * WeChat may store the AES key as a hex-encoded ASCII string
                 * in memory (e.g. "cfcd208495d565ef" = 16 ASCII bytes).
                 * We use the raw ASCII bytes directly as the 16-byte AES key,
                 * since the key is arbitrary bytes and the hex representation
                 * itself is 16 bytes for a 64-bit key half. */
                int run = 0, run_start = 0;
                for (mach_msg_type_number_t j = 0;
                     j <= data_cnt && !stop_flag; j++) {
                    int is_hex = (j < data_cnt) &&
                        ((buf[j]>='a' && buf[j]<='f') ||
                         (buf[j]>='0' && buf[j]<='9'));
                    if (is_hex) {
                        if (!run) run_start = j;
                        run++;
                    } else {
                        if (run >= 16) {
                            for (int k = run_start; k+16 <= run_start+run; k++) {
                                if (k % 16 == 0) continue; /* already tested */
                                tests++;
                                size_t moved;
                                CCCrypt(kCCDecrypt, kCCAlgorithmAES128,
                                    kCCOptionECBMode, buf+k, 16, NULL,
                                    batch_ct, n_unsolved*16,
                                    batch_pt, n_unsolved*16, &moved);
                                for (int p = 0; p < n_unsolved; p++) {
                                    if (is_image_magic(batch_pt + p*16)) {
                                        if (is_rejected(buf+k)) continue;
                                        int idx = unsolved_idx[p];
                                        memcpy(patterns[idx].key, buf+k, 16);
                                        patterns[idx].solved = 1;
                                        char kh[33]; bytes2hex(buf+k, 16, kh);
                                        char ch[33];
                                        bytes2hex(patterns[idx].ct, 16, ch);
                                        printf("\n  *** FOUND KEY: %s ***\n", kh);
                                        printf("      Pattern: %s (%d files)\n",
                                               ch, patterns[idx].file_count);
                                        int ctx_len = data_cnt - run_start;
                                        if (ctx_len > 32) ctx_len = 32;
                                        printf("      ASCII context: %.*s\n",
                                               ctx_len, buf + run_start);
                                        found_this_pid++;
                                        /* Rebuild */
                                        n_unsolved = 0;
                                        for (int i = 0; i < npatterns; i++)
                                            if (!patterns[i].solved)
                                                unsolved_idx[n_unsolved++] = i;
                                        for (int i = 0; i < n_unsolved; i++)
                                            memcpy(batch_ct + i*16,
                                                patterns[unsolved_idx[i]].ct, 16);
                                        if (n_unsolved == 0) goto done;
                                        break;
                                    }
                                }
                            }
                        }
                        run = 0;
                    }
                }

                /* Method 3 (deep mode): byte-by-byte scan for top priority patterns */
                if (g_deep_mode && n_unsolved > 0) {
                    /* Build priority batch: top N unsolved by file_count */
                    int prio_idx[DEEP_PRIORITY_MAX];
                    int n_prio = 0;
                    for (int i = 0; i < n_unsolved && n_prio < DEEP_PRIORITY_MAX; i++) {
                        int pi = unsolved_idx[i];
                        if (patterns[pi].file_count >= 10)
                            prio_idx[n_prio++] = pi;
                    }
                    if (n_prio > 0) {
                        unsigned char prio_ct[DEEP_PRIORITY_MAX * 16];
                        unsigned char prio_pt[DEEP_PRIORITY_MAX * 16];
                        for (int i = 0; i < n_prio; i++)
                            memcpy(prio_ct + i*16, patterns[prio_idx[i]].ct, 16);

                        for (mach_msg_type_number_t j = 0;
                             j + 16 <= data_cnt && !stop_flag; j++) {
                            if (j % 16 == 0) continue; /* already tested in Method 1 */
                            tests++;
                            size_t moved;
                            CCCryptorStatus st = CCCrypt(
                                kCCDecrypt, kCCAlgorithmAES128, kCCOptionECBMode,
                                buf + j, 16, NULL,
                                prio_ct, n_prio * 16,
                                prio_pt, n_prio * 16, &moved);
                            if (st != kCCSuccess) continue;

                            for (int p = 0; p < n_prio; p++) {
                                if (!is_image_magic(prio_pt + p*16)) continue;
                                if (is_rejected(buf + j)) continue;
                                int idx = prio_idx[p];
                                if (patterns[idx].solved) continue;
                                memcpy(patterns[idx].key, buf + j, 16);
                                patterns[idx].solved = 1;

                                char kh[33]; bytes2hex(buf + j, 16, kh);
                                char ch[33]; bytes2hex(patterns[idx].ct, 16, ch);
                                printf("\n  *** FOUND KEY (deep): %s ***\n", kh);
                                printf("      Pattern: %s (%d files)\n",
                                       ch, patterns[idx].file_count);
                                printf("      PID %d, addr=0x%llx+0x%x (unaligned)\n",
                                       pid, addr, j);
                                found_this_pid++;

                                /* Cross-check against all unsolved */
                                for (int q = 0; q < n_unsolved; q++) {
                                    int qi = unsolved_idx[q];
                                    if (qi == idx || patterns[qi].solved) continue;
                                    unsigned char tpt[16];
                                    size_t tm;
                                    CCCrypt(kCCDecrypt, kCCAlgorithmAES128,
                                        kCCOptionECBMode, buf + j, 16, NULL,
                                        patterns[qi].ct, 16, tpt, 16, &tm);
                                    if (is_image_magic(tpt)) {
                                        memcpy(patterns[qi].key, buf + j, 16);
                                        patterns[qi].solved = 1;
                                        char qch[33];
                                        bytes2hex(patterns[qi].ct, 16, qch);
                                        printf("      Also solves: %s (%d files)\n",
                                               qch, patterns[qi].file_count);
                                    }
                                }

                                /* Rebuild main batch */
                                n_unsolved = 0;
                                for (int i = 0; i < npatterns; i++)
                                    if (!patterns[i].solved)
                                        unsolved_idx[n_unsolved++] = i;
                                for (int i = 0; i < n_unsolved; i++)
                                    memcpy(batch_ct + i*16,
                                        patterns[unsolved_idx[i]].ct, 16);
                                /* Rebuild priority batch */
                                n_prio = 0;
                                for (int i = 0; i < n_unsolved && n_prio < DEEP_PRIORITY_MAX; i++) {
                                    int pi2 = unsolved_idx[i];
                                    if (patterns[pi2].file_count >= 10)
                                        prio_idx[n_prio++] = pi2;
                                }
                                for (int i = 0; i < n_prio; i++)
                                    memcpy(prio_ct + i*16, patterns[prio_idx[i]].ct, 16);
                                if (n_unsolved == 0) goto done;
                                break;
                            }
                        }
                    }
                }

                done:
                mach_vm_deallocate(mach_task_self(), data, data_cnt);
                if (n_unsolved == 0) break;
            }
        }
        addr += rsize;
        if (regions % 500 == 0) {
            printf("  [%ld regions, %lld MB, %lld tests]\r",
                   regions, total_bytes/(1024*1024), tests);
            fflush(stdout);
        }
    }

    printf("  PID %d: %ld regions, %lld MB, %lld tests, %ld keys found     \n",
           pid, regions, total_bytes/(1024*1024), tests, found_this_pid);

    free(batch_ct);
    free(batch_pt);
    mach_port_deallocate(mach_task_self(), task);
    return (int)found_this_pid;
}

/* ---- Save results ---- */
static void save_keys(const char *dir) {
    char path[MAX_PATH];
    snprintf(path, sizeof(path), "%s/image_keys.json", dir);

    int solved = 0;
    for (int i = 0; i < npatterns; i++)
        if (patterns[i].solved) solved++;
    if (solved == 0) return;

    FILE *f = fopen(path, "w");
    if (!f) { fprintf(stderr, "Cannot write %s\n", path); return; }

    fprintf(f, "{\n");
    int first = 1;
    for (int i = 0; i < npatterns; i++) {
        if (!patterns[i].solved) continue;
        char ct_hex[33], key_hex[33];
        bytes2hex(patterns[i].ct, 16, ct_hex);
        bytes2hex(patterns[i].key, 16, key_hex);
        fprintf(f, "%s    \"%s\": \"%s\"",
                first ? "" : ",\n", ct_hex, key_hex);
        first = 0;
    }
    fprintf(f, "\n}\n");
    fclose(f);
    printf("\nSaved %d keys to %s\n", solved, path);
}

/* ---- Load existing keys from image_keys.json ---- */
static int load_keys(const char *dir) {
    char path[MAX_PATH];
    snprintf(path, sizeof(path), "%s/image_keys.json", dir);
    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) {
        free(json);
        fclose(f);
        return 0;
    }
    fclose(f);
    json[rd] = '\0';

    int loaded = 0;
    /* Parse "ct_hex": "key_hex" pairs */
    const char *p = json;
    while ((p = strchr(p, '"')) != NULL) {
        p++;
        const char *ct_end = strchr(p, '"');
        if (!ct_end || ct_end - p != 32) { p = ct_end ? ct_end + 1 : p; continue; }
        char ct_str[33]; memcpy(ct_str, p, 32); ct_str[32] = '\0';
        unsigned char ct[16];
        if (hex2bytes(ct_str, ct, 16) != 16) { p = ct_end + 1; continue; }

        p = ct_end + 1;
        p = strchr(p, '"');
        if (!p) break;
        p++;
        const char *key_end = strchr(p, '"');
        if (!key_end || key_end - p != 32) { p = key_end ? key_end + 1 : p; continue; }
        char key_str[33]; memcpy(key_str, p, 32); key_str[32] = '\0';
        unsigned char key[16];
        if (hex2bytes(key_str, key, 16) != 16) { p = key_end + 1; continue; }

        /* Match to pattern */
        for (int i = 0; i < npatterns; i++) {
            if (!patterns[i].solved && memcmp(patterns[i].ct, ct, 16) == 0) {
                memcpy(patterns[i].key, key, 16);
                patterns[i].solved = 1;
                loaded++;
                break;
            }
        }
        p = key_end + 1;
    }
    free(json);
    return loaded;
}

/* ---- Main ---- */
int main(int argc, char *argv[]) {
    signal(SIGINT, sigint_handler);

    printf("=== WeChat V2 Image Key Scanner ===\n\n");
    if (getuid() != 0) {
        fprintf(stderr, "ERROR: Run with sudo!\n"); return 1;
    }

    /* Determine image directory */
    char image_dir[MAX_PATH] = "";
    char exe_dir[MAX_PATH] = ".";
    int deep_mode = 0;
    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]);
    }

    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "--deep") == 0)
            deep_mode = 1;
        else if (image_dir[0] == '\0') {
            strncpy(image_dir, argv[i], sizeof(image_dir) - 1);
            image_dir[sizeof(image_dir) - 1] = '\0';
        }
    }

    if (image_dir[0] == '\0') {
        /* Read config.json */
        char cfg_path[MAX_PATH];
        snprintf(cfg_path, sizeof(cfg_path), "%s/config.json", exe_dir);
        FILE *cf = fopen(cfg_path, "r");
        if (cf) {
            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[rd] = '\0';
            fclose(cf);
            char db_dir[MAX_PATH];
            if (json_get_string(json, "db_dir", db_dir, sizeof(db_dir))) {
                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);
        }
    }

    /* 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 st;
                if (stat(candidate, &st) == 0 && S_ISDIR(st.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:\n");
        fprintf(stderr, "  1. Command line argument\n");
        fprintf(stderr, "  2. config.json db_dir\n");
        fprintf(stderr, "  3. Auto-detect ~/Library/Containers/com.tencent.xinWeChat/...\n\n");
        fprintf(stderr, "Usage: sudo %s [--deep] [image_dir]\n", argv[0]);
        fprintf(stderr, "  image_dir: path to .../xwechat_files/<wxid>/msg\n");
        return 1;
    }

    /* Phase 1: Discover patterns */
    printf("Discovering encryption patterns in:\n  %s\n\n", image_dir);
    discover_dir(image_dir);
    if (npatterns == 0) {
        fprintf(stderr, "No V2 .dat files found!\n"); return 1;
    }
    qsort(patterns, npatterns, sizeof(pattern_t), cmp_patterns);

    int total_covered = 0;
    printf("Found %d patterns across %d V2 files:\n", npatterns, total_v2_files);
    for (int i = 0; i < npatterns; i++) {
        char ch[33]; bytes2hex(patterns[i].ct, 16, ch);
        printf("  #%-2d %s  (%d files)\n", i+1, ch, patterns[i].file_count);
        total_covered += patterns[i].file_count;
    }
    if (total_covered < total_v2_files)
        printf("  ... and %d files in overflow patterns\n",
               total_v2_files - total_covered);

    /* Load previously found keys */
    int preloaded = load_keys(exe_dir);
    if (preloaded > 0)
        printf("\nLoaded %d existing keys from image_keys.json\n", preloaded);

    if (deep_mode) {
        g_deep_mode = 1;
        printf("\n*** DEEP MODE: byte-by-byte scan for top %d unsolved patterns ***\n",
               DEEP_PRIORITY_MAX);
    }

    /* Phase 2: Continuous scanning */
    printf("\nScanning WeChat memory — keep browsing images! (Ctrl+C to stop)\n");
    int round = 0;
    while (!stop_flag) {
        int unsolved = 0;
        for (int i = 0; i < npatterns; i++)
            if (!patterns[i].solved) unsolved++;
        if (unsolved == 0) break;

        round++;
        pid_t pids[64];
        int npids = get_wechat_pids(pids, 64);
        if (npids == 0) {
            printf("  No WeChat processes found, waiting...\n");
            sleep(3);
            continue;
        }

        printf("\n--- Round %d: %d unsolved / %d total, %d PIDs ---\n",
               round, unsolved, npatterns, npids);

        int found_round = 0;
        for (int i = 0; i < npids && !stop_flag; i++) {
            found_round += scan_pid(pids[i]);
        }

        unsolved = 0;
        int solved_files = 0;
        for (int i = 0; i < npatterns; i++) {
            if (patterns[i].solved) solved_files += patterns[i].file_count;
            else unsolved++;
        }

        if (found_round > 0) {
            printf("\n  Progress: %d/%d patterns solved (%d/%d files)\n",
                   npatterns - unsolved, npatterns,
                   solved_files, total_v2_files);
            /* Verify newly found keys */
            for (int i = 0; i < npatterns; i++) {
                if (patterns[i].solved && !verify_key(i)) {
                    char kh[33]; bytes2hex(patterns[i].key, 16, kh);
                    printf("  REJECTED: %s (failed verification)\n", kh);
                    add_rejected(patterns[i].key);
                    patterns[i].solved = 0;
                    memset(patterns[i].key, 0, 16);
                }
            }
            /* Save after each find */
            save_keys(exe_dir);
        }

        if (unsolved > 0 && !stop_flag) {
            printf("  Keep browsing images in different chats...\n");
            sleep(1);
        }
    }

    /* Phase 3: Summary */
    save_keys(exe_dir);

    int solved = 0, solved_files = 0;
    for (int i = 0; i < npatterns; i++) {
        if (patterns[i].solved) {
            solved++;
            solved_files += patterns[i].file_count;
        }
    }

    printf("\n==================================================\n");
    if (solved == npatterns) {
        printf("ALL %d patterns solved! (%d files)\n", npatterns, total_v2_files);
    } else {
        printf("%d/%d patterns solved (%d/%d files)\n",
               solved, npatterns, solved_files, total_v2_files);
        printf("Unsolved:\n");
        for (int i = 0; i < npatterns; i++) {
            if (patterns[i].solved) continue;
            char ch[33]; bytes2hex(patterns[i].ct, 16, ch);
            printf("  %s (%d files)\n", ch, patterns[i].file_count);
        }
    }

    /* Count unique keys */
    int unique_keys = 0;
    for (int i = 0; i < npatterns; i++) {
        if (!patterns[i].solved) continue;
        int dup = 0;
        for (int j = 0; j < i; j++)
            if (patterns[j].solved &&
                memcmp(patterns[i].key, patterns[j].key, 16) == 0) { dup = 1; break; }
        if (!dup) unique_keys++;
    }
    printf("%d unique key(s) found.\n", unique_keys);
    printf("==================================================\n");

    return (solved > 0) ? 0 : 1;
}
