Logger System 0.1.3
High-performance C++20 thread-safe logging system with asynchronous capabilities
Loading...
Searching...
No Matches
encrypted_writer.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
9
10#include <sstream>
11#include <iomanip>
12#include <fstream>
13#include <cstring>
14
15namespace kcenon::logger {
16
17// ============================================================================
18// encrypted_writer implementation
19// ============================================================================
20
22 std::unique_ptr<log_writer_interface> wrapped,
24) : decorator_writer_base(std::move(wrapped), "encrypted"),
25 config_(std::move(config)),
26 last_key_rotation_(std::chrono::system_clock::now())
27{
28 // Validate key size (AES-256 requires 32 bytes)
29 if (config_.key.size() != 32) {
30 throw std::invalid_argument(
31 "Invalid key size: expected 32 bytes, got " +
32 std::to_string(config_.key.size())
33 );
34 }
35
36#ifdef LOGGER_HAS_OPENSSL_CRYPTO
37 auto init_result = init_cipher_context();
38 if (init_result.is_err()) {
39 throw std::runtime_error(
40 "Failed to initialize cipher context: " +
41 get_logger_error_message(init_result)
42 );
43 }
44#endif
45
46 is_initialized_.store(true);
47
48 // Log initialization to audit log
51 "encrypted_writer initialized",
52 {{"algorithm", std::to_string(static_cast<int>(config_.algorithm))},
53 {"wrapped_writer", this->wrapped().get_name()}}
54 );
55}
56
57encrypted_writer::~encrypted_writer() {
58 is_initialized_.store(false);
59
60#ifdef LOGGER_HAS_OPENSSL_CRYPTO
61 cleanup_cipher_context();
62#endif
63
64 // Flush wrapped writer
65 wrapped().flush();
66}
67
68common::VoidResult encrypted_writer::write(const log_entry& entry) {
69 if (!is_initialized_.load()) {
71 logger_error_code::encryption_failed,
72 "encrypted_writer not initialized"
73 );
74 }
75
76 // Check for automatic key rotation
77 auto rotate_result = auto_rotate_key_if_needed();
78 if (rotate_result.is_err()) {
79 // Log rotation failure but continue with current key
80 security::audit_logger::log_audit_event(
81 security::audit_logger::audit_event::suspicious_activity,
82 "Key rotation failed",
83 {{"error", get_logger_error_message(rotate_result)}}
84 );
85 }
86
87 // Format the log entry first
88 std::ostringstream oss;
89 auto time_t_val = std::chrono::system_clock::to_time_t(entry.timestamp);
90 std::tm tm_val;
91#ifdef _WIN32
92 localtime_s(&tm_val, &time_t_val);
93#else
94 localtime_r(&time_t_val, &tm_val);
95#endif
96
97 // Convert log_level from logger_system to common::interfaces
98 auto level = static_cast<common::interfaces::log_level>(static_cast<int>(entry.level));
99
100 oss << std::put_time(&tm_val, "%Y-%m-%dT%H:%M:%S")
101 << " [" << static_cast<int>(level) << "] "
102 << entry.message.to_string();
103
104 if (entry.location) {
105 std::string file = entry.location->file.to_string();
106 std::string function = entry.location->function.to_string();
107 if (!file.empty()) {
108 oss << " (" << file << ":" << entry.location->line << " " << function << ")";
109 }
110 }
111
112 std::string plaintext = oss.str();
113
114 // Encrypt the formatted log entry
115 std::vector<uint8_t> encrypted_data;
116 {
117 std::lock_guard lock(write_mutex_);
118 auto encrypt_result = encrypt_data(plaintext, encrypted_data);
119 if (encrypt_result.is_err()) {
120 return encrypt_result;
121 }
122 }
123
124 // Create encrypted log entry with binary data in message field
125 std::string binary_data(
126 reinterpret_cast<const char*>(encrypted_data.data()),
127 encrypted_data.size()
128 );
129 log_entry encrypted_entry(level, binary_data, entry.timestamp);
130
131 // Delegate to wrapped writer
132 auto write_result = wrapped().write(encrypted_entry);
133 if (write_result.is_ok()) {
134 entries_encrypted_.fetch_add(1);
135 }
136 return write_result;
137}
138
139
140common::VoidResult encrypted_writer::rotate_key(security::secure_key new_key) {
141 // Validate new key
142 if (new_key.size() != 32) {
144 logger_error_code::invalid_key_size,
145 "New key must be 32 bytes for AES-256"
146 );
147 }
148
149 std::lock_guard lock(write_mutex_);
150
151 // Flush pending writes with old key
152 wrapped().flush();
153
154 // Swap keys (old key is securely cleared by secure_key destructor)
155 config_.key = std::move(new_key);
156 last_key_rotation_ = std::chrono::system_clock::now();
157
158 // Save new key if path is configured
159 if (!config_.key_rotation_path.empty()) {
160 auto save_result = security::secure_key_storage::save_key(
161 config_.key,
162 config_.key_rotation_path,
163 config_.key_storage_base
164 );
165 if (save_result.is_err()) {
166 return save_result;
167 }
168 }
169
170 // Log key rotation
171 security::audit_logger::log_audit_event(
172 security::audit_logger::audit_event::encryption_key_rotated,
173 "Encryption key rotated",
174 {{"entries_encrypted", std::to_string(entries_encrypted_.load())}}
175 );
176
177 return common::ok();
178}
179
180uint64_t encrypted_writer::get_entries_encrypted() const {
181 return entries_encrypted_.load();
182}
183
184std::chrono::system_clock::time_point encrypted_writer::get_last_key_rotation() const {
185 return last_key_rotation_;
186}
187
188result<std::string> encrypted_writer::decrypt_entry(
189 const std::vector<uint8_t>& encrypted_data,
190 const security::secure_key& key
191) {
192#ifdef LOGGER_HAS_OPENSSL_CRYPTO
193 // Validate minimum size (header + at least 1 byte of data)
194 if (encrypted_data.size() < sizeof(encrypted_log_header)) {
195 return result<std::string>(
196 logger_error_code::decryption_failed,
197 "Encrypted data too small"
198 );
199 }
200
201 // Parse header
203 std::memcpy(&header, encrypted_data.data(), sizeof(header));
204
205 // Validate magic number
206 if (header.magic != encrypted_log_header::kMagic) {
207 return result<std::string>(
208 logger_error_code::decryption_failed,
209 "Invalid magic number in encrypted data"
210 );
211 }
212
213 // Validate version
214 if (header.version != encrypted_log_header::kVersion) {
215 return result<std::string>(
216 logger_error_code::decryption_failed,
217 "Unsupported encrypted log version"
218 );
219 }
220
221 // Validate data length
222 size_t expected_size = sizeof(header) + header.encrypted_length;
223 if (encrypted_data.size() < expected_size) {
224 return result<std::string>(
225 logger_error_code::decryption_failed,
226 "Encrypted data truncated"
227 );
228 }
229
230 // Initialize decryption context
231 EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
232 if (!ctx) {
233 return result<std::string>(
234 logger_error_code::decryption_failed,
235 "Failed to create cipher context"
236 );
237 }
238
239 // Select cipher based on algorithm
240 const EVP_CIPHER* cipher = nullptr;
241 switch (static_cast<encryption_algorithm>(header.algorithm)) {
242 case encryption_algorithm::aes_256_gcm:
243 cipher = EVP_aes_256_gcm();
244 break;
245 case encryption_algorithm::aes_256_cbc:
246 cipher = EVP_aes_256_cbc();
247 break;
248 case encryption_algorithm::chacha20_poly1305:
249 cipher = EVP_chacha20_poly1305();
250 break;
251 default:
252 EVP_CIPHER_CTX_free(ctx);
253 return result<std::string>(
254 logger_error_code::decryption_failed,
255 "Unknown encryption algorithm"
256 );
257 }
258
259 // Initialize decryption
260 if (EVP_DecryptInit_ex(ctx, cipher, nullptr, nullptr, nullptr) != 1) {
261 EVP_CIPHER_CTX_free(ctx);
262 return result<std::string>(
263 logger_error_code::decryption_failed,
264 "Failed to initialize decryption"
265 );
266 }
267
268 // Set IV length for GCM
269 if (header.algorithm == static_cast<uint8_t>(encryption_algorithm::aes_256_gcm)) {
270 if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 16, nullptr) != 1) {
271 EVP_CIPHER_CTX_free(ctx);
272 return result<std::string>(
273 logger_error_code::decryption_failed,
274 "Failed to set IV length"
275 );
276 }
277 }
278
279 // Set key and IV
280 if (EVP_DecryptInit_ex(ctx, nullptr, nullptr,
281 key.data().data(), header.iv.data()) != 1) {
282 EVP_CIPHER_CTX_free(ctx);
283 return result<std::string>(
284 logger_error_code::decryption_failed,
285 "Failed to set key and IV"
286 );
287 }
288
289 // Set expected tag for GCM
290 if (header.algorithm == static_cast<uint8_t>(encryption_algorithm::aes_256_gcm)) {
291 if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16,
292 const_cast<uint8_t*>(header.tag.data())) != 1) {
293 EVP_CIPHER_CTX_free(ctx);
294 return result<std::string>(
295 logger_error_code::decryption_failed,
296 "Failed to set authentication tag"
297 );
298 }
299 }
300
301 // Decrypt data
302 std::vector<uint8_t> plaintext(header.original_length + 16);
303 int plaintext_len = 0;
304
305 const uint8_t* ciphertext = encrypted_data.data() + sizeof(header);
306 if (EVP_DecryptUpdate(ctx, plaintext.data(), &plaintext_len,
307 ciphertext, header.encrypted_length) != 1) {
308 EVP_CIPHER_CTX_free(ctx);
309 return result<std::string>(
310 logger_error_code::decryption_failed,
311 "Decryption failed"
312 );
313 }
314
315 int final_len = 0;
316 if (EVP_DecryptFinal_ex(ctx, plaintext.data() + plaintext_len, &final_len) != 1) {
317 EVP_CIPHER_CTX_free(ctx);
318 return result<std::string>(
319 logger_error_code::decryption_failed,
320 "Decryption finalization failed (authentication failure)"
321 );
322 }
323
324 EVP_CIPHER_CTX_free(ctx);
325
326 plaintext_len += final_len;
327 return result<std::string>(std::string(
328 reinterpret_cast<char*>(plaintext.data()),
329 plaintext_len
330 ));
331#else
332 return result<std::string>(
333 logger_error_code::decryption_failed,
334 "OpenSSL not available - decryption disabled"
335 );
336#endif
337}
338
339common::VoidResult encrypted_writer::encrypt_data(
340 const std::string& plaintext,
341 std::vector<uint8_t>& output
342) {
343#ifdef LOGGER_HAS_OPENSSL_CRYPTO
344 // Generate IV
345 uint8_t iv[16];
346 auto iv_result = generate_iv(iv);
347 if (iv_result.is_err()) {
348 return iv_result;
349 }
350
351 // Prepare header
353 header.algorithm = static_cast<uint8_t>(config_.algorithm);
354 header.original_length = static_cast<uint32_t>(plaintext.size());
355 std::memcpy(header.iv.data(), iv, encrypted_log_header::kIvSize);
356
357 // Initialize encryption
358 if (EVP_EncryptInit_ex(cipher_ctx_, nullptr, nullptr,
359 config_.key.data().data(), iv) != 1) {
361 logger_error_code::encryption_failed,
362 "Failed to initialize encryption"
363 );
364 }
365
366 // Allocate output buffer (header + ciphertext + potential padding)
367 size_t max_ciphertext_len = plaintext.size() + EVP_MAX_BLOCK_LENGTH;
368 output.resize(sizeof(header) + max_ciphertext_len);
369
370 // Encrypt
371 int ciphertext_len = 0;
372 if (EVP_EncryptUpdate(cipher_ctx_,
373 output.data() + sizeof(header),
374 &ciphertext_len,
375 reinterpret_cast<const uint8_t*>(plaintext.data()),
376 static_cast<int>(plaintext.size())) != 1) {
378 logger_error_code::encryption_failed,
379 "Encryption update failed"
380 );
381 }
382
383 int final_len = 0;
384 if (EVP_EncryptFinal_ex(cipher_ctx_,
385 output.data() + sizeof(header) + ciphertext_len,
386 &final_len) != 1) {
388 logger_error_code::encryption_failed,
389 "Encryption finalization failed"
390 );
391 }
392
393 ciphertext_len += final_len;
394 header.encrypted_length = static_cast<uint32_t>(ciphertext_len);
395
396 // Get authentication tag for GCM
397 if (config_.algorithm == encryption_algorithm::aes_256_gcm) {
398 if (EVP_CIPHER_CTX_ctrl(cipher_ctx_, EVP_CTRL_GCM_GET_TAG,
399 encrypted_log_header::kTagSize, header.tag.data()) != 1) {
401 logger_error_code::encryption_failed,
402 "Failed to get authentication tag"
403 );
404 }
405 }
406
407 // Copy header to output
408 std::memcpy(output.data(), &header, sizeof(header));
409
410 // Resize to actual size
411 output.resize(sizeof(header) + ciphertext_len);
412
413 return common::ok();
414#else
416 logger_error_code::encryption_failed,
417 "OpenSSL not available - encryption disabled"
418 );
419#endif
420}
421
422common::VoidResult encrypted_writer::generate_iv(uint8_t* iv) {
423#ifdef LOGGER_HAS_OPENSSL_CRYPTO
424 if (RAND_bytes(iv, 16) != 1) {
426 logger_error_code::encryption_failed,
427 "Failed to generate random IV"
428 );
429 }
430 return common::ok();
431#else
433 logger_error_code::encryption_failed,
434 "OpenSSL not available"
435 );
436#endif
437}
438
439bool encrypted_writer::should_rotate_key() const {
440 if (!config_.key_rotation_interval.has_value()) {
441 return false;
442 }
443
444 auto now = std::chrono::system_clock::now();
445 auto elapsed = std::chrono::duration_cast<std::chrono::hours>(
446 now - last_key_rotation_
447 );
448
449 return elapsed >= config_.key_rotation_interval.value();
450}
451
452common::VoidResult encrypted_writer::auto_rotate_key_if_needed() {
453 if (!should_rotate_key()) {
454 return common::ok();
455 }
456
457 // Generate new key
458 auto key_result = security::secure_key_storage::generate_key(32);
459 if (!key_result.has_value()) {
461 key_result.error_code(),
462 key_result.error_message()
463 );
464 }
465
466 return rotate_key(std::move(key_result.value()));
467}
468
469#ifdef LOGGER_HAS_OPENSSL_CRYPTO
470common::VoidResult encrypted_writer::init_cipher_context() {
471 cipher_ctx_ = EVP_CIPHER_CTX_new();
472 if (!cipher_ctx_) {
474 logger_error_code::encryption_failed,
475 "Failed to create cipher context"
476 );
477 }
478
479 // Select cipher based on algorithm
480 const EVP_CIPHER* cipher = nullptr;
481 switch (config_.algorithm) {
482 case encryption_algorithm::aes_256_gcm:
483 cipher = EVP_aes_256_gcm();
484 break;
485 case encryption_algorithm::aes_256_cbc:
486 cipher = EVP_aes_256_cbc();
487 break;
488 case encryption_algorithm::chacha20_poly1305:
489 cipher = EVP_chacha20_poly1305();
490 break;
491 default:
492 EVP_CIPHER_CTX_free(cipher_ctx_);
493 cipher_ctx_ = nullptr;
494 return make_logger_void_result(
495 logger_error_code::encryption_failed,
496 "Unknown encryption algorithm"
497 );
498 }
499
500 // Initialize cipher
501 if (EVP_EncryptInit_ex(cipher_ctx_, cipher, nullptr, nullptr, nullptr) != 1) {
502 EVP_CIPHER_CTX_free(cipher_ctx_);
503 cipher_ctx_ = nullptr;
504 return make_logger_void_result(
505 logger_error_code::encryption_failed,
506 "Failed to initialize cipher"
507 );
508 }
509
510 // Set IV length for GCM
511 if (config_.algorithm == encryption_algorithm::aes_256_gcm) {
512 if (EVP_CIPHER_CTX_ctrl(cipher_ctx_, EVP_CTRL_GCM_SET_IVLEN, 16, nullptr) != 1) {
513 EVP_CIPHER_CTX_free(cipher_ctx_);
514 cipher_ctx_ = nullptr;
516 logger_error_code::encryption_failed,
517 "Failed to set IV length"
518 );
519 }
520 }
521
522 return common::ok();
523}
524
525void encrypted_writer::cleanup_cipher_context() {
526 if (cipher_ctx_) {
527 EVP_CIPHER_CTX_free(cipher_ctx_);
528 cipher_ctx_ = nullptr;
529 }
530}
531#endif
532
533// ============================================================================
534// log_decryptor implementation
535// ============================================================================
536
537log_decryptor::log_decryptor(const security::secure_key& key)
538 : key_(key.data()) // Copy key data
539{
540#ifdef LOGGER_HAS_OPENSSL_CRYPTO
541 cipher_ctx_ = EVP_CIPHER_CTX_new();
542#endif
543}
544
546#ifdef LOGGER_HAS_OPENSSL_CRYPTO
547 if (cipher_ctx_) {
548 EVP_CIPHER_CTX_free(cipher_ctx_);
549 cipher_ctx_ = nullptr;
550 }
551#endif
552}
553
555 const std::filesystem::path& input_path,
556 const std::filesystem::path& output_path
557) {
558 std::ofstream output(output_path, std::ios::binary | std::ios::trunc);
559 if (!output) {
560 return result<size_t>(
562 "Failed to open output file: " + output_path.string()
563 );
564 }
565
566 size_t entry_count = 0;
567 auto callback = [&output, &entry_count](const std::string& decrypted) {
568 output << decrypted << "\n";
569 ++entry_count;
570 };
571
572 auto stream_result = decrypt_file_streaming(input_path, callback);
573 if (!stream_result.has_value()) {
574 return stream_result;
575 }
576
577 return result<size_t>(entry_count);
578}
579
581 const std::filesystem::path& input_path,
582 std::function<void(const std::string&)> callback
583) {
584#ifdef LOGGER_HAS_OPENSSL_CRYPTO
585 std::ifstream input(input_path, std::ios::binary);
586 if (!input) {
587 return result<size_t>(
589 "Failed to open input file: " + input_path.string()
590 );
591 }
592
593 size_t entry_count = 0;
594 std::vector<uint8_t> buffer;
595
596 while (input.good()) {
597 // Read header
599 input.read(reinterpret_cast<char*>(&header), sizeof(header));
600
601 if (input.gcount() == 0) {
602 break; // End of file
603 }
604
605 if (input.gcount() != sizeof(header)) {
606 return result<size_t>(
608 "Incomplete header at entry " + std::to_string(entry_count)
609 );
610 }
611
612 // Validate header
613 if (header.magic != encrypted_log_header::kMagic) {
614 return result<size_t>(
616 "Invalid magic number at entry " + std::to_string(entry_count)
617 );
618 }
619
620 // Read encrypted data
621 buffer.resize(sizeof(header) + header.encrypted_length);
622 std::memcpy(buffer.data(), &header, sizeof(header));
623 input.read(
624 reinterpret_cast<char*>(buffer.data() + sizeof(header)),
625 header.encrypted_length
626 );
627
628 if (static_cast<size_t>(input.gcount()) != header.encrypted_length) {
629 return result<size_t>(
631 "Incomplete data at entry " + std::to_string(entry_count)
632 );
633 }
634
635 // Decrypt entry
636 auto decrypt_result = encrypted_writer::decrypt_entry(buffer, key_);
637 if (!decrypt_result.has_value()) {
638 return result<size_t>(
639 decrypt_result.error_code(),
640 "Decryption failed at entry " + std::to_string(entry_count) +
641 ": " + decrypt_result.error_message()
642 );
643 }
644
645 callback(decrypt_result.value());
646 ++entry_count;
647 }
648
649 return result<size_t>(entry_count);
650#else
651 return result<size_t>(
653 "OpenSSL not available - decryption disabled"
654 );
655#endif
656}
657
658} // namespace kcenon::logger
Secure audit logging with tamper detection.
Abstract base class for decorator pattern log writers.
encrypted_writer(std::unique_ptr< log_writer_interface > wrapped, encryption_config config)
Construct encrypted writer with wrapped writer.
static result< std::string > decrypt_entry(const std::vector< uint8_t > &encrypted_data, const security::secure_key &key)
Decrypt a single encrypted log entry.
result< size_t > decrypt_file_streaming(const std::filesystem::path &input_path, std::function< void(const std::string &)> callback)
Decrypt file with callback for each entry.
~log_decryptor()
Destructor - cleans up key material.
result< size_t > decrypt_file(const std::filesystem::path &input_path, const std::filesystem::path &output_path)
Decrypt entire file to output file.
static void log_audit_event(audit_event event, const std::string &details, const std::map< std::string, std::string > &metadata={})
Log an audit event.
RAII wrapper for encryption keys with secure memory management.
size_t size() const
Get key size in bytes.
const std::vector< uint8_t > & data() const
Get const reference to key data.
std::string to_string() const
Convert to std::string.
Encryption wrapper for log writers providing AES-256-GCM encryption.
Data structures for representing log entries and source locations kcenon.
VoidResult ok()
common::VoidResult make_logger_void_result(logger_error_code code, const std::string &message="")
std::string get_logger_error_message(const common::VoidResult &result)
Get error message from a VoidResult.
encryption_algorithm
Supported encryption algorithms for log encryption.
Header prepended to each encrypted log entry.
uint8_t version
Header format version.
std::array< uint8_t, kTagSize > tag
GCM authentication tag.
uint32_t magic
Magic number for validation.
uint8_t algorithm
Encryption algorithm used.
std::array< uint8_t, kIvSize > iv
Initialization vector.
uint32_t encrypted_length
Length of ciphertext.
uint32_t original_length
Length of plaintext.
Configuration for encrypted_writer.
security::secure_key key
Encryption key (must be 32 bytes for AES-256)
Represents a single log entry with all associated metadata.
Definition log_entry.h:155
std::optional< source_location > location
Optional source code location information.
Definition log_entry.h:183
log_level level
Severity level of the log message.
Definition log_entry.h:162
small_string_256 message
The actual log message.
Definition log_entry.h:169
std::chrono::system_clock::time_point timestamp
Timestamp when the log entry was created.
Definition log_entry.h:175
Default human-readable formatter with timestamps kcenon.