feat: security fixes and improved test coverages

Security Fixes

  1. Blowfish2 destructor added (blowfish2.h, blowfish2.cc) — zeros PArray and Sboxes on destruction
  2. Secure memory zeroing (blowfish.cc, blowfish2.cc) — both destructors now use volatile pointer writes to prevent compiler elision
  3. Input validation (blowfish.cc, blowfish2.cc) — initialize() now throws std::invalid_argument for null key, empty key, or key > 56 bytes
  4. Copy assignment deleted (blowfish.h) — prevents accidental key material copies
  5. Constants moved inside include guards (blowfish.h, blowfish2.h)

  Code Quality Fixes

  6. Typo fixed — BF_SBOX_INT → BF_SBOX_INIT in blowfish.cc
  7. CMake standard fixed — blowfish2 target now requires cxx_std_17 instead of cxx_std_14

  Test Fixes & Additions

  8. Fixed "no fixed points" bug (test_properties.cpp) — L is no longer always 0
  9. Eric Young KAT vectors (test_vectors.cpp) — 5 official Blowfish test vectors added
  10. Key length tests — min (1 byte), max (56 bytes), and differing lengths
  11. Invalid key rejection tests — empty, over-length, and null keys
  12. Edge-case blocks — all-zero, all-ones, L==R
  13. Key avalanche tests — flipping each key bit produces large ciphertext changes
  14. Cross-instance consistency — same key → same output across instances
  15. Re-initialization tests — different key after re-init produces different output

Assisted-by: Claude Code

Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
This commit is contained in:
2026-04-15 18:21:01 +05:30
parent 2b4c5a2a6f
commit 1933265148
10 changed files with 437 additions and 22 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ target_include_directories(blowfish PUBLIC ${BF_INCLUDE_DIR})
# ===== blowfish2 library =====
add_library(blowfish2)
target_compile_features(blowfish2 PUBLIC cxx_std_14)
target_compile_features(blowfish2 PUBLIC cxx_std_17)
target_sources(blowfish2
PRIVATE
+5 -3
View File
@@ -5,6 +5,10 @@
// SPDX-FileCopyrightText: 1997 Paul Kocher
#pragma once
#if !defined(BLOWFISH_BLOWFISH_H_)
#define BLOWFISH_BLOWFISH_H_
#include <array>
#include <cstdint>
#include <string>
@@ -12,9 +16,6 @@
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<uint32_t, BF_NUM_ROUNDS + 2> PArray{};
@@ -25,6 +26,7 @@ public:
Blowfish() = default;
explicit Blowfish(std::string const &key);
Blowfish(Blowfish const &) = delete;
Blowfish &operator=(const Blowfish &) = delete;
void initialize(const uint8_t *key, size_t keylen);
void initialize(const std::string &key);
+5 -3
View File
@@ -5,6 +5,10 @@
// SPDX-FileCopyrightText: 2005 Alexander Pukall
#pragma once
#if !defined(BLOWFISH_BLOWFISH2_H_)
#define BLOWFISH_BLOWFISH2_H_
#include <array>
#include <cstdint>
#include <string>
@@ -12,9 +16,6 @@
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:
std::array<uint64_t, BF2_NUM_ROUNDS + 2> PArray{};
@@ -33,6 +34,7 @@ public:
void encrypt(uint64_t &xl, uint64_t &xr) noexcept;
void decrypt(uint64_t &xl, uint64_t &xr) noexcept;
~Blowfish2();
};
#endif // BLOWFISH_BLOWFISH2_H_
+13 -4
View File
@@ -5,6 +5,7 @@
// SPDX-FileCopyrightText: 1997 Paul Kocher
#include <blowfish/blowfish.h>
#include <stdexcept>
static const std::array<uint32_t, 16 + 2> BF_PARRAY_INIT = {
0x243F6A88L, 0x85A308D3L, 0x13198A2EL, 0x03707344L, 0xA4093822L,
@@ -12,7 +13,7 @@ static const std::array<uint32_t, 16 + 2> BF_PARRAY_INIT = {
0xBE5466CFL, 0x34E90C6CL, 0xC0AC29B7L, 0xC97C50DDL, 0x3F84D5B5L,
0xB5470917L, 0x9216D5D9L, 0x8979FB1BL};
static const std::array<std::array<uint32_t, 256>, 4> BF_SBOX_INT = {
static const std::array<std::array<uint32_t, 256>, 4> BF_SBOX_INIT = {
{{0xD1310BA6L, 0x98DFB5ACL, 0x2FFD72DBL, 0xD01ADFB7L, 0xB8E1AFEDL,
0x6A267E96L, 0xBA7C9045L, 0xF12C7F99L, 0x24A19947L, 0xB3916CF7L,
0x0801F2E2L, 0x858EFC16L, 0x636920D8L, 0x71574E69L, 0xA458FEA3L,
@@ -226,11 +227,15 @@ static const std::array<std::array<uint32_t, 256>, 4> BF_SBOX_INT = {
0x3AC372E6L}}};
void Blowfish::initialize(const uint8_t *key, size_t keylen) {
if (key == nullptr || keylen == 0 || keylen > BF_MAX_KEYBYTES)
throw std::invalid_argument(
"Blowfish key must be non-null and between 1 and 56 bytes");
uint32_t data = 0;
uint32_t datal = 0;
uint32_t datar = 0;
Sboxes = BF_SBOX_INT;
Sboxes = BF_SBOX_INIT;
size_t j = 0;
for (uint32_t i = 0; i < BF_NUM_ROUNDS + 2; ++i) {
@@ -312,8 +317,12 @@ void Blowfish::decrypt(uint32_t &xl, uint32_t &xr) noexcept {
xr = Xr;
}
Blowfish::~Blowfish() {
std::fill(PArray.begin(), PArray.end(), 0);
volatile uint32_t *p = PArray.data();
for (size_t i = 0; i < PArray.size(); ++i)
p[i] = 0;
for (auto &row : Sboxes) {
std::fill(row.begin(), row.end(), 0);
volatile uint32_t *s = row.data();
for (size_t i = 0; i < row.size(); ++i)
s[i] = 0;
}
}
+17 -1
View File
@@ -5,6 +5,7 @@
// SPDX-FileCopyrightText: 2005 Alexander Pukall
#include <blowfish/blowfish2.h>
#include <stdexcept>
static const std::array<uint64_t, 64 + 2> BF2_PARRAY_INIT = {
0x243F6A8885A308D3, 0x13198A2E03707344, 0xA4093822299F31D0,
@@ -732,11 +733,15 @@ void Blowfish2::initialize(const std::string &key) {
}
void Blowfish2::initialize(const uint8_t *key, size_t keylen) {
if (key == nullptr || keylen == 0 || keylen > BF2_MAX_KEYBYTES)
throw std::invalid_argument(
"Blowfish2 key must be non-null and between 1 and 56 bytes");
uint64_t data = 0;
uint64_t datal = 0;
uint64_t datar = 0;
Sboxes = BF2_SBOX_INIT; // assumes static const S exists
Sboxes = BF2_SBOX_INIT;
size_t j = 0;
@@ -827,3 +832,14 @@ void Blowfish2::decrypt(uint64_t &xl, uint64_t &xr) noexcept {
xl = Xl;
xr = Xr;
}
Blowfish2::~Blowfish2() {
volatile uint64_t *p = PArray.data();
for (size_t i = 0; i < PArray.size(); ++i)
p[i] = 0;
for (auto &row : Sboxes) {
volatile uint64_t *s = row.data();
for (size_t i = 0; i < row.size(); ++i)
s[i] = 0;
}
}
+32 -2
View File
@@ -8,9 +8,9 @@ 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
// Check that flipping one bit in plaintext
// causes large, unpredictable changes in ciphertext.
TEST("Blowfish2 Avalanche Effect") {
TEST("Blowfish2 Plaintext Avalanche Effect") {
Blowfish2 bf("key-for-avalanche");
uint64_t L = 0x1122334455667788ULL;
@@ -32,3 +32,33 @@ TEST("Blowfish2 Avalanche Effect") {
EXPECT_TRUE(hd > 40);
}
}
// Check that flipping one bit in the key
// causes large, unpredictable changes in ciphertext.
TEST("Blowfish2 Key Avalanche Effect") {
uint8_t basekey[8] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF};
uint64_t L = 0x1122334455667788ULL;
uint64_t R = 0x99AABBCCDDEEFF00ULL;
Blowfish2 bf_base;
bf_base.initialize(basekey, 8);
uint64_t L0 = L, R0 = R;
bf_base.encrypt(L0, R0);
for (int byte = 0; byte < 8; ++byte) {
for (int bit = 0; bit < 8; ++bit) {
uint8_t flipped[8];
std::copy(basekey, basekey + 8, flipped);
flipped[byte] ^= (1u << bit);
Blowfish2 bf_flip;
bf_flip.initialize(flipped, 8);
uint64_t L1 = L, R1 = R;
bf_flip.encrypt(L1, R1);
int hd = hamming128(L0, R0, L1, R1);
EXPECT_TRUE(hd > 40);
}
}
}
+137
View File
@@ -3,6 +3,143 @@
#include "test_framework.h"
#include <blowfish/blowfish2.h>
#include <stdexcept>
TEST("Blowfish2 varying key lengths") {
uint64_t L = 0xDEADBEEFCAFEBABEULL, R = 0x0123456789ABCDEFULL;
// 1-byte key (minimum)
{
Blowfish2 bf;
bf.initialize(reinterpret_cast<const uint8_t *>("A"), 1);
uint64_t l = L, r = R;
bf.encrypt(l, r);
EXPECT_TRUE(l != L || r != R);
bf.decrypt(l, r);
EXPECT_EQ(l, L);
EXPECT_EQ(r, R);
}
// 56-byte key (maximum)
{
const uint8_t maxkey[56] = {
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14,
0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E,
0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32,
0x33, 0x34, 0x35, 0x36, 0x37, 0x38};
Blowfish2 bf;
bf.initialize(maxkey, 56);
uint64_t l = L, r = R;
bf.encrypt(l, r);
EXPECT_TRUE(l != L || r != R);
bf.decrypt(l, r);
EXPECT_EQ(l, L);
EXPECT_EQ(r, R);
}
// Different key lengths produce different ciphertext
{
Blowfish2 bf4, bf8;
bf4.initialize(reinterpret_cast<const uint8_t *>("ABCD"), 4);
bf8.initialize(reinterpret_cast<const uint8_t *>("ABCDEFGH"), 8);
uint64_t l4 = L, r4 = R, l8 = L, r8 = R;
bf4.encrypt(l4, r4);
bf8.encrypt(l8, r8);
EXPECT_TRUE(l4 != l8 || r4 != r8);
}
}
TEST("Blowfish2 rejects invalid keys") {
bool caught = false;
// Empty key
try {
Blowfish2 bf("");
(void)bf;
} catch (const std::invalid_argument &) {
caught = true;
}
EXPECT_TRUE(caught);
// Over-length key (57 bytes)
caught = false;
try {
Blowfish2 bf;
uint8_t bigkey[57] = {};
bf.initialize(bigkey, 57);
} catch (const std::invalid_argument &) {
caught = true;
}
EXPECT_TRUE(caught);
// Null pointer
caught = false;
try {
Blowfish2 bf;
bf.initialize(nullptr, 8);
} catch (const std::invalid_argument &) {
caught = true;
}
EXPECT_TRUE(caught);
}
TEST("Blowfish2 edge-case blocks") {
Blowfish2 bf("edge-test-2");
// All-zero block
{
uint64_t L = 0, R = 0;
uint64_t L2 = L, R2 = R;
bf.encrypt(L2, R2);
EXPECT_TRUE(L2 != 0 || R2 != 0);
bf.decrypt(L2, R2);
EXPECT_EQ(L2, L);
EXPECT_EQ(R2, R);
}
// All-ones block
{
uint64_t L = 0xFFFFFFFFFFFFFFFFULL, R = 0xFFFFFFFFFFFFFFFFULL;
uint64_t L2 = L, R2 = R;
bf.encrypt(L2, R2);
EXPECT_TRUE(L2 != L || R2 != R);
bf.decrypt(L2, R2);
EXPECT_EQ(L2, L);
EXPECT_EQ(R2, R);
}
}
TEST("Blowfish2 cross-instance consistency") {
Blowfish2 bf1("same-key-2");
Blowfish2 bf2("same-key-2");
uint64_t L = 0xAAAAAAAAAAAAAAAAULL, R = 0x5555555555555555ULL;
uint64_t L1 = L, R1 = R, L2 = L, R2 = R;
bf1.encrypt(L1, R1);
bf2.encrypt(L2, R2);
EXPECT_EQ(L1, L2);
EXPECT_EQ(R1, R2);
}
TEST("Blowfish2 re-initialization") {
Blowfish2 bf("key-one");
uint64_t L = 0x1111111111111111ULL, R = 0x2222222222222222ULL;
uint64_t L1 = L, R1 = R;
bf.encrypt(L1, R1);
bf.initialize("key-two");
uint64_t L2 = L, R2 = R;
bf.encrypt(L2, R2);
EXPECT_TRUE(L1 != L2 || R1 != R2);
bf.decrypt(L2, R2);
EXPECT_EQ(L2, L);
EXPECT_EQ(R2, R);
}
TEST("Blowfish2 no fixed points") {
Blowfish2 bf("fixed-point-check");
+33 -2
View File
@@ -8,9 +8,9 @@ static int hamming(uint64_t a, uint64_t b) {
return __builtin_popcountll(a ^ b);
}
// Check that flipping one bit in plaintext or key
// Check that flipping one bit in plaintext
// causes large, unpredictable changes in ciphertext.
TEST("Blowfish Avalanche Effect") {
TEST("Blowfish Plaintext Avalanche Effect") {
Blowfish bf("key");
uint32_t L = 0x11223344, R = 0x55667788;
@@ -32,3 +32,34 @@ TEST("Blowfish Avalanche Effect") {
EXPECT_TRUE(hd > 20); // Strong avalanche threshold
}
}
// Check that flipping one bit in the key
// causes large, unpredictable changes in ciphertext.
TEST("Blowfish Key Avalanche Effect") {
uint8_t basekey[8] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF};
uint32_t L = 0x11223344, R = 0x55667788;
Blowfish bf_base;
bf_base.initialize(basekey, 8);
uint32_t Lc = L, Rc = R;
bf_base.encrypt(Lc, Rc);
uint64_t C1 = (uint64_t(Lc) << 32) | Rc;
for (int byte = 0; byte < 8; ++byte) {
for (int bit = 0; bit < 8; ++bit) {
uint8_t flipped[8];
std::copy(basekey, basekey + 8, flipped);
flipped[byte] ^= (1u << bit);
Blowfish bf_flip;
bf_flip.initialize(flipped, 8);
uint32_t L2 = L, R2 = R;
bf_flip.encrypt(L2, R2);
uint64_t C2 = (uint64_t(L2) << 32) | R2;
int hd = hamming(C1, C2);
EXPECT_TRUE(hd > 20);
}
}
}
+155 -6
View File
@@ -3,16 +3,165 @@
#include "test_framework.h"
#include <blowfish/blowfish.h>
#include <stdexcept>
// Test edge-case blocks, key lengths, symmetry,
// and consistency across instances.
TEST("Blowfish varying key lengths") {
uint32_t L = 0xDEADBEEF, R = 0xCAFEBABE;
// 1-byte key (minimum)
{
Blowfish bf;
bf.initialize(reinterpret_cast<const uint8_t *>("A"), 1);
uint32_t l = L, r = R;
bf.encrypt(l, r);
EXPECT_TRUE(l != L || r != R);
bf.decrypt(l, r);
EXPECT_EQ(l, L);
EXPECT_EQ(r, R);
}
// 56-byte key (maximum)
{
const uint8_t maxkey[56] = {
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14,
0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E,
0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32,
0x33, 0x34, 0x35, 0x36, 0x37, 0x38};
Blowfish bf;
bf.initialize(maxkey, 56);
uint32_t l = L, r = R;
bf.encrypt(l, r);
EXPECT_TRUE(l != L || r != R);
bf.decrypt(l, r);
EXPECT_EQ(l, L);
EXPECT_EQ(r, R);
}
// Different key lengths produce different ciphertext
{
Blowfish bf4, bf8;
bf4.initialize(reinterpret_cast<const uint8_t *>("ABCD"), 4);
bf8.initialize(reinterpret_cast<const uint8_t *>("ABCDEFGH"), 8);
uint32_t l4 = L, r4 = R, l8 = L, r8 = R;
bf4.encrypt(l4, r4);
bf8.encrypt(l8, r8);
EXPECT_TRUE(l4 != l8 || r4 != r8);
}
}
TEST("Blowfish rejects invalid keys") {
bool caught = false;
// Empty key
try {
Blowfish bf("");
(void)bf;
} catch (const std::invalid_argument &) {
caught = true;
}
EXPECT_TRUE(caught);
// Over-length key (57 bytes)
caught = false;
try {
Blowfish bf;
uint8_t bigkey[57] = {};
bf.initialize(bigkey, 57);
} catch (const std::invalid_argument &) {
caught = true;
}
EXPECT_TRUE(caught);
// Null pointer
caught = false;
try {
Blowfish bf;
bf.initialize(nullptr, 8);
} catch (const std::invalid_argument &) {
caught = true;
}
EXPECT_TRUE(caught);
}
TEST("Blowfish edge-case blocks") {
Blowfish bf("edge-test");
// All-zero block
{
uint32_t L = 0, R = 0;
uint32_t L2 = L, R2 = R;
bf.encrypt(L2, R2);
EXPECT_TRUE(L2 != 0 || R2 != 0);
bf.decrypt(L2, R2);
EXPECT_EQ(L2, L);
EXPECT_EQ(R2, R);
}
// All-ones block
{
uint32_t L = 0xFFFFFFFF, R = 0xFFFFFFFF;
uint32_t L2 = L, R2 = R;
bf.encrypt(L2, R2);
EXPECT_TRUE(L2 != L || R2 != R);
bf.decrypt(L2, R2);
EXPECT_EQ(L2, L);
EXPECT_EQ(R2, R);
}
// L == R
{
uint32_t L = 0x12345678, R = 0x12345678;
uint32_t L2 = L, R2 = R;
bf.encrypt(L2, R2);
bf.decrypt(L2, R2);
EXPECT_EQ(L2, L);
EXPECT_EQ(R2, R);
}
}
TEST("Blowfish cross-instance consistency") {
Blowfish bf1("same-key");
Blowfish bf2("same-key");
uint32_t L = 0xAAAAAAAA, R = 0x55555555;
uint32_t L1 = L, R1 = R, L2 = L, R2 = R;
bf1.encrypt(L1, R1);
bf2.encrypt(L2, R2);
EXPECT_EQ(L1, L2);
EXPECT_EQ(R1, R2);
}
TEST("Blowfish re-initialization") {
Blowfish bf("key-one");
uint32_t L = 0x11111111, R = 0x22222222;
uint32_t L1 = L, R1 = R;
bf.encrypt(L1, R1);
bf.initialize("key-two");
uint32_t L2 = L, R2 = R;
bf.encrypt(L2, R2);
// Different keys must produce different ciphertext
EXPECT_TRUE(L1 != L2 || R1 != R2);
// Roundtrip still works after re-init
bf.decrypt(L2, R2);
EXPECT_EQ(L2, L);
EXPECT_EQ(R2, R);
}
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;
for (uint32_t i = 1; i < 20; ++i) {
uint32_t L = i * 0x12345678u;
uint32_t R = i * 0x9ABCDEF0u;
uint32_t L2 = L, R2 = R;
bf.encrypt(L2, R2);
@@ -24,9 +173,9 @@ TEST("Blowfish no fixed points") {
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;
for (uint32_t seed = 1; seed < 10; ++seed) {
uint32_t L = seed * 0x11111111u;
uint32_t R = seed * 0xAAAAAAAAu;
uint32_t a = L, b = R;
+39
View File
@@ -20,3 +20,42 @@ TEST("Blowfish Known Test Vectors") {
EXPECT_EQ(L, 0x424C4F57);
EXPECT_EQ(R, 0x46495348);
}
// Eric Young test vectors — official Blowfish KAT from Schneier's reference.
// Each entry: { key (hex bytes), plaintext_L, plaintext_R, cipher_L, cipher_R }
TEST("Blowfish Eric Young Test Vectors") {
struct TestVector {
const uint8_t key[8];
size_t keylen;
uint32_t plain_l, plain_r;
uint32_t cipher_l, cipher_r;
};
// Subset of the published Eric Young / SSLeay test vectors
static const TestVector vectors[] = {
{{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
8, 0x00000000, 0x00000000, 0x4EF99745, 0x6198DD78},
{{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF},
8, 0xFFFFFFFF, 0xFFFFFFFF, 0x51866FD5, 0xB85ECB8A},
{{0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
8, 0x10000000, 0x00000001, 0x7D856F9A, 0x613063F2},
{{0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF},
8, 0x11111111, 0x11111111, 0x61F9C380, 0x2281B096},
{{0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10},
8, 0x01234567, 0x89ABCDEF, 0x0ACEAB0F, 0xC6A0A28D},
};
for (const auto &v : vectors) {
Blowfish bf;
bf.initialize(v.key, v.keylen);
uint32_t L = v.plain_l, R = v.plain_r;
bf.encrypt(L, R);
EXPECT_EQ(L, v.cipher_l);
EXPECT_EQ(R, v.cipher_r);
bf.decrypt(L, R);
EXPECT_EQ(L, v.plain_l);
EXPECT_EQ(R, v.plain_r);
}
}