PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
audit_log_cipher.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, kcenon
3// See the LICENSE file in the project root for full license information.
4
13
14#include <algorithm>
15#include <cstdlib>
16#include <cstring>
17
18#ifdef PACS_WITH_DIGITAL_SIGNATURES
19 #include <openssl/evp.h>
20 #include <openssl/hmac.h>
21 #include <openssl/rand.h>
22#endif
23
24namespace kcenon::pacs::security {
25
26// =============================================================================
27// Small Encoding Helpers
28// =============================================================================
29
30namespace detail {
31
32namespace {
33
34constexpr char kBase64Alphabet[] =
35 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
36
37// Decoded value 0-63, or -1 on invalid. '=' is handled separately.
38int base64_value(char c) noexcept {
39 if (c >= 'A' && c <= 'Z') return c - 'A';
40 if (c >= 'a' && c <= 'z') return c - 'a' + 26;
41 if (c >= '0' && c <= '9') return c - '0' + 52;
42 if (c == '+') return 62;
43 if (c == '/') return 63;
44 return -1;
45}
46
47int hex_value(char c) noexcept {
48 if (c >= '0' && c <= '9') return c - '0';
49 if (c >= 'a' && c <= 'f') return c - 'a' + 10;
50 if (c >= 'A' && c <= 'F') return c - 'A' + 10;
51 return -1;
52}
53
54} // namespace
55
56std::string base64_encode(const std::uint8_t* data, std::size_t size) {
57 std::string out;
58 out.reserve(((size + 2) / 3) * 4);
59
60 std::size_t i = 0;
61 for (; i + 3 <= size; i += 3) {
62 std::uint32_t v = (static_cast<std::uint32_t>(data[i]) << 16) |
63 (static_cast<std::uint32_t>(data[i + 1]) << 8) |
64 static_cast<std::uint32_t>(data[i + 2]);
65 out.push_back(kBase64Alphabet[(v >> 18) & 0x3F]);
66 out.push_back(kBase64Alphabet[(v >> 12) & 0x3F]);
67 out.push_back(kBase64Alphabet[(v >> 6) & 0x3F]);
68 out.push_back(kBase64Alphabet[v & 0x3F]);
69 }
70
71 const std::size_t rem = size - i;
72 if (rem == 1) {
73 std::uint32_t v = static_cast<std::uint32_t>(data[i]) << 16;
74 out.push_back(kBase64Alphabet[(v >> 18) & 0x3F]);
75 out.push_back(kBase64Alphabet[(v >> 12) & 0x3F]);
76 out.push_back('=');
77 out.push_back('=');
78 } else if (rem == 2) {
79 std::uint32_t v = (static_cast<std::uint32_t>(data[i]) << 16) |
80 (static_cast<std::uint32_t>(data[i + 1]) << 8);
81 out.push_back(kBase64Alphabet[(v >> 18) & 0x3F]);
82 out.push_back(kBase64Alphabet[(v >> 12) & 0x3F]);
83 out.push_back(kBase64Alphabet[(v >> 6) & 0x3F]);
84 out.push_back('=');
85 }
86 return out;
87}
88
89bool base64_decode(const std::string& in, std::vector<std::uint8_t>& out) {
90 if (in.size() % 4 != 0) return false;
91 out.clear();
92 out.reserve((in.size() / 4) * 3);
93
94 for (std::size_t i = 0; i < in.size(); i += 4) {
95 const char c0 = in[i];
96 const char c1 = in[i + 1];
97 const char c2 = in[i + 2];
98 const char c3 = in[i + 3];
99
100 const int v0 = base64_value(c0);
101 const int v1 = base64_value(c1);
102 if (v0 < 0 || v1 < 0) return false;
103
104 const std::uint32_t b0 = static_cast<std::uint32_t>(v0);
105 const std::uint32_t b1 = static_cast<std::uint32_t>(v1);
106
107 out.push_back(static_cast<std::uint8_t>((b0 << 2) | (b1 >> 4)));
108
109 if (c2 == '=') {
110 if (c3 != '=') return false;
111 if (i + 4 != in.size()) return false;
112 continue;
113 }
114 const int v2 = base64_value(c2);
115 if (v2 < 0) return false;
116 const std::uint32_t b2 = static_cast<std::uint32_t>(v2);
117
118 out.push_back(static_cast<std::uint8_t>(((b1 & 0xF) << 4) | (b2 >> 2)));
119
120 if (c3 == '=') {
121 if (i + 4 != in.size()) return false;
122 continue;
123 }
124 const int v3 = base64_value(c3);
125 if (v3 < 0) return false;
126 const std::uint32_t b3 = static_cast<std::uint32_t>(v3);
127 out.push_back(static_cast<std::uint8_t>(((b2 & 0x3) << 6) | b3));
128 }
129 return true;
130}
131
132bool hex_decode(const std::string& in,
133 std::uint8_t* out,
134 std::size_t out_size) {
135 if (in.size() != out_size * 2) return false;
136 for (std::size_t i = 0; i < out_size; ++i) {
137 const int hi = hex_value(in[2 * i]);
138 const int lo = hex_value(in[2 * i + 1]);
139 if (hi < 0 || lo < 0) return false;
140 out[i] = static_cast<std::uint8_t>((hi << 4) | lo);
141 }
142 return true;
143}
144
145bool constant_time_equal(const std::uint8_t* a,
146 const std::uint8_t* b,
147 std::size_t n) noexcept {
148 std::uint8_t diff = 0;
149 for (std::size_t i = 0; i < n; ++i) {
150 diff |= static_cast<std::uint8_t>(a[i] ^ b[i]);
151 }
152 return diff == 0;
153}
154
155} // namespace detail
156
157// =============================================================================
158// audit_cipher_key
159// =============================================================================
160
161void audit_cipher_key::wipe() noexcept {
162 // volatile write to discourage the compiler from optimising it out
163 volatile std::uint8_t* p = data_key.data();
164 for (std::size_t i = 0; i < data_key.size(); ++i) p[i] = 0;
165 volatile std::uint8_t* q = mac_key.data();
166 for (std::size_t i = 0; i < mac_key.size(); ++i) q[i] = 0;
167}
168
172
173// =============================================================================
174// audit_key_manager
175// =============================================================================
176
178 std::lock_guard<std::mutex> lock(mutex_);
179 const std::string id = key.key_id;
180 keys_[id] = std::move(key);
181 if (active_id_.empty()) active_id_ = id;
182}
183
184VoidResult audit_key_manager::set_active(const std::string& key_id) {
185 std::lock_guard<std::mutex> lock(mutex_);
186 if (keys_.find(key_id) == keys_.end()) {
187 return pacs_void_error(
188 kcenon::common::error::codes::common_errors::not_found,
189 "audit key not registered: " + key_id);
190 }
191 active_id_ = key_id;
192 return kcenon::common::ok();
193}
194
196 const std::string& key_id) const {
197 std::lock_guard<std::mutex> lock(mutex_);
198 auto it = keys_.find(key_id);
199 return it == keys_.end() ? nullptr : &it->second;
200}
201
203 std::lock_guard<std::mutex> lock(mutex_);
204 if (active_id_.empty()) {
206 kcenon::common::error::codes::common_errors::not_found,
207 "no active audit encryption key configured");
208 }
209 auto it = keys_.find(active_id_);
210 if (it == keys_.end()) {
212 kcenon::common::error::codes::common_errors::internal_error,
213 "active audit key missing from key store: " + active_id_);
214 }
216}
217
218void audit_key_manager::forget(const std::string& key_id) {
219 std::lock_guard<std::mutex> lock(mutex_);
220 keys_.erase(key_id);
221 if (active_id_ == key_id) active_id_.clear();
222}
223
224std::size_t audit_key_manager::size() const {
225 std::lock_guard<std::mutex> lock(mutex_);
226 return keys_.size();
227}
228
229VoidResult audit_key_manager::decode_key(const std::string& encoded,
230 std::uint8_t* out,
231 std::size_t out_size) {
232 if (encoded.empty() || out == nullptr || out_size == 0) {
233 return pacs_void_error(
234 kcenon::common::error::codes::common_errors::invalid_argument,
235 "empty key encoding or invalid buffer");
236 }
237
238 // Hex first: encoded length must be exactly 2 * out_size
239 if (encoded.size() == out_size * 2) {
240 if (detail::hex_decode(encoded, out, out_size)) {
241 return kcenon::common::ok();
242 }
243 // fall through — may still be base64 of the same length
244 }
245
246 std::vector<std::uint8_t> decoded;
247 if (!detail::base64_decode(encoded, decoded)) {
248 return pacs_void_error(
249 kcenon::common::error::codes::common_errors::invalid_argument,
250 "audit key is neither valid hex nor base64");
251 }
252 if (decoded.size() != out_size) {
253 return pacs_void_error(
254 kcenon::common::error::codes::common_errors::invalid_argument,
255 "audit key has wrong length after decoding");
256 }
257 std::memcpy(out, decoded.data(), out_size);
258 // wipe temporary copy
259 volatile std::uint8_t* p = decoded.data();
260 for (std::size_t i = 0; i < decoded.size(); ++i) p[i] = 0;
261 return kcenon::common::ok();
262}
263
264namespace {
265
266std::string getenv_str(const char* name) {
267 const char* v = std::getenv(name);
268 return v ? std::string(v) : std::string();
269}
270
271std::vector<std::string> split_csv(const std::string& s) {
272 std::vector<std::string> out;
273 std::string tok;
274 for (char c : s) {
275 if (c == ',' || c == ' ') {
276 if (!tok.empty()) {
277 out.push_back(tok);
278 tok.clear();
279 }
280 } else {
281 tok.push_back(c);
282 }
283 }
284 if (!tok.empty()) out.push_back(tok);
285 return out;
286}
287
288} // namespace
289
291 const std::string active = getenv_str("PACS_AUDIT_LOG_ACTIVE_KEY_ID");
292 if (active.empty()) {
293 return pacs_void_error(
294 kcenon::common::error::codes::common_errors::invalid_argument,
295 "PACS_AUDIT_LOG_ACTIVE_KEY_ID is not set");
296 }
297
298 std::vector<std::string> ids;
299 const std::string list = getenv_str("PACS_AUDIT_LOG_KEYS");
300 if (list.empty()) {
301 ids.push_back(active);
302 } else {
303 ids = split_csv(list);
304 if (std::find(ids.begin(), ids.end(), active) == ids.end()) {
305 ids.push_back(active);
306 }
307 }
308
309 for (const auto& id : ids) {
310 if (id.empty() || id.find('|') != std::string::npos) {
311 return pacs_void_error(
312 kcenon::common::error::codes::common_errors::invalid_argument,
313 "invalid audit key id: '" + id + "'");
314 }
315 const std::string key_env = "PACS_AUDIT_LOG_KEY_" + id;
316 const std::string mac_env = "PACS_AUDIT_LOG_MAC_" + id;
317 const std::string key_val = getenv_str(key_env.c_str());
318 const std::string mac_val = getenv_str(mac_env.c_str());
319 if (key_val.empty() || mac_val.empty()) {
320 return pacs_void_error(
321 kcenon::common::error::codes::common_errors::invalid_argument,
322 "missing env var for audit key '" + id + "'");
323 }
324
326 k.key_id = id;
327 auto r1 = decode_key(key_val, k.data_key.data(), k.data_key.size());
328 if (!r1.is_ok()) {
329 return pacs_void_error(
330 kcenon::common::error::codes::common_errors::invalid_argument,
331 "failed to decode " + key_env);
332 }
333 auto r2 = decode_key(mac_val, k.mac_key.data(), k.mac_key.size());
334 if (!r2.is_ok()) {
335 return pacs_void_error(
336 kcenon::common::error::codes::common_errors::invalid_argument,
337 "failed to decode " + mac_env);
338 }
339 register_key(std::move(k));
340 }
341
342 return set_active(active);
343}
344
345// =============================================================================
346// audit_log_cipher
347// =============================================================================
348
350
351bool audit_log_cipher::looks_encrypted(const std::string& line) noexcept {
352 const std::string prefix = std::string(audit_cipher_format_tag) + "|" +
354 return line.size() >= prefix.size() &&
355 std::equal(prefix.begin(), prefix.end(), line.begin());
356}
357
358#ifdef PACS_WITH_DIGITAL_SIGNATURES
359
360namespace {
361
362// RAII helper around EVP_CIPHER_CTX
363struct evp_cipher_ctx_guard {
364 EVP_CIPHER_CTX* ctx{nullptr};
365 evp_cipher_ctx_guard() : ctx(EVP_CIPHER_CTX_new()) {}
366 ~evp_cipher_ctx_guard() {
367 if (ctx) EVP_CIPHER_CTX_free(ctx);
368 }
369 evp_cipher_ctx_guard(const evp_cipher_ctx_guard&) = delete;
370 evp_cipher_ctx_guard& operator=(const evp_cipher_ctx_guard&) = delete;
371};
372
373bool hmac_sha256(const std::uint8_t* key,
374 std::size_t key_size,
375 const std::uint8_t* data,
376 std::size_t data_size,
377 std::uint8_t* out32) {
378 unsigned int out_len = 0;
379 const unsigned char* result = ::HMAC(
380 EVP_sha256(),
381 key, static_cast<int>(key_size),
382 data, data_size,
383 out32, &out_len);
384 return result != nullptr && out_len == 32;
385}
386
387} // namespace
388
390 const std::string& plaintext) const {
391 auto key_r = keys_.active();
392 if (!key_r.is_ok()) {
394 kcenon::common::error::codes::common_errors::not_found,
395 "no active audit encryption key");
396 }
397 const audit_cipher_key& key = key_r.value();
398
399 if (key.key_id.empty() ||
400 key.key_id.find('|') != std::string::npos) {
402 kcenon::common::error::codes::common_errors::invalid_argument,
403 "active audit key has invalid id");
404 }
405
406 // 1. Fresh random IV
407 std::array<std::uint8_t, audit_cipher_iv_bytes> iv{};
408 if (RAND_bytes(iv.data(), static_cast<int>(iv.size())) != 1) {
410 kcenon::common::error::codes::common_errors::internal_error,
411 "RAND_bytes failed for audit log IV");
412 }
413
414 // 2. AES-256-GCM encrypt
415 evp_cipher_ctx_guard guard;
416 if (!guard.ctx) {
418 kcenon::common::error::codes::common_errors::internal_error,
419 "EVP_CIPHER_CTX_new failed");
420 }
421 if (EVP_EncryptInit_ex(guard.ctx, EVP_aes_256_gcm(), nullptr,
422 key.data_key.data(), iv.data()) != 1) {
424 kcenon::common::error::codes::common_errors::internal_error,
425 "EVP_EncryptInit_ex failed");
426 }
427
428 std::vector<std::uint8_t> ct(plaintext.size() + 16); // GCM no padding
429 int out_len = 0;
430 if (!plaintext.empty()) {
431 if (EVP_EncryptUpdate(
432 guard.ctx, ct.data(), &out_len,
433 reinterpret_cast<const unsigned char*>(plaintext.data()),
434 static_cast<int>(plaintext.size())) != 1) {
436 kcenon::common::error::codes::common_errors::internal_error,
437 "EVP_EncryptUpdate failed");
438 }
439 }
440 int final_len = 0;
441 if (EVP_EncryptFinal_ex(guard.ctx, ct.data() + out_len, &final_len) != 1) {
443 kcenon::common::error::codes::common_errors::internal_error,
444 "EVP_EncryptFinal_ex failed");
445 }
446 ct.resize(static_cast<std::size_t>(out_len + final_len));
447
448 std::array<std::uint8_t, audit_cipher_tag_bytes> tag{};
449 if (EVP_CIPHER_CTX_ctrl(guard.ctx, EVP_CTRL_GCM_GET_TAG,
450 static_cast<int>(tag.size()),
451 tag.data()) != 1) {
453 kcenon::common::error::codes::common_errors::internal_error,
454 "EVP_CTRL_GCM_GET_TAG failed");
455 }
456
457 // 3. Serialise prefix and inner fields
458 const std::string iv_b64 = detail::base64_encode(iv.data(), iv.size());
459 const std::string ct_b64 = detail::base64_encode(ct.data(), ct.size());
460 const std::string tag_b64 = detail::base64_encode(tag.data(), tag.size());
461
462 std::string prefix;
463 prefix.reserve(64 + key.key_id.size() + iv_b64.size() +
464 ct_b64.size() + tag_b64.size());
465 prefix.append(audit_cipher_format_tag);
466 prefix.push_back('|');
467 prefix.append(audit_cipher_format_version);
468 prefix.push_back('|');
469 prefix.append(key.key_id);
470 prefix.push_back('|');
471 prefix.append(iv_b64);
472 prefix.push_back('|');
473 prefix.append(ct_b64);
474 prefix.push_back('|');
475 prefix.append(tag_b64);
476
477 // 4. HMAC-SHA256 over prefix, using the independent MAC key
478 std::array<std::uint8_t, 32> mac{};
479 if (!hmac_sha256(key.mac_key.data(), key.mac_key.size(),
480 reinterpret_cast<const std::uint8_t*>(prefix.data()),
481 prefix.size(),
482 mac.data())) {
484 kcenon::common::error::codes::common_errors::internal_error,
485 "HMAC-SHA256 failed");
486 }
487 const std::string mac_b64 = detail::base64_encode(mac.data(), mac.size());
488
489 prefix.push_back('|');
490 prefix.append(mac_b64);
491 return kcenon::common::Result<std::string>(std::move(prefix));
492}
493
494namespace {
495
496struct parsed_record {
497 std::string key_id;
498 std::vector<std::uint8_t> iv;
499 std::vector<std::uint8_t> ciphertext;
500 std::vector<std::uint8_t> tag;
501 std::vector<std::uint8_t> mac;
502 std::string mac_prefix; // the exact bytes that were HMAC'd
503};
504
505// Parse "PACSAUDIT|v1|<id>|<iv>|<ct>|<tag>|<hmac>", trailing '\n' tolerated.
506bool parse_record(std::string line, parsed_record& out) {
507 while (!line.empty() &&
508 (line.back() == '\n' || line.back() == '\r')) {
509 line.pop_back();
510 }
511
512 // Split on '|' into exactly 7 fields.
513 std::array<std::string, 7> fields;
514 std::size_t idx = 0;
515 std::string tok;
516 for (char c : line) {
517 if (c == '|') {
518 if (idx >= fields.size()) return false;
519 fields[idx++] = std::move(tok);
520 tok.clear();
521 } else {
522 tok.push_back(c);
523 }
524 }
525 if (idx >= fields.size()) return false;
526 fields[idx++] = std::move(tok);
527 if (idx != fields.size()) return false;
528
529 if (fields[0] != audit_cipher_format_tag) return false;
530 if (fields[1] != audit_cipher_format_version) return false;
531 if (fields[2].empty()) return false;
532
533 if (!detail::base64_decode(fields[3], out.iv)) return false;
534 if (!detail::base64_decode(fields[4], out.ciphertext)) return false;
535 if (!detail::base64_decode(fields[5], out.tag)) return false;
536 if (!detail::base64_decode(fields[6], out.mac)) return false;
537
538 if (out.iv.size() != audit_cipher_iv_bytes) return false;
539 if (out.tag.size() != audit_cipher_tag_bytes) return false;
540 if (out.mac.size() != 32) return false;
541
542 out.key_id = fields[2];
543 // Reconstruct the MAC-covered prefix (everything before the final '|')
544 out.mac_prefix.reserve(line.size());
545 out.mac_prefix.append(fields[0]);
546 out.mac_prefix.push_back('|');
547 out.mac_prefix.append(fields[1]);
548 out.mac_prefix.push_back('|');
549 out.mac_prefix.append(fields[2]);
550 out.mac_prefix.push_back('|');
551 out.mac_prefix.append(fields[3]);
552 out.mac_prefix.push_back('|');
553 out.mac_prefix.append(fields[4]);
554 out.mac_prefix.push_back('|');
555 out.mac_prefix.append(fields[5]);
556 return true;
557}
558
559} // namespace
560
562 const std::string& encoded) const {
563 parsed_record rec;
564 if (!parse_record(encoded, rec)) {
566 kcenon::common::error::codes::common_errors::invalid_argument,
567 "malformed encrypted audit record");
568 }
569
570 const audit_cipher_key* key = keys_.find(rec.key_id);
571 if (key == nullptr) {
573 kcenon::common::error::codes::common_errors::not_found,
574 "audit record key id not registered: " + rec.key_id);
575 }
576
577 // 1. Verify HMAC first (fail fast before touching GCM)
578 std::array<std::uint8_t, 32> expected_mac{};
579 if (!hmac_sha256(key->mac_key.data(), key->mac_key.size(),
580 reinterpret_cast<const std::uint8_t*>(
581 rec.mac_prefix.data()),
582 rec.mac_prefix.size(),
583 expected_mac.data())) {
585 kcenon::common::error::codes::common_errors::internal_error,
586 "HMAC-SHA256 failed during verify");
587 }
588 if (!detail::constant_time_equal(expected_mac.data(),
589 rec.mac.data(),
590 expected_mac.size())) {
592 kcenon::common::error::codes::common_errors::invalid_argument,
593 "audit record HMAC verification failed");
594 }
595
596 // 2. AES-256-GCM decrypt
597 evp_cipher_ctx_guard guard;
598 if (!guard.ctx) {
600 kcenon::common::error::codes::common_errors::internal_error,
601 "EVP_CIPHER_CTX_new failed");
602 }
603 if (EVP_DecryptInit_ex(guard.ctx, EVP_aes_256_gcm(), nullptr,
604 key->data_key.data(), rec.iv.data()) != 1) {
606 kcenon::common::error::codes::common_errors::internal_error,
607 "EVP_DecryptInit_ex failed");
608 }
609
610 std::vector<std::uint8_t> pt(rec.ciphertext.size() + 16);
611 int out_len = 0;
612 if (!rec.ciphertext.empty()) {
613 if (EVP_DecryptUpdate(guard.ctx, pt.data(), &out_len,
614 rec.ciphertext.data(),
615 static_cast<int>(rec.ciphertext.size())) != 1) {
617 kcenon::common::error::codes::common_errors::invalid_argument,
618 "EVP_DecryptUpdate failed");
619 }
620 }
621
622 if (EVP_CIPHER_CTX_ctrl(guard.ctx, EVP_CTRL_GCM_SET_TAG,
623 static_cast<int>(rec.tag.size()),
624 rec.tag.data()) != 1) {
626 kcenon::common::error::codes::common_errors::internal_error,
627 "EVP_CTRL_GCM_SET_TAG failed");
628 }
629
630 int final_len = 0;
631 if (EVP_DecryptFinal_ex(guard.ctx, pt.data() + out_len, &final_len) != 1) {
633 kcenon::common::error::codes::common_errors::invalid_argument,
634 "audit record GCM tag verification failed");
635 }
636 pt.resize(static_cast<std::size_t>(out_len + final_len));
637
639 std::string(reinterpret_cast<const char*>(pt.data()), pt.size()));
640}
641
642VoidResult audit_log_cipher::verify_record(const std::string& encoded) const {
643 auto r = decrypt_record(encoded);
644 if (!r.is_ok()) return VoidResult(r.error());
645 return kcenon::common::ok();
646}
647
648#else // !PACS_WITH_DIGITAL_SIGNATURES
649
651 const std::string& /*plaintext*/) const {
653 kcenon::common::error::codes::common_errors::internal_error,
654 "audit log encryption requires OpenSSL "
655 "(build with PACS_WITH_OPENSSL=ON)");
656}
657
659 const std::string& /*encoded*/) const {
661 kcenon::common::error::codes::common_errors::internal_error,
662 "audit log decryption requires OpenSSL "
663 "(build with PACS_WITH_OPENSSL=ON)");
664}
665
667 const std::string& /*encoded*/) const {
668 return pacs_void_error(
669 kcenon::common::error::codes::common_errors::internal_error,
670 "audit log verification requires OpenSSL "
671 "(build with PACS_WITH_OPENSSL=ON)");
672}
673
674#endif // PACS_WITH_DIGITAL_SIGNATURES
675
676} // namespace kcenon::pacs::security
At-rest encryption for ATNA audit log records (ISO 27799 compliance)
Holds the active encryption key and any retired keys for decryption of historical records.
VoidResult set_active(const std::string &key_id)
Mark a previously-registered key as the active write key.
static VoidResult decode_key(const std::string &encoded, std::uint8_t *out, std::size_t out_size)
Decode a hex or base64 key string into a fixed-size buffer.
Result< audit_cipher_key > active() const
Retrieve the active write key (error if none configured)
VoidResult load_from_env()
Load keys from environment variables.
void register_key(audit_cipher_key key)
Register a key (idempotent by key_id; overwrites existing entry)
std::unordered_map< std::string, audit_cipher_key > keys_
const audit_cipher_key * find(const std::string &key_id) const
Look up a key by identifier (returns nullptr if unknown)
void forget(const std::string &key_id)
Drop a key by id (does nothing if it was not registered)
std::size_t size() const
Number of registered keys.
Result< std::string > encrypt_record(const std::string &plaintext) const
Encrypt a plaintext audit record.
VoidResult verify_record(const std::string &encoded) const
Verify a record's authenticity without returning plaintext.
audit_log_cipher(audit_key_manager &keys)
Construct a cipher backed by the given key manager.
Result< std::string > decrypt_record(const std::string &encoded) const
Decrypt a previously encoded record.
static bool looks_encrypted(const std::string &line) noexcept
Check whether a line appears to be an encrypted audit record.
bool constant_time_equal(const std::uint8_t *a, const std::uint8_t *b, std::size_t n) noexcept
Constant-time equality on byte buffers of equal length.
bool base64_decode(const std::string &in, std::vector< std::uint8_t > &out)
Standard base64 decode; returns false on malformed input.
bool hex_decode(const std::string &in, std::uint8_t *out, std::size_t out_size)
Hex decode (lowercase or uppercase); returns false on malformed input.
std::string base64_encode(const std::uint8_t *data, std::size_t size)
Standard base64 encode (no line breaks)
constexpr std::size_t audit_cipher_iv_bytes
GCM IV length in bytes (NIST SP 800-38D recommended 96 bits)
constexpr const char * audit_cipher_format_version
@ empty
Z - Replace with zero-length value.
constexpr std::size_t audit_cipher_tag_bytes
GCM authentication tag length in bytes.
constexpr const char * audit_cipher_format_tag
Format version string embedded in every encrypted record.
kcenon::common::Result< T > Result
Result type alias for PACS operations.
Definition result.h:30
VoidResult pacs_void_error(int code, const std::string &message, const std::string &details="")
Create a PACS void error result.
Definition result.h:249
Result< T > pacs_error(int code, const std::string &message, const std::string &details="")
Create a PACS error result with module context.
Definition result.h:234
std::string out
A versioned symmetric key pair (data encryption + MAC)
std::array< std::uint8_t, audit_cipher_key_bytes > data_key
256-bit AES data encryption key
void wipe() noexcept
Secure zero helper (wipes key material on destruction)
std::string key_id
Stable identifier (ASCII, non-empty, no '|' character)
std::array< std::uint8_t, audit_cipher_mac_key_bytes > mac_key
256-bit HMAC-SHA256 key
std::string_view name