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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 0 additions & 33 deletions Counter/DistrictsCsvReader.cs

This file was deleted.

7 changes: 1 addition & 6 deletions Counter/PartiesCsvReader.cs
Original file line number Diff line number Diff line change
@@ -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 {

Expand Down Expand Up @@ -36,7 +31,7 @@ public static class PartiesCsvReader {
public static List<PartyCsvRecord> 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<PartyCsvRecord>().ToList();
}
}
Expand Down
9 changes: 3 additions & 6 deletions Counter/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,19 @@ 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 <signature certificate path> <votes CSV path> [<decryption key path>] [<parties CSV path>] [<districts CSV path>]");
Console.WriteLine("Syntax: Counter <signature certificate path> <votes CSV path> [<decryption key path>] [<parties CSV path>]");
return;
}

var signatureCertificateFile = checkPath(sigCertPath);
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;
Expand All @@ -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);
Expand Down
19 changes: 1 addition & 18 deletions Counter/Results.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,11 @@ public ElectionResult GetOrAddElection(string electionId)

public class ElectionResult {

private readonly ConcurrentDictionary<string, DistrictResult> districtResults;

public string Id { get; }

public ElectionResult(string id) {
Id = id;
districtResults = new ConcurrentDictionary<string, DistrictResult>(StringComparer.InvariantCultureIgnoreCase);
}

public DistrictResult GetOrAddDistrict(string id)
=> districtResults.GetOrAdd(id ?? "", new DistrictResult(id));

public IEnumerable<DistrictResult> DistrictResults => districtResults.Values;
}

public class DistrictResult {

private readonly ConcurrentDictionary<string, PartyResult> partyResults;

public string Id { get; }

public DistrictResult(string id) {
public ElectionResult(string id) {
Id = id;
partyResults = new ConcurrentDictionary<string, PartyResult>(StringComparer.InvariantCultureIgnoreCase);
}
Expand Down
45 changes: 11 additions & 34 deletions Counter/ResultsCsvWriter.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using CsvHelper;
using CsvHelper.Configuration;
using CsvHelper;
using System;
using System.Collections.Generic;
using System.Globalization;
Expand All @@ -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; }
Expand All @@ -34,11 +29,9 @@ public class ResultsCsvWriter {
private const string NullVotesLabel = "Votos nulos";

private readonly List<PartyCsvRecord> parties;
private readonly List<DistrictCsvRecord> districts;

public ResultsCsvWriter(List<PartyCsvRecord> parties, List<DistrictCsvRecord> districts) {
public ResultsCsvWriter(List<PartyCsvRecord> parties) {
this.parties = parties;
this.districts = districts;
}

public void Write(ElectionResultCollection results, Stream outStream) {
Expand All @@ -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<ResultCsvRecord> getDistrictRecords(string electionId, string electionLabel, DistrictResult districtResult) {

var districtLabel = getDistrictLabel(districtResult);
private IEnumerable<ResultCsvRecord> getPartyRecords(string electionId, string electionLabel, IEnumerable<PartyResult> partyResults) {

// Check which parties need to be nullified

var nullifiedPartyResults = new List<PartyResult>();

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) {
Expand All @@ -82,28 +70,24 @@ private IEnumerable<ResultCsvRecord> 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,
Expand All @@ -114,12 +98,10 @@ private IEnumerable<ResultCsvRecord> 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,
Expand All @@ -128,12 +110,10 @@ private IEnumerable<ResultCsvRecord> 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),
Expand All @@ -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) {
Expand Down
11 changes: 10 additions & 1 deletion Counter/Util.cs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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,
};
}
}
60 changes: 6 additions & 54 deletions Counter/VoteCounter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand All @@ -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 {

Expand All @@ -30,7 +30,6 @@ public VoteBatch(int index, IEnumerable<Vote> encryptedVotes) {

private const int BatchSize = 1000;

private readonly Dictionary<int, RSA> serverPublicKeys = new Dictionary<int, RSA>();
private RSA decryptionKey;
private WebVaultKeyParameters decryptionKeyParams;
private WebVaultClient webVaultClient;
Expand Down Expand Up @@ -72,21 +71,6 @@ public async Task<ElectionResultCollection> 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
Expand Down Expand Up @@ -185,32 +169,21 @@ private async Task checkAndCountVotesAsync(ChannelReader<VoteBatch> 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<DecryptionTable> decryptChoicesAsync(List<Vote> 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);
return new DecryptionTable(ciphers, plaintexts);
}

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);
Expand All @@ -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
}
}
Loading