From b1a2dbf283cf8f862809a5d935008af283f11925 Mon Sep 17 00:00:00 2001 From: JohnLCaron Date: Tue, 16 Apr 2024 13:58:14 -0600 Subject: [PATCH] Changes to the Encryption APIs. Add RunAddEncryptedBallots CLI. Standardize CLI arguments. AddEncryptedBallot, RunEncryptBallot allows noDeviceNameInDir argument. Remove ballotOverrideDir: cannot write encrypted ballots to arbitrary place. Update DecryptedTallyOrBallot.compare(). RandomBallotProvider generates sns in (0, LONG.Max) --- README.md | 2 +- docs/CommandLineInterface.md | 102 +++++++++---- .../eg/cli/RunAddEncryptedBallots.kt | 112 +++++++++++++++ .../cryptobiotic/eg/cli/RunBatchEncryption.kt | 7 +- .../cryptobiotic/eg/cli/RunEncryptBallot.kt | 134 ++++++++++-------- .../eg/cli/RunExampleEncryption.kt | 33 +++-- .../eg/election/DecryptedTallyOrBallot.kt | 46 +++--- .../eg/encrypt/AddEncryptedBallot.kt | 23 +-- .../eg/encrypt/EncryptedBallotChain.kt | 13 +- .../eg/input/RandomBallotProvider.kt | 5 +- .../org/cryptobiotic/eg/publish/Consumer.kt | 5 +- .../org/cryptobiotic/eg/publish/Publisher.kt | 4 +- .../eg/publish/json/ConsumerJson.kt | 4 +- .../publish/json/ElectionRecordJsonPaths.kt | 26 ++-- .../eg/publish/json/PublisherJson.kt | 43 +++--- .../eg/cli/RunAddEncryptedBallotsTest.kt | 49 +++++++ .../eg/cli/RunEncryptBallotTest.kt | 44 +++--- .../eg/cli/RunExampleEncryptionTest.kt | 9 +- .../eg/decrypt/EncryptDecryptBallotTest.kt | 15 +- .../eg/encrypt/AddEncryptedBallotTest.kt | 45 +++++- .../eg/encrypt/AddEncryptedChallengedTest.kt | 2 +- 21 files changed, 482 insertions(+), 241 deletions(-) create mode 100644 src/main/kotlin/org/cryptobiotic/eg/cli/RunAddEncryptedBallots.kt create mode 100644 src/test/kotlin/org/cryptobiotic/eg/cli/RunAddEncryptedBallotsTest.kt diff --git a/README.md b/README.md index 489a47f..a03f18e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![License](https://img.shields.io/github/license/JohnLCaron/egk-ec)](https://github.com/JohnLCaron/egk-ec/blob/main/LICENSE.txt) ![GitHub branch checks state](https://img.shields.io/github/actions/workflow/status/JohnLCaron/egk-ec/unit-tests.yml) -![Coverage](https://img.shields.io/badge/coverage-90.5%25%20LOC%20(6924/7647)-blue) +![Coverage](https://img.shields.io/badge/coverage-90.5%25%20LOC%20(7001/7733)-blue) # ElectionGuard-Kotlin Elliptic Curve diff --git a/docs/CommandLineInterface.md b/docs/CommandLineInterface.md index 2f5a1c4..1b168b8 100644 --- a/docs/CommandLineInterface.md +++ b/docs/CommandLineInterface.md @@ -1,6 +1,6 @@ # EGK Workflow and Command Line Programs -last update 04/01/2024 +last update 04/16/2024 * [EGK Workflow and Command Line Programs](#egk-workflow-and-command-line-programs) @@ -12,6 +12,7 @@ last update 04/01/2024 * [Run trusted KeyCeremony](#run-trusted-keyceremony) * [Create fake input ballots](#create-fake-input-ballots) * [Encryption](#encryption) + * [Run AddEncryptedBallots](#run-addencryptedballots) * [Run Encrypt Ballot](#run-encrypt-ballot) * [Run Example Encryption](#run-example-encryption) * [Run Batch Encryption](#run-batch-encryption) @@ -53,16 +54,16 @@ last update 04/01/2024 2. Use existing fake ballots for testing in _src/test/data/fakeBallots_. 5. **Encryption**. - 1. The [_RunEncryptBallot_ CLI](#run-encrypt-ballot) reads a plaintext ballot from disk and writes its encryption to disk. - 2. The [_RunExampleEncryption_ CLI](#run-example-encryption) reads an ElectionInitialized record, generates fake plaintext - ballots, then calls RunEncryptBallot to encrypt the ballots. This can simulate more complex election - records with multiple voting devices. - 3. The [_RunBatchEncryption_ CLI](#run-batch-encryption) reads an ElectionInitialized record and input plaintext - ballots, encrypts the ballots and writes out EncryptedBallot records. If any input plaintext ballot fails validation, - it is annotated and written to a separate directory, and not encrypted. - 4. _org.cryptobiotic.eg.encrypt.AddEncryptedBallot_ is a class that your program calls to encrypt plaintext ballots + 1. The [_RunAddEncryptedBallots_ CLI](#run-addencryptedballots) reads plaintext ballots from a directory and + writes their encryptions into the specified election record. + 1. The [_RunEncryptBallot_ CLI](#run-encrypt-ballot) reads a plaintext ballot from disk and writes its encryption to disk. + 1. The [_RunExampleEncryption_ CLI](#run-example-encryption) Is an example of running RunEncryptBallot to encrypt ballots. + This can simulate more complex election records with multiple voting devices. + 1. The [_RunBatchEncryption_ CLI](#run-batch-encryption) reads plaintext ballots from a directory and writes their encryptions to the + specified election record. It is multithreaded. + 1. _org.cryptobiotic.eg.encrypt.AddEncryptedBallot_ is a class that your program calls to encrypt plaintext ballots and add them to the election record. (See _org.cryptobiotic.eg.cli.ExampleEncryption_ as an example of using AddEncryptedBallot). - 5. To run encryption with the Encryption server, see the webapps CLI. This allows you to run the encryption on a + 1. To run encryption with the Encryption server, see the webapps CLI. This allows you to run the encryption on a different machine than where ballots are generated, and/or to call from a non-JVM program. 6. **Accumulate Tally**. @@ -192,9 +193,40 @@ java -classpath build/libs/egk-ec-2.1-SNAPSHOT-uber.jar \ ## Encryption -Encryption is generally done on the voting device; running your own program linked into the egk-ec library gives you -maximum flexibility for voter challenges and ballot handling. The Encryption server (part of the webapps CLIs -also allows voter challenges. These CLIs have less flexibility but may be easier to use. +Encryption is usually done on the voting device; running your own program linked into the egk-ec library gives you +maximum flexibility for voter challenges and ballot handling. The Encryption server (part of the webapps CLIs) +also allows voter challenges. The CLIs documented here have less flexibility but are easier to use. + +### Run AddEncryptedBallots + +```` +Usage: RunExampleEncryption options_list +Options: + --inputDir, -in -> Directory containing input election record (always required) { String } + --ballotDir, -ballots -> Directory to read Plaintext ballots from (always required) { String } + --device, -device -> voting device name (always required) { String } + --outputDir, -out -> Directory to write output election record (always required) { String } + --challengePct, -challenge [0] -> Challenge percent of ballots { Int } + --help, -h -> Usage info +```` + +This reads plaintext ballots from ballotDir and writes their encryptions into the specified election record. + +If the config file has chainConfirmationCodes = true, then the ballots will be chained. + +Pass in "--challengePct percent" to simulate challenging this percent of ballots, randomly chosen. + +Example: + +```` +java -classpath build/libs/egk-ec-2.1-SNAPSHOT-uber.jar \ + org.cryptobiotic.eg.cli.RunAddEncryptedBallots \ + -in src/test/data/encrypt/testBallotChain \ + -ballot src/test/data/fakeBallots \ + -device device42 \ + -out testOut \ + -challenge 10 +```` ### Run Encrypt Ballot @@ -204,17 +236,22 @@ Options: --inputDir, -in -> Directory containing input election record (always required) { String } --device, -device -> voting device name (always required) { String } --ballotFilepath, -ballot -> Plaintext ballot filepath (or 'CLOSE') (always required) { String } + --outputDir, -out -> Directory to write output election record (always required) { String } --encryptBallotDir, -output -> Write encrypted ballot to this directory (always required) { String } - --help, -h -> Usage info + --noDeviceNameInDir, -deviceDir [false] -> Dont add device name to encrypted ballots directory + --help, -h -> Usage info + ```` -This reads one plaintext ballot from disk and writes its encryption into the specified directory, which must already exist. +This reads one plaintext ballot from disk and writes its encryption into the specified election record, which must already exist. + +The standard place to write encrypted ballots is _encryptBallotDir_ = _outputDir/encrypted_ballots/device/_. If not chaining, you may +specify --noDeviceNameInDir, then the encryptions are written to _outputDir/encrypted_ballots/_. -The standard place to write encrypted ballots is to _workingDir/encrypted_ballots/device/_. The encrypted file is always -named _eballot-ballotId.json_, where _ballotId_ is taken from the plaintext ballot. +The encrypted file is always named _eballot-ballotId.json_, where _ballotId_ is taken from the plaintext ballot. If the config file has chainConfirmationCodes = true, then RunEncryptBallot will expect to be able to read and write _ballot_chain.json_ in the _encryptBallotDir_ directory. The ballot chaining should be closed by sending -ballotFilename = "Close" when the chain is complete. +ballotFilename = "CLOSE" when the chain is complete. Example: @@ -222,9 +259,9 @@ Example: java -classpath build/libs/egk-ec-2.1-SNAPSHOT-uber.jar \ org.cryptobiotic.eg.cli.RunEncryptBallot \ -in src/test/data/encrypt/testBallotNoChain \ - -device device42 \ -ballot src/test/data/fakeBallots/pballot-id153737325.json \ - -output testOut/encrypted_ballots/device42/ + -device device42 \ + -out testOut ```` ### Run Example Encryption @@ -236,18 +273,19 @@ Options: --nballots, -nballots -> Number of test ballots to generate (always required) { Int } --plaintextBallotDir, -pballotDir -> Write plaintext ballots to this directory (always required) { String } --deviceNames, -device -> voting device name(s), comma delimited (always required) { String } - --encryptBallotDir, -eballotDir -> Write encrypted ballots to this directory (always required) { String } - --addDeviceNameToDir, -deviceDir -> Add device name to encrypted ballots directory [true] + --outputDir, -out -> Directory to write output election record (always required) { String } + --noDeviceNameInDir, -deviceDir [false] -> Dont add device name to encrypted ballots directory --help, -h -> Usage info ```` This is an example program that calls RunEncryptBallot to encrypt one ballot at a time, by generating fake ballots. -All ballots are cast (no challenges). +All ballots are cast (no challenges). It is meant as an example, not something used in production. There must be at least one device name. You can generate multiple chains by having multiple device names. In that case the ballots are randomly assigned to a device in the list. Chaining is controlled by the config file flag chainConfirmationCodes = true. If true, then RunExampleEncryption will -close the chain when done, and will always add the device name to the direcory. +close the chain when done, and will add the device name to the encryption directory. If not chaining, then the device name +will not be added to the encryption directory. Example: @@ -257,8 +295,8 @@ java -classpath build/libs/egk-ec-2.1-SNAPSHOT-uber.jar \ -in src/test/data/encrypt/testBallotChain \ -nballots 33 \ -pballotDir testOut/encrypt/RunExampleEncryptionTest/plaintext_ballots \ - -eballotDir testOut/encrypt/RunExampleEncryptionTest/encrypted_ballots \ -device device42,device11,yrnameHere + --outputDir testOut/encrypt/RunExampleEncryptionTest \ ```` ### Run Batch Encryption @@ -268,18 +306,20 @@ Usage: RunBatchEncryption options_list Options: --inputDir, -in -> Directory containing input election record (always required) { String } --ballotDir, -ballots -> Directory to read Plaintext ballots from (always required) { String } - --outputDir, -out -> Directory to write output election record { String } - --encryptDir, -eballots -> Write encrypted ballots here { String } --invalidDir, -invalid -> Directory to write invalid input ballots to { String } --check, -check [None] -> Check encryption { Value should be one of [none, verify, encrypttwice, decryptnonce] } --nthreads, -nthreads [11] -> Number of parallel threads to use { Int } --createdBy, -createdBy -> who created { String } --device, -device -> voting device name (always required) { String } - --cleanOutput, -clean [false] -> clean output dir + --outputDir, -out -> Directory to write output election record { String } + --noDeviceNameInDir, -deviceDir -> Dont add device name to encrypted ballots directory --anonymize, -anon [false] -> anonymize ballot --help, -h -> Usage info ```` -You must specify outputDir or encryptDir. The former copies ElectionInit and writes encrypted ballots to standard election record. + +This reads all plaintext ballot from ballotDir and writes their encryptions into outputDir or encryptDir. + +You must specify either outputDir or encryptDir. The former copies ElectionInit and writes encrypted ballots to standard election record. The latter writes just the encrypted ballots to the specified directory. This runs multithreaded, and you cannot use it to do ballot chaining or challenges. @@ -291,9 +331,8 @@ java -classpath build/libs/egk-ec-2.1-SNAPSHOT-uber.jar \ org.cryptobiotic.eg.cli.RunBatchEncryption \ -in src/test/data/keyceremony/runFakeKeyCeremonyAllEc \ -ballots src/test/data/fakeBallots \ - -out testOut/cliWorkflow/electionRecordEc \ -device device42 \ - --cleanOutput + -out testOut/testBatchEncryption ```` ## Tally @@ -328,6 +367,7 @@ output: Note that at this point in the cliWorkflow example, we are both reading from and writing to the electionRecord. A production workflow may be significantly different. + ## Decryption ### Run trusted Tally Decryption diff --git a/src/main/kotlin/org/cryptobiotic/eg/cli/RunAddEncryptedBallots.kt b/src/main/kotlin/org/cryptobiotic/eg/cli/RunAddEncryptedBallots.kt new file mode 100644 index 0000000..e8b7ad8 --- /dev/null +++ b/src/main/kotlin/org/cryptobiotic/eg/cli/RunAddEncryptedBallots.kt @@ -0,0 +1,112 @@ +package org.cryptobiotic.eg.cli + +import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.cli.ArgParser +import kotlinx.cli.ArgType +import kotlinx.cli.default +import kotlinx.cli.required +import org.cryptobiotic.eg.encrypt.AddEncryptedBallot +import org.cryptobiotic.eg.input.BallotInputValidation +import org.cryptobiotic.eg.input.ManifestInputValidation +import org.cryptobiotic.eg.publish.readElectionRecord +import org.cryptobiotic.util.ErrorMessages +import kotlin.random.Random + +/** + * This reads plaintext ballots from ballotDir and writes their encryptions into the specified election record. + */ +class RunAddEncryptedBallots { + + companion object { + private val logger = KotlinLogging.logger("RunAddEncryptedBallots") + + @JvmStatic + fun main(args: Array) { + val parser = ArgParser("RunAddEncryptedBallots") + val inputDir by parser.option( + ArgType.String, + shortName = "in", + description = "Directory containing input election record" + ).required() + val ballotDir by parser.option( + ArgType.String, + shortName = "ballots", + description = "Directory to read Plaintext ballots from" + ).required() + val device by parser.option( + ArgType.String, + shortName = "device", + description = "voting device name" + ).required() + val outputDir by parser.option( + ArgType.String, + shortName = "out", + description = "Directory to write output election record" + ).required() + val challengePct by parser.option( + ArgType.Int, + shortName = "challenge", + description = "Challenge percent of ballots" + ).default(0) + + parser.parse(args) + + logger.info { + "starting\n inputDir= $inputDir\n ballotDir = $ballotDir\n device = $device\n" + + " outputDir = $outputDir\n challengePct = $challengePct" + } + + val electionRecord = readElectionRecord(inputDir) + val electionInit = electionRecord.electionInit()!! + val consumerIn = electionRecord.consumer() + + val manifest = consumerIn.makeManifest(electionInit.config.manifestBytes) + val errors = ManifestInputValidation(manifest).validate() + if (ManifestInputValidation(manifest).validate().hasErrors()) { + logger.error { "ManifestInputValidation error ${errors}" } + throw RuntimeException("ManifestInputValidation error $errors") + } + val chaining = electionInit.config.chainConfirmationCodes + + var allOk = true + + val encryptor = AddEncryptedBallot( + electionRecord.manifest(), + BallotInputValidation(electionRecord.manifest()), + electionInit.config.chainConfirmationCodes, + electionInit.config.configBaux0, + electionInit.jointPublicKey, + electionInit.extendedBaseHash, + device, + outputDir, + "${outputDir}/invalidDir", + noDeviceNameInDir = !chaining + ) + + var countChallenge = 0 + consumerIn.iteratePlaintextBallots(ballotDir, null).forEach { pballot -> + val errs = ErrorMessages("AddEncryptedBallot ${pballot.ballotId}") + val encrypted = encryptor.encrypt(pballot, errs) + if (encrypted == null) { + logger.error{ "failed errors = $errs"} + allOk = false + } else { + val challengeThisOne = (challengePct != 0) && (Random.nextInt(100) > (100 - challengePct)) + if (challengeThisOne) { + encryptor.challenge(encrypted.confirmationCode) + countChallenge++ + } else { + encryptor.cast(encrypted.confirmationCode) + } + } + } + encryptor.close() + + if (allOk) { + logger.info { "success" } + } else { + logger.error { "failure" } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/cryptobiotic/eg/cli/RunBatchEncryption.kt b/src/main/kotlin/org/cryptobiotic/eg/cli/RunBatchEncryption.kt index 78abff3..c1ab36a 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/cli/RunBatchEncryption.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/cli/RunBatchEncryption.kt @@ -42,7 +42,7 @@ import org.cryptobiotic.util.Stopwatch * Read ElectionConfig from inputDir, write electionInit to outputDir. * Read plaintext ballots from ballotDir. * All ballots will be cast. - * Ballot chaining cannot be used here. + * Ballot chaining cannot be used, since ordering is indeterminate. */ class RunBatchEncryption { @@ -233,9 +233,7 @@ class RunBatchEncryption { // encryptDir is the exact encrypted ballot directory, outputDir is the election record topdir val publisher = makePublisher(encryptDir ?: outputDir!!, cleanOutput) - val sink: EncryptedBallotSinkIF = - if (encryptDir != null) publisher.encryptedBallotSink(null, true) - else publisher.encryptedBallotSink(device, true) + val sink: EncryptedBallotSinkIF = publisher.encryptedBallotSink(device) try { runBlocking { @@ -358,7 +356,6 @@ class RunBatchEncryption { } // coroutines allow parallel encryption at the ballot level - // TODO not possible to do ballot chaining, since the order is indeterminate? or do we just have to work harder? private fun CoroutineScope.launchEncryptor( id: Int, input: ReceiveChannel, diff --git a/src/main/kotlin/org/cryptobiotic/eg/cli/RunEncryptBallot.kt b/src/main/kotlin/org/cryptobiotic/eg/cli/RunEncryptBallot.kt index 352cb9b..d44311c 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/cli/RunEncryptBallot.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/cli/RunEncryptBallot.kt @@ -6,25 +6,22 @@ import com.github.michaelbull.result.unwrap import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.cli.ArgParser import kotlinx.cli.ArgType +import kotlinx.cli.default import kotlinx.cli.required -import org.cryptobiotic.eg.core.GroupContext import org.cryptobiotic.eg.core.pathExists import org.cryptobiotic.eg.election.EncryptedBallot import org.cryptobiotic.eg.encrypt.EncryptedBallotChain import org.cryptobiotic.eg.encrypt.Encryptor import org.cryptobiotic.eg.encrypt.submit import org.cryptobiotic.eg.input.ManifestInputValidation -import org.cryptobiotic.eg.publish.Consumer -import org.cryptobiotic.eg.publish.EncryptedBallotSinkIF -import org.cryptobiotic.eg.publish.makeConsumer -import org.cryptobiotic.eg.publish.makePublisher +import org.cryptobiotic.eg.publish.* import org.cryptobiotic.util.ErrorMessages /** * Reads a plaintext ballot from disk and writes its encryption to disk. - * Note that this does not allow for benolah challenge, ie voter submits a ballot, gets a confirmation code - * (with or without ballot chaining), then decide to challenge or cast. - * It does allow ballot chaining; if chaining then close the chain by calling with ballotFilepath="CLOSE". + * Note that this does not allow for benolah challenge. + * It does allow ballot chaining; if chaining, then make sure device is in encryptBallotDir, + * and close the chain by calling with ballotFilepath="CLOSE". */ class RunEncryptBallot { @@ -49,31 +46,33 @@ class RunEncryptBallot { shortName = "ballot", description = "Plaintext ballot filepath (or 'CLOSE')" ).required() - val encryptBallotDir by parser.option( + val outputDir by parser.option( ArgType.String, - shortName = "output", - description = "Write encrypted ballot to this directory" + shortName = "out", + description = "Directory to write output election record" ).required() + val noDeviceNameInDir by parser.option( + ArgType.Boolean, + shortName = "deviceDir", + description = "Dont add device name to encrypted ballots directory" + ).default(false) parser.parse(args) logger.info { - "starting\n inputDir= $inputDir\n ballotFilepath= $ballotFilepath\n encryptBallotDir = $encryptBallotDir\n device = $device" + "starting\n inputDir= $inputDir\n ballotFilepath= $ballotFilepath\n outputDir = $outputDir\n" + + " device = $device\n noDeviceNameInDir = $noDeviceNameInDir" } - val consumerIn = makeConsumer(inputDir) try { - if (ballotFilepath == "CLOSE") { - close(consumerIn.group, device, encryptBallotDir) - } else { - val retval = encryptBallot( - consumerIn, - ballotFilepath, - encryptBallotDir, - device, - ) - if (retval != 0) { - logger.info { "encryptBallot retval=$retval" } - } + val retval = encryptBallot( + makeConsumer(inputDir), + ballotFilepath, + outputDir, + device, + noDeviceNameInDir, + ) + if (retval != 0) { + logger.error { "failed retval=$retval" } } } catch (t: Throwable) { @@ -86,13 +85,48 @@ class RunEncryptBallot { ballotFilepath: String, encryptBallotDir: String, device: String, + noDeviceNameInDir: Boolean = false ): Int { + if (!pathExists(encryptBallotDir)) { + logger.error { "output Directory '$encryptBallotDir' must already exist" } + return 4 + } + val initResult = consumerIn.readElectionInitialized() if (initResult is Err) { logger.error { "readElectionInitialized error ${initResult.error}" } return 1 } val electionInit = initResult.unwrap() + val chaining = electionInit.config.chainConfirmationCodes + + val publisher = makePublisher(encryptBallotDir, false) + val sink: EncryptedBallotSinkIF = publisher.encryptedBallotSink( if (noDeviceNameInDir) null else device) + + if (ballotFilepath == "CLOSE") { + close(makeConsumer(encryptBallotDir, consumerIn.group), device, publisher) + return 0 + } + + val configBaux0 = electionInit.config.configBaux0 + var currentChain: EncryptedBallotChain? = null + val codeBaux = + if (!chaining) { + configBaux0 + } else { + val consumerChain = makeConsumer(encryptBallotDir, consumerIn.group) + // this reads in an existing chain, or starts one. + val pair = EncryptedBallotChain.makeCodeBaux( + consumerChain, + device, + configBaux0, + electionInit.extendedBaseHash + ) + currentChain = pair.second + pair.first + } + + // validate manifest val manifest = consumerIn.makeManifest(electionInit.config.manifestBytes) val errors = ManifestInputValidation(manifest).validate() if (ManifestInputValidation(manifest).validate().hasErrors()) { @@ -100,14 +134,7 @@ class RunEncryptBallot { return 2 } - val encryptor = Encryptor( - consumerIn.group, - manifest, - electionInit.jointPublicKey, - electionInit.extendedBaseHash, - device, - ) - + // read and validate ballot val result = consumerIn.readPlaintextBallot(ballotFilepath) if (result is Err) { logger.error { "readPlaintextBallot $result" } @@ -115,26 +142,14 @@ class RunEncryptBallot { } val ballot = result.unwrap() - if (!pathExists(encryptBallotDir)) { - logger.error { "output Directory '$encryptBallotDir' must already exist $result" } - return 4 - } - - val chaining = electionInit.config.chainConfirmationCodes - val configBaux0 = electionInit.config.configBaux0 - - var currentChain: EncryptedBallotChain? = null - val codeBaux = - if (!chaining) { - configBaux0 - } else { - val consumerChain = makeConsumer(encryptBallotDir, consumerIn.group) - // this reads in an existing chain, or starts one. - val pair = EncryptedBallotChain.makeCodeBaux(consumerChain, device, encryptBallotDir, configBaux0, electionInit.extendedBaseHash ) - currentChain = pair.second - pair.first - } - + // encryption + val encryptor = Encryptor( + consumerIn.group, + manifest, + electionInit.jointPublicKey, + electionInit.extendedBaseHash, + device, + ) val errs = ErrorMessages("Encrypt ${ballot.ballotId}") val cballot = encryptor.encrypt( ballot, @@ -149,15 +164,12 @@ class RunEncryptBallot { val eballot = cballot!!.submit(EncryptedBallot.BallotState.CAST) try { - val publisher = makePublisher(encryptBallotDir, false) - val sink: EncryptedBallotSinkIF = publisher.encryptedBallotSink(null) // null means ignore device name val fileout = sink.writeEncryptedBallot(eballot) logger.info { "success encrypted ballot written to '$fileout' " } if (chaining) { EncryptedBallotChain.writeChain( publisher, - encryptBallotDir, ballot.ballotId, eballot.confirmationCode, currentChain!! @@ -171,16 +183,14 @@ class RunEncryptBallot { } fun close( - group: GroupContext, + consumer: Consumer, device: String, - encryptBallotDir: String, + publisher: Publisher, ): Int { - val consumer = makeConsumer(encryptBallotDir, group) - val chainResult = consumer.readEncryptedBallotChain(device, encryptBallotDir) + val chainResult = consumer.readEncryptedBallotChain(device) val retval = if (chainResult is Ok) { val chain = chainResult.unwrap() - val publisher = makePublisher(encryptBallotDir) - val termval = EncryptedBallotChain.terminateChain(publisher, encryptBallotDir, chain) + val termval = EncryptedBallotChain.terminateChain(publisher, chain) if (termval != 0) { logger.info { "Cant terminateBallotChain retval=$termval" } } diff --git a/src/main/kotlin/org/cryptobiotic/eg/cli/RunExampleEncryption.kt b/src/main/kotlin/org/cryptobiotic/eg/cli/RunExampleEncryption.kt index fe1fecd..5e62d63 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/cli/RunExampleEncryption.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/cli/RunExampleEncryption.kt @@ -7,7 +7,6 @@ import kotlinx.cli.ArgParser import kotlinx.cli.ArgType import kotlinx.cli.default import kotlinx.cli.required -import org.cryptobiotic.eg.core.createDirectories import org.cryptobiotic.eg.input.ManifestInputValidation import org.cryptobiotic.eg.input.RandomBallotProvider import org.cryptobiotic.eg.publish.makeConsumer @@ -17,7 +16,7 @@ import kotlin.random.Random /** * Simulates using RunEncryptBallot one ballot at a time. * Note that chaining is controlled by config.chainConfirmationCodes, and handled by RunEncryptBallot. - * Note that this does not allow for benolah challenge, ie voter submits a ballot, gets a confirmation code + * Note that RunExampleEncryption does not allow for benolah challenge, ie voter submits a ballot, gets a confirmation code * (with or without ballot chaining), then decide to challenge or cast. So all ballots are cast. */ class RunExampleEncryption { @@ -48,22 +47,22 @@ class RunExampleEncryption { shortName = "device", description = "voting device name(s), comma delimited" ).required() - val encryptBallotDir by parser.option( + val outputDir by parser.option( ArgType.String, - shortName = "eballotDir", - description = "Write encrypted ballots to this directory" + shortName = "out", + description = "Directory to write output election record" ).required() - val addDeviceNameToDir by parser.option( + val noDeviceNameInDir by parser.option( ArgType.Boolean, shortName = "deviceDir", - description = "Add device name to encrypted ballots directory" - ).default(true) + description = "Dont add device name to encrypted ballots directory" + ).default(false) parser.parse(args) val devices = deviceNames.split(",") logger.info { "starting\n inputDir= $inputDir\n nballots= $nballots\n plaintextBallotDir = $plaintextBallotDir\n" + - " encryptBallotDir = $encryptBallotDir\n devices = $devices\n addDeviceNameToDir= $addDeviceNameToDir" + " outputDir = $outputDir\n devices = $devices\n noDeviceNameInDir= $noDeviceNameInDir" } val consumerIn = makeConsumer(inputDir) @@ -90,21 +89,27 @@ class RunExampleEncryption { val pballotFilename = "$plaintextBallotDir/pballot-${pballot.ballotId}.json" val deviceIdx = if(devices.size == 1) 0 else Random.nextInt(devices.size) val device = devices[deviceIdx] - val eballotDir = if (chaining || addDeviceNameToDir) "$encryptBallotDir/$device" else encryptBallotDir - createDirectories(eballotDir) + // val eballotDir = if (chaining || !noDeviceNameInDir) "$encryptBallotDir/$device" else encryptBallotDir + // createDirectories(eballotDir) val retval = RunEncryptBallot.encryptBallot( consumerIn, pballotFilename, - eballotDir, + outputDir, device, + noDeviceNameInDir, ) if (retval != 0) allOk = false } if (chaining) { devices.forEach { device -> - val eballotDir = "$encryptBallotDir/$device" - if (RunEncryptBallot.close(consumerIn.group, device, eballotDir) != 0) allOk = false + RunEncryptBallot.encryptBallot( + consumerIn, + "CLOSE", + outputDir, + device, + noDeviceNameInDir, + ) } } diff --git a/src/main/kotlin/org/cryptobiotic/eg/election/DecryptedTallyOrBallot.kt b/src/main/kotlin/org/cryptobiotic/eg/election/DecryptedTallyOrBallot.kt index 79f92b2..59ad014 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/election/DecryptedTallyOrBallot.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/election/DecryptedTallyOrBallot.kt @@ -1,9 +1,7 @@ package org.cryptobiotic.eg.election -import com.github.michaelbull.result.Err -import com.github.michaelbull.result.Ok -import com.github.michaelbull.result.Result import org.cryptobiotic.eg.core.* +import org.cryptobiotic.util.ErrorMessages /** * The decryption of one encrypted ballot or encrypted tally. @@ -16,7 +14,7 @@ import org.cryptobiotic.eg.core.* data class DecryptedTallyOrBallot( val id: String, val contests: List, - val electionId : UInt256, // threw this in to prevent accidental mixups + val electionId : UInt256, // prevent accidental mixups ) { data class Contest( @@ -33,7 +31,7 @@ data class DecryptedTallyOrBallot( data class DecryptedContestData( val contestData: ContestData, - val encryptedContestData : HashedElGamalCiphertext, // same as EncryptedTally.Contest.contestData + val encryptedContestData: HashedElGamalCiphertext, // same as EncryptedTally.Contest.contestData val proof: ChaumPedersenProof, var beta: ElementModP, // needed to verify 10.2 ) @@ -82,33 +80,35 @@ data class DecryptedTallyOrBallot( } } - fun compare(pballot: PlaintextBallot): Result { - val errs = mutableListOf() - if (pballot.contests.size != contests.size) { - errs.add("Number of contests differ ${pballot.contests.size} != ${contests.size}") - } + fun compare(pballot: PlaintextBallot, errs: ErrorMessages): Boolean { val pcontests = pballot.contests.associateBy { it.contestId } - contests.forEach { contest -> - val pcontest = pcontests[contest.contestId] + contests.forEach { dcontest -> + val pcontest = pcontests[dcontest.contestId] if (pcontest == null) { - errs.add("Cant find ${contest.contestId}") + checkAllZeroes(dcontest, errs.nested("PBallot missing contest=${dcontest.contestId}")) } else { - if (pcontest.selections.size != contest.selections.size) { - errs.add("Number of selections for ${contest.contestId} differ ${pcontest.selections.size} != ${contest.selections}") - } - val pselections = pcontest.selections.associateBy { it.selectionId } - contest.selections.forEach { selection -> - val pselection = pselections[selection.selectionId] + dcontest.selections.forEach { dselection -> + val pselection = pselections[dselection.selectionId] if (pselection == null) { - errs.add("Cant find ${contest.contestId}/${selection.selectionId}") + if (dselection.tally != 0) { + errs.add("${dcontest.contestId}.${dselection.selectionId} is not zero") + } } else { - if (pselection.vote != selection.tally) - errs.add(" Error ${contest.contestId}/${selection.selectionId} ${pselection.vote} != ${selection.tally}") + if (pselection.vote != dselection.tally) { + errs.add("${dcontest.contestId}/${dselection.selectionId} ${pselection.vote} != ${dselection.tally}") + } } } } } - return if (errs.isEmpty()) Ok(true) else Err(errs.joinToString(",")) + return !errs.hasErrors() } + + private fun checkAllZeroes(dcontest: DecryptedTallyOrBallot.Contest, errs: ErrorMessages) { + dcontest.selections.forEach { dselection -> + if (dselection.tally != 0) errs.add("${dcontest.contestId}.${dselection.selectionId} is not zero") + } + } + } \ No newline at end of file diff --git a/src/main/kotlin/org/cryptobiotic/eg/encrypt/AddEncryptedBallot.kt b/src/main/kotlin/org/cryptobiotic/eg/encrypt/AddEncryptedBallot.kt index 391dc69..872bc26 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/encrypt/AddEncryptedBallot.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/encrypt/AddEncryptedBallot.kt @@ -16,13 +16,12 @@ import org.cryptobiotic.eg.publish.makePublisher import org.cryptobiotic.util.ErrorMessages import java.io.Closeable -private val logger = KotlinLogging.logger("AddEncryptedBallot") - /** - * Encrypt a ballot and add to election record. Single threaded or thread confined only. - * Note that chaining is controlled by config.chainConfirmationCodes, and handled here. + * Encrypt a ballot and add to election record, optional chaining and/or challenging. + * Single threaded or thread confined only. + * Note that chaining is specified by config.chainConfirmationCodes, and implemented here. * Note that this allows for benolah challenge, ie voter submits a ballot, gets a confirmation code - * (with or without ballot chaining), then decide to challenge or cast. + * (with or without ballot chaining), then decides to challenge or cast. */ class AddEncryptedBallot( val manifest: ManifestIF, // must already be validated @@ -34,9 +33,10 @@ class AddEncryptedBallot( val device: String, val outputDir: String, // write ballots to outputDir/encrypted_ballots/deviceName, must not have multiple writers to same directory val invalidDir: String, // write plaintext ballots that fail validation + noDeviceNameInDir: Boolean = false, // if not chaining, dont add device into directory name ) : Closeable { val publisher = makePublisher(outputDir, false) - val consumerIn = makeConsumer(outputDir) + val consumerIn = makeConsumer(outputDir, jointPublicKey.context) // note that the encryptor doesnt know if its chained val encryptor = Encryptor( @@ -52,7 +52,8 @@ class AddEncryptedBallot( extendedBaseHash ) - private val sink: EncryptedBallotSinkIF = publisher.encryptedBallotSink(device) + private val sink: EncryptedBallotSinkIF = if (chaining || !noDeviceNameInDir) publisher.encryptedBallotSink(device) + else publisher.encryptedBallotSink(null) private var currentChain: EncryptedBallotChain? = null private val pending = mutableMapOf() // key = ccode.toHex() private var closed = false @@ -77,7 +78,6 @@ class AddEncryptedBallot( val pair = EncryptedBallotChain.makeCodeBaux( consumerIn, device, - null, configBaux0, extendedBaseHash, ) @@ -93,7 +93,6 @@ class AddEncryptedBallot( if (chaining) { currentChain = EncryptedBallotChain.writeChain( publisher, - null, ciphertextBallot.ballotId, ciphertextBallot.confirmationCode, currentChain!! @@ -182,7 +181,6 @@ class AddEncryptedBallot( if (chaining) { EncryptedBallotChain.terminateChain( publisher, - null, currentChain!! ) } @@ -193,4 +191,9 @@ class AddEncryptedBallot( sink.close() closed = true } + + companion object { + private val logger = KotlinLogging.logger("AddEncryptedBallot") + } + } \ No newline at end of file diff --git a/src/main/kotlin/org/cryptobiotic/eg/encrypt/EncryptedBallotChain.kt b/src/main/kotlin/org/cryptobiotic/eg/encrypt/EncryptedBallotChain.kt index fda3108..0485143 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/encrypt/EncryptedBallotChain.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/encrypt/EncryptedBallotChain.kt @@ -11,8 +11,6 @@ import org.cryptobiotic.eg.publish.Consumer import org.cryptobiotic.eg.publish.Publisher import org.cryptobiotic.util.ErrorMessages -// TODO error if ballotChain is already closed ?? - // Let Baux,0 = "Baux,0 must contain at least a unique voting device identifier and possibly other voting device // information as described above and as specified in the election manifest file." p 36. // @@ -41,7 +39,6 @@ data class EncryptedBallotChain( fun makeCodeBaux( consumer: Consumer, device: String, - ballotChainOverrideDir: String?, configBaux0: ByteArray, extendedBaseHash: UInt256, ): Pair { @@ -52,7 +49,7 @@ data class EncryptedBallotChain( // and still have the device name as part of the ballot chaining as required in the spec. val baux0 = if (configBaux0.isEmpty()) device.encodeToByteArray() else configBaux0 - val chainResult = consumer.readEncryptedBallotChain(device, ballotChainOverrideDir) + val chainResult = consumer.readEncryptedBallotChain(device) val codeBaux = if (chainResult is Ok) { if (showChain) print(" next ") chain = chainResult.unwrap() @@ -78,7 +75,6 @@ data class EncryptedBallotChain( // append ballotId, confirmationCode to the ballotChain fun writeChain( publisher: Publisher, - ballotChainOverrideDir: String?, ballotId: String, confirmationCode: UInt256, currentChain: EncryptedBallotChain @@ -87,7 +83,9 @@ data class EncryptedBallotChain( val newChain = currentChain.copy(ballotIds = ids, lastConfirmationCode = confirmationCode) try { - publisher.writeEncryptedBallotChain(newChain, ballotChainOverrideDir) + publisher.writeEncryptedBallotChain(newChain) + logger.info { "write $ballotId into chain " } + } catch (t: Throwable) { logger.error(t) { "error writing chain ${t.message}" } return null @@ -98,7 +96,6 @@ data class EncryptedBallotChain( // append finalConfirmationCode and close the ballotChain fun terminateChain( publisher: Publisher, - ballotChainOverrideDir: String?, currentChain: EncryptedBallotChain, ): Int { // The chain should be closed at the end of an election by forming and publishing @@ -112,7 +109,7 @@ data class EncryptedBallotChain( val ballotChain = currentChain.copy(closingHash = hashFinal) try { - publisher.writeEncryptedBallotChain(ballotChain, ballotChainOverrideDir) + publisher.writeEncryptedBallotChain(ballotChain) } catch (t: Throwable) { logger.error(t) { "error writing chain ${t.message}" } return 6 diff --git a/src/main/kotlin/org/cryptobiotic/eg/input/RandomBallotProvider.kt b/src/main/kotlin/org/cryptobiotic/eg/input/RandomBallotProvider.kt index 35a0af2..aa0152f 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/input/RandomBallotProvider.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/input/RandomBallotProvider.kt @@ -1,6 +1,7 @@ package org.cryptobiotic.eg.input import org.cryptobiotic.eg.election.* +import kotlin.math.abs import kotlin.random.Random /** Create nballots randomly generated fake Ballots, used for testing. */ @@ -51,8 +52,8 @@ class RandomBallotProvider(val manifest: Manifest, val nballots: Int = 11) { for (contestp in manifest.contestsForBallotStyle(useStyle)!!) { contests.add(makeContestFrom(contestp as Manifest.ContestDescription)) } - val sn = Random.nextInt(1,1000) - return PlaintextBallot(ballotId, useStyle, contests, sn.toLong()) + val sn = abs(Random.nextLong()) + return PlaintextBallot(ballotId, useStyle, contests, sn) } fun makeContestFrom(contest: Manifest.ContestDescription): PlaintextBallot.Contest { diff --git a/src/main/kotlin/org/cryptobiotic/eg/publish/Consumer.kt b/src/main/kotlin/org/cryptobiotic/eg/publish/Consumer.kt index 03a0a4e..60f6fe5 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/publish/Consumer.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/publish/Consumer.kt @@ -11,7 +11,6 @@ import org.cryptobiotic.eg.input.ManifestInputValidation import org.cryptobiotic.eg.publish.json.ConsumerJson import org.cryptobiotic.eg.publish.json.ElectionRecordJsonPaths import org.cryptobiotic.util.ErrorMessages -import java.nio.file.Path import java.util.function.Predicate private val logger = KotlinLogging.logger("Consumer") @@ -45,8 +44,8 @@ interface Consumer { fun encryptingDevices(): List /** Read encrypted ballots for specified device. */ fun iterateEncryptedBallots(device: String, filter : Predicate?): Iterable - /** Read encrypted ballot chain for the specified device. If the ballotDir is not overridden, then 'encrypted_ballots/device' will be used. */ - fun readEncryptedBallotChain(device: String, ballotOverrideDir: String? = null) : Result + /** Read encrypted ballot chain for the specified device. */ + fun readEncryptedBallotChain(device: String) : Result /** Read all decrypted ballots in the given directory, or the default if null. */ fun iterateDecryptedBallots(ballotOverrideDir: String? = null): Iterable diff --git a/src/main/kotlin/org/cryptobiotic/eg/publish/Publisher.kt b/src/main/kotlin/org/cryptobiotic/eg/publish/Publisher.kt index 6aaa635..2230e0d 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/publish/Publisher.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/publish/Publisher.kt @@ -17,8 +17,8 @@ interface Publisher { fun writeDecryptionResult(decryption: DecryptionResult) fun writeDecryptedTally(decryption: DecryptedTallyOrBallot) - fun encryptedBallotSink(device: String?, batched : Boolean = false): EncryptedBallotSinkIF - fun writeEncryptedBallotChain(closing: EncryptedBallotChain, ballotOverrideDir: String? = null) + fun encryptedBallotSink(device: String?): EncryptedBallotSinkIF + fun writeEncryptedBallotChain(closing: EncryptedBallotChain) fun decryptedBallotSink(ballotOverrideDir: String? = null): DecryptedBallotSinkIF fun writePlaintextBallot(outputDir: String, plaintextBallots: List) diff --git a/src/main/kotlin/org/cryptobiotic/eg/publish/json/ConsumerJson.kt b/src/main/kotlin/org/cryptobiotic/eg/publish/json/ConsumerJson.kt index c32feac..6c4d681 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/publish/json/ConsumerJson.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/publish/json/ConsumerJson.kt @@ -188,9 +188,9 @@ class ConsumerJson(val topDir: String, usegroup: GroupContext? = null) : Consume return Iterable { EncryptedBallotFileIterator(deviceDirPath, filter) } } - override fun readEncryptedBallotChain(device: String, ballotOverrideDir: String?) : Result { + override fun readEncryptedBallotChain(device: String) : Result { val errs = ErrorMessages("readEncryptedBallotChain device '$device'") - val ballotChainPath = Path.of(jsonPaths.encryptedBallotChain(device, ballotOverrideDir)) + val ballotChainPath = Path.of(jsonPaths.encryptedBallotChain(device)) if (!Files.exists(ballotChainPath)) { return errs.add("'$ballotChainPath' file does not exist") } diff --git a/src/main/kotlin/org/cryptobiotic/eg/publish/json/ElectionRecordJsonPaths.kt b/src/main/kotlin/org/cryptobiotic/eg/publish/json/ElectionRecordJsonPaths.kt index 75b02eb..19d4495 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/publish/json/ElectionRecordJsonPaths.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/publish/json/ElectionRecordJsonPaths.kt @@ -66,7 +66,6 @@ data class ElectionRecordJsonPaths(val topDir : String) { const val ENCRYPTED_DIR = "encrypted_ballots" const val CHALLENGED_DIR = "challenged_ballots" const val ENCRYPTED_BALLOT_CHAIN = "ballot_chain" - } fun manifestPath(): String { @@ -119,26 +118,21 @@ data class ElectionRecordJsonPaths(val topDir : String) { return "$electionRecordDir/$ENCRYPTED_DIR/" } - fun encryptedBallotDir(device: String): String { - val useDevice = device.replace(" ", "_") - return "${encryptedBallotDir()}/$useDevice/" - } - - fun encryptedBallotDevicePath(device: String?, ballotId: String): String { - val id = ballotId.replace(" ", "_") + fun encryptedBallotDir(device: String?): String { return if (device != null) { val useDevice = device.replace(" ", "_") - "${encryptedBallotDir(useDevice)}/${ENCRYPTED_BALLOT_PREFIX}$id${JSON_SUFFIX}" + "${encryptedBallotDir()}/$useDevice/" } else { - "${topDir}/${ENCRYPTED_BALLOT_PREFIX}$id${JSON_SUFFIX}" + encryptedBallotDir() } } - fun encryptedBallotChain(device: String, ballotOverrideDir: String?): String { - return if (ballotOverrideDir == null) { - "${encryptedBallotDir(device)}/${ENCRYPTED_BALLOT_CHAIN}${JSON_SUFFIX}" - } else { - "${ballotOverrideDir}/${ENCRYPTED_BALLOT_CHAIN}${JSON_SUFFIX}" - } + fun encryptedBallotDevicePath(device: String?, ballotId: String): String { + val id = ballotId.replace(" ", "_") + return "${encryptedBallotDir(device)}/${ENCRYPTED_BALLOT_PREFIX}$id${JSON_SUFFIX}" + } + + fun encryptedBallotChain(device: String?): String { + return "${encryptedBallotDir(device)}/${ENCRYPTED_BALLOT_CHAIN}${JSON_SUFFIX}" } } \ No newline at end of file diff --git a/src/main/kotlin/org/cryptobiotic/eg/publish/json/PublisherJson.kt b/src/main/kotlin/org/cryptobiotic/eg/publish/json/PublisherJson.kt index e474614..b1015a7 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/publish/json/PublisherJson.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/publish/json/PublisherJson.kt @@ -10,7 +10,9 @@ import org.cryptobiotic.eg.encrypt.EncryptedBallotChain import org.cryptobiotic.eg.publish.DecryptedBallotSinkIF import org.cryptobiotic.eg.publish.EncryptedBallotSinkIF import org.cryptobiotic.eg.publish.Publisher +import org.cryptobiotic.util.ErrorMessages import java.io.FileOutputStream +import java.io.IOException import java.nio.file.Files import java.nio.file.Path import java.util.* @@ -26,7 +28,9 @@ class PublisherJson(topDir: String, createNew: Boolean) : Publisher { if (createNew) { removeAllFiles(electionRecordDir) } - validateOutputDir(electionRecordDir, Formatter()) + val errs = ErrorMessages("PublisherJson dir=$electionRecordDir") + if (!validateOutputDir(electionRecordDir, errs)) + throw IOException("$errs") } override fun isJson() : Boolean = true @@ -116,9 +120,9 @@ class PublisherJson(topDir: String, createNew: Boolean) : Publisher { //////////////////////////////////////////////// - override fun writeEncryptedBallotChain(closing: EncryptedBallotChain, ballotOverrideDir: String?) { + override fun writeEncryptedBallotChain(closing: EncryptedBallotChain) { val jsonChain = closing.publishJson() - val filename = jsonPaths.encryptedBallotChain(closing.encryptingDevice, ballotOverrideDir) + val filename = jsonPaths.encryptedBallotChain(closing.encryptingDevice) FileOutputStream(filename).use { out -> jsonReader.encodeToStream(jsonChain, out) @@ -126,15 +130,16 @@ class PublisherJson(topDir: String, createNew: Boolean) : Publisher { } } - // batched is only used by proto, so is ignored here - override fun encryptedBallotSink(device: String?, batched: Boolean): EncryptedBallotSinkIF { - val ballotDir = if (device != null) jsonPaths.encryptedBallotDir(device) else jsonPaths.topDir - validateOutputDir(Path.of(ballotDir), Formatter()) // TODO - return EncryptedBallotDeviceSink(device) + override fun encryptedBallotSink(device: String?): EncryptedBallotSinkIF { + val ballotDir = jsonPaths.encryptedBallotDir(device) + val errs = ErrorMessages("encryptedBallotSink dir=$ballotDir") + if (!validateOutputDir(Path.of(ballotDir), errs)) + throw IOException("$errs") + return EncryptedBallotDeviceSink(ballotDir, device) } - inner class EncryptedBallotDeviceSink(val device: String?) : EncryptedBallotSinkIF { - + inner class EncryptedBallotDeviceSink(val ballotDir: String, val device: String?) : EncryptedBallotSinkIF { + fun ballotDir() = ballotDir override fun writeEncryptedBallot(eballot: EncryptedBallot): String { val ballotFile = jsonPaths.encryptedBallotDevicePath(device, eballot.ballotId) val json = eballot.publishJson() @@ -151,7 +156,10 @@ class PublisherJson(topDir: String, createNew: Boolean) : Publisher { ///////////////////////////////////////////////////////////// override fun decryptedBallotSink(ballotOverrideDir: String?): DecryptedBallotSinkIF { - validateOutputDir(Path.of(jsonPaths.decryptedBallotDir(ballotOverrideDir)), Formatter()) + val path = Path.of(jsonPaths.decryptedBallotDir(ballotOverrideDir)) + val errs = ErrorMessages("encryptedBallotSink dir=$path") + if (!validateOutputDir(path, errs)) + throw IOException("$errs") return DecryptedTallyOrBallotSink(ballotOverrideDir) } @@ -170,21 +178,18 @@ class PublisherJson(topDir: String, createNew: Boolean) : Publisher { } /** Make sure output directories exists and are writeable. */ -fun validateOutputDir(path: Path, error: Formatter): Boolean { +fun validateOutputDir(path: Path, errs: ErrorMessages): Boolean { if (!Files.exists(path)) { Files.createDirectories(path) } if (!Files.isDirectory(path)) { - error.format(" Output directory '%s' is not a directory%n", path) - return false + errs.add(" Output directory '$path' is not a directory") } if (!Files.isWritable(path)) { - error.format(" Output directory '%s' is not writeable%n", path) - return false + errs.add(" Output directory '$path' is not writeable") } if (!Files.isExecutable(path)) { - error.format(" Output directory '%s' is not executable%n", path) - return false + errs.add(" Output directory '$path' is not executable") } - return true + return !errs.hasErrors() } \ No newline at end of file diff --git a/src/test/kotlin/org/cryptobiotic/eg/cli/RunAddEncryptedBallotsTest.kt b/src/test/kotlin/org/cryptobiotic/eg/cli/RunAddEncryptedBallotsTest.kt new file mode 100644 index 0000000..15d3caa --- /dev/null +++ b/src/test/kotlin/org/cryptobiotic/eg/cli/RunAddEncryptedBallotsTest.kt @@ -0,0 +1,49 @@ +package org.cryptobiotic.eg.cli + +import org.cryptobiotic.util.Testing +import kotlin.test.* + +class RunAddEncryptedBallotsTest { + + @Test + fun testAddEncryptedBallotsWithChaining() { + val inputDir = "src/test/data/encrypt/testBallotChain" + val ballotDir = "src/test/data/fakeBallots" + val outputDir = "${Testing.testOut}/encrypt/testAddEncryptedBallotsWithChaining" + + RunAddEncryptedBallots.main( + arrayOf( + "--inputDir", inputDir, + "--ballotDir", ballotDir, + "--device", "device11", + "--outputDir", outputDir, + "--challengePct", "10", + ) + ) + + val count = verifyOutput(inputDir, outputDir, true) + assertTrue(count > 0) + } + + @Test + fun testAddEncryptedBallotsNoChaining() { + val inputDir = "src/test/data/encrypt/testBallotNoChain" + val ballotDir = "src/test/data/fakeBallots" + val outputDir = "${Testing.testOut}/encrypt/testAddEncryptedBallotsNoChaining" + + RunAddEncryptedBallots.main( + arrayOf( + "--inputDir", inputDir, + "--ballotDir", ballotDir, + "--device", "device11", + "--outputDir", outputDir, + "--challengePct", "10", + ) + ) + + val count = verifyOutput(inputDir, outputDir, false) + assertTrue(count > 0) + } + +} + diff --git a/src/test/kotlin/org/cryptobiotic/eg/cli/RunEncryptBallotTest.kt b/src/test/kotlin/org/cryptobiotic/eg/cli/RunEncryptBallotTest.kt index fa1b569..73514e4 100644 --- a/src/test/kotlin/org/cryptobiotic/eg/cli/RunEncryptBallotTest.kt +++ b/src/test/kotlin/org/cryptobiotic/eg/cli/RunEncryptBallotTest.kt @@ -2,8 +2,6 @@ package org.cryptobiotic.eg.cli import com.github.michaelbull.result.Err import com.github.michaelbull.result.Ok -import org.cryptobiotic.eg.core.createDirectories -import org.cryptobiotic.eg.core.removeAllFiles import org.cryptobiotic.eg.input.RandomBallotProvider import org.cryptobiotic.eg.publish.makeConsumer import org.cryptobiotic.eg.publish.makePublisher @@ -23,11 +21,12 @@ class RunEncryptBallotTest { val inputDir = "src/test/data/encrypt/testBallotNoChain" val outputDir = "${Testing.testOut}/encrypt/testRunEncryptBadPlaintextBallot" val consumerIn = makeConsumer(inputDir) + val publisher = makePublisher(outputDir, true) val retval = RunEncryptBallot.encryptBallot( consumerIn, "$outputDir/pballot-bad.json", - "$outputDir/encrypted_ballots", + outputDir, "device42", ) assertEquals(3, retval) @@ -46,14 +45,13 @@ class RunEncryptBallotTest { val ballotProvider = RandomBallotProvider(manifest) val ballot = ballotProvider.getFakeBallot(manifest, null, ballotId) - removeAllFiles(Path.of(outputDir)) val publisher = makePublisher(outputDir, true) publisher.writePlaintextBallot(outputDir, listOf(ballot)) val retval = RunEncryptBallot.encryptBallot( consumerIn, "$outputDir/pballot-$ballotId.json", - "nosuchdir", + "$outputDir/nosuchdir", "device42", ) assertEquals(4, retval) @@ -72,21 +70,19 @@ class RunEncryptBallotTest { val ballotProvider = RandomBallotProvider(manifest) val ballot = ballotProvider.getFakeBallot(manifest, null, ballotId) - removeAllFiles(Path.of(outputDir)) val publisher = makePublisher(outputDir, true) publisher.writePlaintextBallot(outputDir, listOf(ballot)) - createDirectories("$outputDir/encrypted_ballots") RunEncryptBallot.main( arrayOf( "--inputDir", inputDir, "--ballotFilepath", "$outputDir/pballot-$ballotId.json", - "--encryptBallotDir", "$outputDir/encrypted_ballots", + "--encryptBallotDir", outputDir, "-device", "device42", ) ) - assertTrue(Files.exists(Path.of("$outputDir/encrypted_ballots/eballot-$ballotId.json"))) + assertTrue(Files.exists(Path.of("$outputDir/encrypted_ballots/device42/eballot-$ballotId.json"))) val count = verifyOutput(inputDir, outputDir, false) assertEquals(1, count) @@ -103,10 +99,8 @@ class RunEncryptBallotTest { val record = readElectionRecord(consumerIn) val manifest = record.manifest() - removeAllFiles(Path.of(outputDir)) val publisher = makePublisher(outputDir, true) val consumerOut = makeConsumer(outputDir, consumerIn.group) - createDirectories("$outputDir/encrypted_ballots") val ballotProvider = RandomBallotProvider(manifest) repeat(nballots) { @@ -120,8 +114,9 @@ class RunEncryptBallotTest { arrayOf( "--inputDir", inputDir, "--ballotFilepath", ballotFilename, - "--encryptBallotDir", "$outputDir/encrypted_ballots", + "--encryptBallotDir", outputDir, "-device", device, + "--noDeviceNameInDir" ) ) @@ -142,29 +137,27 @@ class RunEncryptBallotTest { val inputDir = "src/test/data/encrypt/testBallotChain" val device = "device42" val outputDir = "${Testing.testOut}/encrypt/testRunEncryptBallotsChaining" - val outputDeviceDir = "$outputDir/encrypted_ballots/$device" val nballots = 10 val consumerIn = makeConsumer(inputDir) val record = readElectionRecord(consumerIn) val manifest = record.manifest() - val publisher = makePublisher(outputDeviceDir, true) + val publisher = makePublisher(outputDir, true) val consumerOut = makeConsumer(outputDir, consumerIn.group) - createDirectories(outputDeviceDir) val ballotProvider = RandomBallotProvider(manifest) repeat(nballots) { val ballotId = Random.nextInt().toString() val ballot = ballotProvider.getFakeBallot(manifest, null, ballotId) - publisher.writePlaintextBallot(outputDeviceDir, listOf(ballot)) + publisher.writePlaintextBallot(outputDir, listOf(ballot)) - val ballotFilename = "$outputDeviceDir/pballot-$ballotId.json" + val ballotFilename = "$outputDir/pballot-$ballotId.json" RunEncryptBallot.main( arrayOf( "--inputDir", inputDir, "--ballotFilepath", ballotFilename, - "--encryptBallotDir", outputDeviceDir, + "--encryptBallotDir", outputDir, "-device", device, ) ) @@ -180,7 +173,7 @@ class RunEncryptBallotTest { arrayOf( "--inputDir", inputDir, "--ballotFilepath", "CLOSE", - "--encryptBallotDir", outputDeviceDir, + "--encryptBallotDir", outputDir, "-device", device, ) ) @@ -194,13 +187,12 @@ class RunEncryptBallotTest { val inputDir = "src/test/data/encrypt/testBallotChain" val device = "device42" val outputDir = "${Testing.testOut}/encrypt/testRunEncryptBallotsChainingNotClosed" - val outputDeviceDir = "$outputDir/encrypted_ballots/$device" val nballots = 10 val consumerIn = makeConsumer(inputDir) val record = readElectionRecord(consumerIn) val manifest = record.manifest() - val publisher = makePublisher(outputDeviceDir, true) + val publisher = makePublisher(outputDir, true) val consumerOut = makeConsumer(outputDir, consumerIn.group) val ballotProvider = RandomBallotProvider(manifest) @@ -208,14 +200,14 @@ class RunEncryptBallotTest { val ballotId = Random.nextInt().toString() val ballot = ballotProvider.getFakeBallot(manifest, null, ballotId) - publisher.writePlaintextBallot(outputDeviceDir, listOf(ballot)) + publisher.writePlaintextBallot(outputDir, listOf(ballot)) - val ballotFilename = "$outputDeviceDir/pballot-$ballotId.json" + val ballotFilename = "$outputDir/pballot-$ballotId.json" RunEncryptBallot.main( arrayOf( "--inputDir", inputDir, "--ballotFilepath", ballotFilename, - "--encryptBallotDir", outputDeviceDir, + "--encryptBallotDir", outputDir, "-device", device, ) ) @@ -262,7 +254,7 @@ fun verifyOutput(inputDir: String, outputDir: String, chained: Boolean = false): val ballots = consumerBallots.iterateAllEncryptedBallots( null ) val errs = ErrorMessages("verifyBallots") val (ok, count) = verifier.verifyBallots(ballots, errs) - println(" verifyEncryptedBallots $outputDir: ok= $ok result= $errs") + println(" verifyEncryptedBallots $outputDir: ok= $ok count = $count result= $errs") assertFalse(errs.hasErrors()) if (chained) { @@ -278,7 +270,7 @@ fun verifyOutput(inputDir: String, outputDir: String, chained: Boolean = false): val chain2Errs = ErrorMessages("assembleAndVerifyChains") verifier.assembleAndVerifyChains(consumerBallots, chain2Errs) if (chain2Errs.hasErrors()) { - println(chain2Errs) + println(" $chain2Errs") } assertFalse(chain2Errs.hasErrors()) } diff --git a/src/test/kotlin/org/cryptobiotic/eg/cli/RunExampleEncryptionTest.kt b/src/test/kotlin/org/cryptobiotic/eg/cli/RunExampleEncryptionTest.kt index 2d2862d..29e6622 100644 --- a/src/test/kotlin/org/cryptobiotic/eg/cli/RunExampleEncryptionTest.kt +++ b/src/test/kotlin/org/cryptobiotic/eg/cli/RunExampleEncryptionTest.kt @@ -13,16 +13,15 @@ class RunExampleEncryptionTest { val outputDir = "${Testing.testOut}/encrypt/testExampleEncryptionWithChaining" val nballots = 33 - removeAllFiles(Path.of("$outputDir/encrypted_ballots")) + // removeAllFiles(Path.of("$outputDir/encrypted_ballots")) RunExampleEncryption.main( arrayOf( "--inputDir", inputDir, "--nballots", nballots.toString(), "--plaintextBallotDir", "$outputDir/plaintext", - "--encryptBallotDir", "$outputDir/encrypted_ballots", + "--encryptBallotDir", outputDir, "-device", "device42,device11", - "--addDeviceNameToDir", ) ) @@ -44,8 +43,8 @@ class RunExampleEncryptionTest { "--nballots", nballots.toString(), "--plaintextBallotDir", "$outputDir/plaintext", "-device", "device42,device11", - "--encryptBallotDir", "$outputDir/encrypted_ballots", - "--addDeviceNameToDir", + "--encryptBallotDir", "$outputDir", + "--noDeviceNameInDir", ) ) diff --git a/src/test/kotlin/org/cryptobiotic/eg/decrypt/EncryptDecryptBallotTest.kt b/src/test/kotlin/org/cryptobiotic/eg/decrypt/EncryptDecryptBallotTest.kt index 3a839af..869270f 100644 --- a/src/test/kotlin/org/cryptobiotic/eg/decrypt/EncryptDecryptBallotTest.kt +++ b/src/test/kotlin/org/cryptobiotic/eg/decrypt/EncryptDecryptBallotTest.kt @@ -1,6 +1,5 @@ package org.cryptobiotic.eg.decrypt -import com.github.michaelbull.result.Err import com.github.michaelbull.result.unwrap import org.cryptobiotic.eg.election.* import org.cryptobiotic.eg.core.* @@ -217,13 +216,13 @@ fun testEncryptDecryptCompare( return } - val result = decryptedBallot.compare(ballot) - if (result is Err) { - println("Error $result") - } else { - verifier.verify(decryptedBallot, true, errs.nested("verify"), Stats()) - println(errs) - assertFalse(errs.hasErrors()) + decryptedBallot.compare(ballot, errs) + if (errs.hasErrors()) { + println("decryptedBallot.compare failed errors = $errs") + return } + + verifier.verify(decryptedBallot, true, errs.nested("verify"), Stats()) + assertFalse(errs.hasErrors()) } } \ No newline at end of file diff --git a/src/test/kotlin/org/cryptobiotic/eg/encrypt/AddEncryptedBallotTest.kt b/src/test/kotlin/org/cryptobiotic/eg/encrypt/AddEncryptedBallotTest.kt index d5d6184..ca96de8 100644 --- a/src/test/kotlin/org/cryptobiotic/eg/encrypt/AddEncryptedBallotTest.kt +++ b/src/test/kotlin/org/cryptobiotic/eg/encrypt/AddEncryptedBallotTest.kt @@ -2,6 +2,9 @@ package org.cryptobiotic.eg.encrypt import com.github.michaelbull.result.Err import com.github.michaelbull.result.unwrap +import io.github.oshai.kotlinlogging.KotlinLogging +import org.cryptobiotic.eg.cli.RunEncryptBallot +import org.cryptobiotic.eg.cli.RunEncryptBallot.Companion import kotlin.test.* import org.cryptobiotic.eg.core.* @@ -20,10 +23,11 @@ class AddEncryptedBallotTest { val input = "src/test/data/workflow/allAvailableEc" val testDir = "${Testing.testOut}/encrypt/addEncryptedBallot/Plain" val nballots = 4 + private val logger = KotlinLogging.logger("AddEncryptedBallotTest") @Test - fun testJustOne() { - val outputDir = "$testDir/testJustOne" + fun testOneDevice() { + val outputDir = "$testDir/testOneDevice" val device = "device0" val electionRecord = readElectionRecord(input) @@ -46,7 +50,42 @@ class AddEncryptedBallotTest { repeat(nballots) { val ballot = ballotProvider.makeBallot() - val result = encryptor.encrypt(ballot, ErrorMessages("testJustOne")) + val result = encryptor.encrypt(ballot, ErrorMessages("testOneDevice")) + assertNotNull(result) + encryptor.submit(result.confirmationCode, EncryptedBallot.BallotState.CAST) + } + encryptor.close() + + checkOutput(outputDir, nballots, electionInit.config.chainConfirmationCodes) + } + + @Test + fun testOneDeviceNotInDir() { + val outputDir = "$testDir/testOneDeviceNotInDir" + val device = "device0" + + val electionRecord = readElectionRecord(input) + val electionInit = electionRecord.electionInit()!! + val publisher = makePublisher(outputDir, true) + publisher.writeElectionInitialized(electionInit) + + val encryptor = AddEncryptedBallot( + electionRecord.manifest(), + BallotInputValidation(electionRecord.manifest()), + electionInit.config.chainConfirmationCodes, + electionInit.config.configBaux0, + electionInit.jointPublicKey, + electionInit.extendedBaseHash, + device, + outputDir, + "${outputDir}/invalidDir", + noDeviceNameInDir = true + ) + val ballotProvider = RandomBallotProvider(electionRecord.manifest()) + + repeat(nballots) { + val ballot = ballotProvider.makeBallot() + val result = encryptor.encrypt(ballot, ErrorMessages("testOneDeviceNotInDir")) assertNotNull(result) encryptor.submit(result.confirmationCode, EncryptedBallot.BallotState.CAST) } diff --git a/src/test/kotlin/org/cryptobiotic/eg/encrypt/AddEncryptedChallengedTest.kt b/src/test/kotlin/org/cryptobiotic/eg/encrypt/AddEncryptedChallengedTest.kt index 22117f9..eb4d770 100644 --- a/src/test/kotlin/org/cryptobiotic/eg/encrypt/AddEncryptedChallengedTest.kt +++ b/src/test/kotlin/org/cryptobiotic/eg/encrypt/AddEncryptedChallengedTest.kt @@ -18,7 +18,7 @@ import org.cryptobiotic.util.ErrorMessages import org.cryptobiotic.util.Testing import kotlin.test.assertTrue -// check AddEncryptedBallot doesnt depend on the order that ballots are submitted +// Test challenging ballots when using AddEncryptedBallot. class AddEncryptedChallengedTest { val input = "src/test/data/workflow/allAvailableEc" val outputDirTop = "${Testing.testOut}/encrypt/addEncryptedBallot/ChallengedTest"