From b4bd4f0e55131b0a63270d8f49e8a9cdc9874f4e Mon Sep 17 00:00:00 2001 From: sxndmxn Date: Tue, 13 Jan 2026 10:49:05 -0800 Subject: [PATCH 1/3] Add FIPS-compliant hash algorithm support Add configurable hash algorithms (SHA-256, SHA-384, SHA-512) for FIPS compliance while maintaining backward compatibility with existing MD5 checksums through dual validation. Changes: - Add HashAlgorithmType enum (MD5, SHA256, SHA384, SHA512) - Add ChecksumCalculator utility for centralized hash calculation - Add ChecksumAlgorithm configuration property (default: MD5) - Implement dual validation in MigrationScript.ValidateChecksum() - Expand metadata table checksum column from VARCHAR(32) to VARCHAR(128) - Add --checksum-algorithm CLI option - Update target framework to .NET 10 --- src/Evolve.Cli/Evolve.Cli.csproj | 2 +- src/Evolve.Cli/EvolveFactory.cs | 1 + src/Evolve.Cli/Program.cs | 4 + src/Evolve.Tool/Evolve.Tool.csproj | 2 +- src/Evolve/Configuration/HashAlgorithmType.cs | 32 ++++++ .../Configuration/IEvolveConfiguration.cs | 16 ++- .../CockroachDb/CockroachDbMetadataTable.cs | 2 +- .../Dialect/MySQL/MySQLMetadataTable.cs | 2 +- .../PostgreSQL/PostgreSQLMetadataTable.cs | 2 +- .../SQLServer/SQLServerMetadataTable.cs | 2 +- .../Dialect/SQLite/SQLiteMetadataTable.cs | 2 +- src/Evolve/Evolve.cs | 2 + src/Evolve/Evolve.csproj | 2 +- .../EmbeddedResourceMigrationLoader.cs | 12 +- src/Evolve/Migration/FileMigrationLoader.cs | 12 +- src/Evolve/Migration/FileMigrationScript.cs | 24 ++-- src/Evolve/Migration/MigrationScript.cs | 56 +++++++-- src/Evolve/Utilities/ChecksumCalculator.cs | 90 +++++++++++++++ test/Evolve.Tests/Evolve.Tests.csproj | 2 +- test/Evolve.Tests/EvolveConfiguration.cs | 1 + .../Migration/FileMigrationScriptTest.cs | 70 ++++++++++++ .../Utilities/ChecksumCalculatorTest.cs | 107 ++++++++++++++++++ 22 files changed, 416 insertions(+), 29 deletions(-) create mode 100644 src/Evolve/Configuration/HashAlgorithmType.cs create mode 100644 src/Evolve/Utilities/ChecksumCalculator.cs create mode 100644 test/Evolve.Tests/Utilities/ChecksumCalculatorTest.cs diff --git a/src/Evolve.Cli/Evolve.Cli.csproj b/src/Evolve.Cli/Evolve.Cli.csproj index 8a1937ed..0c3d7be0 100644 --- a/src/Evolve.Cli/Evolve.Cli.csproj +++ b/src/Evolve.Cli/Evolve.Cli.csproj @@ -3,7 +3,7 @@ Exe - net9.0 + net10.0 true EvolveDb.Cli diff --git a/src/Evolve.Cli/EvolveFactory.cs b/src/Evolve.Cli/EvolveFactory.cs index e384f288..ad1688e4 100644 --- a/src/Evolve.Cli/EvolveFactory.cs +++ b/src/Evolve.Cli/EvolveFactory.cs @@ -45,6 +45,7 @@ public static Evolve Build(Program options, Action logInfoDelegate = nul RetryRepeatableMigrationsUntilNoError = options.RetryRepeatableMigrationsUntilNoError, TransactionMode = options.TransactionMode, SkipNextMigrations = options.SkipNextMigrations, + ChecksumAlgorithm = options.ChecksumAlgorithm, }; if (options.Placeholders != null) diff --git a/src/Evolve.Cli/Program.cs b/src/Evolve.Cli/Program.cs index 58df45cc..898e63e8 100644 --- a/src/Evolve.Cli/Program.cs +++ b/src/Evolve.Cli/Program.cs @@ -129,6 +129,10 @@ private int OnExecute(CommandLineApplication app, IConsole console) [Option("--skip-next-migrations", "When set, mark all subsequent migrations as applied. Default: false", CommandOptionType.SingleValue)] public bool SkipNextMigrations { get; } + [Option("--checksum-algorithm", "Hash algorithm for migration checksums. SHA-256, SHA-384, SHA-512 are FIPS-compliant. Default: MD5", CommandOptionType.SingleValue)] + [AllowedValues("MD5", "SHA256", "SHA384", "SHA512", IgnoreCase = true)] + public HashAlgorithmType ChecksumAlgorithm { get; } = Default.ChecksumAlgorithm; + // Cassandra [Option("--keyspace", "A list of keyspaces managed by Evolve (Cassandra only).", CommandOptionType.MultipleValue)] public string[] Keyspaces { get; } diff --git a/src/Evolve.Tool/Evolve.Tool.csproj b/src/Evolve.Tool/Evolve.Tool.csproj index 9141e82a..257054d2 100644 --- a/src/Evolve.Tool/Evolve.Tool.csproj +++ b/src/Evolve.Tool/Evolve.Tool.csproj @@ -3,7 +3,7 @@ Exe - net9.0 + net10.0 true evolve EvolveDb.Tool diff --git a/src/Evolve/Configuration/HashAlgorithmType.cs b/src/Evolve/Configuration/HashAlgorithmType.cs new file mode 100644 index 00000000..413098cb --- /dev/null +++ b/src/Evolve/Configuration/HashAlgorithmType.cs @@ -0,0 +1,32 @@ +namespace EvolveDb.Configuration +{ + /// + /// Hash algorithms supported for migration checksum calculation. + /// + public enum HashAlgorithmType + { + /// + /// MD5 hash algorithm (32 character hex output). + /// Not FIPS-compliant. + /// + MD5, + + /// + /// SHA-256 hash algorithm (64 character hex output). + /// FIPS-compliant. + /// + SHA256, + + /// + /// SHA-384 hash algorithm (96 character hex output). + /// FIPS-compliant. + /// + SHA384, + + /// + /// SHA-512 hash algorithm (128 character hex output). + /// FIPS-compliant. + /// + SHA512 + } +} diff --git a/src/Evolve/Configuration/IEvolveConfiguration.cs b/src/Evolve/Configuration/IEvolveConfiguration.cs index aad44a61..22b52884 100644 --- a/src/Evolve/Configuration/IEvolveConfiguration.cs +++ b/src/Evolve/Configuration/IEvolveConfiguration.cs @@ -214,9 +214,23 @@ public interface IEvolveConfiguration bool SkipNextMigrations { get; } /// - /// A custom used to load all migrations (applied, pending, ignored...) + /// A custom used to load all migrations (applied, pending, ignored...) /// that replaces the built-in ones ( ) /// IMigrationLoader MigrationLoader { get; } + + /// + /// + /// Hash algorithm used for calculating migration checksums. (default: MD5) + /// + /// + /// SHA-256, SHA-384, and SHA-512 are FIPS-compliant alternatives. + /// + /// + /// Note: When validating checksums, Evolve will accept both the configured + /// algorithm and MD5 for backward compatibility with existing databases. + /// + /// + HashAlgorithmType ChecksumAlgorithm { get; } } } diff --git a/src/Evolve/Dialect/CockroachDb/CockroachDbMetadataTable.cs b/src/Evolve/Dialect/CockroachDb/CockroachDbMetadataTable.cs index 4f3152e3..0e6e2c0e 100644 --- a/src/Evolve/Dialect/CockroachDb/CockroachDbMetadataTable.cs +++ b/src/Evolve/Dialect/CockroachDb/CockroachDbMetadataTable.cs @@ -86,7 +86,7 @@ protected override void InternalCreate() "version VARCHAR(50), " + "description VARCHAR(200) NOT NULL, " + "name VARCHAR(300) NOT NULL, " + - "checksum VARCHAR(32), " + + "checksum VARCHAR(128), " + "installed_by VARCHAR(100) NOT NULL, " + "installed_on TIMESTAMP NOT NULL DEFAULT now(), " + "success BOOLEAN NOT NULL " + diff --git a/src/Evolve/Dialect/MySQL/MySQLMetadataTable.cs b/src/Evolve/Dialect/MySQL/MySQLMetadataTable.cs index c1fe244e..4c974d43 100644 --- a/src/Evolve/Dialect/MySQL/MySQLMetadataTable.cs +++ b/src/Evolve/Dialect/MySQL/MySQLMetadataTable.cs @@ -43,7 +43,7 @@ protected override void InternalCreate() "version VARCHAR(50), " + "description VARCHAR(200) NOT NULL, " + "name VARCHAR(300) NOT NULL, " + - "checksum VARCHAR(32), " + + "checksum VARCHAR(128), " + "installed_by VARCHAR(100) NOT NULL, " + "installed_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + "success BOOL NOT NULL " + diff --git a/src/Evolve/Dialect/PostgreSQL/PostgreSQLMetadataTable.cs b/src/Evolve/Dialect/PostgreSQL/PostgreSQLMetadataTable.cs index 635ddcd2..23cadb09 100644 --- a/src/Evolve/Dialect/PostgreSQL/PostgreSQLMetadataTable.cs +++ b/src/Evolve/Dialect/PostgreSQL/PostgreSQLMetadataTable.cs @@ -44,7 +44,7 @@ protected override void InternalCreate() "version VARCHAR(50), " + "description VARCHAR(200) NOT NULL, " + "name VARCHAR(300) NOT NULL, " + - "checksum VARCHAR(32), " + + "checksum VARCHAR(128), " + "installed_by VARCHAR(100) NOT NULL, " + "installed_on TIMESTAMP NOT NULL DEFAULT now(), " + "success BOOLEAN NOT NULL " + diff --git a/src/Evolve/Dialect/SQLServer/SQLServerMetadataTable.cs b/src/Evolve/Dialect/SQLServer/SQLServerMetadataTable.cs index b2fd82f3..3c493471 100644 --- a/src/Evolve/Dialect/SQLServer/SQLServerMetadataTable.cs +++ b/src/Evolve/Dialect/SQLServer/SQLServerMetadataTable.cs @@ -43,7 +43,7 @@ protected override void InternalCreate() "version VARCHAR(50), " + "description VARCHAR(200) NOT NULL, " + "name VARCHAR(300) NOT NULL, " + - "checksum VARCHAR(32), " + + "checksum VARCHAR(128), " + "installed_by VARCHAR(100) NOT NULL, " + "installed_on DATETIME NOT NULL DEFAULT GETDATE(), " + "success BIT NOT NULL " + diff --git a/src/Evolve/Dialect/SQLite/SQLiteMetadataTable.cs b/src/Evolve/Dialect/SQLite/SQLiteMetadataTable.cs index 47ea2b8c..6648ef0f 100644 --- a/src/Evolve/Dialect/SQLite/SQLiteMetadataTable.cs +++ b/src/Evolve/Dialect/SQLite/SQLiteMetadataTable.cs @@ -41,7 +41,7 @@ protected override void InternalCreate() "version VARCHAR(50), " + "description VARCHAR(200) NOT NULL, " + "name VARCHAR(300) NOT NULL, " + - "checksum VARCHAR(32), " + + "checksum VARCHAR(128), " + "installed_by VARCHAR(100) NOT NULL, " + "installed_on TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f','now')), " + "success BOOLEAN NOT NULL " + diff --git a/src/Evolve/Evolve.cs b/src/Evolve/Evolve.cs index 64510adb..df77b515 100644 --- a/src/Evolve/Evolve.cs +++ b/src/Evolve/Evolve.cs @@ -90,6 +90,8 @@ public IMigrationLoader MigrationLoader set { _migrationLoader = value; } } + public HashAlgorithmType ChecksumAlgorithm { get; set; } = HashAlgorithmType.MD5; + #endregion #region Properties diff --git a/src/Evolve/Evolve.csproj b/src/Evolve/Evolve.csproj index 1fbeeaec..154d9936 100644 --- a/src/Evolve/Evolve.csproj +++ b/src/Evolve/Evolve.csproj @@ -2,7 +2,7 @@ - net9.0 + net10.0 true true latest diff --git a/src/Evolve/Migration/EmbeddedResourceMigrationLoader.cs b/src/Evolve/Migration/EmbeddedResourceMigrationLoader.cs index 6e31edb8..4af29677 100644 --- a/src/Evolve/Migration/EmbeddedResourceMigrationLoader.cs +++ b/src/Evolve/Migration/EmbeddedResourceMigrationLoader.cs @@ -58,7 +58,11 @@ public virtual IEnumerable GetMigrations() encoding); }) .ToList() - .ForEach(x => migrations.Add(x)); + .ForEach(x => + { + x.HashAlgorithm = _options.ChecksumAlgorithm; + migrations.Add(x); + }); } return migrations.CheckForDuplicateVersion() @@ -100,7 +104,11 @@ public virtual IEnumerable GetRepeatableMigrations() encoding); }) .ToList() - .ForEach(x => migrations.Add(x)); + .ForEach(x => + { + x.HashAlgorithm = _options.ChecksumAlgorithm; + migrations.Add(x); + }); } return migrations.CheckForDuplicateName() diff --git a/src/Evolve/Migration/FileMigrationLoader.cs b/src/Evolve/Migration/FileMigrationLoader.cs index d1b70473..9e6d9360 100644 --- a/src/Evolve/Migration/FileMigrationLoader.cs +++ b/src/Evolve/Migration/FileMigrationLoader.cs @@ -59,7 +59,11 @@ public virtual IEnumerable GetMigrations() return new FileMigrationScript(path: f.FullName, version, description, MetadataType.Migration, encoding); }) .ToList() - .ForEach(x => migrations.Add(x)); + .ForEach(x => + { + x.HashAlgorithm = _options.ChecksumAlgorithm; + migrations.Add(x); + }); } return migrations.CheckForDuplicateVersion() @@ -100,7 +104,11 @@ public virtual IEnumerable GetRepeatableMigrations() return new FileMigrationScript(f.FullName, version: null, description, MetadataType.RepeatableMigration, encoding); }) .ToList() - .ForEach(x => migrations.Add(x)); + .ForEach(x => + { + x.HashAlgorithm = _options.ChecksumAlgorithm; + migrations.Add(x); + }); } return migrations.CheckForDuplicateName() diff --git a/src/Evolve/Migration/FileMigrationScript.cs b/src/Evolve/Migration/FileMigrationScript.cs index ef947d6e..39db6e91 100644 --- a/src/Evolve/Migration/FileMigrationScript.cs +++ b/src/Evolve/Migration/FileMigrationScript.cs @@ -1,7 +1,7 @@ using System; using System.IO; -using System.Security.Cryptography; using System.Text; +using EvolveDb.Configuration; using EvolveDb.Metadata; using EvolveDb.Utilities; @@ -44,22 +44,32 @@ public override void ValidateChecksum(string? checksum) } catch { - if (checksum != FallbackCheck()) + // Try fallback methods for backward compatibility (pre v1.8.0 hashing of file stream) + if (checksum == FallbackCheck() || + checksum == FallbackCheckWithAlgorithm(HashAlgorithm)) { - throw; + return; } + throw; } } /// - /// Calculate the checksum with the pre v1.8.0 version. + /// Calculate the checksum with the pre v1.8.0 version (reads file directly with MD5). /// private string FallbackCheck() { - using var md5 = MD5.Create(); + return FallbackCheckWithAlgorithm(HashAlgorithmType.MD5); + } + + /// + /// Calculate the checksum with the pre v1.8.0 version using specified algorithm. + /// + /// The hash algorithm to use. + private string FallbackCheckWithAlgorithm(HashAlgorithmType algorithm) + { using FileStream stream = File.OpenRead(Path); - byte[] checksum = md5.ComputeHash(stream); - return BitConverter.ToString(checksum).Replace("-", string.Empty); + return ChecksumCalculator.Calculate(stream, algorithm); } } } diff --git a/src/Evolve/Migration/MigrationScript.cs b/src/Evolve/Migration/MigrationScript.cs index efa4ff3f..8f838282 100644 --- a/src/Evolve/Migration/MigrationScript.cs +++ b/src/Evolve/Migration/MigrationScript.cs @@ -1,7 +1,7 @@ using System; using System.IO; -using System.Security.Cryptography; using System.Text; +using EvolveDb.Configuration; using EvolveDb.Metadata; using EvolveDb.Utilities; @@ -25,6 +25,11 @@ protected MigrationScript(string? version, string description, string name, stri /// public string Content { get; } + /// + /// Gets or sets the hash algorithm used for checksum calculation. + /// + internal HashAlgorithmType HashAlgorithm { get; set; } = HashAlgorithmType.MD5; + /// /// Returns false if the special comment "evolve-tx-off" is found in the first line of the script, true otherwise. /// @@ -37,7 +42,7 @@ protected MigrationScript(string? version, string description, string name, stri /// /// Validates the against the actual migration one. - /// Throws on mismatch. + /// Supports dual validation: accepts both the configured algorithm and MD5 for backward compatibility. /// /// The applied migration checksum. /// @@ -45,21 +50,56 @@ public virtual void ValidateChecksum(string? checksum) { Check.NotNull(checksum, nameof(checksum)); - if (checksum != CalculateChecksum()) + string currentChecksum = CalculateChecksum(); + + // Direct match with configured algorithm + if (checksum == currentChecksum) + { + return; + } + + // Dual validation: try MD5 for backward compatibility if using SHA-2 + if (HashAlgorithm != HashAlgorithmType.MD5) + { + string md5Checksum = CalculateChecksumWithAlgorithm(HashAlgorithmType.MD5); + if (checksum == md5Checksum) + { + return; + } + } + + // Detect stored algorithm and recalculate if different + var detectedAlgorithm = ChecksumCalculator.DetectAlgorithm(checksum); + if (detectedAlgorithm.HasValue && detectedAlgorithm.Value != HashAlgorithm) { - throw new EvolveValidationException(string.Format(IncorrectMigrationChecksum, Name)); + string recalculated = CalculateChecksumWithAlgorithm(detectedAlgorithm.Value); + if (checksum == recalculated) + { + return; + } } + + throw new EvolveValidationException(string.Format(IncorrectMigrationChecksum, Name)); } /// /// Returns the checksum where crlf and lf line endings have been previously normalized to lf. /// - /// + /// The checksum calculated with the configured hash algorithm. public virtual string CalculateChecksum() { - using var md5 = MD5.Create(); - byte[] checksum = md5.ComputeHash(Encoding.UTF8.GetBytes(NormalizeLineEndings(Content))); - return BitConverter.ToString(checksum).Replace("-", string.Empty); + return CalculateChecksumWithAlgorithm(HashAlgorithm); + } + + /// + /// Calculates checksum using a specific algorithm. + /// + /// The hash algorithm to use. + /// The checksum calculated with the specified algorithm. + protected string CalculateChecksumWithAlgorithm(HashAlgorithmType algorithm) + { + string normalizedContent = NormalizeLineEndings(Content); + return ChecksumCalculator.Calculate(normalizedContent, algorithm); } /// diff --git a/src/Evolve/Utilities/ChecksumCalculator.cs b/src/Evolve/Utilities/ChecksumCalculator.cs new file mode 100644 index 00000000..59215aae --- /dev/null +++ b/src/Evolve/Utilities/ChecksumCalculator.cs @@ -0,0 +1,90 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using EvolveDb.Configuration; + +namespace EvolveDb.Utilities +{ + /// + /// Utility class for calculating migration checksums with configurable algorithms. + /// + internal static class ChecksumCalculator + { + /// + /// Returns the expected hex string length for each algorithm. + /// + /// The hash algorithm type. + /// The hex string length (32 for MD5, 64 for SHA256, 96 for SHA384, 128 for SHA512). + public static int GetHashLength(HashAlgorithmType algorithm) => algorithm switch + { + HashAlgorithmType.MD5 => 32, + HashAlgorithmType.SHA256 => 64, + HashAlgorithmType.SHA384 => 96, + HashAlgorithmType.SHA512 => 128, + _ => throw new ArgumentOutOfRangeException(nameof(algorithm)) + }; + + /// + /// Detects the hash algorithm based on checksum string length. + /// + /// The checksum string to analyze. + /// The detected algorithm type, or null if length doesn't match any known algorithm. + public static HashAlgorithmType? DetectAlgorithm(string? checksum) + { + if (string.IsNullOrEmpty(checksum)) + { + return null; + } + + return checksum.Length switch + { + 32 => HashAlgorithmType.MD5, + 64 => HashAlgorithmType.SHA256, + 96 => HashAlgorithmType.SHA384, + 128 => HashAlgorithmType.SHA512, + _ => null + }; + } + + /// + /// Calculates checksum of string content using the specified algorithm. + /// + /// The string content to hash. + /// The hash algorithm to use. + /// The uppercase hex string representation of the hash. + public static string Calculate(string content, HashAlgorithmType algorithm) + { + using var hashAlgorithm = CreateHashAlgorithm(algorithm); + byte[] hash = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(content)); + return BitConverter.ToString(hash).Replace("-", string.Empty); + } + + /// + /// Calculates checksum from a stream using the specified algorithm. + /// + /// The stream to hash. + /// The hash algorithm to use. + /// The uppercase hex string representation of the hash. + public static string Calculate(Stream stream, HashAlgorithmType algorithm) + { + using var hashAlgorithm = CreateHashAlgorithm(algorithm); + byte[] hash = hashAlgorithm.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", string.Empty); + } + + /// + /// Creates a HashAlgorithm instance for the specified algorithm type. + /// + /// The hash algorithm type. + /// A new HashAlgorithm instance. + private static HashAlgorithm CreateHashAlgorithm(HashAlgorithmType algorithm) => algorithm switch + { + HashAlgorithmType.MD5 => MD5.Create(), + HashAlgorithmType.SHA256 => SHA256.Create(), + HashAlgorithmType.SHA384 => SHA384.Create(), + HashAlgorithmType.SHA512 => SHA512.Create(), + _ => throw new ArgumentOutOfRangeException(nameof(algorithm)) + }; + } +} diff --git a/test/Evolve.Tests/Evolve.Tests.csproj b/test/Evolve.Tests/Evolve.Tests.csproj index 59c52dc4..f7cf4600 100644 --- a/test/Evolve.Tests/Evolve.Tests.csproj +++ b/test/Evolve.Tests/Evolve.Tests.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 false latest EvolveDb.Tests diff --git a/test/Evolve.Tests/EvolveConfiguration.cs b/test/Evolve.Tests/EvolveConfiguration.cs index afcce44f..4a67ea26 100644 --- a/test/Evolve.Tests/EvolveConfiguration.cs +++ b/test/Evolve.Tests/EvolveConfiguration.cs @@ -41,6 +41,7 @@ public string MetadataTableSchema public bool RetryRepeatableMigrationsUntilNoError { get; set; } public TransactionKind TransactionMode { get; set; } = TransactionKind.CommitEach; public bool SkipNextMigrations { get; set; } = false; + public HashAlgorithmType ChecksumAlgorithm { get; set; } = HashAlgorithmType.MD5; private IMigrationLoader _migrationLoader; public IMigrationLoader MigrationLoader diff --git a/test/Evolve.Tests/Migration/FileMigrationScriptTest.cs b/test/Evolve.Tests/Migration/FileMigrationScriptTest.cs index 5890c091..f8f288c4 100644 --- a/test/Evolve.Tests/Migration/FileMigrationScriptTest.cs +++ b/test/Evolve.Tests/Migration/FileMigrationScriptTest.cs @@ -1,8 +1,10 @@ using System; using System.IO; using System.Security.Cryptography; +using EvolveDb.Configuration; using EvolveDb.Metadata; using EvolveDb.Migration; +using EvolveDb.Utilities; using Xunit; using static EvolveDb.Tests.TestContext; using static EvolveDb.Tests.TestUtil; @@ -115,5 +117,73 @@ private static string FallbackCheck(string path) byte[] checksum = md5.ComputeHash(stream); return BitConverter.ToString(checksum).Replace("-", string.Empty); } + + [Fact] + [Category(Test.Migration)] + public void ValidateChecksum_accepts_MD5_when_configured_for_SHA256() + { + // Arrange + var script = BuildFileMigrationScript(CrLfScriptPath, "2.3.1"); + string md5Checksum = script.CalculateChecksum(); // Default is MD5 + + // Switch to SHA-256 + script.HashAlgorithm = HashAlgorithmType.SHA256; + + // Act & Assert - should not throw due to dual validation + script.ValidateChecksum(md5Checksum); + } + + [Fact] + [Category(Test.Migration)] + public void ValidateChecksum_accepts_SHA256_when_configured_for_SHA256() + { + // Arrange + var script = BuildFileMigrationScript(CrLfScriptPath, "2.3.1"); + script.HashAlgorithm = HashAlgorithmType.SHA256; + string sha256Checksum = script.CalculateChecksum(); + + // Act & Assert - should not throw + script.ValidateChecksum(sha256Checksum); + } + + [Fact] + [Category(Test.Migration)] + public void CalculateChecksum_returns_different_lengths_for_different_algorithms() + { + // Arrange + var script = BuildFileMigrationScript(CrLfScriptPath, "2.3.1"); + + // Act + script.HashAlgorithm = HashAlgorithmType.MD5; + string md5 = script.CalculateChecksum(); + + script.HashAlgorithm = HashAlgorithmType.SHA256; + string sha256 = script.CalculateChecksum(); + + script.HashAlgorithm = HashAlgorithmType.SHA512; + string sha512 = script.CalculateChecksum(); + + // Assert + Assert.Equal(32, md5.Length); + Assert.Equal(64, sha256.Length); + Assert.Equal(128, sha512.Length); + } + + [Fact] + [Category(Test.Migration)] + public void ValidateChecksum_detects_algorithm_from_stored_checksum_length() + { + // Arrange - script configured for SHA-512 + var script = BuildFileMigrationScript(CrLfScriptPath, "2.3.1"); + script.HashAlgorithm = HashAlgorithmType.SHA512; + + // Calculate SHA-256 checksum separately + var tempScript = BuildFileMigrationScript(CrLfScriptPath, "2.3.1"); + tempScript.HashAlgorithm = HashAlgorithmType.SHA256; + string sha256Checksum = tempScript.CalculateChecksum(); + + // Act & Assert - should detect SHA-256 from length and validate + script.ValidateChecksum(sha256Checksum); + } } } diff --git a/test/Evolve.Tests/Utilities/ChecksumCalculatorTest.cs b/test/Evolve.Tests/Utilities/ChecksumCalculatorTest.cs new file mode 100644 index 00000000..9f179351 --- /dev/null +++ b/test/Evolve.Tests/Utilities/ChecksumCalculatorTest.cs @@ -0,0 +1,107 @@ +using EvolveDb.Configuration; +using EvolveDb.Utilities; +using Xunit; + +namespace EvolveDb.Tests.Utilities +{ + public class ChecksumCalculatorTest + { + private const string TestContent = "SELECT 1;"; + + [Fact] + [Category(Test.Migration)] + public void MD5_returns_32_character_hash() + { + var result = ChecksumCalculator.Calculate(TestContent, HashAlgorithmType.MD5); + Assert.Equal(32, result.Length); + } + + [Fact] + [Category(Test.Migration)] + public void SHA256_returns_64_character_hash() + { + var result = ChecksumCalculator.Calculate(TestContent, HashAlgorithmType.SHA256); + Assert.Equal(64, result.Length); + } + + [Fact] + [Category(Test.Migration)] + public void SHA384_returns_96_character_hash() + { + var result = ChecksumCalculator.Calculate(TestContent, HashAlgorithmType.SHA384); + Assert.Equal(96, result.Length); + } + + [Fact] + [Category(Test.Migration)] + public void SHA512_returns_128_character_hash() + { + var result = ChecksumCalculator.Calculate(TestContent, HashAlgorithmType.SHA512); + Assert.Equal(128, result.Length); + } + + [Theory] + [Category(Test.Migration)] + [InlineData(32, HashAlgorithmType.MD5)] + [InlineData(64, HashAlgorithmType.SHA256)] + [InlineData(96, HashAlgorithmType.SHA384)] + [InlineData(128, HashAlgorithmType.SHA512)] + public void DetectAlgorithm_correctly_identifies_by_length(int length, HashAlgorithmType expected) + { + var checksum = new string('A', length); + Assert.Equal(expected, ChecksumCalculator.DetectAlgorithm(checksum)); + } + + [Fact] + [Category(Test.Migration)] + public void DetectAlgorithm_returns_null_for_unknown_length() + { + var checksum = new string('A', 50); // Invalid length + Assert.Null(ChecksumCalculator.DetectAlgorithm(checksum)); + } + + [Fact] + [Category(Test.Migration)] + public void DetectAlgorithm_returns_null_for_null_or_empty() + { + Assert.Null(ChecksumCalculator.DetectAlgorithm(null)); + Assert.Null(ChecksumCalculator.DetectAlgorithm("")); + } + + [Fact] + [Category(Test.Migration)] + public void Same_content_produces_same_hash() + { + var hash1 = ChecksumCalculator.Calculate(TestContent, HashAlgorithmType.SHA256); + var hash2 = ChecksumCalculator.Calculate(TestContent, HashAlgorithmType.SHA256); + Assert.Equal(hash1, hash2); + } + + [Fact] + [Category(Test.Migration)] + public void Different_algorithms_produce_different_hashes() + { + var md5 = ChecksumCalculator.Calculate(TestContent, HashAlgorithmType.MD5); + var sha256 = ChecksumCalculator.Calculate(TestContent, HashAlgorithmType.SHA256); + Assert.NotEqual(md5, sha256); + } + + [Fact] + [Category(Test.Migration)] + public void GetHashLength_returns_correct_values() + { + Assert.Equal(32, ChecksumCalculator.GetHashLength(HashAlgorithmType.MD5)); + Assert.Equal(64, ChecksumCalculator.GetHashLength(HashAlgorithmType.SHA256)); + Assert.Equal(96, ChecksumCalculator.GetHashLength(HashAlgorithmType.SHA384)); + Assert.Equal(128, ChecksumCalculator.GetHashLength(HashAlgorithmType.SHA512)); + } + + [Fact] + [Category(Test.Migration)] + public void Hash_output_is_uppercase_hex() + { + var hash = ChecksumCalculator.Calculate(TestContent, HashAlgorithmType.MD5); + Assert.Matches("^[0-9A-F]+$", hash); + } + } +} From 2f20e28e1db4e9394fabd386b6fee6a9f9c1a13e Mon Sep 17 00:00:00 2001 From: sxndmxn Date: Tue, 13 Jan 2026 11:29:18 -0800 Subject: [PATCH 2/3] refactor: Use HashAlgorithmName instead of custom enum Replace custom HashAlgorithmType enum with .NET's built-in HashAlgorithmName struct for better alignment with .NET conventions and improved extensibility. Changes: - Delete HashAlgorithmType.cs custom enum - Use System.Security.Cryptography.HashAlgorithmName throughout - Use CryptographicOperations.HashData() for modern, efficient hashing - CLI option accepts string and converts to HashAlgorithmName - Update all tests to use HashAlgorithmName Benefits: - Uses official .NET cryptography pattern - Extensible for future algorithms via HashAlgorithmName constructor - Better FIPS mode handling (automatic) - Works with modern .NET crypto APIs --- src/Evolve.Cli/Program.cs | 11 ++- src/Evolve/Configuration/HashAlgorithmType.cs | 32 --------- .../Configuration/IEvolveConfiguration.cs | 3 +- src/Evolve/Evolve.cs | 3 +- src/Evolve/Migration/FileMigrationScript.cs | 6 +- src/Evolve/Migration/MigrationScript.cs | 10 +-- src/Evolve/Utilities/ChecksumCalculator.cs | 72 ++++++++----------- test/Evolve.Tests/EvolveConfiguration.cs | 3 +- .../Migration/FileMigrationScriptTest.cs | 15 ++-- .../Utilities/ChecksumCalculatorTest.cs | 64 +++++++++++------ 10 files changed, 104 insertions(+), 115 deletions(-) delete mode 100644 src/Evolve/Configuration/HashAlgorithmType.cs diff --git a/src/Evolve.Cli/Program.cs b/src/Evolve.Cli/Program.cs index 898e63e8..e59dcc55 100644 --- a/src/Evolve.Cli/Program.cs +++ b/src/Evolve.Cli/Program.cs @@ -6,6 +6,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Security.Cryptography; using AllowedValuesAttribute = McMaster.Extensions.CommandLineUtils.AllowedValuesAttribute; [Command(ResponseFileHandling = ResponseFileHandling.ParseArgsAsSpaceSeparated)] @@ -131,7 +132,15 @@ private int OnExecute(CommandLineApplication app, IConsole console) [Option("--checksum-algorithm", "Hash algorithm for migration checksums. SHA-256, SHA-384, SHA-512 are FIPS-compliant. Default: MD5", CommandOptionType.SingleValue)] [AllowedValues("MD5", "SHA256", "SHA384", "SHA512", IgnoreCase = true)] - public HashAlgorithmType ChecksumAlgorithm { get; } = Default.ChecksumAlgorithm; + public string ChecksumAlgorithmName { get; } = "MD5"; + + public HashAlgorithmName ChecksumAlgorithm => ChecksumAlgorithmName?.ToUpperInvariant() switch + { + "SHA256" => HashAlgorithmName.SHA256, + "SHA384" => HashAlgorithmName.SHA384, + "SHA512" => HashAlgorithmName.SHA512, + _ => HashAlgorithmName.MD5 + }; // Cassandra [Option("--keyspace", "A list of keyspaces managed by Evolve (Cassandra only).", CommandOptionType.MultipleValue)] diff --git a/src/Evolve/Configuration/HashAlgorithmType.cs b/src/Evolve/Configuration/HashAlgorithmType.cs deleted file mode 100644 index 413098cb..00000000 --- a/src/Evolve/Configuration/HashAlgorithmType.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace EvolveDb.Configuration -{ - /// - /// Hash algorithms supported for migration checksum calculation. - /// - public enum HashAlgorithmType - { - /// - /// MD5 hash algorithm (32 character hex output). - /// Not FIPS-compliant. - /// - MD5, - - /// - /// SHA-256 hash algorithm (64 character hex output). - /// FIPS-compliant. - /// - SHA256, - - /// - /// SHA-384 hash algorithm (96 character hex output). - /// FIPS-compliant. - /// - SHA384, - - /// - /// SHA-512 hash algorithm (128 character hex output). - /// FIPS-compliant. - /// - SHA512 - } -} diff --git a/src/Evolve/Configuration/IEvolveConfiguration.cs b/src/Evolve/Configuration/IEvolveConfiguration.cs index 22b52884..18e0109c 100644 --- a/src/Evolve/Configuration/IEvolveConfiguration.cs +++ b/src/Evolve/Configuration/IEvolveConfiguration.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Reflection; +using System.Security.Cryptography; using System.Text; using EvolveDb.Migration; @@ -231,6 +232,6 @@ public interface IEvolveConfiguration /// algorithm and MD5 for backward compatibility with existing databases. /// /// - HashAlgorithmType ChecksumAlgorithm { get; } + HashAlgorithmName ChecksumAlgorithm { get; } } } diff --git a/src/Evolve/Evolve.cs b/src/Evolve/Evolve.cs index df77b515..68173c1b 100644 --- a/src/Evolve/Evolve.cs +++ b/src/Evolve/Evolve.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +using System.Security.Cryptography; using System.Text; using System.Threading; using System.Transactions; @@ -90,7 +91,7 @@ public IMigrationLoader MigrationLoader set { _migrationLoader = value; } } - public HashAlgorithmType ChecksumAlgorithm { get; set; } = HashAlgorithmType.MD5; + public HashAlgorithmName ChecksumAlgorithm { get; set; } = HashAlgorithmName.MD5; #endregion diff --git a/src/Evolve/Migration/FileMigrationScript.cs b/src/Evolve/Migration/FileMigrationScript.cs index 39db6e91..10388c30 100644 --- a/src/Evolve/Migration/FileMigrationScript.cs +++ b/src/Evolve/Migration/FileMigrationScript.cs @@ -1,7 +1,7 @@ using System; using System.IO; +using System.Security.Cryptography; using System.Text; -using EvolveDb.Configuration; using EvolveDb.Metadata; using EvolveDb.Utilities; @@ -59,14 +59,14 @@ public override void ValidateChecksum(string? checksum) /// private string FallbackCheck() { - return FallbackCheckWithAlgorithm(HashAlgorithmType.MD5); + return FallbackCheckWithAlgorithm(HashAlgorithmName.MD5); } /// /// Calculate the checksum with the pre v1.8.0 version using specified algorithm. /// /// The hash algorithm to use. - private string FallbackCheckWithAlgorithm(HashAlgorithmType algorithm) + private string FallbackCheckWithAlgorithm(HashAlgorithmName algorithm) { using FileStream stream = File.OpenRead(Path); return ChecksumCalculator.Calculate(stream, algorithm); diff --git a/src/Evolve/Migration/MigrationScript.cs b/src/Evolve/Migration/MigrationScript.cs index 8f838282..4a86624d 100644 --- a/src/Evolve/Migration/MigrationScript.cs +++ b/src/Evolve/Migration/MigrationScript.cs @@ -1,7 +1,7 @@ using System; using System.IO; +using System.Security.Cryptography; using System.Text; -using EvolveDb.Configuration; using EvolveDb.Metadata; using EvolveDb.Utilities; @@ -28,7 +28,7 @@ protected MigrationScript(string? version, string description, string name, stri /// /// Gets or sets the hash algorithm used for checksum calculation. /// - internal HashAlgorithmType HashAlgorithm { get; set; } = HashAlgorithmType.MD5; + internal HashAlgorithmName HashAlgorithm { get; set; } = HashAlgorithmName.MD5; /// /// Returns false if the special comment "evolve-tx-off" is found in the first line of the script, true otherwise. @@ -59,9 +59,9 @@ public virtual void ValidateChecksum(string? checksum) } // Dual validation: try MD5 for backward compatibility if using SHA-2 - if (HashAlgorithm != HashAlgorithmType.MD5) + if (HashAlgorithm != HashAlgorithmName.MD5) { - string md5Checksum = CalculateChecksumWithAlgorithm(HashAlgorithmType.MD5); + string md5Checksum = CalculateChecksumWithAlgorithm(HashAlgorithmName.MD5); if (checksum == md5Checksum) { return; @@ -96,7 +96,7 @@ public virtual string CalculateChecksum() /// /// The hash algorithm to use. /// The checksum calculated with the specified algorithm. - protected string CalculateChecksumWithAlgorithm(HashAlgorithmType algorithm) + protected string CalculateChecksumWithAlgorithm(HashAlgorithmName algorithm) { string normalizedContent = NormalizeLineEndings(Content); return ChecksumCalculator.Calculate(normalizedContent, algorithm); diff --git a/src/Evolve/Utilities/ChecksumCalculator.cs b/src/Evolve/Utilities/ChecksumCalculator.cs index 59215aae..335df4fa 100644 --- a/src/Evolve/Utilities/ChecksumCalculator.cs +++ b/src/Evolve/Utilities/ChecksumCalculator.cs @@ -1,8 +1,8 @@ using System; +using System.Collections.Generic; using System.IO; using System.Security.Cryptography; using System.Text; -using EvolveDb.Configuration; namespace EvolveDb.Utilities { @@ -11,40 +11,45 @@ namespace EvolveDb.Utilities /// internal static class ChecksumCalculator { + /// + /// Known algorithm lengths for detection. + /// + private static readonly Dictionary LengthToAlgorithm = new() + { + { 32, HashAlgorithmName.MD5 }, + { 64, HashAlgorithmName.SHA256 }, + { 96, HashAlgorithmName.SHA384 }, + { 128, HashAlgorithmName.SHA512 } + }; + /// /// Returns the expected hex string length for each algorithm. /// - /// The hash algorithm type. + /// The hash algorithm name. /// The hex string length (32 for MD5, 64 for SHA256, 96 for SHA384, 128 for SHA512). - public static int GetHashLength(HashAlgorithmType algorithm) => algorithm switch + public static int GetHashLength(HashAlgorithmName algorithm) { - HashAlgorithmType.MD5 => 32, - HashAlgorithmType.SHA256 => 64, - HashAlgorithmType.SHA384 => 96, - HashAlgorithmType.SHA512 => 128, - _ => throw new ArgumentOutOfRangeException(nameof(algorithm)) - }; + if (algorithm == HashAlgorithmName.MD5) return 32; + if (algorithm == HashAlgorithmName.SHA256) return 64; + if (algorithm == HashAlgorithmName.SHA384) return 96; + if (algorithm == HashAlgorithmName.SHA512) return 128; + // For unknown algorithms, compute a sample hash to determine length + return CryptographicOperations.HashData(algorithm, Array.Empty()).Length * 2; + } /// /// Detects the hash algorithm based on checksum string length. /// /// The checksum string to analyze. - /// The detected algorithm type, or null if length doesn't match any known algorithm. - public static HashAlgorithmType? DetectAlgorithm(string? checksum) + /// The detected algorithm name, or null if length doesn't match any known algorithm. + public static HashAlgorithmName? DetectAlgorithm(string? checksum) { if (string.IsNullOrEmpty(checksum)) { return null; } - return checksum.Length switch - { - 32 => HashAlgorithmType.MD5, - 64 => HashAlgorithmType.SHA256, - 96 => HashAlgorithmType.SHA384, - 128 => HashAlgorithmType.SHA512, - _ => null - }; + return LengthToAlgorithm.TryGetValue(checksum.Length, out var algorithm) ? algorithm : null; } /// @@ -53,11 +58,11 @@ internal static class ChecksumCalculator /// The string content to hash. /// The hash algorithm to use. /// The uppercase hex string representation of the hash. - public static string Calculate(string content, HashAlgorithmType algorithm) + public static string Calculate(string content, HashAlgorithmName algorithm) { - using var hashAlgorithm = CreateHashAlgorithm(algorithm); - byte[] hash = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(content)); - return BitConverter.ToString(hash).Replace("-", string.Empty); + byte[] data = Encoding.UTF8.GetBytes(content); + byte[] hash = CryptographicOperations.HashData(algorithm, data); + return Convert.ToHexString(hash); } /// @@ -66,25 +71,10 @@ public static string Calculate(string content, HashAlgorithmType algorithm) /// The stream to hash. /// The hash algorithm to use. /// The uppercase hex string representation of the hash. - public static string Calculate(Stream stream, HashAlgorithmType algorithm) + public static string Calculate(Stream stream, HashAlgorithmName algorithm) { - using var hashAlgorithm = CreateHashAlgorithm(algorithm); - byte[] hash = hashAlgorithm.ComputeHash(stream); - return BitConverter.ToString(hash).Replace("-", string.Empty); + byte[] hash = CryptographicOperations.HashData(algorithm, stream); + return Convert.ToHexString(hash); } - - /// - /// Creates a HashAlgorithm instance for the specified algorithm type. - /// - /// The hash algorithm type. - /// A new HashAlgorithm instance. - private static HashAlgorithm CreateHashAlgorithm(HashAlgorithmType algorithm) => algorithm switch - { - HashAlgorithmType.MD5 => MD5.Create(), - HashAlgorithmType.SHA256 => SHA256.Create(), - HashAlgorithmType.SHA384 => SHA384.Create(), - HashAlgorithmType.SHA512 => SHA512.Create(), - _ => throw new ArgumentOutOfRangeException(nameof(algorithm)) - }; } } diff --git a/test/Evolve.Tests/EvolveConfiguration.cs b/test/Evolve.Tests/EvolveConfiguration.cs index 4a67ea26..2cf30da8 100644 --- a/test/Evolve.Tests/EvolveConfiguration.cs +++ b/test/Evolve.Tests/EvolveConfiguration.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Security.Cryptography; using System.Text; using EvolveDb.Configuration; using EvolveDb.Migration; @@ -41,7 +42,7 @@ public string MetadataTableSchema public bool RetryRepeatableMigrationsUntilNoError { get; set; } public TransactionKind TransactionMode { get; set; } = TransactionKind.CommitEach; public bool SkipNextMigrations { get; set; } = false; - public HashAlgorithmType ChecksumAlgorithm { get; set; } = HashAlgorithmType.MD5; + public HashAlgorithmName ChecksumAlgorithm { get; set; } = HashAlgorithmName.MD5; private IMigrationLoader _migrationLoader; public IMigrationLoader MigrationLoader diff --git a/test/Evolve.Tests/Migration/FileMigrationScriptTest.cs b/test/Evolve.Tests/Migration/FileMigrationScriptTest.cs index f8f288c4..f6e7f38f 100644 --- a/test/Evolve.Tests/Migration/FileMigrationScriptTest.cs +++ b/test/Evolve.Tests/Migration/FileMigrationScriptTest.cs @@ -1,7 +1,6 @@ using System; using System.IO; using System.Security.Cryptography; -using EvolveDb.Configuration; using EvolveDb.Metadata; using EvolveDb.Migration; using EvolveDb.Utilities; @@ -127,7 +126,7 @@ public void ValidateChecksum_accepts_MD5_when_configured_for_SHA256() string md5Checksum = script.CalculateChecksum(); // Default is MD5 // Switch to SHA-256 - script.HashAlgorithm = HashAlgorithmType.SHA256; + script.HashAlgorithm = HashAlgorithmName.SHA256; // Act & Assert - should not throw due to dual validation script.ValidateChecksum(md5Checksum); @@ -139,7 +138,7 @@ public void ValidateChecksum_accepts_SHA256_when_configured_for_SHA256() { // Arrange var script = BuildFileMigrationScript(CrLfScriptPath, "2.3.1"); - script.HashAlgorithm = HashAlgorithmType.SHA256; + script.HashAlgorithm = HashAlgorithmName.SHA256; string sha256Checksum = script.CalculateChecksum(); // Act & Assert - should not throw @@ -154,13 +153,13 @@ public void CalculateChecksum_returns_different_lengths_for_different_algorithms var script = BuildFileMigrationScript(CrLfScriptPath, "2.3.1"); // Act - script.HashAlgorithm = HashAlgorithmType.MD5; + script.HashAlgorithm = HashAlgorithmName.MD5; string md5 = script.CalculateChecksum(); - script.HashAlgorithm = HashAlgorithmType.SHA256; + script.HashAlgorithm = HashAlgorithmName.SHA256; string sha256 = script.CalculateChecksum(); - script.HashAlgorithm = HashAlgorithmType.SHA512; + script.HashAlgorithm = HashAlgorithmName.SHA512; string sha512 = script.CalculateChecksum(); // Assert @@ -175,11 +174,11 @@ public void ValidateChecksum_detects_algorithm_from_stored_checksum_length() { // Arrange - script configured for SHA-512 var script = BuildFileMigrationScript(CrLfScriptPath, "2.3.1"); - script.HashAlgorithm = HashAlgorithmType.SHA512; + script.HashAlgorithm = HashAlgorithmName.SHA512; // Calculate SHA-256 checksum separately var tempScript = BuildFileMigrationScript(CrLfScriptPath, "2.3.1"); - tempScript.HashAlgorithm = HashAlgorithmType.SHA256; + tempScript.HashAlgorithm = HashAlgorithmName.SHA256; string sha256Checksum = tempScript.CalculateChecksum(); // Act & Assert - should detect SHA-256 from length and validate diff --git a/test/Evolve.Tests/Utilities/ChecksumCalculatorTest.cs b/test/Evolve.Tests/Utilities/ChecksumCalculatorTest.cs index 9f179351..abb1bdd8 100644 --- a/test/Evolve.Tests/Utilities/ChecksumCalculatorTest.cs +++ b/test/Evolve.Tests/Utilities/ChecksumCalculatorTest.cs @@ -1,4 +1,4 @@ -using EvolveDb.Configuration; +using System.Security.Cryptography; using EvolveDb.Utilities; using Xunit; @@ -12,7 +12,7 @@ public class ChecksumCalculatorTest [Category(Test.Migration)] public void MD5_returns_32_character_hash() { - var result = ChecksumCalculator.Calculate(TestContent, HashAlgorithmType.MD5); + var result = ChecksumCalculator.Calculate(TestContent, HashAlgorithmName.MD5); Assert.Equal(32, result.Length); } @@ -20,7 +20,7 @@ public void MD5_returns_32_character_hash() [Category(Test.Migration)] public void SHA256_returns_64_character_hash() { - var result = ChecksumCalculator.Calculate(TestContent, HashAlgorithmType.SHA256); + var result = ChecksumCalculator.Calculate(TestContent, HashAlgorithmName.SHA256); Assert.Equal(64, result.Length); } @@ -28,7 +28,7 @@ public void SHA256_returns_64_character_hash() [Category(Test.Migration)] public void SHA384_returns_96_character_hash() { - var result = ChecksumCalculator.Calculate(TestContent, HashAlgorithmType.SHA384); + var result = ChecksumCalculator.Calculate(TestContent, HashAlgorithmName.SHA384); Assert.Equal(96, result.Length); } @@ -36,20 +36,40 @@ public void SHA384_returns_96_character_hash() [Category(Test.Migration)] public void SHA512_returns_128_character_hash() { - var result = ChecksumCalculator.Calculate(TestContent, HashAlgorithmType.SHA512); + var result = ChecksumCalculator.Calculate(TestContent, HashAlgorithmName.SHA512); Assert.Equal(128, result.Length); } - [Theory] + [Fact] + [Category(Test.Migration)] + public void DetectAlgorithm_correctly_identifies_MD5_by_length() + { + var checksum = new string('A', 32); + Assert.Equal(HashAlgorithmName.MD5, ChecksumCalculator.DetectAlgorithm(checksum)); + } + + [Fact] + [Category(Test.Migration)] + public void DetectAlgorithm_correctly_identifies_SHA256_by_length() + { + var checksum = new string('A', 64); + Assert.Equal(HashAlgorithmName.SHA256, ChecksumCalculator.DetectAlgorithm(checksum)); + } + + [Fact] + [Category(Test.Migration)] + public void DetectAlgorithm_correctly_identifies_SHA384_by_length() + { + var checksum = new string('A', 96); + Assert.Equal(HashAlgorithmName.SHA384, ChecksumCalculator.DetectAlgorithm(checksum)); + } + + [Fact] [Category(Test.Migration)] - [InlineData(32, HashAlgorithmType.MD5)] - [InlineData(64, HashAlgorithmType.SHA256)] - [InlineData(96, HashAlgorithmType.SHA384)] - [InlineData(128, HashAlgorithmType.SHA512)] - public void DetectAlgorithm_correctly_identifies_by_length(int length, HashAlgorithmType expected) + public void DetectAlgorithm_correctly_identifies_SHA512_by_length() { - var checksum = new string('A', length); - Assert.Equal(expected, ChecksumCalculator.DetectAlgorithm(checksum)); + var checksum = new string('A', 128); + Assert.Equal(HashAlgorithmName.SHA512, ChecksumCalculator.DetectAlgorithm(checksum)); } [Fact] @@ -72,8 +92,8 @@ public void DetectAlgorithm_returns_null_for_null_or_empty() [Category(Test.Migration)] public void Same_content_produces_same_hash() { - var hash1 = ChecksumCalculator.Calculate(TestContent, HashAlgorithmType.SHA256); - var hash2 = ChecksumCalculator.Calculate(TestContent, HashAlgorithmType.SHA256); + var hash1 = ChecksumCalculator.Calculate(TestContent, HashAlgorithmName.SHA256); + var hash2 = ChecksumCalculator.Calculate(TestContent, HashAlgorithmName.SHA256); Assert.Equal(hash1, hash2); } @@ -81,8 +101,8 @@ public void Same_content_produces_same_hash() [Category(Test.Migration)] public void Different_algorithms_produce_different_hashes() { - var md5 = ChecksumCalculator.Calculate(TestContent, HashAlgorithmType.MD5); - var sha256 = ChecksumCalculator.Calculate(TestContent, HashAlgorithmType.SHA256); + var md5 = ChecksumCalculator.Calculate(TestContent, HashAlgorithmName.MD5); + var sha256 = ChecksumCalculator.Calculate(TestContent, HashAlgorithmName.SHA256); Assert.NotEqual(md5, sha256); } @@ -90,17 +110,17 @@ public void Different_algorithms_produce_different_hashes() [Category(Test.Migration)] public void GetHashLength_returns_correct_values() { - Assert.Equal(32, ChecksumCalculator.GetHashLength(HashAlgorithmType.MD5)); - Assert.Equal(64, ChecksumCalculator.GetHashLength(HashAlgorithmType.SHA256)); - Assert.Equal(96, ChecksumCalculator.GetHashLength(HashAlgorithmType.SHA384)); - Assert.Equal(128, ChecksumCalculator.GetHashLength(HashAlgorithmType.SHA512)); + Assert.Equal(32, ChecksumCalculator.GetHashLength(HashAlgorithmName.MD5)); + Assert.Equal(64, ChecksumCalculator.GetHashLength(HashAlgorithmName.SHA256)); + Assert.Equal(96, ChecksumCalculator.GetHashLength(HashAlgorithmName.SHA384)); + Assert.Equal(128, ChecksumCalculator.GetHashLength(HashAlgorithmName.SHA512)); } [Fact] [Category(Test.Migration)] public void Hash_output_is_uppercase_hex() { - var hash = ChecksumCalculator.Calculate(TestContent, HashAlgorithmType.MD5); + var hash = ChecksumCalculator.Calculate(TestContent, HashAlgorithmName.MD5); Assert.Matches("^[0-9A-F]+$", hash); } } From 13e4adf776699658693f91b9bcd271e3c600a564 Mon Sep 17 00:00:00 2001 From: sxndmxn Date: Tue, 13 Jan 2026 11:39:37 -0800 Subject: [PATCH 3/3] fix: Allow any hash algorithm name in CLI Remove [AllowedValues] restriction and use HashAlgorithmName constructor directly to accept any system-supported hash algorithm. The runtime will validate algorithm support via CryptographicOperations. HashData(), throwing CryptographicException for unsupported algorithms. --- src/Evolve.Cli/Program.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Evolve.Cli/Program.cs b/src/Evolve.Cli/Program.cs index e59dcc55..6715558b 100644 --- a/src/Evolve.Cli/Program.cs +++ b/src/Evolve.Cli/Program.cs @@ -130,17 +130,12 @@ private int OnExecute(CommandLineApplication app, IConsole console) [Option("--skip-next-migrations", "When set, mark all subsequent migrations as applied. Default: false", CommandOptionType.SingleValue)] public bool SkipNextMigrations { get; } - [Option("--checksum-algorithm", "Hash algorithm for migration checksums. SHA-256, SHA-384, SHA-512 are FIPS-compliant. Default: MD5", CommandOptionType.SingleValue)] - [AllowedValues("MD5", "SHA256", "SHA384", "SHA512", IgnoreCase = true)] + [Option("--checksum-algorithm", "Hash algorithm name (e.g., MD5, SHA256, SHA384, SHA512, SHA3-256). Must be supported by the system. Default: MD5", CommandOptionType.SingleValue)] public string ChecksumAlgorithmName { get; } = "MD5"; - public HashAlgorithmName ChecksumAlgorithm => ChecksumAlgorithmName?.ToUpperInvariant() switch - { - "SHA256" => HashAlgorithmName.SHA256, - "SHA384" => HashAlgorithmName.SHA384, - "SHA512" => HashAlgorithmName.SHA512, - _ => HashAlgorithmName.MD5 - }; + public HashAlgorithmName ChecksumAlgorithm => string.IsNullOrWhiteSpace(ChecksumAlgorithmName) + ? HashAlgorithmName.MD5 + : new HashAlgorithmName(ChecksumAlgorithmName); // Cassandra [Option("--keyspace", "A list of keyspaces managed by Evolve (Cassandra only).", CommandOptionType.MultipleValue)]