Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using FellowOakDicom;
using Microsoft.Health.Dicom.Anonymizer.Core.Models;
using Newtonsoft.Json.Linq;
using Xunit;

namespace Microsoft.Health.Dicom.Anonymizer.Core.UnitTests
{
public class RuntimeKeySettingsTests
{
[Fact]
public void GivenCryptoHashProcessor_WithRuntimeKey_ShouldProduceDifferentResult()
{
// Arrange
var dataset1 = new DicomDataset
{
{ DicomTag.PatientName, "John Doe" },
};

var dataset2 = new DicomDataset
{
{ DicomTag.PatientName, "John Doe" },
};

var config = CreateTestConfiguration();
var engine = new AnonymizerEngine(config);

var runtimeKey1 = new RuntimeKeySettings { CryptoHashKey = "key123" };
var runtimeKey2 = new RuntimeKeySettings { CryptoHashKey = "key456" };

// Act
engine.AnonymizeDataset(dataset1, runtimeKey1);
engine.AnonymizeDataset(dataset2, runtimeKey2);

// Assert
var result1 = dataset1.GetSingleValueOrDefault(DicomTag.PatientName, string.Empty);
var result2 = dataset2.GetSingleValueOrDefault(DicomTag.PatientName, string.Empty);

Assert.NotEqual(result1, result2);
Assert.NotEmpty(result1);
Assert.NotEmpty(result2);
}

[Fact]
public void GivenCryptoHashProcessor_WithSameRuntimeKey_ShouldProduceSameResult()
{
// Arrange
var dataset1 = new DicomDataset
{
{ DicomTag.PatientName, "John Doe" },
};

var dataset2 = new DicomDataset
{
{ DicomTag.PatientName, "John Doe" },
};

var config = CreateTestConfiguration();
var engine = new AnonymizerEngine(config);

var runtimeKey = new RuntimeKeySettings { CryptoHashKey = "key123" };

// Act
engine.AnonymizeDataset(dataset1, runtimeKey);
engine.AnonymizeDataset(dataset2, runtimeKey);

// Assert
var result1 = dataset1.GetSingleValueOrDefault(DicomTag.PatientName, string.Empty);
var result2 = dataset2.GetSingleValueOrDefault(DicomTag.PatientName, string.Empty);

Assert.Equal(result1, result2);
Assert.NotEmpty(result1);
}

[Fact]
public void GivenCryptoHashProcessor_WithoutRuntimeKey_ShouldUseConfigurationKey()
{
// Arrange
var dataset1 = new DicomDataset
{
{ DicomTag.PatientName, "John Doe" },
};

var dataset2 = new DicomDataset
{
{ DicomTag.PatientName, "John Doe" },
};

var config = CreateTestConfiguration();
var engine = new AnonymizerEngine(config);

// Act
engine.AnonymizeDataset(dataset1); // No runtime key
engine.AnonymizeDataset(dataset2, null); // Explicit null runtime key

// Assert
var result1 = dataset1.GetSingleValueOrDefault(DicomTag.PatientName, string.Empty);
var result2 = dataset2.GetSingleValueOrDefault(DicomTag.PatientName, string.Empty);

Assert.Equal(result1, result2);
Assert.NotEmpty(result1);
}

[Fact]
public void GivenDateShiftProcessor_WithRuntimeKey_ShouldProduceDifferentResult()
{
// Arrange
var dataset1 = new DicomDataset
{
{ DicomTag.StudyInstanceUID, "1.2.3.4.5.6" },
{ DicomTag.PatientBirthDate, "20000101" },
};

var dataset2 = new DicomDataset
{
{ DicomTag.StudyInstanceUID, "1.2.3.4.5.6" },
{ DicomTag.PatientBirthDate, "20000101" },
};

var config = CreateTestConfigurationWithDateShift();
var engine = new AnonymizerEngine(config);

var runtimeKey1 = new RuntimeKeySettings { DateShiftKey = "key123" };
var runtimeKey2 = new RuntimeKeySettings { DateShiftKey = "key456" };

// Act
engine.AnonymizeDataset(dataset1, runtimeKey1);
engine.AnonymizeDataset(dataset2, runtimeKey2);

// Assert
var result1 = dataset1.GetSingleValueOrDefault(DicomTag.PatientBirthDate, string.Empty);
var result2 = dataset2.GetSingleValueOrDefault(DicomTag.PatientBirthDate, string.Empty);

Assert.NotEqual(result1, result2);
Assert.NotEqual("20000101", result1); // Should be shifted
Assert.NotEqual("20000101", result2); // Should be shifted
}

private static AnonymizerConfigurationManager CreateTestConfiguration()
{
var config = new
{
rules = new[]
{
new { tag = "(0010,0010)", method = "cryptoHash" }, // PatientName
},
defaultSettings = new
{
cryptoHash = new { cryptoHashKey = "defaultKey123" },
},
};

var json = System.Text.Json.JsonSerializer.Serialize(config);
return AnonymizerConfigurationManager.CreateFromJson(json);
}

private static AnonymizerConfigurationManager CreateTestConfigurationWithDateShift()
{
var config = new
{
rules = new[]
{
new { tag = "(0010,0030)", method = "dateShift" }, // PatientBirthDate
},
defaultSettings = new
{
dateShift = new { dateShiftKey = "defaultDateKey123", dateShiftRange = 50 },
},
};

var json = System.Text.Json.JsonSerializer.Serialize(config);
return AnonymizerConfigurationManager.CreateFromJson(json);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ public AnonymizerEngine(AnonymizerConfigurationManager configurationManager, Ano
}

public void AnonymizeDataset(DicomDataset dataset)
{
AnonymizeDataset(dataset, null);
}

public void AnonymizeDataset(DicomDataset dataset, RuntimeKeySettings runtimeKeySettings)
{
EnsureArg.IsNotNull(dataset, nameof(dataset));

Expand All @@ -45,6 +50,7 @@ public void AnonymizeDataset(DicomDataset dataset)
}

var context = InitContext(dataset);
context.RuntimeKeys = runtimeKeySettings;
DicomUtility.DisableAutoValidation(dataset);

foreach (var rule in _rules)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,10 @@ public class ProcessContext
public string SeriesInstanceUID { get; set; }

public string SopInstanceUID { get; set; }

/// <summary>
/// Gets or sets optional runtime key settings that can override configuration-based keys.
/// </summary>
public RuntimeKeySettings RuntimeKeys { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

namespace Microsoft.Health.Dicom.Anonymizer.Core.Models
{
/// <summary>
/// Provides optional runtime key values that can override configuration-based keys
/// when de-identifying a specific dataset.
/// </summary>
public class RuntimeKeySettings
{
/// <summary>
/// Gets or sets the runtime key for cryptographic hashing operations.
/// If null, the processor will use the key from configuration.
/// </summary>
public string CryptoHashKey { get; set; }

/// <summary>
/// Gets or sets the runtime key for date shifting operations.
/// If null, the processor will use the key from configuration.
/// </summary>
public string DateShiftKey { get; set; }

/// <summary>
/// Gets or sets the runtime key for encryption operations.
/// If null, the processor will use the key from configuration.
/// </summary>
public string EncryptKey { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,31 +26,35 @@ namespace Microsoft.Health.Dicom.Anonymizer.Core.Processors
public class CryptoHashProcessor : IAnonymizerProcessor
{
private readonly CryptoHashFunction _cryptoHashFunction;
private readonly CryptoHashSetting _cryptoHashSetting;
private readonly ILogger _logger = AnonymizerLogging.CreateLogger<CryptoHashProcessor>();

public CryptoHashProcessor(JObject settingObject)
{
EnsureArg.IsNotNull(settingObject, nameof(settingObject));

var settingFactory = new AnonymizerSettingsFactory();
var cryptoHashSetting = settingFactory.CreateAnonymizerSetting<CryptoHashSetting>(settingObject);
_cryptoHashFunction = new CryptoHashFunction(cryptoHashSetting);
_cryptoHashSetting = settingFactory.CreateAnonymizerSetting<CryptoHashSetting>(settingObject);
_cryptoHashFunction = new CryptoHashFunction(_cryptoHashSetting);
}

public void Process(DicomDataset dicomDataset, DicomItem item, ProcessContext context = null)
{
EnsureArg.IsNotNull(dicomDataset, nameof(dicomDataset));
EnsureArg.IsNotNull(item, nameof(item));

// Use runtime key if available, otherwise use configuration key
var cryptoHashFunction = GetCryptoHashFunction(context);

if (item is DicomStringElement)
{
var hashedValues = ((DicomStringElement)item).Get<string[]>().Select(GetCryptoHashString);
var hashedValues = ((DicomStringElement)item).Get<string[]>().Select(value => GetCryptoHashString(value, cryptoHashFunction));
dicomDataset.AddOrUpdate(item.ValueRepresentation, item.Tag, hashedValues.ToArray());
}
else if (item is DicomOtherByte)
{
var valueBytes = ((DicomOtherByte)item).Get<byte[]>();
var hashedBytes = _cryptoHashFunction.Hash(valueBytes);
var hashedBytes = cryptoHashFunction.Hash(valueBytes);
dicomDataset.AddOrUpdate(item.ValueRepresentation, item.Tag, hashedBytes);
}
else if (item is DicomFragmentSequence)
Expand All @@ -61,7 +65,7 @@ public void Process(DicomDataset dicomDataset, DicomItem item, ProcessContext co

foreach (var fragment in (DicomFragmentSequence)item)
{
element.Fragments.Add(new MemoryByteBuffer(_cryptoHashFunction.Hash(fragment.Data)));
element.Fragments.Add(new MemoryByteBuffer(cryptoHashFunction.Hash(fragment.Data)));
}

dicomDataset.AddOrUpdate(element);
Expand All @@ -87,5 +91,30 @@ public string GetCryptoHashString(string input)

return _cryptoHashFunction.Hash(input);
}

private CryptoHashFunction GetCryptoHashFunction(ProcessContext context)
{
// If runtime keys are provided and contain a crypto hash key, use it
if (context?.RuntimeKeys?.CryptoHashKey != null)
{
var runtimeSetting = new CryptoHashSetting
{
CryptoHashKey = context.RuntimeKeys.CryptoHashKey,
CryptoHashType = _cryptoHashSetting.CryptoHashType,
MatchInputStringLength = _cryptoHashSetting.MatchInputStringLength,
};
return new CryptoHashFunction(runtimeSetting);
}

// Fall back to configuration-based function
return _cryptoHashFunction;
}

private string GetCryptoHashString(string input, CryptoHashFunction cryptoHashFunction)
{
EnsureArg.IsNotNull(input, nameof(input));

return cryptoHashFunction.Hash(input);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ namespace Microsoft.Health.Dicom.Anonymizer.Core.Processors
public class DateShiftProcessor : IAnonymizerProcessor
{
private readonly DateShiftFunction _dateShiftFunction;
private readonly DateShiftSetting _dateShiftSetting;
private readonly DateShiftScope _dateShiftScope = DateShiftScope.SopInstance;
private readonly ILogger _logger = AnonymizerLogging.CreateLogger<DateShiftProcessor>();

Expand All @@ -34,8 +35,8 @@ public DateShiftProcessor(JObject settingObject)
EnsureArg.IsNotNull(settingObject, nameof(settingObject));

var settingFactory = new AnonymizerSettingsFactory();
var dateShiftSetting = settingFactory.CreateAnonymizerSetting<DateShiftSetting>(settingObject);
_dateShiftFunction = new DateShiftFunction(dateShiftSetting);
_dateShiftSetting = settingFactory.CreateAnonymizerSetting<DateShiftSetting>(settingObject);
_dateShiftFunction = new DateShiftFunction(_dateShiftSetting);
if (settingObject.TryGetValue("DateShiftScope", StringComparison.OrdinalIgnoreCase, out JToken scope))
{
_dateShiftScope = (DateShiftScope)Enum.Parse(typeof(DateShiftScope), scope.ToString(), true);
Expand All @@ -48,7 +49,10 @@ public void Process(DicomDataset dicomDataset, DicomItem item, ProcessContext co
EnsureArg.IsNotNull(item, nameof(item));
EnsureArg.IsNotNull(context, nameof(context));

_dateShiftFunction.SetDateShiftPrefix(_dateShiftScope switch
// Use runtime key if available, otherwise use configuration key
var dateShiftFunction = GetDateShiftFunction(context);

dateShiftFunction.SetDateShiftPrefix(_dateShiftScope switch
{
DateShiftScope.StudyInstance => context.StudyInstanceUID ?? string.Empty,
DateShiftScope.SeriesInstance => context.SeriesInstanceUID ?? string.Empty,
Expand All @@ -59,7 +63,7 @@ public void Process(DicomDataset dicomDataset, DicomItem item, ProcessContext co
{
var values = DicomUtility.ParseDicomDate((DicomDate)item)
.Where(x => !DateTimeUtility.IsAgeOverThreshold(x)) // Age over 89 will be redacted.
.Select(_dateShiftFunction.Shift);
.Select(dateShiftFunction.Shift);

dicomDataset.AddOrUpdate(item.ValueRepresentation, item.Tag, values.Select(DicomUtility.GenerateDicomDateString).Where(x => x != null).ToArray());
}
Expand All @@ -71,7 +75,7 @@ public void Process(DicomDataset dicomDataset, DicomItem item, ProcessContext co
{
if (!DateTimeUtility.IsAgeOverThreshold(dateObject.DateValue))
{
dateObject.DateValue = _dateShiftFunction.Shift(dateObject.DateValue);
dateObject.DateValue = dateShiftFunction.Shift(dateObject.DateValue);
results.Add(DicomUtility.GenerateDicomDateTimeString(dateObject));
}
}
Expand All @@ -92,5 +96,23 @@ public bool IsSupported(DicomItem item)

return DicomDataModel.DateShiftSupportedVR.Contains(item.ValueRepresentation);
}

private DateShiftFunction GetDateShiftFunction(ProcessContext context)
{
// If runtime keys are provided and contain a date shift key, use it
if (context?.RuntimeKeys?.DateShiftKey != null)
{
var runtimeSetting = new DateShiftSetting
{
DateShiftKey = context.RuntimeKeys.DateShiftKey,
DateShiftRange = _dateShiftSetting.DateShiftRange,
DateShiftKeyPrefix = _dateShiftSetting.DateShiftKeyPrefix,
};
return new DateShiftFunction(runtimeSetting);
}

// Fall back to configuration-based function
return _dateShiftFunction;
}
}
}
Loading