Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .github/workflows/CI-Linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,22 @@ jobs:
strategy:
matrix:
std: [11, 17]
mlock: [ON, OFF]
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: sudo apt-get update && sudo apt-get install -y cmake
- name: Configure
run: cmake -S . -B build -DHMACCPP_BUILD_TESTS=ON -DCMAKE_CXX_STANDARD=${{ matrix.std }}
run: cmake -S . -B build -DHMACCPP_BUILD_TESTS=ON -DHMACCPP_ENABLE_MLOCK=${{ matrix.mlock }} -DCMAKE_CXX_STANDARD=${{ matrix.std }}
- name: Build
run: cmake --build build
- name: Run tests
run: ctest --test-dir build --output-on-failure -R "test_all|test_totp"
run: ctest --test-dir build --output-on-failure
vcpkg:
runs-on: ubuntu-latest
strategy:
matrix:
mlock: [ON, OFF]
steps:
- uses: actions/checkout@v3
- name: Install vcpkg
Expand All @@ -38,8 +42,8 @@ jobs:
./vcpkg/bootstrap-vcpkg.sh
./vcpkg/vcpkg install gtest
- name: Configure
run: cmake -S . -B build -DHMACCPP_BUILD_TESTS=ON -DCMAKE_TOOLCHAIN_FILE=./vcpkg/scripts/buildsystems/vcpkg.cmake
run: cmake -S . -B build -DHMACCPP_BUILD_TESTS=ON -DHMACCPP_ENABLE_MLOCK=${{ matrix.mlock }} -DCMAKE_TOOLCHAIN_FILE=./vcpkg/scripts/buildsystems/vcpkg.cmake
- name: Build
run: cmake --build build
- name: Run tests
run: ctest --test-dir build --output-on-failure -R "test_all|test_totp"
run: ctest --test-dir build --output-on-failure
5 changes: 3 additions & 2 deletions .github/workflows/CI-Win.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
strategy:
matrix:
std: [11, 17]
mlock: [ON, OFF]
steps:
- uses: actions/checkout@v3
- uses: msys2/setup-msys2@v2
Expand All @@ -30,10 +31,10 @@ jobs:
mingw-w64-x86_64-make
- name: Configure
shell: msys2 {0}
run: cmake -S . -B build -DHMACCPP_BUILD_TESTS=ON -DCMAKE_CXX_STANDARD=${{ matrix.std }}
run: cmake -S . -B build -DHMACCPP_BUILD_TESTS=ON -DHMACCPP_ENABLE_MLOCK=${{ matrix.mlock }} -DCMAKE_CXX_STANDARD=${{ matrix.std }}
- name: Build
shell: msys2 {0}
run: cmake --build build
- name: Run tests
shell: msys2 {0}
run: ctest --test-dir build --output-on-failure -R "test_all|test_totp"
run: ctest --test-dir build --output-on-failure
12 changes: 8 additions & 4 deletions .github/workflows/CI-macOS.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
strategy:
matrix:
std: [11, 17]
mlock: [ON, OFF]
steps:
- uses: actions/checkout@v3
- name: Install dependencies
Expand All @@ -26,14 +27,17 @@ jobs:
brew uninstall --ignore-dependencies cmake || true
brew install cmake
- name: Configure
run: cmake -S . -B build -DHMACCPP_BUILD_TESTS=ON -DCMAKE_CXX_STANDARD=${{ matrix.std }}
run: cmake -S . -B build -DHMACCPP_BUILD_TESTS=ON -DHMACCPP_ENABLE_MLOCK=${{ matrix.mlock }} -DCMAKE_CXX_STANDARD=${{ matrix.std }}
- name: Build
run: cmake --build build
- name: Run tests
run: ctest --test-dir build --output-on-failure -R "test_all|test_totp"
run: ctest --test-dir build --output-on-failure

vcpkg:
runs-on: macos-latest
strategy:
matrix:
mlock: [ON, OFF]
steps:
- uses: actions/checkout@v3
- name: Install vcpkg
Expand All @@ -42,8 +46,8 @@ jobs:
./vcpkg/bootstrap-vcpkg.sh
./vcpkg/vcpkg install gtest
- name: Configure
run: cmake -S . -B build -DHMACCPP_BUILD_TESTS=ON -DCMAKE_TOOLCHAIN_FILE=./vcpkg/scripts/buildsystems/vcpkg.cmake
run: cmake -S . -B build -DHMACCPP_BUILD_TESTS=ON -DHMACCPP_ENABLE_MLOCK=${{ matrix.mlock }} -DCMAKE_TOOLCHAIN_FILE=./vcpkg/scripts/buildsystems/vcpkg.cmake
- name: Build
run: cmake --build build
- name: Run tests
run: ctest --test-dir build --output-on-failure -R "test_all|test_totp"
run: ctest --test-dir build --output-on-failure
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@

## [Unreleased]

## [0.6.0] - Unreleased
### Security
- `secure_buffer` now coordinates page locks process-wide at page granularity,
preventing one buffer from unlocking a page still used by another buffer.
- Replacement, copy, assignment, and string-consumption paths wipe old storage
before release and lock replacement storage before copying secrets.

### Changed
- **Breaking ABI change:** `secure_buffer` layout and move-operation exception
specification changed. Shared-library consumers must be fully rebuilt; do not
use a 0.6.0 DLL/SO as a drop-in replacement for a 0.5.x build.
- CI now runs the secure-buffer regression target with page locking both enabled
and disabled.

### Upgrade
- Rebuild every consumer against the 0.6.0 headers and library together.
- The bundled vcpkg port remains at the latest tagged release until `v0.6.0`
is published.

## [0.5.0] - 2025-09-07
### Added
- Lifecycle management APIs and hardened zeroing for `secure_buffer`.
Expand Down
23 changes: 17 additions & 6 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.5)
project(hmac_cpp VERSION 0.5.0 LANGUAGES CXX)
project(hmac_cpp VERSION 0.6.0 LANGUAGES CXX)

option(HMACCPP_BUILD_EXAMPLES "Build the example program" OFF)
option(HMACCPP_BUILD_TESTS "Build the test suite" OFF)
Expand Down Expand Up @@ -33,6 +33,7 @@ set(HMAC_HEADERS
include/hmac_cpp/sha512.hpp
include/hmac_cpp/secure_buffer.hpp
include/hmac_cpp/memlock.hpp
include/hmac_cpp/page_lock_registry.hpp
include/hmac_cpp/secret_string.hpp
include/hmac_cpp/encoding.hpp
include/hmac_cpp/version.hpp
Expand Down Expand Up @@ -129,16 +130,26 @@ if(HMACCPP_BUILD_TESTS)
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
FetchContent_MakeAvailable(googletest)
find_package(OpenSSL REQUIRED)
add_executable(test_all test_all.cpp)
target_link_libraries(test_all PRIVATE hmac_cpp gtest_main OpenSSL::Crypto)
target_include_directories(test_all PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
add_test(NAME test_all COMMAND test_all)

add_executable(test_secure_buffer test_secure_buffer.cpp)
target_link_libraries(test_secure_buffer PRIVATE hmac_cpp gtest_main)
target_include_directories(test_secure_buffer PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
add_test(NAME test_secure_buffer COMMAND test_secure_buffer)

add_executable(test_totp test_totp.cpp)
target_link_libraries(test_totp PRIVATE hmac_cpp gtest_main)
target_include_directories(test_totp PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
add_test(NAME test_totp COMMAND test_totp)

find_package(OpenSSL QUIET)
if(OpenSSL_FOUND)
add_executable(test_all test_all.cpp)
target_link_libraries(test_all PRIVATE hmac_cpp gtest_main OpenSSL::Crypto)
target_include_directories(test_all PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
add_test(NAME test_all COMMAND test_all)
else()
message(STATUS "OpenSSL not found; skipping test_all")
endif()
endif()

export(EXPORT hmac_cppTargets
Expand Down
205 changes: 205 additions & 0 deletions include/hmac_cpp/page_lock_registry.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
#ifndef HMAC_CPP_PAGE_LOCK_REGISTRY_HPP
#define HMAC_CPP_PAGE_LOCK_REGISTRY_HPP

#include <cstddef>
#include <cstdint>
#include <limits>
#include <map>
#include <mutex>

namespace hmac_cpp {
namespace detail {

/// \brief Process-wide ownership registry for page-granular memory locks.
/// \tparam PageLocker Type exposing lock_page() and unlock_page().
template<class PageLocker>
class page_lock_registry {
public:
explicit page_lock_registry(std::size_t page_size, PageLocker locker = PageLocker())
: page_size_(page_size), locker_(locker) {}

bool lock(void* ptr, std::size_t len) noexcept {
std::uintptr_t first = 0;
std::uintptr_t last = 0;
if (!normalize(ptr, len, first, last)) {
return false;
}

std::lock_guard<std::mutex> guard(mutex_);
return lock_range(first, last);
}

bool unlock(void* ptr, std::size_t len) noexcept {
std::uintptr_t first = 0;
std::uintptr_t last = 0;
if (!normalize(ptr, len, first, last)) {
return false;
}

std::lock_guard<std::mutex> guard(mutex_);
if (!all_pages_registered(first, last)) {
return false;
}

bool success = true;
for_each_page(first, last, [this, &success](std::uintptr_t page) {
typename refcounts_type::iterator entry = refcounts_.find(page);
if (entry->second > 1) {
--entry->second;
return true;
}
if (!locker_.unlock_page(reinterpret_cast<void*>(page), page_size_)) {
success = false;
return true;
}
refcounts_.erase(entry);
return true;
});
return success;
}

private:
using refcounts_type = std::map<std::uintptr_t, std::size_t>;

static std::size_t pending_count() noexcept {
return std::numeric_limits<std::size_t>::max();
}

bool normalize(void* ptr, std::size_t len, std::uintptr_t& first,
std::uintptr_t& last) const noexcept {
if (ptr == nullptr || len == 0 || page_size_ == 0) {
return false;
}

const std::uintptr_t address = reinterpret_cast<std::uintptr_t>(ptr);
if (len - 1 > std::numeric_limits<std::uintptr_t>::max() - address) {
return false;
}
const std::uintptr_t end = address + len - 1;
first = address - address % page_size_;
last = end - end % page_size_;
return true;
}

template<class Visitor>
void for_each_page(std::uintptr_t first, std::uintptr_t last, Visitor visitor) noexcept {
for (std::uintptr_t page = first;; page += page_size_) {
if (!visitor(page) || page == last) {
return;
}
}
}

bool all_pages_registered(std::uintptr_t first, std::uintptr_t last) const noexcept {
for (std::uintptr_t page = first;; page += page_size_) {
const typename refcounts_type::const_iterator entry = refcounts_.find(page);
if (entry == refcounts_.end() || entry->second == 0 || entry->second == pending_count()) {
return false;
}
if (page == last) {
return true;
}
}
}

bool lock_range(std::uintptr_t first, std::uintptr_t last) noexcept {
try {
for_each_page(first, last, [this](std::uintptr_t page) {
refcounts_.emplace(page, 0);
return true;
});
} catch (...) {
erase_unlocked_pages(first, last);
return false;
}

bool overflow = false;
for_each_page(first, last, [this, &overflow](std::uintptr_t page) {
const typename refcounts_type::const_iterator entry = refcounts_.find(page);
if (entry->second >= pending_count() - 1) {
overflow = true;
return false;
}
return true;
});
if (overflow) {
erase_unlocked_pages(first, last);
return false;
}

for_each_page(first, last, [this](std::uintptr_t page) {
typename refcounts_type::iterator entry = refcounts_.find(page);
if (entry->second != 0) {
return true;
}
if (!locker_.lock_page(reinterpret_cast<void*>(page), page_size_)) {
return false;
}
entry->second = pending_count();
return true;
});

if (has_unlocked_page(first, last)) {
rollback_pending_locks(first, last);
return false;
}

for_each_page(first, last, [this](std::uintptr_t page) {
typename refcounts_type::iterator entry = refcounts_.find(page);
entry->second = entry->second == pending_count() ? 1 : entry->second + 1;
return true;
});
return true;
}

bool has_unlocked_page(std::uintptr_t first, std::uintptr_t last) const noexcept {
for (std::uintptr_t page = first;; page += page_size_) {
const typename refcounts_type::const_iterator entry = refcounts_.find(page);
if (entry == refcounts_.end() || entry->second == 0) {
return true;
}
if (page == last) {
return false;
}
}
}

void erase_unlocked_pages(std::uintptr_t first, std::uintptr_t last) noexcept {
for_each_page(first, last, [this](std::uintptr_t page) {
typename refcounts_type::iterator entry = refcounts_.find(page);
if (entry != refcounts_.end() && entry->second == 0) {
refcounts_.erase(entry);
}
return true;
});
}

void rollback_pending_locks(std::uintptr_t first, std::uintptr_t last) noexcept {
for_each_page(first, last, [this](std::uintptr_t page) {
typename refcounts_type::iterator entry = refcounts_.find(page);
if (entry == refcounts_.end()) {
return true;
}
if (entry->second == pending_count()) {
if (locker_.unlock_page(reinterpret_cast<void*>(page), page_size_)) {
refcounts_.erase(entry);
} else {
entry->second = 1;
}
} else if (entry->second == 0) {
refcounts_.erase(entry);
}
return true;
});
}

std::size_t page_size_{};
PageLocker locker_;
refcounts_type refcounts_;
std::mutex mutex_;
};

} // namespace detail
} // namespace hmac_cpp

#endif // HMAC_CPP_PAGE_LOCK_REGISTRY_HPP
Loading
Loading