Skip to content

Commit e80abfd

Browse files
committed
feat(secret): add secret_string with page locking
1 parent 6f8621e commit e80abfd

10 files changed

Lines changed: 352 additions & 1 deletion

File tree

CMakeLists.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ option(HMACCPP_BUILD_EXAMPLES "Build the example program" OFF)
55
option(HMACCPP_BUILD_TESTS "Build the test suite" OFF)
66
option(HMACCPP_BUILD_BENCH "Build benchmarks" OFF)
77
option(HMACCPP_BUILD_SHARED "Build hmac_cpp as a shared library" OFF)
8+
option(HMACCPP_ENABLE_MLOCK "Pin secret buffers in RAM using mlock/VirtualLock" ON)
89

910
if(HMACCPP_BUILD_SHARED)
1011
set(BUILD_SHARED_LIBS ON)
@@ -17,6 +18,7 @@ set(HMAC_SOURCES
1718
src/hmac.cpp
1819
src/hmac_utils.cpp
1920
src/encoding.cpp
21+
src/memlock.cpp
2022
)
2123

2224
set(HMAC_HEADERS
@@ -27,6 +29,8 @@ set(HMAC_HEADERS
2729
include/hmac_cpp/sha256.hpp
2830
include/hmac_cpp/sha512.hpp
2931
include/hmac_cpp/secure_buffer.hpp
32+
include/hmac_cpp/memlock.hpp
33+
include/hmac_cpp/secret.hpp
3034
include/hmac_cpp/encoding.hpp
3135
include/hmac_cpp/version.hpp
3236
)
@@ -44,6 +48,10 @@ else()
4448
target_compile_definitions(hmac_cpp PUBLIC HMAC_CPP_STATIC)
4549
endif()
4650

51+
if(HMACCPP_ENABLE_MLOCK)
52+
target_compile_definitions(hmac_cpp PUBLIC HMAC_CPP_ENABLE_MLOCK)
53+
endif()
54+
4755
if(MSVC)
4856
target_compile_options(hmac_cpp PRIVATE /wd4251)
4957
else()
@@ -70,6 +78,10 @@ endif()
7078
add_executable(example_encoding example_encoding.cpp)
7179
target_link_libraries(example_encoding PRIVATE hmac_cpp)
7280
target_include_directories(example_encoding PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
81+
82+
add_executable(example_secret example_secret.cpp)
83+
target_link_libraries(example_secret PRIVATE hmac_cpp)
84+
target_include_directories(example_secret PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
7385
endif()
7486

7587
include(CMakePackageConfigHelpers)

README-RU.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,15 @@ CI охватывает Linux/Windows/macOS. Тестировалась с GCC,
5252
* `HMACCPP_BUILD_EXAMPLES`
5353
* `HMACCPP_BUILD_TESTS`
5454
* `HMACCPP_BUILD_BENCH`
55+
* `HMACCPP_ENABLE_MLOCK`
5556

5657
Библиотека по умолчанию собирается **статически**. Чтобы получить динамическую,
5758
используйте `-DHMACCPP_BUILD_SHARED=ON`. Макрос `HMAC_CPP_API` пуст для статической
5859
сборки и управляет экспортом/импортом символов в динамической.
5960

61+
`HMACCPP_ENABLE_MLOCK` включает попытку пиновать секретные буферы в RAM с помощью
62+
`mlock`/`VirtualLock`. Отключите, если платформа не позволяет.
63+
6064
### Сборка
6165

6266
```bash
@@ -137,6 +141,21 @@ secure_buffer key(std::move(secret_string)); // обнуляет перемещ
137141
auto mac = hmac::get_hmac(key, payload, hmac::TypeHash::SHA256);
138142
```
139143
144+
Для дополнительной защиты в памяти можно использовать `secret_string`, который
145+
обфусцирует данные и по возможности закрепляет их в RAM:
146+
147+
```cpp
148+
#include <hmac_cpp/secret.hpp>
149+
150+
hmac_cpp::secret_string token("super-secret-token");
151+
152+
token.with_plaintext([](const uint8_t* p, size_t n){
153+
// p действует только внутри коллбэка
154+
});
155+
156+
token.clear();
157+
```
158+
140159
### HMAC (сырой буфер)
141160

142161
```cpp

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,15 @@ Examples, tests, and benchmarks are OFF by default. Enable via:
5353
* `HMACCPP_BUILD_EXAMPLES`
5454
* `HMACCPP_BUILD_TESTS`
5555
* `HMACCPP_BUILD_BENCH`
56+
* `HMACCPP_ENABLE_MLOCK`
5657

5758
The library builds **static** by default. Use `-DHMACCPP_BUILD_SHARED=ON`
5859
to produce a shared library. The `HMAC_CPP_API` macro is empty for static
5960
builds and controls symbol export/import for shared builds.
6061

62+
`HMACCPP_ENABLE_MLOCK` toggles best-effort page locking via `mlock`/`VirtualLock`.
63+
Disable it if the platform lacks the necessary privileges.
64+
6165
### Build
6266

6367
```bash
@@ -282,7 +286,24 @@ hmac_cpp::base36_decode(b36, raw);
282286
283287
Returned strings and buffers are not zeroized; if you store secrets, prefer
284288
`secure_buffer` and wipe explicitly. Zeroization is a best‑effort and may be
285-
removed by optimizations or the C++ runtime allocator.
289+
removed by optimizations or the C++ runtime allocator. For higher resistance to
290+
memory scans, use `secret_string` which obfuscates data in memory and optionally
291+
pins buffers in RAM.
292+
293+
```cpp
294+
#include <hmac_cpp/secret.hpp>
295+
296+
hmac_cpp::secret_string token("super-secret-token");
297+
298+
token.with_plaintext([](const uint8_t* p, size_t n){
299+
// p is only valid within this callback
300+
});
301+
302+
// If needed (creates a copy):
303+
std::string plain = token.reveal_copy();
304+
305+
token.clear();
306+
```
286307

287308
---
288309

example_secret.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#include <iostream>
2+
#include <hmac_cpp/secret.hpp>
3+
4+
int main() {
5+
hmac_cpp::secret_string token("super-secret-token");
6+
7+
token.with_plaintext([](const uint8_t* p, size_t n){
8+
std::cout.write(reinterpret_cast<const char*>(p), n);
9+
});
10+
std::cout << '\n';
11+
return 0;
12+
}

include/hmac_cpp/hmac_utils.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <array>
77
#include <string>
88
#include <vector>
9+
#include <cstddef>
910

1011
#ifndef HMAC_CPP_MAX_PBKDF2_ITERATIONS
1112
#define HMAC_CPP_MAX_PBKDF2_ITERATIONS 1000000u
@@ -35,6 +36,11 @@ namespace hmac_cpp {
3536
HMAC_CPP_API bool constant_time_equal(const uint8_t* a, size_t a_len,
3637
const uint8_t* b, size_t b_len);
3738

39+
/// \brief Generate \p n cryptographically random bytes.
40+
/// \param n Number of bytes to generate.
41+
/// \return Vector filled with random bytes.
42+
HMAC_CPP_API std::vector<uint8_t> random_bytes(size_t n);
43+
3844
/// \brief Compare vectors in constant time.
3945
/// \param a First vector.
4046
/// \param b Second vector.

include/hmac_cpp/memlock.hpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#ifndef HMAC_CPP_MEMLOCK_HPP_INCLUDED
2+
#define HMAC_CPP_MEMLOCK_HPP_INCLUDED
3+
4+
#include <cstddef>
5+
#include "hmac_cpp/api.hpp"
6+
7+
namespace hmac_cpp {
8+
9+
// Pin memory pages in RAM. Returns true on success (best-effort).
10+
HMAC_CPP_API bool lock_pages(void* ptr, size_t len) noexcept;
11+
12+
// Unlock previously pinned pages. Returns true on success.
13+
HMAC_CPP_API bool unlock_pages(void* ptr, size_t len) noexcept;
14+
15+
// RAII-guard for temporary buffers.
16+
struct PageLockGuard {
17+
void* p;
18+
size_t n;
19+
bool locked;
20+
PageLockGuard(void* ptr, size_t len) noexcept
21+
: p(ptr), n(len), locked(lock_pages(ptr, len)) {}
22+
~PageLockGuard() { if (locked) unlock_pages(p, n); }
23+
PageLockGuard(const PageLockGuard&) = delete;
24+
PageLockGuard& operator=(const PageLockGuard&) = delete;
25+
};
26+
27+
} // namespace hmac_cpp
28+
29+
#endif // HMAC_CPP_MEMLOCK_HPP_INCLUDED

include/hmac_cpp/secret.hpp

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
#ifndef HMAC_CPP_SECRET_HPP_INCLUDED
2+
#define HMAC_CPP_SECRET_HPP_INCLUDED
3+
4+
#include <vector>
5+
#include <array>
6+
#include <string>
7+
#include <cstdint>
8+
#include <stdexcept>
9+
#include <algorithm>
10+
#include <functional>
11+
#include <cstring>
12+
13+
#include "hmac_cpp/hmac.hpp"
14+
#include "hmac_cpp/hmac_utils.hpp"
15+
#include "hmac_cpp/secure_buffer.hpp"
16+
#include "hmac_cpp/memlock.hpp"
17+
18+
namespace hmac_cpp {
19+
20+
class secret_string {
21+
public:
22+
secret_string() : nonce_(), locked_(false) {}
23+
24+
explicit secret_string(const std::string& s) : nonce_(), locked_(false) { set(s); }
25+
explicit secret_string(const uint8_t* p, size_t n) : nonce_(), locked_(false) { set(p, n); }
26+
27+
secret_string(secret_string&& other) noexcept { move_from(other); }
28+
secret_string& operator=(secret_string&& other) noexcept {
29+
if (this != &other) { clear(); move_from(other); }
30+
return *this;
31+
}
32+
33+
secret_string(const secret_string&) = delete;
34+
secret_string& operator=(const secret_string&) = delete;
35+
36+
~secret_string() { clear(); }
37+
38+
void clear() noexcept {
39+
if (!ct_.empty()) {
40+
secure_zero(ct_.data(), ct_.size());
41+
if (locked_) {
42+
unlock_pages(ct_.data(), ct_.size());
43+
locked_ = false;
44+
}
45+
ct_.clear();
46+
ct_.shrink_to_fit();
47+
}
48+
secure_zero(nonce_.data(), nonce_.size());
49+
}
50+
51+
bool empty() const noexcept { return ct_.empty(); }
52+
size_t size() const noexcept { return ct_.size(); }
53+
54+
void set(const std::string& s) { set(reinterpret_cast<const uint8_t*>(s.data()), s.size()); }
55+
56+
void set(const uint8_t* p, size_t n) {
57+
if (n > 0 && p == NULL) throw std::invalid_argument("secret_string::set: null data with non-zero length");
58+
clear();
59+
60+
std::vector<uint8_t> rnd = hmac_cpp::random_bytes(12);
61+
std::copy(rnd.begin(), rnd.end(), nonce_.begin());
62+
63+
ct_.assign(p, p + n);
64+
65+
if (!ct_.empty()) {
66+
locked_ = lock_pages(ct_.data(), ct_.size());
67+
}
68+
69+
xor_keystream_inplace(ct_.data(), ct_.size(), nonce_.data());
70+
}
71+
72+
bool with_plaintext(const std::function<void(const uint8_t*, size_t)>& fn) const {
73+
std::vector<uint8_t> subkey = hmac_cpp::get_hmac(process_key().data(), process_key().size(),
74+
nonce_.data(), nonce_.size(),
75+
hmac_cpp::TypeHash::SHA256);
76+
std::vector<uint8_t> tmp(ct_);
77+
PageLockGuard g1(tmp.data(), tmp.size());
78+
PageLockGuard g2(subkey.data(), subkey.size());
79+
xor_keystream_inplace_with_key(tmp.data(), tmp.size(), nonce_.data(), subkey.data(), subkey.size());
80+
fn(tmp.data(), tmp.size());
81+
secure_zero(tmp.data(), tmp.size());
82+
secure_zero(subkey.data(), subkey.size());
83+
return true;
84+
}
85+
86+
std::string reveal_copy() const {
87+
std::string out;
88+
out.resize(ct_.size());
89+
with_plaintext([&](const uint8_t* p, size_t n) {
90+
if (n) std::memcpy(&out[0], p, n);
91+
});
92+
return out;
93+
}
94+
95+
private:
96+
static std::array<uint8_t,32>& process_key() {
97+
static std::array<uint8_t,32> k = []{
98+
std::array<uint8_t,32> tmp{};
99+
std::vector<uint8_t> rnd = hmac_cpp::random_bytes(32);
100+
std::copy(rnd.begin(), rnd.end(), tmp.begin());
101+
(void)lock_pages(tmp.data(), tmp.size());
102+
return tmp;
103+
}();
104+
return k;
105+
}
106+
107+
static void be32(uint8_t out[4], uint32_t v) {
108+
out[0] = static_cast<uint8_t>((v >> 24) & 0xFF);
109+
out[1] = static_cast<uint8_t>((v >> 16) & 0xFF);
110+
out[2] = static_cast<uint8_t>((v >> 8) & 0xFF);
111+
out[3] = static_cast<uint8_t>( v & 0xFF);
112+
}
113+
114+
static void xor_keystream_inplace_with_key(uint8_t* buf, size_t len,
115+
const uint8_t* nonce12,
116+
const uint8_t* subkey, size_t sublen) {
117+
if (!buf && len) return;
118+
if (!nonce12) return;
119+
if (!subkey || sublen != 32) return;
120+
121+
uint8_t msg[16];
122+
std::memcpy(msg, nonce12, 12);
123+
124+
size_t pos = 0;
125+
uint32_t ctr = 0;
126+
while (pos < len) {
127+
be32(msg + 12, ctr++);
128+
std::vector<uint8_t> block = hmac_cpp::get_hmac(subkey, 32, msg, sizeof(msg),
129+
hmac_cpp::TypeHash::SHA256);
130+
const size_t take = (len - pos < block.size()) ? (len - pos) : block.size();
131+
for (size_t i = 0; i < take; ++i) buf[pos + i] ^= block[i];
132+
pos += take;
133+
secure_zero(block.data(), block.size());
134+
}
135+
secure_zero(msg, sizeof(msg));
136+
}
137+
138+
void xor_keystream_inplace(uint8_t* buf, size_t len, const uint8_t* nonce12) const {
139+
std::vector<uint8_t> subkey = hmac_cpp::get_hmac(process_key().data(), process_key().size(),
140+
nonce12, 12, hmac_cpp::TypeHash::SHA256);
141+
xor_keystream_inplace_with_key(buf, len, nonce12, subkey.data(), subkey.size());
142+
secure_zero(subkey.data(), subkey.size());
143+
}
144+
145+
void move_from(secret_string& other) noexcept {
146+
ct_ = std::move(other.ct_);
147+
nonce_ = other.nonce_;
148+
locked_ = other.locked_;
149+
other.locked_ = false;
150+
secure_zero(other.nonce_.data(), other.nonce_.size());
151+
}
152+
153+
private:
154+
std::vector<uint8_t> ct_;
155+
std::array<uint8_t,12> nonce_;
156+
bool locked_;
157+
};
158+
159+
} // namespace hmac_cpp
160+
161+
#endif // HMAC_CPP_SECRET_HPP_INCLUDED

src/hmac_utils.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <limits>
77
#include <algorithm>
88
#include <cstring>
9+
#include <random>
910

1011
namespace hmac_cpp {
1112

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

30+
std::vector<uint8_t> random_bytes(size_t n) {
31+
std::vector<uint8_t> out(n);
32+
std::random_device rd;
33+
for (size_t i = 0; i < n; ++i) {
34+
out[i] = static_cast<uint8_t>(rd());
35+
}
36+
return out;
37+
}
38+
2939
static TypeHash to_type_hash(Pbkdf2Hash prf) {
3040
switch (prf) {
3141
case Pbkdf2Hash::Sha1: return TypeHash::SHA1;

0 commit comments

Comments
 (0)