Skip to content

Commit

Permalink
Changes to the Encryption APIs.
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
JohnLCaron committed Apr 16, 2024
1 parent 566ce22 commit b1a2dbf
Show file tree
Hide file tree
Showing 21 changed files with 482 additions and 241 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
102 changes: 71 additions & 31 deletions docs/CommandLineInterface.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# EGK Workflow and Command Line Programs

last update 04/01/2024
last update 04/16/2024

<!-- TOC -->
* [EGK Workflow and Command Line Programs](#egk-workflow-and-command-line-programs)
Expand All @@ -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)
Expand Down Expand Up @@ -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**.
Expand Down Expand Up @@ -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

Expand All @@ -204,27 +236,32 @@ 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:

````
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
Expand All @@ -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:

Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
112 changes: 112 additions & 0 deletions src/main/kotlin/org/cryptobiotic/eg/cli/RunAddEncryptedBallots.kt
Original file line number Diff line number Diff line change
@@ -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<String>) {
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" }
}
}
}
}
7 changes: 2 additions & 5 deletions src/main/kotlin/org/cryptobiotic/eg/cli/RunBatchEncryption.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<PlaintextBallot>,
Expand Down
Loading

0 comments on commit b1a2dbf

Please sign in to comment.