casc 0.1.0

A cross-platform CLI tool for Blizzard CASC archives.
/*****************************************************************************/
/* cmf.cpp                                Copyright (c) Ladislav Zezula 2023 */
/*---------------------------------------------------------------------------*/
/* Support for Content Manifest Files (.cmf)                                 */
/* Know-how from https://github.com/overtools/TACTLib                        */
/*---------------------------------------------------------------------------*/
/*   Date    Ver   Who  Comment                                              */
/* --------  ----  ---  -------                                              */
/* 29.07.23  1.00  Lad  Created                                              */
/*****************************************************************************/

#define __CASCLIB_SELF__
#include "../CascLib.h"
#include "../CascCommon.h"

#include "aes.h"
#include "overwatch.h"

//-----------------------------------------------------------------------------
// Encryption key providers for CMF files. These are taken from TACTLib
// with the kind permission of the TACTLib authors
// (https://github.com/overtools/TACTLib)

// Key and IV provider functions
typedef LPBYTE(*GET_KEY)(const CASC_CMF_HEADER & Header, LPBYTE pbKey, int nLength);
typedef LPBYTE(*GET_IV)(const CASC_CMF_HEADER & Header, LPBYTE nameSha1, LPBYTE pbKey, int nLength);

// Structure for the single provider
typedef struct _CASC_CMF_KEY_PROVIDER
{
    DWORD   dwBuildNumber;
    GET_KEY PfnGetKey;
    GET_IV  PfnGetIV;
} CASC_CMF_KEY_PROVIDER;
typedef const CASC_CMF_KEY_PROVIDER *PCASC_CMF_KEY_PROVIDER;

// Needed by various providers in the cmf-key.cpp file
struct TMath
{
    template <typename TYPE>
    TYPE Max(TYPE value1, TYPE value2)
    {
        return (value1 > value2) ? value1 : value2;
    }
    DWORD dwDummy;
} Math;

// Needed by various providers in the cmf-key.cpp file
static uint Constrain(LONGLONG value)
{
    return (uint)(value % 0xFFFFFFFFULL);
}

// Needed by various providers in the cmf-key.cpp file
static int SignedMod(LONGLONG p1, LONGLONG p2)
{
    int a = (int)p1;
    int b = (int)p2;
    return (a % b) < 0 ? (a % b + b) : (a % b);
}

// Include the CMF key provider functions and the table of providers
// This file is created by the "cmf-update.py" script, DO NOT EDIT.
#include "cmf-key.cpp"

//-----------------------------------------------------------------------------
// Local functions

static PCASC_CMF_KEY_PROVIDER FindCmfKeyProvider(DWORD dwBuildNumber)
{
    PCASC_CMF_KEY_PROVIDER pStartEntry = CmfKeyProviders;
    PCASC_CMF_KEY_PROVIDER pMidleEntry = NULL;
    PCASC_CMF_KEY_PROVIDER pFinalEntry = &CmfKeyProviders[_countof(CmfKeyProviders)];

    // Perform binary search on the table
    while(pStartEntry < pFinalEntry)
    {
        // Calculate the middle of the interval
        pMidleEntry = pStartEntry + ((pFinalEntry - pStartEntry) / 2);

        // Did we find it?
        if(dwBuildNumber == pMidleEntry->dwBuildNumber)
            return pMidleEntry;

        // Move the interval to the left or right
        if(dwBuildNumber > pMidleEntry->dwBuildNumber)
            pStartEntry = pMidleEntry + 1;
        else
            pFinalEntry = pMidleEntry;
    }
/*
    for(size_t i = 0; i < _countof(CmfKeyProviders); i++)
    {
        if(CmfKeyProviders[i].dwBuildNumber == dwBuildNumber)
        {
            return &CmfKeyProviders[i];
        }
    }
*/
    return NULL;
}

static DWORD DecryptCmfStream(const CASC_CMF_HEADER & Header, const char * szPlainName, LPBYTE pbDataPtr, LPBYTE pbDataEnd)
{
    PCASC_CMF_KEY_PROVIDER pKeyProvider;
    AES_KEY AesKey;
    BYTE RawKey[CASC_AES_KEY_LENGTH];
    BYTE RawIV[CASC_AES_IV_LENGTH];
    BYTE nameDigest[SHA1_HASH_SIZE];

    // Find the provider for that Overwatch build
    if((pKeyProvider = FindCmfKeyProvider(Header.m_buildVersion)) == NULL)
        return ERROR_FILE_ENCRYPTED;

    // Create SHA1 from the file name
    CascHash_SHA1(szPlainName, strlen(szPlainName), nameDigest);

    // Retrieve key and IV
    pKeyProvider->PfnGetKey(Header, RawKey, sizeof(RawKey));
    pKeyProvider->PfnGetIV(Header, nameDigest, RawIV, sizeof(RawIV));

    // Decrypt the stream using AES
    AES_set_decrypt_key(RawKey, 256, &AesKey);
    AES_cbc_decrypt(pbDataPtr, pbDataPtr, (pbDataEnd - pbDataPtr), &AesKey, RawIV);
    return ERROR_SUCCESS;
}

//-----------------------------------------------------------------------------
// Public functions

DWORD LoadContentManifestFile(TCascStorage * hs, CASC_FILE_TREE & FileTree, PCASC_CKEY_ENTRY pCKeyEntry, const char * szCmfFileName)
{
    CASC_BLOB CmfFile;
    const char * szCmfPlainName = GetPlainFileName(szCmfFileName);
    DWORD dwErrCode;

    // Load the entire internal file to memory
    if((dwErrCode = LoadInternalFileToMemory(hs, pCKeyEntry, CmfFile)) == ERROR_SUCCESS)
    {
        PCASC_APM_ENTRY_V2 pApmEntries = NULL;
        CASC_CMF_HEADER CmfHeader = {0};
        LPBYTE pbDataEnd = CmfFile.pbData + CmfFile.cbData;
        LPBYTE pbDataPtr = CmfFile.pbData;
        size_t nPlainName;
        DWORD dwBuildVersion;
        char szFileName[MAX_PATH];

        // Get the build version
        if((pbDataPtr = CaptureInteger32(pbDataPtr, pbDataEnd, &dwBuildVersion)) == NULL)
            return ERROR_BAD_FORMAT;
        pbDataPtr = CmfFile.pbData;

        // Parse headers of various versions
        if(dwBuildVersion > CASC_OVERWATCH_VERSION_148_PTR)
        {
            CASC_CMF_HEADER_148 * pHeader148;

            if((pbDataPtr = CaptureStructure(pbDataPtr, pbDataEnd, &pHeader148)) == NULL)
                return ERROR_BAD_FORMAT;
            CmfHeader = *pHeader148;
        }
        else if(dwBuildVersion > CASC_OVERWATCH_VERSION_122_PTR)
        {
            CASC_CMF_HEADER_122 * pHeader122;

            if((pbDataPtr = CaptureStructure(pbDataPtr, pbDataEnd, &pHeader122)) == NULL)
                return ERROR_BAD_FORMAT;
            CmfHeader = *pHeader122;
        }
        else
        {
            CASC_CMF_HEADER_100 * pHeader100;

            if((pbDataPtr = CaptureStructure(pbDataPtr, pbDataEnd, &pHeader100)) == NULL)
                return ERROR_BAD_FORMAT;
            CmfHeader = *pHeader100;
        }

        // Decrypt the stream, if needed
        if(CmfHeader.IsEncrypted())
        {
            if((dwErrCode = DecryptCmfStream(CmfHeader, szCmfPlainName, pbDataPtr, pbDataEnd)) != ERROR_SUCCESS)
            {
                return dwErrCode;
            }
        }

        // Skip APM entries. We don't need them for anything, really
        if((pbDataPtr = CaptureArray(pbDataPtr, pbDataEnd, &pApmEntries, CmfHeader.m_entryCount)) == NULL)
        {
            return ERROR_BAD_FORMAT;
        }

        // Create the name template of the assets
        nPlainName = BuildAssetFileNameTemplate(szFileName,
                                                _countof(szFileName),
                                                "ContentManifestFiles",
                                                szCmfPlainName);

        // Load the hash list This is the list of Asset ID -> CKey
        if(CmfHeader.m_buildVersion >= 57230)
        {
            PCASC_CMF_HASH_ENTRY_135 pHashList;

            if((pbDataPtr = CaptureArray(pbDataPtr, pbDataEnd, &pHashList, CmfHeader.m_dataCount)) == NULL)
                return ERROR_BAD_FORMAT;

            dwErrCode = InsertAssetFiles(hs, FileTree, szFileName, nPlainName, pHashList, CmfHeader.m_dataCount);
        }
        else
        {
            PCASC_CMF_HASH_ENTRY_100 pHashList;

            if((pbDataPtr = CaptureArray(pbDataPtr, pbDataEnd, &pHashList, CmfHeader.m_dataCount)) == NULL)
                return ERROR_BAD_FORMAT;

            dwErrCode = InsertAssetFiles(hs, FileTree, szFileName, nPlainName, pHashList, CmfHeader.m_dataCount);
        }
    }
    return dwErrCode;
}