diff --git a/CMakeLists.txt b/CMakeLists.txt index 25e02df..3f093af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,12 +43,30 @@ target_include_directories(blowfish2 PUBLIC ${BF_INCLUDE_DIR}) # ===== Tests ===== include(CTest) -if(BUILD_TESTING) - add_executable(bf_test tests/Main.cpp) - target_link_libraries(bf_test PRIVATE blowfish) - add_test(NAME bf_test COMMAND bf_test) +if (BUILD_TESTING) - add_executable(bf2_test tests/Main2.cpp) + # ---- Blowfish Test ---- + add_executable(bf_test + tests/Main.cpp + tests/test_framework.h + tests/test_vectors.cpp + tests/test_roundtrip.cpp + tests/test_avalanche.cpp + tests/test_properties.cpp + ) + target_link_libraries(bf_test PRIVATE blowfish) + add_test(NAME BlowfishTests COMMAND bf_test) + + # ---- Blowfish2 Test ---- + add_executable(bf2_test + tests/Main2.cpp + tests/test_framework.h + tests/bf2_test_vectors.cpp + tests/bf2_test_roundtrip.cpp + tests/bf2_test_avalanche.cpp + tests/bf2_test_properties.cpp + ) target_link_libraries(bf2_test PRIVATE blowfish2) - add_test(NAME bf2_test COMMAND bf2_test) + add_test(NAME Blowfish2Tests COMMAND bf2_test) + endif() diff --git a/include/blowfish/blowfish.h b/include/blowfish/blowfish.h index 83c9aa5..6bdbfff 100644 --- a/include/blowfish/blowfish.h +++ b/include/blowfish/blowfish.h @@ -9,15 +9,15 @@ #include #include -#define MAXKEYBYTES 56 // 448 bits max -static constexpr uint32_t N = 16; +static constexpr uint32_t BF_NUM_ROUNDS = 16; +static constexpr uint32_t BF_MAX_KEYBYTES = 56; #if !defined(BLOWFISH_BLOWFISH_H_) #define BLOWFISH_BLOWFISH_H_ class Blowfish { private: - std::array PArray{}; + std::array PArray{}; std::array, 4> Sboxes{}; uint32_t F(uint32_t x) const noexcept; diff --git a/include/blowfish/blowfish2.h b/include/blowfish/blowfish2.h index f38a8df..6254daf 100644 --- a/include/blowfish/blowfish2.h +++ b/include/blowfish/blowfish2.h @@ -9,15 +9,15 @@ #include #include -#define MAXKEYBYTES 56 // 448 bits max +static constexpr uint64_t BF2_NUM_ROUNDS = 64; +static constexpr uint64_t BF2_MAX_KEYBYTES = 56; #if !defined(BLOWFISH_BLOWFISH2_H_) #define BLOWFISH_BLOWFISH2_H_ class Blowfish2 { private: - static constexpr uint64_t N = 64; - std::array PArray{}; + std::array PArray{}; std::array, 8> Sboxes{}; uint64_t F(uint64_t x) const noexcept; diff --git a/src/blowfish.cc b/src/blowfish.cc index 7ca093c..43d479c 100644 --- a/src/blowfish.cc +++ b/src/blowfish.cc @@ -6,13 +6,13 @@ #include -static const std::array P = { +static const std::array BF_PARRAY_INIT = { 0x243F6A88L, 0x85A308D3L, 0x13198A2EL, 0x03707344L, 0xA4093822L, 0x299F31D0L, 0x082EFA98L, 0xEC4E6C89L, 0x452821E6L, 0x38D01377L, 0xBE5466CFL, 0x34E90C6CL, 0xC0AC29B7L, 0xC97C50DDL, 0x3F84D5B5L, 0xB5470917L, 0x9216D5D9L, 0x8979FB1BL}; -static const std::array, 4> S = { +static const std::array, 4> BF_SBOX_INT = { {{0xD1310BA6L, 0x98DFB5ACL, 0x2FFD72DBL, 0xD01ADFB7L, 0xB8E1AFEDL, 0x6A267E96L, 0xBA7C9045L, 0xF12C7F99L, 0x24A19947L, 0xB3916CF7L, 0x0801F2E2L, 0x858EFC16L, 0x636920D8L, 0x71574E69L, 0xA458FEA3L, @@ -230,19 +230,19 @@ void Blowfish::initialize(const uint8_t *key, size_t keylen) { uint32_t datal = 0; uint32_t datar = 0; - Sboxes = S; + Sboxes = BF_SBOX_INT; size_t j = 0; - for (uint32_t i = 0; i < N + 2; ++i) { + for (uint32_t i = 0; i < BF_NUM_ROUNDS + 2; ++i) { data = 0; for (uint32_t k = 0; k < 4; ++k) { data = (data << 8) | key[j]; j = (j + 1) % keylen; } - PArray[i] = P[i] ^ data; + PArray[i] = BF_PARRAY_INIT[i] ^ data; } - for (uint32_t i = 0; i < N + 2; i += 2) { + for (uint32_t i = 0; i < BF_NUM_ROUNDS + 2; i += 2) { encrypt(datal, datar); PArray[i] = datal; PArray[i + 1] = datar; @@ -282,15 +282,15 @@ void Blowfish::encrypt(uint32_t &xl, uint32_t &xr) noexcept { uint32_t Xl = xl; uint32_t Xr = xr; - for (uint32_t i = 0; i < N; ++i) { + for (uint32_t i = 0; i < BF_NUM_ROUNDS; ++i) { Xl ^= PArray[i]; Xr = F(Xl) ^ Xr; std::swap(Xl, Xr); } std::swap(Xl, Xr); - Xr ^= PArray[N]; - Xl ^= PArray[N + 1]; + Xr ^= PArray[BF_NUM_ROUNDS]; + Xl ^= PArray[BF_NUM_ROUNDS + 1]; xl = Xl; xr = Xr; } @@ -299,7 +299,7 @@ void Blowfish::decrypt(uint32_t &xl, uint32_t &xr) noexcept { uint32_t Xl = xl; uint32_t Xr = xr; - for (int i = N + 1; i >= 2; --i) { + for (int i = BF_NUM_ROUNDS + 1; i >= 2; --i) { Xl ^= PArray[i]; Xr ^= F(Xl); std::swap(Xl, Xr); diff --git a/src/blowfish2.cc b/src/blowfish2.cc index 8b6ea33..ad26cc6 100644 --- a/src/blowfish2.cc +++ b/src/blowfish2.cc @@ -6,7 +6,7 @@ #include -static const std::array P = { +static const std::array BF2_PARRAY_INIT = { 0x243F6A8885A308D3, 0x13198A2E03707344, 0xA4093822299F31D0, 0x082EFA98EC4E6C89, 0x452821E638D01377, 0xBE5466CF34E90C6C, 0xC0AC29B7C97C50DD, 0x3F84D5B5B5470917, 0x9216D5D98979FB1B, @@ -30,7 +30,7 @@ static const std::array P = { 0x1BFEDF72429B023D, 0x37D0D724D00A1248, 0xDB0FEAD349F1C09B, 0x075372C980991B7B, 0x25D479D8F6E8DEF7, 0xE3FE501AB6794C3B}; -static const std::array, 8> S = { +static const std::array, 8> BF2_SBOX_INIT = { {{0x976CE0BD04C006BA, 0xC1A94FB6409F60C4, 0x5E5C9EC2196A2463, 0x68FB6FAF3E6C53B5, 0x1339B2EB3B52EC6F, 0x6DFC511F9B30952C, 0xCC814544AF5EBD09, 0xBEE3D004DE334AFD, 0x660F2807192E4BB3, @@ -736,20 +736,20 @@ void Blowfish2::initialize(const uint8_t *key, size_t keylen) { uint64_t datal = 0; uint64_t datar = 0; - Sboxes = S; // assumes static const S exists + Sboxes = BF2_SBOX_INIT; // assumes static const S exists size_t j = 0; - for (uint64_t i = 0; i < N + 2; ++i) { + for (uint64_t i = 0; i < BF2_NUM_ROUNDS + 2; ++i) { data = 0; for (uint64_t k = 0; k < 8; ++k) { data = (data << 8) | key[j]; j = (j + 1) % keylen; } - PArray[i] = P[i] ^ data; + PArray[i] = BF2_PARRAY_INIT[i] ^ data; } - for (uint64_t i = 0; i < N + 2; i += 2) { + for (uint64_t i = 0; i < BF2_NUM_ROUNDS + 2; i += 2) { encrypt(datal, datar); PArray[i] = datal; PArray[i + 1] = datar; @@ -798,15 +798,15 @@ void Blowfish2::encrypt(uint64_t &xl, uint64_t &xr) noexcept { uint64_t Xl = xl; uint64_t Xr = xr; - for (uint64_t i = 0; i < N; ++i) { + for (uint64_t i = 0; i < BF2_NUM_ROUNDS; ++i) { Xl ^= PArray[i]; Xr = F(Xl) ^ Xr; std::swap(Xl, Xr); } std::swap(Xl, Xr); - Xr ^= PArray[N]; - Xl ^= PArray[N + 1]; + Xr ^= PArray[BF2_NUM_ROUNDS]; + Xl ^= PArray[BF2_NUM_ROUNDS + 1]; xl = Xl; xr = Xr; } @@ -815,7 +815,7 @@ void Blowfish2::decrypt(uint64_t &xl, uint64_t &xr) noexcept { uint64_t Xl = xl; uint64_t Xr = xr; - for (uint64_t i = N + 1; i > 1; --i) { + for (uint64_t i = BF2_NUM_ROUNDS + 1; i > 1; --i) { Xl ^= PArray[i]; Xr = F(Xl) ^ Xr; std::swap(Xl, Xr); diff --git a/tests/Main.cpp b/tests/Main.cpp index 70dbf71..d9d498f 100644 --- a/tests/Main.cpp +++ b/tests/Main.cpp @@ -1,58 +1,6 @@ -#include +// SPDX-FileCopyrightText: 2025 Avinal Kumar avinal.xlvii@gmail.com +// SPDX-License-Identifier: MIT -#include +#include "test_framework.h" -std::string from_uint(uint32_t sh) { - std::string re(""); - for (int i = 0; i < 4; i++) { - re += (unsigned char)(sh >> i * 8); - } - return re; -} - -int main(int argc, char const *argv[]) { - std::string key("test@pass47"); - std::string message("My name is Avinal and I am cute"); - std::string cipher(""); - int len = message.length(); - int j = sizeof(uint32_t); - int rem = - ((len > j * 2) ? (((len / j * 2) + 1) * j * 2 - len) : (j * 2 - len)); - message.append(rem, '\0'); - len = message.length(); - Blowfish blowfish(key); - std::cout << "My message is: " << message << std::endl; - uint32_t lm, rm; - for (size_t i = 0; i < len; i += 8) { - lm = 0; - rm = 0; - lm = *reinterpret_cast( - const_cast(message.substr(i, j).c_str())); - rm = *reinterpret_cast( - const_cast(message.substr(i + 4, j).c_str())); - blowfish.encrypt(lm, rm); - cipher += from_uint(lm) + from_uint(rm); - } - std::cout << cipher << std::endl; - std::string decipher(""); - len = cipher.length(); - std::cout << "length: " << len << std::endl; - for (size_t i = 0; i < len; i += 8) { - lm = 0; - rm = 0; - lm = *reinterpret_cast( - const_cast(cipher.substr(i, 4).c_str())); - rm = *reinterpret_cast( - const_cast(cipher.substr(i + 4, 4).c_str())); - blowfish.decrypt(lm, rm); - decipher += from_uint(lm) + from_uint(rm); - } - - std::cout << decipher << std::endl; - if (message == decipher) { - std::cout << "Test successful!" << std::endl; - return 0; - } else { - return 1; - } -} +int main() { return tests_summary(); } diff --git a/tests/Main2.cpp b/tests/Main2.cpp index 1a20572..d9d498f 100644 --- a/tests/Main2.cpp +++ b/tests/Main2.cpp @@ -1,105 +1,6 @@ -#include -#include +// SPDX-FileCopyrightText: 2025 Avinal Kumar avinal.xlvii@gmail.com +// SPDX-License-Identifier: MIT -#include -#include +#include "test_framework.h" -std::string from_uint(uint64_t sh) { - std::string re(""); - for (int i = 0; i < 8; i++) { - re += (unsigned char)(sh >> i * 8); - } - return re; -} - -uint64_t to_uint(std::string &s, size_t index, size_t size) { - return *reinterpret_cast( - const_cast(s.substr(index, size).c_str())); -} - -int main(int argc, char const *argv[]) { - std::string key("test@pass47"); - std::string message("My name is Avinal and I am cute and smart"); - std::string cipher(""); - int len = message.length(); - int J = sizeof(uint64_t); - int rem = - ((len > J * 2) ? (((len / J * 2) + 1) * J * 2 - len) : (J * 2 - len)); - message.append(rem, '\0'); - len = message.length(); - - Blowfish2 blowfish(key); - - std::cout << "My message is: " << message << J << std::endl; - uint64_t L = 0, R = 0; - for (size_t i = 0; i < len; i += 16) { - L = to_uint(message, i, J); - R = to_uint(message, i + J, J); - blowfish.encrypt(L, R); - cipher += from_uint(L) + from_uint(R); - } - std::cout << "Cipher: " << cipher << std::endl; - - std::string decipher(""); - len = cipher.length(); - std::cout << "length: " << len << std::endl; - for (size_t i = 0; i < len; i += 16) { - L = to_uint(cipher, i, J); - R = to_uint(cipher, i + J, J); - blowfish.decrypt(L, R); - decipher += from_uint(L) + from_uint(R); - } - if (message == decipher) { - std::cout << "Test OK." << std::endl; - } else { - std::cout << "Test failed." << std::endl; - } - // C Blowfish 2 tests - - L = 0x0000000000000001, R = 0x0000000000000002; - - blowfish.initialize("TESTKEY"); - blowfish.encrypt(L, R); - if (L == 0x7B2B9DE71D1B1C62 && R == 0x91C230351177BEE8) - std::cout << "Test encryption OK." << std::endl; - else - std::cout << "Test encryption failed." << std::endl; - - blowfish.decrypt(L, R); - if (L == 1 && R == 2) - std::cout << "Test decryption OK." << std::endl; - else - std::cout << "Test decryption failed." << std::endl; - - L = 0x0102030405060708; - R = 0x0910111213141516; - - blowfish.initialize("A"); - blowfish.encrypt(L, R); - if (L == 0xCA38165603F9915C && R == 0x61F0776A0F55E807) - std::cout << "Test encryption OK." << std::endl; - else - std::cout << "Test encryption failed." << std::endl; - - blowfish.decrypt(L, R); - if (L == 0x0102030405060708 && R == 0x0910111213141516) - std::cout << "Test decryption OK." << std::endl; - else - std::cout << "Test decryption failed." << std::endl; - - L = 0x0102030405060708; - R = 0x0910111213141516; - - blowfish.initialize("B"); - blowfish.encrypt(L, R); - if (L == 0xD07690A78B109983 && R == 0x8DDF85826F2366C2) - std::cout << "Test encryption OK." << std::endl; - else - std::cout << "Test encryption failed." << std::endl; - - blowfish.decrypt(L, R); - if (L == 0x0102030405060708 && R == 0x0910111213141516) - std::cout << "Test decryption OK." << std::endl; - else - std::cout << "Test decryption failed." << std::endl; -} +int main() { return tests_summary(); } diff --git a/tests/bf2_test_avalanche.cpp b/tests/bf2_test_avalanche.cpp new file mode 100644 index 0000000..d197bba --- /dev/null +++ b/tests/bf2_test_avalanche.cpp @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2025 Avinal Kumar avinal.xlvii@gmail.com +// SPDX-License-Identifier: MIT + +#include "test_framework.h" +#include + +static int hamming128(uint64_t a1, uint64_t b1, uint64_t a2, uint64_t b2) { + return __builtin_popcountll(a1 ^ a2) + __builtin_popcountll(b1 ^ b2); +} + +// Check that flipping one bit in plaintext or key +// causes large, unpredictable changes in ciphertext. +TEST("Blowfish2 Avalanche Effect") { + Blowfish2 bf("key-for-avalanche"); + + uint64_t L = 0x1122334455667788ULL; + uint64_t R = 0x99AABBCCDDEEFF00ULL; + + uint64_t L0 = L, R0 = R; + bf.encrypt(L0, R0); + + for (int bit = 0; bit < 64; ++bit) { + uint64_t Lf = L ^ (1ULL << bit); + uint64_t Rf = R; + + uint64_t L1 = Lf, R1 = Rf; + bf.encrypt(L1, R1); + + int hd = hamming128(L0, R0, L1, R1); + + // Expect large hamming distance: ideally >40 for Blowfish2 + EXPECT_TRUE(hd > 40); + } +} diff --git a/tests/bf2_test_properties.cpp b/tests/bf2_test_properties.cpp new file mode 100644 index 0000000..cd619f7 --- /dev/null +++ b/tests/bf2_test_properties.cpp @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2025 Avinal Kumar avinal.xlvii@gmail.com +// SPDX-License-Identifier: MIT + +#include "test_framework.h" +#include + +TEST("Blowfish2 no fixed points") { + Blowfish2 bf("fixed-point-check"); + + for (uint64_t i = 1; i < 50; ++i) { + uint64_t L = i * 0x123456789ABCDEFULL; + uint64_t R = i * 0xFEDCBA987654321ULL; + + uint64_t L2 = L, R2 = R; + bf.encrypt(L2, R2); + + EXPECT_TRUE(!(L == L2 && R == R2)); + } +} + +TEST("Blowfish2 no short cycles") { + Blowfish2 bf("cycle-check"); + + for (uint64_t seed = 1; seed <= 5; ++seed) { + uint64_t L0 = seed; + uint64_t R0 = seed * 12345; + + uint64_t L = L0, R = R0; + + for (int iter = 0; iter < 12; ++iter) { + bf.encrypt(L, R); + EXPECT_TRUE(!(L == L0 && R == R0)); + } + } +} diff --git a/tests/bf2_test_roundtrip.cpp b/tests/bf2_test_roundtrip.cpp new file mode 100644 index 0000000..0dd209f --- /dev/null +++ b/tests/bf2_test_roundtrip.cpp @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2025 Avinal Kumar avinal.xlvii@gmail.com +// SPDX-License-Identifier: MIT + +#include "test_framework.h" +#include +#include + +// Ensure encrypt -> decrypt returns original plaintext +// for fixed and randomized test blocks. +TEST("Blowfish2 encrypt/decrypt roundtrip") { + Blowfish2 bf("test-key-2"); + + std::mt19937_64 rng(987654321ULL); + + for (int i = 0; i < 400; ++i) { + uint64_t L = rng(); + uint64_t R = rng(); + + uint64_t L2 = L, R2 = R; + bf.encrypt(L2, R2); + bf.decrypt(L2, R2); + + EXPECT_EQ(L, L2); + EXPECT_EQ(R, R2); + } +} diff --git a/tests/bf2_test_vectors.cpp b/tests/bf2_test_vectors.cpp new file mode 100644 index 0000000..37fdb09 --- /dev/null +++ b/tests/bf2_test_vectors.cpp @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2025 Avinal Kumar avinal.xlvii@gmail.com +// SPDX-License-Identifier: MIT + +#include "test_framework.h" +#include + +// Known Answer Tests (KAT): Validate Blowfish output against fixed reference +// vectors to ensure algorithmic correctness. +TEST("Blowfish2 Known Answer Vector") +{ + Blowfish2 bf("abcdefghijklmnopqrstuvwxyz"); + + uint64_t L = 0x424C4F5742463231ULL; // "BLOWBF21" (just a stable block) + uint64_t R = 0x4649534832463231ULL; // "FISH2F21" + + uint64_t Lc = L, Rc = R; + bf.encrypt(Lc, Rc); + + EXPECT_EQ(Lc, 0xF69E30BC3A4E8B0AULL); + EXPECT_EQ(Rc, 0x31E5F507F5412293ULL); + + bf.decrypt(Lc, Rc); + EXPECT_EQ(Lc, L); + EXPECT_EQ(Rc, R); +} diff --git a/tests/test_avalanche.cpp b/tests/test_avalanche.cpp new file mode 100644 index 0000000..ab3bf0c --- /dev/null +++ b/tests/test_avalanche.cpp @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2025 Avinal Kumar avinal.xlvii@gmail.com +// SPDX-License-Identifier: MIT + +#include "test_framework.h" +#include + +static int hamming(uint64_t a, uint64_t b) { + return __builtin_popcountll(a ^ b); +} + +// Check that flipping one bit in plaintext or key +// causes large, unpredictable changes in ciphertext. +TEST("Blowfish Avalanche Effect") { + Blowfish bf("key"); + + uint32_t L = 0x11223344, R = 0x55667788; + uint32_t Lc = L, Rc = R; + + bf.encrypt(Lc, Rc); + uint64_t C1 = (uint64_t(Lc) << 32) | Rc; + + for (int bit = 0; bit < 32; ++bit) { + uint32_t Lflip = L ^ (1u << bit); + uint32_t Rflip = R; + + uint32_t L2 = Lflip, R2 = Rflip; + bf.encrypt(L2, R2); + + uint64_t C2 = (uint64_t(L2) << 32) | R2; + + int hd = hamming(C1, C2); + EXPECT_TRUE(hd > 20); // Strong avalanche threshold + } +} diff --git a/tests/test_framework.h b/tests/test_framework.h new file mode 100644 index 0000000..143db66 --- /dev/null +++ b/tests/test_framework.h @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2025 Avinal Kumar avinal.xlvii@gmail.com +// SPDX-License-Identifier: MIT + +#pragma once +#include +#include +#include +#include + +inline int g_failures = 0; + +struct TestCase { + std::string name; + std::function fn; +}; + +inline std::vector &test_registry() { + static std::vector r; + return r; +} + +/* helper to build unique identifiers from __LINE__ */ +#define TEST_CONCAT_INNER(a, b) a##b +#define TEST_CONCAT(a, b) TEST_CONCAT_INNER(a, b) + +/* + Usage: + TEST("Some name with spaces") { ... } +*/ +#define TEST(name) \ + static void TEST_CONCAT(test_func_, __LINE__)(); \ + static bool TEST_CONCAT(test_reg_, __LINE__) = []() { \ + test_registry().push_back({name, TEST_CONCAT(test_func_, __LINE__)}); \ + return true; \ + }(); \ + static void TEST_CONCAT(test_func_, __LINE__)() + +#define EXPECT_EQ(a, b) \ + do { \ + if ((a) != (b)) { \ + std::cout << "[FAILED] " << __FILE__ << ":" << __LINE__ << ": expected " \ + << #a << " == " << #b << " but got " << (a) << "\n"; \ + ++g_failures; \ + } \ + } while (0) + +#define EXPECT_NE(a, b) \ + do { \ + if ((a) == (b)) { \ + std::cout << "[FAILED] " << __FILE__ << ":" << __LINE__ << ": expected " \ + << #a << " != " << #b << "\n"; \ + ++g_failures; \ + } \ + } while (0) + +#define EXPECT_TRUE(a) \ + do { \ + if (!(a)) { \ + std::cout << "[FAILED] " << __FILE__ << ":" << __LINE__ \ + << ": expected true: " << #a << "\n"; \ + ++g_failures; \ + } \ + } while (0) + +inline int tests_summary() { + std::cout << "Running " << test_registry().size() << " tests...\n"; + for (auto &t : test_registry()) { + std::cout << "[ RUN ] " << t.name << "\n"; + try { + t.fn(); + } catch (const std::exception &e) { + std::cout << "[ ERROR ] exception: " << e.what() << "\n"; + ++g_failures; + } catch (...) { + std::cout << "[ ERROR ] unknown exception\n"; + ++g_failures; + } + std::cout << "[ DONE ] " << t.name << "\n"; + } + + if (g_failures == 0) + std::cout << "\nAll tests passed \\^_^/\n"; + else + std::cout << "\n" << g_failures << " tests failed /^.^\\\n"; + + return g_failures; +} diff --git a/tests/test_properties.cpp b/tests/test_properties.cpp new file mode 100644 index 0000000..009fb97 --- /dev/null +++ b/tests/test_properties.cpp @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2025 Avinal Kumar avinal.xlvii@gmail.com +// SPDX-License-Identifier: MIT + +#include "test_framework.h" +#include + +// Test edge-case blocks, key lengths, symmetry, +// and consistency across instances. + +TEST("Blowfish no fixed points") { + Blowfish bf("key1"); + + for (uint64_t x = 1; x < 20; ++x) { + uint32_t L = x >> 32; + uint32_t R = x & 0xFFFFFFFFu; + + uint32_t L2 = L, R2 = R; + bf.encrypt(L2, R2); + + EXPECT_TRUE(!(L2 == L && R2 == R)); + } +} + +TEST("Blowfish no short encryption cycles") { + Blowfish bf("another-key"); + + for (uint64_t seed = 1; seed < 10; ++seed) { + uint32_t L = seed >> 32; + uint32_t R = seed & 0xFFFFFFFFu; + + uint32_t a = L, b = R; + + for (int i = 0; i < 10; ++i) { + bf.encrypt(a, b); + EXPECT_TRUE(!(a == L && b == R)); + } + } +} diff --git a/tests/test_roundtrip.cpp b/tests/test_roundtrip.cpp new file mode 100644 index 0000000..b8311be --- /dev/null +++ b/tests/test_roundtrip.cpp @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2025 Avinal Kumar avinal.xlvii@gmail.com +// SPDX-License-Identifier: MIT + +#include "test_framework.h" +#include +#include + +// Ensure encrypt -> decrypt returns original plaintext +// for fixed and randomized test blocks. +TEST("Blowfish encrypt/decrypt roundtrip") { + Blowfish bf("test-key"); + + std::mt19937_64 rng(12345); + + for (int i = 0; i < 500; ++i) { + uint64_t block = rng(); + uint32_t L = block >> 32; + uint32_t R = block & 0xFFFFFFFFu; + + uint32_t L2 = L, R2 = R; + bf.encrypt(L2, R2); + bf.decrypt(L2, R2); + + EXPECT_EQ(L, L2); + EXPECT_EQ(R, R2); + } +} diff --git a/tests/test_vectors.cpp b/tests/test_vectors.cpp new file mode 100644 index 0000000..66a11cb --- /dev/null +++ b/tests/test_vectors.cpp @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2025 Avinal Kumar avinal.xlvii@gmail.com +// SPDX-License-Identifier: MIT + +#include "test_framework.h" +#include + +// Known Answer Tests (KAT): Validate Blowfish output against fixed reference +// vectors to ensure algorithmic correctness. +TEST("Blowfish Known Test Vectors") { + Blowfish bf("abcdefghijklmnopqrstuvwxyz"); + + uint32_t L = 0x424C4F57; // "BLOW" + uint32_t R = 0x46495348; // "FISH" + + bf.encrypt(L, R); + EXPECT_EQ(L, 0x324ED0FE); + EXPECT_EQ(R, 0xF413A203); + + bf.decrypt(L, R); + EXPECT_EQ(L, 0x424C4F57); + EXPECT_EQ(R, 0x46495348); +}