18#ifdef PACS_WITH_DIGITAL_SIGNATURES
19 #include <openssl/evp.h>
20 #include <openssl/hmac.h>
21 #include <openssl/rand.h>
34constexpr char kBase64Alphabet[] =
35 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
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;
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;
58 out.reserve(((size + 2) / 3) * 4);
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]);
71 const std::size_t rem = size - i;
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]);
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]);
90 if (in.size() % 4 != 0)
return false;
92 out.reserve((in.size() / 4) * 3);
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];
100 const int v0 = base64_value(c0);
101 const int v1 = base64_value(c1);
102 if (v0 < 0 || v1 < 0)
return false;
104 const std::uint32_t b0 =
static_cast<std::uint32_t
>(v0);
105 const std::uint32_t b1 =
static_cast<std::uint32_t
>(v1);
107 out.push_back(
static_cast<std::uint8_t
>((b0 << 2) | (b1 >> 4)));
110 if (c3 !=
'=')
return false;
111 if (i + 4 != in.size())
return false;
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);
118 out.push_back(
static_cast<std::uint8_t
>(((b1 & 0xF) << 4) | (b2 >> 2)));
121 if (i + 4 != in.size())
return false;
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));
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);
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]);
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;
178 std::lock_guard<std::mutex> lock(
mutex_);
179 const std::string
id = key.
key_id;
180 keys_[id] = std::move(key);
185 std::lock_guard<std::mutex> lock(
mutex_);
188 kcenon::common::error::codes::common_errors::not_found,
189 "audit key not registered: " + key_id);
192 return kcenon::common::ok();
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;
203 std::lock_guard<std::mutex> lock(
mutex_);
206 kcenon::common::error::codes::common_errors::not_found,
207 "no active audit encryption key configured");
210 if (it ==
keys_.end()) {
212 kcenon::common::error::codes::common_errors::internal_error,
213 "active audit key missing from key store: " +
active_id_);
219 std::lock_guard<std::mutex> lock(
mutex_);
225 std::lock_guard<std::mutex> lock(
mutex_);
231 std::size_t out_size) {
232 if (encoded.empty() ||
out ==
nullptr || out_size == 0) {
234 kcenon::common::error::codes::common_errors::invalid_argument,
235 "empty key encoding or invalid buffer");
239 if (encoded.size() == out_size * 2) {
241 return kcenon::common::ok();
246 std::vector<std::uint8_t> decoded;
249 kcenon::common::error::codes::common_errors::invalid_argument,
250 "audit key is neither valid hex nor base64");
252 if (decoded.size() != out_size) {
254 kcenon::common::error::codes::common_errors::invalid_argument,
255 "audit key has wrong length after decoding");
257 std::memcpy(
out, decoded.data(), out_size);
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();
266std::string getenv_str(
const char*
name) {
267 const char* v = std::getenv(
name);
268 return v ? std::string(v) : std::string();
271std::vector<std::string> split_csv(
const std::string& s) {
272 std::vector<std::string>
out;
275 if (c ==
',' || c ==
' ') {
284 if (!tok.empty())
out.push_back(tok);
291 const std::string
active = getenv_str(
"PACS_AUDIT_LOG_ACTIVE_KEY_ID");
294 kcenon::common::error::codes::common_errors::invalid_argument,
295 "PACS_AUDIT_LOG_ACTIVE_KEY_ID is not set");
298 std::vector<std::string> ids;
299 const std::string
list = getenv_str(
"PACS_AUDIT_LOG_KEYS");
303 ids = split_csv(
list);
304 if (std::find(ids.begin(), ids.end(),
active) == ids.end()) {
309 for (
const auto&
id : ids) {
310 if (
id.
empty() ||
id.
find(
'|') != std::string::npos) {
312 kcenon::common::error::codes::common_errors::invalid_argument,
313 "invalid audit key id: '" +
id +
"'");
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()) {
321 kcenon::common::error::codes::common_errors::invalid_argument,
322 "missing env var for audit key '" +
id +
"'");
330 kcenon::common::error::codes::common_errors::invalid_argument,
331 "failed to decode " + key_env);
336 kcenon::common::error::codes::common_errors::invalid_argument,
337 "failed to decode " + mac_env);
354 return line.size() >= prefix.size() &&
355 std::equal(prefix.begin(), prefix.end(), line.begin());
358#ifdef PACS_WITH_DIGITAL_SIGNATURES
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);
369 evp_cipher_ctx_guard(
const evp_cipher_ctx_guard&) =
delete;
370 evp_cipher_ctx_guard& operator=(
const evp_cipher_ctx_guard&) =
delete;
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(
381 key,
static_cast<int>(key_size),
384 return result !=
nullptr && out_len == 32;
390 const std::string& plaintext)
const {
392 if (!key_r.is_ok()) {
394 kcenon::common::error::codes::common_errors::not_found,
395 "no active audit encryption key");
397 const audit_cipher_key& key = key_r.value();
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");
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");
415 evp_cipher_ctx_guard guard;
418 kcenon::common::error::codes::common_errors::internal_error,
419 "EVP_CIPHER_CTX_new failed");
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");
428 std::vector<std::uint8_t>
ct(plaintext.size() + 16);
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");
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");
446 ct.resize(
static_cast<std::size_t
>(out_len + final_len));
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()),
453 kcenon::common::error::codes::common_errors::internal_error,
454 "EVP_CTRL_GCM_GET_TAG failed");
463 prefix.reserve(64 + key.key_id.size() + iv_b64.size() +
464 ct_b64.size() + tag_b64.size());
466 prefix.push_back(
'|');
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);
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()),
484 kcenon::common::error::codes::common_errors::internal_error,
485 "HMAC-SHA256 failed");
489 prefix.push_back(
'|');
490 prefix.append(mac_b64);
496struct parsed_record {
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;
506bool parse_record(std::string line, parsed_record&
out) {
507 while (!
line.empty() &&
508 (
line.back() ==
'\n' ||
line.back() ==
'\r')) {
513 std::array<std::string, 7> fields;
516 for (
char c : line) {
518 if (idx >= fields.size())
return false;
519 fields[idx++] = std::move(tok);
525 if (idx >= fields.size())
return false;
526 fields[idx++] = std::move(tok);
527 if (idx != fields.size())
return false;
531 if (fields[2].
empty())
return false;
540 if (
out.mac.size() != 32)
return false;
542 out.key_id = fields[2];
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]);
562 const std::string& encoded)
const {
564 if (!parse_record(encoded, rec)) {
566 kcenon::common::error::codes::common_errors::invalid_argument,
567 "malformed encrypted audit record");
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);
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");
590 expected_mac.size())) {
592 kcenon::common::error::codes::common_errors::invalid_argument,
593 "audit record HMAC verification failed");
597 evp_cipher_ctx_guard guard;
600 kcenon::common::error::codes::common_errors::internal_error,
601 "EVP_CIPHER_CTX_new failed");
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");
610 std::vector<std::uint8_t> pt(rec.ciphertext.size() + 16);
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");
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");
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");
636 pt.resize(
static_cast<std::size_t
>(out_len + final_len));
639 std::string(
reinterpret_cast<const char*
>(pt.data()), pt.size()));
644 if (!r.is_ok())
return VoidResult(r.error());
645 return kcenon::common::ok();
651 const std::string& )
const {
653 kcenon::common::error::codes::common_errors::internal_error,
654 "audit log encryption requires OpenSSL "
655 "(build with PACS_WITH_OPENSSL=ON)");
659 const std::string& )
const {
661 kcenon::common::error::codes::common_errors::internal_error,
662 "audit log decryption requires OpenSSL "
663 "(build with PACS_WITH_OPENSSL=ON)");
667 const std::string& )
const {
669 kcenon::common::error::codes::common_errors::internal_error,
670 "audit log verification requires OpenSSL "
671 "(build with PACS_WITH_OPENSSL=ON)");
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_key_manager & keys_
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.
VoidResult pacs_void_error(int code, const std::string &message, const std::string &details="")
Create a PACS void error result.
Result< T > pacs_error(int code, const std::string &message, const std::string &details="")
Create a PACS error result with module context.
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