/**
 * (c) 2019 by Mega Limited, Wellsford, New Zealand
 *
 * This file is part of the MEGA SDK - Client Access Engine.
 *
 * Applications using the MEGA API must present a valid application key
 * and comply with the the rules set forth in the Terms of Service.
 *
 * The MEGA SDK is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * @copyright Simplified (2-clause) BSD License.
 *
 * You should have received a copy of the license along with this
 * program.
 */

#include "../src/crypto/sodium.cpp"
#include "gtest/gtest.h"
#include "mega.h"
#include "utils.h"

#include <cryptopp/hex.h>

#include <array>
#include <math.h>
#include <numeric>

using namespace mega;

// Test encryption/decryption using AES in mode GCM
// (test vectors from 'tlvstore_test.js', in Webclient)
TEST(Crypto, AES_GCM)
{
//    string keyStr = "dGQhii+B7+eLLHRiOA690w==";   //gitleaks:allow
    string keyStr = "dGQhii-B7-eLLHRiOA690w";     //gitleaks:allow Base64 URL encoding
    unsigned keyLen = SymmCipher::KEYLENGTH;
    byte* keyBytes = new byte[keyLen];
    keyLen = static_cast<unsigned>(Base64::atob(keyStr.data(), keyBytes, static_cast<int>(keyLen)));

    string ivStr = "R8q1njARXS7urWv3";
    unsigned ivLen = 12;
    byte* ivBytes = new byte[ivLen];
    ivLen = static_cast<unsigned>(Base64::atob(ivStr.data(), ivBytes, static_cast<int>(ivLen)));

    unsigned tagLen = 16;

    string plainStr = "dGQhwoovwoHDr8OnwossdGI4DsK9w5M";
    auto plainLen = plainStr.length();
    byte* plainBytes = new byte[plainLen];
    plainLen =
        static_cast<size_t>(Base64::atob(plainStr.data(), plainBytes, static_cast<int>(plainLen)));
    string plainText((const char*)plainBytes, plainLen);

    string cipherStr = "L3zqVYAOsRk7zMg2KsNTVShcad8TjIQ7umfsvia21QO0XTj8vaeR";
    auto cipherLen = cipherStr.length();
    byte* cipherBytes = new byte[cipherLen];
    cipherLen = static_cast<size_t>(
        Base64::atob(cipherStr.data(), cipherBytes, static_cast<int>(cipherLen)));
    string cipherText((const char*)cipherBytes, cipherLen);

    SymmCipher key;
    key.setkey(keyBytes, SymmCipher::KEYLENGTH);

    string result;

    // Test AES_GCM_12_16 encryption
    result.clear();
    ASSERT_TRUE(key.gcm_encrypt(&plainText, ivBytes, ivLen, tagLen, &result)) << "GCM encryption failed";

    ASSERT_STREQ(result.data(), cipherText.data()) << "GCM encryption: cipher text doesn't match the expected value";


    // Test AES_GCM_12_16 decryption
    result.clear();
    ASSERT_TRUE(key.gcm_decrypt(&cipherText, ivBytes, ivLen, tagLen, &result)) << "GCM decryption failed";

    ASSERT_STREQ(result.data(), plainText.data()) << "GCM decryption: plain text doesn't match the expected value";

    delete[] keyBytes;
    delete[] ivBytes;
    delete[] plainBytes;
    delete[] cipherBytes;
}



// Test encryption/decryption of the xxTEA algorithm that we use for media file attributes
TEST(Crypto, xxTea)
{
    // two cases with data generated in the javascript version
    {
        uint32_t key1[4] = { 0x00000000, 0x01000000, 0x02000000, 0x03000000 };
        uint32_t data1[16];
        for (unsigned i = sizeof(data1) / sizeof(data1[0]); i--; ) data1[i] = i;
        uint32_t encCmpData[16] = { 140302874, 3625593116, 1921165214, 2581869937, 2444819365, 2195760850, 718076837, 454900461, 2002331402, 793381415, 760353645, 2589596551, 709756921, 4142288381, 633884585, 418697353 };
        xxteaEncrypt(data1, 16, key1);
        ASSERT_TRUE(0 == memcmp(data1, encCmpData, sizeof(data1)));
        xxteaDecrypt(data1, 16, key1);
        for (unsigned i = sizeof(data1) / sizeof(data1[0]); i--; )
        {
            ASSERT_TRUE(data1[i] == i);
        }
    }

    {
        uint32_t key2[4] = { 0, 0xFFFFFFFF,  0xFEFFFFFF, 0xFDFFFFFF };
        uint32_t data2[16];
        for (unsigned i = sizeof(data2) / sizeof(data2[0]); i--;)
            data2[i] = -i;
        uint32_t encCmpData2[16] = { 1331968695, 2520133218, 2881973170, 783802011, 1812010991, 1359505125, 15067484, 3344073997, 4210258643, 824383226, 3584459687, 2866083302, 881254637, 502181030, 680349945, 1722488731 };
        xxteaEncrypt(data2, 16, key2);
        ASSERT_TRUE(0 == memcmp(data2, encCmpData2, sizeof(data2)));
        xxteaDecrypt(data2, 16, key2);
        for (unsigned i = sizeof(data2) / sizeof(data2[0]); i--; )
        {
            ASSERT_TRUE(data2[i] == uint32_t(-(int)i));
        }
    }
}


// Test encryption/decryption using AES in mode CCM
// (test vectors from 'tlvstore_test.js', in Webclient)
TEST(Crypto, AES_CCM)
{
    byte keyBytes[] = {
        0x0f, 0x0e, 0x0d, 0x0c,
        0x0b, 0x0a, 0x09, 0x08,
        0x07, 0x06, 0x05, 0x04,
        0x03, 0x02, 0x01, 0x00 };

    byte ivBytes[] = {
        0x00, 0x01, 0x02, 0x03,
        0x04, 0x05, 0x06, 0x07,
        0x08, 0x09, 0x0a, 0x0b };

    unsigned tagLen = 16;

    byte plainBytes[] = { 0x34, 0x32 };     // "42" in hexadecimal
    string plainText((const char*)plainBytes, sizeof plainBytes);

    byte cipherBytes[] = {
        0x28, 0xbe, 0x1a, 0xc7,
        0xb4, 0x3d, 0x88, 0x68,
        0x86, 0x9b, 0x9a, 0x45,
        0xd3, 0xde, 0x43, 0x6c,
        0xd0, 0xcc };

    string cipherText((const char*)cipherBytes, sizeof cipherBytes);

    SymmCipher key;
    key.setkey(keyBytes, sizeof keyBytes);

    string result;

    // Test AES_CCM_12_16 encryption
    result.clear();
    ASSERT_TRUE(key.ccm_encrypt(&plainText, ivBytes, sizeof ivBytes, tagLen, &result)) << "CCM encryption failed";

    ASSERT_STREQ(result.data(), cipherText.data()) << "CCM encryption: cipher text doesn't match the expected value";


    // Test AES_CCM_12_16 decryption
    result.clear();
    ASSERT_TRUE(key.ccm_decrypt(&cipherText, ivBytes, sizeof ivBytes, tagLen, &result)) << "CCM decryption failed";

    ASSERT_STREQ(result.data(), plainText.data()) << "CCM decryption: plain text doesn't match the expected value";
}

#ifdef ENABLE_CHAT
// Test functions of Ed25519:
// - Binary & Hex fingerprints of public key
// - Creation of signature for RSA public key
// - Verification of signature for RSA public key
// - Creation and verification of signatures for random messages
//
// (test vectors from 'authrigh_test.js', in Webclient)
TEST(Crypto, Ed25519_Signing)
{
//    string prEd255str   = "nWGxne/9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A=";           // Base64
    string prEd255str   = "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A=";           // Base64 URL encoded

//    string puEd255str   = "11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo";            // Base64
    string puEd255str   = "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo";            // Base64 URL encoded

//    string fpEd255str   = "If4x36FUomFia/hUBG/SJxt77Us";                            // Base64
    string fpEd255str   = "If4x36FUomFia_hUBG_SJxt77Us";                            // Base64 url encoded
    string fpEd255hex   = "21FE31DFA154A261626BF854046FD2271B7BED4B";

    string pqstr        = "1XJHwX9WYEVk7KOack5nhOgzgnYWrVdt0UY2yn5Lw38mPzkVn"
                          "kHCmguqWIfL5bzVpbHHhlG9yHumvyyu9r1gKUMz4Y/1cf69"
                          "1WIQmRGfg8dB2TeRUSvwb2A7EFGeFqQZHclgvpM2aq4PXrP"
                          "PmQAciTxjguxcL1lem/fXGd1X6KKxPJ+UfQ5TZbV4O2aOwY"
                          "uxys1YHh3mNHEp/xE1/fx292hdejPTJIX8IC5zjsss76e9P"
                          "SVOgSrz+jQQYKbKpT5Yamml98bEZuLY9ncMGUmw5q4WHi/O"
                          "dcvskHUydAL0qNOqbCwvt1Y7xIQfclR0SQE/AbwuJui0mt3"
                          "PuGjM42T/DQ==";
    string estr         = "AQE=";

    string fpRSAstr     = "GN2sWsukWnEarqVPS7mE5sPro38";                            // Base64 url encoded
    string fpRSAhex     = "18ddac5acba45a711aaea54f4bb984e6c3eba37f";

    string sigRSAstr    = "AAAAAFPqtrj3Qr4d83Oz/Ya6svzJfeoSBtWPC7KBU4"           // Base64
                          "KqWMI8OX3eXT45+IyWCTTA5yeip/GThvkS8O2HBF"
                          "aNLvSAFq5/5lQG";

    uint64_t sigRSAts   = 1407891128; // authring_test.js specify 1407891127650 ms, which is later rounded to seconds


    // Initialize variables

    const int keySeedLen = EdDSA::SEED_KEY_LENGTH;
    unsigned char keySeed[keySeedLen];
    ASSERT_EQ(keySeedLen, Base64::atob(prEd255str.data(), keySeed, keySeedLen))
            << "Failed to convert Ed25519 private key to binary";

    PrnGen rng;
    EdDSA signkey(rng, keySeed);

    string puEd255bin;
    puEd255bin.resize(puEd255str.size() * 3 / 4 + 3);
    puEd255bin.resize(static_cast<size_t>(Base64::atob(puEd255str.data(),
                                                       (byte*)puEd255bin.data(),
                                                       static_cast<int>(puEd255bin.size()))));
    ASSERT_TRUE(!memcmp(puEd255bin.data(), signkey.pubKey, EdDSA::PUBLIC_KEY_LENGTH))
            << "Public Ed25519 key doesn't match the derived public key";

    // convert from Base64 to Base64 URL encoding
    std::replace(pqstr.begin(), pqstr.end(), '+', '-');
    std::replace(pqstr.begin(), pqstr.end(), '/', '_');

    string pqbin;
    pqbin.resize(pqstr.size() * 3 / 4 + 3);
    pqbin.resize(static_cast<size_t>(
        Base64::atob(pqstr.data(), (byte*)pqbin.data(), static_cast<int>(pqbin.size()))));

    string ebin;
    ebin.resize(estr.size() * 3 / 4 + 3);
    ebin.resize(static_cast<size_t>(
        Base64::atob(estr.data(), (byte*)ebin.data(), static_cast<int>(ebin.size()))));

    string pubRSAbin;
    pubRSAbin.append(pqbin.data(), pqbin.size());
    pubRSAbin.append(ebin.data(), ebin.size());

    // convert from Base64 to Base64 URL encoding
    std::replace(sigRSAstr.begin(), sigRSAstr.end(), '+', '-');
    std::replace(sigRSAstr.begin(), sigRSAstr.end(), '/', '_');

    string sigRSAbin;
    sigRSAbin.resize(sigRSAstr.size() * 4 / 3 + 4);
    sigRSAbin.resize(static_cast<size_t>(Base64::atob(sigRSAstr.data(),
                                                      (byte*)sigRSAbin.data(),
                                                      static_cast<int>(sigRSAbin.size()))));

    // ____ Check signature of RSA public key ____

    string sigPubk;
    signkey.signKey((unsigned char *) pubRSAbin.data(), pubRSAbin.size(), &sigPubk, sigRSAts);

    ASSERT_EQ(sigRSAbin.size(), sigPubk.size()) << "Wrong size of signature";
    ASSERT_TRUE(!memcmp(sigRSAbin.data(), sigPubk.data(), sigRSAbin.size())) << "RSA signatures don't match";


    // ____ Verify signature of RSA public key ____

    // good signature
    bool sigOK = EdDSA::verifyKey((unsigned char*) pubRSAbin.data(), pubRSAbin.size(),
                                   &sigRSAbin, (unsigned char*) puEd255bin.data());
    ASSERT_TRUE(sigOK) << "Verification of RSA signature failed.";

    // bad signature
    string sigBuf = sigRSAbin;
    sigBuf.at(70) = 42;
    sigOK = EdDSA::verifyKey((unsigned char*) pubRSAbin.data(), pubRSAbin.size(),
                                       &sigBuf, (unsigned char*) puEd255bin.data());
    ASSERT_FALSE(sigOK) << "Verification of bad RSA signature succeed when it should fail.";

    // empty signature
    sigBuf.clear();
    sigOK = EdDSA::verifyKey((unsigned char*) pubRSAbin.data(), pubRSAbin.size(),
                                       &sigBuf, (unsigned char*) puEd255bin.data());
    ASSERT_FALSE(sigOK) << "Verification of empty RSA signature succeed when it should fail.";

    // bad timestamp
    sigBuf = sigRSAbin;
    sigBuf.at(0) = 42;
    sigOK = EdDSA::verifyKey((unsigned char*) pubRSAbin.data(), pubRSAbin.size(),
                                       &sigBuf, (unsigned char*) puEd255bin.data());
    ASSERT_FALSE(sigOK) << "Verification of RSA signature with wrong timestamp succeed when it should fail.";

    // signature with bad point
    sigBuf = sigRSAbin;
    sigBuf.at(8) = 42;
    sigOK = EdDSA::verifyKey((unsigned char*) pubRSAbin.data(), pubRSAbin.size(),
                                       &sigBuf, (unsigned char*) puEd255bin.data());
    ASSERT_FALSE(sigOK) << "Verification of RSA signature with bad point succeed when it should fail.";


    // ____ Create and verify signatures of random messages ____

    const unsigned keylen = SymmCipher::KEYLENGTH;
    byte key[keylen];
    string sig;
    for (int i = 0; i < 100; i++)
    {
        rng.genblock(key, keylen);
        signkey.signKey((unsigned char *) key, keylen, &sig);

        ASSERT_TRUE(EdDSA::verifyKey((unsigned char*) pubRSAbin.data(), pubRSAbin.size(),
                                      &sigRSAbin, (unsigned char*) puEd255bin.data()))
                << "Verification of signature failed for a random key.";
    }
}

#endif

TEST(Crypto, SymmCipher_xorblock_bytes)
{
    byte src[10] = { (byte)0, (byte)1, (byte)2, (byte)3, (byte)4, (byte)5, (byte)6, (byte)7, (byte)8, (byte)9 };
    byte dest[10] = { (byte)20, (byte)30, (byte)40, (byte)50, (byte)60, (byte)70, (byte)80, (byte)90, (byte)100, (byte)110 };
    SymmCipher::xorblock(src, dest, sizeof(dest));
    byte result[10] = { (byte)(0 ^ (byte)20), (byte)(1 ^ (byte)30), (byte)(2 ^ (byte)40), (byte)(3 ^ (byte)50), (byte)(4 ^ (byte)60), (byte)(5 ^ (byte)70), (byte)(6 ^ (byte)80), (byte)(7 ^ (byte)90), (byte)(8 ^ (byte)100), (byte)(9 ^ (byte)110) };
    ASSERT_EQ(memcmp(dest, result, sizeof(dest)), 0);
}

TEST(Crypto, SymmCipher_xorblock_block_aligned)
{
    byte src[SymmCipher::BLOCKSIZE];
    byte n = 0;
    std::generate(src, src + sizeof(src), [&n]() {return n++; });
    ASSERT_EQ(reinterpret_cast<ptrdiff_t>(src) % static_cast<ptrdiff_t>(sizeof(ptrdiff_t)), 0);

    byte dest[SymmCipher::BLOCKSIZE];
    n = 100;
    std::generate(dest, dest + sizeof(src), [&n]() { return n = static_cast<byte>(n + 3); });
    ASSERT_EQ(reinterpret_cast<ptrdiff_t>(dest) % static_cast<ptrdiff_t>(sizeof(ptrdiff_t)), 0);

    byte result[SymmCipher::BLOCKSIZE];
    byte* output = result;
    std::transform(src, src + sizeof(src), dest, output, [](byte a, byte b) { return (byte)(a ^ b); });
    SymmCipher::xorblock(src, dest); // aligned case

    ASSERT_EQ(memcmp(dest, result, sizeof(dest)), 0);
}

TEST(Crypto, SymmCipher_xorblock_block_unaligned)
{
    byte src[SymmCipher::BLOCKSIZE + 1];
    byte n = 0;
    std::generate(src, src + sizeof(src), [&n]() {return n++; });

    byte dest[SymmCipher::BLOCKSIZE];
    n = 100;
    std::generate(dest, dest + sizeof(dest), [&n]() { return n = static_cast<byte>(n + 3); });

    byte result[SymmCipher::BLOCKSIZE];
    byte* output = result;
    std::transform(src + 1, src + sizeof(src), dest, output, [](byte a, byte b) { return (byte)(a ^ b); });
    SymmCipher::xorblock(src + 1, dest); // un-aligned case

    ASSERT_EQ(memcmp(dest, result, sizeof(dest)), 0);
}

// Test SymmCipher::isZeroKey
//
// Test whether a key is a zerokey or generated with a zerokey
// Use different data structures (byte[], byte*, std::vector<byte>, std::string)
//
// 1) Test a 16-byte key all zeros - isZeroKey should be true
// 2) Test a 16-byte key all ones - isZeroKey should be false
// 3) Test a 32-byte key all zeros - isZeroKey should be true
// 4) Test a 32-byte key all ones - isZeroKey should be true
// 5) Test a 32-byte key half zeros, half ones - isZeroKey should be false
// 6) Test a 32-byte key: "0123456789ABCDEF0123456789ABCDEF" - isZeroKey should be true //gitleaks:allow
TEST(Crypto, SymmCipher_isZeroKey)
{
    // 1) Test a 16-byte key all zeros - isZeroKey should be true
    byte key_test1[SymmCipher::BLOCKSIZE] = {};
    ASSERT_EQ(SymmCipher::isZeroKey(key_test1, SymmCipher::BLOCKSIZE), true);

    // 2) Test a 16-byte key all ones - isZeroKey should be false
    auto key_test2 = std::make_unique<byte[]>(SymmCipher::BLOCKSIZE);
    std::memset(key_test2.get(), 1, SymmCipher::BLOCKSIZE);
    ASSERT_EQ(SymmCipher::isZeroKey(key_test2.get(), SymmCipher::BLOCKSIZE), false);

    // 3) Test a 32-byte key all zeros - isZeroKey should be true
    std::vector<byte> key_test3(FILENODEKEYLENGTH, 0);
    ASSERT_EQ(SymmCipher::isZeroKey(key_test3.data(), FILENODEKEYLENGTH), true);

    // 4) Test a 32-byte key all ones - isZeroKey should be true
    std::string key_test4(FILENODEKEYLENGTH, 1);
    ASSERT_EQ(SymmCipher::isZeroKey(reinterpret_cast<byte*>(key_test4.data()), FILENODEKEYLENGTH), true);

    // 5) Test a 32-byte key half zeros, half ones - isZeroKey should be false
    byte key_test5[FILENODEKEYLENGTH];
    std::memset(key_test5, 0, SymmCipher::BLOCKSIZE);
    std::memset(key_test5 + SymmCipher::BLOCKSIZE, 1, SymmCipher::BLOCKSIZE);
    ASSERT_EQ(SymmCipher::isZeroKey(key_test5, FILENODEKEYLENGTH), false);

    // 6) Test a 32-byte key: "0123456789ABCDEF0123456789ABCDEF" - isZeroKey should be true //gitleaks:allow
    std::string key_test6;
    key_test6.resize(FILENODEKEYLENGTH);
    key_test6.replace(0, SymmCipher::BLOCKSIZE, "0123456789ABCDEF");
    key_test6.replace(SymmCipher::BLOCKSIZE, SymmCipher::BLOCKSIZE, "0123456789ABCDEF");
    ASSERT_EQ(SymmCipher::isZeroKey(reinterpret_cast<byte*>(key_test6.data()), FILENODEKEYLENGTH), true);
}

using CryptoPP::AutoSeededRandomPool;

namespace
{

AutoSeededRandomPool rng;

std::vector<byte> randomBytes(const size_t n)
{
    std::vector<byte> buf(n);
    rng.GenerateBlock(buf.data(), buf.size());
    return buf;
}

::testing::AssertionResult
    equalBuf(const void* const a, const void* const b, const size_t len, const char* msg)
{
    if (std::memcmp(a, b, len) == 0)
        return ::testing::AssertionSuccess();
    return ::testing::AssertionFailure() << msg;
}

} // namespace

TEST(Crypto, SymmCipher_CbcEncryptDecryptWithKey)
{
    SymmCipher cipher;
    const std::string plain =
        "Somewhere in la Mancha, in a place whose name I do not care to remember";
    std::string cipherText, recovered;

    const auto key = randomBytes(SymmCipher::KEYLENGTH);
    const auto iv = randomBytes(SymmCipher::BLOCKSIZE);

    ASSERT_TRUE(cipher.cbc_encrypt_with_key(plain, cipherText, key.data(), key.size(), iv.data()));
    ASSERT_TRUE(
        cipher.cbc_decrypt_with_key(cipherText, recovered, key.data(), key.size(), iv.data()));

    EXPECT_EQ(plain, recovered);
}

TEST(Crypto, SymmCipher_CbcEncryptWithZeroKeyLenFails)
{
    SymmCipher cipher;
    const std::string plain = "irrelevant";
    std::string cipherText;

    const auto key = randomBytes(SymmCipher::KEYLENGTH);
    const auto iv = randomBytes(SymmCipher::BLOCKSIZE);

    const auto ok =
        cipher.cbc_encrypt_with_key(plain, cipherText, key.data(), /*keylen*/ 0, iv.data());
    EXPECT_FALSE(ok);
}

TEST(Crypto, SymmCipher_GcmEncryptDecryptReuseKey)
{
    SymmCipher cipher;

    // set the internal key once
    const auto internalKey = randomBytes(SymmCipher::KEYLENGTH);
    cipher.setkey(internalKey.data());

    const std::string plain = "Chat message with Paco";

    auto testFunc = [&cipher, &plain](unsigned int tagLen)
    {
        std::string cipherText, recovered;

        const auto iv = randomBytes(12); // 96-bit nonce

        ASSERT_TRUE(cipher.gcm_encrypt(&plain,
                                       iv.data(),
                                       static_cast<unsigned>(iv.size()),
                                       tagLen,
                                       &cipherText));

        ASSERT_TRUE(cipher.gcm_decrypt(&cipherText,
                                       iv.data(),
                                       static_cast<unsigned>(iv.size()),
                                       tagLen,
                                       &recovered));
        EXPECT_EQ(plain, recovered);
    };

    testFunc(16);
    testFunc(12);
}

TEST(Crypto, SymmCipher_GcmEncryptDecryptReuseKeyWithAAD)
{
    SymmCipher cipher;

    const auto key = randomBytes(SymmCipher::KEYLENGTH);
    cipher.setkey(key.data());

    const std::string plain = "Chat message with Paco";
    constexpr byte aad[] = {1, 2, 3, 4};

    const auto iv = randomBytes(12); // 96-bit nonce
    constexpr auto tagLen = 16u;

    std::string cipherText;
    ASSERT_TRUE(cipher.gcm_encrypt_add(reinterpret_cast<const byte*>(plain.data()),
                                       plain.size(),
                                       aad,
                                       sizeof(aad),
                                       iv.data(),
                                       iv.size(),
                                       tagLen,
                                       cipherText,
                                       /*expectedSize =*/0));

    const auto ctLen = cipherText.size() - tagLen;
    const byte* ctPtr = reinterpret_cast<const byte*>(cipherText.data());
    const byte* tagPtr = ctPtr + ctLen;

    std::vector<byte> recoveredBuf(plain.size());
    ASSERT_TRUE(cipher.gcm_decrypt_add(ctPtr,
                                       ctLen,
                                       aad,
                                       sizeof(aad),
                                       tagPtr,
                                       tagLen,
                                       iv.data(),
                                       iv.size(),
                                       recoveredBuf.data(),
                                       recoveredBuf.size()));

    std::string recovered(recoveredBuf.begin(), recoveredBuf.end());
    EXPECT_EQ(plain, recovered);
}

TEST(Crypto, SymmCipher_GcmEncryptReuseKeyWithAAD_ReturnFalse)
{
    SymmCipher cipher;

    const auto key = randomBytes(SymmCipher::KEYLENGTH);
    cipher.setkey(key.data());

    const std::string plain = "Test Encryption Return false";
    constexpr byte aad[] = {1, 2, 3, 4};

    const auto iv = randomBytes(12); // 96-bit nonce
    constexpr auto tagLen = 16u;
    std::string cipherText;

    // data is null
    EXPECT_FALSE(cipher.gcm_encrypt_add(nullptr,
                                        plain.size(),
                                        aad,
                                        sizeof(aad),
                                        iv.data(),
                                        iv.size(),
                                        tagLen,
                                        cipherText,
                                        /*expectedSize =*/0));
    // data size is 0
    EXPECT_FALSE(cipher.gcm_encrypt_add(reinterpret_cast<const byte*>(plain.data()),
                                        0,
                                        aad,
                                        sizeof(aad),
                                        iv.data(),
                                        iv.size(),
                                        tagLen,
                                        cipherText,
                                        /*expectedSize =*/0));
    // AAD is null
    EXPECT_FALSE(cipher.gcm_encrypt_add(reinterpret_cast<const byte*>(plain.data()),
                                        plain.size(),
                                        nullptr,
                                        sizeof(aad),
                                        iv.data(),
                                        iv.size(),
                                        tagLen,
                                        cipherText,
                                        /*expectedSize =*/0));
    // AAD size is 0
    EXPECT_FALSE(cipher.gcm_encrypt_add(reinterpret_cast<const byte*>(plain.data()),
                                        plain.size(),
                                        aad,
                                        0,
                                        iv.data(),
                                        iv.size(),
                                        tagLen,
                                        cipherText,
                                        /*expectedSize =*/0));
    // iv is null
    EXPECT_FALSE(cipher.gcm_encrypt_add(reinterpret_cast<const byte*>(plain.data()),
                                        plain.size(),
                                        aad,
                                        sizeof(aad),
                                        nullptr,
                                        iv.size(),
                                        tagLen,
                                        cipherText,
                                        /*expectedSize =*/0));
    // iv size is 0
    EXPECT_FALSE(cipher.gcm_encrypt_add(reinterpret_cast<const byte*>(plain.data()),
                                        plain.size(),
                                        aad,
                                        sizeof(aad),
                                        iv.data(),
                                        0,
                                        tagLen,
                                        cipherText,
                                        /*expectedSize =*/0));
    // tag length is 0
    EXPECT_FALSE(cipher.gcm_encrypt_add(reinterpret_cast<const byte*>(plain.data()),
                                        plain.size(),
                                        aad,
                                        sizeof(aad),
                                        iv.data(),
                                        iv.size(),
                                        0,
                                        cipherText,
                                        /*expectedSize =*/0));

    // expected size is not equal to cipher text size
    EXPECT_FALSE(cipher.gcm_encrypt_add(reinterpret_cast<const byte*>(plain.data()),
                                        plain.size(),
                                        aad,
                                        sizeof(aad),
                                        iv.data(),
                                        iv.size(),
                                        tagLen,
                                        cipherText,
                                        /*expectedSize =*/1));
}

TEST(Crypto, SymmCipher_GcmDecryptReuseKeyWithAAD_ReturnFalse)
{
    SymmCipher cipher;

    const auto key = randomBytes(SymmCipher::KEYLENGTH);
    cipher.setkey(key.data());

    const std::string plain = "Test Decryption Return false";
    constexpr byte aad[] = {1, 2, 3, 4};

    const auto iv = randomBytes(12); // 96-bit nonce
    constexpr auto tagLen = 16u;

    std::string cipherText;
    std::vector<byte> recoveredBuf(plain.size());

    ASSERT_TRUE(cipher.gcm_encrypt_add(reinterpret_cast<const byte*>(plain.data()),
                                       plain.size(),
                                       aad,
                                       sizeof(aad),
                                       iv.data(),
                                       iv.size(),
                                       tagLen,
                                       cipherText,
                                       /*expectedSize =*/0));

    const auto ctLen = cipherText.size() - tagLen;
    const byte* ctPtr = reinterpret_cast<const byte*>(cipherText.data());
    const byte* tagPtr = ctPtr + ctLen;

    // data is null
    EXPECT_FALSE(cipher.gcm_decrypt_add(nullptr,
                                        ctLen,
                                        aad,
                                        sizeof(aad),
                                        tagPtr,
                                        tagLen,
                                        iv.data(),
                                        iv.size(),
                                        recoveredBuf.data(),
                                        recoveredBuf.size()));
    // data size is 0
    EXPECT_FALSE(cipher.gcm_decrypt_add(ctPtr,
                                        0,
                                        aad,
                                        sizeof(aad),
                                        tagPtr,
                                        tagLen,
                                        iv.data(),
                                        iv.size(),
                                        recoveredBuf.data(),
                                        recoveredBuf.size()));
    // AAD is null
    EXPECT_FALSE(cipher.gcm_decrypt_add(ctPtr,
                                        ctLen,
                                        nullptr,
                                        sizeof(aad),
                                        tagPtr,
                                        tagLen,
                                        iv.data(),
                                        iv.size(),
                                        recoveredBuf.data(),
                                        recoveredBuf.size()));
    // AAD size is 0
    EXPECT_FALSE(cipher.gcm_decrypt_add(ctPtr,
                                        ctLen,
                                        aad,
                                        0,
                                        tagPtr,
                                        tagLen,
                                        iv.data(),
                                        iv.size(),
                                        recoveredBuf.data(),
                                        recoveredBuf.size()));
    // tag is null
    EXPECT_FALSE(cipher.gcm_decrypt_add(ctPtr,
                                        ctLen,
                                        aad,
                                        sizeof(aad),
                                        nullptr,
                                        tagLen,
                                        iv.data(),
                                        iv.size(),
                                        recoveredBuf.data(),
                                        recoveredBuf.size()));
    // tag size is 0
    EXPECT_FALSE(cipher.gcm_decrypt_add(ctPtr,
                                        ctLen,
                                        aad,
                                        sizeof(aad),
                                        tagPtr,
                                        0,
                                        iv.data(),
                                        iv.size(),
                                        recoveredBuf.data(),
                                        recoveredBuf.size()));
    // iv is null
    EXPECT_FALSE(cipher.gcm_decrypt_add(ctPtr,
                                        ctLen,
                                        aad,
                                        sizeof(aad),
                                        tagPtr,
                                        tagLen,
                                        nullptr,
                                        iv.size(),
                                        recoveredBuf.data(),
                                        recoveredBuf.size()));
    // iv size is 0
    EXPECT_FALSE(cipher.gcm_decrypt_add(ctPtr,
                                        ctLen,
                                        aad,
                                        sizeof(aad),
                                        tagPtr,
                                        tagLen,
                                        iv.data(),
                                        0,
                                        recoveredBuf.data(),
                                        recoveredBuf.size()));
    // result buf is null
    EXPECT_FALSE(cipher.gcm_decrypt_add(ctPtr,
                                        ctLen,
                                        aad,
                                        sizeof(aad),
                                        tagPtr,
                                        tagLen,
                                        iv.data(),
                                        iv.size(),
                                        nullptr,
                                        recoveredBuf.size()));
    // result buf size is 0
    EXPECT_FALSE(cipher.gcm_decrypt_add(ctPtr,
                                        ctLen,
                                        aad,
                                        sizeof(aad),
                                        tagPtr,
                                        tagLen,
                                        iv.data(),
                                        iv.size(),
                                        recoveredBuf.data(),
                                        0));
}

TEST(Crypto, SymmCipher_KeyRotationBreaksOldCipher)
{
    SymmCipher cipher;
    const auto iv = randomBytes(SymmCipher::BLOCKSIZE);

    // first key
    const auto k1 = randomBytes(SymmCipher::KEYLENGTH);
    cipher.setkey(k1.data());

    constexpr byte kPlain[SymmCipher::BLOCKSIZE] =
        {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
    byte data1[SymmCipher::BLOCKSIZE];
    std::memcpy(data1, kPlain, SymmCipher::BLOCKSIZE);
    cipher.cbc_encrypt(data1, sizeof(data1), iv.data());

    // second key
    const auto k2 = randomBytes(SymmCipher::KEYLENGTH);
    cipher.setkey(k2.data());

    byte tmp[SymmCipher::BLOCKSIZE];
    std::memcpy(tmp, data1, sizeof(tmp));
    ASSERT_TRUE(cipher.cbc_decrypt(tmp, sizeof(tmp), iv.data()));
    EXPECT_FALSE(equalBuf(tmp,
                          kPlain,
                          SymmCipher::BLOCKSIZE,
                          "Decrypt unexpectedly produced the right data"));

    cipher.setkey(k1.data());
    ASSERT_TRUE(cipher.cbc_decrypt(data1, sizeof(data1), iv.data()));
    EXPECT_TRUE(equalBuf(data1, kPlain, SymmCipher::BLOCKSIZE, "Round-trip failed"));
}

// Regression test: verifies ctr_crypt produces identical ciphertext and MAC
// across implementation changes.  Reference vectors were captured from the
// original per-block ecb_encrypt implementation.
TEST(Crypto, CtrCrypt_RegressionVectors)
{
    using Block = std::array<byte, SymmCipher::BLOCKSIZE>;

    constexpr Block key = {0x0f,
                           0x0e,
                           0x0d,
                           0x0c,
                           0x0b,
                           0x0a,
                           0x09,
                           0x08,
                           0x07,
                           0x06,
                           0x05,
                           0x04,
                           0x03,
                           0x02,
                           0x01,
                           0x00};

    SymmCipher cipher;
    cipher.setkey(key.data());

    constexpr uint64_t ctriv = 0x0102030405060708ULL;

    // --- Case 1: single aligned block, encrypt with MAC ---
    {
        constexpr Block plaintext = {0x00,
                                     0x11,
                                     0x22,
                                     0x33,
                                     0x44,
                                     0x55,
                                     0x66,
                                     0x77,
                                     0x88,
                                     0x99,
                                     0xAA,
                                     0xBB,
                                     0xCC,
                                     0xDD,
                                     0xEE,
                                     0xFF};
        constexpr Block expectedCt = {0x74,
                                      0x71,
                                      0x52,
                                      0x94,
                                      0xCD,
                                      0x03,
                                      0x9C,
                                      0xC2,
                                      0xDB,
                                      0x11,
                                      0x5C,
                                      0x36,
                                      0x05,
                                      0x82,
                                      0xC4,
                                      0xCC};
        constexpr Block expectedMac = {0xF4,
                                       0xBB,
                                       0x47,
                                       0x7D,
                                       0xBF,
                                       0x92,
                                       0xCF,
                                       0xF2,
                                       0xDE,
                                       0xA9,
                                       0x59,
                                       0xDA,
                                       0x9F,
                                       0xD4,
                                       0xF4,
                                       0xC2};

        auto data = plaintext;
        Block mac = {};

        cipher.ctr_crypt(data.data(), data.size(), 0, ctriv, mac.data(), true);

        EXPECT_EQ(data, expectedCt) << "Case 1: ciphertext mismatch";
        EXPECT_EQ(mac, expectedMac) << "Case 1: MAC mismatch";

        Block decMac = {};
        cipher.ctr_crypt(data.data(), data.size(), 0, ctriv, decMac.data(), false);

        EXPECT_EQ(data, plaintext) << "Case 1: round-trip failed";
        EXPECT_EQ(decMac, expectedMac) << "Case 1: decrypt MAC mismatch";
    }

    // --- Case 2: three aligned blocks, encrypt with MAC ---
    {
        constexpr unsigned kLen = SymmCipher::BLOCKSIZE * 3;
        using ThreeBlocks = std::array<byte, kLen>;

        ThreeBlocks plaintext;
        std::iota(plaintext.begin(), plaintext.end(), byte{0});

        constexpr ThreeBlocks expectedCt = {
            0x74, 0x61, 0x72, 0xA4, 0x8D, 0x53, 0xFC, 0xB2, 0x5B, 0x81, 0xFC, 0x86,
            0xC5, 0x52, 0x24, 0x3C, 0x68, 0x55, 0x04, 0xC0, 0x65, 0x51, 0x92, 0x47,
            0x34, 0x0D, 0x5F, 0xBF, 0xE7, 0x16, 0x6D, 0xA6, 0x41, 0xC8, 0xEF, 0xC8,
            0x24, 0xA8, 0x9F, 0x7D, 0x79, 0x84, 0x03, 0xB5, 0xA6, 0xED, 0x9B, 0xD4};
        constexpr Block expectedMac = {0xA9,
                                       0x16,
                                       0x6E,
                                       0x2F,
                                       0x31,
                                       0x09,
                                       0xF0,
                                       0xCB,
                                       0xEA,
                                       0x56,
                                       0xD3,
                                       0x38,
                                       0xBE,
                                       0xDB,
                                       0xF8,
                                       0x95};

        auto data = plaintext;
        Block mac = {};

        cipher.ctr_crypt(data.data(), kLen, 0, ctriv, mac.data(), true);

        EXPECT_EQ(data, expectedCt) << "Case 2: ciphertext mismatch";
        EXPECT_EQ(mac, expectedMac) << "Case 2: MAC mismatch";

        Block decMac = {};
        cipher.ctr_crypt(data.data(), kLen, 0, ctriv, decMac.data(), false);

        EXPECT_EQ(data, plaintext) << "Case 2: round-trip failed";
        EXPECT_EQ(decMac, expectedMac) << "Case 2: decrypt MAC mismatch";
    }

    // --- Case 3: non-zero position, encrypt with MAC ---
    {
        constexpr Block plaintext = {0xDE,
                                     0xAD,
                                     0xBE,
                                     0xEF,
                                     0xCA,
                                     0xFE,
                                     0xBA,
                                     0xBE,
                                     0x01,
                                     0x23,
                                     0x45,
                                     0x67,
                                     0x89,
                                     0xAB,
                                     0xCD,
                                     0xEF};
        constexpr Block expectedCt = {0x15,
                                      0xF9,
                                      0xB7,
                                      0xDB,
                                      0xBF,
                                      0x87,
                                      0xD8,
                                      0x63,
                                      0x0A,
                                      0x28,
                                      0xB2,
                                      0x93,
                                      0x50,
                                      0xFE,
                                      0xF2,
                                      0x68};
        constexpr Block expectedMac = {0xB9,
                                       0xC5,
                                       0xE4,
                                       0x82,
                                       0x13,
                                       0x8D,
                                       0xAA,
                                       0xC6,
                                       0x09,
                                       0xD5,
                                       0x3C,
                                       0x53,
                                       0x95,
                                       0x84,
                                       0x3F,
                                       0x71};

        auto data = plaintext;
        Block mac = {};
        constexpr m_off_t pos = SymmCipher::BLOCKSIZE * 4;

        cipher.ctr_crypt(data.data(), data.size(), pos, ctriv, mac.data(), true);

        EXPECT_EQ(data, expectedCt) << "Case 3: ciphertext mismatch";
        EXPECT_EQ(mac, expectedMac) << "Case 3: MAC mismatch";

        Block decMac = {};
        cipher.ctr_crypt(data.data(), data.size(), pos, ctriv, decMac.data(), false);

        EXPECT_EQ(data, plaintext) << "Case 3: round-trip failed";
        EXPECT_EQ(decMac, expectedMac) << "Case 3: decrypt MAC mismatch";
    }

    // --- Case 4: encrypt without MAC ---
    {
        constexpr Block plaintext = {0xAA,
                                     0xBB,
                                     0xCC,
                                     0xDD,
                                     0xEE,
                                     0xFF,
                                     0x00,
                                     0x11,
                                     0x22,
                                     0x33,
                                     0x44,
                                     0x55,
                                     0x66,
                                     0x77,
                                     0x88,
                                     0x99};
        constexpr Block expectedCt = {0xDE,
                                      0xDB,
                                      0xBC,
                                      0x7A,
                                      0x67,
                                      0xA9,
                                      0xFA,
                                      0xA4,
                                      0x71,
                                      0xBB,
                                      0xB2,
                                      0xD8,
                                      0xAF,
                                      0x28,
                                      0xA2,
                                      0xAA};

        auto data = plaintext;

        cipher.ctr_crypt(data.data(), data.size(), 0, ctriv, nullptr, true);

        EXPECT_EQ(data, expectedCt) << "Case 4: ciphertext mismatch";

        cipher.ctr_crypt(data.data(), data.size(), 0, ctriv, nullptr, false);

        EXPECT_EQ(data, plaintext) << "Case 4: round-trip failed";
    }

    // --- Case 5: unaligned len (20 bytes), NUL-padded buffer ---
    {
        constexpr unsigned kActualLen = 20;
        using TwoBlocks = std::array<byte, SymmCipher::BLOCKSIZE * 2>;

        TwoBlocks plaintext = {};
        std::iota(plaintext.begin(), std::next(plaintext.begin(), kActualLen), byte{0xF0});

        constexpr TwoBlocks expectedCt = {0x84, 0x91, 0x82, 0x54, 0x7D, 0xA3, 0x0C, 0x42,
                                          0xAB, 0x71, 0x0C, 0x76, 0x35, 0xA2, 0xD4, 0xCC,
                                          0x78, 0x45, 0x14, 0xD0, 0x71, 0x44, 0x84, 0x50,
                                          0x2C, 0x14, 0x45, 0xA4, 0xFB, 0x0B, 0x73, 0xB9};
        constexpr Block expectedMac = {0x52,
                                       0xDF,
                                       0x31,
                                       0x97,
                                       0x96,
                                       0x73,
                                       0x74,
                                       0x95,
                                       0xC0,
                                       0x2D,
                                       0x27,
                                       0xF5,
                                       0xFB,
                                       0x80,
                                       0xE1,
                                       0xBD};

        auto data = plaintext;
        Block mac = {};

        cipher.ctr_crypt(data.data(), kActualLen, 0, ctriv, mac.data(), true);

        EXPECT_EQ(data, expectedCt) << "Case 5: ciphertext mismatch";
        EXPECT_EQ(mac, expectedMac) << "Case 5: MAC mismatch";

        Block decMac = {};
        data = expectedCt;
        cipher.ctr_crypt(data.data(), kActualLen, 0, ctriv, decMac.data(), false);

        EXPECT_TRUE(
            std::equal(plaintext.begin(), std::next(plaintext.begin(), kActualLen), data.begin()))
            << "Case 5: round-trip failed";
        EXPECT_EQ(decMac, expectedMac) << "Case 5: decrypt MAC mismatch";
    }
}

namespace
{
std::string keyBytesToString(const unsigned char* key, int len)
{
    return std::string(reinterpret_cast<const char*>(key), static_cast<size_t>(len));
}
}

TEST(Crypto, ECDH_DefaultCtor_Initializes)
{
    ECDH ecdh;
    EXPECT_TRUE(ecdh.initializationOK);
}

// The private-key constructor rejects a wrong-sized key and accepts a correct one.
TEST(Crypto, ECDH_PrivKeyCtor_ValidatesSize)
{
    ECDH bad(std::string(ECDH::PRIVATE_KEY_LENGTH - 1, '\x01'));
    EXPECT_FALSE(bad.initializationOK);

    ECDH good(std::string(ECDH::PRIVATE_KEY_LENGTH, '\x01'));
    EXPECT_TRUE(good.initializationOK);
}

TEST(Crypto, ECDH_PrivPubCtor_SetsInitializationOK)
{
    ECDH generated; // a valid key pair to source bytes from
    ASSERT_TRUE(generated.initializationOK);

    const std::string pub = keyBytesToString(generated.getPubKey(), ECDH::PUBLIC_KEY_LENGTH);
    ECDH ecdh(generated.getPrivKey(), pub);

    EXPECT_TRUE(ecdh.initializationOK);
    EXPECT_EQ(keyBytesToString(ecdh.getPrivKey(), ECDH::PRIVATE_KEY_LENGTH),
              keyBytesToString(generated.getPrivKey(), ECDH::PRIVATE_KEY_LENGTH));
    EXPECT_EQ(keyBytesToString(ecdh.getPubKey(), ECDH::PUBLIC_KEY_LENGTH), pub);
}

// The (privk, pubk) constructor must reject a too-short public key instead of reading
// PUBLIC_KEY_LENGTH bytes past the end of the string (out-of-bounds read, caught under ASan).
TEST(Crypto, ECDH_PrivPubCtor_RejectsShortPubKey)
{
    ECDH generated;
    ASSERT_TRUE(generated.initializationOK);

    const std::string shortPub =
        keyBytesToString(generated.getPubKey(), ECDH::PUBLIC_KEY_LENGTH - 1); // one byte short
    ECDH ecdh(generated.getPrivKey(), shortPub);

    EXPECT_FALSE(ecdh.initializationOK);
}

// The copy constructor must propagate the source's validity
TEST(Crypto, ECDH_CopyCtor_PropagatesInitializationOK)
{
    ECDH original;
    ASSERT_TRUE(original.initializationOK);

    ECDH copyOfValid(original);
    EXPECT_TRUE(copyOfValid.initializationOK);
    EXPECT_EQ(keyBytesToString(copyOfValid.getPrivKey(), ECDH::PRIVATE_KEY_LENGTH),
              keyBytesToString(original.getPrivKey(), ECDH::PRIVATE_KEY_LENGTH));
    EXPECT_EQ(keyBytesToString(copyOfValid.getPubKey(), ECDH::PUBLIC_KEY_LENGTH),
              keyBytesToString(original.getPubKey(), ECDH::PUBLIC_KEY_LENGTH));

    ECDH invalid(std::string("too short")); // initializationOK == false
    ASSERT_FALSE(invalid.initializationOK);
    ECDH copyOfInvalid(invalid);
    EXPECT_FALSE(copyOfInvalid.initializationOK);
}

// Positive control: two parties derive the same shared secret, proving the (privk, pubk)
// constructor yields a working key and the new guards don't break X25519 agreement.
TEST(Crypto, ECDH_SharedSecret_RoundTrip)
{
    ECDH alice;
    ECDH bob;
    ASSERT_TRUE(alice.initializationOK);
    ASSERT_TRUE(bob.initializationOK);

    const std::string alicePub = keyBytesToString(alice.getPubKey(), ECDH::PUBLIC_KEY_LENGTH);
    const std::string bobPub = keyBytesToString(bob.getPubKey(), ECDH::PUBLIC_KEY_LENGTH);

    ECDH aliceWithBobPub(alice.getPrivKey(), bobPub);
    ECDH bobWithAlicePub(bob.getPrivKey(), alicePub);
    ASSERT_TRUE(aliceWithBobPub.initializationOK);
    ASSERT_TRUE(bobWithAlicePub.initializationOK);

    std::string secretA;
    std::string secretB;
    ASSERT_EQ(aliceWithBobPub.computeSymmetricKey(secretA), 1);
    ASSERT_EQ(bobWithAlicePub.computeSymmetricKey(secretB), 1);

    EXPECT_EQ(secretA, secretB) << "ECDH parties must agree on the shared secret";
    EXPECT_EQ(secretA.size(), static_cast<size_t>(ECDH::DERIVED_KEY_LENGTH));
}

// A public key whose modulus is below MINKEYLENGTH must be rejected: it is smaller than the
// supported RSA key size and would underflow encrypt()'s padding length.
TEST(Crypto, AsymmCipher_isvalid_RejectsTinyModulus)
{
    AsymmCipher cipher;
    const std::string blob = mt::makePubKeyBlob(1); // 1-byte modulus
    ASSERT_NE(cipher.setkey(AsymmCipher::PUBKEY,
                            reinterpret_cast<const byte*>(blob.data()),
                            static_cast<int>(blob.size())),
              0);
    EXPECT_FALSE(cipher.isvalid(AsymmCipher::PUBKEY))
        << "A modulus below MINKEYLENGTH must be treated as invalid";
}

// A modulus that is non-trivial but still below MINKEYLENGTH (here 1024-bit) must be rejected too:
// only keys >= MINKEYLENGTH (2048-bit) are accepted.
TEST(Crypto, AsymmCipher_isvalid_RejectsUndersizedModulus)
{
    AsymmCipher cipher;
    const std::string blob =
        mt::makePubKeyBlob(128); // 1024-bit modulus, below the 256-byte minimum
    ASSERT_NE(cipher.setkey(AsymmCipher::PUBKEY,
                            reinterpret_cast<const byte*>(blob.data()),
                            static_cast<int>(blob.size())),
              0);
    EXPECT_FALSE(cipher.isvalid(AsymmCipher::PUBKEY))
        << "A modulus below MINKEYLENGTH (2048-bit) must be treated as invalid";
}

// A normally-sized public key must still validate.
TEST(Crypto, AsymmCipher_isvalid_AcceptsNormalModulus)
{
    AsymmCipher cipher;
    const std::string blob = mt::makePubKeyBlob(256); // 2048-bit modulus
    ASSERT_NE(cipher.setkey(AsymmCipher::PUBKEY,
                            reinterpret_cast<const byte*>(blob.data()),
                            static_cast<int>(blob.size())),
              0);
    EXPECT_TRUE(cipher.isvalid(AsymmCipher::PUBKEY));
}

// A plaintext longer than (modulus - 2) bytes must fail cleanly with 0 instead of
// underflowing (modBytes - plainlen - 2) and corrupting memory in genblock().
TEST(Crypto, AsymmCipher_encrypt_RejectsOversizePlaintext)
{
    PrnGen rng;
    AsymmCipher cipher;
    const std::string blob = mt::makePubKeyBlob(256); // modulus is 256 bytes -> max plaintext 254
    ASSERT_NE(cipher.setkey(AsymmCipher::PUBKEY,
                            reinterpret_cast<const byte*>(blob.data()),
                            static_cast<int>(blob.size())),
              0);

    std::vector<byte> plain(300, 0x5a); // 300 > 254
    std::vector<byte> out(AsymmCipher::MAXKEYLENGTH,
                          0); // ample output room, so length is the only issue
    EXPECT_EQ(cipher.encrypt(rng, plain.data(), plain.size(), out.data(), out.size()), 0)
        << "Plaintext larger than modulus - 2 must be rejected, not overflow the buffer";
}

// The existing output-buffer guard: a buffer too small for the worst-case ciphertext returns 0.
TEST(Crypto, AsymmCipher_encrypt_RejectsSmallOutputBuffer)
{
    PrnGen rng;
    AsymmCipher cipher;
    const std::string blob = mt::makePubKeyBlob(256); // needs at least 256 + 2 output bytes
    ASSERT_NE(cipher.setkey(AsymmCipher::PUBKEY,
                            reinterpret_cast<const byte*>(blob.data()),
                            static_cast<int>(blob.size())),
              0);

    const std::array<byte, 16> plain{};
    std::vector<byte> tooSmall(100, 0); // < 258
    EXPECT_EQ(cipher.encrypt(rng, plain.data(), plain.size(), tooSmall.data(), tooSmall.size()), 0);
}

TEST(Crypto, AsymmCipher_encrypt_RejectsUndersizedModulus)
{
    PrnGen rng;
    AsymmCipher cipher;
    const std::string blob = mt::makePubKeyBlob(1); // 1-byte modulus, set without validation
    ASSERT_NE(cipher.setkey(AsymmCipher::PUBKEY,
                            reinterpret_cast<const byte*>(blob.data()),
                            static_cast<int>(blob.size())),
              0);

    const std::array<byte, 16> plain{};
    std::vector<byte> out(AsymmCipher::MAXKEYLENGTH, 0); // ample room, so modulus size is the issue
    EXPECT_EQ(cipher.encrypt(rng, plain.data(), plain.size(), out.data(), out.size()), 0)
        << "A modulus below MINKEYLENGTH must be rejected by encrypt(), not overflow the buffer";
}

// Positive control: a real generated key pair round-trips through encrypt()/decrypt(), proving the
// new guards don't break the normal path.
TEST(Crypto, AsymmCipher_encrypt_decrypt_RoundTrip)
{
    PrnGen rng;

    AsymmCipher priv;
    CryptoPP::Integer pubk[2];
    priv.genkeypair(rng, pubk, 2048); // private key kept in 'priv', public key written to pubk[]

    std::string pubBlob;
    AsymmCipher::serializeintarray(pubk, AsymmCipher::PUBKEY, &pubBlob, true);

    AsymmCipher pub;
    ASSERT_NE(pub.setkey(AsymmCipher::PUBKEY,
                         reinterpret_cast<const byte*>(pubBlob.data()),
                         static_cast<int>(pubBlob.size())),
              0);
    ASSERT_TRUE(pub.isvalid(AsymmCipher::PUBKEY));

    std::array<byte, 32> secret{};
    std::iota(secret.begin(), secret.end(), static_cast<byte>(1));

    std::array<byte, AsymmCipher::MAXKEYLENGTH> cipherText{};
    const int cipherLen =
        pub.encrypt(rng, secret.data(), secret.size(), cipherText.data(), cipherText.size());
    ASSERT_GT(cipherLen, 0);

    std::array<byte, 32> recovered{};
    ASSERT_EQ(priv.decrypt(cipherText.data(),
                           static_cast<size_t>(cipherLen),
                           recovered.data(),
                           recovered.size()),
              1);
    EXPECT_EQ(secret, recovered);
}
