Skip to content

Commit

Permalink
Merge pull request #63 from JohnLCaron/decryptTodo
Browse files Browse the repository at this point in the history
Eliminate TODOs in decrypt classes.
  • Loading branch information
JohnLCaron authored Apr 17, 2024
2 parents 59e1545 + 6f2e52c commit 0e74eeb
Show file tree
Hide file tree
Showing 24 changed files with 140 additions and 229 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
[![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(7001/7733)-blue)
![Coverage](https://img.shields.io/badge/coverage-90.6%25%20LOC%20(6958/7679)-blue)

# ElectionGuard-Kotlin Elliptic Curve

_last update 04/13/2024_
_last update 04/17/2024_

EGK Elliptic Curve (egk-ec) is an experimental implementation of [ElectionGuard](https://github.com/microsoft/electionguard),
[version 2.0](https://github.com/microsoft/electionguard/releases/download/v2.0/EG_Spec_2_0.pdf),
Expand Down
26 changes: 14 additions & 12 deletions docs/CommandLineInterface.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,17 @@ last update 04/16/2024
2. Use existing fake ballots for testing in _src/test/data/fakeBallots_.

5. **Encryption**.
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).
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.
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).
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**.
1. [_RunAccumulateTally_ CLI](#run-accumulate_tally) reads an ElectionInitialized record and EncryptedBallot
Expand All @@ -85,7 +85,9 @@ last update 04/16/2024

## Make ekglib uberJar

For classpath simplicity, the examples below use the [ekglib uberJar](https://github.com/JohnLCaron/egk-ec/blob/main/docs/GettingStarted.md#building-a-library-with-all-dependencies-uber-jar).
For classpath simplicity, the examples below use the
[ekglib uberJar](https://github.com/JohnLCaron/egk-ec/blob/main/docs/GettingStarted.md#building-a-library-with-all-dependencies-uber-jar).
https://github.com/JohnLCaron/egk-ec/blob/main/docs/GettingStarted.md#building-a-library-with-all-dependencies-uber-jar

## Election setup

Expand Down
2 changes: 1 addition & 1 deletion docs/JsonSerializationSpec1.9.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ data class HashedElGamalCiphertextJson(
val c0: ElementModPJson, // ElementModP,
val c1: String, // ByteArray,
val c2: UInt256Json, // UInt256,
val numBytes: Int // TODO needed?
val numBytes: Int
)
@Serializable
Expand Down
4 changes: 3 additions & 1 deletion docs/JsonSerializationSpec2.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ data class HashedElGamalCiphertextJson(
val c0: ElementModPJson, // ElementModP,
val c1: String, // ByteArray,
val c2: UInt256Json, // UInt256,
val numBytes: Int // TODO needed?
val numBytes: Int
)
@Serializable
Expand Down Expand Up @@ -701,6 +701,8 @@ data class ContestDataJson(
val status: String,
)
TODO ContestDataJson is not in any spec.
````

Example:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ class RunTrustedBallotDecryption {
decryptingTrustees,
)

// TODO you may want to put the decryption results in the same directory, but sinks now are append-only.
val publisher = makePublisher(outputDir, false)
val sink: DecryptedBallotSinkIF = publisher.decryptedBallotSink()

Expand Down
41 changes: 6 additions & 35 deletions src/main/kotlin/org/cryptobiotic/eg/decrypt/BallotDecryptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,40 +19,8 @@ class BallotDecryptor(
val guardians: Guardians, // all guardians
decryptingTrustees: List<DecryptingTrusteeIF>, // the trustees available to decrypt
) {
val lagrangeCoordinates: Map<String, LagrangeCoordinate>
val stats = Stats()
val nguardians = guardians.guardians.size // number of guardinas
val quorum = guardians.guardians[0].coefficientCommitments().size
val decryptor = CipherDecryptor(group, extendedBaseHash, publicKey, guardians, decryptingTrustees)

init {
// check that the DecryptingTrustee's match their public key
val badTrustees = mutableListOf<String>()
for (trustee in decryptingTrustees) {
val guardian = guardians.guardianMap[trustee.id()]
if (guardian == null) {
badTrustees.add(trustee.id())
} else {
if (trustee.guardianPublicKey().key != guardian.publicKey()) {
badTrustees.add(trustee.id())
logger.error { "trustee public key = ${trustee.guardianPublicKey()} not equal guardian = ${guardian.publicKey()}" }
}
}
}
if (badTrustees.isNotEmpty()) {
throw RuntimeException("DecryptingTrustee(s) ${badTrustees.joinToString(",")} do not match the public record")
}

// build the lagrangeCoordinates once and for all
val dguardians = mutableListOf<LagrangeCoordinate>()
for (trustee in decryptingTrustees) {
val present: List<Int> = // available trustees minus me
decryptingTrustees.filter { it.id() != trustee.id() }.map { it.xCoordinate() }
val coeff: ElementModQ = group.computeLagrangeCoefficient(trustee.xCoordinate(), present)
dguardians.add(LagrangeCoordinate(trustee.id(), trustee.xCoordinate(), coeff))
}
this.lagrangeCoordinates = dguardians.associateBy { it.guardianId }
}
val stats = Stats()

fun decrypt(eballot: EncryptedBallotIF, errs : ErrorMessages): DecryptedTallyOrBallot? {
if (eballot.electionId != extendedBaseHash) {
Expand Down Expand Up @@ -87,7 +55,7 @@ class BallotDecryptor(
val result = makeBallot(eballot, decryptionAndProofs, contestDecryptionAndProofs, errs.nested("BallotDecryptor.decrypt"))
if (!errs.hasErrors()) {
val ndecrypt = decryptionAndProofs.size + contestDecryptionAndProofs.size
stats.of("decryptTally").accum(stopwatch.stop(), ndecrypt)
stats.of("decryptBallot").accum(stopwatch.stop(), ndecrypt)
}
return if (errs.hasErrors()) null else result!!
}
Expand All @@ -103,10 +71,13 @@ class BallotDecryptor(
val selections = econtest.selections.map { eselection ->
val (decryption, proof) = decryptions[selectionCount++]
val (T, tally) = decryption.decryptCiphertext(publicKey)
if (tally == null) {
errs.add("Cant decrypt tally for ${econtest.contestId}.${eselection.selectionId}")
}

DecryptedTallyOrBallot.Selection(
eselection.selectionId,
tally?: 0, // TODO error handling
tally?: 0,
T,
(decryption.cipher as Ciphertext).delegate,
proof
Expand Down
102 changes: 15 additions & 87 deletions src/main/kotlin/org/cryptobiotic/eg/decrypt/CipherDecryptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,80 +6,39 @@ import org.cryptobiotic.eg.core.*
import org.cryptobiotic.eg.core.Base16.toHex
import org.cryptobiotic.eg.election.*
import org.cryptobiotic.util.ErrorMessages
import org.cryptobiotic.util.Stats

/** Orchestrates the decryption of List<ElGamalCiphertext> or List<HashedElGamalCiphertext> using DecryptingTrustees. */
class CipherDecryptor(
val group: GroupContext,
val extendedBaseHash: UInt256,
val publicKey: ElGamalPublicKey,
val guardians: Guardians, // all guardians
guardians: Guardians, // all guardians
private val decryptingTrustees: List<DecryptingTrusteeIF>, // the trustees available to decrypt
) {
val lagrangeCoordinates: Map<String, LagrangeCoordinate>
val stats = Stats()
val nguardians = guardians.guardians.size // number of guardinas
val quorum = guardians.guardians[0].coefficientCommitments().size

init {
// TODO put these in the guardians
// check that the DecryptingTrustee's match their public key
val badTrustees = mutableListOf<String>()
for (trustee in decryptingTrustees) {
val guardian = guardians.guardianMap[trustee.id()]
if (guardian == null) {
badTrustees.add(trustee.id())
} else {
if (trustee.guardianPublicKey().key != guardian.publicKey()) {
badTrustees.add(trustee.id())
logger.error { "trustee public key = ${trustee.guardianPublicKey()} not equal guardian = ${guardian.publicKey()}" }
}
}
}
if (badTrustees.isNotEmpty()) {
throw RuntimeException("DecryptingTrustee(s) ${badTrustees.joinToString(",")} do not match the public record")
}

// build the lagrangeCoordinates once and for all
val dguardians = mutableListOf<LagrangeCoordinate>()
for (trustee in decryptingTrustees) {
val present: List<Int> = // available trustees minus me
decryptingTrustees.filter { it.id() != trustee.id() }.map { it.xCoordinate() }
val coeff: ElementModQ = group.computeLagrangeCoefficient(trustee.xCoordinate(), present)
dguardians.add(LagrangeCoordinate(trustee.id(), trustee.xCoordinate(), coeff))
}
this.lagrangeCoordinates = dguardians.associateBy { it.guardianId }
}
val lagrangeCoordinates: Map<String, LagrangeCoordinate> = guardians.buildLagrangeCoordinates(decryptingTrustees)

fun decrypt(texts: List<Cipher>, errs : ErrorMessages): List<CipherDecryptionAndProof>? {
if (texts.isEmpty()) return emptyList()

// get the PartialDecryptions from each of the trustees
val partialDecryptions = decryptingTrustees.map { // partialDecryptions are in the order of the decryptingTrustees
it.getPartialDecryptionsFromTrustee(texts, errs)
val partialDecryptions = decryptingTrustees.map { trustee -> // partialDecryptions are in order of the decryptingTrustees
trustee.getPartialDecryptionsFromTrustee(texts, errs)
}
if (errs.hasErrors()) {
logger.error { "partial decryptions failed = ${errs}" }
logger.error { "partial decryptions failed = $errs" }
return null
}

// Do the decryption for each text
val decryptions = texts.mapIndexed { idx, text ->

// TODO could use the shares
// lagrange weighted product of the shares, M = Prod(M_i^w_i) mod p; spec 2.0.0, eq 68
val weightedProduct = with(group) {
val weightedProduct = with (group) {
// for this idx, run over all the trustees
partialDecryptions.mapIndexed { tidx, pds ->
val trustee = decryptingTrustees[tidx]
val lagrange = lagrangeCoordinates[trustee.id()]
val coeff = if (lagrange == null) { // TODO check to make sure this cant happen
errs.add("missing lagrangeCoordinate for ${trustee.id()}")
group.ONE_MOD_Q
} else {
lagrange.lagrangeCoefficient
}
pds.partial[idx].Mi powP coeff
val lagrange = lagrangeCoordinates[trustee.id()]!! // buildLagrangeCoordinates() guarentees exists
pds.partial[idx].Mi powP lagrange.lagrangeCoefficient
}.multP()
}

Expand All @@ -93,7 +52,7 @@ class CipherDecryptor(
CipherDecryption(text, weightedProduct, collectiveChallenge)
}
if (errs.hasErrors()) {
logger.error { "decrypt failed = ${errs}" }
logger.error { "decrypt failed = $errs" }
return null
}

Expand All @@ -103,12 +62,12 @@ class CipherDecryptor(
trustee.getResponsesFromTrustee(batchId, decryptions, errs.nested("trusteeChallengeResponses"))
}
if (errs.hasErrors()) {
logger.error { "decrypt failed = ${errs}" }
logger.error { "decrypt failed = $errs" }
return null
}

// After gathering the challenge responses from the available trustees, we can create the proof
return makeHDecryptionAndProofs( decryptions, challengeResponses, errs)
return makeHDecryptionAndProofs( decryptions, challengeResponses)
}

private fun DecryptingTrusteeIF.getPartialDecryptionsFromTrustee(texts: List<Cipher>, errs : ErrorMessages) : PartialDecryptions {
Expand All @@ -122,9 +81,9 @@ class CipherDecryptor(
return pds
}

// send all challenges for a ballot / tally to one trustee, get all its responses
// send all challenges for a ballot / tally to one trustee, get all its responses, in one call
fun DecryptingTrusteeIF.getResponsesFromTrustee(batchId: Int, decryptions: List<CipherDecryption>, errs : ErrorMessages) : ChallengeResponses {
val wi = lagrangeCoordinates[this.id()]!!.lagrangeCoefficient // TODO ensure cant fail
val wi = lagrangeCoordinates[this.id()]!!.lagrangeCoefficient // buildLagrangeCoordinates() guarentees exists
// Create all the challenges from each Decryption for this trustee
val requests: MutableList<ElementModQ> = mutableListOf()
decryptions.forEach { decryption ->
Expand All @@ -142,14 +101,13 @@ class CipherDecryptor(
fun makeHDecryptionAndProofs(
decryptions: List<CipherDecryption>, // for each text
challengeResponses: List<ChallengeResponses>, // for each trustee, list of responses for each text
errs : ErrorMessages, // TODO errors
): List<CipherDecryptionAndProof> {
val ds = mutableListOf<CipherDecryptionAndProof>()
decryptions.forEachIndexed { idx, decryption ->
val responsesForIdx = challengeResponses.map { it.responses[idx] }
ds.add( makeHDecryptionAndProof( decryption, responsesForIdx) )
}
return ds // for each text
return ds // one for each text
}

private fun makeHDecryptionAndProof(
Expand Down Expand Up @@ -205,6 +163,7 @@ class CipherDecryption(
// the return value of the decryption
data class CipherDecryptionAndProof(val decryption: CipherDecryption, val proof: ChaumPedersenProof)

// abstraction so we can work with either ElGamalCiphertext or HashedElGamalCiphertext
interface Cipher {
fun pad() : ElementModP
fun collectiveChallenge(extendedBaseHash:UInt256, publicKey: ElGamalPublicKey, a: ElementModP, b: ElementModP, beta: ElementModP): UInt256
Expand Down Expand Up @@ -234,35 +193,4 @@ data class HashedCiphertext(val delegate: HashedElGamalCiphertext): Cipher {
delegate.c1.toHex(),
delegate.c2,
a, b, beta)
}

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

data class LagrangeCoordinate(
var guardianId: String,
var xCoordinate: Int,
var lagrangeCoefficient: ElementModQ, // wℓ, spec 2.0.0 eq 67
) {
init {
require(guardianId.isNotEmpty())
require(xCoordinate > 0)
}
}

/** Compute the lagrange coefficient, now that we know which guardians are present; 2.0, section 3.6.2, eq 67. */
fun GroupContext.computeLagrangeCoefficient(coordinate: Int, present: List<Int>): ElementModQ {
val others: List<Int> = present.filter { it != coordinate }
if (others.isEmpty()) {
return this.ONE_MOD_Q
}
val numerator: Int = others.reduce { a, b -> a * b }

val diff: List<Int> = others.map { degree -> degree - coordinate }
val denominator = diff.reduce { a, b -> a * b }

val denomQ =
if (denominator > 0) denominator.toElementModQ(this) else (-denominator).toElementModQ(this)
.unaryMinus()

return numerator.toElementModQ(this) / denomQ
}
Loading

0 comments on commit 0e74eeb

Please sign in to comment.