Core/SSL: Added SecureBuffer and SecureZero (#272)

This commit is contained in:
İsmail Yılmaz 2025-06-10 12:56:05 +03:00 committed by GitHub
parent 6a87189ef6
commit 7fb2d67521
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 459 additions and 0 deletions

View file

@ -0,0 +1,148 @@
#include <Core/Core.h>
#include <Core/SSL/SSL.h>
using namespace Upp;
template<typename T>
bool IsZeroed(const T* ptr, size_t count)
{
auto bytes = reinterpret_cast<const byte*>(ptr);
for(size_t i = 0; i < count * sizeof(T); ++i) {
if(bytes[i] != 0) {
LOG(Format("Memory not zeroed at offset %d: found value 0x%02X", (int) i, (int) bytes[i]));
return false;
}
}
return true;
}
CONSOLE_APP_MAIN
{
StdLogSetup(LOG_COUT | LOG_FILE);
auto Test = [](const String& name, const Function<void()>& fn) {
String txt = "---" + name + ": ";
fn();
LOG(txt << "PASSED");
};
Test("SecureZero: Basic integer array", [&]{
int buffer[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
SecureZero(buffer);
ASSERT(IsZeroed(buffer, 10));
});
Test("SecureZero: Empty array (edge case)", [&]{
int buffer[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
SecureZero(buffer, 0);
ASSERT(buffer[0] == 1 && buffer[9] == 10); // Should not change anything
});
Test("SecureZero: NULL pointer (edge case)", [&]{
// This SHOULD NOT crash
SecureZero<int>(nullptr, 10);
});
Test("SecureZero: Byte-by-byte zeroing", [&]{
byte buffer[7] = {1, 2, 3, 4, 5, 6, 7}; // Odd size to test boundary cases
SecureZero(buffer, 7);
ASSERT(IsZeroed(buffer, 7));
});
Test("SecureZero: Complex types", [&]{
struct TestStruct {
int a;
double b;
char c[10];
} buffer[5];
for(int i = 0; i < 5; ++i) {
buffer[i].a = i;
buffer[i].b = i * 3.14;
memset(buffer[i].c, 'A' + i, 10);
}
SecureZero(buffer, 5);
ASSERT(IsZeroed(buffer, 5));
});
Test("SecureZero: Partial array zeroing", [&]{
int buffer[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
SecureZero(buffer + 3, 4); // Zero elements 3, 4, 5, 6
ASSERT(buffer[0] == 1);
ASSERT(buffer[1] == 2);
ASSERT(buffer[2] == 3);
ASSERT(buffer[3] == 0);
ASSERT(buffer[4] == 0);
ASSERT(buffer[5] == 0);
ASSERT(buffer[6] == 0);
ASSERT(buffer[7] == 8);
ASSERT(buffer[8] == 9);
ASSERT(buffer[9] == 10);
});
Test("SecureZero: Resistance to compiler optimization", [&]{
const int BUFSIZE = 1024;
Buffer<byte> buffer(BUFSIZE, 0xFF);
SecureZero(~buffer, BUFSIZE);
volatile byte checksum = 0;
for(int i = 0 ; i < BUFSIZE; i++)
checksum ^= buffer[i];
ASSERT(checksum == 0);
});
Test("SecureZero: Overlapping memory regions", [&] {
byte buffer[16];
memset(buffer, 0xAB, 16);
SecureZero(buffer + 4, 8); // Zero middle 8 bytes
for(int i = 0; i < 4; ++i) ASSERT(buffer[i] == 0xAB);
for(int i = 4; i < 12; ++i) ASSERT(buffer[i] == 0x00);
for(int i = 12; i < 16; ++i) ASSERT(buffer[i] == 0xAB);
});
Test("SecureBuffer: Basic functionality", [&]{
SecureBuffer<int> buffer(10);
ASSERT(buffer.GetSize() == 10);
buffer[0] = 42;
ASSERT(buffer[0] == 42);
buffer.Clear();
ASSERT(buffer.GetSize() == 0);
});
Test("SecureBuffer: Pick semantics", [&]{
SecureBuffer<int> buffer1(5);
buffer1[0] = 123;
SecureBuffer<int> buffer2 = pick(buffer1);
ASSERT(buffer2.GetSize() == 5);
ASSERT(buffer2[0] == 123);
ASSERT(buffer1.GetSize() == 0); // NOLINT
});
Test("SecureBuffer: Zeroing verification (security critical)", [&]{
struct TestStruct {
int a = 0xDEADBEEF;
char b[8] = "TEST";
};
SecureBuffer<TestStruct> buffer(1);
buffer[0].a = 0xCAFEBABE;
strcpy(buffer[0].b, "SECRET");
TestStruct *ptr = ~buffer;
buffer.Zero();
ASSERT(IsZeroed(ptr, buffer.GetSize()));
});
Test("SecureBuffer: Multiple clear calls", [&]{
SecureBuffer<int> buffer(4);
buffer[0] = 123;
buffer.Clear();
buffer.Clear(); // Should be safe
ASSERT(buffer.GetSize() == 0);
});
Test("SecureBuffer: Edge case", [&]{
SecureBuffer<char> buffer(0);
ASSERT(buffer.GetSize() == 0);
SecureBuffer<double> largebuffer(10000);
ASSERT(largebuffer.GetSize() == 10000);
});
}

View file

@ -0,0 +1,12 @@
description "SecureBuffer unit tests\377";
uses
Core,
Core/SSL;
file
SecureBuffer.cpp;
mainconfig
"" = "";

163
uppsrc/Core/SSL/Buffer.hpp Normal file
View file

@ -0,0 +1,163 @@
#ifndef _Core_Ssl_SecureBuffer_h_
#define _Core_Ssl_SecureBuffer_h_
#ifdef PLATFORM_POSIX
#include <sys/mman.h>
#endif
#ifndef LLOG
#define LLOG(x) // RLOG(x)
template <class T>
void SecureZero(T* ptr, size_t count)
{
static_assert(std::is_trivially_copyable_v<T>
&& std::is_trivially_destructible_v<T>,
"Upp::SecureZero: T must be trivially copyable & destructible");
if(!ptr || count == 0)
return;
OPENSSL_cleanse(reinterpret_cast<void*>(ptr), count * sizeof(T));
}
template<class T, size_t N>
void SecureZero(T (&obj)[N]) // Safe overload for static arrays.
{
SecureZero(obj, N);
}
template <class T>
class SecureBuffer : Moveable<SecureBuffer<T>>, NoCopy {
static_assert(std::is_trivially_copyable_v<T>
&& std::is_trivially_destructible_v<T>,
"Upp::SecureBuffer: T must be trivially copyable & destructible");
public:
SecureBuffer() { size = 0; ptr = nullptr; }
SecureBuffer(size_t size_) { New(size_); }
SecureBuffer(SecureBuffer&& src) { Pick(pick(src)); }
~SecureBuffer() { Free(); }
SecureBuffer& operator=(SecureBuffer&& src) { Pick(pick(src)); return *this; }
void Alloc(size_t size) { Free(); New(size); }
void Clear() { Free(); }
void Zero() { SecureZero(ptr, size); };
size_t GetSize() const { return size; }
bool IsEmpty() const { return ptr == nullptr; }
operator T*() { return ptr; }
operator const T*() const { return ptr; }
T *operator~() { return ptr; }
const T *operator~() const { return ptr; }
T *Get() { return ptr; }
const T *Get() const { return ptr; }
T *begin() { return ptr; }
const T *begin() const { return ptr; }
T* Begin() { return ptr; }
const T* Begin() const { return ptr; }
T* end() { return ptr + size; }
const T* end() const { return ptr + size; }
T* End() { return ptr + size; }
const T* End() const { return ptr + size; }
T& operator[](size_t i) { ASSERT(i < size); return ptr[i]; }
const T& operator[](size_t i) const { ASSERT(i < size); return ptr[i]; }
private:
void New(size_t sz);
void Free();
void Pick(SecureBuffer&& src);
void LockMemory();
void UnlockMemory();
T* ptr = nullptr;
size_t size;
};
template<typename T>
void SecureBuffer<T>::New(size_t sz)
{
size = 0;
ptr = nullptr;
if(sz > 0) {
size = sz;
ptr = (T*) MemoryAlloc(size * sizeof(T));
LockMemory();
}
}
template<typename T>
void SecureBuffer<T>::Pick(SecureBuffer&& src)
{
if(this != &src) {
Free();
ptr = src.ptr;
size = src.size;
src.ptr = nullptr;
src.size = 0;
}
}
template<typename T>
void SecureBuffer<T>::Free()
{
if(ptr) {
Zero();
UnlockMemory();
MemoryFree(ptr);
ptr = nullptr;
size = 0;
}
}
template<typename T>
void SecureBuffer<T>::LockMemory()
{
if(!ptr || !size)
return;
#if defined(PLATFORM_WIN32)
if(!VirtualLock((LPVOID) ptr, size * sizeof(T))) {
LLOG("SecureBuffer::LockMemory: VirtualLock failed with error code " << GetLastError());
}
#elif defined(PLATFORM_POSIX)
if(mlock((void*) ptr, size * sizeof(T)) != 0) {
LLOG("SecureBuffer::LockMemory: mlock failed with errno " << errno << " (" << strerror(errno) << ")");
}
#else
NEVER();
#endif
}
template<typename T>
void SecureBuffer<T>::UnlockMemory()
{
if(!ptr || !size)
return;
#if defined(PLATFORM_WIN32)
if(!VirtualUnlock((LPVOID) ptr, size * sizeof(T))) {
LLOG("SecureBuffer::UnlockMemory: VirtualUnlock failed with error code " << GetLastError());
}
#elif defined(PLATFORM_POSIX)
if(munlock((void*) ptr, size * sizeof(T)) != 0) {
LLOG("SecureBuffer::UnlockMemory: munlock failed with errno " << errno << " (" << strerror(errno) << ")");
}
#else
NEVER();
#endif
}
#undef LLOG
#endif
#endif

View file

@ -180,4 +180,7 @@ String AES256Decrypt(const String& in, const String& password, Gate<int64, int64
bool AES256Encrypt(Stream& in, const String& password, Stream& out, Gate<int64, int64> WhenProgress = Null);
bool AES256Decrypt(Stream& in, const String& password, Stream& out, Gate<int64, int64> WhenProgress = Null);
// Secure buffer
#include "Buffer.hpp"
}

View file

@ -13,6 +13,7 @@ file
Socket.cpp,
P7S.cpp,
AES.cpp,
Buffer.hpp,
SSL.icpp,
Docs readonly separator,
src.tpp;

View file

@ -0,0 +1,132 @@
topic "SecureBuffer";
[i448;a25;kKO9;2 $$1,0#37138531426314131252341829483380:class]
[l288;2 $$2,2#27521748481378242620020725143825:desc]
[0 $$3,0#96390100711032703541132217272105:end]
[H6;0 $$4,0#05600065144404261032431302351956:begin]
[i448;a25;kKO9;2 $$5,0#37138531426314131252341829483370:item]
[l288;a4;*@5;1 $$6,6#70004532496200323422659154056402:requirement]
[l288;i1121;b17;O9;~~~.1408;2 $$7,0#10431211400427159095818037425705:param]
[i448;b42;O9;2 $$8,8#61672508125594000341940100500538:tparam]
[b42;2 $$9,9#13035079074754324216151401829390:normal]
[2 $$0,0#00000000000000000000000000000000:Default]
[{_}
[ {{10000@(113.42.0) [s0;%% [*@7;4 SecureBuffer]]}}&]
[s3; &]
[s1;:noref: [@(0.0.255)3 template][3 ]<[@(0.0.255) class]_[*@4 T][@(0.0.255) >]&]
[s1;:Upp`:`:SecureBuffer: [@(0.0.255) class ][* SecureBuffer ][*@(0.0.255) :][*
][*@3 Moveable][*@(0.0.255) <][* _SecureBuffer][*@(0.0.255) <][*@4 T][*@(0.0.255) >][* _>_,
NoCopy]&]
[s6; [@4 T] must be trivially copyable and destructible.&]
[s0;l288;%% A dynamic buffer designed for storing sensitive cryptographic
data such as private keys, symmetric keys, passwords, certificates,
nonces, and other security`-critical material. SecureBuffer attempts
to lock its memory region in RAM (using mlock/VirtualLock) to
prevent it from being swapped to disk. This locking is best`-effort
and [/ may ]fail on systems with limited permissions or resource
limits. Regardless of locking success, the buffer is securely
zeroed before deallocation to reduce the risk of sensitive data
lingering in memory. SecureBuffer is [/ not ]thread safe. External
synchronization is required for concurrent access.&]
[s3; &]
[ {{10000F(128)G(128)@1 [s0;%% [* Constructor detail]]}}&]
[s3; &]
[s5;:Upp`:`:SecureBuffer`:`:SecureBuffer`(`): [* SecureBuffer]()&]
[s2;%% Default constructor. Creates an empty secure buffer.&]
[s3; &]
[s4; &]
[s5;:Upp`:`:SecureBuffer`:`:SecureBuffer`(size`_t`): [* SecureBuffer](size`_t
[*@3 size])&]
[s2;%% Constructs a buffer of given [%-*@3 size], allocates memory
and locks it.&]
[s3; &]
[s4; &]
[s5;:Upp`:`:SecureBuffer`:`:SecureBuffer`(SecureBuffer`&`&`): [* SecureBuffer]([* SecureB
uffer][@(0.0.255) `&`&] [*@3 src])&]
[s2;%% Pick constructor. Destroys source container [%-*@3 src].&]
[s3; &]
[s4; &]
[s5;:Upp`:`:SecureBuffer`:`:`~SecureBuffer`(`): [* `~SecureBuffer]()&]
[s2;%% Destructor. Frees memory, securely zeroes the content, and
unlocks it from physical memory.&]
[s3; &]
[ {{10000F(128)G(128)@1 [s0;%% [* Public Method List]]}}&]
[s3; &]
[s5;:Upp`:`:SecureBuffer`:`:operator`=`(SecureBuffer`&`&`): SecureBuffer[@(0.0.255) `&]
operator[@(0.0.255) `=]([* SecureBuffer][@(0.0.255) `&`&] [*@3 src])&]
[s2;%% Pick operator. Destroys source buffer [%-*@3 src].&]
[s3; &]
[s4; &]
[s5;:Upp`:`:SecureBuffer`:`:Alloc`(size`_t`): [@(0.0.255) void] [* Alloc](size`_t
[*@3 size])&]
[s2;%% Clears existing buffer and allocates a new one of specified
[%-*@3 size].&]
[s3; &]
[s4; &]
[s5;:Upp`:`:SecureBuffer`:`:Clear`(`): [@(0.0.255) void] [* Clear]()&]
[s2;%% Releases the buffer and securely zeroes its contents.&]
[s3; &]
[s4; &]
[s5;:Upp`:`:SecureBuffer`:`:Zero`(`): [@(0.0.255) void] [* Zero]()&]
[s2;%% Explicitly zeroes the contents of the buffer in a secure way.
Doesn`'t release or destroy the buffer.&]
[s3; &]
[s4; &]
[s5;:Upp`:`:SecureBuffer`:`:GetSize`(`)const: size`_t [* GetSize]()
[@(0.0.255) const]&]
[s2;%% Returns the size of buffer. Return 0 if the buffer is empty.&]
[s3; &]
[s4; &]
[s5;:Upp`:`:SecureBuffer`:`:IsEmpty`(`)const: [@(0.0.255) bool] [* IsEmpty]()
[@(0.0.255) const]&]
[s2;%% Returns true if the buffer is empty.&]
[s3; &]
[s4; &]
[s5;:Upp`:`:SecureBuffer`:`:operator T`*`(`): operator T [@(0.0.255) `*]()&]
[s5;:Upp`:`:SecureBuffer`:`:operator const T`*`(`)const: operator
[@(0.0.255) const] T [@(0.0.255) `*]() [@(0.0.255) const]&]
[s5;:Upp`:`:SecureBuffer`:`:operator`~`(`): T [@(0.0.255) `*]operator[@(0.0.255) `~]()&]
[s5;:Upp`:`:SecureBuffer`:`:operator`~`(`)const: [@(0.0.255) const]
T [@(0.0.255) `*]operator[@(0.0.255) `~]() [@(0.0.255) const]&]
[s5;:Upp`:`:SecureBuffer`:`:Get`(`): T [@(0.0.255) `*][* Get]()&]
[s5;:Upp`:`:SecureBuffer`:`:Get`(`)const: [@(0.0.255) const] T [@(0.0.255) `*][* Get]()
[@(0.0.255) const]&]
[s5;:Upp`:`:SecureBuffer`:`:begin`(`): T [@(0.0.255) `*][* begin]()&]
[s5;:Upp`:`:SecureBuffer`:`:begin`(`)const: [@(0.0.255) const] T [@(0.0.255) `*][* begin]()
[@(0.0.255) const]&]
[s5;:Upp`:`:SecureBuffer`:`:Begin`(`): T [@(0.0.255) `*][* Begin]()&]
[s5;:Upp`:`:SecureBuffer`:`:Begin`(`)const: [@(0.0.255) const] T [@(0.0.255) `*][* Begin]()
[@(0.0.255) const]&]
[s2;%% Returns a pointer to the first element of the buffer or [@(0.0.255) nullptr
]if the buffer is empty.&]
[s3; &]
[s4; &]
[s5;:Upp`:`:SecureBuffer`:`:end`(`): T [@(0.0.255) `*][* end]()&]
[s5;:Upp`:`:SecureBuffer`:`:end`(`)const: [@(0.0.255) const] T [@(0.0.255) `*][* end]()
[@(0.0.255) const]&]
[s5;:Upp`:`:SecureBuffer`:`:End`(`): T [@(0.0.255) `*][* End]()&]
[s5;:Upp`:`:SecureBuffer`:`:End`(`)const: [@(0.0.255) const] T [@(0.0.255) `*][* End]()
[@(0.0.255) const]&]
[s2;%% Returns a pointer to the last element of the buffer or [@(0.0.255) nullptr
]if the buffer is empty.&]
[s3; &]
[s4; &]
[s5;:Upp`:`:SecureBuffer`:`:operator`[`]`(size`_t`): T[@(0.0.255) `&]
operator[@(0.0.255) `[`]](size`_t i)&]
[s5;:Upp`:`:SecureBuffer`:`:operator`[`]`(size`_t`)const: [@(0.0.255) const]
T[@(0.0.255) `&] operator[@(0.0.255) `[`]](size`_t i) [@(0.0.255) const]&]
[s2;%% Provides indexed access to elements. Checks bounds (and asserts)
in DEBUG mode.&]
[s3; &]
[ {{10000F(128)G(128)@1 [s0;%% [* Function List]]}}&]
[s3; &]
[s5;:Upp`:`:SecureZero`(T`*`,size`_t`): [@(0.0.255) template] <[@(0.0.255) class]
T>&]
[s5;:Upp`:`:SecureZero`(T`*`,size`_t`): [@(0.0.255) void] [* SecureZero](T
[@(0.0.255) `*][*@3 ptr], size`_t [*@3 count])&]
[s0;:Upp`:`:SecureZero`(T`& obj`): [@(0.0.255) void] [* SecureZero](T[@(0.0.255) `&]
[*@3 obj])&]
[s6; [@4 T] must be trivially copyable and destructible.&]
[s2;%% A secure memory zeroing function used internally by SecureBuffer.
This function overwrites memory contents in a way that is [/ not]
optimized away by the compiler.&]
[s0;%% ]]