-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #428 from JohnLCaron/mixnet
Add verificatum mixnet data, tests, and json reading
- Loading branch information
Showing
85 changed files
with
30,132 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
66 changes: 66 additions & 0 deletions
66
egklib/src/commonMain/kotlin/electionguard/decrypt/CiphertextDecryptor.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
package electionguard.decrypt | ||
|
||
import com.github.michaelbull.result.* | ||
import electionguard.core.* | ||
import electionguard.publish.Consumer | ||
import electionguard.publish.makeConsumer | ||
import electionguard.publish.makeTrusteeSource | ||
import electionguard.util.ErrorMessages | ||
import electionguard.util.mergeErrorMessages | ||
|
||
// uses ElectionInitialized.guardians to read DecryptingTrustee's from trusteeDir | ||
class CiphertextDecryptor( | ||
group: GroupContext, | ||
inputDir: String, | ||
trusteeDir: String, | ||
missing: String? = null | ||
) { | ||
val trustees : List<DecryptingTrusteeIF> | ||
val lagrangeCoeff : List<ElementModQ> | ||
val secretKey : ElementModQ | ||
val keyPair : ElGamalKeypair | ||
|
||
init { | ||
val consumerIn = makeConsumer(group, inputDir) | ||
val initResult = consumerIn.readElectionInitialized() | ||
if (initResult is Err) { | ||
throw RuntimeException(initResult.error.toString()) | ||
} | ||
val init = initResult.unwrap() | ||
val trusteeSource: Consumer = makeTrusteeSource(trusteeDir, group, consumerIn.isJson()) | ||
val readTrusteeResults: List<Result<DecryptingTrusteeIF, ErrorMessages>> = | ||
init.guardians.map { trusteeSource.readTrustee(trusteeDir, it.guardianId) } | ||
val (allTrustees, allErrors) = readTrusteeResults.partition() | ||
if (allErrors.isNotEmpty()) { | ||
throw RuntimeException(mergeErrorMessages("readDecryptingTrustees", allErrors).toString()) | ||
} | ||
trustees = if (missing.isNullOrEmpty()) { | ||
allTrustees | ||
} else { | ||
// remove missing guardians | ||
val missingX = missing.split(",").map { it.toInt() } | ||
allTrustees.filter { !missingX.contains(it.xCoordinate()) } | ||
} | ||
|
||
// build the lagrangeCoordinates once and for all | ||
val coeffs = mutableListOf<ElementModQ>() | ||
for (trustee in trustees) { | ||
// available trustees minus me | ||
val present: List<Int> = trustees.filter { it.id() != trustee.id() }.map { it.xCoordinate() } | ||
coeffs.add( group.computeLagrangeCoefficient(trustee.xCoordinate(), present)) | ||
} | ||
this.lagrangeCoeff = coeffs | ||
|
||
// The decryption M = A^s mod p can be computed as shown in Equation (68) because | ||
// s = Sum( wi * P(i)) mod q, i∈U | ||
secretKey = with (group) { | ||
trustees.mapIndexed { idx, it -> (it as DecryptingTrusteeDoerre).keyShare * lagrangeCoeff[idx] }.addQ() | ||
} | ||
require(init.jointPublicKey == group.gPowP(secretKey)) | ||
keyPair = ElGamalKeypair(ElGamalSecretKey(secretKey), ElGamalPublicKey(init.jointPublicKey)) | ||
} | ||
|
||
fun decrypt(ciphertext : ElGamalCiphertext) : Int? { | ||
return ciphertext.decrypt(keyPair) | ||
} | ||
} |
215 changes: 215 additions & 0 deletions
215
egklib/src/commonMain/kotlin/electionguard/mixnet/ByteTreeReader.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,215 @@ | ||
package electionguard.mixnet | ||
|
||
import electionguard.core.Base16.fromHex | ||
import electionguard.util.Indent | ||
import java.io.EOFException | ||
import java.io.File | ||
|
||
// seems likely i ported this from the java in vcr | ||
|
||
fun readTextLinesFromFile(filename : String, maxLines : Int = -1) { | ||
println("readTextTreeFromFile = ${filename}") | ||
|
||
var count = 0 | ||
val file = File(filename) | ||
file.forEachLine { | ||
if (maxLines > 0 && count < maxLines) { // TODO LAME | ||
val tree = readByteTree(it) | ||
println(tree.show()) | ||
} | ||
count++ | ||
} | ||
println("total nlines = $count") | ||
} | ||
|
||
fun readByteTreeFromFile(filename : String) : ByteTreeRoot { | ||
println("readByteTreeFromFile = ${filename}") | ||
|
||
// gulp the entire file to a byte array | ||
val file = File(filename) | ||
val ba : ByteArray = file.readBytes() | ||
return readByteTree(ba) | ||
} | ||
|
||
fun readByteTree(marsh : String) : ByteTreeRoot { | ||
var beforeDoubleColon : String? = null | ||
var byteArray : ByteArray? = if (marsh.contains("::")) { | ||
val frags = marsh.split("::") | ||
// frags.forEach { println(it) } | ||
beforeDoubleColon = frags[0] | ||
frags[1].fromHex() | ||
} else { | ||
marsh.fromHex() | ||
} | ||
if (byteArray == null) { | ||
val result = ByteTreeRoot(ByteArray(0)) | ||
result.error = "Did not find a hex array" | ||
result.beforeDoubleColon = beforeDoubleColon | ||
return result | ||
} | ||
|
||
val result = ByteTreeRoot(byteArray) | ||
|
||
result.beforeDoubleColon = beforeDoubleColon | ||
if (result.root.children.size == 2) { | ||
val classNode = result.root.children[0] | ||
if (classNode.content != null) { // && is UTF | ||
result.className = String(classNode.content) | ||
} | ||
} | ||
return result | ||
} | ||
|
||
val COLON = ':'.code.toByte() | ||
fun readByteTree(ba : ByteArray) : ByteTreeRoot { | ||
var split = -1 | ||
for (idx in 0..100) { | ||
if (ba[idx] == COLON && ba[idx+1] == COLON) { | ||
split = idx | ||
} | ||
} | ||
|
||
var beforeDoubleColon : String? = null | ||
var byteArray : ByteArray? = if (split > 0) { | ||
val beforeBytes = ByteArray(split) { ba[it] } | ||
beforeDoubleColon = String(beforeBytes) | ||
val remaining = ba.size - (split + 2) | ||
ByteArray(remaining) { ba[it + split + 2] } | ||
} else { | ||
ba | ||
} | ||
if (byteArray == null) { | ||
val result = ByteTreeRoot(ByteArray(0)) | ||
result.error = "Did not find a hex array" | ||
result.beforeDoubleColon = beforeDoubleColon | ||
return result | ||
} | ||
|
||
val result = ByteTreeRoot(byteArray) | ||
|
||
result.beforeDoubleColon = beforeDoubleColon | ||
if (result.root.children.size == 2) { | ||
val classNode = result.root.children[0] | ||
if (classNode.content != null) { | ||
result.className = String(classNode.content) | ||
} | ||
} | ||
return result | ||
} | ||
|
||
class ByteTreeRoot(byteArray : ByteArray) { | ||
var error: String? = null | ||
var beforeDoubleColon: String? = null | ||
var className: String? = null | ||
private var nodeCount = 0 | ||
val root : Node = Node(byteArray, 0, "root") | ||
|
||
fun show(maxDepth: Int = 100): String { | ||
return buildString { | ||
if (error != null) { | ||
appendLine("error = $error") | ||
} else { | ||
if (beforeDoubleColon != null) appendLine("beforeDoubleColon = '$beforeDoubleColon'") | ||
if (className != null) appendLine("marshalled className = '$className'") | ||
append(root.show(Indent(0), maxDepth)) | ||
} | ||
} | ||
} | ||
|
||
fun makeNode(ba: ByteArray, start: Int, name : String) : Node { | ||
return Node(ba, start, name) | ||
} | ||
|
||
inner class Node(ba: ByteArray, start: Int, val name : String) { | ||
val isLeaf: Boolean | ||
val n: Int | ||
val children = mutableListOf<Node>() | ||
val content: ByteArray? | ||
var size: Int = 5 | ||
var nodeCount = 1 | ||
|
||
init { | ||
if (ba.size == 0) { | ||
isLeaf = false | ||
n = 0 | ||
content = null | ||
} else { | ||
if (start >= ba.size) { | ||
throw RuntimeException("exceeded size") | ||
} | ||
if (ba[start] > 1) { | ||
throw RuntimeException("not a ByteTree") | ||
} | ||
isLeaf = ba[start] == 1.toByte() | ||
n = readInt(ba, start + 1) | ||
if (n >= ba.size) { | ||
throw RuntimeException("Illegal value for n = $n") | ||
} | ||
// println("$name $isLeaf $start $n") | ||
if (isLeaf) { | ||
content = ByteArray(n) { ba[start + 5 + it] } | ||
size += n | ||
} else { | ||
content = null | ||
var idx = start + 5 | ||
repeat(n) { | ||
val child = makeNode(ba, idx, "$name-$nodeCount") | ||
nodeCount++ | ||
children.add(child) | ||
idx += child.size | ||
this.size += child.size | ||
} | ||
} | ||
} | ||
} | ||
|
||
fun show(indent: Indent, maxDepth: Int = 100): String { | ||
return if (indent.level > maxDepth && nodeCount > 11) "" else { | ||
return buildString { | ||
append("${indent}$name n=$n size=$size ") | ||
if (isLeaf) { | ||
appendLine("content='${content!!.toHexLower()}'") | ||
} else { | ||
appendLine() | ||
children.forEach { append(it.show(indent.incr(), maxDepth)) } | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
fun readInt(ba : ByteArray, start : Int) : Int { | ||
val ch1: Int = ba[start].toInt() | ||
val ch2: Int = ba[start+1].toInt() | ||
val ch3: Int = ba[start+2].toInt() | ||
val ch4: Int = ba[start+3].toInt() | ||
if (ch1 or ch2 or ch3 or ch4 < 0) { | ||
throw EOFException() | ||
} | ||
return (ch1 shl 24) + (ch2 shl 16) + (ch3 shl 8) + ch4 | ||
} | ||
|
||
private val hexChars = | ||
charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f') | ||
|
||
fun ByteArray.toHexLower(): String { | ||
// Performance note: since we're doing lookups in an array of characters, this | ||
// is going to run pretty quickly. This code is in the path for computing | ||
// cryptographic hashes, so performance matters here. | ||
|
||
if (isEmpty()) return "" // hopefully won't happen | ||
|
||
val result = | ||
CharArray(2 * this.size) { | ||
val offset: Int = it / 2 | ||
val even: Boolean = (it and 1) == 0 | ||
val nibble = | ||
if (even) | ||
(this[offset].toInt() and 0xf0) shr 4 | ||
else | ||
this[offset].toInt() and 0xf | ||
hexChars[nibble] | ||
} | ||
return result.concatToString() | ||
} |
87 changes: 87 additions & 0 deletions
87
egklib/src/commonMain/kotlin/electionguard/mixnet/MixnetBallotJson.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package electionguard.mixnet | ||
|
||
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.core.Base16.fromHex | ||
import electionguard.core.ElGamalCiphertext | ||
import electionguard.core.ElementModP | ||
import electionguard.core.GroupContext | ||
import electionguard.core.fileReadText | ||
import electionguard.util.Indent | ||
import kotlinx.serialization.Serializable | ||
import kotlinx.serialization.json.Json | ||
|
||
@Serializable | ||
data class MixnetBallotJson( | ||
val wtf : List<List<List<String>>>, | ||
) { | ||
fun show(): String{ | ||
return buildString { | ||
val indent = Indent(0) | ||
wtf.forEachIndexed { idx1, it1 -> | ||
appendLine("${indent}ballot-${idx1+1} [") | ||
val indent1 = indent.incr() | ||
it1.forEachIndexed { idx2, it2 -> | ||
val what = if (idx2 == 0) "pad" else "data" | ||
appendLine("${indent1}${what} [") | ||
val indent2 = indent1.incr() | ||
it2.forEachIndexed { idx3, it3 -> | ||
appendLine("$indent2 ${idx3+1} ${it3.substring(2, 20)}...") | ||
} | ||
appendLine("$indent1]") | ||
} | ||
appendLine("$indent]") | ||
} | ||
} | ||
} | ||
} | ||
|
||
data class MixnetBallot( | ||
val ciphertext: List<ElGamalCiphertext> | ||
) { | ||
fun show(): String{ | ||
return buildString { | ||
ciphertext.forEachIndexed { idx, it -> | ||
appendLine("${idx+1} $it") | ||
} | ||
} | ||
} | ||
} | ||
|
||
fun MixnetBallotJson.import(group: GroupContext) : List<MixnetBallot> { | ||
val mxBallots = | ||
wtf.map { padAndData -> | ||
val pads = padAndData[0].import(group) | ||
val datas = padAndData[1].import(group) | ||
val ciphers = pads.zip(datas).map { (pad, data) -> ElGamalCiphertext(pad, data )} | ||
MixnetBallot(ciphers) | ||
} | ||
return mxBallots | ||
} | ||
|
||
private fun List<String>.import(group: GroupContext) : List<ElementModP> { | ||
val ps = this.map { | ||
val strip00 = it.substring(2) | ||
group.binaryToElementModP(strip00.fromHex()!!)!! | ||
} | ||
return ps | ||
} | ||
|
||
fun readMixnetJsonBallots(group: GroupContext, filename: String): List<MixnetBallot> { | ||
val jsonReader = Json { explicitNulls = false; ignoreUnknownKeys = true } | ||
val result = readMixnetBallotWrapped(jsonReader, filename) | ||
return result.unwrap().import(group) | ||
} | ||
|
||
private fun readMixnetBallotWrapped(jsonReader: Json, filename: String): Result<MixnetBallotJson, String> = | ||
try { | ||
val text = fileReadText(filename) | ||
val wrap = "{ \"wtf\": $text }" | ||
var mixnetInput: MixnetBallotJson = jsonReader.decodeFromString<MixnetBallotJson>(wrap) | ||
Ok(mixnetInput) | ||
} catch (e: Exception) { | ||
e.printStackTrace() | ||
Err(e.message ?: "readMixnetBallotWrapped on $filename has error") | ||
} |
Oops, something went wrong.