diff --git a/autotest/SecureBuffer/SecureBuffer.cpp b/autotest/SecureBuffer/SecureBuffer.cpp new file mode 100644 index 000000000..63afa7b88 --- /dev/null +++ b/autotest/SecureBuffer/SecureBuffer.cpp @@ -0,0 +1,148 @@ +#include +#include + +using namespace Upp; + +template +bool IsZeroed(const T* ptr, size_t count) +{ + auto bytes = reinterpret_cast(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& 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(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 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 buffer(10); + ASSERT(buffer.GetSize() == 10); + buffer[0] = 42; + ASSERT(buffer[0] == 42); + buffer.Clear(); + ASSERT(buffer.GetSize() == 0); + }); + + Test("SecureBuffer: Pick semantics", [&]{ + SecureBuffer buffer1(5); + buffer1[0] = 123; + SecureBuffer 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 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 buffer(4); + buffer[0] = 123; + buffer.Clear(); + buffer.Clear(); // Should be safe + ASSERT(buffer.GetSize() == 0); + }); + + Test("SecureBuffer: Edge case", [&]{ + SecureBuffer buffer(0); + ASSERT(buffer.GetSize() == 0); + SecureBuffer largebuffer(10000); + ASSERT(largebuffer.GetSize() == 10000); + }); +} diff --git a/autotest/SecureBuffer/SecureBuffer.upp b/autotest/SecureBuffer/SecureBuffer.upp new file mode 100644 index 000000000..259075bad --- /dev/null +++ b/autotest/SecureBuffer/SecureBuffer.upp @@ -0,0 +1,12 @@ +description "SecureBuffer unit tests\377"; + +uses + Core, + Core/SSL; + +file + SecureBuffer.cpp; + +mainconfig + "" = ""; + diff --git a/uppsrc/Core/SSL/Buffer.hpp b/uppsrc/Core/SSL/Buffer.hpp new file mode 100644 index 000000000..93150b9c6 --- /dev/null +++ b/uppsrc/Core/SSL/Buffer.hpp @@ -0,0 +1,163 @@ +#ifndef _Core_Ssl_SecureBuffer_h_ +#define _Core_Ssl_SecureBuffer_h_ + +#ifdef PLATFORM_POSIX +#include +#endif + +#ifndef LLOG +#define LLOG(x) // RLOG(x) + +template +void SecureZero(T* ptr, size_t count) +{ + static_assert(std::is_trivially_copyable_v + && std::is_trivially_destructible_v, + "Upp::SecureZero: T must be trivially copyable & destructible"); + + if(!ptr || count == 0) + return; + + OPENSSL_cleanse(reinterpret_cast(ptr), count * sizeof(T)); +} + +template +void SecureZero(T (&obj)[N]) // Safe overload for static arrays. +{ + SecureZero(obj, N); +} + +template +class SecureBuffer : Moveable>, NoCopy { + + static_assert(std::is_trivially_copyable_v + && std::is_trivially_destructible_v, + "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 +void SecureBuffer::New(size_t sz) +{ + size = 0; + ptr = nullptr; + + if(sz > 0) { + size = sz; + ptr = (T*) MemoryAlloc(size * sizeof(T)); + LockMemory(); + } +} + +template +void SecureBuffer::Pick(SecureBuffer&& src) +{ + if(this != &src) { + Free(); + ptr = src.ptr; + size = src.size; + src.ptr = nullptr; + src.size = 0; + } +} + +template +void SecureBuffer::Free() +{ + if(ptr) { + Zero(); + UnlockMemory(); + MemoryFree(ptr); + ptr = nullptr; + size = 0; + } +} + +template +void SecureBuffer::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 +void SecureBuffer::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 diff --git a/uppsrc/Core/SSL/SSL.h b/uppsrc/Core/SSL/SSL.h index f51519302..3a943882e 100644 --- a/uppsrc/Core/SSL/SSL.h +++ b/uppsrc/Core/SSL/SSL.h @@ -180,4 +180,7 @@ String AES256Decrypt(const String& in, const String& password, Gate WhenProgress = Null); bool AES256Decrypt(Stream& in, const String& password, Stream& out, Gate WhenProgress = Null); +// Secure buffer +#include "Buffer.hpp" + } diff --git a/uppsrc/Core/SSL/SSL.upp b/uppsrc/Core/SSL/SSL.upp index a4b721f1b..f25fdc9ec 100644 --- a/uppsrc/Core/SSL/SSL.upp +++ b/uppsrc/Core/SSL/SSL.upp @@ -13,6 +13,7 @@ file Socket.cpp, P7S.cpp, AES.cpp, + Buffer.hpp, SSL.icpp, Docs readonly separator, src.tpp; diff --git a/uppsrc/Core/SSL/src.tpp/Upp_SSL_SecureBuffer_en-us.tpp b/uppsrc/Core/SSL/src.tpp/Upp_SSL_SecureBuffer_en-us.tpp new file mode 100644 index 000000000..ae26ab451 --- /dev/null +++ b/uppsrc/Core/SSL/src.tpp/Upp_SSL_SecureBuffer_en-us.tpp @@ -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;%% ]] \ No newline at end of file