Skip to content
Merged
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: 12 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -17,6 +18,7 @@ set(HMAC_SOURCES
src/hmac.cpp
src/hmac_utils.cpp
src/encoding.cpp
src/memlock.cpp
)

set(HMAC_HEADERS
Expand All @@ -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
)
Expand All @@ -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()
Expand All @@ -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)
Expand Down
19 changes: 19 additions & 0 deletions README-RU.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.hpp>

hmac_cpp::secret_string token("super-secret-token");

token.with_plaintext([](const uint8_t* p, size_t n){
// p действует только внутри коллбэка
});

token.clear();
```

### HMAC (сырой буфер)

```cpp
Expand Down
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.hpp>

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();
```

---

Expand Down
12 changes: 12 additions & 0 deletions example_secret.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#include <iostream>
#include <hmac_cpp/secret.hpp>

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<const char*>(p), n);
});
std::cout << '\n';
return 0;
}
6 changes: 6 additions & 0 deletions include/hmac_cpp/hmac_utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <array>
#include <string>
#include <vector>
#include <cstddef>

#ifndef HMAC_CPP_MAX_PBKDF2_ITERATIONS
#define HMAC_CPP_MAX_PBKDF2_ITERATIONS 1000000u
Expand Down Expand Up @@ -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<uint8_t> random_bytes(size_t n);

/// \brief Compare vectors in constant time.
/// \param a First vector.
/// \param b Second vector.
Expand Down
29 changes: 29 additions & 0 deletions include/hmac_cpp/memlock.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#ifndef HMAC_CPP_MEMLOCK_HPP_INCLUDED
#define HMAC_CPP_MEMLOCK_HPP_INCLUDED

#include <cstddef>
#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
161 changes: 161 additions & 0 deletions include/hmac_cpp/secret.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
#ifndef HMAC_CPP_SECRET_HPP_INCLUDED
#define HMAC_CPP_SECRET_HPP_INCLUDED

#include <vector>
#include <array>
#include <string>
#include <cstdint>
#include <stdexcept>
#include <algorithm>
#include <functional>
#include <cstring>

#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<const uint8_t*>(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<uint8_t> 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<void(const uint8_t*, size_t)>& fn) const {
std::vector<uint8_t> subkey = hmac_cpp::get_hmac(process_key().data(), process_key().size(),
nonce_.data(), nonce_.size(),
hmac_cpp::TypeHash::SHA256);
std::vector<uint8_t> 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<uint8_t,32>& process_key() {
static std::array<uint8_t,32> k = []{
std::array<uint8_t,32> tmp{};
std::vector<uint8_t> 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<uint8_t>((v >> 24) & 0xFF);
out[1] = static_cast<uint8_t>((v >> 16) & 0xFF);
out[2] = static_cast<uint8_t>((v >> 8) & 0xFF);
out[3] = static_cast<uint8_t>( 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<uint8_t> 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<uint8_t> 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<uint8_t> ct_;
std::array<uint8_t,12> nonce_;
bool locked_;
};

} // namespace hmac_cpp

#endif // HMAC_CPP_SECRET_HPP_INCLUDED
10 changes: 10 additions & 0 deletions src/hmac_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <limits>
#include <algorithm>
#include <cstring>
#include <random>

namespace hmac_cpp {

Expand All @@ -26,6 +27,15 @@ namespace hmac_cpp {
return constant_time_equals(a, a_len, b, b_len);
}

std::vector<uint8_t> random_bytes(size_t n) {
std::vector<uint8_t> out(n);
std::random_device rd;
for (size_t i = 0; i < n; ++i) {
out[i] = static_cast<uint8_t>(rd());
}
return out;
}

static TypeHash to_type_hash(Pbkdf2Hash prf) {
switch (prf) {
case Pbkdf2Hash::Sha1: return TypeHash::SHA1;
Expand Down
Loading
Loading