Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Eliminate Exceptions from encryption code #424

Merged
merged 2 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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