diff --git a/CMakeLists.txt b/CMakeLists.txt index cc11d35..274faa1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,7 @@ option(HMACCPP_BUILD_EXAMPLES "Build the example program" OFF) option(HMACCPP_BUILD_TESTS "Build the test suite" OFF) option(HMACCPP_BUILD_BENCH "Build benchmarks" OFF) option(HMACCPP_BUILD_SHARED "Build hmac_cpp as a shared library" OFF) +option(HMACCPP_ENABLE_MLOCK "Pin secret buffers in RAM using mlock/VirtualLock" ON) if(HMACCPP_BUILD_SHARED) set(BUILD_SHARED_LIBS ON) @@ -17,6 +18,7 @@ set(HMAC_SOURCES src/hmac.cpp src/hmac_utils.cpp src/encoding.cpp + src/memlock.cpp ) set(HMAC_HEADERS @@ -27,6 +29,8 @@ set(HMAC_HEADERS include/hmac_cpp/sha256.hpp include/hmac_cpp/sha512.hpp include/hmac_cpp/secure_buffer.hpp + include/hmac_cpp/memlock.hpp + include/hmac_cpp/secret.hpp include/hmac_cpp/encoding.hpp include/hmac_cpp/version.hpp ) @@ -44,6 +48,10 @@ else() target_compile_definitions(hmac_cpp PUBLIC HMAC_CPP_STATIC) endif() +if(HMACCPP_ENABLE_MLOCK) + target_compile_definitions(hmac_cpp PUBLIC HMAC_CPP_ENABLE_MLOCK) +endif() + if(MSVC) target_compile_options(hmac_cpp PRIVATE /wd4251) else() @@ -70,6 +78,10 @@ endif() add_executable(example_encoding example_encoding.cpp) target_link_libraries(example_encoding PRIVATE hmac_cpp) target_include_directories(example_encoding PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) + + add_executable(example_secret example_secret.cpp) + target_link_libraries(example_secret PRIVATE hmac_cpp) + target_include_directories(example_secret PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) endif() include(CMakePackageConfigHelpers) diff --git a/README-RU.md b/README-RU.md index 7dabd0a..b7c48be 100644 --- a/README-RU.md +++ b/README-RU.md @@ -52,11 +52,15 @@ CI охватывает Linux/Windows/macOS. Тестировалась с GCC, * `HMACCPP_BUILD_EXAMPLES` * `HMACCPP_BUILD_TESTS` * `HMACCPP_BUILD_BENCH` +* `HMACCPP_ENABLE_MLOCK` Библиотека по умолчанию собирается **статически**. Чтобы получить динамическую, используйте `-DHMACCPP_BUILD_SHARED=ON`. Макрос `HMAC_CPP_API` пуст для статической сборки и управляет экспортом/импортом символов в динамической. +`HMACCPP_ENABLE_MLOCK` включает попытку пиновать секретные буферы в RAM с помощью +`mlock`/`VirtualLock`. Отключите, если платформа не позволяет. + ### Сборка ```bash @@ -137,6 +141,21 @@ secure_buffer key(std::move(secret_string)); // обнуляет перемещ auto mac = hmac::get_hmac(key, payload, hmac::TypeHash::SHA256); ``` +Для дополнительной защиты в памяти можно использовать `secret_string`, который +обфусцирует данные и по возможности закрепляет их в RAM: + +```cpp +#include + +hmac_cpp::secret_string token("super-secret-token"); + +token.with_plaintext([](const uint8_t* p, size_t n){ + // p действует только внутри коллбэка +}); + +token.clear(); +``` + ### HMAC (сырой буфер) ```cpp diff --git a/README.md b/README.md index 79e8bd2..8bc24c1 100644 --- a/README.md +++ b/README.md @@ -53,11 +53,15 @@ Examples, tests, and benchmarks are OFF by default. Enable via: * `HMACCPP_BUILD_EXAMPLES` * `HMACCPP_BUILD_TESTS` * `HMACCPP_BUILD_BENCH` +* `HMACCPP_ENABLE_MLOCK` The library builds **static** by default. Use `-DHMACCPP_BUILD_SHARED=ON` to produce a shared library. The `HMAC_CPP_API` macro is empty for static builds and controls symbol export/import for shared builds. +`HMACCPP_ENABLE_MLOCK` toggles best-effort page locking via `mlock`/`VirtualLock`. +Disable it if the platform lacks the necessary privileges. + ### Build ```bash @@ -282,7 +286,24 @@ hmac_cpp::base36_decode(b36, raw); Returned strings and buffers are not zeroized; if you store secrets, prefer `secure_buffer` and wipe explicitly. Zeroization is a best‑effort and may be -removed by optimizations or the C++ runtime allocator. +removed by optimizations or the C++ runtime allocator. For higher resistance to +memory scans, use `secret_string` which obfuscates data in memory and optionally +pins buffers in RAM. + +```cpp +#include + +hmac_cpp::secret_string token("super-secret-token"); + +token.with_plaintext([](const uint8_t* p, size_t n){ + // p is only valid within this callback +}); + +// If needed (creates a copy): +std::string plain = token.reveal_copy(); + +token.clear(); +``` --- diff --git a/example_secret.cpp b/example_secret.cpp new file mode 100644 index 0000000..ee45623 --- /dev/null +++ b/example_secret.cpp @@ -0,0 +1,12 @@ +#include +#include + +int main() { + hmac_cpp::secret_string token("super-secret-token"); + + token.with_plaintext([](const uint8_t* p, size_t n){ + std::cout.write(reinterpret_cast(p), n); + }); + std::cout << '\n'; + return 0; +} diff --git a/include/hmac_cpp/hmac_utils.hpp b/include/hmac_cpp/hmac_utils.hpp index fc71c59..213befa 100644 --- a/include/hmac_cpp/hmac_utils.hpp +++ b/include/hmac_cpp/hmac_utils.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #ifndef HMAC_CPP_MAX_PBKDF2_ITERATIONS #define HMAC_CPP_MAX_PBKDF2_ITERATIONS 1000000u @@ -35,6 +36,11 @@ namespace hmac_cpp { HMAC_CPP_API bool constant_time_equal(const uint8_t* a, size_t a_len, const uint8_t* b, size_t b_len); + /// \brief Generate \p n cryptographically random bytes. + /// \param n Number of bytes to generate. + /// \return Vector filled with random bytes. + HMAC_CPP_API std::vector random_bytes(size_t n); + /// \brief Compare vectors in constant time. /// \param a First vector. /// \param b Second vector. diff --git a/include/hmac_cpp/memlock.hpp b/include/hmac_cpp/memlock.hpp new file mode 100644 index 0000000..1c62793 --- /dev/null +++ b/include/hmac_cpp/memlock.hpp @@ -0,0 +1,29 @@ +#ifndef HMAC_CPP_MEMLOCK_HPP_INCLUDED +#define HMAC_CPP_MEMLOCK_HPP_INCLUDED + +#include +#include "hmac_cpp/api.hpp" + +namespace hmac_cpp { + +// Pin memory pages in RAM. Returns true on success (best-effort). +HMAC_CPP_API bool lock_pages(void* ptr, size_t len) noexcept; + +// Unlock previously pinned pages. Returns true on success. +HMAC_CPP_API bool unlock_pages(void* ptr, size_t len) noexcept; + +// RAII-guard for temporary buffers. +struct PageLockGuard { + void* p; + size_t n; + bool locked; + PageLockGuard(void* ptr, size_t len) noexcept + : p(ptr), n(len), locked(lock_pages(ptr, len)) {} + ~PageLockGuard() { if (locked) unlock_pages(p, n); } + PageLockGuard(const PageLockGuard&) = delete; + PageLockGuard& operator=(const PageLockGuard&) = delete; +}; + +} // namespace hmac_cpp + +#endif // HMAC_CPP_MEMLOCK_HPP_INCLUDED diff --git a/include/hmac_cpp/secret.hpp b/include/hmac_cpp/secret.hpp new file mode 100644 index 0000000..955bc42 --- /dev/null +++ b/include/hmac_cpp/secret.hpp @@ -0,0 +1,161 @@ +#ifndef HMAC_CPP_SECRET_HPP_INCLUDED +#define HMAC_CPP_SECRET_HPP_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "hmac_cpp/hmac.hpp" +#include "hmac_cpp/hmac_utils.hpp" +#include "hmac_cpp/secure_buffer.hpp" +#include "hmac_cpp/memlock.hpp" + +namespace hmac_cpp { + +class secret_string { +public: + secret_string() : nonce_(), locked_(false) {} + + explicit secret_string(const std::string& s) : nonce_(), locked_(false) { set(s); } + explicit secret_string(const uint8_t* p, size_t n) : nonce_(), locked_(false) { set(p, n); } + + secret_string(secret_string&& other) noexcept { move_from(other); } + secret_string& operator=(secret_string&& other) noexcept { + if (this != &other) { clear(); move_from(other); } + return *this; + } + + secret_string(const secret_string&) = delete; + secret_string& operator=(const secret_string&) = delete; + + ~secret_string() { clear(); } + + void clear() noexcept { + if (!ct_.empty()) { + secure_zero(ct_.data(), ct_.size()); + if (locked_) { + unlock_pages(ct_.data(), ct_.size()); + locked_ = false; + } + ct_.clear(); + ct_.shrink_to_fit(); + } + secure_zero(nonce_.data(), nonce_.size()); + } + + bool empty() const noexcept { return ct_.empty(); } + size_t size() const noexcept { return ct_.size(); } + + void set(const std::string& s) { set(reinterpret_cast(s.data()), s.size()); } + + void set(const uint8_t* p, size_t n) { + if (n > 0 && p == NULL) throw std::invalid_argument("secret_string::set: null data with non-zero length"); + clear(); + + std::vector rnd = hmac_cpp::random_bytes(12); + std::copy(rnd.begin(), rnd.end(), nonce_.begin()); + + ct_.assign(p, p + n); + + if (!ct_.empty()) { + locked_ = lock_pages(ct_.data(), ct_.size()); + } + + xor_keystream_inplace(ct_.data(), ct_.size(), nonce_.data()); + } + + bool with_plaintext(const std::function& fn) const { + std::vector subkey = hmac_cpp::get_hmac(process_key().data(), process_key().size(), + nonce_.data(), nonce_.size(), + hmac_cpp::TypeHash::SHA256); + std::vector tmp(ct_); + PageLockGuard g1(tmp.data(), tmp.size()); + PageLockGuard g2(subkey.data(), subkey.size()); + xor_keystream_inplace_with_key(tmp.data(), tmp.size(), nonce_.data(), subkey.data(), subkey.size()); + fn(tmp.data(), tmp.size()); + secure_zero(tmp.data(), tmp.size()); + secure_zero(subkey.data(), subkey.size()); + return true; + } + + std::string reveal_copy() const { + std::string out; + out.resize(ct_.size()); + with_plaintext([&](const uint8_t* p, size_t n) { + if (n) std::memcpy(&out[0], p, n); + }); + return out; + } + +private: + static std::array& process_key() { + static std::array k = []{ + std::array tmp{}; + std::vector rnd = hmac_cpp::random_bytes(32); + std::copy(rnd.begin(), rnd.end(), tmp.begin()); + (void)lock_pages(tmp.data(), tmp.size()); + return tmp; + }(); + return k; + } + + static void be32(uint8_t out[4], uint32_t v) { + out[0] = static_cast((v >> 24) & 0xFF); + out[1] = static_cast((v >> 16) & 0xFF); + out[2] = static_cast((v >> 8) & 0xFF); + out[3] = static_cast( v & 0xFF); + } + + static void xor_keystream_inplace_with_key(uint8_t* buf, size_t len, + const uint8_t* nonce12, + const uint8_t* subkey, size_t sublen) { + if (!buf && len) return; + if (!nonce12) return; + if (!subkey || sublen != 32) return; + + uint8_t msg[16]; + std::memcpy(msg, nonce12, 12); + + size_t pos = 0; + uint32_t ctr = 0; + while (pos < len) { + be32(msg + 12, ctr++); + std::vector block = hmac_cpp::get_hmac(subkey, 32, msg, sizeof(msg), + hmac_cpp::TypeHash::SHA256); + const size_t take = (len - pos < block.size()) ? (len - pos) : block.size(); + for (size_t i = 0; i < take; ++i) buf[pos + i] ^= block[i]; + pos += take; + secure_zero(block.data(), block.size()); + } + secure_zero(msg, sizeof(msg)); + } + + void xor_keystream_inplace(uint8_t* buf, size_t len, const uint8_t* nonce12) const { + std::vector subkey = hmac_cpp::get_hmac(process_key().data(), process_key().size(), + nonce12, 12, hmac_cpp::TypeHash::SHA256); + xor_keystream_inplace_with_key(buf, len, nonce12, subkey.data(), subkey.size()); + secure_zero(subkey.data(), subkey.size()); + } + + void move_from(secret_string& other) noexcept { + ct_ = std::move(other.ct_); + nonce_ = other.nonce_; + locked_ = other.locked_; + other.locked_ = false; + secure_zero(other.nonce_.data(), other.nonce_.size()); + } + +private: + std::vector ct_; + std::array nonce_; + bool locked_; +}; + +} // namespace hmac_cpp + +#endif // HMAC_CPP_SECRET_HPP_INCLUDED diff --git a/src/hmac_utils.cpp b/src/hmac_utils.cpp index a239c1b..7891bb1 100644 --- a/src/hmac_utils.cpp +++ b/src/hmac_utils.cpp @@ -6,6 +6,7 @@ #include #include #include +#include namespace hmac_cpp { @@ -26,6 +27,15 @@ namespace hmac_cpp { return constant_time_equals(a, a_len, b, b_len); } + std::vector random_bytes(size_t n) { + std::vector out(n); + std::random_device rd; + for (size_t i = 0; i < n; ++i) { + out[i] = static_cast(rd()); + } + return out; + } + static TypeHash to_type_hash(Pbkdf2Hash prf) { switch (prf) { case Pbkdf2Hash::Sha1: return TypeHash::SHA1; diff --git a/src/memlock.cpp b/src/memlock.cpp new file mode 100644 index 0000000..f0cf68e --- /dev/null +++ b/src/memlock.cpp @@ -0,0 +1,60 @@ +#include "hmac_cpp/memlock.hpp" + +#if defined(HMAC_CPP_ENABLE_MLOCK) + +#if defined(_WIN32) +#include +namespace { +inline bool do_lock(void* p, size_t n) { return VirtualLock(p, n) != 0; } +inline bool do_unlock(void* p, size_t n){ return VirtualUnlock(p, n) != 0; } +} +namespace hmac_cpp { +bool lock_pages(void* ptr, size_t len) noexcept { return (ptr && len) ? do_lock(ptr, len) : false; } +bool unlock_pages(void* ptr, size_t len) noexcept { return (ptr && len) ? do_unlock(ptr, len) : false; } +} + +#else +#include +#include +#include + +namespace { +inline void* page_align(void* p, size_t len, size_t& out_len) { + if (!p || !len) { out_len = 0; return p; } + long ps = sysconf(_SC_PAGESIZE); + if (ps <= 0) { out_len = 0; return p; } + uintptr_t addr = reinterpret_cast(p); + uintptr_t start = addr & ~static_cast(ps - 1); + uintptr_t end = (addr + len + ps - 1) & ~static_cast(ps - 1); + out_len = static_cast(end - start); + return reinterpret_cast(start); +} +} + +namespace hmac_cpp { +bool lock_pages(void* ptr, size_t len) noexcept { + if (!ptr || !len) return false; + size_t alen = 0; + void* aptr = page_align(ptr, len, alen); + if (!aptr || !alen) return false; + return ::mlock(aptr, alen) == 0; +} +bool unlock_pages(void* ptr, size_t len) noexcept { + if (!ptr || !len) return false; + size_t alen = 0; + void* aptr = page_align(ptr, len, alen); + if (!aptr || !alen) return false; + return ::munlock(aptr, alen) == 0; +} +} + +#endif + +#else // !HMAC_CPP_ENABLE_MLOCK + +namespace hmac_cpp { +bool lock_pages(void* ptr, size_t len) noexcept { (void)ptr; (void)len; return false; } +bool unlock_pages(void* ptr, size_t len) noexcept { (void)ptr; (void)len; return false; } +} + +#endif // HMAC_CPP_ENABLE_MLOCK diff --git a/test_all.cpp b/test_all.cpp index 9c98d82..a4041b2 100644 --- a/test_all.cpp +++ b/test_all.cpp @@ -12,6 +12,7 @@ #include "hmac_cpp/hmac.hpp" #include "hmac_cpp/hmac_utils.hpp" #include "hmac_cpp/encoding.hpp" +#include "hmac_cpp/secret.hpp" static std::time_t mock_time_value = 0; static int mock_errno_value = 0; @@ -749,6 +750,26 @@ TEST(EncodingPropertyTest, Base36LeadingZeros) { } } +TEST(SecretStringTest, BasicOperations) { + hmac_cpp::secret_string s("top-secret"); + std::string out; + s.with_plaintext([&](const uint8_t* p, size_t n) { + out.assign(reinterpret_cast(p), n); + }); + EXPECT_EQ(out, "top-secret"); + EXPECT_EQ(s.reveal_copy(), "top-secret"); + s.clear(); + EXPECT_TRUE(s.empty()); + EXPECT_EQ(s.reveal_copy(), ""); +} + +TEST(SecretStringTest, MoveSemantics) { + hmac_cpp::secret_string a("abc"); + hmac_cpp::secret_string b(std::move(a)); + EXPECT_TRUE(a.empty()); + EXPECT_EQ(b.reveal_copy(), "abc"); +} + int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS();