Skip to content

Commit 650b91f

Browse files
committed
test(secure_buffer): verify wipe and lock ranges
1 parent f4ceae1 commit 650b91f

4 files changed

Lines changed: 290 additions & 88 deletions

File tree

‎CMakeLists.txt‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,16 +129,26 @@ if(HMACCPP_BUILD_TESTS)
129129
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
130130
)
131131
FetchContent_MakeAvailable(googletest)
132-
find_package(OpenSSL REQUIRED)
133-
add_executable(test_all test_all.cpp)
134-
target_link_libraries(test_all PRIVATE hmac_cpp gtest_main OpenSSL::Crypto)
135-
target_include_directories(test_all PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
136-
add_test(NAME test_all COMMAND test_all)
132+
133+
add_executable(test_secure_buffer test_secure_buffer.cpp)
134+
target_link_libraries(test_secure_buffer PRIVATE hmac_cpp gtest_main)
135+
target_include_directories(test_secure_buffer PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
136+
add_test(NAME test_secure_buffer COMMAND test_secure_buffer)
137137

138138
add_executable(test_totp test_totp.cpp)
139139
target_link_libraries(test_totp PRIVATE hmac_cpp gtest_main)
140140
target_include_directories(test_totp PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
141141
add_test(NAME test_totp COMMAND test_totp)
142+
143+
find_package(OpenSSL QUIET)
144+
if(OpenSSL_FOUND)
145+
add_executable(test_all test_all.cpp)
146+
target_link_libraries(test_all PRIVATE hmac_cpp gtest_main OpenSSL::Crypto)
147+
target_include_directories(test_all PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
148+
add_test(NAME test_all COMMAND test_all)
149+
else()
150+
message(STATUS "OpenSSL not found; skipping test_all")
151+
endif()
142152
endif()
143153

144154
export(EXPORT hmac_cppTargets

‎include/hmac_cpp/secure_buffer.hpp‎

Lines changed: 112 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
#ifndef HMAC_CPP_SECURE_BUFFER_HPP
22
#define HMAC_CPP_SECURE_BUFFER_HPP
33

4-
#include <atomic>
54
#include <algorithm>
5+
#include <atomic>
66
#include <cstddef>
77
#include <cstdint>
88
#include <cstring>
9-
#include <vector>
10-
#include <type_traits>
9+
#include <memory>
1110
#include <string>
11+
#include <type_traits>
12+
#include <vector>
1213
#include "hmac_cpp/memlock.hpp"
1314

1415
#if defined(HAVE_EXPLICIT_BZERO)
@@ -28,6 +29,12 @@
2829

2930
namespace hmac_cpp {
3031

32+
/// \brief Default page-locking policy used by secure_buffer.
33+
struct page_locker {
34+
static bool lock(void* ptr, size_t len) noexcept { return lock_pages(ptr, len); }
35+
static bool unlock(void* ptr, size_t len) noexcept { return unlock_pages(ptr, len); }
36+
};
37+
3138
/// \brief Securely zeroes a memory region.
3239
/// \param ptr Pointer to the memory to wipe.
3340
/// \param len Number of bytes to set to zero.
@@ -48,81 +55,71 @@ static inline void secure_zero(void* ptr, size_t len) {
4855
/// \brief Vector-like buffer that zeroizes its contents on destruction.
4956
/// \tparam T Trivial value type stored in the buffer (defaults to uint8_t).
5057
/// \tparam LockOnAlloc Lock pages in memory on allocation.
51-
template<class T = uint8_t, bool LockOnAlloc = false>
58+
/// \tparam Allocator Allocator used for the backing storage.
59+
/// \tparam PageLocker Page-locking policy used when LockOnAlloc is true.
60+
template<class T = uint8_t,
61+
bool LockOnAlloc = false,
62+
class Allocator = std::allocator<T>,
63+
class PageLocker = page_locker>
5264
struct secure_buffer {
5365
static_assert(std::is_trivial<T>::value, "secure_buffer requires trivial type");
5466

67+
using vector_type = std::vector<T, Allocator>;
68+
5569
secure_buffer() {
56-
if (LockOnAlloc && !buf.empty()) {
57-
locked_ = lock_pages(buf.data(), buf.size() * sizeof(T));
58-
}
70+
lock_current_range();
5971
}
6072

6173
/// \brief Construct with n default-initialized elements.
6274
/// \param n Element count.
6375
explicit secure_buffer(size_t n) : buf(n) {
64-
if (LockOnAlloc && !buf.empty()) {
65-
locked_ = lock_pages(buf.data(), buf.size() * sizeof(T));
66-
}
76+
lock_current_range();
6777
}
6878

6979
/// \brief Construct from vector, moving its contents.
7080
/// \param v Source vector.
71-
explicit secure_buffer(std::vector<T>&& v) : buf(std::move(v)) {
72-
if (LockOnAlloc && !buf.empty()) {
73-
locked_ = lock_pages(buf.data(), buf.size() * sizeof(T));
74-
}
81+
explicit secure_buffer(vector_type&& v) : buf(std::move(v)) {
82+
lock_current_range();
7583
}
7684

7785
/// \brief Construct from std::string rvalue and zeroize the source.
7886
/// \param s Source string.
7987
template<class U = T, typename std::enable_if<std::is_same<U, uint8_t>::value, int>::type = 0>
8088
explicit secure_buffer(std::string&& s) : buf(s.begin(), s.end()) {
81-
if (LockOnAlloc && !buf.empty()) {
82-
locked_ = lock_pages(buf.data(), buf.size() * sizeof(T));
83-
}
89+
lock_current_range();
8490
if (!s.empty()) {
8591
secure_zero(&s[0], s.size());
8692
s.clear();
8793
}
8894
}
8995

9096
secure_buffer(const secure_buffer& other) : buf(other.buf) {
91-
if (LockOnAlloc && !buf.empty()) {
92-
locked_ = lock_pages(buf.data(), buf.size() * sizeof(T));
93-
}
97+
lock_current_range();
9498
}
9599

96100
secure_buffer& operator=(const secure_buffer& other) {
97101
if (this != &other) {
98-
secure_zero(buf.data(), buf.size() * sizeof(T));
99-
if (locked_) {
100-
unlock_pages(buf.data(), buf.size() * sizeof(T));
101-
}
102+
clear();
102103
buf = other.buf;
103-
if (LockOnAlloc && !buf.empty()) {
104-
locked_ = lock_pages(buf.data(), buf.size() * sizeof(T));
105-
} else {
106-
locked_ = false;
107-
}
104+
lock_current_range();
108105
}
109106
return *this;
110107
}
111108

112109
secure_buffer(secure_buffer&& other) noexcept
113-
: buf(std::move(other.buf)), locked_(other.locked_) {
114-
other.locked_ = false;
110+
: buf(std::move(other.buf)),
111+
locked_ptr_(other.locked_ptr_),
112+
locked_bytes_(other.locked_bytes_) {
113+
other.clear_locked_range();
115114
}
116115

117116
secure_buffer& operator=(secure_buffer&& other) noexcept {
118117
if (this != &other) {
119-
secure_zero(buf.data(), buf.size() * sizeof(T));
120-
if (locked_) {
121-
unlock_pages(buf.data(), buf.size() * sizeof(T));
122-
}
118+
clear();
123119
buf = std::move(other.buf);
124-
locked_ = other.locked_;
125-
other.locked_ = false;
120+
locked_ptr_ = other.locked_ptr_;
121+
locked_bytes_ = other.locked_bytes_;
122+
other.clear_locked_range();
126123
}
127124
return *this;
128125
}
@@ -131,17 +128,14 @@ struct secure_buffer {
131128
~secure_buffer() noexcept { clear(); }
132129

133130
/// \brief Check whether pages are locked.
134-
bool is_locked() const noexcept { return locked_; }
131+
bool is_locked() const noexcept { return locked_ptr_ != nullptr; }
135132

136133
/// \brief Clear and deallocate the buffer.
137134
void clear() noexcept {
138135
secure_zero(buf.data(), buf.size() * sizeof(T));
139-
if (locked_) {
140-
unlock_pages(buf.data(), buf.size() * sizeof(T));
141-
locked_ = false;
142-
}
143-
buf.clear();
144-
buf.shrink_to_fit();
136+
unlock_locked_range();
137+
vector_type empty(buf.get_allocator());
138+
buf.swap(empty);
145139
}
146140

147141
/// \brief Resize the buffer, zeroizing data before a reallocation releases it.
@@ -156,35 +150,26 @@ struct secure_buffer {
156150
}
157151

158152
// std::vector::resize may release the old allocation before we can
159-
// wipe it. Allocate the replacement first so the old secret remains
160-
// reachable until it has been zeroized and unlocked.
161-
std::vector<T> replacement(n);
153+
// wipe it. Allocate and lock the replacement first so the old secret
154+
// remains reachable until it has been zeroized and unlocked.
155+
vector_type replacement(n, T{}, buf.get_allocator());
156+
replacement_guard guard(replacement.data(), replacement.size() * sizeof(T));
157+
guard.lock();
162158
std::copy(buf.begin(), buf.end(), replacement.begin());
163159

164160
secure_zero(buf.data(), old_sz * sizeof(T));
165-
if (locked_) {
166-
unlock_pages(buf.data(), old_sz * sizeof(T));
167-
locked_ = false;
168-
}
161+
unlock_locked_range();
169162

170163
buf.swap(replacement);
171-
if (LockOnAlloc && !buf.empty()) {
172-
locked_ = lock_pages(buf.data(), buf.size() * sizeof(T));
173-
}
164+
set_locked_range(buf.data(), buf.size() * sizeof(T), guard.release());
174165
}
175166

176167
/// \brief Assign from raw pointer.
177168
void assign(const T* p, size_t n) {
178169
secure_zero(buf.data(), buf.size() * sizeof(T));
179-
if (locked_) {
180-
unlock_pages(buf.data(), buf.size() * sizeof(T));
181-
}
170+
unlock_locked_range();
182171
buf.assign(p, p + n);
183-
if (LockOnAlloc && !buf.empty()) {
184-
locked_ = lock_pages(buf.data(), buf.size() * sizeof(T));
185-
} else {
186-
locked_ = false;
187-
}
172+
lock_current_range();
188173
}
189174

190175
/// \brief Assign from std::string rvalue and zeroize the source.
@@ -204,14 +189,73 @@ struct secure_buffer {
204189
T& operator[](size_t i) { return buf[i]; }
205190
const T& operator[](size_t i) const { return buf[i]; }
206191

207-
typename std::vector<T>::iterator begin() { return buf.begin(); }
208-
typename std::vector<T>::iterator end() { return buf.end(); }
209-
typename std::vector<T>::const_iterator begin() const { return buf.begin(); }
210-
typename std::vector<T>::const_iterator end() const { return buf.end(); }
192+
typename vector_type::iterator begin() { return buf.begin(); }
193+
typename vector_type::iterator end() { return buf.end(); }
194+
typename vector_type::const_iterator begin() const { return buf.begin(); }
195+
typename vector_type::const_iterator end() const { return buf.end(); }
211196

212197
private:
213-
std::vector<T> buf;
214-
bool locked_{};
198+
struct replacement_guard {
199+
replacement_guard(void* ptr, size_t bytes) noexcept : ptr_(ptr), bytes_(bytes) {}
200+
201+
~replacement_guard() {
202+
if (!active_) {
203+
return;
204+
}
205+
secure_zero(ptr_, bytes_);
206+
if (locked_) {
207+
PageLocker::unlock(ptr_, bytes_);
208+
}
209+
}
210+
211+
void lock() noexcept {
212+
if (LockOnAlloc && ptr_ != nullptr && bytes_ != 0) {
213+
locked_ = PageLocker::lock(ptr_, bytes_);
214+
}
215+
}
216+
217+
bool release() noexcept {
218+
active_ = false;
219+
return locked_;
220+
}
221+
222+
void* ptr_{};
223+
size_t bytes_{};
224+
bool locked_{};
225+
bool active_{true};
226+
};
227+
228+
void lock_current_range() noexcept {
229+
if (LockOnAlloc && !buf.empty()) {
230+
set_locked_range(buf.data(), buf.size() * sizeof(T),
231+
PageLocker::lock(buf.data(), buf.size() * sizeof(T)));
232+
}
233+
}
234+
235+
void unlock_locked_range() noexcept {
236+
if (locked_ptr_ != nullptr) {
237+
PageLocker::unlock(locked_ptr_, locked_bytes_);
238+
}
239+
clear_locked_range();
240+
}
241+
242+
void set_locked_range(void* ptr, size_t bytes, bool locked) noexcept {
243+
if (locked) {
244+
locked_ptr_ = ptr;
245+
locked_bytes_ = bytes;
246+
} else {
247+
clear_locked_range();
248+
}
249+
}
250+
251+
void clear_locked_range() noexcept {
252+
locked_ptr_ = nullptr;
253+
locked_bytes_ = 0;
254+
}
255+
256+
vector_type buf;
257+
void* locked_ptr_{};
258+
size_t locked_bytes_{};
215259
};
216260

217261
} // namespace hmac_cpp

‎test_all.cpp‎

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
#include "hmac_cpp/hmac.hpp"
1313
#include "hmac_cpp/hmac_utils.hpp"
1414
#include "hmac_cpp/encoding.hpp"
15-
#include "hmac_cpp/secure_buffer.hpp"
1615
#include "hmac_cpp/secret_string.hpp"
1716

1817
static std::time_t mock_time_value = 0;
@@ -33,20 +32,6 @@ static std::vector<uint8_t> from_hex(const std::string& hex) {
3332
return out;
3433
}
3534

36-
TEST(SecureBufferTest, ResizeGrowthPreservesContents) {
37-
hmac_cpp::secure_buffer<uint8_t, true> buffer(8);
38-
for (size_t i = 0; i < buffer.size(); ++i) {
39-
buffer[i] = static_cast<uint8_t>(i + 1);
40-
}
41-
42-
buffer.resize(4096);
43-
44-
ASSERT_EQ(buffer.size(), 4096u);
45-
for (size_t i = 0; i < 8; ++i) {
46-
EXPECT_EQ(buffer[i], static_cast<uint8_t>(i + 1));
47-
}
48-
}
49-
5035
TEST(HashTest, SHA1) {
5136
EXPECT_EQ(hmac_hash::sha1("grape"),
5237
"bc8a2f8cdedb005b5c787692853709b060db75ff");

0 commit comments

Comments
 (0)