diff --git a/Counter/DistrictsCsvReader.cs b/Counter/DistrictsCsvReader.cs deleted file mode 100644 index cbd4cef..0000000 --- a/Counter/DistrictsCsvReader.cs +++ /dev/null @@ -1,33 +0,0 @@ -using CsvHelper; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Counter { - - public class DistrictCsvRecord { - - public string SubscriptionId { get; set; } - - public string SubscriptionName { get; set; } - - public string DistrictId { get; set; } - - public string DistrictName { get; set; } - } - - public static class DistrictsCsvReader { - - public static List Read(FileInfo file) { - using var stream = file.OpenRead(); - using var streamReader = new StreamReader(stream); - using var csvReader = new CsvReader(streamReader, Thread.CurrentThread.CurrentCulture); - return csvReader.GetRecords().ToList(); - } - } -} diff --git a/Counter/PartiesCsvReader.cs b/Counter/PartiesCsvReader.cs index b06263e..4f4cdab 100644 --- a/Counter/PartiesCsvReader.cs +++ b/Counter/PartiesCsvReader.cs @@ -1,12 +1,7 @@ using CsvHelper; -using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; namespace Counter { @@ -36,7 +31,7 @@ public static class PartiesCsvReader { public static List Read(FileInfo file) { using var stream = file.OpenRead(); using var streamReader = new StreamReader(stream); - using var csvReader = new CsvReader(streamReader, Thread.CurrentThread.CurrentCulture); + using var csvReader = new CsvReader(streamReader, Util.CsvConfiguration); return csvReader.GetRecords().ToList(); } } diff --git a/Counter/Program.cs b/Counter/Program.cs index 5bcf616..b9f9c20 100644 --- a/Counter/Program.cs +++ b/Counter/Program.cs @@ -20,10 +20,9 @@ async static Task runAsync(string[] args) { var votesCsvPath = args.ElementAtOrDefault(1); var decKeyPath = args.ElementAtOrDefault(2); var partiesCsvPath = args.ElementAtOrDefault(3); - var districtsCsvPath = args.ElementAtOrDefault(4); if (string.IsNullOrEmpty(sigCertPath) || string.IsNullOrEmpty(votesCsvPath)) { - Console.WriteLine("Syntax: Counter [] [] []"); + Console.WriteLine("Syntax: Counter [] []"); return; } @@ -31,11 +30,9 @@ async static Task runAsync(string[] args) { var votesCsvFile = checkPath(votesCsvPath); var decryptionKeyFile = !string.IsNullOrEmpty(decKeyPath) ? checkPath(decKeyPath) : null; var partiesCsvFile = !string.IsNullOrEmpty(partiesCsvPath) ? checkPath(partiesCsvPath) : null; - var districtsCsvFile = !string.IsNullOrEmpty(districtsCsvPath) ? checkPath(districtsCsvPath) : null; - // Parties and districts are only needed later, but we'll read them ahead of time to raise exceptions sooner rather than later + // Parties are only needed later, but we'll read them ahead of time to raise exceptions sooner rather than later var parties = partiesCsvFile != null ? PartiesCsvReader.Read(partiesCsvFile) : null; - var districts = districtsCsvFile != null ? DistrictsCsvReader.Read(districtsCsvFile) : null; var degreeOfParallelismVar = Environment.GetEnvironmentVariable("COUNTER_WORKERS"); var degreeOfParallelism = !string.IsNullOrEmpty(degreeOfParallelismVar) ? int.Parse(degreeOfParallelismVar) : 32; @@ -51,7 +48,7 @@ async static Task runAsync(string[] args) { return; } - var resultsWriter = new ResultsCsvWriter(parties, districts); + var resultsWriter = new ResultsCsvWriter(parties); byte[] resultsFileBytes; using (var buffer = new MemoryStream()) { resultsWriter.Write(results, buffer); diff --git a/Counter/Results.cs b/Counter/Results.cs index b5da048..e5630bd 100644 --- a/Counter/Results.cs +++ b/Counter/Results.cs @@ -19,28 +19,11 @@ public ElectionResult GetOrAddElection(string electionId) public class ElectionResult { - private readonly ConcurrentDictionary districtResults; - - public string Id { get; } - - public ElectionResult(string id) { - Id = id; - districtResults = new ConcurrentDictionary(StringComparer.InvariantCultureIgnoreCase); - } - - public DistrictResult GetOrAddDistrict(string id) - => districtResults.GetOrAdd(id ?? "", new DistrictResult(id)); - - public IEnumerable DistrictResults => districtResults.Values; - } - - public class DistrictResult { - private readonly ConcurrentDictionary partyResults; public string Id { get; } - public DistrictResult(string id) { + public ElectionResult(string id) { Id = id; partyResults = new ConcurrentDictionary(StringComparer.InvariantCultureIgnoreCase); } diff --git a/Counter/ResultsCsvWriter.cs b/Counter/ResultsCsvWriter.cs index 6f19f28..d0c7825 100644 --- a/Counter/ResultsCsvWriter.cs +++ b/Counter/ResultsCsvWriter.cs @@ -1,5 +1,4 @@ -using CsvHelper; -using CsvHelper.Configuration; +using CsvHelper; using System; using System.Collections.Generic; using System.Globalization; @@ -17,10 +16,6 @@ public class ResultCsvRecord { public string ElectionLabel { get; set; } - public string DistrictId { get; set; } - - public string DistrictLabel { get; set; } - public string PartyIdentifier { get; set; } public string PartyLabel { get; set; } @@ -34,11 +29,9 @@ public class ResultsCsvWriter { private const string NullVotesLabel = "Votos nulos"; private readonly List parties; - private readonly List districts; - public ResultsCsvWriter(List parties, List districts) { + public ResultsCsvWriter(List parties) { this.parties = parties; - this.districts = districts; } public void Write(ElectionResultCollection results, Stream outStream) { @@ -47,31 +40,26 @@ public void Write(ElectionResultCollection results, Stream outStream) { foreach (var electionResult in results.ElectionResults) { var electionLabel = getElectionLabel(electionResult); - foreach (var districtResult in electionResult.DistrictResults) { - records.AddRange(getDistrictRecords(electionResult.Id, electionLabel, districtResult)); - } + records.AddRange(getPartyRecords(electionResult.Id, electionLabel, electionResult.PartyResults)); } var orderedRecords = records .OrderBy(r => r.ElectionLabel) - .ThenBy(r => r.DistrictLabel) .ThenBy(r => r.PartyLabel == BlankVotesLabel || r.PartyLabel == NullVotesLabel ? 1 : 0) .ThenByDescending(r => r.Votes); using var streamWriter = new StreamWriter(outStream, Encoding.UTF8); - using var csvWriter = new CsvWriter(streamWriter, Thread.CurrentThread.CurrentCulture); + using var csvWriter = new CsvWriter(streamWriter, Util.CsvConfiguration); csvWriter.WriteRecords(orderedRecords); } - private IEnumerable getDistrictRecords(string electionId, string electionLabel, DistrictResult districtResult) { - - var districtLabel = getDistrictLabel(districtResult); + private IEnumerable getPartyRecords(string electionId, string electionLabel, IEnumerable partyResults) { // Check which parties need to be nullified var nullifiedPartyResults = new List(); - foreach (var partyResult in districtResult.PartyResults) { + foreach (var partyResult in partyResults) { if (!partyResult.IsBlankOrNull) { var party = parties?.FirstOrDefault(p => p.PartyId.Equals(partyResult.Identifier, StringComparison.OrdinalIgnoreCase)); if (party != null && !party.IsEnabled) { @@ -82,28 +70,24 @@ private IEnumerable getDistrictRecords(string electionId, strin // Yield parties/blanks/nulls (except nullified ones) - foreach (var partyResult in districtResult.PartyResults.Except(nullifiedPartyResults)) { + foreach (var partyResult in partyResults.Except(nullifiedPartyResults)) { yield return new ResultCsvRecord { ElectionId = electionId, ElectionLabel = electionLabel, - DistrictId = districtResult.Id, - DistrictLabel = districtLabel, PartyIdentifier = partyResult.Identifier, PartyLabel = getPartyLabel(partyResult), Votes = partyResult.Votes + (partyResult.IsNull ? nullifiedPartyResults.Sum(npr => npr.Votes) : 0), }; } - // Yield enabled parties without votes (not in `districtResult.PartyResults`) + // Yield enabled parties without votes (not in `partyResults`) if (parties != null) { foreach (var party in parties.Where(p => p.IsEnabled && p.ElectionId.Equals(electionId, StringComparison.OrdinalIgnoreCase))) { - if (!districtResult.PartyResults.Any(r => r.Identifier.Equals(party.PartyId, StringComparison.OrdinalIgnoreCase))) { + if (!partyResults.Any(r => r.Identifier.Equals(party.PartyId, StringComparison.OrdinalIgnoreCase))) { yield return new ResultCsvRecord { ElectionId = electionId, ElectionLabel = electionLabel, - DistrictId = districtResult.Id, - DistrictLabel = districtLabel, PartyIdentifier = party.PartyId, PartyLabel = getPartyLabel(party), Votes = 0, @@ -114,12 +98,10 @@ private IEnumerable getDistrictRecords(string electionId, strin // Yield blanks row if not already yielded - if (!districtResult.PartyResults.Any(p => p.Identifier.Equals(PartyResult.BlankIdentifier, StringComparison.OrdinalIgnoreCase))) { + if (!partyResults.Any(p => p.Identifier.Equals(PartyResult.BlankIdentifier, StringComparison.OrdinalIgnoreCase))) { yield return new ResultCsvRecord { ElectionId = electionId, ElectionLabel = electionLabel, - DistrictId = districtResult.Id, - DistrictLabel = districtLabel, PartyIdentifier = PartyResult.BlankIdentifier, PartyLabel = BlankVotesLabel, Votes = 0, @@ -128,12 +110,10 @@ private IEnumerable getDistrictRecords(string electionId, strin // Yield nulls row if not already yielded - if (!districtResult.PartyResults.Any(p => p.Identifier.Equals(PartyResult.NullIdentifier, StringComparison.OrdinalIgnoreCase))) { + if (!partyResults.Any(p => p.Identifier.Equals(PartyResult.NullIdentifier, StringComparison.OrdinalIgnoreCase))) { yield return new ResultCsvRecord { ElectionId = electionId, ElectionLabel = electionLabel, - DistrictId = districtResult.Id, - DistrictLabel = districtLabel, PartyIdentifier = PartyResult.NullIdentifier, PartyLabel = NullVotesLabel, Votes = nullifiedPartyResults.Sum(npr => npr.Votes), @@ -146,9 +126,6 @@ private string getElectionLabel(ElectionResult electionResult) { return partyFromElection != null ? $"{partyFromElection.SubscriptionName} - {partyFromElection.ElectionName}" : null; } - private string getDistrictLabel(DistrictResult districtResult) - => districts?.FirstOrDefault(d => d.DistrictId.Equals(districtResult.Id, StringComparison.OrdinalIgnoreCase))?.DistrictName ?? "(não especificado)"; - private string getPartyLabel(PartyResult partyResult) { if (partyResult.Identifier == PartyResult.BlankIdentifier) { diff --git a/Counter/Util.cs b/Counter/Util.cs index 3200e46..55f5aaf 100644 --- a/Counter/Util.cs +++ b/Counter/Util.cs @@ -1,7 +1,9 @@ -using System; +using CsvHelper.Configuration; +using System; using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Counter { @@ -29,5 +31,12 @@ public static byte[] DecodePem(string pem) { var base64 = string.Join("", lines.Where(l => !l.StartsWith("---"))); return Convert.FromBase64String(base64); } + + public static CsvConfiguration CsvConfiguration => new CsvConfiguration(Thread.CurrentThread.CurrentCulture) { + Delimiter = ";", + HasHeaderRecord = true, + IgnoreBlankLines = true, + TrimOptions = TrimOptions.Trim, + }; } } diff --git a/Counter/VoteCounter.cs b/Counter/VoteCounter.cs index 837d51b..2babe6f 100644 --- a/Counter/VoteCounter.cs +++ b/Counter/VoteCounter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -12,7 +12,7 @@ namespace Counter { public class VoteCounter { - private record Vote(int PoolId, int Slot, byte[] EncodedValue, byte[] CmsSignature, byte[] ServerSignature, int ServerInstanceId, Asn1Vote Value); + private record Vote(byte[] EncodedValue, byte[] CmsSignature, Asn1VoteChoice Value); private class VoteBatch { @@ -30,7 +30,6 @@ public VoteBatch(int index, IEnumerable encryptedVotes) { private const int BatchSize = 1000; - private readonly Dictionary serverPublicKeys = new Dictionary(); private RSA decryptionKey; private WebVaultKeyParameters decryptionKeyParams; private WebVaultClient webVaultClient; @@ -72,21 +71,6 @@ public async Task CountAsync(FileInfo votesCsvFile, in var results = new ElectionResultCollection(); - Console.Write("Reading server keys ..."); - var voteIndex = 0; - using (var votesCsvReader = VotesCsvReader.Open(votesCsvFile)) { - foreach (var voteRecord in votesCsvReader.GetRecords()) { - if (!serverPublicKeys.ContainsKey(voteRecord.ServerInstanceId)) { - serverPublicKeys[voteRecord.ServerInstanceId] = getPublicKey(Util.DecodeHex(voteRecord.ServerPublicKey)); - Console.Write("."); - } - if (++voteIndex % 1000 == 0) { - Console.Write("."); - } - } - } - Console.WriteLine(); - if (decryptionKey != null || decryptionKeyParams != null) { // Decryption key given, check and count votes @@ -185,17 +169,14 @@ private async Task checkAndCountVotesAsync(ChannelReader inQueue, Ele } private Vote decodeVote(VoteCsvRecord csvEntry) { - var poolId = csvEntry.PoolId; - var slot = csvEntry.Slot; var encodedValue = Util.DecodeHex(csvEntry.Value); var cmsSignature = Util.DecodeHex(csvEntry.CmsSignature); - var serverSignature = Util.DecodeHex(csvEntry.ServerSignature); var value = VoteEncoding.Decode(encodedValue); - return new Vote(poolId, slot, encodedValue, cmsSignature, serverSignature, csvEntry.ServerInstanceId, value); + return new Vote(encodedValue, cmsSignature, value); } private async Task decryptChoicesAsync(List votes) { - var ciphers = votes.SelectMany(v => v.Value.Choices.Select(c => c.EncryptedChoice)); + var ciphers = votes.Select(v => v.Value.EncryptedChoice); var plaintexts = decryptionKey != null ? ciphers.Select(c => decryptionKey.Decrypt(c, RSAEncryptionPadding.OaepSHA256)) : await webVaultClient.DecryptBatchAsync(decryptionKeyParams.KeyId, ciphers); @@ -203,14 +184,6 @@ private async Task decryptChoicesAsync(List votes) { } private void checkVote(Vote vote) { - - // Check server signature - var serverSigOk = verifyServerSignature(serverPublicKeys[vote.ServerInstanceId], vote.CmsSignature, vote.ServerSignature) - || serverPublicKeys.Any(pk => verifyServerSignature(pk.Value, vote.CmsSignature, vote.ServerSignature)); - if (!serverSigOk) { - throw new Exception($"Vote on pool {vote.PoolId} slot {vote.Slot} has an invalid server signature"); - } - var cmsInfo = CmsEncoding.Decode(vote.CmsSignature); var expectedMessageDigestValue = HashAlgorithm.Create(cmsInfo.MessageDigest.Algorithm.Name).ComputeHash(vote.EncodedValue); @@ -228,35 +201,14 @@ private void checkVote(Vote vote) { if (!signatureCertificatePublicKey.VerifyHash(cmsInfo.SignedAttributesDigest.Value, cmsInfo.Signature, cmsInfo.SignedAttributesDigest.Algorithm, RSASignaturePadding.Pkcs1)) { throw new Exception("Signature mismatch"); } - - // Check PoolId and Slot integrity - if (vote.PoolId != vote.Value.PoolId || vote.Slot != vote.Value.Slot) { - throw new Exception("Vote address corruption"); - } } - private bool verifyServerSignature(RSA serverPublicKey, byte[] data, byte[] signature) - => serverPublicKey.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - private void countVote(ElectionResultCollection results, DecryptionTable choiceDecryptions, Vote vote) { - foreach (var choice in vote.Value.Choices) { - var decryptedChoice = Encoding.UTF8.GetString(choiceDecryptions.GetDecryption(choice.EncryptedChoice)); + var decryptedChoice = Encoding.UTF8.GetString(choiceDecryptions.GetDecryption(vote.Value.EncryptedChoice)); results - .GetOrAddElection(choice.ElectionId) - .GetOrAddDistrict(choice.DistrictId) + .GetOrAddElection(vote.Value.ElectionId) .GetOrAddParty(decryptedChoice) .Increment(); - } } - - #region Helper methods - - private RSA getPublicKey(byte[] encodedPublicKey) { - var rsa = RSA.Create(); - rsa.ImportSubjectPublicKeyInfo(encodedPublicKey, out _); - return rsa; - } - - #endregion } } diff --git a/Counter/VoteEncoding.cs b/Counter/VoteEncoding.cs index 5619971..0f848a6 100644 --- a/Counter/VoteEncoding.cs +++ b/Counter/VoteEncoding.cs @@ -7,53 +7,18 @@ namespace Counter { public static class VoteEncoding { - public static Asn1Vote Decode(byte[] encoded) => new Asn1Vote(new Asn1InputStream(encoded).ReadObject()); - } - - public class Asn1Vote { - - public Asn1Vote(Asn1Encodable asn1Object) { - var seq = (Asn1Sequence)asn1Object; - Choices = new Asn1VoteChoiceList(seq[0]); - PoolId = ((DerInteger)seq[1]).Value.IntValue; - Slot = ((DerInteger)seq[2]).Value.IntValue; - } - - public Asn1VoteChoiceList Choices { get; } - - public int PoolId { get; } - - public int Slot { get; } - } - - public class Asn1VoteChoiceList : List { - - public Asn1VoteChoiceList(Asn1Encodable asn1Object) : base(decode(asn1Object)) { - } - - private static IEnumerable decode(Asn1Encodable asn1Object) { - var seq = (Asn1Sequence)asn1Object; - var choices = new List(); - foreach (var item in seq) { - choices.Add(new Asn1VoteChoice(item)); - } - return choices; - } + public static Asn1VoteChoice Decode(byte[] encoded) => new Asn1VoteChoice(new Asn1InputStream(encoded).ReadObject()); } public class Asn1VoteChoice { - - public string ElectionId { get; } - - public string DistrictId { get; } + public string ElectionId{ get; } public byte[] EncryptedChoice { get; } public Asn1VoteChoice(Asn1Encodable asn1Object) { var seq = (Asn1Sequence)asn1Object; ElectionId = ((DerPrintableString)seq[0]).GetString(); - DistrictId = ((DerPrintableString)seq[1]).GetString(); - EncryptedChoice = ((DerOctetString)seq[2]).GetOctets(); + EncryptedChoice = ((DerOctetString)seq[1]).GetOctets(); } } } diff --git a/Counter/VotesCsvReader.cs b/Counter/VotesCsvReader.cs index 41e98ca..2840830 100644 --- a/Counter/VotesCsvReader.cs +++ b/Counter/VotesCsvReader.cs @@ -1,29 +1,14 @@ -using CsvHelper; +using CsvHelper; using System; using System.Collections.Generic; -using System.Globalization; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Counter { public class VoteCsvRecord { - - public int PoolId { get; set; } - - public int Slot { get; set; } - public string Value { get; set; } public string CmsSignature { get; set; } - - public string ServerSignature { get; set; } - - public int ServerInstanceId { get; set; } - - public string ServerPublicKey { get; set; } } public class VotesCsvReader : IDisposable { @@ -35,7 +20,7 @@ public class VotesCsvReader : IDisposable { public static VotesCsvReader Open(FileInfo file) { var stream = file.OpenRead(); var streamReader = new StreamReader(stream); - var csvReader = new CsvReader(streamReader, CultureInfo.InvariantCulture /* default in SSMS is exporting with commas regardless of the OS culture */); + var csvReader = new CsvReader(streamReader, Util.CsvConfiguration); return new VotesCsvReader(stream, streamReader, csvReader); }