diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..3726de3f --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,6 @@ +plugins { + // this is necessary to avoid the plugins to be loaded multiple times + // in each subproject's classloader + kotlin("jvm") apply false + kotlin("multiplatform") apply false +} \ No newline at end of file diff --git a/docs/CommandLineInterface.md b/docs/CommandLineInterface.md index 89d11aea..0cab65c1 100644 --- a/docs/CommandLineInterface.md +++ b/docs/CommandLineInterface.md @@ -84,7 +84,7 @@ Options: --ncontests, -ncontests -> number of contests (always required) { Int } --nselections, -nselections -> number of selections per contest (always required) { Int } --outputType, -type [JSON] -> JSON or PROTO { String } - --outputDir, -out -> Directory to write output Manifest (always required) { String } + --outputDir, -out -> Directory to write test manifest file (always required) { String } --help, -h -> Usage info ```` diff --git a/egklib/build.gradle.kts b/egklib/build.gradle.kts index be4415c1..b0ef91ff 100644 --- a/egklib/build.gradle.kts +++ b/egklib/build.gradle.kts @@ -1,12 +1,5 @@ -buildscript { - repositories { - gradlePluginPortal() - mavenCentral() - } -} - plugins { - kotlin("multiplatform") version "1.9.10" + kotlin("multiplatform") alias(libs.plugins.serialization) application } @@ -98,6 +91,7 @@ nativeTarget.apply { */ } + jvmToolchain(17) } // val protoGenSource by extra("build/generated/source/proto") @@ -149,3 +143,6 @@ configurations.forEach { tasks.withType() .configureEach { kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" } +dependencies { + implementation(kotlin("stdlib-jdk8")) +} \ No newline at end of file diff --git a/egklib/src/commonMain/kotlin/electionguard/ballot/EncryptedBallot.kt b/egklib/src/commonMain/kotlin/electionguard/ballot/EncryptedBallot.kt index bf20bbef..094708d8 100644 --- a/egklib/src/commonMain/kotlin/electionguard/ballot/EncryptedBallot.kt +++ b/egklib/src/commonMain/kotlin/electionguard/ballot/EncryptedBallot.kt @@ -7,16 +7,16 @@ import electionguard.core.* * All contests and selections must be present, so that an inspection of an EncryptedBallot reveals no information. */ data class EncryptedBallot( - override val ballotId: String, - val ballotStyleId: String, // matches a Manifest.BallotStyle - val encryptingDevice: String, - val timestamp: Long, - val codeBaux: ByteArray, // Baux in spec 2.0.0 eq 58 - val confirmationCode: UInt256, // tracking code = H(B) eq 58 - override val electionId : UInt256, - override val contests: List, - override val state: BallotState, - val isPreencrypt: Boolean = false, + override val ballotId: String, + val ballotStyleId: String, // matches a Manifest.BallotStyle + val encryptingDevice: String, + val timestamp: Long, + val codeBaux: ByteArray, // Baux in spec 2.0.0 eq 58 + val confirmationCode: UInt256, // tracking code = H(B) eq 58 + override val electionId : UInt256, + override val contests: List, + override val state: BallotState, + val isPreencrypt: Boolean = false, ) : EncryptedBallotIF { init { diff --git a/egklib/src/commonMain/kotlin/electionguard/publish/Consumer.kt b/egklib/src/commonMain/kotlin/electionguard/publish/Consumer.kt index ef109122..f66c3597 100644 --- a/egklib/src/commonMain/kotlin/electionguard/publish/Consumer.kt +++ b/egklib/src/commonMain/kotlin/electionguard/publish/Consumer.kt @@ -9,6 +9,9 @@ import electionguard.decrypt.DecryptingTrusteeIF import electionguard.input.ManifestInputValidation import electionguard.pep.BallotPep import electionguard.util.ErrorMessages +import io.github.oshai.kotlinlogging.KotlinLogging + +private val logger = KotlinLogging.logger("Consumer") /** public API to read from the election record */ interface Consumer { @@ -23,31 +26,32 @@ interface Consumer { fun readTallyResult(): Result fun readDecryptionResult(): Result - /** The list of devices that have encrytpted ballots. */ + /** Are there any encrypted ballots? */ + fun hasEncryptedBallots() : Boolean + /** The list of devices that have encrypted ballots. */ fun encryptingDevices(): List /** The encrypted ballot chain for specified device. */ fun readEncryptedBallotChain(device: String) : Result - /** Read encrypted ballots for specified devices. */ + /** Read a specific file containing an encrypted ballot. */ + fun readEncryptedBallot(ballotDir: String, ballotId: String) : Result + /** Read encrypted ballots for specified device. */ fun iterateEncryptedBallots(device: String, filter : ((EncryptedBallot) -> Boolean)? ): Iterable /** Read all encrypted ballots for all devices. */ fun iterateAllEncryptedBallots(filter : ((EncryptedBallot) -> Boolean)? ): Iterable fun iterateAllCastBallots(): Iterable = iterateAllEncryptedBallots{ it.state == EncryptedBallot.BallotState.CAST } fun iterateAllSpoiledBallots(): Iterable = iterateAllEncryptedBallots{ it.state == EncryptedBallot.BallotState.SPOILED } - fun hasEncryptedBallots() : Boolean /** Read all decrypted ballots, usually the challenged ones. */ fun iterateDecryptedBallots(): Iterable //// not part of the election record + /** read plaintext ballots in given directory, private data. */ fun iteratePlaintextBallots(ballotDir: String, filter : ((PlaintextBallot) -> Boolean)? ): Iterable /** read trustee in given directory for given guardianId, private data. */ fun readTrustee(trusteeDir: String, guardianId: String): Result - /** Read all the PEP ratio ballots in the given directory. */ fun iteratePepBallots(pepDir : String): Iterable - /** Read a specific file containing an encrypted ballot (eg for PEP). */ - fun readEncryptedBallot(ballotDir: String, ballotId: String) : Result } fun makeConsumer( @@ -92,29 +96,31 @@ fun makeTrusteeSource( } } -// specify the manifest filename, or the directory that its in. May be JSON or proto. If JSON, may be zipped. -// check that the file parses and validates ok -// TODO needs testing -fun readAndCheckManifestBytes( - group: GroupContext, - manifestDirOrFile: String, -): Triple { +/** + * Read the manifest and check that the file parses and validates. + * @param manifestDirOrFile manifest filename, or the directory that its in. May be JSON or proto. If JSON, may be zipped + * @return isJson, manifest, manifestBytes + */ +fun readAndCheckManifest(group: GroupContext, manifestDirOrFile: String): Triple { + + val isZip = manifestDirOrFile.endsWith(".zip") val isDirectory = isDirectory(manifestDirOrFile) val isJson = if (isDirectory) { - manifestDirOrFile.endsWith(".zip") || pathExists("$manifestDirOrFile/${ElectionRecordJsonPaths.MANIFEST_FILE}") } else { - manifestDirOrFile.endsWith(".json") + isZip || manifestDirOrFile.endsWith(".json") } val manifestFile = if (isDirectory) { if (isJson) "$manifestDirOrFile/${ElectionRecordJsonPaths.MANIFEST_FILE}" else "$manifestDirOrFile/${ElectionRecordProtoPaths.MANIFEST_FILE}" + } else if (isZip) { + ElectionRecordJsonPaths.MANIFEST_FILE } else { manifestDirOrFile } - val manifestDir = if (isDirectory) { + val manifestDir = if (isDirectory || isZip) { manifestDirOrFile } else { manifestDirOrFile.substringBeforeLast("/") @@ -126,16 +132,21 @@ fun readAndCheckManifestBytes( ConsumerProto(manifestDir, group) } - val manifestBytes = consumer.readManifestBytes(manifestFile) - // make sure it parses - val manifest = consumer.makeManifest(manifestBytes) - // make sure it validates - val errors = ManifestInputValidation(manifest).validate() - if (errors.hasErrors()) { - println("*** ManifestInputValidation error on manifest in $manifestDirOrFile") - println("$errors") - throw RuntimeException("*** ManifestInputValidation error on manifest in $manifestDirOrFile") + try { + val manifestBytes = consumer.readManifestBytes(manifestFile) + // make sure it parses + val manifest = consumer.makeManifest(manifestBytes) + // make sure it validates + val errors = ManifestInputValidation(manifest).validate() + if (errors.hasErrors()) { + logger.error { "*** ManifestInputValidation error on manifest file= $manifestFile \n $errors" } + throw RuntimeException("*** ManifestInputValidation error on manifest file= $manifestFile \n $errors") + } + return Triple(isJson, manifest, manifestBytes) + + } catch (t: Throwable) { + logger.error {"readAndCheckManifestBytes Exception= ${t.message} ${t.stackTraceToString()}" } + throw t } - return Triple(isJson, manifest, manifestBytes) } \ No newline at end of file diff --git a/egklib/src/commonMain/kotlin/electionguard/publish/ConsumerJson.kt b/egklib/src/commonMain/kotlin/electionguard/publish/ConsumerJson.kt index 2946acb8..eea62ab0 100644 --- a/egklib/src/commonMain/kotlin/electionguard/publish/ConsumerJson.kt +++ b/egklib/src/commonMain/kotlin/electionguard/publish/ConsumerJson.kt @@ -19,17 +19,17 @@ expect class ConsumerJson (topDir: String, group: GroupContext) : Consumer { override fun readTallyResult(): Result override fun readDecryptionResult(): Result + override fun hasEncryptedBallots() : Boolean override fun encryptingDevices(): List override fun readEncryptedBallotChain(device: String) : Result + override fun readEncryptedBallot(ballotDir: String, ballotId: String) : Result override fun iterateEncryptedBallots(device: String, filter : ((EncryptedBallot) -> Boolean)? ): Iterable override fun iterateAllEncryptedBallots(filter : ((EncryptedBallot) -> Boolean)? ): Iterable - override fun hasEncryptedBallots() : Boolean override fun iterateDecryptedBallots(): Iterable - override fun iteratePepBallots(pepDir : String): Iterable override fun iteratePlaintextBallots(ballotDir: String, filter : ((PlaintextBallot) -> Boolean)? ): Iterable override fun readTrustee(trusteeDir: String, guardianId: String): Result + override fun iteratePepBallots(pepDir : String): Iterable - override fun readEncryptedBallot(ballotDir: String, ballotId: String) : Result } \ No newline at end of file diff --git a/egklib/src/commonMain/kotlin/electionguard/publish/ConsumerJsonR.kt b/egklib/src/commonMain/kotlin/electionguard/publish/ConsumerJsonR.kt index 5881f448..24601bc3 100644 --- a/egklib/src/commonMain/kotlin/electionguard/publish/ConsumerJsonR.kt +++ b/egklib/src/commonMain/kotlin/electionguard/publish/ConsumerJsonR.kt @@ -19,17 +19,16 @@ expect class ConsumerJsonR (topDir: String, group: GroupContext) : Consumer { override fun readTallyResult(): Result override fun readDecryptionResult(): Result + override fun hasEncryptedBallots() : Boolean override fun encryptingDevices(): List override fun readEncryptedBallotChain(device: String) : Result + override fun readEncryptedBallot(ballotDir: String, ballotId: String) : Result override fun iterateEncryptedBallots(device: String, filter : ((EncryptedBallot) -> Boolean)? ): Iterable override fun iterateAllEncryptedBallots(filter : ((EncryptedBallot) -> Boolean)? ): Iterable - override fun hasEncryptedBallots() : Boolean override fun iterateDecryptedBallots(): Iterable - override fun iteratePepBallots(pepDir : String): Iterable override fun iteratePlaintextBallots(ballotDir: String, filter : ((PlaintextBallot) -> Boolean)? ): Iterable override fun readTrustee(trusteeDir: String, guardianId: String): Result - - override fun readEncryptedBallot(ballotDir: String, ballotId: String) : Result + override fun iteratePepBallots(pepDir : String): Iterable } \ No newline at end of file diff --git a/egklib/src/commonMain/kotlin/electionguard/publish/ConsumerProto.kt b/egklib/src/commonMain/kotlin/electionguard/publish/ConsumerProto.kt index 5eb26822..4236fe8c 100644 --- a/egklib/src/commonMain/kotlin/electionguard/publish/ConsumerProto.kt +++ b/egklib/src/commonMain/kotlin/electionguard/publish/ConsumerProto.kt @@ -9,7 +9,7 @@ import electionguard.util.ErrorMessages expect class ConsumerProto (topDir: String, groupContext: GroupContext) : Consumer { override fun topdir() : String - override fun isJson(): Boolean + override fun isJson() : Boolean override fun readManifestBytes(filename : String): ByteArray override fun makeManifest(manifestBytes: ByteArray): Manifest @@ -19,17 +19,16 @@ expect class ConsumerProto (topDir: String, groupContext: GroupContext) : Consum override fun readTallyResult(): Result override fun readDecryptionResult(): Result + override fun hasEncryptedBallots() : Boolean override fun encryptingDevices(): List override fun readEncryptedBallotChain(device: String) : Result + override fun readEncryptedBallot(ballotDir: String, ballotId: String) : Result override fun iterateEncryptedBallots(device: String, filter : ((EncryptedBallot) -> Boolean)? ): Iterable override fun iterateAllEncryptedBallots(filter : ((EncryptedBallot) -> Boolean)? ): Iterable - override fun hasEncryptedBallots() : Boolean override fun iterateDecryptedBallots(): Iterable - override fun iteratePepBallots(pepDir : String): Iterable override fun iteratePlaintextBallots(ballotDir: String, filter : ((PlaintextBallot) -> Boolean)? ): Iterable override fun readTrustee(trusteeDir: String, guardianId: String): Result - - override fun readEncryptedBallot(ballotDir: String, ballotId: String) : Result + override fun iteratePepBallots(pepDir : String): Iterable } \ No newline at end of file diff --git a/egklib/src/commonMain/kotlin/electionguard/publish/ElectionRecordProtoPaths.kt b/egklib/src/commonMain/kotlin/electionguard/publish/ElectionRecordProtoPaths.kt index b33576a3..6a425ff4 100644 --- a/egklib/src/commonMain/kotlin/electionguard/publish/ElectionRecordProtoPaths.kt +++ b/egklib/src/commonMain/kotlin/electionguard/publish/ElectionRecordProtoPaths.kt @@ -48,10 +48,6 @@ data class ElectionRecordProtoPaths(val topDir : String) { return "$ballotDir/$PLAINTEXT_BALLOT_FILE" } - fun encryptedBallotPath(): String { - return "$electionRecordDir/$ENCRYPTED_BATCH_FILE" - } - fun decryptedBatchPath(): String { return "$electionRecordDir/$DECRYPTED_BATCH_FILE" } diff --git a/egklib/src/commonTest/data/testElectionRecord/allAvailableJson.zip b/egklib/src/commonTest/data/testElectionRecord/allAvailableJson.zip new file mode 100644 index 00000000..13744af3 Binary files /dev/null and b/egklib/src/commonTest/data/testElectionRecord/allAvailableJson.zip differ diff --git a/egklib/src/commonTest/kotlin/electionguard/cli/RunCreateTestManifestTest.kt b/egklib/src/commonTest/kotlin/electionguard/cli/RunCreateTestManifestTest.kt index e58d797c..6a9551ce 100644 --- a/egklib/src/commonTest/kotlin/electionguard/cli/RunCreateTestManifestTest.kt +++ b/egklib/src/commonTest/kotlin/electionguard/cli/RunCreateTestManifestTest.kt @@ -1,7 +1,7 @@ package electionguard.cli import electionguard.core.productionGroup -import electionguard.publish.readAndCheckManifestBytes +import electionguard.publish.readAndCheckManifest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -43,14 +43,14 @@ class RunCreateTestManifestTest { // get all and see if they compare equal val group = productionGroup() - val (isJsonOrg, manifestOrg, _) = readAndCheckManifestBytes(group, "testOut/manifest/runCreateTestManifest") + val (isJsonOrg, manifestOrg, _) = readAndCheckManifest(group, "testOut/manifest/runCreateTestManifest") assertTrue(isJsonOrg) - val (isJsonProto, manifestProto, _) = readAndCheckManifestBytes(group, "testOut/manifest/testConvertManifestFromJsonToProto") + val (isJsonProto, manifestProto, _) = readAndCheckManifest(group, "testOut/manifest/testConvertManifestFromJsonToProto") assertFalse(isJsonProto) assertEquals(manifestOrg, manifestProto) - val (isJsonRoundTrip, manifestRoundtrip, _) = readAndCheckManifestBytes(group, "testOut/manifest/testConvertManifestFromProtoToJson") + val (isJsonRoundTrip, manifestRoundtrip, _) = readAndCheckManifest(group, "testOut/manifest/testConvertManifestFromProtoToJson") assertTrue(isJsonRoundTrip) assertEquals(manifestProto, manifestRoundtrip) diff --git a/egklib/src/commonTest/kotlin/electionguard/publish/ConsumerTest.kt b/egklib/src/commonTest/kotlin/electionguard/publish/ConsumerTest.kt index 4cb7852a..856927ee 100644 --- a/egklib/src/commonTest/kotlin/electionguard/publish/ConsumerTest.kt +++ b/egklib/src/commonTest/kotlin/electionguard/publish/ConsumerTest.kt @@ -1,48 +1,56 @@ package electionguard.publish import com.github.michaelbull.result.* -import electionguard.ballot.protocolVersion -import electionguard.cli.ManifestBuilder.Companion.electionScopeId import electionguard.core.productionGroup import electionguard.core.runTest import electionguard.decrypt.DecryptingTrusteeIF +import org.junit.jupiter.api.Assertions.assertNotNull import kotlin.test.Test -import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue class ConsumerTest { - private val topdir = "src/commonTest/data/workflow/someAvailableJson" + private val group = productionGroup() + val topdir = "src/commonTest/data/workflow/chainedJson" @Test - fun readElectionRecord() { - runTest { - val group = productionGroup() - val electionRecord = readElectionRecord(group, topdir) - val manifest = electionRecord.manifest() - println("electionRecord.manifest.specVersion = ${electionRecord.manifest().specVersion}") - assertEquals(electionScopeId, manifest.electionScopeId) - assertEquals(protocolVersion, manifest.specVersion) - } + fun consumerJson() { + val consumerIn = makeConsumer(group, topdir) + assertTrue(consumerIn.readElectionConfig() is Ok) // proto doesnt always have config, may have just init + testConsumer(consumerIn) } @Test - fun readSpoiledBallotTallys() { - runTest { - val group = productionGroup() - val consumerIn = makeConsumer(group, topdir) - var count = 0 - for (tally in consumerIn.iterateDecryptedBallots()) { - println("$count tally = ${tally.id}") - count++ - } - } + fun consumerProto() { + testConsumer(makeConsumer(group, "src/commonTest/data/workflow/chainedProto")) + } + + fun testConsumer(consumerIn : Consumer) { + assertTrue( consumerIn.readElectionInitialized() is Ok) + assertTrue( consumerIn.readTallyResult() is Ok) + assertTrue( consumerIn.readDecryptionResult() is Ok) + assertTrue( consumerIn.hasEncryptedBallots()) + assertTrue( consumerIn.encryptingDevices().isNotEmpty()) + val device = consumerIn.encryptingDevices()[0] + + assertTrue( consumerIn.readEncryptedBallotChain(device) is Ok) + + var iter = consumerIn.iterateEncryptedBallots(device) { true } + assertNotNull(iter) + assertTrue(iter.count() > 0) + + iter = consumerIn.iterateAllEncryptedBallots { true } + assertNotNull(iter) + assertTrue(iter.count() > 0) + + val iter2 = consumerIn.iterateDecryptedBallots() + assertNotNull(iter2) + assertTrue(iter.count() >= 0) } @Test fun readEncryptedBallots() { runTest { - val group = productionGroup() val consumerIn = makeConsumer(group, topdir) var count = 0 for (ballot in consumerIn.iterateAllEncryptedBallots { true} ) { @@ -55,7 +63,6 @@ class ConsumerTest { @Test fun readEncryptedBallotsCast() { runTest { - val group = productionGroup() val consumerIn = makeConsumer(group, topdir) var count = 0 for (ballot in consumerIn.iterateAllCastBallots()) { @@ -68,7 +75,6 @@ class ConsumerTest { @Test fun readSubmittedBallotsSpoiled() { runTest { - val group = productionGroup() val consumerIn = makeConsumer(group, topdir) var count = 0 for (ballot in consumerIn.iterateAllSpoiledBallots()) { @@ -78,10 +84,21 @@ class ConsumerTest { } } + @Test + fun readDecryptedBallots() { + runTest { + val consumerIn = makeConsumer(group, topdir) + var count = 0 + for (tally in consumerIn.iterateDecryptedBallots()) { + println("$count tally = ${tally.id}") + count++ + } + } + } + @Test fun readTrustee() { runTest { - val group = productionGroup() val consumerIn = makeConsumer(group, topdir) val result = consumerIn.readElectionInitialized() val init = result.unwrap() diff --git a/egklib/src/commonTest/kotlin/electionguard/publish/ElectionRecordIterablesTest.kt b/egklib/src/commonTest/kotlin/electionguard/publish/ElectionRecordIterablesTest.kt index 60c3befb..7db9e7fe 100644 --- a/egklib/src/commonTest/kotlin/electionguard/publish/ElectionRecordIterablesTest.kt +++ b/egklib/src/commonTest/kotlin/electionguard/publish/ElectionRecordIterablesTest.kt @@ -14,7 +14,6 @@ class ElectionRecordIterablesTest { @Test fun testElectionRecordIterablesProto() { readBallots("src/commonTest/data/workflow/chainedProto") - } @Test diff --git a/egklib/src/commonTest/kotlin/electionguard/publish/ElectionRecordTest.kt b/egklib/src/commonTest/kotlin/electionguard/publish/ElectionRecordTest.kt index 5c795d87..0601bddc 100644 --- a/egklib/src/commonTest/kotlin/electionguard/publish/ElectionRecordTest.kt +++ b/egklib/src/commonTest/kotlin/electionguard/publish/ElectionRecordTest.kt @@ -1,8 +1,8 @@ package electionguard.publish -import com.github.michaelbull.result.getOrThrow import com.github.michaelbull.result.unwrap import electionguard.ballot.* +import electionguard.cli.ManifestBuilder import electionguard.cli.RunVerifier import electionguard.core.* import kotlin.test.Test @@ -12,6 +12,21 @@ import kotlin.test.assertTrue // Test election records that have been fully decrypted class ElectionRecordTest { + + @Test + fun readElectionRecord() { + val topdir = "src/commonTest/data/workflow/someAvailableJson" + + runTest { + val group = productionGroup() + val electionRecord = readElectionRecord(group, topdir) + val manifest = electionRecord.manifest() + println("electionRecord.manifest.specVersion = ${electionRecord.manifest().specVersion}") + assertEquals(ManifestBuilder.electionScopeId, manifest.electionScopeId) + assertEquals(protocolVersion, manifest.specVersion) + } + } + @Test fun allAvailableProto() { readElectionRecordAndValidate("src/commonTest/data/workflow/allAvailableProto") diff --git a/egklib/src/commonTest/kotlin/electionguard/publish/ReadAndCheckManifestTest.kt b/egklib/src/commonTest/kotlin/electionguard/publish/ReadAndCheckManifestTest.kt new file mode 100644 index 00000000..b858606b --- /dev/null +++ b/egklib/src/commonTest/kotlin/electionguard/publish/ReadAndCheckManifestTest.kt @@ -0,0 +1,80 @@ +package electionguard.publish + +import electionguard.core.productionGroup +import java.io.FileNotFoundException +import kotlin.test.* + +class ReadAndCheckManifestTest { + val group = productionGroup() + + @Test + fun readAndCheckManifestJsonTest() { + val (isJsonOrg, manifest, manifestBytes) = readAndCheckManifest(group, "src/commonTest/data/startConfigJson/manifest.json") + assertTrue(isJsonOrg) + assertNotNull(manifest) + assertNotNull(manifestBytes) + println("manifest = $manifest") + } + + @Test + fun readAndCheckManifestJsonDirTest() { + val (isJsonOrg, manifest, manifestBytes) = readAndCheckManifest(group, "src/commonTest/data/startManifestJson") + assertTrue(isJsonOrg) + assertNotNull(manifest) + assertNotNull(manifestBytes) + println("manifest = $manifest") + } + + @Test + fun readAndCheckManifestJsonZipTest() { + val (isJsonOrg, manifest, manifestBytes) = readAndCheckManifest(group, "src/commonTest/data/testElectionRecord/allAvailableJson.zip") + assertTrue(isJsonOrg) + assertNotNull(manifest) + assertNotNull(manifestBytes) + println("manifest = $manifest") + } + + @Test + fun readAndCheckManifestProtoTest() { + val (isJsonOrg, manifest, manifestBytes) = readAndCheckManifest(group, "src/commonTest/data/startManifestProto/manifest.protobuf") + assertFalse(isJsonOrg) + assertNotNull(manifest) + assertNotNull(manifestBytes) + println("manifest = $manifest") + } + + @Test + fun readAndCheckManifestProtoDirTest() { + val (isJsonOrg, manifest, manifestBytes) = readAndCheckManifest(group, "src/commonTest/data/startManifestProto") + assertFalse(isJsonOrg) + assertNotNull(manifest) + assertNotNull(manifestBytes) + println("manifest = $manifest") + } + + @Test + fun missingManifestTest() { + val ex = assertFailsWith( + block = { readAndCheckManifest(group, "src/commonTest/data/missing") } + ) + assertContains(ex.message!!, "No such file or directory") + } + + @Test + fun badManifestJsonTest() { + val ex = assertFailsWith( + block = { readAndCheckManifest(group, "src/commonTest/data/startConfigJson/constants.json") } + ) + assertContains(ex.message!!, "Unexpected JSON token") + } + + @Test + fun badManifestProtoTest() { + val ex = assertFailsWith( + block = { readAndCheckManifest(group, "src/commonTest/data/startConfigProto/electionConfig.protobuf") } + ) + assertContains(ex.message!!, "Unrecognized wire type") + } + +} + diff --git a/egklib/src/jvmMain/kotlin/electionguard/cli/RunConvertManifest.kt b/egklib/src/jvmMain/kotlin/electionguard/cli/RunConvertManifest.kt index 1cb936e2..cc23e20b 100644 --- a/egklib/src/jvmMain/kotlin/electionguard/cli/RunConvertManifest.kt +++ b/egklib/src/jvmMain/kotlin/electionguard/cli/RunConvertManifest.kt @@ -2,7 +2,7 @@ package electionguard.cli import electionguard.core.productionGroup import electionguard.publish.makePublisher -import electionguard.publish.readAndCheckManifestBytes +import electionguard.publish.readAndCheckManifest import kotlinx.cli.ArgParser import kotlinx.cli.ArgType import kotlinx.cli.required @@ -34,7 +34,7 @@ class RunConvertManifest { val group = productionGroup() - val (isJson, manifest, _) = readAndCheckManifestBytes(group, electionManifest) + val (isJson, manifest, _) = readAndCheckManifest(group, electionManifest) val publisher = makePublisher(outputDir, true, !isJson) diff --git a/egklib/src/jvmMain/kotlin/electionguard/cli/RunCreateElectionConfig.kt b/egklib/src/jvmMain/kotlin/electionguard/cli/RunCreateElectionConfig.kt index 2f287425..fdb57123 100644 --- a/egklib/src/jvmMain/kotlin/electionguard/cli/RunCreateElectionConfig.kt +++ b/egklib/src/jvmMain/kotlin/electionguard/cli/RunCreateElectionConfig.kt @@ -5,7 +5,7 @@ import electionguard.ballot.protocolVersion import electionguard.core.getSystemDate import electionguard.core.productionGroup import electionguard.publish.makePublisher -import electionguard.publish.readAndCheckManifestBytes +import electionguard.publish.readAndCheckManifest import kotlinx.cli.ArgParser import kotlinx.cli.ArgType import kotlinx.cli.default @@ -68,7 +68,7 @@ class RunCreateElectionConfig { val group = productionGroup() - val (isJson, _, manifestBytes) = readAndCheckManifestBytes(group, electionManifest) + val (isJson, _, manifestBytes) = readAndCheckManifest(group, electionManifest) // As input, either specify the input directory that contains electionConfig.protobuf file, // OR the election manifest, nguardians and quorum. diff --git a/egklib/src/jvmMain/kotlin/electionguard/cli/RunCreateTestManifest.kt b/egklib/src/jvmMain/kotlin/electionguard/cli/RunCreateTestManifest.kt index 56c5ba08..e35329ab 100644 --- a/egklib/src/jvmMain/kotlin/electionguard/cli/RunCreateTestManifest.kt +++ b/egklib/src/jvmMain/kotlin/electionguard/cli/RunCreateTestManifest.kt @@ -31,7 +31,7 @@ class RunCreateTestManifest { val outputDir by parser.option( ArgType.String, shortName = "out", - description = "Directory to write output Manifest" + description = "Directory to write test manifest file" ).required() parser.parse(args) diff --git a/egklib/src/jvmMain/kotlin/electionguard/publish/ConsumerJson.kt b/egklib/src/jvmMain/kotlin/electionguard/publish/ConsumerJson.kt index 657649e4..3e32d08d 100644 --- a/egklib/src/jvmMain/kotlin/electionguard/publish/ConsumerJson.kt +++ b/egklib/src/jvmMain/kotlin/electionguard/publish/ConsumerJson.kt @@ -38,10 +38,10 @@ actual class ConsumerJson actual constructor(val topDir: String, val group: Grou if (topDir.endsWith(".zip")) { val filePath = Path.of(topDir) fileSystem = FileSystems.newFileSystem(filePath, emptyMap()) - val wtf = fileSystem.rootDirectories - wtf.forEach { root -> - Files.walk(root).forEach { path -> println(path) } - } + //val wtf = fileSystem.rootDirectories + //wtf.forEach { root -> + // Files.walk(root).forEach { path -> println(path) } + //} fileSystemProvider = fileSystem.provider() jsonPaths = ElectionRecordJsonPaths("") @@ -93,7 +93,35 @@ actual class ConsumerJson actual constructor(val topDir: String, val group: Grou ) } - /////////////////////////////////////////////////////////////////////////// + actual override fun readTallyResult(): Result { + val init = readElectionInitialized() + if (init is Err) { + return Err(init.error) + } + return readTallyResult( + fileSystem.getPath(jsonPaths.encryptedTallyPath()), + init.unwrap(), + ) + } + + actual override fun readDecryptionResult(): Result { + val tally = readTallyResult() + if (tally is Err) { + return Err(tally.error) + } + + return readDecryptionResult( + fileSystem.getPath(jsonPaths.decryptedTallyPath()), + tally.unwrap() + ) + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + + actual override fun hasEncryptedBallots(): Boolean { + val iter = iterateAllEncryptedBallots { true } + return iter.iterator().hasNext() + } actual override fun encryptingDevices(): List { val topBallotPath = Path.of(jsonPaths.encryptedBallotDir()) @@ -121,6 +149,23 @@ actual class ConsumerJson actual constructor(val topDir: String, val group: Grou } } + actual override fun readEncryptedBallot(ballotDir: String, ballotId: String) : Result { + val errs = ErrorMessages("readEncryptedBallot ballotId=$ballotId from directory $ballotDir") + val ballotFilename = jsonPaths.encryptedBallotPath(ballotDir, ballotId) + if (!Files.exists(fileSystem.getPath(ballotFilename))) { + return errs.add("'$ballotFilename' file does not exist") + } + return try { + fileSystemProvider.newInputStream(fileSystem.getPath(ballotFilename), StandardOpenOption.READ).use { inp -> + val json = Json.decodeFromStream(inp) + val eballot = json.import(group, errs) + if (errs.hasErrors()) Err(errs) else Ok(eballot!!) + } + } catch (t: Throwable) { + errs.add("Exception= ${t.message} ${t.stackTraceToString()}") + } + } + actual override fun iterateEncryptedBallots(device: String, filter : ((EncryptedBallot) -> Boolean)? ): Iterable { val deviceDirPath = Path.of(jsonPaths.encryptedBallotDir(device)) if (!Files.exists(deviceDirPath)) { @@ -140,54 +185,7 @@ actual class ConsumerJson actual constructor(val topDir: String, val group: Grou return Iterable { DeviceIterator(devices.iterator(), filter) } } - private inner class DeviceIterator( - val devices: Iterator, - private val filter : ((EncryptedBallot) -> Boolean)?, - ) : AbstractIterator() { - var innerIterator: Iterator? = null - - override fun computeNext() { - while (true) { - if (innerIterator != null && innerIterator!!.hasNext()) { - return setNext(innerIterator!!.next()) - } - if (devices.hasNext()) { - innerIterator = iterateEncryptedBallots(devices.next(), filter).iterator() - } else { - return done() - } - } - } - } - - ////////////////////////////////////////////////////////////////////////////////////////////////// - - actual override fun readTallyResult(): Result { - val init = readElectionInitialized() - if (init is Err) { - return Err(init.error) - } - return readTallyResult( - fileSystem.getPath(jsonPaths.encryptedTallyPath()), - init.unwrap(), - ) - } - - actual override fun readDecryptionResult(): Result { - val tally = readTallyResult() - if (tally is Err) { - return Err(tally.error) - } - - return readDecryptionResult( - fileSystem.getPath(jsonPaths.decryptedTallyPath()), - tally.unwrap() - ) - } - - actual override fun hasEncryptedBallots(): Boolean { - return Files.exists(fileSystem.getPath(jsonPaths.encryptedBallotDir())) - } + ////////////////////////////////////////////////////////////////////////////////// // decrypted spoiled ballots actual override fun iterateDecryptedBallots(): Iterable { @@ -220,23 +218,6 @@ actual class ConsumerJson actual constructor(val topDir: String, val group: Grou return readTrustee(fileSystem.getPath(filename)) } - actual override fun readEncryptedBallot(ballotDir: String, ballotId: String) : Result { - val errs = ErrorMessages("readEncryptedBallot ballotId=$ballotId from directory $ballotDir") - val ballotFilename = jsonPaths.encryptedBallotPath(ballotDir, ballotId) - if (!Files.exists(fileSystem.getPath(ballotFilename))) { - return errs.add("'$ballotFilename' file does not exist") - } - return try { - fileSystemProvider.newInputStream(fileSystem.getPath(ballotFilename), StandardOpenOption.READ).use { inp -> - val json = Json.decodeFromStream(inp) - val eballot = json.import(group, errs) - if (errs.hasErrors()) Err(errs) else Ok(eballot!!) - } - } catch (t: Throwable) { - errs.add("Exception= ${t.message} ${t.stackTraceToString()}") - } - } - actual override fun iteratePepBallots(pepDir : String): Iterable { return Iterable { PepBallotIterator(group, Path.of(pepDir)) } } @@ -378,6 +359,26 @@ actual class ConsumerJson actual constructor(val topDir: String, val group: Grou } } + private inner class DeviceIterator( + val devices: Iterator, + private val filter : ((EncryptedBallot) -> Boolean)?, + ) : AbstractIterator() { + var innerIterator: Iterator? = null + + override fun computeNext() { + while (true) { + if (innerIterator != null && innerIterator!!.hasNext()) { + return setNext(innerIterator!!.next()) + } + if (devices.hasNext()) { + innerIterator = iterateEncryptedBallots(devices.next(), filter).iterator() + } else { + return done() + } + } + } + } + private inner class PlaintextBallotIterator( ballotDir: Path, private val filter: Predicate? @@ -467,7 +468,7 @@ actual class ConsumerJson actual constructor(val topDir: String, val group: Grou } } - fun readEncryptedBallot(ballotFilePath : Path, errs: ErrorMessages): EncryptedBallot? { + private fun readEncryptedBallot(ballotFilePath : Path, errs: ErrorMessages): EncryptedBallot? { fileSystemProvider.newInputStream(ballotFilePath, StandardOpenOption.READ).use { inp -> val json = Json.decodeFromStream(inp) return json.import(group, errs) diff --git a/egklib/src/jvmMain/kotlin/electionguard/publish/ConsumerProto.kt b/egklib/src/jvmMain/kotlin/electionguard/publish/ConsumerProto.kt index 246604df..e78e00d5 100644 --- a/egklib/src/jvmMain/kotlin/electionguard/publish/ConsumerProto.kt +++ b/egklib/src/jvmMain/kotlin/electionguard/publish/ConsumerProto.kt @@ -38,7 +38,7 @@ actual class ConsumerProto actual constructor(val topDir: String, val groupConte actual override fun isJson() = false - actual override fun readManifestBytes(filename : String): ByteArray { + actual override fun readManifestBytes(filename: String): ByteArray { return fileReadBytes(filename) } @@ -70,10 +70,10 @@ actual class ConsumerProto actual constructor(val topDir: String, val groupConte return emptyList() } val deviceDirs: Stream = Files.list(topBallotPath) - return deviceDirs.map { it.getName( it.nameCount - 1).toString() }.toList() // last name in the path + return deviceDirs.map { it.getName(it.nameCount - 1).toString() }.toList() // last name in the path } - actual override fun readEncryptedBallotChain(device: String) : Result { + actual override fun readEncryptedBallotChain(device: String): Result { val errs = ErrorMessages("readEncryptedBallotChain device='$device'") val ballotChainFile = protoPaths.encryptedBallotChain(device) if (!Files.exists(Path.of(ballotChainFile))) { @@ -84,14 +84,17 @@ actual class ConsumerProto actual constructor(val topDir: String, val groupConte FileInputStream(ballotChainFile).use { inp -> proto = electionguard.protogen.EncryptedBallotChain.decodeFromStream(inp) } - val result = proto.import( errs) + val result = proto.import(errs) if (errs.hasErrors()) Err(errs) else Ok(result!!) } catch (t: Throwable) { errs.add("Exception= ${t.message} ${t.stackTraceToString()}") } } - actual override fun iterateEncryptedBallots(device: String, filter : ((EncryptedBallot) -> Boolean)? ): Iterable { + actual override fun iterateEncryptedBallots( + device: String, + filter: ((EncryptedBallot) -> Boolean)? + ): Iterable { val dirPath = Path.of(protoPaths.encryptedBallotDir(device)) if (!Files.exists(dirPath)) { throw RuntimeException("$dirPath doesnt exist") @@ -126,13 +129,14 @@ actual class ConsumerProto actual constructor(val topDir: String, val groupConte return emptyList() } - actual override fun iterateAllEncryptedBallots(filter : ((EncryptedBallot) -> Boolean)? ): Iterable { + actual override fun iterateAllEncryptedBallots(filter: ((EncryptedBallot) -> Boolean)?): Iterable { val devices = encryptingDevices() return Iterable { DeviceIterator(devices.iterator(), filter) } } actual override fun hasEncryptedBallots(): Boolean { - return Files.exists(Path.of(protoPaths.encryptedBallotPath())) + val iter = iterateAllEncryptedBallots { true } + return iter.iterator().hasNext() } // plaintext ballots in given directory, with filter @@ -147,28 +151,36 @@ actual class ConsumerProto actual constructor(val topDir: String, val groupConte } // trustee in given directory for given guardianId - actual override fun readTrustee(trusteeDir: String, guardianId: String): Result { + actual override fun readTrustee( + trusteeDir: String, + guardianId: String + ): Result { val filename = protoPaths.decryptingTrusteePath(trusteeDir, guardianId) return groupContext.readTrustee(filename) } - actual override fun readEncryptedBallot(ballotDir: String, ballotId: String) : Result { + actual override fun readEncryptedBallot( + ballotDir: String, + ballotId: String + ): Result { val errs = ErrorMessages("readEncryptedBallot '$ballotDir/$ballotId'") return errs.add("Not implemented yet") } - actual override fun iteratePepBallots(pepDir : String): Iterable { + actual override fun iteratePepBallots(pepDir: String): Iterable { throw RuntimeException("Not implemented yet") } //////////////////////////////////////////////////////////////////// // The low level reading functions for protobuf - private fun makeManifestResult(manifestBytes: ByteArray): Result { + private fun makeManifestResult(manifestBytes: ByteArray): Result { return try { var proto: electionguard.protogen.Manifest - ByteArrayInputStream(manifestBytes).use { inp -> proto = electionguard.protogen.Manifest.decodeFromStream(inp) } + ByteArrayInputStream(manifestBytes).use { inp -> + proto = electionguard.protogen.Manifest.decodeFromStream(inp) + } Ok(proto.import()) } catch (t: Throwable) { ErrorMessages("makeManifestResult").add("Exception= ${t.message} ${t.stackTraceToString()}") @@ -212,8 +224,8 @@ actual class ConsumerProto actual constructor(val topDir: String, val groupConte } return try { var proto: electionguard.protogen.TallyResult - FileInputStream(filename).use { - inp -> proto = electionguard.protogen.TallyResult.decodeFromStream(inp) + FileInputStream(filename).use { inp -> + proto = electionguard.protogen.TallyResult.decodeFromStream(inp) } val result = proto.import(this, errs) if (errs.hasErrors()) Err(errs) else Ok(result!!) @@ -386,7 +398,7 @@ actual class ConsumerProto actual constructor(val topDir: String, val groupConte private inner class DeviceIterator( val devices: Iterator, - private val filter : ((EncryptedBallot) -> Boolean)?, + private val filter: ((EncryptedBallot) -> Boolean)?, ) : AbstractIterator() { var innerIterator: Iterator? = null @@ -453,7 +465,6 @@ actual class ConsumerProto actual constructor(val topDir: String, val groupConte return done() } } - } // variable length (base 128) int32 diff --git a/egkliball/build.gradle.kts b/egkliball/build.gradle.kts index bebe2f0d..eb860c8d 100644 --- a/egkliball/build.gradle.kts +++ b/egkliball/build.gradle.kts @@ -1,12 +1,5 @@ -buildscript { - repositories { - gradlePluginPortal() - mavenCentral() - } -} - plugins { - kotlin("jvm") version "1.9.10" + kotlin("jvm") alias(libs.plugins.serialization) application } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e70d0542..334bc174 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] coroutines-version = "1.6.4" -kotlin-version = "1.9.0" +kotlin-version = "1.9.20" ktor-version = "2.3.4" [libraries] diff --git a/settings.gradle.kts b/settings.gradle.kts index 5043f141..5db4f6bf 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,22 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + } + + plugins { + val kotlinVersion = "1.9.20" + + kotlin("jvm").version(kotlinVersion) + kotlin("multiplatform").version(kotlinVersion) + kotlin("plugin.serialization").version(kotlinVersion) + } +} + +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0" +} + rootProject.name = "electionguard-kotlin-multiplatform" include ("egklib")