Skip to content

Conversation

@JoshuaSBrown
Copy link
Collaborator

@JoshuaSBrown JoshuaSBrown commented Oct 7, 2025

Ticket

Description

How Has This Been Tested?

Artifacts (if appropriate):

Tasks

  • - A description of the PR has been provided, and a diagram included if it is a new feature.
  • - Formatter has been run
  • - CHANGELOG comment has been added
  • - Labels have been assigned to the pr
  • - A reviwer has been added
  • - A user has been assigned to work on the pr
  • - If new feature a unit test has been added

Summary by Sourcery

Introduce a symmetric encryption feature via CipherEngine in the common module, including file-based key loading, OpenSSL integration, and comprehensive unit tests.

New Features:

  • Add CipherEngine class for AES-256-CBC encryption and decryption with Base64 encoding support
  • Add readFile utility function for loading binary key files

Enhancements:

  • Integrate OpenSSL headers and link SSL/CRYPTO libraries in common and core/server modules

Build:

  • Update CMakeLists across common, tests, and core/server to link OpenSSL libraries

Tests:

  • Add unit tests for key generation, encryption/decryption flows, IV generation, and JSON integration

JoshuaSBrown and others added 25 commits September 25, 2025 08:20
* Update devel from staging
* fix(install_foxx): remove ssl_args argument it is not a valid flag.
* [DAPS-1651] fixed bug in compose env generator (#1656), compose_generate_env.sh script was not reading existing values correctly.
* feature: Implemented logging functions
* refactor: remove all scripts related to dependencies from DataFed repo
* feature: update DataFedDependencies submodule and move ground truth of requirements.txt to DataFedDependencies.
@JoshuaSBrown JoshuaSBrown self-assigned this Oct 7, 2025
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Oct 7, 2025

Reviewer's Guide

This PR introduces a new symmetric cipher engine in the common library, leveraging OpenSSL for AES-256-CBC encryption/decryption and Base64 encoding/decoding, integrates the necessary OpenSSL dependencies into the build system, extends utility functions to support reading binary keys, and adds comprehensive unit tests.

Class diagram for the new CipherEngine class

classDiagram
class SDMS::CipherEngine {
  +static int BASE64_ENCODED_BLOCK_SIZE
  +static int BASE64_INPUT_BLOCK_SIZE
  +static int NULL_TERMINATOR_SIZE
  +static int IV_LENGTH
  +static int KEY_LENGTH
  +static int MAX_MSG_LENGTH
  +static int ENCODED_IV_LENGTH
  +static int ENCODED_MSG_LENGTH
  +CipherEngine(const unsigned char* inputKey)
  +CipherEngine()
  +static void generateEncryptionKey(unsigned char token_key[KEY_LENGTH])
  +static void generateIV(unsigned char iv[IV_LENGTH])
  +CipherBytes encryptAlgorithm(unsigned char* iv, const std::string& msg, LogContext log_context)
  +CipherString encodeBytes(CipherBytes unencoded_bytes, LogContext log_context)
  +CipherString encrypt(unsigned char *iv, const std::string& msg, LogContext log_context)
  +CipherString encrypt(const std::string& msg, LogContext log_context)
  +std::string decrypt(const CipherString& encrypted_string, LogContext log_context)
  +static bool tokenNeedsUpdate(const libjson::Value::Object &obj)
  -std::unique_ptr<char[]> encode64(const unsigned char* input, const int length, LogContext log_context) const
  -std::unique_ptr<unsigned char[]> decode64(const char* input, const int length, LogContext log_context) const
  -unsigned char key[KEY_LENGTH]
  -static void handleErrors(void)
}
class SDMS::CipherEngine::CipherBytes {
  +unsigned char encrypted_msg[ENCODED_MSG_LENGTH]
  +unsigned char iv[IV_LENGTH]
  +int encrypted_msg_len
}
class SDMS::CipherEngine::CipherString {
  +std::unique_ptr<char[]> encrypted_msg
  +std::unique_ptr<char[]> iv
  +int encrypted_msg_len
}
SDMS::CipherEngine "1" *-- "1" SDMS::CipherEngine::CipherBytes
SDMS::CipherEngine "1" *-- "1" SDMS::CipherEngine::CipherString
Loading

Class diagram for the new readFile utility function

classDiagram
class Util {
  +void readFile(const std::string &fileName, const int arraySize, unsigned char* array)
}
Loading

File-Level Changes

Change Details Files
Add readFile utility for binary key loading
  • Implemented readFile in Util.cpp with ifstream and memcpy
  • Declared readFile in Util.hpp and removed commented legacy code
  • Included and in Util.cpp
common/source/Util.cpp
common/include/common/Util.hpp
Integrate OpenSSL dependencies into CMake builds
  • Linked OPENSSL_SSL_LIBRARY and OPENSSL_CRYPTO_LIBRARY in common and server targets
  • Updated common/tests/unit CMakeLists to link OpenSSL for unit tests
  • Cleaned up file glob formatting in common/CMakeLists
common/CMakeLists.txt
common/tests/unit/CMakeLists.txt
core/server/CMakeLists.txt
Add CipherEngine class with full OpenSSL-based implementation
  • Defined CipherEngine API in CipherEngine.hpp with key/IV generation and encrypt/decrypt methods
  • Implemented AES-256-CBC encryption, Base64 encode/decode, and error handling in CipherEngine.cpp
  • Included libjson integration for tokenNeedsUpdate logic
common/include/common/CipherEngine.hpp
common/source/CipherEngine.cpp
Add unit tests covering key generation and encryption/decryption
  • Created test_CipherEngine.cpp with cases for key gen, encrypt/decrypt with and without provided IV
  • Added JSON-based decryption test using libjson Value object
  • Updated unit CMakeLists to include the new test and link OpenSSL
common/tests/unit/test_CipherEngine.cpp
common/tests/unit/CMakeLists.txt

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes and found some issues that need to be addressed.

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `common/source/Util.cpp:55-58` </location>
<code_context>
   return result;
 }

+void readFile(const std::string &fileName,const int arraySize, unsigned char* array)
+{
+    //Converting Key for encryption funct
+    unsigned char keyChar[arraySize] = {};
+    //Grabbing key
+    std::ifstream keyFile(fileName, std::ios::binary);
+    if(!keyFile.is_open())
+    {
+        throw TraceException(__FILE__, __LINE__, 0, std::string("Cannot find token key file: ") + fileName); 
+        return;
+    }
+
+    keyFile.read(reinterpret_cast<char*>(keyChar),arraySize);
+
+    std::copy(keyChar, keyChar + arraySize, array);
+}
 size_t curlResponseWriteCB(char *ptr, size_t size, size_t nmemb,
</code_context>

<issue_to_address>
**suggestion:** Consider handling partial reads in readFile for robustness.

Since keyFile.read may read fewer than arraySize bytes if the file is too short, consider checking keyFile.gcount() and handling incomplete reads appropriately, such as by throwing an exception or zeroing the unused buffer portion.

```suggestion
    keyFile.read(reinterpret_cast<char*>(keyChar), arraySize);
    std::streamsize bytesRead = keyFile.gcount();

    if (bytesRead < arraySize) {
        // Zero the unused portion of the buffer
        std::memset(keyChar + bytesRead, 0, arraySize - bytesRead);
        throw TraceException(__FILE__, __LINE__, 0,
            std::string("Key file '") + fileName + "' is too short: expected " +
            std::to_string(arraySize) + " bytes, got " + std::to_string(bytesRead));
        // Optionally, you could return or handle differently, but throwing is robust
    }

    std::copy(keyChar, keyChar + arraySize, array);
}
```
</issue_to_address>

### Comment 2
<location> `common/source/CipherEngine.cpp:161-163` </location>
<code_context>
+         * the encrypted output.
+         * EVP_EncryptUpdate can be called multiple times if necessary
+         */
+        if(1 != EVP_EncryptUpdate(ctx, bytes_result.encrypted_msg, &len, msg_unsigned.data(), msg_unsigned.size()))
+            handleErrors();
+        bytes_result.encrypted_msg_len = len;
+
+        /*
</code_context>

<issue_to_address>
**issue (bug_risk):** Potential buffer overflow risk in encrypted_msg assignment.

Check that ENCODED_MSG_LENGTH accounts for possible padding added during encryption, or switch to dynamic allocation to prevent overflow.
</issue_to_address>

### Comment 3
<location> `common/source/CipherEngine.cpp:233` </location>
<code_context>
+        handleErrors();
+    }
+    int plaintext_len = 0;
+    unsigned char plaintext[encoded_encrypted_string.encrypted_msg_len + EVP_MAX_BLOCK_LENGTH] = {};
+    const int ciphertext_len = encoded_encrypted_string.encrypted_msg_len;
+    int len = 0;
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Consider using dynamic allocation for plaintext buffer in decrypt.

Stack allocation may cause issues with large encrypted_msg_len values. Prefer std::vector or std::unique_ptr for safer, flexible buffer management.

Suggested implementation:

```cpp
    int plaintext_len = 0;
    std::vector<unsigned char> plaintext(encoded_encrypted_string.encrypted_msg_len + EVP_MAX_BLOCK_LENGTH, 0);
    const int ciphertext_len = encoded_encrypted_string.encrypted_msg_len;
    int len = 0;

```

```cpp
    if(1 != EVP_DecryptUpdate(ctx, plaintext.data(), &len, ciphertext.get(), ciphertext_len))
    {
        handleErrors();
    }
    plaintext_len = len;

```
</issue_to_address>

### Comment 4
<location> `common/source/CipherEngine.cpp:214` </location>
<code_context>
+    EVP_CIPHER_CTX *ctx = nullptr;
+
+    //converts the cipherstring back to a unsigned char
+    std::unique_ptr<unsigned char[]> ciphertext = decode64(encoded_encrypted_string.encrypted_msg.get(), static_cast<int>(strlen(encoded_encrypted_string.encrypted_msg.get())), log_context);
+    std::unique_ptr<unsigned char[]> iv = decode64(encoded_encrypted_string.iv.get(), static_cast<int>(strlen(encoded_encrypted_string.iv.get())),log_context);
+    /* Create and initialise the context */
</code_context>

<issue_to_address>
**issue (bug_risk):** Using strlen on encrypted_msg may be unsafe for binary data.

If encrypted_msg has null bytes, strlen will give an incorrect length. Use the actual length of the encoded string instead of relying on null-termination.
</issue_to_address>

### Comment 5
<location> `common/tests/unit/test_CipherEngine.cpp:23-32` </location>
<code_context>
+BOOST_AUTO_TEST_CASE(testing_KeyGeneration)
</code_context>

<issue_to_address>
**suggestion (testing):** Missing negative and error condition tests for key generation.

Add tests that simulate key generation failure, such as mocking RAND_bytes to return 0, to verify error handling in CipherEngine::generateEncryptionKey.

Suggested implementation:

```cpp
BOOST_AUTO_TEST_CASE(testing_KeyGeneration)
{
    unsigned char token_key[SDMS::CipherEngine::KEY_LENGTH];
    unsigned char keyArray[SDMS::CipherEngine::KEY_LENGTH];
    unsigned char finalArray[SDMS::CipherEngine::KEY_LENGTH];
    CipherEngine::generateEncryptionKey(token_key);
    std::string fname = "datafed-token-key.txt";

    std::ofstream outf(fname, std::ios::binary);
    outf.write(reinterpret_cast<const char*>(token_key), SDMS::CipherEngine::KEY_LENGTH);
    outf.close();
}

// Negative test: Simulate RAND_bytes failure
BOOST_AUTO_TEST_CASE(testing_KeyGenerationFailure)
{
    unsigned char token_key[SDMS::CipherEngine::KEY_LENGTH];

    // Save original RAND_bytes
    int (*orig_rand_bytes)(unsigned char *, int) = RAND_bytes;

    // Mock RAND_bytes to always fail
    RAND_bytes = [](unsigned char *buf, int num) -> int {
        (void)buf; (void)num;
        return 0;
    };

    // Expect generateEncryptionKey to throw or handle error
    BOOST_CHECK_THROW(CipherEngine::generateEncryptionKey(token_key), std::runtime_error);

    // Restore original RAND_bytes
    RAND_bytes = orig_rand_bytes;
}

```

- If `CipherEngine::generateEncryptionKey` does not throw a `std::runtime_error` on failure, adjust the `BOOST_CHECK_THROW` to match the actual error handling (e.g., check return value or error code).
- If you use a mocking framework (e.g., GMock), use its facilities to mock `RAND_bytes` instead of direct assignment.
- Ensure that the test suite is linked against OpenSSL and that `RAND_bytes` is accessible for mocking.
</issue_to_address>

### Comment 6
<location> `common/tests/unit/test_CipherEngine.cpp:67-76` </location>
<code_context>
+BOOST_AUTO_TEST_CASE(test_EncryptionDecryption)
</code_context>

<issue_to_address>
**suggestion (testing):** No tests for invalid key or corrupted ciphertext/IV.

Add tests for decryption with invalid keys, corrupted ciphertext, and IV to verify proper error handling.
</issue_to_address>

### Comment 7
<location> `common/tests/unit/test_CipherEngine.cpp:92-89` </location>
<code_context>
+BOOST_AUTO_TEST_CASE(test_EncryptionDecryption_KeyGen)
</code_context>

<issue_to_address>
**suggestion (testing):** Test does not cover empty or very large message input.

Please add tests for empty strings and messages at or above MAX_MSG_LENGTH to ensure proper handling and expected exceptions.

Suggested implementation:

```cpp
BOOST_AUTO_TEST_CASE(test_EncryptionDecryption_KeyGen)
{

    LogContext log_context;
    unsigned char key[SDMS::CipherEngine::KEY_LENGTH];
    CipherEngine::generateEncryptionKey(key);

    //Instance only used for encrypting
    CipherEngine encryptCipher(key);
    //Sets struct CipherString: which contains cipherText, cipherIV, cipherPaddedLen
    //Mimicing a globus token

    // Test 1: Empty string
    {
        std::string empty_msg = "";
        CipherString encoded_encrypted_packet = encryptCipher.encrypt(empty_msg, log_context);
        CipherEngine decryptCipher(key);
        std::string decrypted_msg = decryptCipher.decrypt(encoded_encrypted_packet, log_context);
        BOOST_CHECK(decrypted_msg == empty_msg);
    }

    // Test 2: Message at MAX_MSG_LENGTH
    {
        std::string max_msg(SDMS::CipherEngine::MAX_MSG_LENGTH, 'A');
        CipherString encoded_encrypted_packet = encryptCipher.encrypt(max_msg, log_context);
        CipherEngine decryptCipher(key);
        std::string decrypted_msg = decryptCipher.decrypt(encoded_encrypted_packet, log_context);
        BOOST_CHECK(decrypted_msg == max_msg);
    }

    // Test 3: Message above MAX_MSG_LENGTH
    {
        std::string over_max_msg(SDMS::CipherEngine::MAX_MSG_LENGTH + 1, 'B');
        bool exception_thrown = false;
        try {
            CipherString encoded_encrypted_packet = encryptCipher.encrypt(over_max_msg, log_context);
        } catch (const std::exception& e) {
            exception_thrown = true;
        }
        BOOST_CHECK(exception_thrown);
    }

```

- Ensure that `SDMS::CipherEngine::MAX_MSG_LENGTH` is defined and accessible in this test file.
- If the encrypt function does not throw an exception for oversized input, you may need to update the CipherEngine implementation to enforce this limit and throw an exception.
- If a specific exception type is used, replace `std::exception` with the correct type in the catch block.
</issue_to_address>

### Comment 8
<location> `common/tests/unit/test_CipherEngine.cpp:117-89` </location>
<code_context>
+BOOST_AUTO_TEST_CASE(test_EncryptionDecryption_IVGen)
</code_context>

<issue_to_address>
**suggestion (testing):** No test for invalid IV length or content.

Add a test with an invalid IV length or content to verify that the functions correctly handle errors and raise exceptions.

Suggested implementation:

```cpp
BOOST_AUTO_TEST_CASE(test_EncryptionDecryption_IVGen)
{
    LogContext log_context;
    unsigned char key[SDMS::CipherEngine::KEY_LENGTH];
    readFile("datafed-token-key.txt", SDMS::CipherEngine::KEY_LENGTH, key);

    //Instance only used for encrypting
    CipherEngine encryptCipher(key);

    //Sets struct CipherString: which contains cipherText, cipherIV, cipherPaddedLen
    //Mimicing a globus token
}

// Test for invalid IV length or content
BOOST_AUTO_TEST_CASE(test_EncryptionDecryption_InvalidIV)
{
    LogContext log_context;
    unsigned char key[SDMS::CipherEngine::KEY_LENGTH];
    readFile("datafed-token-key.txt", SDMS::CipherEngine::KEY_LENGTH, key);

    CipherEngine decryptCipher(key);

    // Prepare a valid encrypted packet (simulate or use a dummy)
    std::string valid_encrypted_packet = "dummy_encrypted_data";
    // Prepare an invalid IV (wrong length)
    std::string invalid_iv(SDMS::CipherEngine::IV_LENGTH - 2, 'A'); // IV too short

    // Simulate a packet with invalid IV (how your decrypt function expects input)
    // If decrypt expects a struct or specific format, adjust accordingly
    try {
        // This assumes decryptCipher.decrypt takes the IV as part of the packet or as a parameter
        // You may need to adjust this call to match your actual API
        std::string result = decryptCipher.decrypt(valid_encrypted_packet, log_context, invalid_iv);
        BOOST_FAIL("Expected exception for invalid IV length, but none was thrown.");
    } catch (const std::exception& e) {
        BOOST_CHECK_MESSAGE(true, "Exception thrown as expected for invalid IV length.");
    } catch (...) {
        BOOST_FAIL("Unexpected exception type thrown for invalid IV length.");
    }
}

```

- If your `decrypt` function does not take the IV as a separate parameter, you will need to adjust the test to construct the encrypted packet with the invalid IV in the correct format.
- If your code uses a custom exception type for invalid IV, you may want to catch that specific exception instead of `std::exception`.
- Ensure that the dummy encrypted packet and invalid IV are constructed in a way that matches your actual decryption API.
</issue_to_address>

### Comment 9
<location> `common/tests/unit/test_CipherEngine.cpp:147-89` </location>
<code_context>
+BOOST_AUTO_TEST_CASE(test_EncryptionDecryptionJSONValue)
</code_context>

<issue_to_address>
**suggestion (testing):** No test for missing or malformed JSON fields.

Please add tests for cases where required JSON fields are missing or malformed to verify error handling.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +55 to +58
keyFile.read(reinterpret_cast<char*>(keyChar),arraySize);

std::copy(keyChar, keyChar + arraySize, array);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider handling partial reads in readFile for robustness.

Since keyFile.read may read fewer than arraySize bytes if the file is too short, consider checking keyFile.gcount() and handling incomplete reads appropriately, such as by throwing an exception or zeroing the unused buffer portion.

Suggested change
keyFile.read(reinterpret_cast<char*>(keyChar),arraySize);
std::copy(keyChar, keyChar + arraySize, array);
}
keyFile.read(reinterpret_cast<char*>(keyChar), arraySize);
std::streamsize bytesRead = keyFile.gcount();
if (bytesRead < arraySize) {
// Zero the unused portion of the buffer
std::memset(keyChar + bytesRead, 0, arraySize - bytesRead);
throw TraceException(__FILE__, __LINE__, 0,
std::string("Key file '") + fileName + "' is too short: expected " +
std::to_string(arraySize) + " bytes, got " + std::to_string(bytesRead));
// Optionally, you could return or handle differently, but throwing is robust
}
std::copy(keyChar, keyChar + arraySize, array);
}

@JoshuaSBrown JoshuaSBrown changed the title 1575 daps feature add cipher engine [DAPS-1575] - feature (core) add cipher engine Oct 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants