Skip to content

Commit

Permalink
Merge pull request #428 from JohnLCaron/mixnet
Browse files Browse the repository at this point in the history
Add verificatum mixnet data, tests, and json reading
  • Loading branch information
JohnLCaron authored Nov 13, 2023
2 parents 6915402 + d1a6f41 commit e0358f6
Show file tree
Hide file tree
Showing 85 changed files with 30,132 additions and 19 deletions.
5 changes: 4 additions & 1 deletion egklib/src/commonMain/kotlin/electionguard/core/UtilsKmm.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ expect fun isDirectory(path: String): Boolean
/** Read lines from a file. */
expect fun fileReadLines(filename: String): List<String>

/** Read bytes from a file. */
/** Read all the bytes in a file. */
expect fun fileReadBytes(filename: String): ByteArray

/** Read all int text in a file. */
expect fun fileReadText(filename: String): String

/** Determine endianness of machine. */
expect fun isBigEndian(): Boolean

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 egklib/src/commonMain/kotlin/electionguard/mixnet/ByteTreeReader.kt
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()
}
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")
}
Loading

0 comments on commit e0358f6

Please sign in to comment.