Skip to content

Commit

Permalink
Merge pull request #444 from JohnLCaron/reorgElgamal
Browse files Browse the repository at this point in the history
Reorganize ElGamal.
  • Loading branch information
JohnLCaron authored Jan 21, 2024
2 parents be1a34f + b7d95f3 commit f630e12
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 165 deletions.
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ElectionGuard-Kotlin-Multiplatform

_last update 12/13/2023_
_last update 01/21/2024_

ElectionGuard-Kotlin-Multiplatform (EGK) is a [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html)
implementation of
Expand All @@ -23,9 +23,7 @@ Currently Java 17 is required.
* [Getting Started](#getting-started)
* [Workflow and Command Line Programs](#workflow-and-command-line-programs)
* [Serialization](#serialization)
* [Protobuf Serialization](#protobuf-serialization)
* [JSON Serialization](#json-serialization)
* [Previous Serialization specs](#previous-serialization-specs)
* [JSON Serialization specification](#json-serialization-specification)
* [Validation](#validation)
* [Verification](#verification)
* [Test Vectors](#test-vectors)
Expand Down Expand Up @@ -65,6 +63,7 @@ These are JSON files that give inputs and expected outputs for the purpose of te
* [Test Vectors](docs/TestVectors.md)

## Implementation Notes
* [Cryptography Notes](docs/CryptographyNotes.md)
* [Implementation Notes](docs/ImplementationNotes.md)

## Authors
Expand Down
39 changes: 39 additions & 0 deletions docs/CryptographyNotes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Cryptography Notes for spec 2.0

_8/07/2023_

- EKM uses an optimized encoding of an encrypted ElGamal counter, proposed by [Pereira](https://perso.uclouvain.be/olivier.pereira/). Where
ElectionGuard 1.0 defines $\mathrm{Encrypt}(g^a, r, m) = \left(g^r, g^{ar}g^m\right)$,
EKM instead defines $\mathrm{Encrypt}(g^a, r, m) = \left(g^r, g^{a(r + m)}\right)$.
This allows for one fewer exponentiation per encryption. EKM includes corresponding
changes in its Chaum-Pedersen proofs and discrete-log engine to support this.

- EKM further optimizes the Chaum-Pedersen proofs with a space-optimization from [Boneh and Shoup](http://toc.cryptobook.us/) that allows
the larger elements-mod-p to be elided from the proofs because they can be recomputed by the verifier.

- EKM includes a Chaum-Pedersen "range proof", which is a proof that a ciphertext
corresponds to a plaintext from 0 to a given constant. These are a generalization of the
earlier 0-or-1 disjunctive proofs. The size of the proof will be
linear with respect to the size of the constant, and when the constant is "1",
the proof will be the same size as the original disjunctive proof.

- EKM defines the result of a hash function as a 256-bit unsigned integer `UInt256` type rather than a `ElementModQ`
type. This simplifies the code in a variety of ways. The `UInt256` type is used in a number of other contexts,
like HMAC keys, allowing those implementations to be independent of the ElGamal group parameters.
For serialization, the output of `UInt256` type is identical to `ElementModQ`, so this is an internal
difference compatible with other implementations.

- Pereira's "pow-radix" precomputation, used for common bases like
the group generator or a public key, replaces modular exponentiation with
table lookups and multiplies. We support three table sizes. Batch computations
might then use larger tables and gain greater speedups, while memory-limited
computations would use smaller tables.

- In this table, we transform the numbers to [Montgomery form](https://en.wikipedia.org/wiki/Montgomery_modular_multiplication), allowing us
to avoid an expensive modulo operation after each multiply. HACL* has native support
for this transformation, resulting in significant speedups.
We also get modest speedups within the JVM.

- ElectionGuard defines two sets of global parameters: using either 3072-bit
or 4096-bit modular arithmetic. EKM supports both sets of parameters as well as a
"tiny" set of 32-bit parameters, used to radically speed up the unit tests.
62 changes: 12 additions & 50 deletions docs/ImplementationNotes.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
# Implementation Notes for spec 2.0

_draft 8/07/2023_
- EKM uses Kotlin's "multiplatform" features to support both JVM platforms (including
Android) and "native" platforms, including iOS. The primary
difference between these is how big integers are handled. On the JVM, we
use [java.math.BigInteger](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/math/BigInteger.html).
On native platforms, we use [Microsoft HACL*](https://www.microsoft.com/en-us/research/publication/hacl-a-verified-modern-cryptographic-library/).

IN PROGRESS
- All the code in EKM is thread-safe, and it's mostly functional. This
means that you can easily use libraries like [Java Streams](https://docs.oracle.com/javase/tutorial/collections/streams/parallelism.html)
to do parallel operations on a multicore computer. Similarly, for a voting
machine, you might take advantage of [Kotlin's coroutines](https://kotlinlang.org/docs/coroutines-guide.html)
to run an encryption in the background without creating lag on the UI thread.

## Specification Notes

### 3.1.3 Sequence numbers (indices)
* Sequence numbers must be unique with their containing object, and are used for canonical ordering.
Expand All @@ -18,54 +28,6 @@ version of the manifest, unless you can verify that it matches the original one.

## Historical Notes

- EKM uses Kotlin's "multiplatform" features to support both JVM platforms (including
Android) and "native" platforms, including iOS. The primary
difference between these is how big integers are handled. On the JVM, we
use [java.math.BigInteger](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/math/BigInteger.html).
On native platforms, we use [Microsoft HACL*](https://www.microsoft.com/en-us/research/publication/hacl-a-verified-modern-cryptographic-library/).

- EKM uses an optimized encoding of an encrypted ElGamal counter, proposed by [Pereira](https://perso.uclouvain.be/olivier.pereira/). Where
ElectionGuard 1.0 defines $\mathrm{Encrypt}(g^a, r, m) = \left(g^r, g^{ar}g^m\right)$,
EKM instead defines $\mathrm{Encrypt}(g^a, r, m) = \left(g^r, g^{a(r + m)}\right)$.
This allows for one fewer exponentiation per encryption. EKM includes corresponding
changes in its Chaum-Pedersen proofs and discrete-log engine to support this.

- EKM further optimizes the Chaum-Pedersen proofs with a space-optimization from [Boneh and Shoup](http://toc.cryptobook.us/) that allows
the larger elements-mod-p to be elided from the proofs because they can be recomputed by the verifier.

- EKM includes a Chaum-Pedersen "range proof", which is a proof that a ciphertext
corresponds to a plaintext from 0 to a given constant. These are a generalization of the
earlier 0-or-1 disjunctive proofs. The size of the proof will be
linear with respect to the size of the constant, and when the constant is "1",
the proof will be the same size as the original disjunctive proof.

- EKM defines the result of a hash function as a 256-bit unsigned integer `UInt256` type rather than a `ElementModQ`
type. This simplifies the code in a variety of ways. The `UInt256` type is used in a number of other contexts,
like HMAC keys, allowing those implementations to be independent of the ElGamal group parameters.
For serialization, the output of `UInt256` type is identical to `ElementModQ`, so this is an internal
difference compatible with other implementations.

- Pereira's "pow-radix" precomputation, used for common bases like
the group generator or a public key, replaces modular exponentiation with
table lookups and multiplies. We support three table sizes. Batch computations
might then use larger tables and gain greater speedups, while memory-limited
computations would use smaller tables.

- In this table, we transform the numbers to [Montgomery form](https://en.wikipedia.org/wiki/Montgomery_modular_multiplication), allowing us
to avoid an expensive modulo operation after each multiply. HACL* has native support
for this transformation, resulting in significant speedups.
We also get modest speedups within the JVM.

- ElectionGuard defines two sets of global parameters: using either 3072-bit
or 4096-bit modular arithmetic. EKM supports both sets of parameters as well as a
"tiny" set of 32-bit parameters, used to radically speed up the unit tests.

- All the code in EKM is thread-safe, and it's mostly functional. This
means that you can easily use libraries like [Java Streams](https://docs.oracle.com/javase/tutorial/collections/streams/parallelism.html)
to do parallel operations on a multicore computer. Similarly, for a voting
machine, you might take advantage of [Kotlin's coroutines](https://kotlinlang.org/docs/coroutines-guide.html)
to run an encryption in the background without creating lag on the UI thread.

### What about JavaScript?

We tried to include JavaScript, which you can see at the tag [BEFORE_REMOVING_JS](https://github.com/danwallach/electionguard-kotlin-multiplatform/releases/tag/BEFORE_REMOVING_JS). This
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package electionguard.core

/**
* An "exponential ElGamal ciphertext" (i.e., with the plaintext in the exponent to allow for
* homomorphic addition). (See [ElGamal 1982](https://ieeexplore.ieee.org/abstract/document/1057074))
*
* In a "normal" ElGamal encryption where the message goes into the exponent, a secret key `a`
* with corresponding public key `g^a`, message `M` and nonce `R` would be encoded as the tuple
* `(g^R, (g^a)^r * g^M)`.
*
* In this particular ElGamal implementation, we're instead encoding the ciphertext as
* `(g^R, (g^a)^{R+M})`. This accelerates both the encryption process and the process of generating
* the corresponding Chaum-Pedersen proofs.
*
* This also means that this ElGamal ciphertext is *not compatible with ElectionGuard 1.0*, but
* is anticipated to be the standard for ElectionGuard 2.0 and later.
*/
data class ElGamalCiphertext(val pad: ElementModP, val data: ElementModP) {
override fun toString() = "pad=${pad.toStringShort()} data=${data.toStringShort()} "


/** Decrypts using the secret key from the keypair. If the decryption fails, `null` is returned. */
fun decrypt(keypair: ElGamalKeypair): Int? {
compatibleContextOrFail(pad, data, keypair.secretKey.key)
val blind = pad powP keypair.secretKey.negativeKey
val kPowM = data * blind

return keypair.publicKey.dLog(kPowM)
}

/** Compute the share of the decryption from the secret key. */
fun computeShare(secretKey: ElGamalSecretKey): ElementModP {
compatibleContextOrFail(pad, data, secretKey.key)
return pad powP secretKey.key
}

fun decryptWithShares(publicKey: ElGamalPublicKey, shares: Iterable<ElementModP>): Int? {
val sharesList = shares.toList()
val context = compatibleContextOrFail(pad, data, publicKey.key, *(sharesList.toTypedArray()))
val allSharesProductM: ElementModP = with(context) { sharesList.multP() }
val decryptedValue: ElementModP = this.data / allSharesProductM
return publicKey.dLog(decryptedValue)
}

/** Decrypts a message by knowing the nonce. If the decryption fails, `null` is returned. */
fun decryptWithNonce(publicKey: ElGamalPublicKey, nonce: ElementModQ, maxResult: Int = -1): Int? {
compatibleContextOrFail(pad, data, publicKey.key, nonce)

val blind = publicKey powP (-nonce)
val kPowM = data * blind // data * blind = publicKey ^ m
return publicKey.dLog(kPowM, maxResult)
}

/** Homomorphically "adds" two ElGamal ciphertexts together through piecewise multiplication. */
operator fun plus(o: ElGamalCiphertext): ElGamalCiphertext {
compatibleContextOrFail(this.pad, this.data, o.pad, o.data)
return ElGamalCiphertext(pad * o.pad, data * o.data)
}

// reencrypt with another nonce. used in mixnet
fun reencrypt(publicKey: ElGamalPublicKey, nonce: ElementModQ): ElGamalCiphertext {
// Encr(m) = (g^ξ, K^(m+ξ)) = (a, b)
// ReEncr(m) = (g^(ξ+ξ'), K^(m+ξ+ξ')) = (a * g^ξ', b * K^ξ')
// Encr(0) = (g^ξ', K^ξ') = (a', b'), so ReEncr(m) = (a * a', b * b') = Encr(0) * Encr(m)

val group = publicKey.context
val ap = group.gPowP(nonce)
val bp = publicKey.key powP nonce
return ElGamalCiphertext(this.pad * ap, this.data * bp)
}

}

/**
* Homomorphically "adds" a sequence of ElGamal ciphertexts through piecewise multiplication.
* @throws ArithmeticException if the sequence is empty
*/
fun List<ElGamalCiphertext>.encryptedSum(): ElGamalCiphertext? {
return if (this.isEmpty()) null else this.reduce { a, b -> a + b }
}

/** Add two lists by component-wise multiplication */
fun List<ElGamalCiphertext>.add(other: List<ElGamalCiphertext>): List<ElGamalCiphertext> {
require(this.size == other.size)
val result = mutableListOf<ElGamalCiphertext>()
this.forEachIndexed { index, value ->
result.add(value + other[index])
}
return result
}
Loading

0 comments on commit f630e12

Please sign in to comment.