diff --git a/README.md b/README.md index 95a3c02..1481d48 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,12 @@ This is a tool that can be used to verify the signature of the French fiscal arc ### Usage -```Mews.Fiscalization.SignatureChecker.exe -- --``` +After downloading the latest version of the tool and extracting it: +Windows: run ```Mews.Fiscalization.SignatureChecker.exe``` then provide the path to the ZIP archive and the environment for which the signature should be verified. +Command example: ```2025.zip --production``` or ```2025.zip --develop``` + +Mac: run ```chmod +x Mews.Fiscalization.SignatureChecker``` then provide the path to the ZIP archive and the environment for which the signature should be verified. +Command example: ```2025.zip --production``` or ```2025.zip --develop``` ### Parameters @@ -15,8 +20,4 @@ This is a tool that can be used to verify the signature of the French fiscal arc ### Output -The tool will output the result of the signature verification. If the signature is valid, the tool will output `Archive signature is valid.`. If the signature is invalid, the tool will output `Archive signature is invalid.`. - -### Example - -```Mews.Fiscalization.SignatureChecker.exe --C:\path\to\archive.zip --develop``` \ No newline at end of file +The tool will output the result of the signature verification. If the signature is valid, the tool will output `Archive signature is valid.`. If the signature is invalid, the tool will output `Archive signature is invalid.`. \ No newline at end of file diff --git a/src/CryptoServiceProvider.cs b/src/CryptoServiceProvider.cs index 3614720..8567938 100644 --- a/src/CryptoServiceProvider.cs +++ b/src/CryptoServiceProvider.cs @@ -4,8 +4,8 @@ namespace Mews.Fiscalization.SignatureChecker; public static class CryptoServiceProvider { - private static readonly string ProductionKey = "1NWh/S4jzrtO/N3Dm1gkfaok0A/u04/pExwDES2SmubDSeFwssXGBqWZ4UsIONKXdXkDrJ1kqgednDjkdkCYW6BmYr/Ds1U+3viiZtl6nBaJp2MTGLSGDR/9algLOYr60bk/18KJFbr2xzKMadimrQ5J2p0LVkfPvIcX0d69xbei0Kzn5bFOKlA3WgimOdh9ed9ZZ7IEylVVQQG6dWElWbbMgDiOzwyGiPn7/9Tb+oh6zxBiazc5/HpACC/RBpr85aIJQtXgDTK4mlwjrL/WHoAHyehwQSJ08QsRyd/tux8QxBZDk4hWoipC8W+tlyFg4n/n79AuxQX0TaOmMQFjm4hVVX3cS34zmELpGsZiH2uMtGJqgehchlF+FSE/12v2p9sEesPLANEmajjChmajQgvbnlS/SSPc/gd2FYb0pq8jLM/90idOKKMx7ZbxS/8yro8VbS79VR8UdMKRzJLFDbHeDa0hVVbaSWR8jodnp7gPd55SIZ47AcDrpKNQ4s9R1Ah38Czq/QKInCkNjTCC+JdxrF+XnqsUFL5DbOUDbxDzVq0TJj36xMC/LICcIbTmt7rg0auqXq4RhG6lnwyCpUaUr5YZ4pJm44kA94Zgy+o/7wpqYhYayGIZqu1VgK82gBs5m5uPXHiTFfb4cZzCZG356I8nzr2LQjTe4hXDaRE=AQAB"; - private static readonly string DevelopKey = "ufmEzn3l/SsD01uvqILxYTEy8XV47r/uMD4sOZ07J4P0kx73LBcbMx+Gag4T9nYzTs0KmBPl6ogSdqA1hsJQ0BBlKFVZK53WTe1GjtXVPyhyuvfTd6pXPuW7ozBcd0xuHU5qhBY4TEjrbRZoSMHLMDsQB4Egdx+H5Y4T9TXSri7yraxUUqNNET5zU1mpxJqC3R6M6UcBiMagNYvcrL/iWLLvfhC3pjFmBiS/0rxAU5TRiWumwlFrkUpt7/dUMBDD1Adt1/nPEA5IZBscoFQWQGi1tCpzs1K71nJF04BgrALpetf3KYucrbiY85ANIAjE8pvDaYQVOSg5xIRIUVkd92uA8dFc2QRd6EL7wxZ6u3y5FsTm9ILaEfc54MlSVWx2E+bTvBL2jK1Pz+hSNBOWnRJbAJFWuToCGOykkyHOqQwmPX5MhbJ5q3qYh//E8D2SeJqc1Nr3NDaUNhJf1gkkr53yN+kbF9M2+xqn8p1jV6BUjQI+qDYeTyuBVgxEz5+H0xEyrSZ5DDjm9HsxXhzdXhHK7HDtvrSjgAr4HGmSODCuWpynzjGdNVEANsfpjT//zhgGEkW0JHiRmXjRbNslmspeW7xG5/uOxlZIFVahM0n544hoQbZx9mYcNMDkuT6d9wgjFEKAXVzn+45gPZ627eeVCF3pbAJCSx5EVPJfZGU=AQAB"; + private const string ProductionKey = "1NWh/S4jzrtO/N3Dm1gkfaok0A/u04/pExwDES2SmubDSeFwssXGBqWZ4UsIONKXdXkDrJ1kqgednDjkdkCYW6BmYr/Ds1U+3viiZtl6nBaJp2MTGLSGDR/9algLOYr60bk/18KJFbr2xzKMadimrQ5J2p0LVkfPvIcX0d69xbei0Kzn5bFOKlA3WgimOdh9ed9ZZ7IEylVVQQG6dWElWbbMgDiOzwyGiPn7/9Tb+oh6zxBiazc5/HpACC/RBpr85aIJQtXgDTK4mlwjrL/WHoAHyehwQSJ08QsRyd/tux8QxBZDk4hWoipC8W+tlyFg4n/n79AuxQX0TaOmMQFjm4hVVX3cS34zmELpGsZiH2uMtGJqgehchlF+FSE/12v2p9sEesPLANEmajjChmajQgvbnlS/SSPc/gd2FYb0pq8jLM/90idOKKMx7ZbxS/8yro8VbS79VR8UdMKRzJLFDbHeDa0hVVbaSWR8jodnp7gPd55SIZ47AcDrpKNQ4s9R1Ah38Czq/QKInCkNjTCC+JdxrF+XnqsUFL5DbOUDbxDzVq0TJj36xMC/LICcIbTmt7rg0auqXq4RhG6lnwyCpUaUr5YZ4pJm44kA94Zgy+o/7wpqYhYayGIZqu1VgK82gBs5m5uPXHiTFfb4cZzCZG356I8nzr2LQjTe4hXDaRE=AQAB"; + private const string DevelopKey = "ufmEzn3l/SsD01uvqILxYTEy8XV47r/uMD4sOZ07J4P0kx73LBcbMx+Gag4T9nYzTs0KmBPl6ogSdqA1hsJQ0BBlKFVZK53WTe1GjtXVPyhyuvfTd6pXPuW7ozBcd0xuHU5qhBY4TEjrbRZoSMHLMDsQB4Egdx+H5Y4T9TXSri7yraxUUqNNET5zU1mpxJqC3R6M6UcBiMagNYvcrL/iWLLvfhC3pjFmBiS/0rxAU5TRiWumwlFrkUpt7/dUMBDD1Adt1/nPEA5IZBscoFQWQGi1tCpzs1K71nJF04BgrALpetf3KYucrbiY85ANIAjE8pvDaYQVOSg5xIRIUVkd92uA8dFc2QRd6EL7wxZ6u3y5FsTm9ILaEfc54MlSVWx2E+bTvBL2jK1Pz+hSNBOWnRJbAJFWuToCGOykkyHOqQwmPX5MhbJ5q3qYh//E8D2SeJqc1Nr3NDaUNhJf1gkkr53yN+kbF9M2+xqn8p1jV6BUjQI+qDYeTyuBVgxEz5+H0xEyrSZ5DDjm9HsxXhzdXhHK7HDtvrSjgAr4HGmSODCuWpynzjGdNVEANsfpjT//zhgGEkW0JHiRmXjRbNslmspeW7xG5/uOxlZIFVahM0n544hoQbZx9mYcNMDkuT6d9wgjFEKAXVzn+45gPZ627eeVCF3pbAJCSx5EVPJfZGU=AQAB"; public static RSACryptoServiceProvider GetProduction() { diff --git a/src/Dto/Archive.cs b/src/Dto/Archive.cs index 24e90a5..41ac8f5 100644 --- a/src/Dto/Archive.cs +++ b/src/Dto/Archive.cs @@ -1,6 +1,3 @@ -using System.Collections.Generic; -using FuncSharp; - namespace Mews.Fiscalization.SignatureChecker.Dto; internal sealed class Archive diff --git a/src/Dto/ArchiveMetadata.cs b/src/Dto/ArchiveMetadata.cs index 6dbe60e..9f7c2df 100644 --- a/src/Dto/ArchiveMetadata.cs +++ b/src/Dto/ArchiveMetadata.cs @@ -1,4 +1,3 @@ -using System; using Newtonsoft.Json; namespace Mews.Fiscalization.SignatureChecker.Dto; diff --git a/src/Dto/ArchiveReader.cs b/src/Dto/ArchiveReader.cs index 6dcbf26..6b09027 100644 --- a/src/Dto/ArchiveReader.cs +++ b/src/Dto/ArchiveReader.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using System.Linq; -using FuncSharp; - namespace Mews.Fiscalization.SignatureChecker.Dto; internal static class ArchiveReader diff --git a/src/Dto/CsvData.cs b/src/Dto/CsvData.cs index 95c1f64..5828f82 100644 --- a/src/Dto/CsvData.cs +++ b/src/Dto/CsvData.cs @@ -1,6 +1,3 @@ -using System.Collections.Generic; -using FuncSharp; - namespace Mews.Fiscalization.SignatureChecker.Dto; internal sealed class CsvData diff --git a/src/Dto/CsvRow.cs b/src/Dto/CsvRow.cs index cd99da1..2375376 100644 --- a/src/Dto/CsvRow.cs +++ b/src/Dto/CsvRow.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; - namespace Mews.Fiscalization.SignatureChecker.Dto; internal sealed class CsvRow diff --git a/src/Extensions.cs b/src/Extensions.cs index 6505cad..6c07b94 100644 --- a/src/Extensions.cs +++ b/src/Extensions.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FuncSharp; using Mews.Fiscalization.SignatureChecker.Model; namespace Mews.Fiscalization.SignatureChecker; @@ -34,17 +30,6 @@ internal static string ToSignatureString(this TaxSummary taxSummary) return string.Join("|", parts); } - internal static Tuple, IReadOnlyCollection> Partition(this IEnumerable e, Func predicate) - { - var passing = new List(); - var violating = new List(); - foreach (var i in e) - { - (predicate(i) ? passing : violating).Add(i); - } - return Tuple.Create, IReadOnlyCollection>(passing, violating); - } - private static string ToSignatureString(this TaxRate taxRate) { var rateNormalizationConstant = 100 * 100; diff --git a/src/GlobalUsings.cs b/src/GlobalUsings.cs new file mode 100644 index 0000000..23e8349 --- /dev/null +++ b/src/GlobalUsings.cs @@ -0,0 +1,6 @@ +// Global using directives + +global using System; +global using System.Linq; +global using System.Collections.Generic; +global using FuncSharp; \ No newline at end of file diff --git a/src/Mews.Fiscalization.SignatureChecker.csproj b/src/Mews.Fiscalization.SignatureChecker.csproj index d0b5beb..db8bd62 100644 --- a/src/Mews.Fiscalization.SignatureChecker.csproj +++ b/src/Mews.Fiscalization.SignatureChecker.csproj @@ -2,7 +2,7 @@ Exe - 1.0.5 + 2.0.0 net8.0 12 Mews.Fiscalization.SignatureChecker diff --git a/src/Model/Amount.cs b/src/Model/Amount.cs index c165642..8cd489b 100644 --- a/src/Model/Amount.cs +++ b/src/Model/Amount.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; using System.Globalization; -using System.Linq; -using FuncSharp; namespace Mews.Fiscalization.SignatureChecker.Model; diff --git a/src/Model/Archive.cs b/src/Model/Archive.cs index 34fdd45..31cd6c6 100644 --- a/src/Model/Archive.cs +++ b/src/Model/Archive.cs @@ -1,6 +1,3 @@ -using System.Collections.Generic; -using FuncSharp; - namespace Mews.Fiscalization.SignatureChecker.Model; internal sealed class Archive @@ -27,7 +24,7 @@ public static Try> Create(IReadOnlyList return archive.FlatMap(c => Parse(c)); } - public static Try> Parse(Dto.Archive archive) + private static Try> Parse(Dto.Archive archive) { return ArchiveMetadata.Create(archive).FlatMap(metadata => { diff --git a/src/Model/ArchiveMetadata.cs b/src/Model/ArchiveMetadata.cs index 92ec652..07d13c3 100644 --- a/src/Model/ArchiveMetadata.cs +++ b/src/Model/ArchiveMetadata.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using FuncSharp; using Newtonsoft.Json; namespace Mews.Fiscalization.SignatureChecker.Model; @@ -31,19 +28,22 @@ public static Try> Create(Dto.Archive arc var rawMetadata = Try.Catch(_ => JsonConvert.DeserializeObject(archive.Metadata.Content)); return rawMetadata.MapError(_ => $"Invalid data ({archive.Metadata.Name}).".ToReadOnlyList()).FlatMap(metadata => { - var version = metadata.Version.Match( - "1.0", _ => Try.Success>(ArchiveVersion.v100), - "4.0", _ => Try.Success>(ArchiveVersion.v400), - "4.1", _ => Try.Success>(ArchiveVersion.v410), - "4.1.1", _ => Try.Success>(ArchiveVersion.v411), - _ => Try.Error>("Archive version is not supported.".ToReadOnlyList()) - ); - var archiveType = version.FlatMap(v => v.Match( - ArchiveVersion.v100, u => Try.Success>(ArchiveType.Archiving), - ArchiveVersion.v400, u => ParseVersion4ArchiveType(metadata), - ArchiveVersion.v410, u => ParseVersion4ArchiveType(metadata), - ArchiveVersion.v411, u => ParseVersion4ArchiveType(metadata) - )); + var version = metadata.Version switch + { + "1.0" => Try.Success>(ArchiveVersion.v100), + "4.0" => Try.Success>(ArchiveVersion.v400), + "4.1" => Try.Success>(ArchiveVersion.v410), + "4.1.1" => Try.Success>(ArchiveVersion.v411), + _ => Try.Error>($"Archive version {metadata.Version} is not supported.".ToReadOnlyList()) + }; + var archiveType = version.FlatMap(v => v switch + { + ArchiveVersion.v100 => Try.Success>(ArchiveType.Archiving), + ArchiveVersion.v400 => ParseVersion4ArchiveType(metadata), + ArchiveVersion.v410 => ParseVersion4ArchiveType(metadata), + ArchiveVersion.v411 => ParseVersion4ArchiveType(metadata), + _ => Try.Error>($"Archive type {v} is not supported.".ToReadOnlyList()) + }); var previousRecordSignature = metadata.PreviousRecordSignature.ToOption().Match( s => Signature.Create(s), _ => Try.Success>(null) @@ -59,11 +59,12 @@ public static Try> Create(Dto.Archive arc private static Try> ParseVersion4ArchiveType(Dto.ArchiveMetadata archiveMetadata) { - return archiveMetadata.ArchiveType.Match( - "DAY", _ => Try.Success>(ArchiveType.Day), - "MONTH", _ => Try.Success>(ArchiveType.Month), - "FISCALYEAR", _ => Try.Success>(ArchiveType.FiscalYear), - _ => Try.Error>($"{nameof(Model.ArchiveType)} is not supported.".ToReadOnlyList()) - ); + return archiveMetadata.ArchiveType switch + { + "DAY" => Try.Success>(ArchiveType.Day), + "MONTH" => Try.Success>(ArchiveType.Month), + "FISCALYEAR" => Try.Success>(ArchiveType.FiscalYear), + _ => Try.Error>($"Archive type {archiveMetadata.ArchiveType} is not supported.".ToReadOnlyList()) + }; } } \ No newline at end of file diff --git a/src/Model/Parser.cs b/src/Model/Parser.cs index 5acf68f..2b20ce5 100644 --- a/src/Model/Parser.cs +++ b/src/Model/Parser.cs @@ -1,7 +1,5 @@ -using System.Collections.Generic; using System.Globalization; using System.Text.RegularExpressions; -using FuncSharp; namespace Mews.Fiscalization.SignatureChecker.Model; diff --git a/src/Model/ReportedValue.cs b/src/Model/ReportedValue.cs index 945ddef..8151656 100644 --- a/src/Model/ReportedValue.cs +++ b/src/Model/ReportedValue.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using System.Linq; -using FuncSharp; - namespace Mews.Fiscalization.SignatureChecker.Model; internal sealed class ReportedValue @@ -15,12 +11,14 @@ private ReportedValue(Amount value) public static Try> Create(Dto.Archive archive, ArchiveVersion version) { - var reportedValue = version.Match( - ArchiveVersion.v100, _ => GetReportedValueV1(archive), - ArchiveVersion.v400, _ => GetReportedValueV4(archive), - ArchiveVersion.v410, _ => GetReportedValueV4(archive), - ArchiveVersion.v411, _ => GetReportedValueV4(archive) - ); + var reportedValue = version switch + { + ArchiveVersion.v100 => GetReportedValueV1(archive), + ArchiveVersion.v400 => GetReportedValueV4(archive), + ArchiveVersion.v410 => GetReportedValueV4(archive), + ArchiveVersion.v411 => GetReportedValueV4(archive), + _ => Try.Error>($"Unsupported archive version {version}.".ToReadOnlyList()) + }; return reportedValue.Map(value => new ReportedValue(value)); } diff --git a/src/Model/Signature.cs b/src/Model/Signature.cs index b1b0d1b..42312ce 100644 --- a/src/Model/Signature.cs +++ b/src/Model/Signature.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using FuncSharp; - namespace Mews.Fiscalization.SignatureChecker.Model; internal sealed class Signature @@ -29,6 +25,6 @@ public static Try> Create(string base64UrlStrin return Convert.FromBase64String(sourceBase64); }); - return value.Map(v => new Signature(base64UrlString, v)).MapError(e => "Failed to read signature.".ToReadOnlyList()); + return value.Map(v => new Signature(base64UrlString, v)).MapError(e => $"Failed to read signature: {e.Message}.".ToReadOnlyList()); } } \ No newline at end of file diff --git a/src/Model/TaxRate.cs b/src/Model/TaxRate.cs index 52e157d..9105196 100644 --- a/src/Model/TaxRate.cs +++ b/src/Model/TaxRate.cs @@ -1,6 +1,3 @@ -using System; -using FuncSharp; - namespace Mews.Fiscalization.SignatureChecker.Model; internal sealed class TaxRate : Product1, IComparable diff --git a/src/Model/TaxSummary.cs b/src/Model/TaxSummary.cs index f21bc73..e6bf18c 100644 --- a/src/Model/TaxSummary.cs +++ b/src/Model/TaxSummary.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using System.Linq; -using FuncSharp; - namespace Mews.Fiscalization.SignatureChecker.Model; internal sealed class TaxSummary @@ -15,12 +11,14 @@ private TaxSummary(IReadOnlyDictionary data) public static Try> Create(Dto.Archive archive, ArchiveVersion version) { - return version.Match( - ArchiveVersion.v100, _ => GetV1TaxSummary(archive), - ArchiveVersion.v400, _ => GetV4TaxSummary(archive), - ArchiveVersion.v410, _ => GetV4TaxSummary(archive), - ArchiveVersion.v411, _ => GetV4TaxSummary(archive) - ); + return version switch + { + ArchiveVersion.v100 => GetV1TaxSummary(archive), + ArchiveVersion.v400 => GetV4TaxSummary(archive), + ArchiveVersion.v410 => GetV4TaxSummary(archive), + ArchiveVersion.v411 => GetV4TaxSummary(archive), + _ => throw new ArgumentOutOfRangeException(nameof(version), version, "Invalid archive version.") + }; } private static TaxSummary Sum(IEnumerable summaries) diff --git a/src/Program.cs b/src/Program.cs index b65c03e..d360025 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,92 +1,131 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography; +using System.Security.Cryptography; using System.Text; -using FuncSharp; +using Mews.Fiscalization.SignatureChecker; using Mews.Fiscalization.SignatureChecker.Model; -namespace Mews.Fiscalization.SignatureChecker; - -internal class Program +while (true) { - public static void Main(string[] args) + Console.WriteLine("Enter the archive zip file path and options (e.g. 2025.zip --develop or 2025.zip --production):"); + + var input = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(input)) { - var (optionArguments, pathArguments) = args.Partition(a => a.StartsWith("--")); + LogMessage("Invalid input. Please try again.", isError: true); + continue; + } + var arguments = input.Split(' '); + var (optionArguments, pathArguments) = arguments.Partition(a => a.StartsWith("--")); - var archivePath = pathArguments.SingleOption().ToTry(_ => "Invalid arguments".ToReadOnlyList()); - var archiveFiles = archivePath.FlatMap(p => ZipFileReader.Read(p)); - var result = archiveFiles.FlatMap(files => - { - var archive = Archive.Create(files); - var cryptoServiceProvider = GetCryptoServiceProvider(optionArguments); - - return archive.Map(a => - { - var isArchiveValid = IsArchiveValid(a, cryptoServiceProvider, files); - return isArchiveValid.Match( - t => "Archive signature IS valid.", - f => "Archive signature IS NOT valid." - ); - }); - }); - - result.Match( - r => Console.WriteLine(r), - errors => Console.WriteLine(errors.MkLines()) - ); + if (!pathArguments.Any() || pathArguments.Count > 1) + { + LogMessage("Invalid path argument/s. One path argument is expected.", isError: true); + continue; } - private static RSACryptoServiceProvider GetCryptoServiceProvider(IEnumerable optionArguments) + var archivePath = pathArguments.SingleOption().ToTry(_ => "Invalid arguments".ToReadOnlyList()); + if (archivePath.IsError) { - var useDevelopProvider = optionArguments.Contains("--develop", StringComparer.InvariantCultureIgnoreCase); - return useDevelopProvider.Match( - t => CryptoServiceProvider.GetDevelop(), - f => CryptoServiceProvider.GetProduction() - ); + LogMessage(archivePath.Error.Get().MkLines(), isError: true); + continue; } - private static bool IsArchiveValid(Archive archive, RSACryptoServiceProvider cryptoServiceProvider, IEnumerable files) + var archiveFiles = archivePath.FlatMap(p => ZipFileReader.Read(p)); + if (archiveFiles.IsError) { - var computedSignature = ComputeSignature(archive, files); - var hashAlgorithmName = archive.Metadata.Version.Match( - ArchiveVersion.v100, _ => HashAlgorithmName.SHA1, - ArchiveVersion.v400, _ => HashAlgorithmName.SHA256, - ArchiveVersion.v410, _ => HashAlgorithmName.SHA256, - ArchiveVersion.v411, _ => HashAlgorithmName.SHA256 - ); - return cryptoServiceProvider.VerifyData(computedSignature, archive.Signature.Value, hashAlgorithmName, RSASignaturePadding.Pkcs1); + LogMessage(archiveFiles.Error.Get().MkLines(), isError: true); + continue; } - private static byte[] ComputeSignature(Archive archive, IEnumerable files) + var result = archiveFiles.FlatMap(files => ValidateArchive(files, optionArguments)); + + result.Match( + r => PrintResult(isValid: r), + errors => PrintResult(isValid: false, errors.MkLines()) + ); + + Console.WriteLine("Do you want to verify another file? (yes/no):"); + + var response = Console.ReadLine(); + if (!string.Equals(response?.Trim().ToLower(), "yes", StringComparison.InvariantCultureIgnoreCase)) { - var archiveFilesContentHash = archive.Metadata.Version.Match( - ArchiveVersion.v411, _ => - { - var applicableFiles = files.Where(f => f.Name.Contains(".csv") || f.Name.Contains(".html")); - var allFilesBytes = applicableFiles.SelectMany(f => Encoding.UTF8.GetBytes(f.Content)); - return SHA256.Create().ComputeHash(allFilesBytes.ToArray()).ToOption(); - }, - _ => Option.Empty() - ); - - var taxSummary = archive.TaxSummary; - var reportedValue = archive.ReportedValue; - var previousSignatureFlag = archive.Metadata.PreviousRecordSignature.Match( - _ => "Y", - _ => "N" - ); - var signatureProperties = new List - { - taxSummary.ToSignatureString(), - reportedValue.Value.ToSignatureString(), - archive.Metadata.Created.ToSignatureString(), - archive.Metadata.TerminalIdentification, - archive.Metadata.ArchiveType.ToString().ToUpperInvariant(), - archiveFilesContentHash.Map(h => Convert.ToBase64String(h)).GetOrNull(), - previousSignatureFlag, - archive.Metadata.PreviousRecordSignature.Map(s => s.Base64UrlString).GetOrElse("") - }; - return Encoding.UTF8.GetBytes(String.Join(",", signatureProperties.Where(p => p != null))); + break; } +} + +return; + +static void PrintResult(bool isValid, string message = null) +{ + isValid.Match( + t => LogMessage("Archive signature is valid."), + f => LogMessage($"Archive signature is not valid. {message}", isError: true) + ); +} + +static void LogMessage(string message, bool isError = false) +{ + Console.ForegroundColor = ConsoleColor.White; + Console.BackgroundColor = isError ? ConsoleColor.Red : ConsoleColor.Green; + Console.WriteLine(message); + Console.ResetColor(); +} + +static Try> ValidateArchive(IReadOnlyList files, IEnumerable optionArguments) +{ + var archive = Archive.Create(files); + var cryptoServiceProvider = GetCryptoServiceProvider(optionArguments); + + return archive.Map(a => IsArchiveValid(a, cryptoServiceProvider, files)); +} + +static RSACryptoServiceProvider GetCryptoServiceProvider(IEnumerable optionArguments) +{ + var useDevelopProvider = optionArguments.Contains("--develop", StringComparer.InvariantCultureIgnoreCase); + return useDevelopProvider ? CryptoServiceProvider.GetDevelop() : CryptoServiceProvider.GetProduction(); +} + +static bool IsArchiveValid(Archive archive, RSACryptoServiceProvider cryptoServiceProvider, IEnumerable files) +{ + var computedSignature = ComputeSignature(archive, files); + var hashAlgorithmName = archive.Metadata.Version switch + { + ArchiveVersion.v100 => HashAlgorithmName.SHA1, + ArchiveVersion.v400 => HashAlgorithmName.SHA256, + ArchiveVersion.v410 => HashAlgorithmName.SHA256, + ArchiveVersion.v411 => HashAlgorithmName.SHA256, + _ => throw new NotImplementedException("Invalid archive version.") + }; + return cryptoServiceProvider.VerifyData(computedSignature, archive.Signature.Value, hashAlgorithmName, RSASignaturePadding.Pkcs1); +} + +static byte[] ComputeSignature(Archive archive, IEnumerable files) +{ + var archiveFilesContentHash = archive.Metadata.Version.Match( + ArchiveVersion.v411, _ => + { + var applicableFiles = files.Where(f => f.Name.Contains(".csv") || f.Name.Contains(".html")); + var allFilesBytes = applicableFiles.SelectMany(f => Encoding.UTF8.GetBytes(f.Content)); + return SHA256.HashData(allFilesBytes.ToArray()).ToOption(); + }, + _ => Option.Empty() + ); + + var taxSummary = archive.TaxSummary; + var reportedValue = archive.ReportedValue; + var previousSignatureFlag = archive.Metadata.PreviousRecordSignature.Match( + _ => "Y", + _ => "N" + ); + var signatureProperties = new[] + { + taxSummary.ToSignatureString(), + reportedValue.Value.ToSignatureString(), + archive.Metadata.Created.ToSignatureString(), + archive.Metadata.TerminalIdentification, + archive.Metadata.ArchiveType.ToString().ToUpperInvariant(), + archiveFilesContentHash.GetOrNull(h => Convert.ToBase64String(h)), + previousSignatureFlag, + archive.Metadata.PreviousRecordSignature.Map(s => s.Base64UrlString).GetOrElse("") + }; + return Encoding.UTF8.GetBytes(string.Join(",", signatureProperties.Where(p => p is not null))); } \ No newline at end of file diff --git a/src/ZipFileReader.cs b/src/ZipFileReader.cs index 170f760..76f35a8 100644 --- a/src/ZipFileReader.cs +++ b/src/ZipFileReader.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; using System.IO; using System.IO.Compression; -using System.Linq; using System.Text; -using FuncSharp; namespace Mews.Fiscalization.SignatureChecker; @@ -12,7 +8,7 @@ internal static class ZipFileReader { public static Try, IReadOnlyList> Read(string path) { - var validPath = path.ToOption().Where(p => File.Exists(p)).ToTry(_ => "File does not exist.".ToReadOnlyList()); + var validPath = path.ToOption().Where(p => File.Exists(p)).ToTry(_ => $"File {path} does not exist.".ToReadOnlyList()); return validPath.FlatMap(p => { var entries = Try.Catch, Exception>(_ => @@ -21,7 +17,7 @@ internal static class ZipFileReader using var zip = new ZipArchive(stream, ZipArchiveMode.Read); return zip.Entries.Select(e => ReadEntry(e)).AsReadOnlyList(); }); - return entries.MapError(e => "Cannot read archive.".ToReadOnlyList()); + return entries.MapError(e => $"Cannot read archive: {e.Message}.".ToReadOnlyList()); }); }