Skip to content

Commit

Permalink
Merge pull request #424 from JohnLCaron/encryptErrors
Browse files Browse the repository at this point in the history
Eliminate Exceptions from encryption code
  • Loading branch information
JohnLCaron authored Nov 6, 2023
2 parents 753d429 + 92fac81 commit c308003
Show file tree
Hide file tree
Showing 17 changed files with 156 additions and 106 deletions.
10 changes: 8 additions & 2 deletions egklib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@ version = "2.0.0-SNAPSHOT"

kotlin {
jvm {
compilations.all { kotlinOptions.jvmTarget = "1.8" }
// withJava()
compilations.all {
kotlinOptions.jvmTarget = "1.8"
kotlinOptions.freeCompilerArgs = listOf(
"-Xexpect-actual-classes",
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi,kotlinx.serialization.ExperimentalSerializationApi"
)
}

testRuns["test"].executionTask
.configure {
useJUnitPlatform()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ private val logger = KotlinLogging.logger("AddEncryptedBallot")
/** Encrypt a ballot and add to election record. Single threaded only. */
class AddEncryptedBallot(
val group: GroupContext,
val manifest: Manifest,
val manifest: Manifest, // should already be validated
val electionInit: ElectionInitialized,
val deviceName: String,
val outputDir: String, // write ballots to outputDir/encrypted_ballots/deviceName, must not have multiple writers to same directory
Expand All @@ -46,23 +46,17 @@ class AddEncryptedBallot(

val publisher = makePublisher(outputDir, false, isJson)
val sink: EncryptedBallotSinkIF = publisher.encryptedBallotSink(deviceName)
val ballotIds = mutableListOf<String>()
val pending = mutableMapOf<String, CiphertextBallot>() // key = ccode.toHex()
val configBaux0: ByteArray = electionInit.config.configBaux0
val configChaining: Boolean = electionInit.config.chainConfirmationCodes
val baux0: ByteArray

private val ballotIds = mutableListOf<String>()
private val pending = mutableMapOf<UInt256, CiphertextBallot>() // key = ccode.toHex()
private var lastConfirmationCode: UInt256 = UInt256.ZERO
private var first = true
private var closed = false

init {
val manifestValidator = ManifestInputValidation(manifest)
val errors = manifestValidator.validate()
if (errors.hasErrors()) {
throw RuntimeException("ManifestInputValidation error $errors")
}

val consumer = makeConsumer(group, outputDir, isJson)
val chainResult = consumer.readEncryptedBallotChain(deviceName)
if (chainResult is Ok) {
Expand Down Expand Up @@ -106,7 +100,7 @@ class AddEncryptedBallot(
this.lastConfirmationCode = ciphertextBallot.confirmationCode

// hmmm you could write CiphertextBallot to a log, in case of crash
pending[ciphertextBallot.confirmationCode.toHex()] = ciphertextBallot
pending[ciphertextBallot.confirmationCode] = ciphertextBallot
return Ok(ciphertextBallot)
}

Expand All @@ -122,13 +116,13 @@ class AddEncryptedBallot(
submit(eballot.confirmationCode, EncryptedBallot.BallotState.CAST)
} else {
// remove from pending
pending.remove(eballot.confirmationCode.toHex())
pending.remove(eballot.confirmationCode)
}
return Ok(eballot)
}

fun submit(ccode: UInt256, state: EncryptedBallot.BallotState): Result<Boolean, String> {
val cballot = pending.remove(ccode.toHex())
val cballot = pending.remove(ccode)
if (cballot == null) {
logger.error { "Tried to submit state=$state unknown ballot ccode=$ccode" }
return Err("Tried to submit state=$state unknown ballot ccode=$ccode")
Expand All @@ -152,7 +146,7 @@ class AddEncryptedBallot(
}

fun challengeAndDecrypt(ccode: UInt256): Result<PlaintextBallot, String> {
val cballot = pending.remove(ccode.toHex())
val cballot = pending.remove(ccode)
if (cballot == null) {
logger.error { "Tried to submit unknown ballot ccode=$ccode" }
return Err("Tried to submit unknown ballot ccode=$ccode")
Expand All @@ -177,11 +171,9 @@ class AddEncryptedBallot(
// write out pending encryptedBallots, and chain (if chainCodes is true)
fun sync() {
if (pending.isNotEmpty()) {
val keys = pending.keys.toList()
keys.forEach {
logger.error { "pending Ciphertext ballot ${it} was not submitted" }
val ba = it.fromHex() ?: throw RuntimeException("illegal confirmation code")
submit(UInt256(ba), EncryptedBallot.BallotState.UNKNOWN)
pending.keys.forEach {
logger.error { "pending Ciphertext ballot ${it} was not submitted, marking 'UNKNOWN'" }
submit(it, EncryptedBallot.BallotState.UNKNOWN)
}
}
val closing =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import io.github.oshai.kotlinlogging.KotlinLogging
private val logger = KotlinLogging.logger("ManifestInputValidation")

/**
* Validate an election manifest, give human readable error information.
* See [ElectionGuard Input Validation](https://github.com/danwallach/electionguard-kotlin-multiplatform/blob/main/docs/InputValidation.md)
* Validate an election manifest, return human readable error information.
* See [Input Validation](https://github.com/votingworks/electionguard-kotlin-multiplatform/blob/main/docs/InputValidation.md)
*/
class ManifestInputValidation(val manifest: Manifest) {
private val gpUnits: Set<String> = manifest.geopoliticalUnits.map { it.geopoliticalUnitId }.toSet()
Expand Down
48 changes: 32 additions & 16 deletions egklib/src/commonMain/kotlin/electionguard/preencrypt/PreBallot.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package electionguard.preencrypt

import electionguard.core.*
import electionguard.util.ErrorMessages

/**
* Intermediate working ballot to transform pre encrypted ballot to an Encrypted ballot.
Expand Down Expand Up @@ -31,7 +32,7 @@ internal data class PreContest(
val votedFor: List<Boolean> // nselections, in order by sequence_order
) {
init {
require(votedFor.size == allSelectionHashes.size - selectedVectors.size)
require(votedFor.size == allSelectionHashes.size - selectedVectors.size) // TODO
}
fun selectedCodes() : List<String> = selectedVectors.map { it.shortCode }
fun nselections() = votedFor.size
Expand All @@ -52,37 +53,52 @@ internal data class PreSelectionVector(
}
}

internal fun MarkedPreEncryptedBallot.makePreBallot(preeBallot : PreEncryptedBallot): PreBallot {
internal fun MarkedPreEncryptedBallot.makePreBallot(preeBallot : PreEncryptedBallot, errs : ErrorMessages): PreBallot? {
val contests = mutableListOf<PreContest>()
preeBallot.contests.forEach { preeContest ->
val markedContest = this.contests.find { it.contestId == preeContest.contestId }
?: throw IllegalArgumentException("Cant find ${preeContest.contestId}")
if (markedContest == null) {
errs.add("Cant find PreContest ${preeContest.contestId}")
return null
}

// find the selected selections by their shortCode
val selected = mutableListOf<PreEncryptedSelection>()
val preSelections = mutableListOf<PreEncryptedSelection>()
markedContest.selectedCodes.map { selectedShortCode ->
val selection = preeContest.selections.find { it.shortCode == selectedShortCode } ?:
throw RuntimeException()
selected.add(selection)
val selection = preeContest.selections.find { it.shortCode == selectedShortCode }
if (selection == null) {
errs.add("Cant find PreEncryptedSelection $selectedShortCode")
} else {
preSelections.add(selection)
}
}
if (errs.hasErrors()) return null

val nselections = preeContest.selections.size - preeContest.votesAllowed
val votedFor = mutableListOf<Boolean>()
repeat(nselections) { idx ->
val selection = preeContest.selections[idx]
votedFor.add( selected.find { it.selectionId == selection.selectionId } != null)
votedFor.add( preSelections.find { it.selectionId == selection.selectionId } != null)
}

// add null vector on undervote
val votesMissing = preeContest.votesAllowed - selected.size
// add null vectors on undervote
val votesMissing = preeContest.votesAllowed - preSelections.size
repeat (votesMissing) {
val nullVector = findNullVectorNotSelected(preeContest.selections, selected)
selected.add(nullVector)
val nullVector = findNullVectorNotSelected(preeContest.selections, preSelections)
if (nullVector == null) {
errs.add("Cant find NullVector idx=$it")
} else {
preSelections.add(nullVector)
}
}
if (errs.hasErrors()) return null
if (preSelections.size != preeContest.votesAllowed) {
errs.add("preSelections.size ${preSelections.size } != preeContest.votesAllowed ${preeContest.votesAllowed}")
return null
}
require (selected.size == preeContest.votesAllowed)

// The selectionVectors are sorted numerically by selectionHash, so cant be associated with a selection
val sortedSelectedVectors = selected.sortedBy { it.selectionHash }
val sortedSelectedVectors = preSelections.sortedBy { it.selectionHash }
val sortedRecordedVectors = sortedSelectedVectors.map { preeSelection ->
PreSelectionVector(preeSelection.selectionId, preeSelection.selectionHash, preeSelection.shortCode,
preeSelection.selectionVector, preeSelection.selectionNonces)
Expand All @@ -108,13 +124,13 @@ internal fun MarkedPreEncryptedBallot.makePreBallot(preeBallot : PreEncryptedBal
}

// find a null vector not already in selections
private fun findNullVectorNotSelected(allSelections : List<PreEncryptedSelection>, selections : List<PreEncryptedSelection>) : PreEncryptedSelection {
private fun findNullVectorNotSelected(allSelections : List<PreEncryptedSelection>, selections : List<PreEncryptedSelection>) : PreEncryptedSelection? {
allSelections.forEach {
if (it.selectionId.startsWith("null")) {
if (null == selections.find{ have -> have.selectionId == it.selectionId }) {
return it
}
}
}
throw RuntimeException()
return null
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import electionguard.core.*

/**
* The crypto part of the "The Ballot Encrypting Tool"
* The encrypting/decrypting primaryNonce is done external to this.
* The encrypting/decrypting of the primaryNonce is done external to this.
*/
class PreEncryptor(
val group: GroupContext,
Expand All @@ -26,8 +26,11 @@ class PreEncryptor(
• for each contest, votesAllowed additional null selections vectors, and a contest hash
• a confirmation code for the ballot
*/
internal fun preencrypt(ballotId: String, ballotStyleId: String, primaryNonce: UInt256,
codeBaux : ByteArray = ByteArray(0)
internal fun preencrypt(
ballotId: String,
ballotStyleId: String,
primaryNonce: UInt256,
codeBaux : ByteArray = ByteArray(0)
): PreEncryptedBallot {

val mcontests = manifest.contestsForBallotStyle(ballotStyleId)
Expand Down Expand Up @@ -61,7 +64,7 @@ class PreEncryptor(
// In a contest with a selection limit of L, an additional L null vectors are added
var sequence = this.selections.size
for (nullVectorIdx in (1..contestLimit)) {
// TODO null labels may be in manifest, see 4.2.1
// TODO null labels may be in manifest, see 4.2.1. wtf?
preeSelections.add( preencryptSelection(primaryNonce, this.sequenceOrder, "null${nullVectorIdx}", sequence, sortedSelectionIndices))
sequence++
}
Expand Down
46 changes: 29 additions & 17 deletions egklib/src/commonMain/kotlin/electionguard/preencrypt/Recorder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import electionguard.ballot.ContestDataStatus
import electionguard.ballot.ManifestIF
import electionguard.core.*
import electionguard.encrypt.CiphertextBallot
import electionguard.util.ErrorMessages

/**
* The crypto part of the "The Recording Tool".
Expand Down Expand Up @@ -38,31 +39,35 @@ class Recorder(
For each uncast (implicitly or explicitly challenged) ballot, the recording tool returns the primary
nonce that enables the encryptions to be opened and checked.
*/
internal fun MarkedPreEncryptedBallot.record(ballotNonce: UInt256, codeBaux : ByteArray = ByteArray(0)): Pair<RecordedPreBallot, CiphertextBallot> {
internal fun MarkedPreEncryptedBallot.record(
ballotNonce: UInt256,
errs: ErrorMessages,
codeBaux: ByteArray = ByteArray(0),
): Pair<RecordedPreBallot, CiphertextBallot>? {

// uses the primary nonce ξ to regenerate all of the encryptions on the ballot
val preEncryptedBallot = preEncryptor.preencrypt(this.ballotId, this.ballotStyleId, ballotNonce)
val preBallot = this.makePreBallot(preEncryptedBallot)
val timestamp = (getSystemTimeInMillis() / 1000) // secs since epoch
val preBallot = this.makePreBallot(preEncryptedBallot, errs)
if (errs.hasErrors()) return null

// match against the choices in MarkedPreEncryptedBallot
val preContests = preBallot.contests.associateBy { it.contestId }
val preContests = preBallot!!.contests.associateBy { it.contestId }

val contests = mutableListOf<CiphertextBallot.Contest>()
for (preeContest in preEncryptedBallot.contests) {
val preContest = preContests[preeContest.contestId]!!
val cipherContest = preContest.makeContest(ballotNonce, preeContest)
contests.add( cipherContest)
val contests = preEncryptedBallot.contests.map {
val preContest = preContests[it.contestId] ?: errs.addNull("Cant find contest ${it.contestId}") as PreContest?
preContest?.makeContest(ballotNonce, it, errs.nested("PreContest ${it.contestId}"))
}
if (errs.hasErrors()) return null

val ciphertextBallot = CiphertextBallot(
ballotId,
ballotStyleId,
votingDevice,
timestamp,
(getSystemTimeInMillis() / 1000), // secs since epoch
codeBaux,
preEncryptedBallot.confirmationCode,
extendedBaseHash,
contests,
contests.filterNotNull(),
ballotNonce,
true,
)
Expand All @@ -71,7 +76,7 @@ class Recorder(
return Pair(recordPreBallot, ciphertextBallot)
}

private fun PreContest.makeContest(ballotNonce: UInt256, preeContest: PreEncryptedContest): CiphertextBallot.Contest {
private fun PreContest.makeContest(ballotNonce: UInt256, preeContest: PreEncryptedContest, errs: ErrorMessages): CiphertextBallot.Contest {

// Find the pre-encryptions corresponding to the selections made by the voter and, using
// the encryption nonces derived from the primary nonce, generate proofs of ballot correctness as in
Expand All @@ -84,7 +89,7 @@ class Recorder(
// to create suitable nonces for this combined pre-encryption vector. These derived nonces will be
// necessary to form zero-knowledge proofs that the associated encryption vectors are well-formed.

val selections = this.makeSelections(preeContest)
val selections = this.makeSelections(preeContest, errs)

val texts: List<ElGamalCiphertext> = selections.map { it.ciphertext }
val ciphertextAccumulation: ElGamalCiphertext = texts.encryptedSum()?: 0.encrypt(publicKeyEG)
Expand Down Expand Up @@ -123,11 +128,13 @@ class Recorder(
contestHash, selections, proof, contestDataEncrypted)
}

private fun PreContest.makeSelections(preeContest: PreEncryptedContest): List<CiphertextBallot.Selection> {
private fun PreContest.makeSelections(preeContest: PreEncryptedContest, errs: ErrorMessages): List<CiphertextBallot.Selection> {

val nselections = preeContest.selections.size - preeContest.votesAllowed
val nvectors = this.selectedVectors.size
require (nvectors == preeContest.votesAllowed)
if (nvectors != preeContest.votesAllowed) {
errs.add("nvectors $nvectors != ${preeContest.votesAllowed} preeContest.votesAllowed")
}

// homomorphically combine the selected pre-encryption vectors by component wise multiplication
val combinedEncryption = mutableListOf<ElGamalCiphertext>()
Expand All @@ -145,10 +152,15 @@ class Recorder(
}

if (preeContest.votesAllowed == 1) {
require(combinedEncryption.size == nselections)
if (nselections != combinedEncryption.size) {
errs.add("nselections $nselections != ${combinedEncryption.size} combinedEncryption.size")
}

val selectedEncryption = this.selectedVectors[0].encryptions
repeat(nselections) { idx ->
require(combinedEncryption[idx] == selectedEncryption[idx])
if (combinedEncryption[idx] != selectedEncryption[idx]) {
errs.add("$idx combinedEncryption != selectedEncryption")
}
}
}

Expand Down
Loading

0 comments on commit c308003

Please sign in to comment.