// SPDX-License-Identifier: GPL-2.0-only
/*
 * Digital Voice Modem - Converged FNE Software
 * GPLv2 Open Source. Use is subject to license terms.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 *  Copyright (C) 2025 Bryan Biedenkapp, N2PLL
 *
 */
#include "common/AESCrypto.h"
#include "common/Log.h"
#include "common/Timer.h"
#include "common/Utils.h"
#include "common/zlib/zlib.h"
#if defined(ENABLE_SSL)
#include "xml/rapidxml.h"
#endif // ENABLE_SSL
#include "CryptoContainer.h"

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>

#if defined(ENABLE_SSL)
#include <openssl/bio.h>
#include <openssl/evp.h>
#endif // ENABLE_SSL

using namespace crypto;

// ---------------------------------------------------------------------------
//  Constants
// ---------------------------------------------------------------------------

#define CHUNK 16384

// ---------------------------------------------------------------------------
//  Global Functions
// ---------------------------------------------------------------------------

#if defined(ENABLE_SSL)
/**
 * @brief Calculates the length of a decoded base64 string.
 * @param b64input String containing the base64 encoded data.
 * @returns int Length of buffer to contain base64 encoded data.
 */
int calcDecodeLength(const char* b64input)
{
    int len = strlen(b64input);
    int padding = 0;
  
    // last two chars are =
    if (b64input[len-1] == '=' && b64input[len-2] == '=') 
        padding = 2;
    else if (b64input[len-1] == '=') // last char is =
        padding = 1;
  
    return (int)len * 0.75 - padding;
}

/**
 * @brief Decodes a base64 encoded string.
 * @param b64message String containing the base64 encoded data.
 * @param buffer Buffer pointer to place encoded data.
 * @returns int 
 */
int base64Decode(char* b64message, uint8_t** buffer)
{
    int decodeLen = calcDecodeLength(b64message), len = 0;
    
    *buffer = (uint8_t*)malloc(decodeLen + 1);
    FILE* stream = ::fmemopen(b64message, ::strlen(b64message), "r");
  
    BIO* b64 = BIO_new(BIO_f_base64());
    BIO* bio = BIO_new_fp(stream, BIO_NOCLOSE);
    bio = BIO_push(b64, bio);
    BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL); // do not use newlines to flush buffer
    len = BIO_read(bio, *buffer, ::strlen(b64message));

    // can test here if len == decodeLen - if not, then return an error
    (*buffer)[len] = '\0';
  
    BIO_free_all(bio);
    ::fclose(stream);

    return decodeLen;
}
#endif // ENABLE_SSL

/**
 * @brief 
 * @param buffer 
 * @param len 
 * @param target 
 * @return int 
 */
int findFirstChar(const uint8_t* buffer, uint32_t len, char target) 
{
    for (uint32_t i = 0U; i < len; i++) {
      if (buffer[i] == target) {
        return (int)i; 
      }
    }

    return -1;
}

/**
 * @brief 
 * @param buffer 
 * @param len 
 * @param target 
 * @return int 
 */
int findLastChar(const uint8_t* buffer, uint32_t len, char target) 
{
    if (buffer == nullptr) {
        return -1; 
    }

    int lastIndex = -1;
    for (uint32_t i = 0U; i < len; i++) {
        if (buffer[i] == target) {
            lastIndex = i;
        }
    }
    return lastIndex;
}

// ---------------------------------------------------------------------------
//  Static Class Members
// ---------------------------------------------------------------------------

std::mutex CryptoContainer::s_mutex;

// ---------------------------------------------------------------------------
//  Public Class Members
// ---------------------------------------------------------------------------

/* Initializes a new instance of the CryptoContainer class. */

CryptoContainer::CryptoContainer(const std::string& filename, const std::string& password, 
    const std::string& remotePassword, bool remoteAccessEnabled, uint32_t reloadTime, bool enabled) : Thread(),
    m_file(filename),
    m_password(password),
    m_remotePassword(remotePassword),
    m_remoteAccessEnabled(remoteAccessEnabled),
    m_reloadTime(reloadTime),
    m_lastLoadTime(0U),
#if !defined(ENABLE_SSL)
    m_enabled(false),
#else
    m_enabled(enabled),
#endif // !ENABLE_SSL
    m_stop(false),
    m_keys(),
    m_ukeks(),
    m_llas()
{
    /* stub */
}

/* Finalizes a instance of the CryptoContainer class. */

CryptoContainer::~CryptoContainer() = default;

/* Thread entry point. This function is provided to run the thread for the lookup table. */

void CryptoContainer::entry()
{
    if (m_reloadTime == 0U) {
        return;
    }

    Timer timer(1U, 60U * m_reloadTime);
    timer.start();

    while (!m_stop) {
        sleep(1000U);

        timer.clock();
        if (timer.hasExpired()) {
            load();
            timer.start();
        }
    }
}

/* Stops and unloads this lookup table. */

void CryptoContainer::stop(bool noDestroy)
{
    if (!m_enabled)
        return;

    if (m_reloadTime == 0U) {
        if (!noDestroy)
            delete this;
        return;
    }

    m_stop = true;

    wait();
}

/* Reads the lookup table from the specified lookup table file. */

bool CryptoContainer::read()
{
    if (!m_enabled)
        return false;

    bool ret = load();

    if (m_reloadTime > 0U)
        run();
    setName("fne:crypto-lookup-tbl");

    return ret;
}

/* Clears all entries from the lookup table. */

void CryptoContainer::clear()
{
    std::lock_guard<std::mutex> lock(s_mutex);
    m_keys.clear();
    m_ukeks.clear();
    m_llas.clear();
}

/* Adds a new entry to the lookup table by the specified unique ID. */

void CryptoContainer::addEntry(EKCKeyItem key)
{
    if (key.isInvalid())
        return;

    EKCKeyItem entry = key;
    uint32_t id = entry.id();
    uint32_t kId = entry.kId();

    std::lock_guard<std::mutex> lock(s_mutex);
    auto it = std::find_if(m_keys.begin(), m_keys.end(),
        [&](EKCKeyItem& x)
        {
            return x.id() == id && x.kId() == kId;
        });
    if (it != m_keys.end()) {
        m_keys[it - m_keys.begin()] = entry;
    }
    else {
        m_keys.push_back(entry);
    }
}

/* Erases an existing entry from the lookup table by the specified unique ID. */

void CryptoContainer::eraseEntry(uint32_t id)
{
    std::lock_guard<std::mutex> lock(s_mutex);
    auto it = std::find_if(m_keys.begin(), m_keys.end(),
        [&](EKCKeyItem& x) {
            return x.id() == id; 
        });
    if (it != m_keys.end()) {
        m_keys.erase(it);
    }
}

/* Finds a table entry in this lookup table. */

EKCKeyItem CryptoContainer::find(uint32_t kId)
{
    EKCKeyItem entry;

    std::lock_guard<std::mutex> lock(s_mutex);
    auto it = std::find_if(m_keys.begin(), m_keys.end(),
        [&](EKCKeyItem& x) {
            return x.kId() == kId;
        });
    if (it != m_keys.end()) {
        entry = *it;
    } else {
        entry = EKCKeyItem();
    }

    return entry;
}

/* Finds a table entry in this lookup table. */

EKCKeyItem CryptoContainer::findUKEK(uint32_t rsi)
{
    EKCKeyItem entry;

    std::lock_guard<std::mutex> lock(s_mutex);

    // prefer exact RSI/LLID match, then fall back to the default (RSI 0) key.
    auto it = std::find_if(m_ukeks.begin(), m_ukeks.end(),
        [&](const EKCKeyItem& x) {
            return x.rsiId() == rsi;
        });
    if (it != m_ukeks.end()) {
        entry = *it;
        return entry;
    }

    it = std::find_if(m_ukeks.begin(), m_ukeks.end(),
        [&](const EKCKeyItem& x) {
            return x.rsiId() == 0U;
        });
    if (it != m_ukeks.end()) {
        entry = *it;
        return entry;
    }

    entry = EKCKeyItem();

    return entry;
}

/* Finds a table entry in this lookup table. */

EKCKeyItem CryptoContainer::findLLA(uint32_t rsi)
{
    EKCKeyItem entry;

    std::lock_guard<std::mutex> lock(s_mutex);

    // prefer exact RSI/LLID match, then fall back to the default (RSI 0) key.
    auto it = std::find_if(m_llas.begin(), m_llas.end(),
        [&](const EKCKeyItem& x) {
            return x.rsiId() == rsi;
        });
    if (it != m_llas.end()) {
        entry = *it;
        return entry;
    }

    it = std::find_if(m_llas.begin(), m_llas.end(),
        [&](const EKCKeyItem& x) {
            return x.rsiId() == 0U;
        });
    if (it != m_llas.end()) {
        entry = *it;
        return entry;
    }

    entry = EKCKeyItem();

    return entry;
}

// ---------------------------------------------------------------------------
//  Private Class Members
// ---------------------------------------------------------------------------

/* Loads the table from the passed lookup table file. */

bool CryptoContainer::load()
{
#if !defined(ENABLE_SSL)
    return false;
#else
    if (!m_enabled) {
        return false;
    }
    if (m_file.length() <= 0) {
        return false;
    }
    if (m_password.length() <= 0) {
        return false;
    }

    FILE* ekcFile = ::fopen(m_file.c_str(), "rb");
    if (ekcFile == nullptr) {
        LogError(LOG_HOST, "Cannot open the crypto container file - %s", m_file.c_str());
        return false;
    }

    // inflate file
    // compression structures
    z_stream strm;
    strm.zalloc = Z_NULL;
    strm.zfree = Z_NULL;
    strm.opaque = Z_NULL;
    
    // set input data
    strm.avail_in = 0U;
    strm.next_in = Z_NULL;

    // initialize decompression
    int ret = inflateInit2(&strm, 16 + MAX_WBITS);
    if (ret != Z_OK) {
        LogError(LOG_HOST, "Error initializing ZLIB, ret = %d", ret);
        ::fclose(ekcFile);
        return false;
    }

    // skip 4 bytes (C# adds a header on the GZIP stream for the decompressed length)
    ::fseek(ekcFile, 4, SEEK_SET);

    // decompress data
    std::vector<uint8_t> decompressedData;
    uint8_t inBuffer[CHUNK];
    uint8_t outBuffer[CHUNK];
    do {
        strm.avail_in = fread(inBuffer, 1, CHUNK, ekcFile);
        if (::ferror(ekcFile)) {
            inflateEnd(&strm);
            ::fclose(ekcFile);
            return false;
        }

        if (strm.avail_in == 0)
            break;
        strm.next_in = inBuffer;

        uint32_t have = 0U;
        do {
            strm.avail_out = CHUNK;
            strm.next_out = outBuffer;

            ret = inflate(&strm, Z_NO_FLUSH);
            if (ret == Z_DATA_ERROR) {
                // deflate stream invalid
                LogError(LOG_HOST, "Error decompressing EKC: %s", (strm.msg == NULL) ? "compressed data error" : strm.msg);
                inflateEnd(&strm);
                ::fclose(ekcFile);
                return false;
            }

            if (ret == Z_STREAM_ERROR || ret < 0) {
                LogError(LOG_HOST, "Error decompressing EKC, ret = %d", ret);
                inflateEnd(&strm);
                ::fclose(ekcFile);
                return false;
            }

            have = CHUNK - strm.avail_out;
            decompressedData.insert(decompressedData.end(), outBuffer, outBuffer + have);
        } while (strm.avail_out == 0);
    } while (ret != Z_STREAM_END);

    // cleanup
    inflateEnd(&strm);
    ::fclose(ekcFile);

    try {
        // ensure zero termination
        decompressedData.push_back(0U);

        uint8_t* decompressed = decompressedData.data();

        // parse outer container DOM
        enum { PARSE_FLAGS = rapidxml::parse_full };
        rapidxml::xml_document<> ekcOuterContainer;
        ekcOuterContainer.parse<PARSE_FLAGS>(reinterpret_cast<char*>(decompressed));

        rapidxml::xml_node<>* outerRoot = ekcOuterContainer.first_node("OuterContainer");
        if (outerRoot != nullptr) {
            // get EKC version
            std::string version = "";
            rapidxml::xml_attribute<>* versionAttr = outerRoot->first_attribute("version");
            if (versionAttr != nullptr)
                version = std::string(versionAttr->value());

            // validate EKC version is set and is 1.0
            if (version == "") {
                ::LogError(LOG_HOST, "Error opening EKC: incorrect version, expected 1.0 got none");
                return false;
            }

            if (version != "1.0") {
                ::LogError(LOG_HOST, "Error opening EKC: incorrect version, expected 1.0 got %s", version.c_str());
                return false;
            }

            // get key derivation node
            rapidxml::xml_node<>* keyDerivation = outerRoot->first_node("KeyDerivation");
            if (keyDerivation == nullptr) {
                ::LogError(LOG_HOST, "Error opening EKC: failed to process XML");
                return false;
            }
            // retreive and parse salt
            uint8_t* salt = nullptr;
            rapidxml::xml_node<>* saltNode = keyDerivation->first_node("Salt");
            if (saltNode == nullptr) {
                ::LogError(LOG_HOST, "Error opening EKC: failed to process XML");
                return false;
            }
            int8_t saltBufLen = base64Decode(saltNode->value(), &salt);

            // retrieve interation count
            int32_t iterationCount = 0;
            rapidxml::xml_node<>* iterNode = keyDerivation->first_node("IterationCount");
            if (iterNode == nullptr) {
                ::LogError(LOG_HOST, "Error opening EKC: failed to process XML");
                return false;
            }
            iterationCount = ::strtoul(iterNode->value(), NULL, 10);
            
            // retrieve key length
            int32_t keyLength = 0;
            rapidxml::xml_node<>* keyLenNode = keyDerivation->first_node("KeyLength");
            if (keyLenNode == nullptr) {
                ::LogError(LOG_HOST, "Error opening EKC: failed to process XML");
                return false;
            }
            keyLength = ::strtoul(keyLenNode->value(), NULL, 10);

            // generate crypto key to decrypt inner container
            uint8_t key[EVP_MAX_KEY_LENGTH];
            ::memset(key, 0x00U, EVP_MAX_KEY_LENGTH);
            uint8_t iv[EVP_MAX_IV_LENGTH];
            ::memset(iv, 0x00U, EVP_MAX_IV_LENGTH);

            uint8_t keyIv[EVP_MAX_KEY_LENGTH + EVP_MAX_IV_LENGTH];
            ::memset(keyIv, 0x00U, EVP_MAX_KEY_LENGTH + EVP_MAX_IV_LENGTH);
            if (PKCS5_PBKDF2_HMAC(m_password.c_str(), m_password.size(), salt, saltBufLen, iterationCount, EVP_sha512(), keyLength + EVP_MAX_IV_LENGTH, keyIv)) {
                ::memcpy(key, keyIv, keyLength);
                ::memcpy(iv, keyIv + keyLength, EVP_MAX_IV_LENGTH);
            }
            free(salt); // base64Decode allocates memory with malloc()

            // get inner container encrypted data
            // bryanb: annoying levels of XML encapsulation...
            rapidxml::xml_node<>* encryptedDataNode = outerRoot->first_node("EncryptedData");
            if (encryptedDataNode == nullptr) {
                ::LogError(LOG_HOST, "Error opening EKC: failed to process XML");
                return false;
            }
            rapidxml::xml_node<>* cipherDataNode = encryptedDataNode->first_node("CipherData");
            if (cipherDataNode == nullptr) {
                ::LogError(LOG_HOST, "Error opening EKC: failed to process XML");
                return false;
            }
            rapidxml::xml_node<>* cipherValue = cipherDataNode->first_node("CipherValue");
            if (cipherValue == nullptr) {
                ::LogError(LOG_HOST, "Error opening EKC: failed to process XML");
                return false;
            }

            uint8_t* innerContainerCrypted = nullptr;
            int innerContainerLen = base64Decode(cipherValue->value(), &innerContainerCrypted);

            // decrypt inner container
            AES aes = AES(AESKeyLength::AES_256);
            uint8_t* innerContainer = aes.decryptCBC(innerContainerCrypted, innerContainerLen, key, iv);
            free(innerContainerCrypted); // base64Decode allocates memory with malloc()

            /*
            ** bryanb: this is probably slightly error prone...
            */

            int xmlFirstTagChar = findFirstChar(innerContainer, innerContainerLen, '<');
            int xmlLastTagChar = findLastChar(innerContainer, innerContainerLen, '>');
            // zero all bytes after the last > character
            ::memset(innerContainer + xmlLastTagChar + 1U, 0x00U, innerContainerLen - xmlLastTagChar);

            rapidxml::xml_document<> ekcInnerContainer;
            ekcInnerContainer.parse<PARSE_FLAGS>(reinterpret_cast<char*>(innerContainer + xmlFirstTagChar));

            rapidxml::xml_node<>* innerRoot = ekcInnerContainer.first_node("InnerContainer");
            if (innerRoot != nullptr) {
                // clear table
                clear();

                std::lock_guard<std::mutex> lock(s_mutex);

                // get keys node
                rapidxml::xml_node<>* keys = innerRoot->first_node("Keys");
                if (keys != nullptr) {
                    uint32_t i = 0U;
                    for (rapidxml::xml_node<>* keyNode = keys->first_node("KeyItem"); keyNode; keyNode = keyNode->next_sibling("KeyItem")) {
                        EKCKeyItem key = EKCKeyItem();
                        key.id(i);

                        // get name
                        rapidxml::xml_node<>* nameNode = keyNode->first_node("Name");
                        if (nameNode == nullptr) {
                            continue;
                        }
                        key.name(nameNode->value());

                        // get keyset ID
                        rapidxml::xml_node<>* keysetIdNode = keyNode->first_node("KeysetId");
                        if (keysetIdNode == nullptr) {
                            continue;
                        }
                        key.keysetId(::strtoul(keysetIdNode->value(), NULL, 10));

                        // get SLN
                        rapidxml::xml_node<>* slnNode = keyNode->first_node("Sln");
                        if (slnNode == nullptr) {
                            continue;
                        }
                        key.sln(::strtoul(slnNode->value(), NULL, 10));

                        // get algorithm ID
                        rapidxml::xml_node<>* algIdNode = keyNode->first_node("AlgorithmId");
                        if (algIdNode == nullptr) {
                            continue;
                        }
                        key.algId(::strtoul(algIdNode->value(), NULL, 10));

                        // get key ID
                        rapidxml::xml_node<>* kIdNode = keyNode->first_node("KeyId");
                        if (kIdNode == nullptr) {
                            continue;
                        }
                        key.kId(::strtoul(kIdNode->value(), NULL, 10));

                        // get key material
                        rapidxml::xml_node<>* keyMatNode = keyNode->first_node("Key");
                        if (keyMatNode == nullptr) {
                            continue;
                        }
                        key.keyMaterial(keyMatNode->value());

                        ::LogInfoEx(LOG_HOST, "Key NAME: %s SLN: %u ALGID: $%02X, KID: $%04X", key.name().c_str(), key.sln(), key.algId(), key.kId());

                        m_keys.push_back(key);
                        i++;
                    }
                }

                // get UKEKs node
                rapidxml::xml_node<>* ukeks = innerRoot->first_node("UKEKs");
                if (ukeks != nullptr) {
                    uint32_t i = 0U;
                    for (rapidxml::xml_node<>* keyNode = ukeks->first_node("RSIKeyItem"); keyNode; keyNode = keyNode->next_sibling("RSIKeyItem")) {
                        EKCKeyItem key = EKCKeyItem();
                        key.id(i);

                        rapidxml::xml_node<>* rsiNode = keyNode->first_node("RsiId");
                        if (rsiNode == nullptr) {
                            continue;
                        }
                        key.rsiId(::strtoul(rsiNode->value(), NULL, 10));

                        rapidxml::xml_node<>* nameNode = keyNode->first_node("Name");
                        if (nameNode == nullptr) {
                            continue;
                        }
                        key.name(nameNode->value());

                        rapidxml::xml_node<>* keysetIdNode = keyNode->first_node("KeysetId");
                        if (keysetIdNode == nullptr) {
                            continue;
                        }
                        key.keysetId(::strtoul(keysetIdNode->value(), NULL, 10));

                        rapidxml::xml_node<>* slnNode = keyNode->first_node("Sln");
                        if (slnNode != nullptr) {
                            key.sln(::strtoul(slnNode->value(), NULL, 10));
                        }

                        rapidxml::xml_node<>* algIdNode = keyNode->first_node("AlgorithmId");
                        if (algIdNode == nullptr) {
                            continue;
                        }
                        key.algId(::strtoul(algIdNode->value(), NULL, 10));

                        rapidxml::xml_node<>* kIdNode = keyNode->first_node("KeyId");
                        if (kIdNode == nullptr) {
                            continue;
                        }
                        key.kId(::strtoul(kIdNode->value(), NULL, 10));

                        rapidxml::xml_node<>* keyMatNode = keyNode->first_node("Key");
                        if (keyMatNode == nullptr) {
                            continue;
                        }
                        key.keyMaterial(keyMatNode->value());

                        if (key.isInvalid()) {
                            continue;
                        }

                        ::LogInfoEx(LOG_HOST, "UKEK RSI: %u NAME: %s ALGID: $%02X, KID: $%04X", key.rsiId(), key.name().c_str(), key.algId(), key.kId());

                        m_ukeks.push_back(key);
                        i++;
                    }
                }

                // get LLAs node
                rapidxml::xml_node<>* llas = innerRoot->first_node("LLAs");
                if (llas != nullptr) {
                    uint32_t i = 0U;
                    for (rapidxml::xml_node<>* keyNode = llas->first_node("RSIKeyItem"); keyNode; keyNode = keyNode->next_sibling("RSIKeyItem")) {
                        EKCKeyItem key = EKCKeyItem();
                        key.id(i);

                        rapidxml::xml_node<>* rsiNode = keyNode->first_node("RsiId");
                        if (rsiNode == nullptr) {
                            continue;
                        }
                        key.rsiId(::strtoul(rsiNode->value(), NULL, 10));

                        rapidxml::xml_node<>* nameNode = keyNode->first_node("Name");
                        if (nameNode == nullptr) {
                            continue;
                        }
                        key.name(nameNode->value());

                        rapidxml::xml_node<>* keysetIdNode = keyNode->first_node("KeysetId");
                        if (keysetIdNode == nullptr) {
                            continue;
                        }
                        key.keysetId(::strtoul(keysetIdNode->value(), NULL, 10));

                        rapidxml::xml_node<>* slnNode = keyNode->first_node("Sln");
                        if (slnNode != nullptr) {
                            key.sln(::strtoul(slnNode->value(), NULL, 10));
                        }

                        rapidxml::xml_node<>* algIdNode = keyNode->first_node("AlgorithmId");
                        if (algIdNode == nullptr) {
                            continue;
                        }
                        key.algId(::strtoul(algIdNode->value(), NULL, 10));

                        rapidxml::xml_node<>* kIdNode = keyNode->first_node("KeyId");
                        if (kIdNode == nullptr) {
                            continue;
                        }
                        key.kId(::strtoul(kIdNode->value(), NULL, 10));

                        rapidxml::xml_node<>* keyMatNode = keyNode->first_node("Key");
                        if (keyMatNode == nullptr) {
                            continue;
                        }
                        key.keyMaterial(keyMatNode->value());

                        if (key.isInvalid()) {
                            continue;
                        }

                        ::LogInfoEx(LOG_HOST, "LLA RSI: %u NAME: %s ALGID: $%02X, KID: $%04X", key.rsiId(), key.name().c_str(), key.algId(), key.kId());

                        m_llas.push_back(key);
                        i++;
                    }
                }
            }

            delete[] innerContainer;
        }
    } catch(const std::exception& e) {
        ::LogError(LOG_HOST, "Error opening EKC: %s", e.what());
        return false;
    }

    if (m_keys.size() == 0U && m_ukeks.size() == 0U && m_llas.size() == 0U) {
        ::LogError(LOG_HOST, "No encryption keys defined!");
        return false;
    }

    size_t size = m_keys.size() + m_ukeks.size() + m_llas.size();
    if (size == 0U) {
        return false;
    }

    uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
    m_lastLoadTime = now;

    LogInfoEx(LOG_HOST, "Loaded %lu entries into crypto lookup table (TEK=%lu, UKEK=%lu, LLA=%lu)",
        size, m_keys.size(), m_ukeks.size(), m_llas.size());

    return true;
#endif // !ENABLE_SSL
}
