diff --git a/kmipclient/examples/example_create_aes.cpp b/kmipclient/examples/example_create_aes.cpp index cf99814..cf5e7f3 100644 --- a/kmipclient/examples/example_create_aes.cpp +++ b/kmipclient/examples/example_create_aes.cpp @@ -11,12 +11,13 @@ #include #include +#include using namespace kmipclient; namespace { - void print_hex(const std::vector &bytes) { + void print_hex(std::span bytes) { for (const auto b : bytes) { std::cout << std::hex << std::setw(2) << std::setfill('0') << static_cast(b); diff --git a/kmipclient/examples/example_get.cpp b/kmipclient/examples/example_get.cpp index 9cb3402..ac78429 100644 --- a/kmipclient/examples/example_get.cpp +++ b/kmipclient/examples/example_get.cpp @@ -11,10 +11,11 @@ #include "kmipclient/kmipclient_version.hpp" #include +#include using namespace kmipclient; -void print_hex(const std::vector &key) { +void print_hex(std::span key) { for (auto const &c : key) { std::cout << std::hex << static_cast(c); } diff --git a/kmipclient/examples/example_get_attributes.cpp b/kmipclient/examples/example_get_attributes.cpp index 37a75b0..d37df3a 100644 --- a/kmipclient/examples/example_get_attributes.cpp +++ b/kmipclient/examples/example_get_attributes.cpp @@ -12,10 +12,11 @@ #include "kmipcore/kmip_basics.hpp" #include +#include using namespace kmipclient; -void print_hex(const std::vector &key) { +void print_hex(std::span key) { for (auto const &c : key) { std::cout << std::hex << static_cast(c); } diff --git a/kmipclient/examples/example_get_logger.cpp b/kmipclient/examples/example_get_logger.cpp index d2e138a..6d6172d 100644 --- a/kmipclient/examples/example_get_logger.cpp +++ b/kmipclient/examples/example_get_logger.cpp @@ -13,6 +13,7 @@ #include #include +#include using namespace kmipclient; @@ -31,7 +32,7 @@ namespace { } }; - void print_hex(const std::vector &key) { + void print_hex(std::span key) { for (auto const &c : key) { std::cout << std::hex << static_cast(c); } diff --git a/kmipclient/examples/example_get_tls_verify.cpp b/kmipclient/examples/example_get_tls_verify.cpp index d0581c1..cac46d0 100644 --- a/kmipclient/examples/example_get_tls_verify.cpp +++ b/kmipclient/examples/example_get_tls_verify.cpp @@ -12,13 +12,14 @@ #include #include +#include #include using namespace kmipclient; namespace { - void print_hex(const std::vector &key) { + void print_hex(std::span key) { for (auto const &c : key) { std::cout << std::hex << static_cast(c); } diff --git a/kmipclient/examples/example_register_key.cpp b/kmipclient/examples/example_register_key.cpp index b3c62c1..19da0b0 100644 --- a/kmipclient/examples/example_register_key.cpp +++ b/kmipclient/examples/example_register_key.cpp @@ -11,12 +11,13 @@ #include #include +#include using namespace kmipclient; namespace { - void print_hex(const std::vector &bytes) { + void print_hex(std::span bytes) { for (const auto b : bytes) { std::cout << std::hex << std::setw(2) << std::setfill('0') << static_cast(b); diff --git a/kmipclient/examples/example_register_secret.cpp b/kmipclient/examples/example_register_secret.cpp index aeb19de..d2bee71 100644 --- a/kmipclient/examples/example_register_secret.cpp +++ b/kmipclient/examples/example_register_secret.cpp @@ -11,13 +11,14 @@ #include #include +#include #include using namespace kmipclient; namespace { - void print_hex(const std::vector &bytes) { + void print_hex(std::span bytes) { for (const auto b : bytes) { std::cout << std::hex << std::setw(2) << std::setfill('0') << static_cast(b); diff --git a/kmipclient/include/kmipclient/KeyBase.hpp b/kmipclient/include/kmipclient/KeyBase.hpp index 577ab06..a1a0376 100644 --- a/kmipclient/include/kmipclient/KeyBase.hpp +++ b/kmipclient/include/kmipclient/KeyBase.hpp @@ -36,7 +36,7 @@ namespace kmipclient { // ---- Raw bytes ---- - [[nodiscard]] const std::vector &value() const noexcept { + [[nodiscard]] const kmipcore::secure_bytes &value() const noexcept { return key_value_; } @@ -103,11 +103,10 @@ namespace kmipclient { * @param value Key material bytes. * @param attrs Type-safe attribute bag (algorithm, length, mask, …). */ - Key(const std::vector &value, - kmipcore::Attributes attrs = {}); + Key(std::span value, kmipcore::Attributes attrs = {}); private: - std::vector key_value_; + kmipcore::secure_bytes key_value_; kmipcore::Attributes attributes_; }; diff --git a/kmipclient/include/kmipclient/kmipclient_version.hpp b/kmipclient/include/kmipclient/kmipclient_version.hpp index 88be33b..2f17ef3 100644 --- a/kmipclient/include/kmipclient/kmipclient_version.hpp +++ b/kmipclient/include/kmipclient/kmipclient_version.hpp @@ -13,9 +13,9 @@ /** @brief kmipclient semantic version major component. */ #define KMIPCLIENT_VERSION_MAJOR 0 /** @brief kmipclient semantic version minor component. */ -#define KMIPCLIENT_VERSION_MINOR 2 +#define KMIPCLIENT_VERSION_MINOR 3 /** @brief kmipclient semantic version patch component. */ -#define KMIPCLIENT_VERSION_PATCH 1 +#define KMIPCLIENT_VERSION_PATCH 0 /** @brief Internal helper for macro-stringification. */ #define KMIPCLIENT_STRINGIFY_I(x) #x diff --git a/kmipclient/src/IOUtils.cpp b/kmipclient/src/IOUtils.cpp index 948dbe3..bca3574 100644 --- a/kmipclient/src/IOUtils.cpp +++ b/kmipclient/src/IOUtils.cpp @@ -52,7 +52,7 @@ namespace kmipclient { } } - void IOUtils::send(const std::vector &request_bytes) const { + void IOUtils::send(std::span request_bytes) const { const int dlen = static_cast(request_bytes.size()); if (dlen <= 0) { throw KmipIOException( @@ -63,8 +63,7 @@ namespace kmipclient { int total_sent = 0; while (total_sent < dlen) { const int sent = net_client.send( - std::span(request_bytes) - .subspan(static_cast(total_sent)) + request_bytes.subspan(static_cast(total_sent)) ); if (sent <= 0) { std::ostringstream oss; @@ -92,7 +91,7 @@ namespace kmipclient { } } - std::vector IOUtils::receive_message(size_t max_message_size) { + kmipcore::secure_bytes IOUtils::receive_message(size_t max_message_size) { std::array msg_len_buf{}; read_exact(msg_len_buf); @@ -107,7 +106,7 @@ namespace kmipclient { throw KmipIOException(kmipcore::KMIP_EXCEED_MAX_MESSAGE_SIZE, oss.str()); } - std::vector response( + kmipcore::secure_bytes response( KMIP_MSG_LENGTH_BYTES + static_cast(length) ); memcpy(response.data(), msg_len_buf.data(), KMIP_MSG_LENGTH_BYTES); @@ -123,8 +122,8 @@ namespace kmipclient { } void IOUtils::do_exchange( - const std::vector &request_bytes, - std::vector &response_bytes, + std::span request_bytes, + kmipcore::secure_bytes &response_bytes, size_t max_message_size ) { try { diff --git a/kmipclient/src/IOUtils.hpp b/kmipclient/src/IOUtils.hpp index 0585abf..f8ce866 100644 --- a/kmipclient/src/IOUtils.hpp +++ b/kmipclient/src/IOUtils.hpp @@ -11,6 +11,7 @@ #include "kmipclient/NetClient.hpp" #include "kmipclient/types.hpp" #include "kmipcore/kmip_logger.hpp" +#include "kmipcore/secure_memory.hpp" #include #include @@ -27,15 +28,15 @@ namespace kmipclient { : net_client(nc), logger_(logger) {}; void do_exchange( - const std::vector &request_bytes, - std::vector &response_bytes, + std::span request_bytes, + kmipcore::secure_bytes &response_bytes, size_t max_message_size ); private: void log_debug(const char *event, std::span ttlv) const; - void send(const std::vector &request_bytes) const; - std::vector receive_message(size_t max_message_size); + void send(std::span request_bytes) const; + kmipcore::secure_bytes receive_message(size_t max_message_size); /** * Reads exactly n bytes from the network into the buffer. diff --git a/kmipclient/src/Key.cpp b/kmipclient/src/Key.cpp index c84bc31..61f3584 100644 --- a/kmipclient/src/Key.cpp +++ b/kmipclient/src/Key.cpp @@ -11,8 +11,8 @@ namespace kmipclient { - Key::Key(const std::vector &value, kmipcore::Attributes attrs) - : key_value_(value), attributes_(std::move(attrs)) {} + Key::Key(std::span value, kmipcore::Attributes attrs) + : key_value_(value.begin(), value.end()), attributes_(std::move(attrs)) {} kmipcore::Key Key::to_core_key() const { return kmipcore::Key(key_value_, type(), attributes_); diff --git a/kmipclient/src/KmipClient.cpp b/kmipclient/src/KmipClient.cpp index ecf8bf4..c01746b 100644 --- a/kmipclient/src/KmipClient.cpp +++ b/kmipclient/src/KmipClient.cpp @@ -156,7 +156,7 @@ namespace kmipclient { ) ); - std::vector response_bytes; + kmipcore::secure_bytes response_bytes; io->do_exchange( request.serialize(), response_bytes, request.getMaxResponseSize() ); @@ -183,7 +183,7 @@ namespace kmipclient { ) ); - std::vector response_bytes; + kmipcore::secure_bytes response_bytes; io->do_exchange( request.serialize(), response_bytes, request.getMaxResponseSize() ); @@ -214,7 +214,7 @@ namespace kmipclient { ) ); - std::vector response_bytes; + kmipcore::secure_bytes response_bytes; io->do_exchange( request.serialize(), response_bytes, request.getMaxResponseSize() ); @@ -243,7 +243,7 @@ namespace kmipclient { ) ); - std::vector response_bytes; + kmipcore::secure_bytes response_bytes; io->do_exchange( request.serialize(), response_bytes, request.getMaxResponseSize() ); @@ -300,7 +300,7 @@ namespace kmipclient { id, {}, request.getHeader().getProtocolVersion(), true ) ); - std::vector response_bytes; + kmipcore::secure_bytes response_bytes; io->do_exchange( request.serialize(), response_bytes, request.getMaxResponseSize() ); @@ -343,7 +343,7 @@ namespace kmipclient { ) ); - std::vector response_bytes; + kmipcore::secure_bytes response_bytes; io->do_exchange( request.serialize(), response_bytes, request.getMaxResponseSize() ); @@ -408,7 +408,7 @@ namespace kmipclient { id, {}, request.getHeader().getProtocolVersion(), true ) ); - std::vector response_bytes; + kmipcore::secure_bytes response_bytes; io->do_exchange( request.serialize(), response_bytes, request.getMaxResponseSize() ); @@ -448,7 +448,7 @@ namespace kmipclient { const auto batch_item_id = request.add_batch_item(kmipcore::ActivateRequest(id)); - std::vector response_bytes; + kmipcore::secure_bytes response_bytes; io->do_exchange( request.serialize(), response_bytes, request.getMaxResponseSize() ); @@ -467,7 +467,7 @@ namespace kmipclient { const auto batch_item_id = request.add_batch_item(kmipcore::GetAttributeListRequest(id)); - std::vector response_bytes; + kmipcore::secure_bytes response_bytes; io->do_exchange( request.serialize(), response_bytes, request.getMaxResponseSize() ); @@ -495,7 +495,7 @@ namespace kmipclient { ) ); - std::vector response_bytes; + kmipcore::secure_bytes response_bytes; io->do_exchange( request.serialize(), response_bytes, request.getMaxResponseSize() ); @@ -546,7 +546,7 @@ namespace kmipclient { ) ); - std::vector response_bytes; + kmipcore::secure_bytes response_bytes; io->do_exchange( request.serialize(), response_bytes, request.getMaxResponseSize() ); @@ -649,7 +649,7 @@ namespace kmipclient { ) ); - std::vector response_bytes; + kmipcore::secure_bytes response_bytes; io->do_exchange( request.serialize(), response_bytes, request.getMaxResponseSize() ); @@ -761,7 +761,7 @@ namespace kmipclient { ); const auto batch_item_id = request.add_batch_item(std::move(item)); - std::vector response_bytes; + kmipcore::secure_bytes response_bytes; io->do_exchange( request.serialize(), response_bytes, request.getMaxResponseSize() ); @@ -809,7 +809,7 @@ namespace kmipclient { item.setRequestPayload(payload); const auto batch_item_id = request.add_batch_item(std::move(item)); - std::vector response_bytes; + kmipcore::secure_bytes response_bytes; io->do_exchange( request.serialize(), response_bytes, request.getMaxResponseSize() ); @@ -856,7 +856,7 @@ namespace kmipclient { kmipcore::RevokeRequest(id, reason, message, occurrence_time) ); - std::vector response_bytes; + kmipcore::secure_bytes response_bytes; io->do_exchange( request.serialize(), response_bytes, request.getMaxResponseSize() ); @@ -874,7 +874,7 @@ namespace kmipclient { const auto batch_item_id = request.add_batch_item(kmipcore::DestroyRequest(id)); - std::vector response_bytes; + kmipcore::secure_bytes response_bytes; io->do_exchange( request.serialize(), response_bytes, request.getMaxResponseSize() ); diff --git a/kmipclient/tests/IOUtilsTest.cpp b/kmipclient/tests/IOUtilsTest.cpp index aa09587..18d53a5 100644 --- a/kmipclient/tests/IOUtilsTest.cpp +++ b/kmipclient/tests/IOUtilsTest.cpp @@ -104,7 +104,7 @@ namespace { return out; } - std::vector + kmipcore::secure_bytes serialize_element(const std::shared_ptr &element) { kmipcore::SerializationBuffer buf; element->serialize(buf); @@ -136,7 +136,7 @@ TEST(IOUtilsTest, SendRetriesOnShortWritesUntilComplete) { kmipclient::IOUtils io(nc); const std::vector request{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; - std::vector response; + kmipcore::secure_bytes response; ASSERT_NO_THROW(io.do_exchange(request, response, 1024)); EXPECT_EQ(nc.sent_bytes, request); @@ -151,7 +151,7 @@ TEST(IOUtilsTest, SendFailsIfTransportStopsProgress) { kmipclient::IOUtils io(nc); const std::vector request{0, 1, 2, 3}; - std::vector response; + kmipcore::secure_bytes response; EXPECT_THROW( io.do_exchange(request, response, 1024), kmipclient::KmipIOException @@ -166,7 +166,7 @@ TEST(IOUtilsTest, AcceptsResponsesLargerThanLegacy64KiBLimit) { kmipclient::IOUtils io(nc); const std::vector request{0x01}; - std::vector response; + kmipcore::secure_bytes response; ASSERT_NO_THROW( io.do_exchange(request, response, kmipcore::KMIP_MAX_MESSAGE_SIZE) @@ -181,7 +181,7 @@ TEST(IOUtilsTest, RejectsResponseThatExceedsCallerLimit) { kmipclient::IOUtils io(nc); const std::vector request{0x01}; - std::vector response; + kmipcore::secure_bytes response; try { io.do_exchange(request, response, 1024); @@ -202,7 +202,7 @@ TEST( kmipclient::IOUtils io(nc); const std::vector request{0x01}; - std::vector response; + kmipcore::secure_bytes response; try { io.do_exchange( @@ -257,11 +257,14 @@ TEST(IOUtilsTest, DebugLoggingRedactsSensitiveTtlvFields) { ) ); response->asStructure()->add(secret_data); - nc.response_bytes = serialize_element(response); + { + const auto sb = serialize_element(response); + nc.response_bytes.assign(sb.begin(), sb.end()); + } auto logger = std::make_shared(); kmipclient::IOUtils io(nc, logger); - std::vector response_bytes; + kmipcore::secure_bytes response_bytes; ASSERT_NO_THROW(io.do_exchange(request_bytes, response_bytes, 1024)); ASSERT_EQ(logger->records.size(), 2u); diff --git a/kmipcore/CMakeLists.txt b/kmipcore/CMakeLists.txt index 3747aff..0abc5e2 100644 --- a/kmipcore/CMakeLists.txt +++ b/kmipcore/CMakeLists.txt @@ -36,3 +36,7 @@ add_test(NAME kmip_parser_test COMMAND kmip_parser_test) add_executable(kmip_serialization_buffer_test tests/test_serialization_buffer.cpp) target_link_libraries(kmip_serialization_buffer_test PRIVATE kmipcore) add_test(NAME kmip_serialization_buffer_test COMMAND kmip_serialization_buffer_test) + +add_executable(kmip_secure_memory_test tests/test_secure_memory.cpp) +target_link_libraries(kmip_secure_memory_test PRIVATE kmipcore) +add_test(NAME kmip_secure_memory_test COMMAND kmip_secure_memory_test) diff --git a/kmipcore/include/kmipcore/key.hpp b/kmipcore/include/kmipcore/key.hpp index 285488b..f3c292c 100644 --- a/kmipcore/include/kmipcore/key.hpp +++ b/kmipcore/include/kmipcore/key.hpp @@ -40,7 +40,7 @@ namespace kmipcore { * @param attrs Type-safe attribute bag. */ explicit Key( - const std::vector &value, + std::span value, KeyType k_type, Attributes attrs = {} ) diff --git a/kmipcore/include/kmipcore/kmip_basics.hpp b/kmipcore/include/kmipcore/kmip_basics.hpp index 803ade9..60484d8 100644 --- a/kmipcore/include/kmipcore/kmip_basics.hpp +++ b/kmipcore/include/kmipcore/kmip_basics.hpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -146,8 +147,7 @@ namespace kmipcore { /** @brief Creates a Boolean element. */ static std::shared_ptr createBoolean(Tag t, bool v); /** @brief Creates a Text String element. */ - static std::shared_ptr - createTextString(Tag t, const std::string &v); + static std::shared_ptr createTextString(Tag t, std::string_view v); /** @brief Creates a Byte String element. */ static std::shared_ptr createByteString(Tag t, const std::vector &v); diff --git a/kmipcore/include/kmipcore/kmip_protocol.hpp b/kmipcore/include/kmipcore/kmip_protocol.hpp index 2c41011..4013409 100644 --- a/kmipcore/include/kmipcore/kmip_protocol.hpp +++ b/kmipcore/include/kmipcore/kmip_protocol.hpp @@ -10,9 +10,11 @@ #include "kmipcore/kmip_basics.hpp" #include "kmipcore/kmip_enums.hpp" +#include "kmipcore/secure_memory.hpp" #include #include +#include #include #include namespace kmipcore { @@ -119,13 +121,19 @@ namespace kmipcore { void setUserName(const std::optional &userName) { userName_ = userName; } - /** @brief Returns optional authentication password. */ - [[nodiscard]] const std::optional &getPassword() const { + /** + * @brief Returns optional authentication password (scrubbed storage). + */ + [[nodiscard]] const std::optional &getPassword() const { return password_; } /** @brief Sets optional authentication password. */ - void setPassword(const std::optional &password) { - password_ = password; + void setPassword(std::optional password) { + if (password) { + password_.emplace(password->begin(), password->end()); + } else { + password_.reset(); + } } /** @brief Encodes header to TTLV element form. */ [[nodiscard]] std::shared_ptr toElement() const; @@ -139,7 +147,7 @@ namespace kmipcore { std::optional timeStamp_; std::optional batchOrderOption_; std::optional userName_; - std::optional password_; + std::optional password_; }; /** @brief One KMIP operation entry within a request batch. */ @@ -321,7 +329,7 @@ namespace kmipcore { [[nodiscard]] size_t getMaxResponseSize() const; /** @brief Serializes complete message to TTLV bytes. */ - [[nodiscard]] std::vector serialize() const; + [[nodiscard]] secure_bytes serialize() const; /** @brief Encodes request message to TTLV element tree. */ [[nodiscard]] std::shared_ptr toElement() const; diff --git a/kmipcore/include/kmipcore/kmip_requests.hpp b/kmipcore/include/kmipcore/kmip_requests.hpp index 009eb52..1cd2168 100644 --- a/kmipcore/include/kmipcore/kmip_requests.hpp +++ b/kmipcore/include/kmipcore/kmip_requests.hpp @@ -117,7 +117,7 @@ namespace kmipcore { RegisterSymmetricKeyRequest( const std::string &name, const std::string &group, - const std::vector &key_value, + std::span key_value, ProtocolVersion version = {} ); }; @@ -153,7 +153,7 @@ namespace kmipcore { RegisterSecretRequest( const std::string &name, const std::string &group, - const std::vector &secret, + std::span secret, secret_data_type secret_type, ProtocolVersion version = {} ); diff --git a/kmipcore/include/kmipcore/kmipcore_version.hpp b/kmipcore/include/kmipcore/kmipcore_version.hpp index acbdb32..c2d5375 100644 --- a/kmipcore/include/kmipcore/kmipcore_version.hpp +++ b/kmipcore/include/kmipcore/kmipcore_version.hpp @@ -11,9 +11,9 @@ /** @brief kmipcore semantic version major component. */ #define KMIPCORE_VERSION_MAJOR 0 /** @brief kmipcore semantic version minor component. */ -#define KMIPCORE_VERSION_MINOR 1 +#define KMIPCORE_VERSION_MINOR 2 /** @brief kmipcore semantic version patch component. */ -#define KMIPCORE_VERSION_PATCH 2 +#define KMIPCORE_VERSION_PATCH 0 /** @brief Internal helper for macro stringification. */ #define KMIPCORE_STRINGIFY_I(x) #x diff --git a/kmipcore/include/kmipcore/managed_object.hpp b/kmipcore/include/kmipcore/managed_object.hpp index 805be37..634cbf1 100644 --- a/kmipcore/include/kmipcore/managed_object.hpp +++ b/kmipcore/include/kmipcore/managed_object.hpp @@ -9,7 +9,9 @@ #define KMIPCORE_MANAGED_OBJECT_HPP #include "kmipcore/kmip_attributes.hpp" +#include "kmipcore/secure_memory.hpp" +#include #include namespace kmipcore { @@ -32,9 +34,9 @@ namespace kmipcore { * @param attrs Type-safe attribute bag. */ explicit ManagedObject( - const std::vector &value, Attributes attrs = {} + std::span value, Attributes attrs = {} ) - : value_(value), attributes_(std::move(attrs)) {} + : value_(value.begin(), value.end()), attributes_(std::move(attrs)) {} /** @brief Virtual destructor for subclass-safe cleanup. */ virtual ~ManagedObject() = default; @@ -46,14 +48,12 @@ namespace kmipcore { // ---- Raw bytes ---- - /** @brief Returns raw object payload bytes. */ - [[nodiscard]] const std::vector &value() const noexcept { - return value_; - } + /** @brief Returns raw object payload bytes (stored in scrubbed memory). */ + [[nodiscard]] const secure_bytes &value() const noexcept { return value_; } /** @brief Replaces raw object payload bytes. */ - void set_value(const std::vector &val) noexcept { - value_ = val; + void set_value(std::span val) { + value_.assign(val.begin(), val.end()); } // ---- Attribute bag ---- @@ -85,7 +85,7 @@ namespace kmipcore { } protected: - std::vector value_; + secure_bytes value_; Attributes attributes_; }; diff --git a/kmipcore/include/kmipcore/response_parser.hpp b/kmipcore/include/kmipcore/response_parser.hpp index 5f28b31..156ab16 100644 --- a/kmipcore/include/kmipcore/response_parser.hpp +++ b/kmipcore/include/kmipcore/response_parser.hpp @@ -141,7 +141,7 @@ namespace kmipcore { static const char *operationToString(int32_t operation); static const char *resultStatusToString(int32_t status); - std::vector responseBytes_; + secure_bytes responseBytes_; ResponseMessage responseMessage_{}; bool isParsed_ = false; /** Maps uniqueBatchItemId → operation code extracted from the request. */ diff --git a/kmipcore/include/kmipcore/secret.hpp b/kmipcore/include/kmipcore/secret.hpp index ade4c74..d8da21f 100644 --- a/kmipcore/include/kmipcore/secret.hpp +++ b/kmipcore/include/kmipcore/secret.hpp @@ -11,6 +11,7 @@ #include "kmipcore/kmip_enums.hpp" #include "kmipcore/managed_object.hpp" +#include #include #include #include @@ -37,7 +38,7 @@ namespace kmipcore { * @param attrs Attribute bag (may include state, name, …). */ Secret( - const std::vector &val, + std::span val, secret_data_type type, Attributes attrs = {} ) @@ -89,8 +90,12 @@ namespace kmipcore { }; } - /** @brief Returns payload interpreted as UTF-8/byte-preserving text. */ - [[nodiscard]] std::string as_text() const { + /** + * @brief Returns payload interpreted as UTF-8/byte-preserving text. + * @note Returned in scrubbed memory (@ref secure_string); the payload is + * secret material. + */ + [[nodiscard]] secure_string as_text() const { return {value_.begin(), value_.end()}; } diff --git a/kmipcore/include/kmipcore/secure_memory.hpp b/kmipcore/include/kmipcore/secure_memory.hpp new file mode 100644 index 0000000..43400cb --- /dev/null +++ b/kmipcore/include/kmipcore/secure_memory.hpp @@ -0,0 +1,131 @@ +/* Copyright (c) 2026 Percona LLC and/or its affiliates. All rights reserved. + * + * This file is dual licensed under the terms of the Apache 2.0 License and + * the BSD 3-Clause License. See the LICENSE file in the root of this + * repository for more information. + */ + +#ifndef KMIPCORE_SECURE_MEMORY_HPP +#define KMIPCORE_SECURE_MEMORY_HPP + +#include +#include +#include +#include +#include + +namespace kmipcore { + + /** + * @brief Overwrite @p n bytes at @p p with zeros in a way the compiler is + * not permitted to optimize away. + * + * Plain std::memset on a buffer that is about to be freed is a textbook + * dead-store elimination target: the optimizer sees the memory is never read + * again and removes the write, leaving secrets in place. secure_clear routes + * through a platform "explicit" zeroing primitive when available and falls + * back to a volatile write loop otherwise, so the store always happens. + * + * kmipcore intentionally does not depend on OpenSSL, so this does not use + * OPENSSL_cleanse. + */ + void secure_clear(void *p, std::size_t n) noexcept; + + /** + * @brief Allocator that scrubs memory before handing it back to the runtime. + * + * Drop-in for the default std::allocator except that deallocate() runs + * @ref secure_clear over the whole block first. Compose it with the standard + * containers to get storage whose contents never survive the container: + * reallocation on growth, moves, and destruction all pass through + * deallocate() and therefore zero the freed bytes. + */ + template struct secure_allocator { + using value_type = T; + + secure_allocator() noexcept = default; + + template + secure_allocator(const secure_allocator & /*other*/) noexcept {} + + template struct rebind { + using other = secure_allocator; + }; + + [[nodiscard]] T *allocate(std::size_t n) { + if (n > (static_cast(-1) / sizeof(T))) { + throw std::bad_alloc(); + } + return static_cast(::operator new(n * sizeof(T))); + } + + void deallocate(T *p, std::size_t n) noexcept { + if (p != nullptr) { + secure_clear(p, n * sizeof(T)); + ::operator delete(p); + } + } + }; + + template + bool operator==( + const secure_allocator & /*a*/, const secure_allocator & /*b*/ + ) noexcept { + return true; + } + + template + bool operator!=( + const secure_allocator & /*a*/, const secure_allocator & /*b*/ + ) noexcept { + return false; + } + + /** @brief Byte buffer whose storage is zeroed when freed. */ + using secure_bytes = + std::vector>; + + /** @brief String whose storage is zeroed when freed. */ + using secure_string = + std::basic_string, secure_allocator>; + + // ---- Heterogeneous comparisons ---- + // + // secure_bytes / secure_string differ from the default-allocator containers + // only in their allocator type, but the standard comparison operators are not + // heterogeneous across allocators. These let callers and tests compare secure + // and plain containers element-wise without copying secrets into a plain one. + + inline bool + operator==(const secure_bytes &a, const std::vector &b) { + return std::equal(a.begin(), a.end(), b.begin(), b.end()); + } + inline bool + operator==(const std::vector &a, const secure_bytes &b) { + return b == a; + } + inline bool + operator!=(const secure_bytes &a, const std::vector &b) { + return !(a == b); + } + inline bool + operator!=(const std::vector &a, const secure_bytes &b) { + return !(b == a); + } + + inline bool operator==(const secure_string &a, const std::string &b) { + return std::equal(a.begin(), a.end(), b.begin(), b.end()); + } + inline bool operator==(const std::string &a, const secure_string &b) { + return b == a; + } + inline bool operator!=(const secure_string &a, const std::string &b) { + return !(a == b); + } + inline bool operator!=(const std::string &a, const secure_string &b) { + return !(b == a); + } + +} // namespace kmipcore + +#endif // KMIPCORE_SECURE_MEMORY_HPP diff --git a/kmipcore/include/kmipcore/serialization_buffer.hpp b/kmipcore/include/kmipcore/serialization_buffer.hpp index c4f77eb..d008623 100644 --- a/kmipcore/include/kmipcore/serialization_buffer.hpp +++ b/kmipcore/include/kmipcore/serialization_buffer.hpp @@ -8,6 +8,8 @@ #ifndef KMIPCORE_SERIALIZATION_BUFFER_HPP #define KMIPCORE_SERIALIZATION_BUFFER_HPP +#include "kmipcore/secure_memory.hpp" + #include #include #include @@ -155,9 +157,7 @@ namespace kmipcore { * Get const reference to underlying vector. * @return Reference to internal vector */ - [[nodiscard]] const std::vector &getBuffer() const { - return buffer_; - } + [[nodiscard]] const secure_bytes &getBuffer() const { return buffer_; } // ==================== TRANSFER OWNERSHIP ==================== @@ -175,7 +175,7 @@ namespace kmipcore { * * @return Vector containing serialized data */ - std::vector release(); + secure_bytes release(); /** * Release all heap memory, including reserved capacity. @@ -188,7 +188,7 @@ namespace kmipcore { void shrink(); private: - std::vector buffer_; + secure_bytes buffer_; size_t current_offset_ = 0; /** diff --git a/kmipcore/src/key_parser.cpp b/kmipcore/src/key_parser.cpp index ed18bce..455d61d 100644 --- a/kmipcore/src/key_parser.cpp +++ b/kmipcore/src/key_parser.cpp @@ -47,7 +47,7 @@ namespace kmipcore { } auto raw_bytes = key_material->toBytes(); - std::vector kv(raw_bytes.begin(), raw_bytes.end()); + secure_bytes kv(raw_bytes.begin(), raw_bytes.end()); // Parse attributes from the key value's Attribute children. Attributes key_attrs = AttributesParser::parse( @@ -120,9 +120,7 @@ namespace kmipcore { auto raw_bytes = key_material->toBytes(); Secret secret; - secret.set_value( - std::vector(raw_bytes.begin(), raw_bytes.end()) - ); + secret.set_value(secure_bytes(raw_bytes.begin(), raw_bytes.end())); secret.set_secret_type(checked_secret_data_type(secret_type->toEnum())); return secret; } diff --git a/kmipcore/src/kmip_basics.cpp b/kmipcore/src/kmip_basics.cpp index 5f7095e..d4d5eb5 100644 --- a/kmipcore/src/kmip_basics.cpp +++ b/kmipcore/src/kmip_basics.cpp @@ -372,9 +372,9 @@ namespace kmipcore { ); } std::shared_ptr - Element::createTextString(Tag t, const std::string &v) { + Element::createTextString(Tag t, std::string_view v) { return std::make_shared( - t, static_cast(KMIP_TYPE_TEXT_STRING), TextString{v} + t, static_cast(KMIP_TYPE_TEXT_STRING), TextString{std::string{v}} ); } std::shared_ptr diff --git a/kmipcore/src/kmip_protocol.cpp b/kmipcore/src/kmip_protocol.cpp index 31709d7..d359e53 100644 --- a/kmipcore/src/kmip_protocol.cpp +++ b/kmipcore/src/kmip_protocol.cpp @@ -200,7 +200,9 @@ namespace kmipcore { auto password = credentialValue->getChild(tag::KMIP_TAG_PASSWORD); if (password) { - rh.password_ = password->toString(); + auto pw = password->toString(); + rh.password_.emplace(pw.begin(), pw.end()); + secure_clear(pw.data(), pw.size()); } } } @@ -304,7 +306,7 @@ namespace kmipcore { : DEFAULT_MAX_RESPONSE_SIZE; } - std::vector RequestMessage::serialize() const { + secure_bytes RequestMessage::serialize() const { if (batchItems_.empty()) { throw KmipException( "Cannot serialize RequestMessage with no batch items" diff --git a/kmipcore/src/kmip_requests.cpp b/kmipcore/src/kmip_requests.cpp index b60c380..8d571c1 100644 --- a/kmipcore/src/kmip_requests.cpp +++ b/kmipcore/src/kmip_requests.cpp @@ -156,7 +156,7 @@ namespace kmipcore { return template_attribute; } std::shared_ptr - make_key_value(const std::vector &bytes) { + make_key_value(std::span bytes) { auto key_value = Element::createStructure(tag::KMIP_TAG_KEY_VALUE); key_value->asStructure()->add( Element::createByteString( @@ -168,7 +168,7 @@ namespace kmipcore { } std::shared_ptr make_key_block( int32_t key_format_type, - const std::vector &bytes, + std::span bytes, std::optional algorithm, std::optional cryptographic_length ) { @@ -258,7 +258,7 @@ namespace kmipcore { } } std::shared_ptr - make_symmetric_key(const std::vector &key_value) { + make_symmetric_key(std::span key_value) { auto symmetric_key = Element::createStructure(tag::KMIP_TAG_SYMMETRIC_KEY); symmetric_key->asStructure()->add(make_key_block( @@ -270,7 +270,7 @@ namespace kmipcore { return symmetric_key; } std::shared_ptr make_secret_data( - const std::vector &secret, secret_data_type secret_type + std::span secret, secret_data_type secret_type ) { auto secret_data = Element::createStructure(tag::KMIP_TAG_SECRET_DATA); secret_data->asStructure()->add( @@ -555,7 +555,7 @@ namespace kmipcore { RegisterSymmetricKeyRequest::RegisterSymmetricKeyRequest( const std::string &name, const std::string &group, - const std::vector &key_value, + std::span key_value, ProtocolVersion version ) { setOperation(KMIP_OP_REGISTER); @@ -699,7 +699,7 @@ namespace kmipcore { RegisterSecretRequest::RegisterSecretRequest( const std::string &name, const std::string &group, - const std::vector &secret, + std::span secret, secret_data_type secret_type, ProtocolVersion version ) { diff --git a/kmipcore/src/secure_memory.cpp b/kmipcore/src/secure_memory.cpp new file mode 100644 index 0000000..881cf7a --- /dev/null +++ b/kmipcore/src/secure_memory.cpp @@ -0,0 +1,44 @@ +/* Copyright (c) 2026 Percona LLC and/or its affiliates. All rights reserved. + * + * This file is dual licensed under the terms of the Apache 2.0 License and + * the BSD 3-Clause License. See the LICENSE file in the root of this + * repository for more information. + */ + +#include "kmipcore/secure_memory.hpp" + +#include + +#if defined(_WIN32) + #include +#endif + +namespace kmipcore { + + void secure_clear(void *p, std::size_t n) noexcept { + if (p == nullptr || n == 0) { + return; + } + +#if defined(_WIN32) + ::SecureZeroMemory(p, n); +#elif defined(__STDC_LIB_EXT1__) + // C11 Annex K bounds-checked memset that is not subject to dead-store + // elimination. + ::memset_s(p, n, 0, n); +#elif defined(__GLIBC__) && \ + (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 25)) + // glibc >= 2.25 and the BSDs provide explicit_bzero. + ::explicit_bzero(p, n); +#else + // Portable fallback: a volatile write loop the optimizer must not drop. + volatile unsigned char *vp = static_cast(p); + while (n-- != 0) { + *vp++ = 0; + } + // Compiler barrier so the writes are not reordered/coalesced away. + __asm__ __volatile__("" : : "r"(p) : "memory"); +#endif + } + +} // namespace kmipcore diff --git a/kmipcore/src/serialization_buffer.cpp b/kmipcore/src/serialization_buffer.cpp index 58c395b..1080bb3 100644 --- a/kmipcore/src/serialization_buffer.cpp +++ b/kmipcore/src/serialization_buffer.cpp @@ -110,18 +110,20 @@ namespace kmipcore { buffer_.reserve(new_capacity); } - std::vector SerializationBuffer::release() { - // Copy only the serialized bytes into the result. + secure_bytes SerializationBuffer::release() { + // Copy only the serialized bytes into the result (secure_bytes, so both the + // copy and this buffer scrub their storage when freed). // Use iterators instead of buffer_.data() + offset to avoid pointer // arithmetic on a potentially-null data() when size()==0 (UB even for +0). - std::vector result( + secure_bytes result( buffer_.begin(), buffer_.begin() + static_cast(current_offset_) ); - // Reset write position and logical size but KEEP the reserved capacity so - // the buffer can be reused immediately without a new heap allocation. - // Callers that need to reclaim memory explicitly can call shrink(). + // clear() keeps the reserved capacity for reuse, so it would otherwise + // leave the just-serialized secret bytes lingering in that capacity. + // Scrub the live bytes explicitly before clearing (F12). + secure_clear(buffer_.data(), buffer_.size()); buffer_.clear(); // size -> 0, capacity unchanged current_offset_ = 0; @@ -130,8 +132,9 @@ namespace kmipcore { void SerializationBuffer::shrink() { // Aggressively release all heap memory (capacity included). - // Use the swap-with-empty idiom because shrink_to_fit() is advisory. - std::vector().swap(buffer_); + // Use the swap-with-empty idiom because shrink_to_fit() is advisory; the + // freed storage is scrubbed by secure_allocator::deallocate. + secure_bytes().swap(buffer_); current_offset_ = 0; } diff --git a/kmipcore/tests/test_parsers.cpp b/kmipcore/tests/test_parsers.cpp index 2c2d0bb..b634c05 100644 --- a/kmipcore/tests/test_parsers.cpp +++ b/kmipcore/tests/test_parsers.cpp @@ -40,7 +40,7 @@ namespace { } // namespace -std::vector create_mock_response_bytes_with_result( +secure_bytes create_mock_response_bytes_with_result( int32_t operation, std::shared_ptr payload, int32_t result_status, @@ -49,7 +49,7 @@ std::vector create_mock_response_bytes_with_result( ); // Helper to create a basic success response message with one item -std::vector create_mock_response_bytes( +secure_bytes create_mock_response_bytes( int32_t operation, std::shared_ptr payload ) { return create_mock_response_bytes_with_result( @@ -61,7 +61,7 @@ std::vector create_mock_response_bytes( ); } -std::vector create_mock_response_bytes_with_result( +secure_bytes create_mock_response_bytes_with_result( int32_t operation, std::shared_ptr payload, int32_t result_status, @@ -115,7 +115,7 @@ void test_response_parser_failure_preserves_reason_code() { // Helper: build a response where the Operation tag is intentionally omitted // (simulates the pyKMIP behaviour of omitting Operation in failure responses). -std::vector create_mock_response_bytes_no_operation( +secure_bytes create_mock_response_bytes_no_operation( int32_t result_status, std::optional result_reason, const std::optional &result_message, diff --git a/kmipcore/tests/test_secure_memory.cpp b/kmipcore/tests/test_secure_memory.cpp new file mode 100644 index 0000000..e7de07d --- /dev/null +++ b/kmipcore/tests/test_secure_memory.cpp @@ -0,0 +1,136 @@ +/* Copyright (c) 2026 Percona LLC and/or its affiliates. All rights reserved. + * + * This file is dual licensed under the terms of the Apache 2.0 License and + * the BSD 3-Clause License. See the LICENSE file in the root of this + * repository for more information. + */ + +#include "kmipcore/secure_memory.hpp" +#include "kmipcore/serialization_buffer.hpp" + +#include +#include +#include +#include +#include +#include + +using namespace kmipcore; + +#define EXPECT(cond) \ + do { \ + if (!(cond)) { \ + throw std::runtime_error( \ + std::string(__FILE__) + ":" + std::to_string(__LINE__) + \ + ": expectation failed: " #cond \ + ); \ + } \ + } while (false) + +void testSecureClearZeroes() { + unsigned char buf[16]; + for (unsigned char &b : buf) { + b = 0xAB; + } + secure_clear(buf, sizeof(buf)); + for (unsigned char b : buf) { + EXPECT(b == 0); + } + + // No-ops must not crash. + secure_clear(nullptr, 16); + secure_clear(buf, 0); + + std::cout << "testSecureClearZeroes passed" << std::endl; +} + +void testHeterogeneousByteCompare() { + const std::vector plain{0x01, 0x02, 0x03}; + secure_bytes secure{0x01, 0x02, 0x03}; + + EXPECT(secure == plain); + EXPECT(plain == secure); + EXPECT(!(secure != plain)); + + secure.push_back(0x04); + EXPECT(secure != plain); + EXPECT(plain != secure); + + std::cout << "testHeterogeneousByteCompare passed" << std::endl; +} + +void testHeterogeneousStringCompare() { + const std::string plain = "s3cr3t"; + secure_string secure = "s3cr3t"; + + EXPECT(secure == plain); + EXPECT(plain == secure); + EXPECT(secure == "s3cr3t"); // basic_string vs const char* + EXPECT(secure != std::string("other")); + + std::cout << "testHeterogeneousStringCompare passed" << std::endl; +} + +// F12: release() must scrub the bytes left behind in the retained capacity. +void testReleaseScrubsRetainedCapacity() { + SerializationBuffer buf(64); + + const std::vector secret{0xDE, 0xAD, 0xBE, 0xEF, 0x11, 0x22}; + buf.writeBytes(std::as_bytes(std::span{secret})); + EXPECT(buf.size() == secret.size()); + + // Capture the storage pointer before release(); clear() keeps capacity, so + // the pointer stays valid and no reallocation happens. + const uint8_t *storage = buf.data(); + const size_t written = buf.size(); + + const secure_bytes released = buf.release(); + + // The returned copy still holds the serialized secret ... + EXPECT(released.size() == written); + EXPECT(released[0] == 0xDE); + + // ... but the buffer's retained capacity has been zeroed. + EXPECT(buf.size() == 0); + bool all_zero = true; + for (size_t i = 0; i < written; ++i) { + if (storage[i] != 0) { + all_zero = false; + break; + } + } + EXPECT(all_zero); + + std::cout << "testReleaseScrubsRetainedCapacity passed" << std::endl; +} + +void testShrinkReleasesCapacity() { + SerializationBuffer buf(64); + const std::vector secret{0x01, 0x02, 0x03, 0x04}; + buf.writeBytes(std::as_bytes(std::span{secret})); + EXPECT(buf.capacity() > 0); + + buf.shrink(); + EXPECT(buf.size() == 0); + EXPECT(buf.capacity() == 0); + + std::cout << "testShrinkReleasesCapacity passed" << std::endl; +} + +int main() { + std::cout << "Running secure_memory tests..." << std::endl; + + try { + testSecureClearZeroes(); + testHeterogeneousByteCompare(); + testHeterogeneousStringCompare(); + testReleaseScrubsRetainedCapacity(); + testShrinkReleasesCapacity(); + + std::cout << "All secure_memory tests passed" << std::endl; + return 0; + } catch (const std::exception &e) { + std::cerr << "Test failed: " << e.what() << std::endl; + return 1; + } +} diff --git a/kmipcore/tests/test_serialization_buffer.cpp b/kmipcore/tests/test_serialization_buffer.cpp index e0388c0..2bcc628 100644 --- a/kmipcore/tests/test_serialization_buffer.cpp +++ b/kmipcore/tests/test_serialization_buffer.cpp @@ -154,7 +154,7 @@ void testRelease() { EXPECT(buf.size() == 5); - std::vector result = buf.release(); + secure_bytes result = buf.release(); EXPECT(result.size() == 5); EXPECT(result[0] == 0x11);