Skip to content

Commit

Permalink
Eliminate TODOs, duplicate code in ContestData.
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnLCaron committed Apr 18, 2024
1 parent ac22834 commit 8c8968f
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 119 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[![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.7%25%20LOC%20(6944/7658)-blue)
![Coverage](https://img.shields.io/badge/coverage-90.7%25%20LOC%20(6931/7639)-blue)

# ElectionGuard-Kotlin Elliptic Curve

Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/org/cryptobiotic/eg/core/ElGamalKeys.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class ElGamalPublicKey(inputKey: ElementModP) {
class ElGamalSecretKey(val key: ElementModQ) {
init {
if (key < key.context.TWO_MOD_Q)
throw ArithmeticException("secret key must be in [2, Q)")
throw ArithmeticException("secret key must be in [2, Q) group=${key.context.javaClass.name}")
}
val negativeKey: ElementModQ = -key
val context: GroupContext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class EcElementModP(val group: EcGroupContext, val ec: VecElementP): ElementModP
}

// TODO what does it mean to be in bounds ??
override fun inBounds(): Boolean = true // TODO("Not yet implemented")
override fun inBounds(): Boolean = true

// TODO check this
override fun isValidResidue(): Boolean {
Expand Down
142 changes: 59 additions & 83 deletions src/main/kotlin/org/cryptobiotic/eg/election/ContestData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -86,97 +86,89 @@ data class ContestData(
trialSize = trialContestDataBA.size
trialSizes.add(trialSize)
}
logger.debug{ "encodedData = $trialContestData trialSizes = $trialSizes" }
logger.debug { "encodedData = $trialContestData trialSizes = $trialSizes" }

return trialContestDataBA.encryptContestData(publicKey, extendedBaseHash, contestId, contestIndex, ballotNonce)
}

companion object {
val logger = KotlinLogging.logger("ContestData")
private const val BLOCK_SIZE : Int = 32
private const val CHOP_WRITE_INS : Int = 30
private const val BLOCK_SIZE: Int = 32
private const val CHOP_WRITE_INS: Int = 30

const val label = "share_enc_keys"
const val contestDataLabel = "contest_data"
}
}

// TODO can be replaced by ByteArray.encryptToHashedElGamal()
fun ByteArray.encryptContestData(
publicKey: ElGamalPublicKey, // aka K
extendedBaseHash: UInt256, // aka He
contestId: String, // aka Λ
contestIndex: Int, // ind_c(Λ)
ballotNonce: UInt256): HashedElGamalCiphertext {

// D = D_1 ∥ D_2 ∥ · · · ∥ D_bD ; (spec 2.0, eq 49)
val messageBlocks: List<UInt256> =
this.toList()
.chunked(32) { block ->
// pad each block of the message to 32 bytes
val result = ByteArray(32) { 0 }
block.forEachIndexed { index, byte -> result[index] = byte }
UInt256(result)
}
ballotNonce: UInt256
): HashedElGamalCiphertext {

val group = compatibleContextOrFail(publicKey.key)

// ξ = H(HE ; 0x20, ξB , indc (Λ), “contest data”) (spec 2.0, eq 50)
val contestDataNonce = hashFunction(extendedBaseHash.bytes, 0x20.toByte(), ballotNonce, contestIndex, ContestData.contestDataLabel)

// ElectionGuard spec: (α, β) = (g^ξ mod p, K^ξ mod p); by encrypting a zero, we achieve exactly this
val (alpha, beta) = 0.encrypt(publicKey, contestDataNonce.toElementModQ(group))
// k = H(HE ; 0x22, K, α, β) ; (spec 2.0, eq 51)
val kdfKey = hashFunction(extendedBaseHash.bytes, 0x22.toByte(), publicKey, alpha, beta)

// TODO check
// context = b(”contest_data”) ∥ b(Λ).
val context = "${ContestData.contestDataLabel}$contestId"
val kdf = KDF(kdfKey, ContestData.label, context, this.size * 8) // TODO is this eq(52) ??

val k0 = kdf[0]
val c0 = alpha.byteArray() // (53)
val encryptedBlocks = messageBlocks.mapIndexed { i, p -> (p xor kdf[i + 1]).bytes }.toTypedArray()
val c1 = concatByteArrays(*encryptedBlocks) // (54)
val c2 = (c0 + c1).hmacSha256(k0) // ; eq (55) TODO can we use hmacFunction() ??

return HashedElGamalCiphertext(alpha, c1, c2, this.size)
val contestDataNonce =
hashFunction(extendedBaseHash.bytes, 0x20.toByte(), ballotNonce, contestIndex, ContestData.contestDataLabel)

return this.encryptToHashedElGamal(
group,
publicKey,
extendedBaseHash,
0x22.toByte(),
ContestData.label,
"${ContestData.contestDataLabel}$contestId",
contestDataNonce.toElementModQ(group),
)
}

// TODO could be used in Encryptor ??
fun makeContestData(
votesAllowed: Int,
contestLimit: Int,
optionLimit: Int,
selections: List<PlaintextBallot.Selection>,
writeIns: List<String>
): ContestData {
): Pair<ContestData, Int> {
// count the number of votes
val votedFor = mutableListOf<Int>()
for (selection in selections) {
var selectionOvervote = false
selections.forEach { selection ->
if (selection.vote > 0) {
votedFor.add(selection.sequenceOrder)
if (selection.vote > optionLimit) {
selectionOvervote = true
}
}
}

// Compute the contest status
val totalVotedFor = votedFor.size + writeIns.size
val status = if (totalVotedFor == 0) ContestDataStatus.null_vote
else if (totalVotedFor < votesAllowed) ContestDataStatus.under_vote
else if (totalVotedFor > votesAllowed) ContestDataStatus.over_vote
else ContestDataStatus.normal
else if (selectionOvervote || totalVotedFor > contestLimit) ContestDataStatus.over_vote
else if (totalVotedFor < contestLimit) ContestDataStatus.under_vote
else ContestDataStatus.normal

return ContestData(
val votes = if (status == ContestDataStatus.over_vote) 0 else totalVotedFor

val contestData = ContestData(
if (status == ContestDataStatus.over_vote) votedFor else emptyList(),
writeIns,
status
)

return Pair(contestData, votes)
}

fun HashedElGamalCiphertext.decryptWithBetaToContestData(
publicKey: ElGamalPublicKey, // aka K
extendedBaseHash: UInt256, // aka He
contestId: String, // aka Λ
beta : ElementModP) : Result<ContestData, String> {
beta: ElementModP
): Result<ContestData, String> {

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

Expand All @@ -185,56 +177,40 @@ fun HashedElGamalCiphertext.decryptWithNonceToContestData(
extendedBaseHash: UInt256, // aka He
contestId: String, // aka Λ
contestIndex: Int,
ballotNonce: UInt256) : Result<ContestData, String> {
ballotNonce: UInt256
): Result<ContestData, String> {

val group = compatibleContextOrFail(publicKey.key)
val contestDataNonce = hashFunction(extendedBaseHash.bytes, 0x20.toByte(), ballotNonce, contestIndex, ContestData.contestDataLabel)
val contestDataNonce =
hashFunction(extendedBaseHash.bytes, 0x20.toByte(), ballotNonce, contestIndex, ContestData.contestDataLabel)
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 ba: ByteArray = this.decryptContestData(publicKey, extendedBaseHash, contestId, alpha, beta)
?: return Err("decryptWithNonceToContestData did not succeed")
return ba.decodeToContestData()
}

fun HashedElGamalCiphertext.decryptWithSecretKey(
publicKey: ElGamalPublicKey, // aka K
extendedBaseHash: UInt256, // aka He
contestId: String, // aka Λ
secretKey: ElGamalSecretKey): ByteArray? = decryptContestData(publicKey, extendedBaseHash, contestId, c0, c0 powP secretKey.key)
secretKey: ElGamalSecretKey
): ByteArray? = decryptContestData(publicKey, extendedBaseHash, contestId, c0, c0 powP secretKey.key)

// TODO can be replaced by HashedElGamalCiphertext.decryptToByteArray()
fun HashedElGamalCiphertext.decryptContestData(
publicKey: ElGamalPublicKey, // aka K
extendedBaseHash: UInt256, // aka He
contestId: String, // aka Λ
alpha: ElementModP,
beta: ElementModP): ByteArray? {

// k = H(HE ; 22, K, α, β). (51)
val kdfKey = hashFunction(extendedBaseHash.bytes, 0x22.toByte(), publicKey, alpha, beta)

// context = b(”contest_data”) ∥ b(Λ).
val context = "${ContestData.contestDataLabel}$contestId"
val kdf = KDF(kdfKey, ContestData.label, context, numBytes * 8) // TODO check this (86, 87) ??
val k0 = kdf[0]

val expectedHmac = (c0.byteArray() + c1).hmacSha256(k0) // TODO use hmacFunction() ?

if (expectedHmac != c2) {
ContestData.logger.error { "HashedElGamalCiphertext decryptContestData failure: HMAC doesn't match" }
return null
}

val ciphertextBlocks = c1.toList().chunked(32) { it.toByteArray().toUInt256safe() } // eq 88
val plaintextBlocks = ciphertextBlocks.mapIndexed { i, c -> (c xor kdf[i + 1]).bytes }.toTypedArray()
val plaintext = concatByteArrays(*plaintextBlocks) // eq 89

return if (plaintext.size == numBytes) {
plaintext
} else {
// Truncate trailing values, which should be zeros.
// No need to check, because we've already validated the HMAC on the data.
plaintext.copyOfRange(0, numBytes)
}
beta: ElementModP
): ByteArray? {

return this.decryptToByteArray(
publicKey,
extendedBaseHash,
0x22.toByte(),
ContestData.label,
"${ContestData.contestDataLabel}$contestId",
alpha,
beta,
)
}


41 changes: 11 additions & 30 deletions src/main/kotlin/org/cryptobiotic/eg/encrypt/Encryptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -94,34 +94,16 @@ class Encryptor(
optionLimit: Int,
ballotNonce: UInt256,
): PendingEncryptedBallot.Contest {
val ballotSelections = this.selections.associateBy { it.selectionId }

// count the number of votes
val votedFor = mutableListOf<Int>()
var selectionOvervote = false
for (mselection: ManifestIF.Selection in mcontest.selections) {
val plaintextSelection = ballotSelections[mselection.selectionId] // Find the plaintext selection matching the manifest selectionId.
if (plaintextSelection != null && plaintextSelection.vote > 0) {
votedFor.add(plaintextSelection.sequenceOrder)
if (plaintextSelection.vote > optionLimit) {
selectionOvervote = true
}
}
}

// Compute the contest status
val totalVotedFor = votedFor.size + this.writeIns.size
val status = if (totalVotedFor == 0) ContestDataStatus.null_vote
else if (selectionOvervote || totalVotedFor > contestLimit) ContestDataStatus.over_vote
else if (totalVotedFor < contestLimit) ContestDataStatus.under_vote
else ContestDataStatus.normal
val (contestData, votes) = makeContestData(contestLimit, optionLimit, this.selections, this.writeIns)

val ballotSelections = this.selections.associateBy { it.selectionId }
val encryptedSelections = mutableListOf<PendingEncryptedBallot.Selection>()
for (mselection: ManifestIF.Selection in mcontest.selections) {
var plaintextSelection = ballotSelections[mselection.selectionId]

// Set vote to zero if not in manifest or this contest is overvoted. See 3.3.3 "Overvotes".
if (plaintextSelection == null || (status == ContestDataStatus.over_vote)) {
if (plaintextSelection == null || (contestData.status == ContestDataStatus.over_vote)) {
plaintextSelection = makeZeroSelection(mselection.selectionId, mselection.sequenceOrder)
}
encryptedSelections.add( plaintextSelection.encryptSelection(
Expand All @@ -131,21 +113,20 @@ class Encryptor(
))
}

val contestData = ContestData(
if (status == ContestDataStatus.over_vote) votedFor else emptyList(),
this.writeIns,
status
)

val contestDataEncrypted = contestData.encrypt(jointPublicKey, extendedBaseHash, mcontest.contestId,
mcontest.sequenceOrder, ballotNonce, contestLimit)
val contestDataEncrypted = contestData.encrypt(
jointPublicKey,
extendedBaseHash,
mcontest.contestId,
mcontest.sequenceOrder,
ballotNonce,
contestLimit)

return this.encryptContest(
group,
jointPublicKey,
extendedBaseHash,
contestLimit,
if (status == ContestDataStatus.over_vote) 0 else totalVotedFor,
votes,
encryptedSelections.sortedBy { it.sequenceOrder },
contestDataEncrypted,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ class DecryptionWithNonceTest {
// contestData matches
ballot.contests.forEach { orgContest ->
val mcontest = electionRecord.manifest().contests.find { it.contestId == orgContest.contestId }!!
val orgContestData = makeContestData(mcontest.contestSelectionLimit, orgContest.selections, orgContest.writeIns)
val (orgContestData, _) = makeContestData(mcontest.contestSelectionLimit, mcontest.optionSelectionLimit, orgContest.selections, orgContest.writeIns)

val dcontest = decryptedBallot.contests.find { it.contestId == orgContest.contestId }!!
assertEquals(dcontest.writeIns, orgContestData.writeIns)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@ fun testEncryptDecryptVerify(

ballot.contests.forEach { orgContest ->
val mcontest = manifest.contests.find { it.contestId == orgContest.contestId }!!
val orgContestData =
makeContestData(mcontest.contestSelectionLimit, orgContest.selections, orgContest.writeIns)
val (orgContestData, _) =
makeContestData(mcontest.contestSelectionLimit, mcontest.optionSelectionLimit, orgContest.selections, orgContest.writeIns)

val dcontest = decryptedBallot.contests.find { it.contestId == orgContest.contestId }
assertNotNull(dcontest)
Expand Down

0 comments on commit 8c8968f

Please sign in to comment.