25#ifdef PACS_WITH_DIGITAL_SIGNATURES
26 #include <openssl/evp.h>
27 #include <openssl/rand.h>
40 : profile_{
other.profile_}
41 , custom_actions_{
other.custom_actions_}
42 , date_offset_{
other.date_offset_}
43 , encryption_key_{
other.encryption_key_}
44 , hash_salt_{
other.hash_salt_}
45 , private_tag_action_{
other.private_tag_action_}
46 , detailed_reporting_{
other.detailed_reporting_} {}
49 : profile_{
other.profile_}
50 , custom_actions_{std::move(
other.custom_actions_)}
51 , date_offset_{
other.date_offset_}
52 , encryption_key_{std::move(
other.encryption_key_)}
53 , hash_salt_{std::move(
other.hash_salt_)}
54 , private_tag_action_{
other.private_tag_action_}
55 , detailed_reporting_{
other.detailed_reporting_} {}
59 profile_ =
other.profile_;
60 custom_actions_ =
other.custom_actions_;
61 date_offset_ =
other.date_offset_;
62 encryption_key_ =
other.encryption_key_;
63 hash_salt_ =
other.hash_salt_;
64 private_tag_action_ =
other.private_tag_action_;
65 detailed_reporting_ =
other.detailed_reporting_;
72 profile_ =
other.profile_;
73 custom_actions_ = std::move(
other.custom_actions_);
74 date_offset_ =
other.date_offset_;
75 encryption_key_ = std::move(
other.encryption_key_);
76 hash_salt_ = std::move(
other.hash_salt_);
77 private_tag_action_ =
other.private_tag_action_;
78 detailed_reporting_ =
other.detailed_reporting_;
86 return anonymize_with_mapping(dataset, temp_mapping);
95 report.date_offset = date_offset_;
96 report.timestamp = std::chrono::system_clock::now();
99 auto profile_actions = get_profile_actions(profile_);
102 for (
const auto& [tag, config] : custom_actions_) {
103 profile_actions[tag] = config;
107 for (
const auto& [tag, config] : profile_actions) {
108 if (!dataset.contains(tag)) {
112 auto record = apply_action(dataset, tag, config, &mapping);
113 report.total_tags_processed++;
115 switch (config.action) {
118 if (record.success) {
123 if (record.success) {
128 if (record.success) {
133 if (record.success) {
141 if (record.success) {
146 if (record.success) {
151 if (record.success) {
157 if (!record.success && !record.error_message.empty()) {
158 report.errors.push_back(record.error_message);
161 if (detailed_reporting_) {
162 report.action_records.push_back(std::move(record));
168 std::vector<dicom_tag> tags_to_remove;
170 for (
auto it = dataset.begin(); it != dataset.end(); ++it) {
171 auto tag = it->first;
173 if (tag.is_private()) {
174 tags_to_remove.push_back(tag);
177 if (tag.is_private_data()) {
178 tags_to_remove.push_back(tag);
183 for (
const auto& tag : tags_to_remove) {
184 if (dataset.remove(tag)) {
185 report.private_tags_removed++;
216 const std::map<dicom_tag, tag_action_config>& actions
218 for (
const auto& [tag, config] : actions) {
224 return custom_actions_.erase(tag) > 0;
232 auto it = custom_actions_.find(tag);
233 if (it != custom_actions_.end()) {
237 auto profile_actions = get_profile_actions(profile_);
238 auto profile_it = profile_actions.find(tag);
239 if (profile_it != profile_actions.end()) {
240 return profile_it->second;
251 -> std::optional<std::chrono::days> {
260 std::chrono::days min_days,
261 std::chrono::days max_days
262) -> std::chrono::days {
263 std::random_device rd;
264 std::mt19937 gen(rd());
265 std::uniform_int_distribution<int> dist(
266 static_cast<int>(min_days.count()),
267 static_cast<int>(max_days.count())
269 return std::chrono::days{dist(gen)};
273 -> kcenon::common::VoidResult {
274 if (key.size() != 32) {
275 return kcenon::common::make_error<std::monostate>(
276 1,
"Encryption key must be 32 bytes for AES-256",
"anonymizer"
279 encryption_key_.assign(key.begin(), key.end());
280 return kcenon::common::ok();
304 -> std::map<dicom_tag, tag_action_config> {
305 std::map<dicom_tag, tag_action_config> actions;
308 auto add_patient_identifiers = [&actions]() {
319 auto add_institution_identifiers = [&actions]() {
326 auto add_personnel_identifiers = [&actions]() {
335 auto add_uid_replacements = [&actions]() {
347 auto add_study_identifiers = [&actions]() {
353 auto add_date_shifting = [&actions]() {
364 add_patient_identifiers();
365 add_institution_identifiers();
366 add_personnel_identifiers();
367 add_uid_replacements();
368 add_study_identifiers();
372 add_patient_identifiers();
373 add_institution_identifiers();
374 add_personnel_identifiers();
375 add_uid_replacements();
376 add_study_identifiers();
382 add_patient_identifiers();
383 add_institution_identifiers();
384 add_personnel_identifiers();
385 add_uid_replacements();
386 add_study_identifiers();
392 add_patient_identifiers();
393 add_institution_identifiers();
394 add_personnel_identifiers();
395 add_uid_replacements();
396 add_study_identifiers();
401 add_patient_identifiers();
402 add_institution_identifiers();
403 add_personnel_identifiers();
404 add_uid_replacements();
405 add_study_identifiers();
414 add_patient_identifiers();
415 add_institution_identifiers();
416 add_personnel_identifiers();
417 add_uid_replacements();
418 add_study_identifiers();
420 for (
const auto& tag : get_hipaa_identifier_tags()) {
421 if (actions.find(tag) == actions.end()) {
430 add_patient_identifiers();
431 add_institution_identifiers();
432 add_personnel_identifiers();
433 add_uid_replacements();
434 add_study_identifiers();
449 std::vector<dicom_tag> tags;
479 record.action = config.action;
481 auto* element = dataset.get(tag);
482 if (element ==
nullptr) {
483 record.success =
false;
484 record.error_message =
"Tag not found in dataset";
488 record.original_value = element->as_string().unwrap_or(
"");
490 switch (config.action) {
493 record.success = dataset.remove(tag);
497 dataset.set_string(tag, element->vr(),
"");
498 record.new_value =
"";
499 record.success =
true;
503 record.new_value = record.original_value;
504 record.success =
true;
508 dataset.set_string(tag, element->vr(), config.replacement_value);
509 record.new_value = config.replacement_value;
510 record.success =
true;
514 if (mapping !=
nullptr) {
515 auto result = mapping->get_or_create(record.original_value);
516 if (result.is_ok()) {
517 dataset.set_string(tag, element->vr(), result.value());
518 record.new_value = result.value();
519 record.success =
true;
521 record.success =
false;
522 record.error_message =
"Failed to create UID mapping";
528 if (result.is_ok()) {
529 dataset.set_string(tag, element->vr(), result.value());
530 record.new_value = result.value();
531 record.success =
true;
533 record.success =
false;
534 record.error_message =
"Failed to generate new UID";
540 auto hashed = hash_value(record.original_value);
541 if (hashed.empty()) {
542 record.success =
false;
543 record.error_message =
"Hash computation failed";
545 dataset.set_string(tag, element->vr(), hashed);
546 record.new_value = hashed;
547 record.success =
true;
553 auto result = encrypt_value(record.original_value);
554 if (result.is_ok()) {
555 dataset.set_string(tag, element->vr(), result.value());
556 record.new_value = result.value();
557 record.success =
true;
559 record.success =
false;
560 record.error_message = result.error().message;
566 if (date_offset_.has_value()) {
567 auto shifted =
shift_date(record.original_value);
568 dataset.set_string(tag, element->vr(), shifted);
569 record.new_value = shifted;
570 record.success =
true;
573 dataset.set_string(tag, element->vr(),
"");
574 record.new_value =
"";
575 record.success =
true;
585 if (date_string.empty() || !date_offset_.has_value()) {
590 if (date_string.length() < 8) {
591 return std::string(date_string);
595 int year = std::stoi(std::string(date_string.substr(0, 4)));
596 int month = std::stoi(std::string(date_string.substr(4, 2)));
597 int day = std::stoi(std::string(date_string.substr(6, 2)));
601 tm.tm_year = year - 1900;
602 tm.tm_mon = month - 1;
605 auto time_point = std::chrono::system_clock::from_time_t(std::mktime(&tm));
608 time_point += date_offset_.value();
611 auto shifted_time = std::chrono::system_clock::to_time_t(time_point);
612 std::tm shifted_tm{};
614 localtime_s(&shifted_tm, &shifted_time);
616 localtime_r(&shifted_time, &shifted_tm);
619 std::ostringstream oss;
620 oss << std::setfill(
'0')
621 << std::setw(4) << (shifted_tm.tm_year + 1900)
622 << std::setw(2) << (shifted_tm.tm_mon + 1)
623 << std::setw(2) << shifted_tm.tm_mday;
627 }
catch (
const std::exception&) {
628 return std::string(date_string);
633 std::string to_hash = std::string(value);
635 if (hash_salt_.has_value()) {
636 to_hash = hash_salt_.value() + to_hash;
639#ifdef PACS_WITH_DIGITAL_SIGNATURES
640 auto* ctx = EVP_MD_CTX_new();
641 if (ctx ==
nullptr) {
645 struct md_ctx_deleter {
646 void operator()(EVP_MD_CTX* c)
const { EVP_MD_CTX_free(c); }
648 std::unique_ptr<EVP_MD_CTX, md_ctx_deleter> ctx_guard(ctx);
650 if (EVP_DigestInit_ex(ctx, EVP_sha256(),
nullptr) != 1
651 || EVP_DigestUpdate(ctx, to_hash.data(), to_hash.size()) != 1) {
655 std::array<unsigned char, EVP_MAX_MD_SIZE> digest{};
656 unsigned int digest_len = 0;
657 if (EVP_DigestFinal_ex(ctx, digest.data(), &digest_len) != 1) {
661 std::ostringstream oss;
662 oss << std::hex << std::setfill(
'0');
663 for (
unsigned int i = 0; i < digest_len; ++i) {
664 oss << std::setw(2) << static_cast<int>(digest[i]);
669 auto hash = std::hash<std::string>{}(to_hash);
670 std::ostringstream oss;
671 oss << std::hex << std::setfill(
'0') << std::setw(16) <<
hash;
678 if (encryption_key_.empty()) {
679 return kcenon::common::make_error<std::string>(
680 1,
"No encryption key configured");
683#ifdef PACS_WITH_DIGITAL_SIGNATURES
684 constexpr int iv_length = 12;
685 constexpr int tag_length = 16;
688 std::array<unsigned char, iv_length> iv{};
689 if (RAND_bytes(iv.data(), iv_length) != 1) {
690 return kcenon::common::make_error<std::string>(
691 2,
"Failed to generate random IV");
695 struct cipher_ctx_deleter {
696 void operator()(EVP_CIPHER_CTX* c)
const { EVP_CIPHER_CTX_free(c); }
699 std::unique_ptr<EVP_CIPHER_CTX, cipher_ctx_deleter> ctx(
700 EVP_CIPHER_CTX_new());
702 return kcenon::common::make_error<std::string>(
703 3,
"Failed to create cipher context");
707 if (EVP_EncryptInit_ex(
708 ctx.get(), EVP_aes_256_gcm(),
nullptr,
nullptr,
nullptr)
710 return kcenon::common::make_error<std::string>(
711 4,
"Failed to initialize AES-256-GCM");
714 if (EVP_CIPHER_CTX_ctrl(
715 ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv_length,
nullptr)
717 return kcenon::common::make_error<std::string>(
718 5,
"Failed to set IV length");
721 if (EVP_EncryptInit_ex(ctx.get(),
nullptr,
nullptr,
722 encryption_key_.data(), iv.data())
724 return kcenon::common::make_error<std::string>(
725 6,
"Failed to set encryption key and IV");
729 std::vector<unsigned char> ciphertext(value.size() + EVP_MAX_BLOCK_LENGTH);
731 if (EVP_EncryptUpdate(
732 ctx.get(), ciphertext.data(), &out_len,
733 reinterpret_cast<const unsigned char*
>(value.data()),
734 static_cast<int>(value.size()))
736 return kcenon::common::make_error<std::string>(
737 7,
"Encryption failed");
739 int ciphertext_len = out_len;
742 if (EVP_EncryptFinal_ex(
743 ctx.get(), ciphertext.data() + out_len, &out_len)
745 return kcenon::common::make_error<std::string>(
746 8,
"Encryption finalization failed");
748 ciphertext_len += out_len;
751 std::array<unsigned char, tag_length> tag{};
752 if (EVP_CIPHER_CTX_ctrl(
753 ctx.get(), EVP_CTRL_GCM_GET_TAG, tag_length, tag.data())
755 return kcenon::common::make_error<std::string>(
756 9,
"Failed to get authentication tag");
760 std::ostringstream oss;
761 oss << std::hex << std::setfill(
'0');
762 for (
int i = 0; i < iv_length; ++i) {
763 oss << std::setw(2) << static_cast<int>(iv[i]);
765 for (
int i = 0; i < ciphertext_len; ++i) {
766 oss << std::setw(2) << static_cast<int>(ciphertext[i]);
768 for (
int i = 0; i < tag_length; ++i) {
769 oss << std::setw(2) << static_cast<int>(tag[i]);
775 return kcenon::common::make_error<std::string>(
776 1,
"Encryption requires OpenSSL (build with PACS_WITH_DIGITAL_SIGNATURES)");
void clear_custom_actions()
Clear all custom tag actions.
auto hash_value(std::string_view value) const -> std::string
static auto get_hipaa_identifier_tags() -> std::vector< core::dicom_tag >
Get a list of HIPAA Safe Harbor identifier tags.
private_tag_action private_tag_action_
Action for private tags.
void set_detailed_reporting(bool enable)
Enable detailed action recording.
std::optional< std::string > hash_salt_
Hash salt.
auto apply_action(core::dicom_dataset &dataset, core::dicom_tag tag, const tag_action_config &config, uid_mapping *mapping) -> tag_action_record
void add_tag_action(core::dicom_tag tag, tag_action_config config)
Add or override a tag action.
void set_hash_salt(std::string salt)
Set salt for hash operations.
anonymizer(anonymization_profile profile=anonymization_profile::basic)
Construct with a specific profile.
static auto get_gdpr_personal_data_tags() -> std::vector< core::dicom_tag >
Get a list of GDPR personal data tags.
auto get_tag_action(core::dicom_tag tag) const -> tag_action_config
Get the effective action for a tag.
std::optional< std::chrono::days > date_offset_
Date offset for shifting.
void set_profile(anonymization_profile profile)
Set a new profile.
auto get_date_offset() const noexcept -> std::optional< std::chrono::days >
Get the current date offset.
void initialize_profile_actions()
static auto get_profile_actions(anonymization_profile profile) -> std::map< core::dicom_tag, tag_action_config >
Get tags to process for a given profile.
std::vector< std::uint8_t > encryption_key_
Encryption key (if set)
void clear_date_offset()
Clear the date offset (dates will be zeroed instead)
void add_tag_actions(const std::map< core::dicom_tag, tag_action_config > &actions)
Add multiple tag actions.
auto anonymize_with_mapping(core::dicom_dataset &dataset, uid_mapping &mapping) -> kcenon::common::Result< anonymization_report >
Anonymize with consistent UID mapping.
auto shift_date(std::string_view date_string) const -> std::string
auto has_encryption_key() const noexcept -> bool
Check if encryption is configured.
auto get_hash_salt() const -> std::optional< std::string >
Get the current hash salt.
static auto generate_random_date_offset(std::chrono::days min_days=std::chrono::days{-365}, std::chrono::days max_days=std::chrono::days{365}) -> std::chrono::days
Generate a random date offset.
anonymization_profile profile_
Current anonymization profile.
auto set_encryption_key(std::span< const std::uint8_t > key) -> kcenon::common::VoidResult
Set encryption key for encrypt actions.
void set_date_offset(std::chrono::days offset)
Set date offset for longitudinal consistency.
auto encrypt_value(std::string_view value) const -> kcenon::common::Result< std::string >
auto get_profile() const noexcept -> anonymization_profile
Get the current profile.
auto is_detailed_reporting() const noexcept -> bool
Check if detailed reporting is enabled.
bool detailed_reporting_
Whether to include detailed action records in report.
auto get_private_tag_action() const noexcept -> private_tag_action
Get the current private tag action.
auto remove_tag_action(core::dicom_tag tag) -> bool
Remove a custom tag action (reverts to profile default)
std::map< core::dicom_tag, tag_action_config > custom_actions_
Custom tag actions (override profile defaults)
auto anonymize(core::dicom_dataset &dataset) -> kcenon::common::Result< anonymization_report >
Anonymize a DICOM dataset.
auto operator=(const anonymizer &other) -> anonymizer &
Copy assignment.
void set_private_tag_action(private_tag_action action)
Set the action to take on private tags during anonymization.
auto get_or_create(std::string_view original_uid) -> kcenon::common::Result< std::string >
Get existing mapping or create new one.
Compile-time constants for commonly used DICOM tags.
DICOM de-identification/anonymization per PS3.15 Annex E.
auto get_all_identifier_tags() -> std::vector< core::dicom_tag >
Get all HIPAA identifier tags.
constexpr auto to_string(anonymization_profile profile) noexcept -> std::string_view
Convert profile enum to string representation.
anonymization_profile
DICOM de-identification profiles based on PS3.15 Annex E.
@ clean_pixel
Clean Pixel Data - Remove burned-in annotations.
@ hipaa_safe_harbor
HIPAA Safe Harbor - 18 identifier removal.
@ gdpr_compliant
GDPR Compliant - European data protection.
@ retain_longitudinal
Retain Longitudinal - Preserve temporal relationships.
@ retain_patient_characteristics
Retain Patient Characteristics.
@ clean_descriptions
Clean Descriptions - Sanitize text fields.
@ basic
Basic Profile - Remove direct identifiers.
@ hash
Hash the value for research linkage.
@ remove
D - Remove the attribute entirely.
@ keep
K - Keep the attribute unchanged.
@ replace_uid
U - Replace UIDs with new values.
@ encrypt
Encrypt the value.
@ shift_date
Shift dates by a fixed offset.
@ remove_or_empty
X - Remove or empty based on presence.
@ replace
C - Clean (replace with dummy value)
@ empty
Z - Replace with zero-length value.
private_tag_action
Action to take on private tags during anonymization.
@ keep
Preserve all private tags (default for backward compatibility)
@ remove_data
Remove private data elements but keep creators (for auditing)
@ remove_all
Remove all private data elements and their creators.
Report generated after anonymization.
std::string profile_name
Profile used for anonymization.
Configuration for a custom tag action.
static auto make_remove() -> tag_action_config
Create a remove action config.
static auto make_hash(std::string algorithm="SHA256", bool salt=true) -> tag_action_config
Create a hash action config.
static auto make_keep() -> tag_action_config
Create a keep action config.
static auto make_replace(std::string value) -> tag_action_config
Create a replace action config with a custom value.
static auto make_empty() -> tag_action_config
Create an empty action config.
Record of an action performed on a tag.