Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
Expand Up @@ -256,7 +256,7 @@ public void TestValidate_CryptoHashKey_AboveMinimum_DoesNotThrow()
}

// -----------------------------------------------------------------------
// DateShiftKey + DateShiftScope validation — new tests
// DateShiftKey + DateShiftScope validation
// -----------------------------------------------------------------------

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,135 +3,25 @@
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>f7a47fd1-bd24-41a6-b3c6-2dd062523271</SharedGUID>
<SharedGUID>a4d6c14a-30bf-4a7e-8421-c4cb3e8e7123</SharedGUID>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>Fhir.Anonymizer.Core.UnitTests</Import_RootNamespace>
<Import_RootNamespace>Fhir.Anonymizer.Shared.Core.UnitTests</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)AnonymizerConfigurations\AnonymizationFhirPathRuleTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)AnonymizerConfigurations\AnonymizerConfigurationManagerTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)AnonymizerConfigurations\AnonymizerConfigurationTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)AnonymizerConfigurations\AnonymizerConfigurationValidatorTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)AnonymizerConfigurations\ParameterConfigurationTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)EmptyElementTest.cs" />
<Compile Include="$(MSBuildThisFileDirectory)AnonymizerEngineTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\TypedElementNavExtensionsTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\ElementNodeVisitorExtensionsTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\ElementNodeOperationExtensionsTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\FhirPathSymbolExtensionsTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)MaskProcessor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)MockAnonymizerProcessor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)PartitionedExecution\FhirEnumerableReaderTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)PartitionedExecution\FhirPartitionedExecutionTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)PartitionedExecution\FhirStreamConsumerTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)PartitionedExecution\FhirStreamReaderTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Processors\CryptoHashProcessorTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Processors\CustomProcessorFactoryUnitTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Processors\DateShiftProcessorTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Processors\EncryptProcessorTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Processors\GeneralizeProcessorTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Processors\PerturbProcessorTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Processors\RedactProcessorTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Processors\ResourceProcessorTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Processors\Settings\GeneralizeSettingTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Processors\Settings\SubstituteSettingTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Processors\Settings\PerturbSettingTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Processors\SubstituteProcessorTests.cs" />
<None Include="$(MSBuildThisFileDirectory)TestConfigurations\configuration-generalize-fail-compiled-expression.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)TestConfigurations\configuration-generalize-invalid-othervalues.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)TestConfigurations\configuration-generalize-miss-cases.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)TestConfigurations\configuration-perturb-negative-span.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)TestConfigurations\configuration-perturb-miss-span.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)TestConfigurations\configuration-perturb-negative-roundTo.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)TestConfigurations\configuration-perturb-exceed-28-roundTo.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)TestConfigurations\configuration-perturb-wrong-roundTo.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)TestConfigurations\configuration-perturb-wrong-rangetype.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)TestConfigurations\configuration-miss-replacement.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<Compile Include="$(MSBuildThisFileDirectory)Utility\CryptoHashUtilityTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Utility\DateTimeUtilityTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Utility\EncryptUtilityTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Utility\PostalCodeUtilityTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Utility\ReferenceUtilityTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Validation\AttributeValidatorTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Visitors\AnonymizationVisitorTests.cs" />
<None Include="$(MSBuildThisFileDirectory)TestConfigurations\configuration-raise-processing-error.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)TestConfigurations\configuration-custom-processor.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)TestConfigurations\configuration-without-processing-error.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<None Include="$(MSBuildThisFileDirectory)TestConfigurationsVersion\configuration-empty-version.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)TestConfigurationsVersion\configuration-invalid-version.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)TestConfigurationsVersion\configuration-null-version.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)TestConfigurationsVersion\configuration-R4-version.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)TestConfigurationsVersion\configuration-Stu3-version.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)TestConfigurations\configuration-invalid-fhirpath.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)TestConfigurations\configuration-miss-rules.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)TestConfigurations\configuration-test-sample.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)TestConfigurations\configuration-unsupported-method.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)TestResources\bundle-basic.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)TestResources\contained-basic.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Content Include="$(MSBuildThisFileDirectory)TestConfigurations\configuration-invalid-encryptkey.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)TestResources\bundle-empty.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)TestResources\condition-empty.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)TestResources\patient-empty.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Health.Fhir.Anonymizer.Core.Exceptions;

namespace Microsoft.Health.Fhir.Anonymizer.Core.AnonymizerConfigurations
{
/// <summary>
/// Static utility class that encapsulates all cryptographic key security validation logic
/// for anonymization configuration parameters.
///
/// SECURITY: This class enforces that keys are not placeholder values, not whitespace-only,
/// not obviously weak values, and meet minimum length/size requirements.
/// </summary>
public static class CryptographicKeyValidator
{
private static readonly ILogger s_logger = AnonymizerLogging.LoggerFactory.CreateLogger(typeof(CryptographicKeyValidator).FullName);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security (medium): The static logger in CryptographicKeyValidator is created with the wrong generic type argument: AnonymizerLogging.CreateLoggerParameterConfiguration(). This means all log entries from CryptographicKeyValidator will be categorized under the 'ParameterConfiguration' logger category. While not a direct data-exposure vulnerability, this creates misleading audit/log trails — security-related key validation warnings will appear attributed to ParameterConfiguration rather than CryptographicKeyValida...

💡 Change to AnonymizerLogging.CreateLoggerCryptographicKeyValidator() so log categories correctly reflect the source component.

/// <summary>
/// Valid AES key sizes in bits. Used to validate EncryptKey without allocating an Aes instance.
/// AES supports 128-bit (16 bytes), 192-bit (24 bytes), and 256-bit (32 bytes) keys.
/// </summary>
private static readonly HashSet<int> s_validAesKeySizeBits = new HashSet<int> { 128, 192, 256 };

/// <summary>
/// Dangerous placeholder patterns that must be rejected.
/// </summary>
private static readonly string[] s_dangerousPlaceholderPatterns = new[]
{
"$HMAC_KEY",
"YOUR_KEY_HERE",
"YOUR_SECURE_KEY",
"YOUR_ENCRYPTION_KEY",
"PLACEHOLDER",
"CHANGE_ME",
"CHANGEME",
"REPLACE_ME",
"EXAMPLE_KEY",
"TEST_KEY",
"SAMPLE_KEY",
"INSERT_KEY_HERE",
"<YOUR_KEY>",
"[YOUR_KEY]",
"{{YOUR_KEY}}",
"TODO",
"FIXME"
};

/// <summary>
/// Validate a key parameter doesn't contain placeholder values or consist solely of whitespace.
/// SECURITY CRITICAL: Prevents use of example/template keys and whitespace-only values in production.
/// </summary>
/// <param name="keyValue">The key value to validate.</param>
/// <param name="parameterName">The configuration parameter name (for error messages).</param>
/// <param name="keyType">Human-readable key type description (for error messages).</param>
public static void ValidateKeyParameter(string keyValue, string parameterName, string keyType)
{
if (string.IsNullOrEmpty(keyValue))
{
return; // Empty/null keys are allowed if the feature is not used
}

// SECURITY: Reject whitespace-only keys — they provide no entropy
if (string.IsNullOrWhiteSpace(keyValue))
{
throw new SecurityException(
$"SECURITY ERROR: Whitespace-only {keyType} key detected in '{parameterName}'. " +
"A key consisting entirely of whitespace characters provides no entropy and must not be used. " +
"Generate a cryptographically secure random key using: openssl rand -base64 32");
}

// Trim and convert to uppercase for case-insensitive comparison
var normalizedKey = keyValue.Trim().ToUpperInvariant();

// Check against all dangerous placeholder patterns
foreach (var pattern in s_dangerousPlaceholderPatterns)
{
if (normalizedKey.Contains(pattern))
{
throw new SecurityException(
$"SECURITY ERROR: Placeholder {keyType} key detected in '{parameterName}'.\n\n" +
$"The configuration contains a placeholder value ('{pattern}') that must be replaced " +
"with a cryptographically secure key before use.\n\n" +
"TO GENERATE A SECURE KEY:\n" +
" Linux/macOS: openssl rand -base64 32\n" +
" Windows: pwsh -Command \"[Convert]::ToBase64String((1..32 | ForEach-Object { Get-Random -Minimum 0 -Maximum 256 }))\"\n" +
" .NET: var key = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));\n\n" +
"SECURITY WARNING: Using placeholder keys in production:\n" +
" - Compromises cryptographic operations\n" +
" - May lead to predictable hash values\n" +
" - Enables re-identification attacks\n" +
" - Violates privacy guarantees\n\n" +
"BEST PRACTICES:\n" +
" - Never commit actual keys to version control\n" +
" - Use environment variables: Environment.GetEnvironmentVariable(\"CRYPTO_KEY\")\n" +
" - Use Azure Key Vault, AWS Secrets Manager, or similar for production\n" +
" - Rotate keys periodically according to your security policy\n" +
" - Use different keys for different environments (dev/staging/production)\n");
}
}

// Additional checks for weak or test keys
if (keyValue.Length < 16)
{
s_logger.LogWarning(
$"The {keyType} key in '{parameterName}' is very short ({keyValue.Length} characters). " +
"Recommended minimum is 32 bytes (44 characters in Base64). " +
"Short keys provide inadequate security and may be vulnerable to brute force attacks.");
}

// Check for obviously weak patterns
if (keyValue.Equals("12345678", StringComparison.Ordinal) ||
keyValue.Equals("password", StringComparison.OrdinalIgnoreCase) ||
keyValue.Equals("secret", StringComparison.OrdinalIgnoreCase) ||
keyValue.Equals("key", StringComparison.OrdinalIgnoreCase) ||
keyValue.All(c => c == keyValue[0])) // All same character
{
throw new SecurityException(
$"SECURITY ERROR: Weak {keyType} key detected in '{parameterName}'. " +
"The key appears to be a common weak value (e.g., 'password', '12345678', repeated characters). " +
"Generate a cryptographically secure random key using: openssl rand -base64 32");
}
}

/// <summary>
/// Validate that the encrypt key size is a valid AES key size (128, 192, or 256 bits).
/// Uses a static HashSet of valid sizes to avoid allocating an Aes instance on every call.
/// Only validates when encryptKey is non-null and non-empty.
/// </summary>
/// <param name="encryptKey">The encryption key string to validate.</param>
public static void ValidateEncryptKeySize(string encryptKey)
{
if (string.IsNullOrEmpty(encryptKey))
{
return;
}

var encryptKeySize = Encoding.UTF8.GetByteCount(encryptKey) * 8;
if (!s_validAesKeySizeBits.Contains(encryptKeySize))
{
throw new AnonymizerConfigurationException(
$"Invalid encrypt key size : {encryptKeySize} bits! Please provide key sizes of 128, 192 or 256 bits.");
}
}
}
}
Loading
Loading