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

Remove protobuf serialization for ContestData, use ad-hoc ByteArraySerialization #439

Merged
merged 1 commit into from
Dec 13, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package electionguard.ballot

import com.github.michaelbull.result.Result

expect fun ContestData.encodeToByteArray(fill: String? = null): ByteArray

expect fun ByteArray.decodeToContestData() : Result<ContestData, String>

71 changes: 7 additions & 64 deletions egklib/src/commonMain/kotlin/electionguard/ballot/ContestData.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package electionguard.ballot

import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
import electionguard.core.*
import io.github.oshai.kotlinlogging.KotlinLogging
import pbandk.decodeFromByteArray
import pbandk.encodeToByteArray
import kotlin.math.max

private val logger = KotlinLogging.logger("ContestData")
Expand All @@ -27,30 +24,6 @@ data class ContestData(
val writeIns: List<String>,
val status: ContestDataStatus = if (overvotes.isNotEmpty()) ContestDataStatus.over_vote else ContestDataStatus.normal,
) {

fun publish(filler: String = ""): electionguard.protogen.ContestData {
return publish(
this.status,
this.overvotes,
this.writeIns,
filler,
)
}

fun publish(
status: ContestDataStatus,
overvotes: List<Int>,
writeIns: List<String>,
filler: String = ""
): electionguard.protogen.ContestData {
return electionguard.protogen.ContestData(
status.publishContestDataStatus(),
overvotes,
writeIns,
filler,
)
}

// Make sure that the HashedElGamalCiphertext message is exactly (votesAllowed + 1) * BLOCK_SIZE
// If too large, remove extra writeIns, add "*" to list to indicate some were removed
// If still too large, truncate writeIns to CHOP_WRITE_INS characters, append "*" to string to indicate truncated
Expand All @@ -68,7 +41,7 @@ data class ContestData(
val messageSize = (1 + contestLimit) * BLOCK_SIZE

var trialContestData = this
var trialContestDataBA = trialContestData.publish().encodeToByteArray()
var trialContestDataBA = trialContestData.encodeToByteArray()
var trialSize = trialContestDataBA.size
val trialSizes = mutableListOf<Int>()
trialSizes.add(trialSize)
Expand All @@ -81,7 +54,7 @@ data class ContestData(
trialContestData = trialContestData.copy(
writeIns = truncateWriteIns,
)
trialContestDataBA = trialContestData.publish().encodeToByteArray()
trialContestDataBA = trialContestData.encodeToByteArray()
trialSize = trialContestDataBA.size
trialSizes.add(trialSize)
}
Expand All @@ -93,7 +66,7 @@ data class ContestData(
if (it.length <= CHOP_WRITE_INS) it else it.substring(0, chop) + "*"
}
trialContestData = trialContestData.copy(writeIns = truncateWriteIns)
trialContestDataBA = trialContestData.publish().encodeToByteArray()
trialContestDataBA = trialContestData.encodeToByteArray()
trialSize = trialContestDataBA.size
trialSizes.add(trialSize)
}
Expand All @@ -102,7 +75,7 @@ data class ContestData(
while (trialSize > messageSize && (trialContestData.overvotes.size > contestLimit + 1)) {
val chopList = trialContestData.overvotes.subList(0, contestLimit + 1) + (-1)
trialContestData = trialContestData.copy(overvotes = chopList)
trialContestDataBA = trialContestData.publish().encodeToByteArray()
trialContestDataBA = trialContestData.encodeToByteArray()
trialSize = trialContestDataBA.size
trialSizes.add(trialSize)
}
Expand All @@ -112,7 +85,7 @@ data class ContestData(
val filler = StringBuilder().apply {
repeat(messageSize - trialSize - 2) { append("*") }
}
trialContestDataBA = trialContestData.publish(filler.toString()).encodeToByteArray()
trialContestDataBA = trialContestData.encodeToByteArray(filler.toString())
trialSize = trialContestDataBA.size
trialSizes.add(trialSize)
}
Expand Down Expand Up @@ -204,8 +177,7 @@ fun HashedElGamalCiphertext.decryptWithBetaToContestData(

val ba: ByteArray = this.decryptContestData(publicKey, extendedBaseHash, contestId, c0, beta) ?:
return Err( "decryptWithBetaToContestData did not succeed")
val proto = electionguard.protogen.ContestData.decodeFromByteArray(ba)
return importContestData(proto)
return ba.decodeToContestData()
}

fun HashedElGamalCiphertext.decryptWithNonceToContestData(
Expand All @@ -220,8 +192,7 @@ fun HashedElGamalCiphertext.decryptWithNonceToContestData(
val (alpha, beta) = 0.encrypt(publicKey, contestDataNonce.toElementModQ(group))
val ba: ByteArray = this.decryptContestData(publicKey, extendedBaseHash, contestId, alpha, beta) ?:
return Err( "decryptWithNonceToContestData did not succeed")
val proto = electionguard.protogen.ContestData.decodeFromByteArray(ba)
return importContestData(proto)
return ba.decodeToContestData()
}

fun HashedElGamalCiphertext.decryptWithSecretKey(
Expand Down Expand Up @@ -266,32 +237,4 @@ fun HashedElGamalCiphertext.decryptContestData(
}
}

//////////////////////////////////////////////////////////////////
// LOOK maybe move to protoconvert

fun importContestData(proto : electionguard.protogen.ContestData?): Result<ContestData, String> {
if (proto == null) return Err( "ContestData is missing")
return Ok(ContestData(
proto.overVotes,
proto.writeIns,
importContestDataStatus(proto.status)?: ContestDataStatus.normal,
))
}

private fun importContestDataStatus(proto: electionguard.protogen.ContestData.Status): ContestDataStatus? {
val result = safeEnumValueOf<ContestDataStatus>(proto.name)
if (result == null) {
logger.error { "ContestDataStatus $proto has missing or unknown name" }
}
return result
}

private fun ContestDataStatus.publishContestDataStatus(): electionguard.protogen.ContestData.Status {
return try {
electionguard.protogen.ContestData.Status.fromName(this.name)
} catch (e: IllegalArgumentException) {
logger.error { "ContestDataStatus $this has missing or unknown name" }
electionguard.protogen.ContestData.Status.NORMAL
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ data class DecryptedTallyOrBallot(

data class DecryptedContestData(
val contestData: ContestData,
val encryptedContestData : HashedElGamalCiphertext, // same as EncryptedTally.Selection.ciphertext
val encryptedContestData : HashedElGamalCiphertext, // same as EncryptedTally.Contest.contestData
val proof: ChaumPedersenProof,
var beta: ElementModP, // needed to verify 10.2
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import electionguard.ballot.decryptWithBetaToContestData
import electionguard.core.*
import electionguard.util.ErrorMessages
import electionguard.util.Stats
import io.github.oshai.kotlinlogging.KotlinLogging

private const val doVerifierSelectionProof = true

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import electionguard.ballot.EncryptedBallot
import electionguard.core.*
import electionguard.core.Base16.fromHex
import electionguard.core.Base16.toHex
import electionguard.preencrypt.RecordedPreBallot
import electionguard.preencrypt.RecordedPreEncryption
import electionguard.preencrypt.RecordedSelectionVector
import electionguard.util.ErrorMessages
import kotlinx.serialization.Serializable

Expand Down Expand Up @@ -139,6 +142,64 @@ fun EncryptedSelectionJson.import(group : GroupContext, errs : ErrorMessages): E
)
}

////////////////////////////////////////////////////////////////////////////////////////////////
// preencrypt

fun EncryptedBallot.publishJson(recordedPreBallot: RecordedPreBallot) = EncryptedBallotJson(
this.ballotId,
this.ballotStyleId,
this.encryptingDevice,
this.timestamp,
this.codeBaux.toHex(),
this.confirmationCode.publishJson(),
this.electionId.publishJson(),
this.contests.map { it.publishJson(recordedPreBallot) },
this.state.name,
this.encryptedSn?.publishJson(),
true,
null,
)

private fun EncryptedBallot.Contest.publishJson(recordedPreBallot: RecordedPreBallot): EncryptedContestJson {

val rcontest = recordedPreBallot.contests.find { it.contestId == this.contestId }
?: throw IllegalArgumentException("Cant find ${this.contestId}")

return EncryptedContestJson(
this.contestId,
this.sequenceOrder,
this.votesAllowed,
this.contestHash.publishJson(),
this.selections.map {
EncryptedSelectionJson(
it.selectionId,
it.sequenceOrder,
it.encryptedVote.publishJson(),
it.proof.publishJson(),
)
},
this.proof.publishJson(),
this.contestData.publishJson(),
rcontest.publishJson(),
)
}

private fun RecordedPreEncryption.publishJson(): PreEncryptionJson {
return PreEncryptionJson(
this.preencryptionHash.publishJson(),
this.allSelectionHashes.map { it.publishJson() },
this.selectedVectors.map { it.publishJson() },
)
}

private fun RecordedSelectionVector.publishJson(): SelectionVectorJson {
return SelectionVectorJson(
this.selectionHash.toUInt256safe().publishJson(),
this.shortCode,
this.encryptions.map { it.publishJson() },
)
}

@Serializable
data class PreEncryptionJson(
val preencryption_hash: UInt256Json,
Expand Down Expand Up @@ -192,4 +253,4 @@ fun SelectionVectorJson.import(group: GroupContext, errs: ErrorMessages): Encryp
this.short_code,
encryptions.filterNotNull(),
)
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package electionguard.protoconvert

import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
import com.github.michaelbull.result.unwrap
import electionguard.ballot.DecryptedTallyOrBallot
import electionguard.ballot.importContestData
import electionguard.core.ElGamalCiphertext
import electionguard.core.ElementModP
import electionguard.core.GroupContext
import electionguard.core.UInt256
import electionguard.ballot.*
import electionguard.core.*
import electionguard.util.ErrorMessages

fun electionguard.protogen.DecryptedTallyOrBallot.import(group: GroupContext, errs : ErrorMessages): DecryptedTallyOrBallot? {
Expand Down Expand Up @@ -109,4 +107,36 @@ private fun DecryptedTallyOrBallot.DecryptedContestData.publishProto() =
this.encryptedContestData.publishProto(),
this.proof.publishProto(),
this.beta.publishProto(),
)
)

//////////////////////////////////////////////////////////////////

fun ContestData.publish(filler: String = ""): electionguard.protogen.ContestData {
return electionguard.protogen.ContestData(
status.publishContestDataStatus(),
overvotes,
writeIns,
filler,
)
}

fun importContestData(proto : electionguard.protogen.ContestData?): Result<ContestData, String> {
if (proto == null) return Err( "ContestData is missing")
return Ok(ContestData(
proto.overVotes,
proto.writeIns,
importContestDataStatus(proto.status)?: ContestDataStatus.normal,
))
}

private fun importContestDataStatus(proto: electionguard.protogen.ContestData.Status): ContestDataStatus? {
return safeEnumValueOf<ContestDataStatus>(proto.name)
}

private fun ContestDataStatus.publishContestDataStatus(): electionguard.protogen.ContestData.Status {
return try {
electionguard.protogen.ContestData.Status.fromName(this.name)
} catch (e: IllegalArgumentException) {
electionguard.protogen.ContestData.Status.NORMAL
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import electionguard.ballot.*
import electionguard.core.UInt256
import electionguard.keyceremony.KeyCeremonyTrustee

/** Read/write the Election Record as protobuf files. */
/** Read/write the Election Record as JSON files. */
expect class PublisherJson(topDir: String, createNew: Boolean = false) : Publisher {
override fun isJson() : Boolean

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package electionguard.preencrypt

import com.github.michaelbull.result.unwrap
import electionguard.ballot.Manifest
import electionguard.core.*
import electionguard.encrypt.cast
import electionguard.cli.ManifestBuilder
import electionguard.protoconvert.import
import electionguard.protoconvert.publishProto
import electionguard.json2.import
import electionguard.json2.publishJson
import electionguard.publish.makePublisher
import electionguard.publish.readElectionRecord
import electionguard.util.ErrorMessages
Expand All @@ -15,7 +14,7 @@ import kotlin.test.Test

private val random = Random

internal class PreEncryptorOutputTest {
class PreEncryptorOutputTest {

// multiple selections per contest
@Test
Expand Down Expand Up @@ -100,10 +99,10 @@ internal class PreEncryptorOutputTest {
println()
}

// roundtrip through the proto, combines the recordedBallot
// roundtrip through the serialization, which combines the recordedBallot
val encryptedBallot = ciphertextBallot.cast()
val proto = encryptedBallot.publishProto(recordedBallot)
val fullEncryptedBallot = proto.import(group, ErrorMessages(""))!!
val json = encryptedBallot.publishJson(recordedBallot)
val fullEncryptedBallot = json.import(group, ErrorMessages(""))!!

// show what ends up in the election record
if (show) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import electionguard.core.*
import electionguard.decryptBallot.DecryptPreencryptWithNonce
import electionguard.encrypt.cast
import electionguard.cli.ManifestBuilder
import electionguard.protoconvert.import
import electionguard.protoconvert.publishProto
import electionguard.json2.import
import electionguard.json2.publishJson
import electionguard.publish.readElectionRecord
import electionguard.util.ErrorMessages
import electionguard.util.Stats
Expand All @@ -26,8 +26,8 @@ import kotlin.test.*

private val random = Random

internal class PreEncryptorTest {
val input = "src/commonTest/data/workflow/allAvailableProto"
class PreEncryptorTest {
val input = "src/commonTest/data/workflow/allAvailableJson"
val group = productionGroup()

// sanity check that PreEncryptor.preencrypt doesnt barf
Expand Down Expand Up @@ -227,10 +227,10 @@ internal fun runComplete(
println()
}

// roundtrip through the proto, combines the recordedBallot
// roundtrip through the serialization, which combines the recordedBallot
val encryptedBallot = ciphertextBallot.cast()
val proto = encryptedBallot.publishProto(recordedBallot)
val fullEncryptedBallot = proto.import(group, ErrorMessages(""))!!
val json = encryptedBallot.publishJson(recordedBallot)
val fullEncryptedBallot = json.import(group, ErrorMessages(""))!!

// show what ends up in the election record
if (show) {
Expand Down
Loading