diff --git a/bulletin-board-server/build.gradle b/bulletin-board-server/build.gradle index 7e06c1c..1606ef8 100644 --- a/bulletin-board-server/build.gradle +++ b/bulletin-board-server/build.gradle @@ -259,5 +259,3 @@ publishing { } } - - diff --git a/distributed-key-generation/src/main/java/meerkat/crypto/dkg/feldman/Protocol.java b/distributed-key-generation/src/main/java/meerkat/crypto/dkg/feldman/Protocol.java index d81c75a..f612b07 100644 --- a/distributed-key-generation/src/main/java/meerkat/crypto/dkg/feldman/Protocol.java +++ b/distributed-key-generation/src/main/java/meerkat/crypto/dkg/feldman/Protocol.java @@ -33,7 +33,7 @@ public class Protocol extends VerifiableSecretSharing { Waiting, /** - * Party gave invalid answer to conplaint. + * Party gave invalid answer to complaint. */ Disqualified, @@ -79,6 +79,7 @@ public class Protocol extends VerifiableSecretSharing { * it must be chosen such that computing discrete logarithms is hard in this group. * @param encoder Encode/Decode group elements (of type T) to/from byte array */ + //TODO: why the use of regular Random? Should it be changed? public Protocol(int t, int n, BigInteger zi, Random random, BigInteger q, T g , Group group, int id, ByteEncoder encoder) { super(t, n, zi, random, q, g,group); diff --git a/distributed-key-generation/src/main/java/meerkat/crypto/dkg/feldman/User.java b/distributed-key-generation/src/main/java/meerkat/crypto/dkg/feldman/User.java index 6563dfd..f7c4624 100644 --- a/distributed-key-generation/src/main/java/meerkat/crypto/dkg/feldman/User.java +++ b/distributed-key-generation/src/main/java/meerkat/crypto/dkg/feldman/User.java @@ -436,7 +436,7 @@ public class User implements Runnable { /** * complaint message is valid if: * 1. it was received in broadcast chanel - * 2. the sender didn't complained against id before + * 2. the sender didn't complain against id before */ protected boolean isValidComplaintMessage(int sender, boolean isBroadcast, DKG.IDMessage complaintMessage){ int i = sender; diff --git a/distributed-key-generation/src/main/java/meerkat/crypto/secretsharing/shamir/Polynomial.java b/distributed-key-generation/src/main/java/meerkat/crypto/secretsharing/shamir/Polynomial.java index 58f38fb..b00aec5 100644 --- a/distributed-key-generation/src/main/java/meerkat/crypto/secretsharing/shamir/Polynomial.java +++ b/distributed-key-generation/src/main/java/meerkat/crypto/secretsharing/shamir/Polynomial.java @@ -198,6 +198,7 @@ public class Polynomial implements Comparable { @Override public boolean equals(Object obj) { + //TODO: is this implementation correct? cannot understand its logic (hai) if(!super.equals(obj)) return false; Point other = (Point)obj; diff --git a/distributed-key-generation/src/test/java/meerkat/crypto/dkg/gjkr/SDKGTest.java b/distributed-key-generation/src/test/java/meerkat/crypto/dkg/gjkr/SDKGTest.java index e4550f0..d172c23 100644 --- a/distributed-key-generation/src/test/java/meerkat/crypto/dkg/gjkr/SDKGTest.java +++ b/distributed-key-generation/src/test/java/meerkat/crypto/dkg/gjkr/SDKGTest.java @@ -10,6 +10,7 @@ import meerkat.crypto.secretsharing.shamir.Polynomial; import meerkat.crypto.secretsharing.shamir.SecretSharing; import meerkat.crypto.utils.BigIntegerByteEncoder; import meerkat.crypto.utils.GenerateRandomPrime; +import meerkat.protobuf.Crypto; import org.factcenter.qilin.primitives.Group; import org.factcenter.qilin.primitives.concrete.Zpstar; import org.factcenter.qilin.util.ByteEncoder; @@ -20,10 +21,7 @@ import org.junit.internal.runners.statements.Fail; import static org.junit.Assert.*; import java.math.BigInteger; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Random; -import java.util.Set; +import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -42,58 +40,57 @@ public class SDKGTest { BigInteger q = p.subtract(BigInteger.ONE).divide(BigInteger.valueOf(2)); Group group = new Zpstar(p); Arithmetic arithmetic = new Fp(q); - int t = 1; - int n = 20; + int initialT = 17; + int initialN = 20; Random rand = new Random(1); public void oneTest(Testable testable) throws Exception { - for (int i = 0; i < testable.sdkgs.length ; i++){ + for (int i = 0; i < testable.sdkgs.length; i++){ testable.futures[i] = executorService.submit(testable.sdkgs[i]); } - for (int i = 0; i < testable.futures.length ; i++){ - testable.futures[i].get(); + for (Future ftr : testable.futures) { + ftr.get(); } // got the right public value - BigInteger publicValue = group.multiply(testable.g,testable.secret); - for (int i: testable.valids){ - assert (testable.sdkgs[i - 1].getPublicValue().equals(publicValue)); + BigInteger publicValue = group.multiply(testable.g, testable.secret); + for (int i : testable.valids){ + assertEquals (testable.sdkgs[i-1].getPublicValue(), publicValue); } // assert valid verification values BigInteger expected,verification; - for (int i: testable.valids){ + for (int i : testable.valids){ expected = group.multiply(testable.g, testable.sdkgs[i - 1].getShare().y); verification = VerifiableSecretSharing.computeVerificationValue(i, testable.sdkgs[i - 1].getCommitments(), group); - assert (expected.equals(verification)); + assertEquals (expected, verification); } // restore the secret from shares - ArrayList sharesList = new ArrayList(); + ArrayList sharesList = new ArrayList<>(); - for (int i: testable.valids){ + for (int i : testable.valids){ sharesList.add(testable.sdkgs[i - 1].getShare()); } Polynomial.Point[] shares = new Polynomial.Point[sharesList.size()]; - for (int i = 0; i < shares.length; i ++){ - shares[i] = sharesList.get(i); - } + shares = sharesList.toArray(shares); - BigInteger calculatedSecret = SecretSharing.recoverSecret(shares,arithmetic); - assert (calculatedSecret.equals(testable.secret)); + BigInteger calculatedSecret = SecretSharing.recoverSecret(shares, arithmetic); + assertEquals (calculatedSecret, testable.secret); } @Test - public void test() throws Exception { + public void runSharingProtocol() throws Exception { Testable testable; for (int i = 0; i < NUM_TESTS; i++) { - testable = new Testable(n, t, group, q, rand); + testable = new Testable(initialN+i, initialT+i, group, q, rand); oneTest(testable); } } + static class Testable { Set valids; Set QUAL; @@ -118,14 +115,14 @@ public class SDKGTest { this.q = q; this.random = random; this.sdkgs = new User[n]; - this.valids = new HashSet(); - this.QUAL = new HashSet(); - this.aborted = new HashSet(); - this.malicious = new HashSet(); + this.valids = new HashSet<>(); + this.QUAL = new HashSet<>(); + this.aborted = new HashSet<>(); + this.malicious = new HashSet<>(); this.futures = new Future[n]; this.g = sampleGenerator(random); - this.h = group.multiply(g,randomIntModQ(random)); - ArrayList ids = new ArrayList(); + this.h = group.multiply(g, randomIntModQ(random)); + List ids = new ArrayList<>(); for (int id = 1; id<= n ; id++){ ids.add(id); } @@ -139,8 +136,8 @@ public class SDKGTest { id = ids.remove(random.nextInt(ids.size())); s = randomIntModQ(random); channel = channels.getChannel(id); - sdkg = new Protocol(t, n, s, random, q, g , h, group, id,encoder); - sdkgs[id - 1] = randomSDKGUser(id,channel,sdkg); + sdkg = new Protocol<>(t, n, s, random, q, g , h, group, id, encoder); + sdkgs[id - 1] = randomSDKGUser(id, channel, sdkg); if(QUAL.contains(id)){ this.secret = this.secret.add(s).mod(q); } @@ -159,7 +156,7 @@ public class SDKGTest { case HONEST: valids.add(id); QUAL.add(id); - return new User(sdkg,channel); + return new User<>(sdkg,channel); case FAILSTOP: int abortStage = random.nextInt(3) + 1; // 1 or 2 or 3 @@ -167,13 +164,13 @@ public class SDKGTest { if (abortStage > 1){ QUAL.add(id); } - return new SDKGUserImplAbort(sdkg,channel,abortStage); + return new SDKGUserImplAbort(sdkg, channel, abortStage); case MALICIOUS: malicious.add(id); - Set falls = DKGMaliciousUser.selectFallsRandomly(valids,random); - Protocol maliciousSDKG = SDKGMaliciousUserImpl.generateMaliciousSDKG(sdkg,channel,random); - return new SDKGMaliciousUserImpl(sdkg,maliciousSDKG,channel,falls); + Set falls = DKGMaliciousUser.selectFallsRandomly(valids, random); + Protocol maliciousSDKG = SDKGMaliciousUserImpl.generateMaliciousSDKG(sdkg, channel, random); + return new SDKGMaliciousUserImpl(sdkg, maliciousSDKG, channel, falls); } fail("Unknown user type"); @@ -203,4 +200,6 @@ public class SDKGTest { } } + + } diff --git a/gradle/wrapper/gradle-wrapper.properties.orig b/gradle/wrapper/gradle-wrapper.properties.orig new file mode 100644 index 0000000..1384b44 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties.orig @@ -0,0 +1,11 @@ +<<<<<<< HEAD +#Sun Mar 20 15:13:00 IST 2016 +======= +#Tue Aug 05 03:26:05 IDT 2014 +>>>>>>> e8e511d9ce636a127bb33d70ebfd9b2f230c6e1d +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.9-all.zip +distributionSha256Sum=4647967f8de78d6d6d8093cdac50f368f8c2b8038f41a5afe1c3bce4c69219a9 diff --git a/meerkat-common/build.gradle b/meerkat-common/build.gradle index a9035b0..6bf5ad7 100644 --- a/meerkat-common/build.gradle +++ b/meerkat-common/build.gradle @@ -50,7 +50,7 @@ dependencies { compile 'com.google.guava:guava:15.0' // Crypto - compile 'org.factcenter.qilin:qilin:1.2+' + compile 'org.factcenter.qilin:qilin:1.2.+' compile 'org.bouncycastle:bcprov-jdk15on:1.53' testCompile 'junit:junit:4.+' diff --git a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java index c51f2f2..e775226 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java @@ -47,7 +47,7 @@ public class ECDSASignature implements DigitalSignature { ByteBuffer lenBuf = ByteBuffer.allocate(4); - Map loadedCertificates = new HashMap<>(); + Map loadedCertificates = new HashMap(); /** * Signature currently loaded (will be used in calls to {@link #verify()}). diff --git a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECElGamalEncryption.java b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECElGamalEncryption.java index ab14421..31ad4c1 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECElGamalEncryption.java +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECElGamalEncryption.java @@ -107,6 +107,15 @@ public class ECElGamalEncryption implements Encryption { .build(); } + public static ConcreteCrypto.ElGamalCiphertext RerandomizableEncryptedMessage2ElGamalCiphertext(Crypto.RerandomizableEncryptedMessage msg) throws InvalidProtocolBufferException { + return ConcreteCrypto.ElGamalCiphertext.parseFrom(msg.getData()); + } + + public static Crypto.RerandomizableEncryptedMessage elGamalCiphertext2RerandomizableEncryptedMessage(ConcreteCrypto.ElGamalCiphertext msg) { + return Crypto.RerandomizableEncryptedMessage.newBuilder() + .setData(msg.toByteString()).build(); + } + @Override public Crypto.RerandomizableEncryptedMessage rerandomize(Crypto.RerandomizableEncryptedMessage msg, Crypto.EncryptionRandomness rnd) throws InvalidProtocolBufferException { BigInteger rndInt = BigIntegers.fromUnsignedByteArray(rnd.getData().toByteArray()); @@ -135,4 +144,8 @@ public class ECElGamalEncryption implements Encryption { return retval; } + + public BigInteger extractRandomness(Crypto.EncryptionRandomness encryptionRandomness){ + return new BigInteger(1,encryptionRandomness.getData().toByteArray()); + } } diff --git a/meerkat-common/src/main/java/meerkat/crypto/mixnet/Mix2ZeroKnowledgeProver.java b/meerkat-common/src/main/java/meerkat/crypto/mixnet/Mix2ZeroKnowledgeProver.java index 1113bfd..487483e 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/mixnet/Mix2ZeroKnowledgeProver.java +++ b/meerkat-common/src/main/java/meerkat/crypto/mixnet/Mix2ZeroKnowledgeProver.java @@ -1,5 +1,6 @@ package meerkat.crypto.mixnet; +import com.google.protobuf.InvalidProtocolBufferException; import meerkat.protobuf.Crypto; import meerkat.protobuf.Mixing; @@ -11,8 +12,9 @@ public interface Mix2ZeroKnowledgeProver { Crypto.RerandomizableEncryptedMessage in2, Crypto.RerandomizableEncryptedMessage out1, Crypto.RerandomizableEncryptedMessage out2, - boolean switched, + boolean switched,int i,int j, int layer, // switch info Crypto.EncryptionRandomness r1, - Crypto.EncryptionRandomness r2); + Crypto.EncryptionRandomness r2) throws InvalidProtocolBufferException; + } diff --git a/meerkat-common/src/main/java/meerkat/crypto/mixnet/Mix2ZeroKnowledgeVerifier.java b/meerkat-common/src/main/java/meerkat/crypto/mixnet/Mix2ZeroKnowledgeVerifier.java index dd3c251..f98f0ac 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/mixnet/Mix2ZeroKnowledgeVerifier.java +++ b/meerkat-common/src/main/java/meerkat/crypto/mixnet/Mix2ZeroKnowledgeVerifier.java @@ -1,5 +1,6 @@ package meerkat.crypto.mixnet; +import com.google.protobuf.InvalidProtocolBufferException; import meerkat.protobuf.Crypto; import meerkat.protobuf.Mixing; @@ -19,5 +20,5 @@ public interface Mix2ZeroKnowledgeVerifier { Crypto.RerandomizableEncryptedMessage in2, Crypto.RerandomizableEncryptedMessage out1, Crypto.RerandomizableEncryptedMessage out2, - Mixing.ZeroKnowledgeProof proof); + Mixing.ZeroKnowledgeProof proof) throws InvalidProtocolBufferException; } diff --git a/meerkat-common/src/main/java/meerkat/crypto/mixnet/Mixer.java b/meerkat-common/src/main/java/meerkat/crypto/mixnet/Mixer.java index 52e8844..a9b61d7 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/mixnet/Mixer.java +++ b/meerkat-common/src/main/java/meerkat/crypto/mixnet/Mixer.java @@ -1,11 +1,15 @@ package meerkat.crypto.mixnet; +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.protobuf.Crypto; + import java.util.List; -import static meerkat.protobuf.Voting.*; +import java.util.Random; /** * Created by talm on 25/10/15. */ public interface Mixer { - public List mix(List ballots); + public MixerOutput mix(List ciphertexts,Random random) + throws InvalidProtocolBufferException; } diff --git a/meerkat-common/src/main/java/meerkat/crypto/mixnet/MixerOutput.java b/meerkat-common/src/main/java/meerkat/crypto/mixnet/MixerOutput.java new file mode 100644 index 0000000..eb71e61 --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/crypto/mixnet/MixerOutput.java @@ -0,0 +1,13 @@ +package meerkat.crypto.mixnet; + +import meerkat.protobuf.Crypto; +import meerkat.protobuf.Mixing; + +/** + * Created by Tzlil on 1/18/2016. + */ +public interface MixerOutput { + public Mixing.ZeroKnowledgeProof[][] getProofs(); + public Crypto.RerandomizableEncryptedMessage[][] getEncryptedMessages(); + public int getN(); +} diff --git a/meerkat-common/src/main/proto/meerkat/BulletinBoardAPI.proto b/meerkat-common/src/main/proto/meerkat/BulletinBoardAPI.proto index fd6ea3c..f5f4bf5 100644 --- a/meerkat-common/src/main/proto/meerkat/BulletinBoardAPI.proto +++ b/meerkat-common/src/main/proto/meerkat/BulletinBoardAPI.proto @@ -7,6 +7,14 @@ option java_package = "meerkat.protobuf"; import 'meerkat/crypto.proto'; import 'google/protobuf/timestamp.proto'; +message BoolMsg { + bool value = 1; +} + +message IntMsg { + int32 value = 1; +} + message MessageID { // The ID of a message for unique retrieval. // Note that it is assumed that this ID is a function of the message itself. diff --git a/meerkat-common/src/main/proto/meerkat/PollingStation.proto b/meerkat-common/src/main/proto/meerkat/PollingStation.proto index 0cdc658..09596a5 100644 --- a/meerkat-common/src/main/proto/meerkat/PollingStation.proto +++ b/meerkat-common/src/main/proto/meerkat/PollingStation.proto @@ -2,11 +2,16 @@ syntax = "proto3"; package meerkat; +import "meerkat/voting.proto"; + option java_package = "meerkat.protobuf"; // Container for scanned data message ScannedData { - bytes data = 1; + bytes channel = 1; + + SignedEncryptedBallot signed_encrypted_ballot = 2; + } // Container for error messages diff --git a/meerkat-common/src/main/proto/meerkat/mixing.proto b/meerkat-common/src/main/proto/meerkat/mixing.proto index 50cffc7..83c87d6 100644 --- a/meerkat-common/src/main/proto/meerkat/mixing.proto +++ b/meerkat-common/src/main/proto/meerkat/mixing.proto @@ -6,7 +6,57 @@ option java_package = "meerkat.protobuf"; import 'meerkat/crypto.proto'; -// TODO: -message ZeroKnowledgeProof { +message Plaintext{ bytes data = 1; -} \ No newline at end of file +} + +message ZeroKnowledgeProof { + message OrProof { + message ForRandomOracle{ + bytes g1 = 1; + bytes h1 = 2; + bytes g2 = 3; + bytes h2 = 4; + bytes g1Tag = 5; + bytes h1Tag = 6; + bytes g2Tag = 7; + bytes h2Tag = 8; + bytes u = 9; + bytes v = 10; + bytes uTag = 11; + bytes vTag = 12; + } + //input : g1,h1, g2, h2, g1Tag, h1Tag, g2Tag, h2Tag; + bytes g1 = 1; + bytes h1 = 2; + bytes g2 = 3; + bytes h2 = 4; + bytes g1Tag = 5; + bytes h1Tag = 6; + bytes g2Tag = 7; + bytes h2Tag = 8; + + //calc: u, v, uTag, vTag; + bytes u = 9; + bytes v = 10; + bytes uTag = 11; + bytes vTag = 12; + + //generated: c1,c2,z,zTag + bytes c1 = 13; + bytes c2 = 14; + bytes z = 15; + bytes zTag = 16; + } + message Location{ + int32 i = 1; + int32 j = 2; + int32 layer = 3; + } + + OrProof first = 1; + OrProof second = 2; + OrProof third = 3; + OrProof fourth = 4; + Location location = 5; +} diff --git a/meerkat-common/src/main/proto/meerkat/voting.proto b/meerkat-common/src/main/proto/meerkat/voting.proto index 9837cce..6e03a5b 100644 --- a/meerkat-common/src/main/proto/meerkat/voting.proto +++ b/meerkat-common/src/main/proto/meerkat/voting.proto @@ -6,11 +6,49 @@ import 'meerkat/crypto.proto'; option java_package = "meerkat.protobuf"; -// A ballot question. This is an opaque -// data type that is parsed by the UI to display -// the question. + + +// Type of the element data to be presented by UI +enum UIElementDataType { + TEXT = 0; + IMAGE = 1; + VOICE = 2; +} + +// Type of question +enum QuestionType { + MULTIPLE_CHOICE = 0; + MULTIPLE_SELECTION = 1; + ORDER = 2; +} + +// An element to be presented by UI +message UIElement { + UIElementDataType type = 1; + bytes data = 2; +} + +// A question in the ballot +// is_mandatory determines whether the question may be skipped with no answer +// description might hold information/guidlines for the voter message BallotQuestion { - bytes data = 1; + bool is_mandatory = 1; + UIElement question = 2; + UIElement description = 3; + repeated UIElement answer = 4; +} + + +message QuestionCluster { + UIElement cluster_description = 1; + repeated int32 question_index = 2; + +} + +message Channel { + UIElement channel_description = 1; + repeated int32 cluster_index = 2; + } // An answer to a specific ballot question. @@ -22,17 +60,22 @@ message BallotAnswer { } message PlaintextBallot { - uint64 serialNumber = 1; // Ballot serial number - - repeated BallotAnswer answers = 2; + uint64 serial_number = 1; // Ballot serial number + bytes channel_identifier = 2; + repeated BallotAnswer answers = 3; } message EncryptedBallot { - uint64 serialNumber = 1; // Ballot serial number + uint64 serial_number = 1; // Ballot serial number RerandomizableEncryptedMessage data = 2; } +message SignedEncryptedBallot { + EncryptedBallot encrypted_ballot = 1; + Signature signature = 2; +} + message BallotSecrets { PlaintextBallot plaintext_ballot = 1; @@ -45,6 +88,10 @@ message BoothParams { } +message BoothSystemMessages { + map system_message = 1; +} + // A table to translate to and from compactly encoded answers // and their human-understandable counterparts. // This should be parsable by the UI @@ -79,12 +126,29 @@ message ElectionParams { // How many mixers must participate for the mixing to be considered valid uint32 mixerThreshold = 5; - // Candidate list (or other question format) - repeated BallotQuestion questions = 6; + // questions to first indicate the voter's channel + repeated BallotQuestion channel_choice_questions = 6; - // Translation table between answers and plaintext encoding - BallotAnswerTranslationTable answerTranslationTable = 7; + // translating the channel-choice answers to the voter's channel + SimpleCategoriesSelectionData selection_data = 7; + + // Candidate list (or other question format) + repeated BallotQuestion race_questions = 8; // Data required in order to access the Bulletin Board Servers - BulletinBoardClientParams bulletinBoardClientParams = 8; + BulletinBoardClientParams bulletinBoardClientParams = 9; } + +message Category { + repeated uint32 questionIndex = 1; +} + +message CategoryChooser { + repeated Category category = 1; +} + +message SimpleCategoriesSelectionData { + Category shared_defaults = 1; + repeated CategoryChooser categoryChooser = 2; +} + diff --git a/meerkat-common/src/test/java/meerkat/crypto/concrete/ECElGamalUtils.java b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECElGamalUtils.java index e57c817..e26e00b 100644 --- a/meerkat-common/src/test/java/meerkat/crypto/concrete/ECElGamalUtils.java +++ b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECElGamalUtils.java @@ -52,7 +52,7 @@ public class ECElGamalUtils { .setSubjectPublicKeyInfo(ByteString.copyFrom(javaPk.getEncoded())).build(); return serializedPk; - } catch (NoSuchAlgorithmException|InvalidKeySpecException e) { + } catch (Exception e) { logger.error("Should never happen!", e); throw new RuntimeException("Error converting public key!", e); } @@ -80,7 +80,7 @@ public class ECElGamalUtils { try { java.lang.reflect.Method newBuilder = plaintextMessageType.getMethod("newBuilder"); - GeneratedMessage.Builder builder = (GeneratedMessage.Builder) newBuilder.invoke(plaintextMessageType); + Message.Builder builder = (Message.Builder) newBuilder.invoke(plaintextMessageType); builder.mergeDelimitedFrom(in); return plaintextMessageType.cast(builder.build()); } catch (Exception e) { diff --git a/mixer/build.gradle b/mixer/build.gradle new file mode 100644 index 0000000..ba26598 --- /dev/null +++ b/mixer/build.gradle @@ -0,0 +1,208 @@ + +plugins { + id "us.kirchmeier.capsule" version "1.0.1" + id 'com.google.protobuf' version '0.7.0' +} + +apply plugin: 'java' +apply plugin: 'eclipse' +apply plugin: 'idea' + +apply plugin: 'maven-publish' + +// Uncomment the lines below to define an application +// (this will also allow you to build a "fatCapsule" which includes +// the entire application, including all dependencies in a single jar) +//apply plugin: 'application' +//mainClassName='your.main.ApplicationClass' + +// Is this a snapshot version? +ext { isSnapshot = false } + +ext { + groupId = 'org.factcenter.meerkat' + nexusRepository = "https://cs.idc.ac.il/nexus/content/groups/${isSnapshot ? 'unstable' : 'public'}/" + + // Credentials for IDC nexus repositories (needed only for using unstable repositories and publishing) + // Should be set in ${HOME}/.gradle/gradle.properties + nexusUser = project.hasProperty('nexusUser') ? project.property('nexusUser') : "" + nexusPassword = project.hasProperty('nexusPassword') ? project.property('nexusPassword') : "" +} + +description = "TODO: Add a description" + +// Your project version +version = "0.0" + +version += "${isSnapshot ? '-SNAPSHOT' : ''}" + + +dependencies { + // Meerkat common + compile project(':meerkat-common') + + // Logging + compile 'org.slf4j:slf4j-api:1.7.7' + runtime 'ch.qos.logback:logback-classic:1.1.2' + runtime 'ch.qos.logback:logback-core:1.1.2' + + // Google protobufs + compile 'com.google.protobuf:protobuf-java:3.+' + + // Crypto + compile 'org.factcenter.qilin:qilin:1.2.+' + + testCompile 'junit:junit:4.+' + + runtime 'org.codehaus.groovy:groovy:2.4.+' +} + + +/*==== You probably don't have to edit below this line =======*/ + +// The run task added by the application plugin +// is also of type JavaExec. +tasks.withType(JavaExec) { + // Assign all Java system properties from + // the command line to the JavaExec task. + systemProperties System.properties +} + + +protobuf { + // Configure the protoc executable + protoc { + // Download from repositories + artifact = 'com.google.protobuf:protoc:3.+' + } +} + + +idea { + module { + project.sourceSets.each { sourceSet -> + + def srcDir = "${protobuf.generatedFilesBaseDir}/$sourceSet.name/java" + + println "Adding $srcDir" + // add protobuf generated sources to generated source dir. + if ("test".equals(sourceSet.name)) { + testSourceDirs += file(srcDir) + } else { + sourceDirs += file(srcDir) + } + generatedSourceDirs += file(srcDir) + + } + + // Don't exclude build directory + excludeDirs -= file(buildDir) + } +} + + +/*=================================== + * "Fat" Build targets + *===================================*/ + + +if (project.hasProperty('mainClassName') && (mainClassName != null)) { + + task mavenCapsule(type: MavenCapsule) { + description = "Generate a capsule jar that automatically downloads and caches dependencies when run." + applicationClass mainClassName + destinationDir = buildDir + } + + task fatCapsule(type: FatCapsule) { + description = "Generate a single capsule jar containing everything. Use -Pfatmain=... to override main class" + + destinationDir = buildDir + + def fatMain = hasProperty('fatmain') ? fatmain : mainClassName + + applicationClass fatMain + + def testJar = hasProperty('test') + + if (hasProperty('fatmain')) { + appendix = "fat-${fatMain}" + } else { + appendix = "fat" + } + + if (testJar) { + from sourceSets.test.output + } + } +} + + +/*=================================== + * Repositories + *===================================*/ + +repositories { + + // Prefer the local nexus repository (it may have 3rd party artifacts not found in mavenCentral) + maven { + url nexusRepository + + if (isSnapshot) { + credentials { username + password + + username nexusUser + password nexusPassword + } + } + } + + // Use local maven repository + mavenLocal() + + // Use 'maven central' for other dependencies. + mavenCentral() +} + +task "info" << { + println "Project: ${project.name}" +println "Description: ${project.description}" + println "--------------------------" + println "GroupId: $groupId" + println "Version: $version (${isSnapshot ? 'snapshot' : 'release'})" + println "" +} +info.description 'Print some information about project parameters' + + +/*=================================== + * Publishing + *===================================*/ + +publishing { + publications { + mavenJava(MavenPublication) { + groupId project.groupId + pom.withXml { + asNode().appendNode('description', project.description) + } + from project.components.java + + } + } + repositories { + maven { + url "https://cs.idc.ac.il/nexus/content/repositories/${project.isSnapshot ? 'snapshots' : 'releases'}" + credentials { username + password + + username nexusUser + password nexusPassword + } + } + } +} + + + diff --git a/mixer/src/main/java/meerkat/mixer/main/BatchConverter.java b/mixer/src/main/java/meerkat/mixer/main/BatchConverter.java new file mode 100644 index 0000000..1ad8204 --- /dev/null +++ b/mixer/src/main/java/meerkat/mixer/main/BatchConverter.java @@ -0,0 +1,118 @@ +package meerkat.mixer.main; + +import com.google.protobuf.ByteString; +import meerkat.crypto.mixnet.MixerOutput; +import meerkat.protobuf.BulletinBoardAPI; +import meerkat.protobuf.Crypto; +import meerkat.protobuf.Mixing; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Tzlil on 12/17/2015. + * provide convert operation from batch data to meerkat.mixer.mixing output and backwards + */ +public class BatchConverter { + + private final int n,layers; + + /** + * constructor + * @param n + * @param layers + */ + public BatchConverter(int n,int layers){ + this.n = n; + this.layers = layers; + } + + /** + * convert integer to byte string + * @param a + * @return a as byte string + */ + private ByteString Integer2ByteString(int a){ + return ByteString.copyFrom(BigInteger.valueOf(a).toByteArray()); + } + + /** + * convert byte string to integer + * @param bs + * @return bs as int + */ + private int ByteString2Integer(ByteString bs) { + return Integer.valueOf(bs.toString()); + } + + /** + * convert meerkat.mixer.mixing output to batch data + * @param mixerOutput + * @return meerkat.mixer.mixing output as list of batch data + */ + public List MixerOutput2BatchChunk(MixerOutput mixerOutput) { + + List result = new ArrayList(); + + result.add(BulletinBoardAPI.BatchChunk.newBuilder() + .setData(Integer2ByteString(n)) + .build()); + + for (Mixing.ZeroKnowledgeProof[] zkpLayer : mixerOutput.getProofs()) { + for (Mixing.ZeroKnowledgeProof zkp : zkpLayer) { + result.add(BulletinBoardAPI.BatchChunk.newBuilder() + .setData(zkp.toByteString()) + .build()); + } + } + for (Crypto.RerandomizableEncryptedMessage[] encryptionLayer : mixerOutput.getEncryptedMessages()) { + for (Crypto.RerandomizableEncryptedMessage encryption : encryptionLayer) { + result.add(BulletinBoardAPI.BatchChunk.newBuilder() + .setData(encryption.toByteString()) + .build()); + } + } + return result; + } + + /** + * convert batch data list to meerkat.mixer.mixing output + * @param batchChunkList + * @return batch data list as MixerOutput + * @throws Exception + */ + public MixerOutput BatchChunkList2MixerOutput + (List batchChunkList) throws Exception { + + if (n != ByteString2Integer(batchChunkList.remove(0).getData())){ + throw new Exception(); + } + + int nDiv2 = n >>1; + Mixing.ZeroKnowledgeProof[][] proofs = new Mixing.ZeroKnowledgeProof[layers][nDiv2]; + for (int layer = 0; layer < layers; layer++) + { + for (int proofIndex = 0 ; proofIndex < nDiv2 ; proofIndex ++) + { + proofs[layer][proofIndex] = Mixing.ZeroKnowledgeProof.parseFrom(batchChunkList.remove(0).getData()); + } + } + + Crypto.RerandomizableEncryptedMessage[][] encryptions + = new Crypto.RerandomizableEncryptedMessage[layers + 1][n]; + for (int layer = 0; layer < layers + 1; layer++) + { + for (int encryptionIndex = 0 ; encryptionIndex < n ; encryptionIndex ++) + { + encryptions[layer][encryptionIndex] = Crypto.RerandomizableEncryptedMessage + .parseFrom(batchChunkList.remove(0).getData()); + } + } + + return new meerkat.mixer.mixing.MixerOutput(n,layers,proofs,encryptions); + + } + +} + diff --git a/mixer/src/main/java/meerkat/mixer/main/BatchHandler.java b/mixer/src/main/java/meerkat/mixer/main/BatchHandler.java new file mode 100644 index 0000000..8356475 --- /dev/null +++ b/mixer/src/main/java/meerkat/mixer/main/BatchHandler.java @@ -0,0 +1,104 @@ +package meerkat.mixer.main; + +import meerkat.crypto.mixnet.Mix2ZeroKnowledgeVerifier; +import meerkat.crypto.mixnet.MixerOutput; +import meerkat.protobuf.Crypto; +import meerkat.mixer.necessary.AsyncBulletinBoardClient; +import meerkat.mixer.necessary.CompleteBatch; +import meerkat.mixer.verifier.VerifyTable; + +import java.util.Arrays; +import java.util.List; + +/** + * Created by Tzlil on 12/17/2015. + * implements AsyncBulletinBoardClient.ClientCallback + */ +public class BatchHandler implements AsyncBulletinBoardClient.ClientCallback { + + private MixerOutput mixerOutput; + private boolean msgReceived; + private Throwable t; + private CompleteBatch msg; + + private final int n, layers; + private final Mix2ZeroKnowledgeVerifier verifier; + + /** + * constructor + * @param n + * @param layers + * @param verifier + */ + public BatchHandler(int n, int layers, Mix2ZeroKnowledgeVerifier verifier) { + this.mixerOutput = null; + this.n = n; + this.layers = layers; + this.msgReceived = false; + this.t = null; + this.verifier = verifier; + } + + @Override + public void handleCallback(CompleteBatch msg) { + this.msg = msg; + synchronized (this) { + this.msgReceived = true; + notifyAll(); + } + } + + @Override + public void handleFailure(Throwable t) { + this.t = t; + synchronized (this) { + this.msgReceived = true; + notifyAll(); + } + } + + /** + * return true iff msg was received + * @return msgReceived + */ + public boolean isMsgReceived() { + return msgReceived; + } + + /** + * convert batch data to meerkat.mixer.mixing output + * @throws Exception + */ + private void convertMessage() throws Exception { + BatchConverter batchConverter = new BatchConverter(n,layers); + this.mixerOutput = batchConverter.BatchChunkList2MixerOutput(msg.getBatchChunkList()); + } + + /** + * call convert message, and if succeed verify the table + * @return return true iff the given batch message is valid meerkat.mixer.mixing output + * @throws Exception + */ + public boolean verifyTable() throws Exception { + if (mixerOutput == null) { + convertMessage(); + } + return VerifyTable.verifyTable(verifier, n, mixerOutput); + } + + /** + * extract input for meerkat.mixer.mixing from previous mixers output + * @return last layer of encrypted votes as list + * @throws Throwable in case if failure + */ + public List getInputForMixer() throws Throwable { + if (t != null) { + throw t; + } + if(!verifyTable()){ + throw new Exception("in valid table"); + } + return Arrays.asList(mixerOutput.getEncryptedMessages()[layers]);//there are layers + 1 + } +} + diff --git a/mixer/src/main/java/meerkat/mixer/main/MainMixing.java b/mixer/src/main/java/meerkat/mixer/main/MainMixing.java new file mode 100644 index 0000000..148352b --- /dev/null +++ b/mixer/src/main/java/meerkat/mixer/main/MainMixing.java @@ -0,0 +1,107 @@ +package meerkat.mixer.main; + +import meerkat.crypto.mixnet.Mix2ZeroKnowledgeVerifier; +import meerkat.crypto.mixnet.Mixer; +import meerkat.crypto.mixnet.MixerOutput; +import meerkat.protobuf.BulletinBoardAPI; +import meerkat.protobuf.Crypto; +import meerkat.mixer.mixing.MixNetwork; +import meerkat.mixer.necessary.AsyncBulletinBoardClient; + + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + + +/** + * Created by Tzlil on 12/17/2015. + * this class define all the operation meerkat.mixer.mixing party should do: + * 1. receive previous mixers output (re encrypted votes + proofs) + * 2. verify its input + * 3. mix + * 4. send the meerkat.mixer.mixing output + */ +public class MainMixing { + + private final Mixer mixer; + private final Mix2ZeroKnowledgeVerifier verifier; + private final int n, layers; + private final AsyncBulletinBoardClient asyncBulletinBoardClient; + private final byte[] id; + + + /** + * constructor + * @param mixer + * @param verifier + * @param n + * @param asyncBulletinBoardClient + * @param id + */ + public MainMixing(Mixer mixer, Mix2ZeroKnowledgeVerifier verifier, int n + , AsyncBulletinBoardClient asyncBulletinBoardClient, byte[] id) { + this.mixer = mixer; + this.verifier = verifier; + this.n = n; + this.layers = MixNetwork.numberOfLayers(n); + this.asyncBulletinBoardClient = asyncBulletinBoardClient; + this.id = id; + } + + /** + * + * @param prevBatchIds + * @param batchId + * @param random + * @param callback + * @throws Throwable + */ + public void main(List prevBatchIds, int batchId, Random random, AsyncBulletinBoardClient.ClientCallback callback) throws Throwable { + + List mixerInput; + + List batchHandlers = new ArrayList(prevBatchIds.size()); + BatchHandler currentBatchHandler; + for (Integer prevBatchId : prevBatchIds) { + currentBatchHandler = new BatchHandler(n, layers,verifier); + asyncBulletinBoardClient.readBatch(id, prevBatchId,currentBatchHandler); + batchHandlers.add(currentBatchHandler); + } + // check all handlers messages were received + for (BatchHandler batchHandler : batchHandlers) { + synchronized (batchHandler){ + if(!batchHandler.isMsgReceived()) + batchHandler.wait(); + } + } + // assert all handlers succeeded + for (BatchHandler batchHandler : batchHandlers) { + if(!batchHandler.verifyTable()){ + throw new Exception("invalid input"); + } + } + + BatchHandler lastBatchHandler = batchHandlers.get(batchHandlers.size() - 1); + mixerInput = lastBatchHandler.getInputForMixer(); + + MixerOutput mixerOutput = mixer.mix(mixerInput,random); + updateBB(mixerOutput, batchId, callback); + + } + + /** + * send meerkat.mixer.mixing output to BB + * @param mixerOutput + * @param batchId + * @param callback + */ + private void updateBB(MixerOutput mixerOutput + , int batchId, AsyncBulletinBoardClient.ClientCallback callback) { + + BatchConverter batchConverter = new BatchConverter(n,layers); + List batchChunkList = batchConverter.MixerOutput2BatchChunk(mixerOutput); + asyncBulletinBoardClient.postBatch(id, batchId, batchChunkList, callback); + } + +} diff --git a/mixer/src/main/java/meerkat/mixer/mixing/MixNetwork.java b/mixer/src/main/java/meerkat/mixer/mixing/MixNetwork.java new file mode 100644 index 0000000..e820ebd --- /dev/null +++ b/mixer/src/main/java/meerkat/mixer/mixing/MixNetwork.java @@ -0,0 +1,214 @@ +package meerkat.mixer.mixing; + +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ArrayBlockingQueue; + +/** + * Created by Tzlil on 12/15/2015. + * contains benes mix network. + * the network is generated in the constructor and can't be change + */ +public class MixNetwork { + + private final Switch[][] switches; + + /** + * constructor + * @param randomPermutation + */ + public MixNetwork(RandomPermutation randomPermutation) { + this.switches = generateSwitchesValue(randomPermutation.permutation); + } + + /** + * implements benes mix network algorithm + * @param permutation - random permutation + * @return switches + */ + private Switch[][] generateSwitchesValue(int[] permutation){ + int n = permutation.length; + assert ((n & n-1) == 0); //n == 2^k + int layers = numberOfLayers(n); + + int[] pi, piL, piR; + Queue permutationsQueue = new ArrayBlockingQueue(n); + Graph graph; + int iDiv2; + int nDiv2 = n >> 1; + Switch[][] switches = new Switch[layers][nDiv2]; + int index1,index2; + + permutationsQueue.add(permutation); + for (int i = n, layer = 0; i > 1; i >>= 1, layer++) // i == permutation size + { + iDiv2 = i >> 1; + for (int j = 0; j < nDiv2; j += iDiv2) // j == permutation start index + { + pi = permutationsQueue.remove(); + graph = new Graph(pi); + piL = new int[iDiv2]; + piR = new int[iDiv2]; + for (int k = 0; k < iDiv2; k++){ // k == switch index in permutation j + + index1 = k + (j << 1); + index2 = index1 + iDiv2; + switches[layers - layer - 1][k + j] = new Switch(index1,index2,layers - layer - 1,graph.getSwitchValue(k, true)); + switches[layer][k + j] = new Switch(index1,index2,layer,graph.getSwitchValue(k, false)); + + if (!switches[layers - layer - 1][k + j].value) { + piL[k] = pi[k] % iDiv2; + piR[k] = pi[k + iDiv2] % iDiv2; + } else { + piL[k] = pi[k + iDiv2] % iDiv2; + piR[k] = pi[k] % iDiv2; + } + } + permutationsQueue.add(piL); + permutationsQueue.add(piR); + } + } + return switches; + } + + /** + * getter for switches value at layer + * @param layer + * @return switches[layer] + */ + public Switch[] getSwitchesByLayer(int layer) + { + return switches[layer]; + } + + /** + * calc number of layers for n values + * @param n number of votes + * @return layers + */ + public static int numberOfLayers(int n){ + return (int) (2 * Math.log(n) / Math.log(2)) - 1; + } + + /** + * inner class + * graph object, part of benes mix network algorithm + */ + private class Graph { + private int n; + private int nDiv2; + private Node[][] nodes; + protected Graph(int[] permutation){ + n = permutation.length; // n = 2^k + nDiv2 = n >> 1; + createNodes(); + createEdges(permutation); + setSwitches(); + } + + /** + * provide an access to algorithm result + * index must be less then n/2 + */ + protected boolean getSwitchValue(int index,boolean up) { + return up ? nodes[0][index].value : nodes[1][index].value; + } + + + + + + + /** + * create two lines of nodes size n/2 each + * the value of the i th node is (i,i+n/2) if i < n /2 (first line) + * otherwise its value is (i - n/2 , i) (second line) + */ + private void createNodes() { + nodes = new Node[2][nDiv2]; + for (int i = 0; i < nDiv2; i++) { + nodes[0][i] = new Node(); + nodes[1][i] = new Node(); + } + } + + /** create an edge between each pair of nodes i,j from different lines (i index of the first line) + * if exists k in i th node's value and t in j th node's value + * s.t permutation[k] == t + * the edge is broken if (k < n/2 and t >= n/2) or (k >= n/2 and t < n/2) + * Note: in purpose to avoid edge cases, each node has exactly two edges + */ + private void createEdges(int[] permutation) { + int j; + for (int i = 0; i < nDiv2; i++) { + j = permutation[i] % nDiv2; + nodes[0][i].edges.add(new Edge(nodes[1][j], (permutation[i] >= nDiv2))); + nodes[1][j].edges.add(new Edge(nodes[0][i], (permutation[i] >= nDiv2))); + + j = permutation[i + nDiv2] % nDiv2; + nodes[0][i].edges.add(new Edge(nodes[1][j], (permutation[i + nDiv2] < nDiv2))); + nodes[1][j].edges.add(new Edge(nodes[0][i], (permutation[i + nDiv2] < nDiv2))); + } + } + + /** + * set switch's value (on/off) for each switch (node) + * s.t if nodes i,j connected by edge e, i th switch's value + * must be equal to j's if e is broken or not equal if e is not broken + */ + private void setSwitches() { + Node node; + boolean v; + Edge e0,e1; + // iterate over first line of nodes + for (int i = 0; i < nDiv2; i++) { + node = nodes[0][i]; + if (node.set) + continue; + //select default value for first node in connected component + v = false; + // set value to all reachable nodes from node + while (true) { + node.set = true; + node.value = v; + e0 = node.edges.get(0); e1 = node.edges.get(1); + if (e0.neighbor.set && e1.neighbor.set) + break; + v ^= (!e0.neighbor.set) ? e0.broken : e1.broken; + node = (!e0.neighbor.set) ? e0.neighbor : e1.neighbor; + } + } + } + + /** + * inner class + * node object in graph + * there are exactly twp edges for each node + */ + private class Node { + public List edges; + private boolean value; + private boolean set; + public Node() { + edges = new ArrayList(2); + set = false; + } + } + + /** + * inner class + * edge object in graph + */ + private class Edge { + public Node neighbor; + public boolean broken; + public Edge(Node neighbor, boolean broken) { + this.neighbor = neighbor; + this.broken = broken; + } + } + } + + +} diff --git a/mixer/src/main/java/meerkat/mixer/mixing/Mixer.java b/mixer/src/main/java/meerkat/mixer/mixing/Mixer.java new file mode 100644 index 0000000..fbe091c --- /dev/null +++ b/mixer/src/main/java/meerkat/mixer/mixing/Mixer.java @@ -0,0 +1,193 @@ +package meerkat.mixer.mixing; + +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.crypto.Encryption; +import meerkat.crypto.mixnet.Mix2ZeroKnowledgeProver; +import meerkat.crypto.mixnet.MixerOutput; +import meerkat.protobuf.Crypto.EncryptionRandomness; +import meerkat.protobuf.Crypto.RerandomizableEncryptedMessage; +import meerkat.protobuf.Mixing.ZeroKnowledgeProof; + +import java.util.List; +import java.util.Random; + +/** + * an implementation of meerkat.crypto.mixnet.Mixer + * meerkat.mixer.mixing algorithm on set of n encrypted votes: + * 0. asset n is power of two + * 1. set switches according to benes network on random permutation + * 2. re encrypt and mix with respect to switches values (encryptor.rerandomize) + * 3. generate zero knowledge proof on each re encrypted couple + * 4. return proofs table + encryption table + * + */ +public class Mixer implements meerkat.crypto.mixnet.Mixer { + + private final Mix2ZeroKnowledgeProver prover; + private final Encryption encryptor; + + /** + * constructor + * @param prover + * @param encryptor + */ + public Mixer(Mix2ZeroKnowledgeProver prover, Encryption encryptor) { + this.prover = prover; + this.encryptor = encryptor; + } + + /** + * return True iff n == 2 ^ k + * @param n + * @return + */ + public static boolean isPowerOfTwo(int n){ + return (n & (n - 1)) == 0; + } + + /** + * initialize encryption table of size (layers + 1)* n + * @param n number of votes + * @param layers + * @param ciphertexts encrypted votes + * @return an initialized encryption table s.t first layer == given encrypted votes + */ + private RerandomizableEncryptedMessage[][] initializeEncryptionTable(int n,int layers,List ciphertexts){ + // set first level of encryption + RerandomizableEncryptedMessage[][] encryptionTable = new RerandomizableEncryptedMessage[layers + 1][n]; + for (int j = 0; j < n; j++) { + encryptionTable[0][j] = ciphertexts.get(j); + } + return encryptionTable; + } + + /** + * generate randomness for all rerandomize operations + * @param n number of votes + * @param layers + * @return an initialized randomness table of size layers * n + */ + private EncryptionRandomness[][] generateRandomnessesForRerandomize(int n,int layers,Random random){ + EncryptionRandomness[][] randomnesses = new EncryptionRandomness[layers][n]; + for (int layer = 0; layer < layers; layer++) + { + for (int i = 0; i < n; i++) { + randomnesses[layer][i] = encryptor.generateRandomness(random);; + } + } + return randomnesses; + } + + /** + * generate new random mix network + * @param n number of votes + * @param random + * @return new random mix network + */ + private MixNetwork generateMixNetwork(int n,Random random){ + return new MixNetwork(new RandomPermutation(n,random)); + } + + /** + * fills the encryption table with rerandomize encrypted votes. + * @param layers + * @param mixNetwork switches table (boolean values) + * @param encryptionTable an initialized encryption table s.t first layer == given encrypted votes + * @param randomnesses an initialized randomness table of size layers * n, use for rerandomize operations + * @throws InvalidProtocolBufferException + */ + private void rerandomize(int layers,MixNetwork mixNetwork, RerandomizableEncryptedMessage[][] encryptionTable + ,EncryptionRandomness[][] randomnesses) throws InvalidProtocolBufferException { + Switch[] switchesLayer; + int index1,index2; + RerandomizableEncryptedMessage e1,e2; + EncryptionRandomness r1,r2; + for (int layer = 0; layer < layers; layer++) + { + switchesLayer = mixNetwork.getSwitchesByLayer(layer); + for (Switch sw : switchesLayer) { + index1 = sw.i; + index2 = sw.j; + e1 = encryptionTable[layer][index1]; + e2 = encryptionTable[layer][index2]; + + r1 = randomnesses[layer][index1]; + r2 = randomnesses[layer][index2]; + if (!sw.value) { + encryptionTable[layer + 1][index1] = encryptor.rerandomize(e1, r1); + encryptionTable[layer + 1][index2] = encryptor.rerandomize(e2, r2); + + } else { + encryptionTable[layer + 1][index1] = encryptor.rerandomize(e2, r2); + encryptionTable[layer + 1][index2] = encryptor.rerandomize(e1, r1); + } + } + } + } + + /** + * generate zero knowledge proof for each rerandomize encrypted votes couple in encryptionTable + * @param n number of votes + * @param layers + * @param mixNetwork switches table (boolean values) used for set encryption table + * @param encryptionTable full encryption table + * @param randomnesses randomness table of size layers * n, used for set encryption table + * @return zero knowledge proofs table + * @throws InvalidProtocolBufferException + */ + private ZeroKnowledgeProof[][] generateZeroKnowledgeProofTable(int n, int layers, MixNetwork mixNetwork + , RerandomizableEncryptedMessage[][] encryptionTable + , EncryptionRandomness[][] randomnesses) throws InvalidProtocolBufferException { + Switch[] switchesLayer; + int index1,index2; + int switchIndex = 0; + int nDiv2 = n >> 1; + ZeroKnowledgeProof[][] proofsTable = new ZeroKnowledgeProof[layers][nDiv2]; + + RerandomizableEncryptedMessage e1,e2; + EncryptionRandomness r1,r2; + for (int layer = 0; layer < layers; layer++) + { + switchesLayer = mixNetwork.getSwitchesByLayer(layer); + for (Switch sw : switchesLayer) { + index1 = sw.i; + index2 = sw.j; + e1 = encryptionTable[layer][index1]; + e2 = encryptionTable[layer][index2]; + r1 = randomnesses[layer][index1]; + r2 = randomnesses[layer][index2]; + + proofsTable[layer][switchIndex] = + prover.prove(e1, e2, encryptionTable[layer + 1][index1], + encryptionTable[layer + 1][index2], + sw.value, sw.i, sw.j, sw.layer, r1, r2); + + switchIndex = (switchIndex + 1) % nDiv2; + } + } + return proofsTable; + } + + /** + * mix given encrypted votes using random + * @param ciphertexts encrypted votes + * @param random + * @return meerkat.mixer.mixing result + * @throws InvalidProtocolBufferException + */ + public MixerOutput mix(List ciphertexts,Random random) throws InvalidProtocolBufferException { + + int n = ciphertexts.size(); + assert (n > 1 && isPowerOfTwo(n)); + + int layers = MixNetwork.numberOfLayers(n); // layers = 2logn -1 + RerandomizableEncryptedMessage[][] encryptionTable = initializeEncryptionTable(n,layers,ciphertexts); + EncryptionRandomness[][] randomnesses = generateRandomnessesForRerandomize(n,layers,random); + MixNetwork mixNetwork = generateMixNetwork(n,random); + rerandomize(layers,mixNetwork,encryptionTable,randomnesses); + ZeroKnowledgeProof[][] proofsTable = generateZeroKnowledgeProofTable(n,layers,mixNetwork,encryptionTable,randomnesses); + + return new meerkat.mixer.mixing.MixerOutput(n,layers,proofsTable, encryptionTable); + } + +} \ No newline at end of file diff --git a/mixer/src/main/java/meerkat/mixer/mixing/MixerOutput.java b/mixer/src/main/java/meerkat/mixer/mixing/MixerOutput.java new file mode 100644 index 0000000..64636cc --- /dev/null +++ b/mixer/src/main/java/meerkat/mixer/mixing/MixerOutput.java @@ -0,0 +1,130 @@ +package meerkat.mixer.mixing; + +import meerkat.protobuf.Crypto; +import meerkat.protobuf.Mixing; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + + +/** + * Created by Tzlil on 1/18/2016. + * implements meerkat.crypto.mixnet.MixerOutput interface + * container for meerkat.mixer.mixing.mix result. + */ +public class MixerOutput implements meerkat.crypto.mixnet.MixerOutput{ + private final Mixing.ZeroKnowledgeProof[][] proofs; + private final Crypto.RerandomizableEncryptedMessage[][] encryptedMessages; + private final int n; + private final int layers; + + /** + * constructor + * @param n number of votes + * @param layers + * @param encryptedMessages at level 0 , contains the original encrypted votes + * at each other level contains the re encrypted votes + * @param proofs in each cell (level,switch) contains the match zero knowledge proof + */ + public MixerOutput(int n,int layers,Mixing.ZeroKnowledgeProof[][] proofs + , Crypto.RerandomizableEncryptedMessage[][] encryptedMessages) { + this.proofs = proofs; + this.encryptedMessages = encryptedMessages; + this.n = n; + this.layers = layers; + } + + + + @Override + public Mixing.ZeroKnowledgeProof[][] getProofs() { + return proofs; + } + + @Override + public Crypto.RerandomizableEncryptedMessage[][] getEncryptedMessages() { + return encryptedMessages; + } + + @Override + public int getN() { + return n; + } + + /** + * print the output, encrypted messages and proofs, to folder + * @param dir - directory + * @throws IOException + */ + public void outToFolder(String dir) throws IOException { + + (new File(dir)).mkdirs(); + //create files + String proofsDir = dir + "/Proofs"; + String encDir = dir + "/EncryptedMessages"; + (new File(proofsDir)).mkdir(); + (new File(encDir)).mkdir(); + for (int layer = 0; layer < layers; layer++){ + (new File(proofsDir +"/layer" + layer )).mkdir(); + (new File(encDir +"/layer" + layer )).mkdir(); + } + (new File(encDir +"/input")).mkdir(); + + + for (int layer = 0; layer < layers; layer++){ + for(int i = 0; i < proofs[layer].length; i ++){ + writeProofToFile(proofsDir,proofs[layer][i]); + } + } + + for (int layer = 0; layer <= layers; layer++){ + for(int i = 0; i < encryptedMessages[layer].length; i ++){ + writeEncToFile(encDir,layer - 1, i,encryptedMessages[layer][i]); + } + } + } + + /** + * create new file contains single proof + * @param proofsDir + * @param proof + * @throws IOException + */ + private void writeProofToFile(String proofsDir, Mixing.ZeroKnowledgeProof proof) throws IOException { + Mixing.ZeroKnowledgeProof.Location location = proof.getLocation(); + int layer = location.getLayer(); + int i = location.getI(); + int j = location.getJ(); + String fileName = proofsDir+"/layer" + layer +"/" + i +"_" + j; + + File file = new File(fileName); + file.createNewFile(); + FileOutputStream fos = new FileOutputStream(file); + fos.write(proof.toByteArray()); + fos.close(); + } + + /** + * create new file contains single encrypted message + * @param encDir + * @param layer + * @param i + * @param enc + * @throws IOException + */ + private void writeEncToFile(String encDir,int layer,int i, Crypto.RerandomizableEncryptedMessage enc) throws IOException { + + String fileName; + if(layer >= 0) + fileName = encDir+"/layer" + layer +"/" + i; + else + fileName = encDir+"/input/" + i; + + File file = new File(fileName); + file.createNewFile(); + FileOutputStream fos = new FileOutputStream(file); + fos.write(enc.toByteArray()); + fos.close(); + } +} diff --git a/mixer/src/main/java/meerkat/mixer/mixing/RandomPermutation.java b/mixer/src/main/java/meerkat/mixer/mixing/RandomPermutation.java new file mode 100644 index 0000000..7f18530 --- /dev/null +++ b/mixer/src/main/java/meerkat/mixer/mixing/RandomPermutation.java @@ -0,0 +1,45 @@ +package meerkat.mixer.mixing; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * Created by Tzlil on 12/17/2015. + * container for random permutation + * the permutation is sated in constructor and can't be change + */ +public class RandomPermutation { + public final int[] permutation; + + /** + * constructor + * @param n permutation size + * @param random + */ + public RandomPermutation(int n,Random random) { + this.permutation = generatePermutation(n,random); + } + + /** + * generate random permutation ovver [0,n) + * @param n permutation size + * @param random + * @return permutation + */ + private int[] generatePermutation(int n,Random random){ + List numbers= new ArrayList(n); + for (int i = 0; i < n; i++) { + numbers.add(i); + } + + int[] result = new int[n]; + int index; + for (int i = 0; i < n; i++) { + index = random.nextInt(n - i); + result[i] = numbers.get(index); + numbers.remove(index); + } + return result; + } +} diff --git a/mixer/src/main/java/meerkat/mixer/mixing/Switch.java b/mixer/src/main/java/meerkat/mixer/mixing/Switch.java new file mode 100644 index 0000000..5de522e --- /dev/null +++ b/mixer/src/main/java/meerkat/mixer/mixing/Switch.java @@ -0,0 +1,36 @@ +package meerkat.mixer.mixing; + +/** + * Created by Tzlil on 12/15/2015. + * container for switch + */ +public class Switch{ + + public final int i, j, layer; + public final boolean value; + + /** + * constructor + * @param i + * @param j + * @param layer + * @param value the switch is on or off + */ + public Switch(int i, int j, int layer, boolean value) { + this.i = i; + this.j = j; + this.layer = layer; + this.value = value; + } + + + @Override + public String toString() { + return "Switch{" + + "i=" + i + + ", j=" + j + + ", layer=" + layer + + ", value=" + value + + '}'; + } +} diff --git a/mixer/src/main/java/meerkat/mixer/necessary/AsyncBulletinBoardClient.java b/mixer/src/main/java/meerkat/mixer/necessary/AsyncBulletinBoardClient.java new file mode 100644 index 0000000..208c7c7 --- /dev/null +++ b/mixer/src/main/java/meerkat/mixer/necessary/AsyncBulletinBoardClient.java @@ -0,0 +1,80 @@ +package meerkat.mixer.necessary; + +import meerkat.protobuf.BulletinBoardAPI.*; + +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 14-Dec-15. + * + * ToDo: this should be replaced by AsyncBulletinBoardClient at BB project + */ +public interface AsyncBulletinBoardClient extends BulletinBoardClient { + + public interface ClientCallback { + void handleCallback(T msg); + void handleFailure(Throwable t); + } + + public interface MessageHandler { + void handleNewMessages(List messageList); + } + + /** + * Post a message to the bulletin board in an asynchronous manner + * @param msg is the message to be posted + * @param callback is a class containing methods to handle the result of the operation + * @return a unique message ID for the message, that can be later used to retrieve the batch + */ + public MessageID postMessage(BulletinBoardMessage msg, ClientCallback callback); + + /** + * This method allows for sending large messages as a batch to the bulletin board + * @param signerId is the canonical form for the ID of the sender of this batch + * @param batchId is a unique (per signer) ID for this batch + * @param batchChunkList is the (canonically ordered) list of data comprising the batch message + * @param startPosition is the location (in the batch) of the first entry in batchChunkList (optionally used to continue interrupted post operations) + * @param callback is a callback function class for handling results of the operation + * @return a unique message ID for the entire message, that can be later used to retrieve the batch + */ + public MessageID postBatch(byte[] signerId, int batchId, List batchChunkList, int startPosition, ClientCallback callback); + + /** + * Overloading of the postBatch method in which startPosition is set to the default value 0 + */ + public MessageID postBatch(byte[] signerId, int batchId, List batchChunkList, ClientCallback callback); + + /** + * Check how "safe" a given message is in an asynchronous manner + * The result of the computation is a rank between 0.0 and 1.0 indicating the fraction of servers containing the message + * @param id is the unique message identifier for retrieval + * @param callback is a callback function class for handling results of the operation + */ + public void getRedundancy(MessageID id, ClientCallback callback); + + /** + * Read all messages posted matching the given filter in an asynchronous manner + * Note that if messages haven't been "fully posted", this might return a different + * set of messages in different calls. However, messages that are fully posted + * are guaranteed to be included. + * @param filterList return only messages that match the filters (null means no filtering). + * @param callback is a callback function class for handling results of the operation + */ + public void readMessages(MessageFilterList filterList, ClientCallback> callback); + + /** + * Read a given batch message from the bulletin board + * @param signerId is the ID of the signer (sender) of the batch message + * @param batchId is the unique (per signer) ID of the batch + * @param callback is a callback class for handling the result of the operation + */ + public void readBatch(byte[] signerId, int batchId, ClientCallback callback); + + /** + * Subscribes to a notifier that will return any new messages on the server that match the given filters + * @param filterList defines the set of filters for message retrieval + * @param messageHandler defines the handler for new messages received + */ + public void subscribe(MessageFilterList filterList, MessageHandler messageHandler); + +} diff --git a/mixer/src/main/java/meerkat/mixer/necessary/BulletinBoardClient.java b/mixer/src/main/java/meerkat/mixer/necessary/BulletinBoardClient.java new file mode 100644 index 0000000..aebe469 --- /dev/null +++ b/mixer/src/main/java/meerkat/mixer/necessary/BulletinBoardClient.java @@ -0,0 +1,61 @@ +package meerkat.mixer.necessary; + +import meerkat.comm.CommunicationException; +import meerkat.protobuf.Voting.BulletinBoardClientParams; + +import java.util.List; + +import static meerkat.protobuf.BulletinBoardAPI.*; + +/** + * Created by talm on 24/10/15. + * + * ToDo: this should be replaced by BulletinBoardClient at BB project + */ +public interface BulletinBoardClient { + + interface ClientCallback { + void handleCallback(T msg); + void handleFailure(Throwable t); + } + + /** + * Initialize the client to use some specified servers + * @param clientParams contains the parameters required for the client setup + */ + void init(BulletinBoardClientParams clientParams); + + /** + * Post a message to the bulletin board in a synchronous manner + * @param msg is the message to be posted + * @return a unique message ID for the message, that can be later used to retrieve the batch + * @throws CommunicationException + */ + MessageID postMessage(BulletinBoardMessage msg) throws CommunicationException; + + + + /** + * Check how "safe" a given message is in a synchronous manner + * @param id is the unique message identifier for retrieval + * @return a normalized "redundancy score" from 0 (local only) to 1 (fully published) + */ + float getRedundancy(MessageID id); + + /** + * Read all messages posted matching the given filter in a synchronous manner + * Note that if messages haven't been "fully posted", this might return a different + * set of messages in different calls. However, messages that are fully posted + * are guaranteed to be included. + * @param filterList return only messages that match the filters (null means no filtering). + * @return the list of messages + */ + List readMessages(MessageFilterList filterList); + + /** + * Closes all connections, if any. + * This is msgRecived in a synchronous (blocking) way. + */ + void close(); + +} diff --git a/mixer/src/main/java/meerkat/mixer/necessary/CompleteBatch.java b/mixer/src/main/java/meerkat/mixer/necessary/CompleteBatch.java new file mode 100644 index 0000000..4e18d43 --- /dev/null +++ b/mixer/src/main/java/meerkat/mixer/necessary/CompleteBatch.java @@ -0,0 +1,70 @@ +package meerkat.mixer.necessary; + + +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.protobuf.Crypto.*; + +import java.util.LinkedList; +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 14-Dec-15. + * + * A data structure for holding a complete batch message along with its signature + * + * ToDo: this should be replaced by CompleteBatch at BB project + */ +public class CompleteBatch { + + private BeginBatchMessage beginBatchMessage; + private List batchChunkList; + private Signature signature; + + public CompleteBatch() { + batchChunkList = new LinkedList(); + } + + public CompleteBatch(BeginBatchMessage newBeginBatchMessage) { + this(); + beginBatchMessage = newBeginBatchMessage; + } + + public CompleteBatch(BeginBatchMessage newBeginBatchMessage, List newChunkList) { + this(newBeginBatchMessage); + appendBatchChunk(newChunkList); + } + + public CompleteBatch(BeginBatchMessage newBeginBatchMessage, List newChunkList, Signature newSignature) { + this(newBeginBatchMessage, newChunkList); + signature = newSignature; + } + + public BeginBatchMessage getBeginBatchMessage() { + return beginBatchMessage; + } + + public List getBatchChunkList() { + return batchChunkList; + } + + public Signature getSignature() { + return signature; + } + + public void setBeginBatchMessage(BeginBatchMessage beginBatchMessage) { + this.beginBatchMessage = beginBatchMessage; + } + + public void appendBatchChunk(BatchChunk newBatchChunk) { + batchChunkList.add(newBatchChunk); + } + + public void appendBatchChunk(List newBatchChunkList) { + batchChunkList.addAll(newBatchChunkList); + } + + public void setSignature(Signature newSignature) { + signature = newSignature; + } + +} diff --git a/mixer/src/main/java/meerkat/mixer/prover/ElGamalProofOrganizer.java b/mixer/src/main/java/meerkat/mixer/prover/ElGamalProofOrganizer.java new file mode 100644 index 0000000..153dc12 --- /dev/null +++ b/mixer/src/main/java/meerkat/mixer/prover/ElGamalProofOrganizer.java @@ -0,0 +1,303 @@ +package meerkat.mixer.prover; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.crypto.concrete.ECElGamalEncryption; +import meerkat.protobuf.ConcreteCrypto; +import meerkat.protobuf.Crypto; +import org.bouncycastle.math.ec.ECPoint; +import org.factcenter.qilin.primitives.concrete.ECGroup; + +/** + * use for organize the input for each ZKP + * + * both meerkat.mixer.prover and meerkat.mixer.verifier implantation are using it + */ +public class ElGamalProofOrganizer { + + private final ECGroup group; + private final ECPoint g; + private final ECPoint h; + private final byte[] gEncoded; + private final byte[] hEncoded; + + /** + * @param group + * @param g - generator of group + * @param h - h = g ^ SecretKey + */ + public ElGamalProofOrganizer(ECGroup group, ECPoint g, ECPoint h){ + this.group = group; + this.g = g; + this.h = h; + this.gEncoded = group.encode(g); + this.hEncoded = group.encode(h); + } + + public enum OrProofOrder { + first, second, third, fourth + } + + public enum TrueCouple { + left, right, unknown + } + + /** + * can be used by meerkat.mixer.prover only + * + * call to the meerkat.mixer.main overload with flag = true + */ + protected ElGamalProofInput createProofInput(Crypto.RerandomizableEncryptedMessage in1, Crypto.RerandomizableEncryptedMessage in2 + , Crypto.RerandomizableEncryptedMessage out1, Crypto.RerandomizableEncryptedMessage out2 + , Crypto.EncryptionRandomness r1, Crypto.EncryptionRandomness r2, boolean switched) throws InvalidProtocolBufferException { + + //boolean flag = true; + return createProofInput(in1,in2,out1,out2,r1,r2,switched,true); + } + + /** + * can be used by anyone, e.g meerkat.mixer.verifier + * + * call to the meerkat.mixer.main overload with flag = false + */ + public ElGamalProofInput createProofInput(Crypto.RerandomizableEncryptedMessage in1, Crypto.RerandomizableEncryptedMessage in2 + , Crypto.RerandomizableEncryptedMessage out1, Crypto.RerandomizableEncryptedMessage out2) throws InvalidProtocolBufferException { + + // flag = false; + return createProofInput(in1,in2,out1,out2,null,null,false,false); + } + + /** + * inner method + * convert each encrypted message to ElGamalCiphertext + * + * @param flag - true if called by meerkat.mixer.prover ( r1,r2,switched are known) + * @return ElGamalProofInput + * @throws InvalidProtocolBufferException - in case that at least one of the encrypted messages isn't + * ElGamalCiphertext + */ + private ElGamalProofInput createProofInput(Crypto.RerandomizableEncryptedMessage in1, Crypto.RerandomizableEncryptedMessage in2 + , Crypto.RerandomizableEncryptedMessage out1, Crypto.RerandomizableEncryptedMessage out2 + , Crypto.EncryptionRandomness r1, Crypto.EncryptionRandomness r2, boolean switched,boolean flag) + throws InvalidProtocolBufferException { + + //convert RerandomizableEncryptedMessage to ElGamalCiphertext + ConcreteCrypto.ElGamalCiphertext in1ElGamal = ECElGamalEncryption.RerandomizableEncryptedMessage2ElGamalCiphertext(in1); + ConcreteCrypto.ElGamalCiphertext in2ElGamal = ECElGamalEncryption.RerandomizableEncryptedMessage2ElGamalCiphertext(in2); + ConcreteCrypto.ElGamalCiphertext out1ElGamal = ECElGamalEncryption.RerandomizableEncryptedMessage2ElGamalCiphertext(out1); + ConcreteCrypto.ElGamalCiphertext out2ElGamal = ECElGamalEncryption.RerandomizableEncryptedMessage2ElGamalCiphertext(out2); + + if(flag) { + return new ElGamalProofInput(in1ElGamal, in2ElGamal, out1ElGamal, out2ElGamal, r1, r2, switched); + }else { + return new ElGamalProofInput(in1ElGamal, in2ElGamal, out1ElGamal, out2ElGamal); + } + } + + + /** + * can be construct by instance of organizer only by calling createProofInput method (all constructors are private) + * + * in construction it use for preparing the input for prove, while avoiding double converting or calculations + * + * use as a container for 4 OrProofInput. + */ + public class ElGamalProofInput { + + private final OrProofInput first; + private final OrProofInput second; + private final OrProofInput third; + private final OrProofInput fourth; + + private ECPoint convert2ECPoint(ByteString bs){ + return group.decode(bs.toByteArray()); + } + + + /** + * @param flag - true if called by meerkat.mixer.prover ( r1,r2,switched are known) + */ + private ElGamalProofInput(ConcreteCrypto.ElGamalCiphertext e1, ConcreteCrypto.ElGamalCiphertext e2 + , ConcreteCrypto.ElGamalCiphertext e1New, ConcreteCrypto.ElGamalCiphertext e2New + , Crypto.EncryptionRandomness r1, Crypto.EncryptionRandomness r2, boolean switched,boolean flag){ + + ECPoint e1c1 = convert2ECPoint(e1.getC1()); + ECPoint e1c2 = convert2ECPoint(e1.getC2()); + ECPoint e2c1 = convert2ECPoint(e2.getC1()); + ECPoint e2c2 = convert2ECPoint(e2.getC2()); + ECPoint e1Nc1 = convert2ECPoint(e1New.getC1()); + ECPoint e1Nc2 = convert2ECPoint(e1New.getC2()); + ECPoint e2Nc1 = convert2ECPoint(e2New.getC1()); + ECPoint e2Nc2 = convert2ECPoint(e2New.getC2()); + + ECPoint c1_e1NDive1 = group.add(e1Nc1, group.negate(e1c1)); + ECPoint c1_e2NDive1 = group.add(e2Nc1, group.negate(e1c1)); + ECPoint c1_e1NDive2 = group.add(e1Nc1, group.negate(e2c1)); + ECPoint c1_e2NDive2 = group.add(e2Nc1, group.negate(e2c1)); + + ECPoint c2_e1NDive1 = group.add(e1Nc2, group.negate(e1c2)); + ECPoint c2_e2NDive1 = group.add(e2Nc2, group.negate(e1c2)); + ECPoint c2_e1NDive2 = group.add(e1Nc2, group.negate(e2c2)); + ECPoint c2_e2NDive2 = group.add(e2Nc2, group.negate(e2c2)); + + + if(!flag){ + + this.first = new OrProofInput(c1_e1NDive1,c2_e1NDive1,c1_e1NDive2,c2_e1NDive2); + this.second = new OrProofInput(c1_e1NDive1,c2_e1NDive1,c1_e2NDive1,c2_e2NDive1); + this.third = new OrProofInput(c1_e1NDive2,c2_e1NDive2,c1_e2NDive2,c2_e2NDive2); + this.fourth = new OrProofInput(c1_e2NDive1,c2_e2NDive1,c1_e2NDive2,c2_e2NDive2); + + }else { + + byte[] c1_e1NDive1Encoded = group.encode(c1_e1NDive1); + byte[] c1_e2NDive1Encoded = group.encode(c1_e2NDive1); + byte[] c1_e1NDive2Encoded = group.encode(c1_e1NDive2); + byte[] c1_e2NDive2Encoded = group.encode(c1_e2NDive2); + byte[] c2_e1NDive1Encoded = group.encode(c2_e1NDive1); + byte[] c2_e2NDive1Encoded = group.encode(c2_e2NDive1); + byte[] c2_e1NDive2Encoded = group.encode(c2_e1NDive2); + byte[] c2_e2NDive2Encoded = group.encode(c2_e2NDive2); + + + if (!switched) { + this.first = new OrProofInput(c1_e1NDive1, c2_e1NDive1, c1_e1NDive2, c2_e1NDive2 + , c1_e1NDive1Encoded, c2_e1NDive1Encoded, c1_e1NDive2Encoded, c2_e1NDive2Encoded + , r1, TrueCouple.left); + this.second = new OrProofInput(c1_e1NDive1, c2_e1NDive1, c1_e2NDive1, c2_e2NDive1 + , c1_e1NDive1Encoded, c2_e1NDive1Encoded, c1_e2NDive1Encoded, c2_e2NDive1Encoded + , r1, TrueCouple.left); + this.third = new OrProofInput(c1_e1NDive2, c2_e1NDive2, c1_e2NDive2, c2_e2NDive2 + , c1_e1NDive2Encoded, c2_e1NDive2Encoded, c1_e2NDive2Encoded, c2_e2NDive2Encoded + , r2, TrueCouple.right); + this.fourth = new OrProofInput(c1_e2NDive1, c2_e2NDive1, c1_e2NDive2, c2_e2NDive2 + , c1_e2NDive1Encoded, c2_e2NDive1Encoded, c1_e2NDive2Encoded, c2_e2NDive2Encoded + , r2, TrueCouple.right); + } else { + this.first = new OrProofInput(c1_e1NDive1, c2_e1NDive1, c1_e1NDive2, c2_e1NDive2 + , c1_e1NDive1Encoded, c2_e1NDive1Encoded, c1_e1NDive2Encoded, c2_e1NDive2Encoded + , r2, TrueCouple.right); + this.second = new OrProofInput(c1_e1NDive1, c2_e1NDive1, c1_e2NDive1, c2_e2NDive1 + , c1_e1NDive1Encoded, c2_e1NDive1Encoded, c1_e2NDive1Encoded, c2_e2NDive1Encoded + , r1, TrueCouple.right); + this.third = new OrProofInput(c1_e1NDive2, c2_e1NDive2, c1_e2NDive2, c2_e2NDive2 + , c1_e1NDive2Encoded, c2_e1NDive2Encoded, c1_e2NDive2Encoded, c2_e2NDive2Encoded + , r2, TrueCouple.left); + this.fourth = new OrProofInput(c1_e2NDive1, c2_e2NDive1, c1_e2NDive2, c2_e2NDive2 + , c1_e2NDive1Encoded, c2_e2NDive1Encoded, c1_e2NDive2Encoded, c2_e2NDive2Encoded + , r1, TrueCouple.left); + } + } + } + + + /** + * used by the meerkat.mixer.prover + * call to the meerkat.mixer.main constructor with flag = true + */ + private ElGamalProofInput(ConcreteCrypto.ElGamalCiphertext e1, ConcreteCrypto.ElGamalCiphertext e2 + , ConcreteCrypto.ElGamalCiphertext e1New, ConcreteCrypto.ElGamalCiphertext e2New + , Crypto.EncryptionRandomness r1, Crypto.EncryptionRandomness r2, boolean switched){ + //flag = true; + this(e1,e2,e1New,e2New,r1,r2,switched,true); + } + + /** + * used by meerkat.mixer.prover + * call to the meerkat.mixer.main constructor with flag = true + */ + private ElGamalProofInput(ConcreteCrypto.ElGamalCiphertext e1, ConcreteCrypto.ElGamalCiphertext e2 + , ConcreteCrypto.ElGamalCiphertext e1New, ConcreteCrypto.ElGamalCiphertext e2New){ + //flag = false; + this(e1,e2,e1New,e2New,null,null,false,false); + } + + /** + * getter for all 4 OrProofInputs + * + * @param orProofOrder + * @return the required OrProof + */ + public OrProofInput getOrProofInput(OrProofOrder orProofOrder) { + switch (orProofOrder) { + + case first: + return this.first; + case second: + return this.second; + case third: + return this.third; + case fourth: + return this.fourth; + } + return null; + } + } + + + /** + * can't be constructed (all constructors are private) + * + * 4 instances will be constructed for each new ElGamalProofInput + * + * container for all meerkat.mixer.necessary inputs for single OrProof + */ + public class OrProofInput{ + public final ECPoint g1; + public final ECPoint h1; + public final ECPoint g2; + public final ECPoint h2; + public final ECPoint g1Tag; + public final ECPoint h1Tag; + public final ECPoint g2Tag; + public final ECPoint h2Tag; + + // can be access by meerkat.mixer.prover only + protected final byte[] g1Encoded; + protected final byte[] h1Encoded; + protected final byte[] g2Encoded; + protected final byte[] h2Encoded; + protected final byte[] g1TagEncoded; + protected final byte[] h1TagEncoded; + protected final byte[] g2TagEncoded; + protected final byte[] h2TagEncoded; + protected final Crypto.EncryptionRandomness x; + protected final TrueCouple flag; + + /** + * used by meerkat.mixer.prover only + */ + private OrProofInput(ECPoint h1, ECPoint h2, ECPoint h1Tag, ECPoint h2Tag + ,byte[] h1Encoded, byte[] h2Encoded, byte[] h1TagEncoded, byte[] h2TagEncoded + , Crypto.EncryptionRandomness x, TrueCouple flag) { + this.g1 = g; + this.h1 = h1; + this.g2 = h; + this.h2 = h2; + this.g1Tag = g; + this.h1Tag = h1Tag; + this.g2Tag = h; + this.h2Tag = h2Tag; + + this.g1Encoded = gEncoded; + this.h1Encoded = h1Encoded; + this.g2Encoded = hEncoded; + this.h2Encoded = h2Encoded; + this.g1TagEncoded = gEncoded; + this.h1TagEncoded = h1TagEncoded; + this.g2TagEncoded = hEncoded; + this.h2TagEncoded = h2TagEncoded; + + this.x = x; + this.flag = flag; + + } + + /** + * used by meerkat.mixer.verifier + */ + private OrProofInput(ECPoint h1, ECPoint h2, ECPoint h1Tag, ECPoint h2Tag) { + this(h1,h2,h1Tag,h2Tag,null,null,null,null,null,TrueCouple.unknown); + } + } +} diff --git a/mixer/src/main/java/meerkat/mixer/prover/Prover.java b/mixer/src/main/java/meerkat/mixer/prover/Prover.java new file mode 100644 index 0000000..d540974 --- /dev/null +++ b/mixer/src/main/java/meerkat/mixer/prover/Prover.java @@ -0,0 +1,216 @@ +package meerkat.mixer.prover; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.crypto.concrete.ECElGamalEncryption; +import meerkat.crypto.mixnet.Mix2ZeroKnowledgeProver; +import meerkat.protobuf.Crypto; +import meerkat.protobuf.Mixing; +import org.bouncycastle.math.ec.ECPoint; +import org.factcenter.qilin.primitives.RandomOracle; +import org.factcenter.qilin.primitives.concrete.ECGroup; + +import java.math.BigInteger; +import java.util.Random; + + +/** + * implements of Mix2ZeroKnowledgeProver interface + * this implementation assumes that each RerandomizableEncryptedMessage is ElGamalCiphertext + */ +public class Prover implements Mix2ZeroKnowledgeProver { + + private final ECGroup group; + private final RandomOracle randomOracle; + private final Random rand; + private final ECElGamalEncryption encryptor; + private final ECPoint g,h; + private final BigInteger groupOrderUpperBound; + private final ElGamalProofOrganizer organizer; + + /** + * @param rand + * @param encryptor + * @param randomOracle - use for Fiat–Shamir heuristic + */ + public Prover(Random rand,ECElGamalEncryption encryptor,RandomOracle randomOracle) { + + this.rand = rand; + this.encryptor = encryptor; + this.randomOracle = randomOracle; + this.group = this.encryptor.getGroup(); + this.g = group.getGenerator(); + this.h = this.encryptor.getElGamalPK().getPK(); + this.organizer = new ElGamalProofOrganizer(group,g,h); + this.groupOrderUpperBound = group.orderUpperBound(); + } + + /** + * @param in1 + * @param in2 + * @param out1 - if sw then out1 = rerandomize(in2,r2) else out1 = rerandomize(in1,r1) + * @param out2 - if sw then out2 = rerandomize(in1,r1) else out1 = rerandomize(in2,r2) + * @param sw - flag + * @param i - column of in1 and out1 in encryption table + * @param j - column of in2 and out2 in encryption table + * @param layer - row of in1,in2 in encryption table + * @param r1 + * @param r2 + * @return - a valid ZKP that indeed out1,out2 calculated as required + * @throws InvalidProtocolBufferException + */ + public Mixing.ZeroKnowledgeProof prove(Crypto.RerandomizableEncryptedMessage in1, + Crypto.RerandomizableEncryptedMessage in2, + Crypto.RerandomizableEncryptedMessage out1, + Crypto.RerandomizableEncryptedMessage out2, + boolean sw,int i,int j, int layer, + Crypto.EncryptionRandomness r1, + Crypto.EncryptionRandomness r2) throws InvalidProtocolBufferException { + Mixing.ZeroKnowledgeProof.OrProof first,second,third,fourth; + + ElGamalProofOrganizer.ElGamalProofInput input = organizer.createProofInput(in1,in2,out1,out2,r1,r2,sw); + + first = createOrProofElGamal(input.getOrProofInput(ElGamalProofOrganizer.OrProofOrder.first)); + second = createOrProofElGamal(input.getOrProofInput(ElGamalProofOrganizer.OrProofOrder.second)); + third = createOrProofElGamal(input.getOrProofInput(ElGamalProofOrganizer.OrProofOrder.third)); + fourth = createOrProofElGamal(input.getOrProofInput(ElGamalProofOrganizer.OrProofOrder.fourth)); + + Mixing.ZeroKnowledgeProof.Location location = Mixing.ZeroKnowledgeProof.Location.newBuilder() + .setI(i) + .setJ(j) + .setLayer(layer) + .build(); + + Mixing.ZeroKnowledgeProof result = Mixing.ZeroKnowledgeProof.newBuilder() + .setFirst(first) + .setSecond(second) + .setThird(third) + .setFourth(fourth) + .setLocation(location) + .build(); + return result; + } + + + /** + * Fiat–Shamir heuristic + * @param input - protobuf contains all parameters from the first step of the current prove + * @param randomOracle + * @return randomOracle.hash(input) + */ + public static BigInteger hash(Mixing.ZeroKnowledgeProof.OrProof.ForRandomOracle input, RandomOracle randomOracle) { + byte[] arr = input.toByteArray(); + return new BigInteger(1,randomOracle.hash(arr,arr.length)); + } + + + /** + * @param orProofInput + * @return ZKP OrProof: there exists x s.t (g1 ^ x == h1 and g2 ^ x == h2) or (g1' ^ x == h1 and g2' ^ x == h2) + * assuming DLog is hard in this.group then that proves x is known for the meerkat.mixer.prover + */ + private Mixing.ZeroKnowledgeProof.OrProof createOrProofElGamal(ElGamalProofOrganizer.OrProofInput orProofInput) { + + ECPoint g1 = orProofInput.g1; + ECPoint h1 = orProofInput.h1; + ECPoint g2 = orProofInput.g2; + ECPoint h2 = orProofInput.h2; + + ECPoint g1Tag = orProofInput.g1Tag; + ECPoint h1Tag = orProofInput.h1Tag; + ECPoint g2Tag = orProofInput.g2Tag; + ECPoint h2Tag = orProofInput.h2Tag; + + BigInteger r = encryptor.extractRandomness(encryptor.generateRandomness(rand)).mod(groupOrderUpperBound); + BigInteger c1,c2,z,zTag; + ECPoint u,v,uTag,vTag; + Mixing.ZeroKnowledgeProof.OrProof.ForRandomOracle forRandomOracle; + + + switch (orProofInput.flag) { + case left: + c2 = encryptor.extractRandomness(encryptor.generateRandomness(rand)).mod(groupOrderUpperBound); + zTag = encryptor.extractRandomness(encryptor.generateRandomness(rand)).mod(groupOrderUpperBound); + //step 1 + u = group.multiply(g1, r); + v = group.multiply(g2, r); + uTag = group.add(group.multiply(g1Tag, zTag), group.negate(group.multiply(h1Tag, c2))); + vTag = group.add(group.multiply(g2Tag, zTag), group.negate(group.multiply(h2Tag, c2))); + //step 2 + // c1 = (hash(input + step1) + group size - c2)% group size + forRandomOracle = + Mixing.ZeroKnowledgeProof.OrProof.ForRandomOracle.newBuilder() + .setG1(ByteString.copyFrom(orProofInput.g1Encoded)) + .setH1(ByteString.copyFrom(orProofInput.h1Encoded)) + .setG2(ByteString.copyFrom(orProofInput.g2Encoded)) + .setH2(ByteString.copyFrom(orProofInput.h2Encoded)) + .setG1Tag(ByteString.copyFrom(orProofInput.g1TagEncoded)) + .setH1Tag(ByteString.copyFrom(orProofInput.h1TagEncoded)) + .setG2Tag(ByteString.copyFrom(orProofInput.g2TagEncoded)) + .setH2Tag(ByteString.copyFrom(orProofInput.h2TagEncoded)) + .setU(ByteString.copyFrom(group.encode(u))) + .setV(ByteString.copyFrom(group.encode(v))) + .setUTag(ByteString.copyFrom(group.encode(uTag))) + .setVTag(ByteString.copyFrom(group.encode(vTag))) + .build(); + c1 = hash(forRandomOracle,randomOracle).add(group.orderUpperBound().subtract(c2)).mod(groupOrderUpperBound); + //step 3 + //z = (r + c1 * x) % group size; + z = r.add(c1.multiply(encryptor.extractRandomness(orProofInput.x))).mod(groupOrderUpperBound); + break; + case right: + c1 = encryptor.extractRandomness(encryptor.generateRandomness(rand)).mod(groupOrderUpperBound); + z = encryptor.extractRandomness(encryptor.generateRandomness(rand)).mod(groupOrderUpperBound); + //step 1 + uTag = group.multiply(g1Tag, r); + vTag = group.multiply(g2Tag, r); + u = group.add(group.multiply(g1, z), group.negate(group.multiply(h1, c1))); + v = group.add(group.multiply(g2, z), group.negate(group.multiply(h2, c1))); + //step 2 + // c1 = (hash(input + step1) + group size - c1)% group size + forRandomOracle = + Mixing.ZeroKnowledgeProof.OrProof.ForRandomOracle.newBuilder() + .setG1(ByteString.copyFrom(orProofInput.g1Encoded)) + .setH1(ByteString.copyFrom(orProofInput.h1Encoded)) + .setG2(ByteString.copyFrom(orProofInput.g2Encoded)) + .setH2(ByteString.copyFrom(orProofInput.h2Encoded)) + .setG1Tag(ByteString.copyFrom(orProofInput.g1TagEncoded)) + .setH1Tag(ByteString.copyFrom(orProofInput.h1TagEncoded)) + .setG2Tag(ByteString.copyFrom(orProofInput.g2TagEncoded)) + .setH2Tag(ByteString.copyFrom(orProofInput.h2TagEncoded)) + .setU(ByteString.copyFrom(group.encode(u))) + .setV(ByteString.copyFrom(group.encode(v))) + .setUTag(ByteString.copyFrom(group.encode(uTag))) + .setVTag(ByteString.copyFrom(group.encode(vTag))) + .build(); + c2 = hash(forRandomOracle,randomOracle).add(group.orderUpperBound().subtract(c1)).mod(groupOrderUpperBound); + //step 3 + //zTag = (r + c2 * x) % group size; + zTag = r.add(c2.multiply(encryptor.extractRandomness(orProofInput.x))).mod(groupOrderUpperBound); + break; + default: + return null; + } + + + return Mixing.ZeroKnowledgeProof.OrProof.newBuilder() + .setG1(forRandomOracle.getG1()) + .setH1(forRandomOracle.getH1()) + .setG2(forRandomOracle.getG2()) + .setH2(forRandomOracle.getH2()) + .setG1Tag(forRandomOracle.getG1()) + .setH1Tag(forRandomOracle.getH1Tag()) + .setG2Tag(forRandomOracle.getG2Tag()) + .setH2Tag(forRandomOracle.getH2Tag()) + .setU(forRandomOracle.getU()) + .setV(forRandomOracle.getV()) + .setUTag(forRandomOracle.getUTag()) + .setVTag(forRandomOracle.getVTag()) + .setC1(ByteString.copyFrom(c1.toByteArray())) + .setC2(ByteString.copyFrom(c2.toByteArray())) + .setZ(ByteString.copyFrom(z.toByteArray())) + .setZTag(ByteString.copyFrom(zTag.toByteArray())) + .build(); + } +} + diff --git a/mixer/src/main/java/meerkat/mixer/verifier/Verifier.java b/mixer/src/main/java/meerkat/mixer/verifier/Verifier.java new file mode 100644 index 0000000..ed08512 --- /dev/null +++ b/mixer/src/main/java/meerkat/mixer/verifier/Verifier.java @@ -0,0 +1,95 @@ +package meerkat.mixer.verifier; + +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.crypto.concrete.ECElGamalEncryption; +import meerkat.crypto.mixnet.Mix2ZeroKnowledgeVerifier; +import meerkat.protobuf.Crypto; +import meerkat.protobuf.Mixing; +import org.bouncycastle.math.ec.ECPoint; +import meerkat.mixer.prover.ElGamalProofOrganizer; +import meerkat.mixer.prover.Prover; +import org.factcenter.qilin.primitives.RandomOracle; +import org.factcenter.qilin.primitives.concrete.ECGroup; + +/** + * implements Mix2ZeroKnowledgeVerifier + */ +public class Verifier implements Mix2ZeroKnowledgeVerifier { + + + private final ECGroup group; + private final RandomOracle randomOracle; + private final ECPoint g,h; + private final ElGamalProofOrganizer organizer; + private final ZeroKnowledgeOrProofParser parser; + + /** + * constructor + * @param encryptor should be as the encryptor used by meerkat.mixer.prover + * @param randomOracle should be as the random oracle used by meerkat.mixer.prover + */ + public Verifier(ECElGamalEncryption encryptor, RandomOracle randomOracle) { + this.group = encryptor.getGroup(); + this.g = group.getGenerator(); + this.h = encryptor.getElGamalPK().getPK(); + this.randomOracle = randomOracle; + this.organizer = new ElGamalProofOrganizer(group,g,h); + this.parser = new ZeroKnowledgeOrProofParser(group); + } + + /** + * verify zero knowledge proof + * @param in1 + * @param in2 + * @param out1 + * @param out2 + * @param proof out1 = rerandomize(in1) && out2 = rerandomize(in2) + * || + * out1 = rerandomize(in2) && out2 = rerandomize(in1) + * @return true iff all 4 or proofs are valid + * @throws InvalidProtocolBufferException + */ + public boolean verify(Crypto.RerandomizableEncryptedMessage in1, + Crypto.RerandomizableEncryptedMessage in2, + Crypto.RerandomizableEncryptedMessage out1, + Crypto.RerandomizableEncryptedMessage out2, + Mixing.ZeroKnowledgeProof proof) throws InvalidProtocolBufferException { + ElGamalProofOrganizer.ElGamalProofInput input = organizer.createProofInput(in1,in2,out1,out2); + return verifyElGamaOrProof(input.getOrProofInput(ElGamalProofOrganizer.OrProofOrder.first), proof.getFirst())&& + verifyElGamaOrProof(input.getOrProofInput(ElGamalProofOrganizer.OrProofOrder.second), proof.getSecond())&& + verifyElGamaOrProof(input.getOrProofInput(ElGamalProofOrganizer.OrProofOrder.third), proof.getThird())&& + verifyElGamaOrProof(input.getOrProofInput(ElGamalProofOrganizer.OrProofOrder.fourth), proof.getFourth()); + + } + + + /** + * + * @param orProofInput + * @param orProof + * @return verify single or proof + */ + private boolean verifyElGamaOrProof(ElGamalProofOrganizer.OrProofInput orProofInput, + Mixing.ZeroKnowledgeProof.OrProof orProof) { + ZeroKnowledgeOrProofParser.ZeroKnowledgeOrProofContainer container = parser.parseOrProof(orProof); + return container.g1.equals(orProofInput.g1) && + container.h1.equals(orProofInput.h1) && + container.g2.equals(orProofInput.g2) && + container.h2.equals(orProofInput.h2) && + container.g1Tag.equals(orProofInput.g1Tag) && + container.h1Tag.equals(orProofInput.h1Tag) && + container.g2Tag.equals(orProofInput.g2Tag) && + container.h2Tag.equals(orProofInput.h2Tag) && + container.c1.add(container.c2).mod(group.orderUpperBound()) + .equals(Prover.hash(container.forRandomOracle,randomOracle).mod(group.orderUpperBound())) && + group.multiply(container.g1, container.z) + .equals(group.add(container.u, group.multiply(container.h1,container.c1))) && + group.multiply(container.g2, container.z) + .equals(group.add(container.v, group.multiply(container.h2,container.c1))) && + group.multiply(container.g1Tag, container.zTag) + .equals(group.add(container.uTag, group.multiply(container.h1Tag,container.c2))) && + group.multiply(container.g2Tag, container.zTag) + .equals(group.add(container.vTag, group.multiply(container.h2Tag,container.c2))); + + } +} diff --git a/mixer/src/main/java/meerkat/mixer/verifier/VerifyTable.java b/mixer/src/main/java/meerkat/mixer/verifier/VerifyTable.java new file mode 100644 index 0000000..2bb3781 --- /dev/null +++ b/mixer/src/main/java/meerkat/mixer/verifier/VerifyTable.java @@ -0,0 +1,91 @@ +package meerkat.mixer.verifier; + +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.crypto.mixnet.Mix2ZeroKnowledgeVerifier; +import meerkat.crypto.mixnet.MixerOutput; +import meerkat.protobuf.Crypto; +import meerkat.protobuf.Mixing; + +import java.util.Arrays; + +/** + * Created by Tzlil on 12/30/2015. + * provide one operation - verify meerkat.mixer.mixing output + */ +public final class VerifyTable { + /** + * constructor + * @param verifier + * @param n + * @param mixerOutput + * @return true iff the meerkat.mixer.mixing output is valid + * @throws InvalidProtocolBufferException + */ + public static boolean verifyTable(Mix2ZeroKnowledgeVerifier verifier,int n,MixerOutput mixerOutput) + throws InvalidProtocolBufferException { + int index1,index2,layer; + + //assert n = 2^k + if ( (n &(n-1)) != 0) + throw new IllegalArgumentException("n"); + + int layers = 2*(int)(Math.log(n) / Math.log(2)) - 1; + //initialize locationChecksum table + // use for check BeneshNet validity + boolean[][] locationChecksum = new boolean[layers][n]; + for (boolean[] locationChecksumLayer: locationChecksum) { + Arrays.fill(locationChecksumLayer,false); + } + + Mixing.ZeroKnowledgeProof[][] zeroKnowledgeProofs = mixerOutput.getProofs(); + Crypto.RerandomizableEncryptedMessage[][] rerandomizableEncryptedMessages = mixerOutput.getEncryptedMessages(); + + for (int i = 0; i < zeroKnowledgeProofs.length ; i++){ + for (int j = 0; j < zeroKnowledgeProofs[i].length ; j ++){ + Mixing.ZeroKnowledgeProof zkp = zeroKnowledgeProofs[i][j]; + Mixing.ZeroKnowledgeProof.Location location = zkp.getLocation(); + index1 = location.getI(); + index2 = location.getJ(); + layer = location.getLayer(); + + // check location validity + if (layer > layers >> 1) { + if (index2 - index1 != n >> (layers - layer)) + return false; + } + else{ + if (index2 - index1 != n >> (layer + 1)) + return false; + } + + // mark location in table + locationChecksum[layer][index1] = true; + locationChecksum[layer][index2] = true; + + // verify proof + if(!verifier.verify(rerandomizableEncryptedMessages[layer][index1], + rerandomizableEncryptedMessages[layer][index2], + rerandomizableEncryptedMessages[layer + 1][index1], + rerandomizableEncryptedMessages[layer + 1][index2], + zkp)) { + + verifier.verify(rerandomizableEncryptedMessages[layer][index1], + rerandomizableEncryptedMessages[layer][index2], + rerandomizableEncryptedMessages[layer + 1][index1], + rerandomizableEncryptedMessages[layer + 1][index2], + zkp); + return false; + } + } + } + + // verify all meerkat.mixer.necessary locations for BeneshNet were proved + for (boolean[] checksumLayer: locationChecksum) { + for (boolean locationBoolean: checksumLayer) { + if (!locationBoolean) + return false; + } + } + return true; + } +} diff --git a/mixer/src/main/java/meerkat/mixer/verifier/ZeroKnowledgeOrProofParser.java b/mixer/src/main/java/meerkat/mixer/verifier/ZeroKnowledgeOrProofParser.java new file mode 100644 index 0000000..01ff344 --- /dev/null +++ b/mixer/src/main/java/meerkat/mixer/verifier/ZeroKnowledgeOrProofParser.java @@ -0,0 +1,95 @@ +package meerkat.mixer.verifier; + +import com.google.protobuf.ByteString; +import meerkat.protobuf.Mixing; +import org.bouncycastle.math.ec.ECPoint; +import org.factcenter.qilin.primitives.concrete.ECGroup; + +import java.math.BigInteger; + +/** + * Created by Tzlil on 1/25/2016. + * zero knowledge proof parser + */ +public class ZeroKnowledgeOrProofParser { + + private final ECGroup group; + + /** + * parse or proof message and return the result in zero knowledge or proof container + * @param orProof + * @return zero knowledge or proof container + */ + public ZeroKnowledgeOrProofContainer parseOrProof(Mixing.ZeroKnowledgeProof.OrProof orProof){ + return new ZeroKnowledgeOrProofContainer(orProof); + } + + /** + * getter + * @param group + */ + public ZeroKnowledgeOrProofParser(ECGroup group) { + this.group = group; + } + + /** + * convert ByteString to ECPoint + * @param bs + * @return + */ + public ECPoint convert2ECPoint(ByteString bs){ + return group.decode(bs.toByteArray()); + } + + + /** + * inner class + * container for parsed zero knowledge or proof + * constructor is private, can be construct using ZeroKnowledgeOrProofParser.parseOrProof + */ + public class ZeroKnowledgeOrProofContainer{ + public final ECPoint g1,g2,h1,h2; + public final ECPoint g1Tag,g2Tag,h1Tag,h2Tag; + public final ECPoint u,v,uTag,vTag; + public final BigInteger c1,c2,z,zTag; + public final Mixing.ZeroKnowledgeProof.OrProof.ForRandomOracle forRandomOracle; + + /** + * constructor + * @param orProof + */ + private ZeroKnowledgeOrProofContainer(Mixing.ZeroKnowledgeProof.OrProof orProof){ + this.forRandomOracle = + Mixing.ZeroKnowledgeProof.OrProof.ForRandomOracle.newBuilder() + .setG1(orProof.getG1()) + .setH1(orProof.getH1()) + .setG2(orProof.getG2()) + .setH2(orProof.getH2()) + .setG1Tag(orProof.getG1Tag()) + .setH1Tag(orProof.getH1Tag()) + .setG2Tag(orProof.getG2Tag()) + .setH2Tag(orProof.getH2Tag()) + .setU(orProof.getU()) + .setV(orProof.getV()) + .setUTag(orProof.getUTag()) + .setVTag(orProof.getVTag()) + .build(); + this.g1 = convert2ECPoint(orProof.getG1()); + this.g2 = convert2ECPoint(orProof.getG2()); + this.h1 = convert2ECPoint(orProof.getH1()); + this.h2 = convert2ECPoint(orProof.getH2()); + this.g1Tag = convert2ECPoint(orProof.getG1Tag()); + this.g2Tag = convert2ECPoint(orProof.getG2Tag()); + this.h1Tag = convert2ECPoint(orProof.getH1Tag()); + this.h2Tag = convert2ECPoint(orProof.getH2Tag()); + this.u = convert2ECPoint(orProof.getU()); + this.v = convert2ECPoint(orProof.getV()); + this.uTag = convert2ECPoint(orProof.getUTag()); + this.vTag = convert2ECPoint(orProof.getVTag()); + this.c1 = new BigInteger(orProof.getC1().toByteArray()); + this.c2 = new BigInteger(orProof.getC2().toByteArray()); + this.z = new BigInteger(orProof.getZ().toByteArray()); + this.zTag = new BigInteger(orProof.getZTag().toByteArray()); + } + } +} diff --git a/mixer/src/test/java/meerkat/mixer/CreateTestVector.java b/mixer/src/test/java/meerkat/mixer/CreateTestVector.java new file mode 100644 index 0000000..fe45e1b --- /dev/null +++ b/mixer/src/test/java/meerkat/mixer/CreateTestVector.java @@ -0,0 +1,100 @@ +package meerkat.mixer; + +import meerkat.crypto.concrete.ECElGamalEncryption; +import meerkat.crypto.mixnet.Mix2ZeroKnowledgeProver; +import meerkat.crypto.mixnet.Mix2ZeroKnowledgeVerifier; +import meerkat.mixer.mixing.Mixer; +import meerkat.mixer.mixing.MixerOutput; +import meerkat.mixer.prover.Prover; +import meerkat.mixer.verifier.Verifier; +import meerkat.mixer.verifier.VerifyTable; +import meerkat.protobuf.Crypto; +import meerkat.protobuf.Voting; +import org.factcenter.qilin.primitives.RandomOracle; +import org.factcenter.qilin.primitives.concrete.DigestOracle; +import org.factcenter.qilin.primitives.concrete.ECElGamal; +import org.factcenter.qilin.primitives.concrete.ECGroup; +import org.junit.Before; + +import java.io.IOException; +import java.security.spec.InvalidKeySpecException; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * Created by Tzlil on 1/19/2016. + */ +public class CreateTestVector { + + + ECElGamalEncryption encryptor; + ECGroup group; + Random random,randomMixer,randomProver; + RandomOracle randomOracle; + Mix2ZeroKnowledgeVerifier verifier; + Mix2ZeroKnowledgeProver prover; + meerkat.crypto.mixnet.Mixer mixer; + private int layers; + private int n; + + + @Before + public void setup() throws InvalidKeySpecException { + // initialization + random = new Random(); + group = new ECGroup("secp256k1"); + encryptor = new ECElGamalEncryption(); + encryptor.init(Utils.serializePk(group, new ECElGamal.SK(group, ECElGamal.generateSecretKey(group, random)))); + randomMixer = new Random(); + randomProver = new Random(); + randomOracle = new DigestOracle(); + verifier = new Verifier(encryptor,randomOracle); + prover = new Prover(randomProver,encryptor,randomOracle); + mixer = new Mixer(prover,encryptor); + + // generate n + int logN = 10; // + random.nextInt(8) + layers = 2*logN - 1; + n = 1 << logN; + } + + public List generateMixerInput(){ + List result = new ArrayList(); + Voting.PlaintextBallot msg; + for (int i = 0; i < n ; i++){ + msg = Utils.genRandomBallot(2,3,16); + result.add(encryptor.encrypt(msg, encryptor.generateRandomness(random))); + } + return result; + } + //@SimpleRerandomizeTest + public void createValidTest() throws IOException { + + List mixerInput = generateMixerInput(); + System.out.println("start mixing"); + MixerOutput mixerOutput = (MixerOutput)mixer.mix(mixerInput,randomMixer); + System.out.println("mixing ended, start verification"); + assert (VerifyTable.verifyTable(verifier,n,mixerOutput)); + System.out.println("verification ended, start printing"); + mixerOutput.outToFolder("C:\\Users\\Tzlil\\Desktop\\TestVector\\Test3"); + System.out.println("all done"); + } + + //@SimpleRerandomizeTest + public void createInvalidTest() throws IOException { + + //Mix2ZeroKnowledgeVerifier corruptedVerifier = new Verifier(encryptor,randomOracle,true); + //Mix2ZeroKnowledgeProver corruptedProver = new Prover(randomProver,encryptor,randomOracle,true); + //mixer = new Mixer(randomMixer,corruptedProver,encryptor,corruptedVerifier); + + List mixerInput = generateMixerInput(); + System.out.println("start mixing"); + MixerOutput mixerOutput = (MixerOutput)mixer.mix(mixerInput,random); + System.out.println("mixing ended, start negative verification"); + assert (!VerifyTable.verifyTable(verifier,n,mixerOutput)); + System.out.println("verification ended, start printing"); + mixerOutput.outToFolder("C:\\Users\\Tzlil\\Desktop\\TestVector\\Test5"); + System.out.println("all done"); + } +} diff --git a/mixer/src/test/java/meerkat/mixer/MixNetworkTest.java b/mixer/src/test/java/meerkat/mixer/MixNetworkTest.java new file mode 100644 index 0000000..23eaea4 --- /dev/null +++ b/mixer/src/test/java/meerkat/mixer/MixNetworkTest.java @@ -0,0 +1,46 @@ +package meerkat.mixer; + +/** + * Created by Tzlil on 12/17/2015. + */ + +import meerkat.mixer.mixing.MixNetwork; +import meerkat.mixer.mixing.RandomPermutation; +import meerkat.mixer.mixing.Switch; +import org.junit.Test; +import java.util.Arrays; +import java.util.Random; + +public class MixNetworkTest { + + @Test + public void testMixNetwork() throws Exception{ + + Random random = new Random(); + int logn = 10; + int n = 1 << logn; + int layers = 2*logn - 1; + RandomPermutation randomPermutation = new RandomPermutation(n,random); + MixNetwork mixNetwork = new MixNetwork(randomPermutation); + + //initialize arr s.t arr[i] = i + int[] arr = new int[n]; + for (int i = 0; i < n ;i ++) + arr[i] = i; + + // layer by layer swap between values + for (int layer = 0 ; layer< layers ; layer ++) { + for (Switch sw : mixNetwork.getSwitchesByLayer(layer)) { + if(sw.value) { + arr[sw.i] += arr[sw.j]; + arr[sw.j] = arr[sw.i] - arr[sw.j]; + arr[sw.i] -= arr[sw.j]; + } + } + } + + assert(Arrays.equals(arr,randomPermutation.permutation)); + + } + +} diff --git a/mixer/src/test/java/meerkat/mixer/MixingTest.java b/mixer/src/test/java/meerkat/mixer/MixingTest.java new file mode 100644 index 0000000..f9ec6f8 --- /dev/null +++ b/mixer/src/test/java/meerkat/mixer/MixingTest.java @@ -0,0 +1,92 @@ +package meerkat.mixer; + +/** + * Created by Tzlil on 12/30/2015. + */ + +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.crypto.concrete.ECElGamalEncryption; +import meerkat.crypto.mixnet.Mix2ZeroKnowledgeVerifier; +import meerkat.mixer.mixing.Mixer; +import meerkat.mixer.prover.Prover; +import meerkat.mixer.verifier.Verifier; +import meerkat.mixer.verifier.VerifyTable; +import meerkat.protobuf.Crypto; +import meerkat.protobuf.Voting; +import org.factcenter.qilin.primitives.RandomOracle; +import org.factcenter.qilin.primitives.concrete.DigestOracle; +import org.factcenter.qilin.primitives.concrete.ECElGamal; +import org.factcenter.qilin.primitives.concrete.ECGroup; +import org.junit.Before; +import org.junit.Test; + +import java.security.spec.InvalidKeySpecException; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class MixingTest { + + ECElGamalEncryption encryptor; + ECGroup group; + Random random,randomMixer,randomProver; + RandomOracle randomOracle; + Mix2ZeroKnowledgeVerifier verifier; + Prover prover; + meerkat.crypto.mixnet.Mixer mixer; + private int layers; + private int n; + + + @Before + public void setup() throws InvalidKeySpecException { + // initialization + random = new Random(); + group = new ECGroup("secp256k1"); + encryptor = new ECElGamalEncryption(); + encryptor.init(Utils.serializePk(group, new ECElGamal.SK(group, ECElGamal.generateSecretKey(group, random)))); + randomMixer = new Random(); + randomProver = new Random(); + randomOracle = new DigestOracle(); + verifier = new Verifier(encryptor,randomOracle); + prover = new Prover(randomProver,encryptor,randomOracle); + mixer = new Mixer(prover,encryptor); + + // generate n + int logN = 8; // + random.nextInt(8) + layers = 2*logN - 1; + n = 1 << logN; + } + + public List generateMixerInput(){ + List result = new ArrayList(); + Voting.PlaintextBallot msg; + for (int i = 0; i < n ; i++){ + msg = Utils.genRandomBallot(2,3,16); + result.add(encryptor.encrypt(msg, encryptor.generateRandomness(random))); + } + return result; + } + + @Test + public void mixingTest() throws InvalidProtocolBufferException { + System.out.println("n is : " + n); + System.out.println(" generating input"); + List mixerInput = generateMixerInput(); + System.out.println(" start mixing"); + long startTime = System.currentTimeMillis(); + + meerkat.crypto.mixnet.MixerOutput mixerOutput = mixer.mix(mixerInput,randomMixer); + + long finishTime = System.currentTimeMillis(); + System.out.println(" that took: "+(finishTime-startTime)+ " ms"); + + System.out.println("start verification"); + startTime = System.currentTimeMillis(); + + assert (VerifyTable.verifyTable(verifier,n,mixerOutput)); + + finishTime = System.currentTimeMillis(); + System.out.println(" that took: "+(finishTime-startTime)+ " ms"); + } +} diff --git a/mixer/src/test/java/meerkat/mixer/RerandomizeTest.java b/mixer/src/test/java/meerkat/mixer/RerandomizeTest.java new file mode 100644 index 0000000..dea0069 --- /dev/null +++ b/mixer/src/test/java/meerkat/mixer/RerandomizeTest.java @@ -0,0 +1,90 @@ +package meerkat.mixer; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.crypto.concrete.ECElGamalEncryption; +import meerkat.protobuf.ConcreteCrypto; +import meerkat.protobuf.Crypto; +import meerkat.protobuf.Voting; +import org.bouncycastle.math.ec.ECPoint; +import org.factcenter.qilin.primitives.RandomOracle; +import org.factcenter.qilin.primitives.concrete.DigestOracle; +import org.factcenter.qilin.primitives.concrete.ECElGamal; +import org.factcenter.qilin.primitives.concrete.ECGroup; +import org.junit.Before; +import org.junit.Test; + +import java.math.BigInteger; +import java.util.Random; + +/** + * Created by Tzlil on 1/17/2016. + */ +public class RerandomizeTest { + + Random rand; + ECElGamal.SK key; + ECGroup group; + ECElGamalEncryption enc; + ConcreteCrypto.ElGamalPublicKey serializedPk; + ECPoint g ; + ECPoint h ; + @Before + public void setup() throws Exception { + rand = new Random(); + group = new ECGroup("secp256k1"); + BigInteger sk = ECElGamal.generateSecretKey(group, rand); + key = new ECElGamal.SK(group, sk); + serializedPk = Utils.serializePk(group, key); + enc = new ECElGamalEncryption(); + enc.init(serializedPk); + RandomOracle randomOracle = new DigestOracle(); + g = group.getGenerator(); + h = enc.getElGamalPK().getPK(); + } + + private ECPoint convert2ECPoint(ByteString bs){ + return group.decode(bs.toByteArray()); + } + + public void oneRerandomizeTest() throws InvalidProtocolBufferException { + Voting.PlaintextBallot msg = Utils.genRandomBallot(2,3,16); // 2 questions with 3 answers each, in range 0-15. + + Crypto.EncryptionRandomness r = enc.generateRandomness(rand); + + + Crypto.RerandomizableEncryptedMessage e = enc.encrypt(msg, enc.generateRandomness(rand)); + Crypto.RerandomizableEncryptedMessage eNew = enc.rerandomize(e, r); + + assert (Utils.decrypt(Voting.PlaintextBallot.class, key, group, e).equals(msg)); + assert (Utils.decrypt(Voting.PlaintextBallot.class, key, group, eNew).equals(msg)); + + ConcreteCrypto.ElGamalCiphertext eElGamal = ECElGamalEncryption.RerandomizableEncryptedMessage2ElGamalCiphertext(e); + ConcreteCrypto.ElGamalCiphertext eNewElGamal = ECElGamalEncryption.RerandomizableEncryptedMessage2ElGamalCiphertext(eNew); + + ECPoint expected1 = g.multiply(enc.extractRandomness(r)); + ECPoint result1 = group.add(convert2ECPoint(eNewElGamal.getC1()),group.negate(convert2ECPoint(eElGamal.getC1()))); + expected1.normalize(); + result1.normalize(); + assert (expected1.equals(result1)); + + ECPoint expected2 = h.multiply(enc.extractRandomness(r)); + ECPoint result2 = group.add(convert2ECPoint(eNewElGamal.getC2()), group.negate(convert2ECPoint(eElGamal.getC2()))); + expected2.normalize(); + result2.normalize(); + + assert (expected2.equals(result2)); + } + + + @Test + public void rerandomizeTest() throws InvalidProtocolBufferException { + + int tests = 1000; + + for (int i = 0; i < tests; i ++){ + System.out.println("re-randomize test #" + i); + oneRerandomizeTest(); + } + } +} diff --git a/mixer/src/test/java/meerkat/mixer/SimpleRerandomizeTest.java b/mixer/src/test/java/meerkat/mixer/SimpleRerandomizeTest.java new file mode 100644 index 0000000..27e4d2d --- /dev/null +++ b/mixer/src/test/java/meerkat/mixer/SimpleRerandomizeTest.java @@ -0,0 +1,89 @@ +package meerkat.mixer; + +import org.bouncycastle.math.ec.ECPoint; +import org.factcenter.qilin.primitives.concrete.ECGroup; +import org.junit.Before; + +import java.math.BigInteger; +import java.util.Random; + +/** + * Created by Tzlil on 3/20/2016. + */ +public class SimpleRerandomizeTest { + + Random rand; + ECGroup group; + ECPoint g ; + ECPoint h ; + BigInteger sk; + @Before + public void setup() throws Exception { + rand = new Random(); + group = new ECGroup("secp256k1"); + g = group.getGenerator(); + sk = random(); + h = g.multiply(sk); + } + + public BigInteger random(){ + return new BigInteger(256,rand).mod(group.orderUpperBound()); + } + + public ECPoint randomMessage(){ + return group.sample(rand); + } + + public Encryption encrypt(ECPoint message,BigInteger randomness){ + return new Encryption(g.multiply(randomness), message.add(h.multiply(randomness))); + } + + public Encryption rerandomize(Encryption encryption,BigInteger randomness){ + return new Encryption(encryption.a.add(g.multiply(randomness)),encryption.b.add(h.multiply(randomness))); + } + + public ECPoint decrypt(Encryption encryption){ + return encryption.a.multiply(sk).negate().add(encryption.b); + } + + public class Encryption{ + ECPoint a,b; + Encryption(ECPoint a, ECPoint b){ + this.a = a; + this.b = b; + } + } + + public void oneRerandomizeTest(){ + ECPoint message = randomMessage(); + Encryption e = encrypt(message,random()); + BigInteger r = random(); + Encryption eNew = rerandomize(e,r); + assert (decrypt(e).equals(message)); + assert (decrypt(eNew).equals(message)); + + ECPoint expected1 = g.multiply(r); + ECPoint result1 = group.add(eNew.a,e.a.negate()); + expected1.normalize(); + result1.normalize(); + assert (expected1.equals(result1)); + + ECPoint expected2 = h.multiply(r); + ECPoint result2 = group.add(eNew.b,e.b.negate()); + expected2.normalize(); + result2.normalize(); + assert (expected2.equals(result2)); + } + + + @org.junit.Test + public void rerandomizeTest(){ + + int tests = 1000; + + for (int i = 0; i < tests; i ++){ + System.out.println("rerandomize test #" + i); + oneRerandomizeTest(); + } + } +} diff --git a/mixer/src/test/java/meerkat/mixer/Utils.java b/mixer/src/test/java/meerkat/mixer/Utils.java new file mode 100644 index 0000000..06329ef --- /dev/null +++ b/mixer/src/test/java/meerkat/mixer/Utils.java @@ -0,0 +1,103 @@ +package meerkat.mixer; + +import com.google.protobuf.ByteString; +import com.google.protobuf.GeneratedMessage; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Message; +import meerkat.crypto.concrete.ECElGamalEncryption; +import meerkat.crypto.concrete.GlobalCryptoSetup; +import meerkat.protobuf.ConcreteCrypto; +import meerkat.protobuf.Crypto; +import meerkat.protobuf.Voting; +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.bouncycastle.jce.spec.ECPublicKeySpec; +import org.bouncycastle.math.ec.ECPoint; +import org.factcenter.qilin.primitives.concrete.ECElGamal; +import org.factcenter.qilin.primitives.concrete.ECGroup; +import org.factcenter.qilin.primitives.generic.ElGamal; +import org.factcenter.qilin.util.Pair; + +import java.io.ByteArrayInputStream; +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.util.Random; + +/** + * Created by Tzlil on 1/1/2016. + */ +public class Utils { + + + public final static String ENCRYPTION_KEY_ALGORITHM = "ECDH"; + /** + * Serialize an El-Gamal public key into a form acceptable by {@link ECElGamalEncryption} + * @param pk + * @return + */ + public static ConcreteCrypto.ElGamalPublicKey serializePk(ECGroup group, ElGamal.PK pk) { + ECPoint pkPoint = pk.getPK(); + ECParameterSpec params = group.getCurveParams(); + + ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(pkPoint, params); + + try { + KeyFactory fact = KeyFactory.getInstance(ENCRYPTION_KEY_ALGORITHM, + GlobalCryptoSetup.getInstance().getBouncyCastleProvider()); + PublicKey javaPk = fact.generatePublic(pubKeySpec); + ConcreteCrypto.ElGamalPublicKey serializedPk = ConcreteCrypto.ElGamalPublicKey.newBuilder() + .setSubjectPublicKeyInfo(ByteString.copyFrom(javaPk.getEncoded())).build(); + + return serializedPk; + } catch (Exception e) { + throw new RuntimeException("Error converting public key!", e); + } + } + + /** + * Standard (non-threshold) decryption for testing purposes. + * @param secretKey + * @return + */ + public static T decrypt(Class plaintextMessageType, ECElGamal.SK secretKey, ECGroup group, Crypto.RerandomizableEncryptedMessage opaqueCipher) + throws InvalidProtocolBufferException { + ConcreteCrypto.ElGamalCiphertext cipherText = ConcreteCrypto.ElGamalCiphertext.parseFrom(opaqueCipher.getData()); + ByteString c1encoded = cipherText.getC1(); + ByteString c2encoded = cipherText.getC2(); + + ECPoint c1 = group.decode(c1encoded.toByteArray()); + ECPoint c2 = group.decode(c2encoded.toByteArray()); + + assert (group.contains(c1)); + assert (group.contains(c2)); + + ECPoint plaintextEncoded = secretKey.decrypt(new Pair(c1, c2)); + + byte[] plaintext = group.injectiveDecode(plaintextEncoded); + + ByteArrayInputStream in = new ByteArrayInputStream(plaintext); + + try { + java.lang.reflect.Method newBuilder = plaintextMessageType.getMethod("newBuilder"); + Message.Builder builder = (Message.Builder) newBuilder.invoke(plaintextMessageType); + builder.mergeDelimitedFrom(in); + return plaintextMessageType.cast(builder.build()); + } catch (Exception e) { + throw new InvalidProtocolBufferException("Plaintext protobuf error"); + } + } + + static Random random = new Random(); + + public static Voting.PlaintextBallot genRandomBallot(int numQuestions, int numAnswers, int maxAnswer) { + Voting.PlaintextBallot.Builder ballot = Voting.PlaintextBallot.newBuilder(); + ballot.setSerialNumber(random.nextInt(1000000)); + for (int i = 0; i < numQuestions; ++i) { + Voting.BallotAnswer.Builder answers = ballot.addAnswersBuilder(); + for (int j = 0; j < numAnswers; ++j) { + answers.addAnswer(random.nextInt(maxAnswer)); + } + } + return ballot.build(); + } +} diff --git a/mixer/src/test/java/meerkat/mixer/ZeroKnowledgeProofTest.java b/mixer/src/test/java/meerkat/mixer/ZeroKnowledgeProofTest.java new file mode 100644 index 0000000..4f2daca --- /dev/null +++ b/mixer/src/test/java/meerkat/mixer/ZeroKnowledgeProofTest.java @@ -0,0 +1,105 @@ +package meerkat.mixer; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.crypto.concrete.ECElGamalEncryption; +import meerkat.crypto.mixnet.Mix2ZeroKnowledgeProver; +import meerkat.crypto.mixnet.Mix2ZeroKnowledgeVerifier; +import meerkat.mixer.prover.Prover; +import meerkat.mixer.verifier.Verifier; +import meerkat.protobuf.ConcreteCrypto; +import meerkat.protobuf.Crypto; +import meerkat.protobuf.Voting; +//import meerkat.protobuf.Voting.PlaintextBallot; +import org.bouncycastle.math.ec.ECPoint; +import org.factcenter.qilin.primitives.RandomOracle; +import org.factcenter.qilin.primitives.concrete.DigestOracle; +import org.factcenter.qilin.primitives.concrete.ECElGamal; +import org.factcenter.qilin.primitives.concrete.ECGroup; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.math.BigInteger; +import java.util.Random; + +/** + * Created by Tzlil on 12/31/2015. + */ +public class ZeroKnowledgeProofTest { + + Random rand; + ECElGamal.SK key; + ECGroup group; + ECElGamalEncryption enc; + ConcreteCrypto.ElGamalPublicKey serializedPk; + Mix2ZeroKnowledgeVerifier verifier ; + Mix2ZeroKnowledgeProver prover ; + + @Before + public void setup() throws Exception { + rand = new Random(); + group = new ECGroup("secp256k1"); + BigInteger sk = ECElGamal.generateSecretKey(group, rand); + key = new ECElGamal.SK(group, sk); + serializedPk = Utils.serializePk(group, key); + enc = new ECElGamalEncryption(); + enc.init(serializedPk); + RandomOracle randomOracle = new DigestOracle(); + verifier = new Verifier(enc,randomOracle); + prover = new Prover(new Random(),enc,randomOracle); + } + + private ECPoint convert2ECPoint(ByteString bs){ + return group.decode(bs.toByteArray()); + } + + public void oneZKPTest() throws InvalidProtocolBufferException { + Voting.PlaintextBallot msg1 = Utils.genRandomBallot(2,3,16); // 2 questions with 3 answers each, in range 0-15. + Voting.PlaintextBallot msg2 = Utils.genRandomBallot(2,3,16); + Crypto.EncryptionRandomness r1 = enc.generateRandomness(rand); + Crypto.EncryptionRandomness r2 = enc.generateRandomness(rand); + + Crypto.RerandomizableEncryptedMessage e1 = enc.encrypt(msg1, enc.generateRandomness(rand)); + Crypto.RerandomizableEncryptedMessage e2 = enc.encrypt(msg2, enc.generateRandomness(rand)); + Crypto.RerandomizableEncryptedMessage e1New = enc.rerandomize(e1, r1); + Crypto.RerandomizableEncryptedMessage e2New = enc.rerandomize(e2, r2); + + assertEquals (Utils.decrypt(Voting.PlaintextBallot.class, key, group, e1), msg1); + assertEquals (Utils.decrypt(Voting.PlaintextBallot.class, key, group, e1New), msg1); + assertEquals (Utils.decrypt(Voting.PlaintextBallot.class, key, group, e2), msg2); + assertEquals (Utils.decrypt(Voting.PlaintextBallot.class, key, group, e2New), msg2); + + ECPoint g = group.getGenerator(); + ECPoint h = enc.getElGamalPK().getPK(); + + ConcreteCrypto.ElGamalCiphertext e1ElGamal = ECElGamalEncryption.RerandomizableEncryptedMessage2ElGamalCiphertext(e1); + ConcreteCrypto.ElGamalCiphertext e2ElGamal = ECElGamalEncryption.RerandomizableEncryptedMessage2ElGamalCiphertext(e2); + ConcreteCrypto.ElGamalCiphertext e1TagElGamal = ECElGamalEncryption.RerandomizableEncryptedMessage2ElGamalCiphertext(e1New); + ConcreteCrypto.ElGamalCiphertext e2TagElGamal = ECElGamalEncryption.RerandomizableEncryptedMessage2ElGamalCiphertext(e2New); + + + assertEquals (g.multiply(enc.extractRandomness(r1)), + group.add(convert2ECPoint(e1TagElGamal.getC1()),group.negate(convert2ECPoint(e1ElGamal.getC1())))); + assertEquals (h.multiply(enc.extractRandomness(r1)), + group.add(convert2ECPoint(e1TagElGamal.getC2()),group.negate(convert2ECPoint(e1ElGamal.getC2())))); + assertEquals (g.multiply(enc.extractRandomness(r2)), + group.add(convert2ECPoint(e2TagElGamal.getC1()),group.negate(convert2ECPoint(e2ElGamal.getC1())))); + assertEquals (h.multiply(enc.extractRandomness(r2)), + group.add(convert2ECPoint(e2TagElGamal.getC2()),group.negate(convert2ECPoint(e2ElGamal.getC2())))); + + assertTrue (verifier.verify(e1,e2,e1New,e2New,prover.prove(e1,e2,e1New,e2New,false,0,0,0,r1,r2))); + } + + @Test + public void zeroKnowledgeProofTest() throws InvalidProtocolBufferException { + + int tests = 1000; + + for (int i = 0; i < tests; i ++){ + System.out.println("ZKP test #" + i); + oneZKPTest(); + } + } +} diff --git a/mixer/src/test/java/profiling/BigInteger/AddSub.java b/mixer/src/test/java/profiling/BigInteger/AddSub.java new file mode 100644 index 0000000..8e3b5c1 --- /dev/null +++ b/mixer/src/test/java/profiling/BigInteger/AddSub.java @@ -0,0 +1,63 @@ +package profiling.BigInteger; + +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.crypto.concrete.ECElGamalEncryption; +import meerkat.protobuf.ConcreteCrypto; +import meerkat.mixer.Utils; +import org.factcenter.qilin.primitives.concrete.ECElGamal; +import org.factcenter.qilin.primitives.concrete.ECGroup; + +import java.math.BigInteger; +import java.util.Random; + +/** + * Created by Tzlil on 1/21/2016. + */ +public class AddSub { + + int tests; + BigInteger[] randoms; + BigInteger[] randoms2; + + public void setup() throws Exception { + Random rand = new Random(); + tests = 1 << 17; + randoms = new BigInteger[tests]; + rand = new Random(); + + ECGroup group = new ECGroup("secp256k1"); + BigInteger sk = ECElGamal.generateSecretKey(group, rand); + ECElGamal.SK key = new ECElGamal.SK(group, sk); + ConcreteCrypto.ElGamalPublicKey serializedPk = Utils.serializePk(group, key); + ECElGamalEncryption enc = new ECElGamalEncryption(); + enc.init(serializedPk); + + for (int i =0 ; i < tests ; i++){ + randoms[i] = new BigInteger(enc.generateRandomness(rand).getData().toByteArray()); + } + randoms2 = new BigInteger[tests]; + for (int i =0 ; i < tests ; i++){ + randoms2[i] = new BigInteger(enc.generateRandomness(rand).getData().toByteArray()); + } + + } + + public void AddSubProfiling() throws InvalidProtocolBufferException { + + System.out.println("AddSub"); + System.out.println("#" + tests + " tests"); + long startTime = System.currentTimeMillis(); + int i = 0; + while (i < randoms.length / 2) { + randoms[i].add(randoms2[i]); + i++; + } + while (i < randoms.length) { + randoms[i].subtract(randoms2[i]); + i++; + } + long finishTime = System.currentTimeMillis(); + System.out.println(" that took: "+(finishTime-startTime)+ " ms"); + System.out.println(" avg of "+((double)(finishTime-startTime))/tests + " ms"); + } +} diff --git a/mixer/src/test/java/profiling/BigInteger/GenerateRandomness.java b/mixer/src/test/java/profiling/BigInteger/GenerateRandomness.java new file mode 100644 index 0000000..ebe80f0 --- /dev/null +++ b/mixer/src/test/java/profiling/BigInteger/GenerateRandomness.java @@ -0,0 +1,46 @@ +package profiling.BigInteger; + +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.crypto.concrete.ECElGamalEncryption; +import meerkat.protobuf.ConcreteCrypto; +import meerkat.mixer.Utils; +import org.factcenter.qilin.primitives.concrete.ECElGamal; +import org.factcenter.qilin.primitives.concrete.ECGroup; + +import java.math.BigInteger; +import java.util.Random; + +/** + * Created by Tzlil on 1/25/2016. + */ +public class GenerateRandomness { + int tests; + ECElGamalEncryption enc; + Random rand; + + public void setup() throws Exception { + rand = new Random(); + ECGroup group = new ECGroup("secp256k1"); + tests = 1<<18; + + BigInteger sk = ECElGamal.generateSecretKey(group, rand); + ECElGamal.SK key = new ECElGamal.SK(group, sk); + ConcreteCrypto.ElGamalPublicKey serializedPk = Utils.serializePk(group, key); + enc = new ECElGamalEncryption(); + enc.init(serializedPk); + + } + + public void GenerateRandomnessProfiling() throws InvalidProtocolBufferException { + + System.out.println("GenerateRandomnessProfiling"); + System.out.println("#" + tests + " tests"); + long startTime = System.currentTimeMillis(); + for (int i =0 ; i < tests ; i++){ + enc.generateRandomness(rand).getData().toByteArray(); + } + long finishTime = System.currentTimeMillis(); + System.out.println(" that took: "+(finishTime-startTime)+ " ms"); + System.out.println(" avg of "+((double)(finishTime-startTime))/tests + " ms"); + } +} diff --git a/mixer/src/test/java/profiling/BigInteger/Modulo.java b/mixer/src/test/java/profiling/BigInteger/Modulo.java new file mode 100644 index 0000000..d0149b0 --- /dev/null +++ b/mixer/src/test/java/profiling/BigInteger/Modulo.java @@ -0,0 +1,50 @@ +package profiling.BigInteger; + +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.crypto.concrete.ECElGamalEncryption; +import meerkat.protobuf.ConcreteCrypto; +import meerkat.mixer.Utils; +import org.factcenter.qilin.primitives.concrete.ECElGamal; +import org.factcenter.qilin.primitives.concrete.ECGroup; + +import java.math.BigInteger; +import java.util.Random; + +/** + * Created by Tzlil on 1/21/2016. + */ +public class Modulo { + int tests; + BigInteger[] randoms; + BigInteger orderUpperBound; + + public void setup() throws Exception { + Random rand = new Random(); + ECGroup group = new ECGroup("secp256k1"); + orderUpperBound = group.orderUpperBound(); + tests = 1<<17; + randoms = new BigInteger[tests]; + + BigInteger sk = ECElGamal.generateSecretKey(group, rand); + ECElGamal.SK key = new ECElGamal.SK(group, sk); + ConcreteCrypto.ElGamalPublicKey serializedPk = Utils.serializePk(group, key); + ECElGamalEncryption enc = new ECElGamalEncryption(); + enc.init(serializedPk); + for (int i =0 ; i < tests ; i++){ + randoms[i] = new BigInteger(enc.generateRandomness(rand).getData().toByteArray()); + } + } + + public void ModuloProfiling() throws InvalidProtocolBufferException { + + System.out.println("ModuloProfiling"); + System.out.println("#" + tests + " tests"); + long startTime = System.currentTimeMillis(); + for (int i = 0 ; i < randoms.length;i++) { + randoms[i].mod(orderUpperBound); + } + long finishTime = System.currentTimeMillis(); + System.out.println(" that took: "+(finishTime-startTime)+ " ms"); + System.out.println(" avg of "+((double)(finishTime-startTime))/tests + " ms"); + } +} diff --git a/mixer/src/test/java/profiling/BigInteger/Mul.java b/mixer/src/test/java/profiling/BigInteger/Mul.java new file mode 100644 index 0000000..4ac913a --- /dev/null +++ b/mixer/src/test/java/profiling/BigInteger/Mul.java @@ -0,0 +1,59 @@ +package profiling.BigInteger; + +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.crypto.concrete.ECElGamalEncryption; +import meerkat.protobuf.ConcreteCrypto; +import meerkat.mixer.Utils; +import org.factcenter.qilin.primitives.concrete.ECElGamal; +import org.factcenter.qilin.primitives.concrete.ECGroup; + +import java.math.BigInteger; +import java.util.Random; + +/** + * Created by Tzlil on 1/25/2016. + */ +public class Mul { + + int tests; + BigInteger[] randoms; + BigInteger[] randoms2; + + public void setup() throws Exception { + Random rand = new Random(); + tests = 1 << 17; + randoms = new BigInteger[tests]; + rand = new Random(); + + ECGroup group = new ECGroup("secp256k1"); + BigInteger sk = ECElGamal.generateSecretKey(group, rand); + ECElGamal.SK key = new ECElGamal.SK(group, sk); + ConcreteCrypto.ElGamalPublicKey serializedPk = Utils.serializePk(group, key); + ECElGamalEncryption enc = new ECElGamalEncryption(); + enc.init(serializedPk); + + for (int i =0 ; i < tests ; i++){ + randoms[i] = new BigInteger(enc.generateRandomness(rand).getData().toByteArray()); + } + randoms2 = new BigInteger[tests]; + for (int i =0 ; i < tests ; i++){ + randoms2[i] = new BigInteger(enc.generateRandomness(rand).getData().toByteArray()); + } + + } + + public void MulProfiling() throws InvalidProtocolBufferException { + + System.out.println("Mul"); + System.out.println("#" + tests + " tests"); + long startTime = System.currentTimeMillis(); + int i = 0; + while (i < randoms.length) { + randoms[i].multiply(randoms2[i]); + i++; + } + long finishTime = System.currentTimeMillis(); + System.out.println(" that took: "+(finishTime-startTime)+ " ms"); + System.out.println(" avg of "+((double)(finishTime-startTime))/tests + " ms"); + } +} diff --git a/mixer/src/test/java/profiling/BigInteger/SHA256.java b/mixer/src/test/java/profiling/BigInteger/SHA256.java new file mode 100644 index 0000000..0db8279 --- /dev/null +++ b/mixer/src/test/java/profiling/BigInteger/SHA256.java @@ -0,0 +1,46 @@ +package profiling.BigInteger; + +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.protobuf.Voting; +import org.factcenter.qilin.primitives.RandomOracle; +import org.factcenter.qilin.primitives.concrete.DigestOracle; + +import java.math.BigInteger; +import java.util.Random; + +/** + * Created by Tzlil on 1/25/2016. + */ +public class SHA256 { + final int LENGTH = 420; + int tests; + BigInteger[] randoms; + RandomOracle randomOracle; + + public void setup() throws Exception { + Random rand = new Random(); + randomOracle = new DigestOracle(); + tests = 1<<15; + Voting.PlaintextBallot msg; + randoms = new BigInteger[tests]; + byte[] arr = new byte[LENGTH]; + for (int i =0 ; i < tests ; i++){ + rand.nextBytes(arr); + randoms[i] = new BigInteger(arr); + } + } + + public void SHA256Profiling() throws InvalidProtocolBufferException { + + System.out.println("SHA256Profiling"); + System.out.println("#" + tests + " tests"); + long startTime = System.currentTimeMillis(); + for (int i = 0 ; i < randoms.length;i++) { + byte[] arr = randoms[i].toByteArray(); + new BigInteger(this.randomOracle.hash(arr, arr.length)); + } + long finishTime = System.currentTimeMillis(); + System.out.println(" that took: "+(finishTime-startTime)+ " ms"); + System.out.println(" avg of "+((double)(finishTime-startTime))/tests + " ms"); + } +} diff --git a/mixer/src/test/java/profiling/Convert/ByteString2ECPoint.java b/mixer/src/test/java/profiling/Convert/ByteString2ECPoint.java new file mode 100644 index 0000000..f9b5b2e --- /dev/null +++ b/mixer/src/test/java/profiling/Convert/ByteString2ECPoint.java @@ -0,0 +1,68 @@ +package profiling.Convert; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.crypto.concrete.ECElGamalEncryption; +import meerkat.protobuf.ConcreteCrypto; +import meerkat.protobuf.Voting; +import meerkat.mixer.Utils; +import org.bouncycastle.math.ec.ECPoint; +import org.factcenter.qilin.primitives.concrete.ECElGamal; +import org.factcenter.qilin.primitives.concrete.ECGroup; + +import java.math.BigInteger; +import java.util.Random; + +/** + * Created by Tzlil on 1/25/2016. + */ +public class ByteString2ECPoint { + Random rand; + ECElGamal.SK key; + ECGroup group; + ECElGamalEncryption enc; + ConcreteCrypto.ElGamalPublicKey serializedPk; + int tests; + ConcreteCrypto.ElGamalCiphertext[] encryptedMessage; + + public void setup() throws Exception { + rand = new Random(); + group = new ECGroup("secp256k1"); + BigInteger sk = ECElGamal.generateSecretKey(group, rand); + key = new ECElGamal.SK(group, sk); + serializedPk = Utils.serializePk(group, key); + enc = new ECElGamalEncryption(); + enc.init(serializedPk); + tests = 1024 * 19; + encryptedMessage = new ConcreteCrypto.ElGamalCiphertext[tests]; + + Voting.PlaintextBallot msg; + + for (int i = 0; i < tests; i ++){ + msg = Utils.genRandomBallot(2,3,16); + + encryptedMessage[i] = ECElGamalEncryption.RerandomizableEncryptedMessage2ElGamalCiphertext + (enc.encrypt(msg, enc.generateRandomness(rand))); + } + } + private ECPoint convert2ECPoint(ByteString bs){ + return group.decode(bs.toByteArray()); + } + + public void ByteString2ECPointProfiling() throws InvalidProtocolBufferException { + + System.out.println("ByteString2ECPointProfiling"); + System.out.println("#"+ tests + " tests"); + System.out.println("start tests operations"); + long startTime = System.currentTimeMillis(); + + for (int i = 0; i < tests; i ++){ + convert2ECPoint(encryptedMessage[i].getC1()); + convert2ECPoint(encryptedMessage[i].getC2()); + } + + long finishTime = System.currentTimeMillis(); + System.out.println(" that took: "+(finishTime-startTime)+ " ms"); + System.out.println(" avg of "+((double)(finishTime-startTime))/ (2 * tests) + " ms"); + } +} diff --git a/mixer/src/test/java/profiling/Convert/RerandomizableEncryptedMessage2ElGamalCiphertext.java b/mixer/src/test/java/profiling/Convert/RerandomizableEncryptedMessage2ElGamalCiphertext.java new file mode 100644 index 0000000..3d7435f --- /dev/null +++ b/mixer/src/test/java/profiling/Convert/RerandomizableEncryptedMessage2ElGamalCiphertext.java @@ -0,0 +1,62 @@ +package profiling.Convert; + +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.crypto.concrete.ECElGamalEncryption; +import meerkat.protobuf.ConcreteCrypto; +import meerkat.protobuf.Crypto; +import meerkat.protobuf.Voting; +import meerkat.mixer.Utils; +import org.factcenter.qilin.primitives.concrete.ECElGamal; +import org.factcenter.qilin.primitives.concrete.ECGroup; + +import java.math.BigInteger; +import java.util.Random; + +/** + * Created by Tzlil on 1/25/2016. + */ +public class RerandomizableEncryptedMessage2ElGamalCiphertext { + Random rand; + ECElGamal.SK key; + ECGroup group; + ECElGamalEncryption enc; + ConcreteCrypto.ElGamalPublicKey serializedPk; + int tests; + Crypto.RerandomizableEncryptedMessage[] encryptedMessage; + + public void setup() throws Exception { + rand = new Random(); + group = new ECGroup("secp256k1"); + BigInteger sk = ECElGamal.generateSecretKey(group, rand); + key = new ECElGamal.SK(group, sk); + serializedPk = Utils.serializePk(group, key); + enc = new ECElGamalEncryption(); + enc.init(serializedPk); + tests = 1024 * 18; + encryptedMessage = new Crypto.RerandomizableEncryptedMessage[tests]; + + Voting.PlaintextBallot msg; + + for (int i = 0; i < tests; i ++){ + msg = Utils.genRandomBallot(2,3,16); + + encryptedMessage[i] = enc.encrypt(msg, enc.generateRandomness(rand)); + } + } + + public void RerandomizableEncryptedMessage2ElGamalCiphertext() throws InvalidProtocolBufferException { + + System.out.println("RerandomizableEncryptedMessage2ElGamalCiphertext"); + System.out.println("#"+ tests + " tests"); + System.out.println("start tests operations"); + long startTime = System.currentTimeMillis(); + + for (int i = 0; i < tests; i ++){ + ECElGamalEncryption.RerandomizableEncryptedMessage2ElGamalCiphertext(encryptedMessage[i]); + } + + long finishTime = System.currentTimeMillis(); + System.out.println(" that took: "+(finishTime-startTime)+ " ms"); + System.out.println(" avg of "+((double)(finishTime-startTime))/ tests + " ms"); + } +} diff --git a/mixer/src/test/java/profiling/ECGroup/Add.java b/mixer/src/test/java/profiling/ECGroup/Add.java new file mode 100644 index 0000000..e0b5406 --- /dev/null +++ b/mixer/src/test/java/profiling/ECGroup/Add.java @@ -0,0 +1,61 @@ +package profiling.ECGroup; + +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.crypto.concrete.ECElGamalEncryption; +import meerkat.mixer.Utils; +import org.bouncycastle.math.ec.ECPoint; +import org.factcenter.qilin.primitives.concrete.ECElGamal; +import org.factcenter.qilin.primitives.concrete.ECGroup; + +import java.security.spec.InvalidKeySpecException; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * Created by Tzlil on 1/20/2016. + */ +public class Add { + + ECElGamalEncryption encryptor; + ECGroup group; + Random random; + private int n; + List members1; + List members2; + + public void setup() throws InvalidKeySpecException { + // initialization + random = new Random(); + group = new ECGroup("secp256k1"); + encryptor = new ECElGamalEncryption(); + encryptor.init(Utils.serializePk(group, new ECElGamal.SK(group, ECElGamal.generateSecretKey(group, random)))); + // generate n; + int sqrtn = 128; + n = sqrtn*sqrtn; + + members1 = new ArrayList(sqrtn); + members2 = new ArrayList(sqrtn); + + for (int i = 0 ; i < sqrtn; i ++){ + members1.add(group.sample(random)); + members2.add(group.sample(random)); + } + } + + public void addProfiling() throws InvalidProtocolBufferException { + System.out.println("AddSub"); + System.out.println("n is : " + n); + System.out.println("start n operations"); + long startTime = System.currentTimeMillis(); + for (ECPoint member1: members1) { + for (ECPoint member2: members2) { + group.add(member1,member2); + } + } + + long finishTime = System.currentTimeMillis(); + System.out.println(" that took: "+(finishTime-startTime)+ " ms"); + System.out.println(" avg of"+((double)(finishTime-startTime))/(n)+ " ms"); + } +} diff --git a/mixer/src/test/java/profiling/ECGroup/Encode.java b/mixer/src/test/java/profiling/ECGroup/Encode.java new file mode 100644 index 0000000..59201e3 --- /dev/null +++ b/mixer/src/test/java/profiling/ECGroup/Encode.java @@ -0,0 +1,57 @@ +package profiling.ECGroup; + +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.crypto.concrete.ECElGamalEncryption; +import meerkat.mixer.Utils; +import org.bouncycastle.math.ec.ECPoint; +import org.factcenter.qilin.primitives.concrete.ECElGamal; +import org.factcenter.qilin.primitives.concrete.ECGroup; + +import java.security.spec.InvalidKeySpecException; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * Created by Tzlil on 1/20/2016. + */ +public class Encode { + + ECElGamalEncryption encryptor; + ECGroup group; + Random random; + private int n; + List members; + + + public void setup() throws InvalidKeySpecException { + // initialization + random = new Random(); + group = new ECGroup("secp256k1"); + encryptor = new ECElGamalEncryption(); + encryptor.init(Utils.serializePk(group, new ECElGamal.SK(group, ECElGamal.generateSecretKey(group, random)))); + // generate n; + int sqrtn = 128; + n = sqrtn*sqrtn; + + members = new ArrayList(n); + for (int i = 0 ; i < n; i ++){ + members.add(group.sample(random)); + } + } + + public void encodeProfiling() throws InvalidProtocolBufferException { + System.out.println("Encode"); + System.out.println("n is : " + n); + + System.out.println("start n operations"); + long startTime = System.currentTimeMillis(); + for (ECPoint member: members) { + group.encode(member); + } + + long finishTime = System.currentTimeMillis(); + System.out.println(" that took: "+(finishTime-startTime)+ " ms"); + System.out.println(" avg of"+((double)(finishTime-startTime))/(n)+ " ms"); + } +} diff --git a/mixer/src/test/java/profiling/ECGroup/Mul.java b/mixer/src/test/java/profiling/ECGroup/Mul.java new file mode 100644 index 0000000..836db0e --- /dev/null +++ b/mixer/src/test/java/profiling/ECGroup/Mul.java @@ -0,0 +1,69 @@ +package profiling.ECGroup; + +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.crypto.concrete.ECElGamalEncryption; +import meerkat.mixer.Utils; +import org.bouncycastle.math.ec.ECPoint; +import org.factcenter.qilin.primitives.concrete.ECElGamal; +import org.factcenter.qilin.primitives.concrete.ECGroup; + +import java.math.BigInteger; +import java.security.spec.InvalidKeySpecException; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * Created by Tzlil on 1/19/2016. + */ +public class Mul { + + ECElGamalEncryption encryptor; + ECGroup group; + Random random; + private int n; + List members; + List randomnesses; + + public void setup() throws InvalidKeySpecException { + // initialization + random = new Random(); + group = new ECGroup("secp256k1"); + encryptor = new ECElGamalEncryption(); + encryptor.init(Utils.serializePk(group, new ECElGamal.SK(group, ECElGamal.generateSecretKey(group, random)))); + // generate n + int sqrtn = 128; + n = sqrtn*sqrtn; + + members = new ArrayList(sqrtn); + randomnesses = new ArrayList(sqrtn); + + byte[] arr = new byte[256]; + for (int i = 0 ; i < sqrtn; i ++){ + members.add(group.sample(random)); + random.nextBytes(arr); + randomnesses.add(new BigInteger(arr)); + } + } + + public void mulProfiling() throws InvalidProtocolBufferException { + System.out.println("Multiply"); + System.out.println("n is : " + n); + + System.out.println("start n operations"); + long startTime = System.currentTimeMillis(); + for(ECPoint member:members) { + for (BigInteger rand : randomnesses) { + group.multiply(member, rand); + + } + } + + long finishTime = System.currentTimeMillis(); + System.out.println(" that took: "+(finishTime-startTime)+ " ms"); + System.out.println(" avg of"+((double)(finishTime-startTime))/(n)+ " ms"); + } + + + +} diff --git a/mixer/src/test/java/profiling/ECGroup/Negate.java b/mixer/src/test/java/profiling/ECGroup/Negate.java new file mode 100644 index 0000000..d190186 --- /dev/null +++ b/mixer/src/test/java/profiling/ECGroup/Negate.java @@ -0,0 +1,56 @@ +package profiling.ECGroup; + +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.crypto.concrete.ECElGamalEncryption; +import meerkat.mixer.Utils; +import org.bouncycastle.math.ec.ECPoint; +import org.factcenter.qilin.primitives.concrete.ECElGamal; +import org.factcenter.qilin.primitives.concrete.ECGroup; + +import java.security.spec.InvalidKeySpecException; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * Created by Tzlil on 1/20/2016. + */ +public class Negate { + + ECElGamalEncryption encryptor; + ECGroup group; + Random random; + private int n; + List members; + + public void setup() throws InvalidKeySpecException { + // initialization + random = new Random(); + group = new ECGroup("secp256k1"); + encryptor = new ECElGamalEncryption(); + encryptor.init(Utils.serializePk(group, new ECElGamal.SK(group, ECElGamal.generateSecretKey(group, random)))); + // generate n; + int sqrtn = 128; + n = sqrtn*sqrtn; + + members = new ArrayList(n); + for (int i = 0 ; i < n; i ++){ + members.add(group.sample(random)); + } + } + + public void negProfiling() throws InvalidProtocolBufferException { + System.out.println("Neg"); + System.out.println("n is : " + n); + + System.out.println("start n operations"); + long startTime = System.currentTimeMillis(); + for (ECPoint member: members) { + group.negate(member); + } + + long finishTime = System.currentTimeMillis(); + System.out.println(" that took: "+(finishTime-startTime)+ " ms"); + System.out.println(" avg of"+((double)(finishTime-startTime))/(n)+ " ms"); + } +} diff --git a/mixer/src/test/java/profiling/Rerandomize.java b/mixer/src/test/java/profiling/Rerandomize.java new file mode 100644 index 0000000..5a113c9 --- /dev/null +++ b/mixer/src/test/java/profiling/Rerandomize.java @@ -0,0 +1,67 @@ +package profiling; + +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.crypto.concrete.ECElGamalEncryption; +import meerkat.protobuf.ConcreteCrypto; +import meerkat.protobuf.Crypto; +import meerkat.protobuf.Voting; +import meerkat.mixer.Utils; +import org.factcenter.qilin.primitives.concrete.ECElGamal; +import org.factcenter.qilin.primitives.concrete.ECGroup; + +import java.math.BigInteger; +import java.util.Random; + +/** + * Created by Tzlil on 1/20/2016. + */ +public class Rerandomize { + + Random rand; + ECElGamal.SK key; + ECGroup group; + ECElGamalEncryption enc; + ConcreteCrypto.ElGamalPublicKey serializedPk; + int n; + Crypto.EncryptionRandomness[] randomnesses; + Crypto.RerandomizableEncryptedMessage[] encryptedMessage; + + public void setup() throws Exception { + rand = new Random(); + group = new ECGroup("secp256k1"); + BigInteger sk = ECElGamal.generateSecretKey(group, rand); + key = new ECElGamal.SK(group, sk); + serializedPk = Utils.serializePk(group, key); + enc = new ECElGamalEncryption(); + enc.init(serializedPk); + int LogVotes = 10; + int layers = 2*LogVotes - 1; + n = layers * (1< { InputStream entityStream) throws IOException, WebApplicationException { try { Method newBuilder = type.getMethod("newBuilder"); - GeneratedMessage.Builder builder = (GeneratedMessage.Builder) newBuilder.invoke(type); + Message.Builder builder = (Message.Builder) newBuilder.invoke(type); return builder.mergeFrom(entityStream).build(); } catch (Exception e) { throw new WebApplicationException(e); diff --git a/settings.gradle b/settings.gradle index 851bfc3..542a831 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,6 +3,6 @@ include 'voting-booth' include 'bulletin-board-server' include 'polling-station' include 'restful-api-common' +include 'mixer' include 'bulletin-board-client' include 'distributed-key-generation' - diff --git a/settings.gradle.orig b/settings.gradle.orig new file mode 100644 index 0000000..a5cec3b --- /dev/null +++ b/settings.gradle.orig @@ -0,0 +1,11 @@ +include 'meerkat-common' +include 'voting-booth' +include 'bulletin-board-server' +include 'polling-station' +include 'restful-api-common' +<<<<<<< HEAD +include 'mixer' +======= +include 'bulletin-board-client' + +>>>>>>> e8e511d9ce636a127bb33d70ebfd9b2f230c6e1d diff --git a/voting-booth/build.gradle b/voting-booth/build.gradle index 70d7340..544f0b4 100644 --- a/voting-booth/build.gradle +++ b/voting-booth/build.gradle @@ -1,4 +1,3 @@ - plugins { id "us.kirchmeier.capsule" version "1.0.1" id 'com.google.protobuf' version '0.7.0' @@ -40,6 +39,15 @@ version += "${isSnapshot ? '-SNAPSHOT' : ''}" dependencies { // Meerkat common compile project(':meerkat-common') + compile project(':restful-api-common') + + // Jersey for RESTful API + compile 'org.glassfish.jersey.containers:jersey-container-servlet:2.5.+' + + // Servlets + compile group: 'org.eclipse.jetty', name: 'jetty-server', version: '9.3.+' + compile 'org.eclipse.jetty:jetty-server:9.3.+' + compile 'org.eclipse.jetty:jetty-servlet:9.3.+' // Logging compile 'org.slf4j:slf4j-api:1.7.7' @@ -52,6 +60,10 @@ dependencies { testCompile 'junit:junit:4.+' runtime 'org.codehaus.groovy:groovy:2.4.+' + + // Meerkat polling station + compile project(':polling-station') + } @@ -188,6 +200,3 @@ publishing { } } } - - - diff --git a/voting-booth/src/main/java/meerkat/voting/ToyEncryption.java b/voting-booth/src/main/java/meerkat/voting/ToyEncryption.java new file mode 100644 index 0000000..e0089c8 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ToyEncryption.java @@ -0,0 +1,46 @@ +package meerkat.voting; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Message; +import meerkat.crypto.Encryption; +import meerkat.protobuf.Crypto.*; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Random; + +/** + * Created by hai on 07/06/16. + */ +public class ToyEncryption implements Encryption { + + @Override + public RerandomizableEncryptedMessage encrypt(Message plaintext, EncryptionRandomness rnd) throws IOException { + ByteString cipher = ByteString.copyFromUtf8("Encryption(") + .concat(plaintext.toByteString()) + .concat(ByteString.copyFromUtf8(", Random(")) + .concat(rnd.getData()) + .concat(ByteString.copyFromUtf8("))")); + return RerandomizableEncryptedMessage.newBuilder() + .setData(cipher) + .build(); + } + + @Override + public RerandomizableEncryptedMessage rerandomize + (RerandomizableEncryptedMessage msg, EncryptionRandomness rnd) throws InvalidProtocolBufferException { + throw new UnsupportedOperationException(); + } + + @Override + public EncryptionRandomness generateRandomness(Random rand) { + ByteBuffer b = ByteBuffer.allocate(4); + b.putInt(rand.nextInt()); + byte[] bArr = b.array(); + ByteString bs = ByteString.copyFrom(bArr); + return EncryptionRandomness.newBuilder() + .setData(bs) + .build(); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/ToySignature.java b/voting-booth/src/main/java/meerkat/voting/ToySignature.java new file mode 100644 index 0000000..89bfc58 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ToySignature.java @@ -0,0 +1,90 @@ +package meerkat.voting; + +import com.google.protobuf.ByteString; +import com.google.protobuf.Message; +import meerkat.crypto.DigitalSignature; +import meerkat.protobuf.Crypto.*; +import meerkat.protobuf.Crypto.Signature; + +import java.io.IOException; +import java.io.InputStream; +import java.security.*; +import java.security.cert.CertificateException; + +/** + * Created by hai on 07/06/16. + */ +public class ToySignature implements DigitalSignature { + + private final ByteString signerID; + private ByteString msgByteString; + + + public ToySignature(String signerID) { + this.signerID = ByteString.copyFromUtf8(signerID); + } + + @Override + public ByteString getSignerID() { + return signerID; + } + + @Override + public void updateContent(Message msg) throws SignatureException { + msgByteString = msg.toByteString(); + } + + @Override + public Signature sign() throws SignatureException { + ByteString signature = ByteString.copyFromUtf8("Signature(") + .concat(msgByteString) + .concat(ByteString.copyFromUtf8(")")); + return Signature.newBuilder() + .setType(SignatureType.ECDSA) + .setData(signature) + .setSignerId(getSignerID()) + .build(); + } + + + @Override + public void loadVerificationCertificates(InputStream certStream) throws CertificateException { + throw new UnsupportedOperationException(); + } + + @Override + public void clearVerificationCertificates() { + throw new UnsupportedOperationException(); + } + + @Override + public void updateContent(byte[] data) throws SignatureException { + throw new UnsupportedOperationException(); + } + + @Override + public void initVerify(Signature sig) throws CertificateException, InvalidKeyException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean verify() { + throw new UnsupportedOperationException(); + } + + @Override + public KeyStore.Builder getPKCS12KeyStoreBuilder(InputStream keyStream, char[] password) throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException { + throw new UnsupportedOperationException(); + } + + @Override + public void loadSigningCertificate(KeyStore.Builder keyStoreBuilder) throws IOException, CertificateException, UnrecoverableKeyException { + throw new UnsupportedOperationException(); + } + + + @Override + public void clearSigningKey() { + throw new UnsupportedOperationException(); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/VotingBoothToyRun.java b/voting-booth/src/main/java/meerkat/voting/VotingBoothToyRun.java new file mode 100644 index 0000000..65b6685 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/VotingBoothToyRun.java @@ -0,0 +1,290 @@ +package meerkat.voting; + +import com.google.protobuf.ByteString; +import meerkat.crypto.DigitalSignature; +import meerkat.crypto.Encryption; +import meerkat.protobuf.Voting.*; +import meerkat.voting.controller.*; +import meerkat.voting.output.*; +import meerkat.voting.storage.*; +import meerkat.voting.encryptor.*; +import meerkat.voting.ui.*; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.*; + +/** + * Created by hai on 26/04/16. + */ +public class VotingBoothToyRun { + + public static void main(String[] args) { + + try { + generateSystemMessages(); + generateDemoQuestions(); + } + catch (Exception e) { + return; + } + + Random rand = new Random(); + Encryption enc = new ToyEncryption(); + DigitalSignature sig = new ToySignature("MY_SIGNER_ID"); + + StorageManager storageManager = new StorageManagerMockup(); + SystemConsoleOutputDevice outputDevice = new SystemConsoleOutputDevice(); + VBCryptoManager cryptoManager = new VBCryptoManagerImpl(rand, enc, sig); + SystemConsoleUI ui = new SystemConsoleUI (); + + + VotingBoothImpl controller = new VotingBoothImpl(); + + try { + controller.init(outputDevice, cryptoManager, ui, storageManager); + } + catch (Exception e) { + System.err.println("init failed"); + return; + } + + + // create threads + + + Thread controllerThread = new Thread(controller); + controllerThread.setName("Meerkat VB-Controller Thread"); + Thread uiThread = new Thread(ui); + uiThread.setName("Meerkat VB-UI Thread"); + Thread outputThread = new Thread(outputDevice); + outputThread.setName("Meerkat VB-Output Thread"); + + uiThread.start(); + controllerThread.start(); + outputThread.start(); + + } + + + private static void generateDemoQuestions() throws IOException { + + ElectionParams electionParams = ElectionParams.newBuilder() + .addAllRaceQuestions(generateBallotQuestions()) + .addAllChannelChoiceQuestions(generateChannelChoiceQuestions()) + .setSelectionData(generateSelectionData()) + .build(); + + try { + FileOutputStream output = new FileOutputStream(StorageManagerMockup.electionParamFullFilename); + electionParams.writeTo(output); + output.close(); + System.out.println("Successfully wrote election parameter protobuf to a file"); + } + catch (IOException e) { + System.err.println("Could not write to the election parameter file: '" + StorageManagerMockup.electionParamFullFilename + "'."); + throw e; + } + + } + + + private static List generateChannelChoiceQuestions() { + ArrayList channelChoiceQuestions = new ArrayList(); + + String[] ans1 = {"Red", "Blue", "Green"}; + BallotQuestion ccquestion1 = generateBallotQuestion("What is your favorite color?", "Pick one answer", ans1); + channelChoiceQuestions.add(ccquestion1); + + String[] ans2 = {"Yes", "No"}; + BallotQuestion ccquestion2 = generateBallotQuestion("Are you a republican?", "Pick one answer", ans2); + channelChoiceQuestions.add(ccquestion2); + + return channelChoiceQuestions; + } + + + private static List generateBallotQuestions() { + ArrayList allBallotQuestions = new ArrayList(); + + String[] answers1 = {"answer 1", "answer 2", "answer 3", "answer 4"}; + allBallotQuestions.add(generateBallotQuestion("question 1. Asking something...", "Pick one answer", answers1)); + + String[] answers2 = {"Miranda Kerr", "Doutzen Kroes", "Moran Atias", "Roslana Rodina", "Adriana Lima"}; + allBallotQuestions.add(generateBallotQuestion("question 2: Which model do you like", "Mark as many as you want", answers2)); + + allBallotQuestions.add(generateBallotQuestion("question 3. Asking something...", "Pick one answer", answers1)); + allBallotQuestions.add(generateBallotQuestion("question 4. Asking something...", "Pick one answer", answers1)); + + String[] answers5 = {"Clint Eastwood", "Ninja", "Sonic", "Tai-chi", "Diablo", "Keanu"}; + allBallotQuestions.add(generateBallotQuestion("question 5: Good name for a cat", "Pick the best one", answers5)); + + allBallotQuestions.add(generateBallotQuestion("question 6. Asking something...", "Pick one answer", answers1)); + allBallotQuestions.add(generateBallotQuestion("question 7. Asking something...", "Pick one answer", answers1)); + allBallotQuestions.add(generateBallotQuestion("question 8. Asking something...", "Pick one answer", answers1)); + allBallotQuestions.add(generateBallotQuestion("question 9. Asking something...", "Pick one answer", answers1)); + allBallotQuestions.add(generateBallotQuestion("question 10. Asking something...", "Pick one answer", answers1)); + allBallotQuestions.add(generateBallotQuestion("question 11. Asking something...", "Pick one answer", answers1)); + allBallotQuestions.add(generateBallotQuestion("question 12. Asking something...", "Pick one answer", answers1)); + allBallotQuestions.add(generateBallotQuestion("question 13. Asking something...", "Pick one answer", answers1)); + allBallotQuestions.add(generateBallotQuestion("question 14. Asking something...", "Pick one answer", answers1)); + + return allBallotQuestions; + } + + + private static BallotQuestion generateBallotQuestion(String questionStr, String descriptionStr, String[] answers) { + UIElement question = UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(stringToBytes(questionStr)) + .build(); + + UIElement description = UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(stringToBytes(descriptionStr)) + .build(); + + BallotQuestion.Builder bqb = BallotQuestion.newBuilder(); + bqb.setIsMandatory(false); + bqb.setQuestion(question); + bqb.setDescription(description); + for (String answerStr : answers) { + UIElement answer = UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(stringToBytes(answerStr)) + .build(); + bqb.addAnswer(answer); + } + + return bqb.build(); + } + + + private static SimpleCategoriesSelectionData generateSelectionData() { + Category sharedDefaults = Category.newBuilder() + .addQuestionIndex(0) + .addQuestionIndex(5) + .addQuestionIndex(9) + .build(); + + Category cat00 = Category.newBuilder() + .addQuestionIndex(1) + .addQuestionIndex(4) + .addQuestionIndex(6) + .addQuestionIndex(7) + .build(); + + Category cat01 = Category.newBuilder() + .addQuestionIndex(2) + .addQuestionIndex(4) + .addQuestionIndex(8) + .build(); + + Category cat02 = Category.newBuilder() + .addQuestionIndex(3) + .addQuestionIndex(8) + .build(); + + Category cat10 = Category.newBuilder() + .addQuestionIndex(10) + .addQuestionIndex(11) + .build(); + + Category cat11 = Category.newBuilder() + .addQuestionIndex(12) + .addQuestionIndex(13) + .build(); + + CategoryChooser catChooser0 = CategoryChooser.newBuilder() + .addCategory(cat00) + .addCategory(cat01) + .addCategory(cat02) + .build(); + + CategoryChooser catChooser1 = CategoryChooser.newBuilder() + .addCategory(cat10) + .addCategory(cat11) + .build(); + + return SimpleCategoriesSelectionData.newBuilder() + .setSharedDefaults(sharedDefaults) + .addCategoryChooser(catChooser0) + .addCategoryChooser(catChooser1) + .build(); + + } + + + + private static ByteString stringToBytes (String s) { + return ByteString.copyFromUtf8(s); + } + + + private static void generateSystemMessages() throws IOException{ + Map systemMessageMap = new HashMap(); + + systemMessageMap.put(StorageManager.WAIT_FOR_COMMIT_MESSAGE, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Please wait while committing to ballot")) + .build()); + systemMessageMap.put(StorageManager.WAIT_FOR_AUDIT_MESSAGE, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Please wait while auditing your ballot")) + .build()); + systemMessageMap.put(StorageManager.WAIT_FOR_CAST_MESSAGE, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Please wait while finalizing your ballot for voting")) + .build()); + systemMessageMap.put(StorageManager.RESTART_VOTING_BUTTON, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Restart voting")) + .build()); + systemMessageMap.put(StorageManager.UNRECOGNIZED_FINALIZE_RESPONSE_MESSAGE, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Could not understand response for Cast or Audit. Force restarting.")) + .build()); + systemMessageMap.put(StorageManager.UNSUCCESSFUL_CHANNEL_CHOICE_MESSAGE, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Choice of channel was unsuccessful. Force restarting.")) + .build()); + systemMessageMap.put(StorageManager.OUTPUT_DEVICE_FAILURE_MESSAGE, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Ballot output device failure. Force restarting.")) + .build()); + systemMessageMap.put(StorageManager.UNSUCCESSFUL_VOTING_MESSAGE, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Voting was unsuccessful. Force restarting.")) + .build()); + systemMessageMap.put(StorageManager.SOMETHING_WRONG_MESSAGE, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Something was terribly wrong. Force restarting.")) + .build()); + systemMessageMap.put(StorageManager.ENCRYPTION_FAILED_MESSAGE, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Encryption failed for some unknown reason.")) + .build()); + systemMessageMap.put(StorageManager.RETRY_BUTTON, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Retry")) + .build()); + systemMessageMap.put(StorageManager.CANCEL_VOTE_BUTTON, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Cancel Vote")) + .build()); + + BoothSystemMessages systemMessages = BoothSystemMessages.newBuilder().putAllSystemMessage(systemMessageMap).build(); + + try { + FileOutputStream output = new FileOutputStream(StorageManagerMockup.systemMessagesFilename); + systemMessages.writeTo(output); + output.close(); + System.out.println("Successfully wrote system messages protobuf to a file"); + } + catch (IOException e) { + System.err.println("Could not write to the system messages file: '" + StorageManagerMockup.systemMessagesFilename + "'."); + throw e; + } + + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/VotingBoothController.java b/voting-booth/src/main/java/meerkat/voting/controller/VotingBoothController.java new file mode 100644 index 0000000..6008d24 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/VotingBoothController.java @@ -0,0 +1,34 @@ +package meerkat.voting.controller; + +import meerkat.voting.encryptor.VBCryptoManager; +import meerkat.voting.ui.VotingBoothUI; +import meerkat.voting.output.BallotOutputDevice; +import meerkat.voting.storage.StorageManager; + +import java.io.IOException; + + +/** + * An interface for the controller component of the voting booth + */ +public interface VotingBoothController extends Runnable { + + /** + * initialize by setting all the different components of the Voting Booth to be recognized by this controller + * @param outputDevice the ballot output device. Naturally a printer and/or ethernet connection + * @param vbCrypto the crypto module + * @param vbUI User interface in which the voter chooses his answers + * @param vbStorageManager storage component for handling files and USB sticks + */ + public void init (BallotOutputDevice outputDevice, + VBCryptoManager vbCrypto, + VotingBoothUI vbUI, + StorageManager vbStorageManager) throws IOException; + + /** + * an asynchronous call from Admin Console (If there is such one implemented) to shut down the system + */ + public void callShutDown(); + + +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/VotingBoothImpl.java b/voting-booth/src/main/java/meerkat/voting/controller/VotingBoothImpl.java new file mode 100644 index 0000000..4903424 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/VotingBoothImpl.java @@ -0,0 +1,473 @@ +package meerkat.voting.controller; + +import meerkat.protobuf.Voting.*; +import meerkat.voting.controller.callbacks.*; +import meerkat.voting.controller.commands.*; +import meerkat.voting.controller.selector.QuestionSelector; +import meerkat.voting.controller.selector.SimpleListCategoriesSelector; +import meerkat.voting.encryptor.VBCryptoManager; +import meerkat.voting.encryptor.VBCryptoManager.EncryptionAndSecrets; +import meerkat.voting.output.BallotOutputDevice; +import meerkat.voting.storage.StorageManager; +import meerkat.voting.ui.VotingBoothUI; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.security.SignatureException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * An asynchronous implementation of the VotingBoothController. + * This implementation binds the other components (storage, ui, output device, and crypto manager), + * and runs as its own thread controlling the whole VB process. + * The high level details are that it has a queue of commands to handle in order, and a State object which keeps + * all data from previous tasks which is necessary for the next task. + * It calls executions in the UI and output device asynchronously. + */ +public class VotingBoothImpl implements VotingBoothController { + + private final Logger logger = LoggerFactory.getLogger(VotingBoothImpl.class); + + // the component interfaces of the Voting Booth + private BallotOutputDevice outputDevice; + private VBCryptoManager crypto; + private VotingBoothUI ui; + private StorageManager storageManager; + + // election details and info + private List questionsForChoosingChannel; + private QuestionSelector questionSelector; + private Map systemMessages; + + // state + private ControllerState state; + private volatile boolean shutDownHasBeenCalled; + private LinkedBlockingQueue queue; + protected final int MAX_REQUEST_IDENTIFIER = 100000; + private static int requestCounter = 0; + + + // a simple constructor + public VotingBoothImpl () { + logger.info("A VotingBoothImpl is constructed"); + shutDownHasBeenCalled = false; + queue = new LinkedBlockingQueue<>(); + state = new ControllerState(); + } + + @Override + public void init(BallotOutputDevice outputDevice, + VBCryptoManager vbCrypto, + VotingBoothUI vbUI, + StorageManager vbStorageManager) throws IOException { + logger.info("init is called"); + + // keep pointers to the VB components + this.outputDevice = outputDevice; + this.crypto = vbCrypto; + this.ui = vbUI; + this.storageManager = vbStorageManager; + + // store election details and info + ElectionParams electionParams; + try { + logger.info("init: reading election params"); + electionParams = storageManager.readElectionParams(); + logger.info("init: reading system messages"); + systemMessages = storageManager.readSystemMessages(); + } + catch (IOException e) { + logger.error("init could not read info from a file. Exception is: " + e); + throw e; + } + + logger.info("init: setting the election parameters"); + this.questionsForChoosingChannel = electionParams.getChannelChoiceQuestionsList(); + List questions = electionParams.getRaceQuestionsList(); + this.questionSelector = new SimpleListCategoriesSelector(questions, electionParams.getSelectionData()); + logger.info("init: setting finished"); + } + + + @Override + public void run() { + logger.info("run command has been called"); + runVotingFlow(); + doShutDown(); + + } + + /** + * a method for running the Voting flow of the VB (in contrast to the admin-setup flow + * It simply loops: takes the next command in its inner queue and handles it + */ + private void runVotingFlow () { + logger.info("entered the voting flow"); + + queue.add(new RestartVotingCommand(generateRequestIdentifier(), state.currentBallotSerialNumber)); + + while (! wasShutDownCalled()) { + try { + ControllerCommand Command = queue.take(); + handleSingleCommand(Command); + } + catch (InterruptedException e) { + logger.warn ("Interrupted while reading from command queue " + e); + } + } + } + + @Override + public void callShutDown() { + logger.info("callShutDown command has been called"); + shutDownHasBeenCalled = true; + queue.clear(); + ui.callShutDown(); + outputDevice.callShutDown(); + } + + /** + * this method decides upon a given command if to ignore it (if it has an old serial number) or to handle it + * If we choose to handle it, then it simply calls the matching method which handles this type of command + * @param command a command to handle next (probably from the inner command queue) + */ + private void handleSingleCommand(ControllerCommand command) { + // check if the command is old and should be ignored + if (command.getBallotSerialNumber() != state.currentBallotSerialNumber && !(command instanceof RestartVotingCommand)) { + // probably an old command relating to some old ballot serial number. Simply log it and ignore it. + String errorMessage = "handleSingleCommand: received a task too old. " + + command.getBallotSerialNumber() + " " + state.currentBallotSerialNumber; + logger.debug(errorMessage); + return; + } + + // decide which method to run according to the command type + if (command instanceof RestartVotingCommand) { + doRestartVoting (); + } + else if (command instanceof ChannelChoiceCommand) { + doChooseChannel(); + } + else if (command instanceof ChannelDeterminedCommand) { + doSetChannelAndAskQuestions ((ChannelDeterminedCommand)command); + } + else if (command instanceof ChooseFinalizeOptionCommand) { + doChooseFinalizeOption(); + } + else if (command instanceof CastCommand) { + doFinalize(false); + } + else if (command instanceof AuditCommand) { + doFinalize(true); + } + else if (command instanceof EncryptAndCommitBallotCommand) { + doCommit ((EncryptAndCommitBallotCommand)command); + } + else if (command instanceof ReportErrorCommand) { + doReportErrorAndForceRestart((ReportErrorCommand)command); + } + else { + logger.error("handleSingleCommand: unknown type of ControllerCommand received: " + command.getClass().getName()); + doReportErrorAndForceRestart(systemMessages.get(StorageManager.SOMETHING_WRONG_MESSAGE)); + } + } + + private boolean wasShutDownCalled () { + return shutDownHasBeenCalled; + } + + private void doShutDown () { + logger.info("running callShutDown"); + state.clearAndResetState(VBState.SHUT_DOWN); + //TODO: add commands to actually shut down the machine + } + + /** + * a method to execute a Restart Voting Command + */ + private void doRestartVoting () { + queue.clear(); + state.clearAndResetState(VBState.NEW_VOTER); + ui.startNewVoterSession(new NewVoterCallback(generateRequestIdentifier(), state.currentBallotSerialNumber, this.queue)); + } + + /** + * a (overloaded) method to execute a Report Error Command. + * It actually just runs the overloaded version of this method with the error message inside the command + * @param command the command has the info of the error message to report + */ + private void doReportErrorAndForceRestart(ReportErrorCommand command) { + doReportErrorAndForceRestart(command.getErrorMessage()); + } + + /** + * a (overloaded) method to report an error message to the voter + * @param errorMessage message to show the voter + */ + private void doReportErrorAndForceRestart(UIElement errorMessage) { + queue.clear(); + state.clearAndResetState(VBState.FATAL_ERROR_FORCE_NEW_VOTER); + ui.showErrorMessageWithButtons(errorMessage, + new UIElement[]{systemMessages.get(StorageManager.RESTART_VOTING_BUTTON)}, + new ErrorMessageRestartCallback(generateRequestIdentifier(), + state.currentBallotSerialNumber, + this.queue)); + } + + /** + * a method to execute a Channel Choice Command + * it notifies the UI to present the channel choice questions to the voter + */ + private void doChooseChannel () { + if (state.stateIdentifier == VBState.NEW_VOTER) { + logger.debug("doing chooseChannel"); + state.stateIdentifier = VBState.CHOOSE_CHANNEL; + ui.chooseChannel(this.questionsForChoosingChannel, + new ChannelChoiceCallback(generateRequestIdentifier(), + state.currentBallotSerialNumber, + this.queue, + systemMessages.get(StorageManager.UNSUCCESSFUL_CHANNEL_CHOICE_MESSAGE))); + } + else { + logger.debug("doChooseChannel: current state is " + state.stateIdentifier); + // ignore this request + } + } + + /** + * a method to execute a Channel Determined Command + * (this actually sets the channel now after the voter has answered the channel choice questions) + * It then determines the race questions for the voter, and notifies the UI to present them to the voter + * @param command details of the voter's answers on the channel choice questions + */ + private void doSetChannelAndAskQuestions (ChannelDeterminedCommand command) { + if (state.stateIdentifier == VBState.CHOOSE_CHANNEL) { + logger.debug("doing set channel and ask questions"); + state.stateIdentifier = VBState.ANSWER_QUESTIONS; + List channelChoiceAnswers = command.channelChoiceAnswers; + state.channelIdentifier = questionSelector.getChannelIdentifier(channelChoiceAnswers); + state.channelSpecificQuestions = questionSelector.selectQuestionsForVoter(state.channelIdentifier); + ui.askVoterQuestions(state.channelSpecificQuestions, + new VotingCallback(generateRequestIdentifier(), + state.currentBallotSerialNumber, + this.queue, + systemMessages.get(StorageManager.UNSUCCESSFUL_VOTING_MESSAGE))); + } + else { + logger.debug("doSetChannelAndAskQuestions: current state is " + state.stateIdentifier); + // ignore this request + } + } + + /** + * a method to execute a Do-Finalzie-Option Command + * notifies the UI to present the cast-or-audit question to the voter + */ + private void doChooseFinalizeOption() { + if (state.stateIdentifier == VBState.COMMITTING_TO_BALLOT) { + logger.debug("doChooseFinalizeOption"); + state.stateIdentifier = VBState.CAST_OR_AUDIT; + ui.castOrAudit(new CastOrAuditCallback(generateRequestIdentifier(), + state.currentBallotSerialNumber, + this.queue, + systemMessages.get(StorageManager.UNRECOGNIZED_FINALIZE_RESPONSE_MESSAGE))); + } + else { + logger.debug("doChooseFinalizeOption: current state is " + state.stateIdentifier); + // ignore this request + } + } + + /** + * a method to execute a Encrypt-and-Commit Command + * It sends a notification to commit to the output device + * @param command details of the voter's answers on the ballot questions + */ + private void doCommit (EncryptAndCommitBallotCommand command) { + if (state.stateIdentifier == VBState.ANSWER_QUESTIONS) { + logger.debug("doing commit"); + try { + setBallotData(command); + ui.notifyVoterToWaitForFinish(systemMessages.get(StorageManager.WAIT_FOR_COMMIT_MESSAGE), + new WaitForFinishCallback(generateRequestIdentifier(), + state.currentBallotSerialNumber, + this.queue, + systemMessages.get(StorageManager.SOMETHING_WRONG_MESSAGE))); + outputDevice.commitToBallot(state.plaintextBallot, + state.signedEncryptedBallot, + new OutputDeviceCommitCallback(generateRequestIdentifier(), + state.currentBallotSerialNumber, + this.queue, + systemMessages.get(StorageManager.OUTPUT_DEVICE_FAILURE_MESSAGE))); + state.stateIdentifier = VBState.COMMITTING_TO_BALLOT; + } + catch (SignatureException | IOException e) { + logger.error("doCommit: encryption failed. exception: " + e); + + // in case the encryption failed for some unknown reason, we send the UI a notification + // to ask the voter whether he wants to retry or cancel the ballot + UIElement errorMessage = systemMessages.get(StorageManager.ENCRYPTION_FAILED_MESSAGE); + UIElement[] buttons = new UIElement[]{ + systemMessages.get(StorageManager.RETRY_BUTTON), + systemMessages.get(StorageManager.CANCEL_VOTE_BUTTON)}; + + EncryptionFailedCallback callback = new EncryptionFailedCallback(generateRequestIdentifier(), + state.currentBallotSerialNumber, + this.queue); + ui.showErrorMessageWithButtons(errorMessage, buttons, callback); + } + } + else { + logger.debug("doCommit: current state is " + state.stateIdentifier); + // ignore this request + } + } + + /** + * encrypt the ballot, and keep all info (plaintext, encryption and secrets) in the state's attributes + * @param command either an EncryptAndCommitBallotCommand if we encrypt the plaintext for the first time, or a RetryEncryptAndCommitBallotCommand if we already got here but encryption failed before + * @throws IOException problems in the encryption process + * @throws SignatureException problems in the digital signature process + */ + private void setBallotData (EncryptAndCommitBallotCommand command) throws IOException, SignatureException{ + // a Retry command is given only if we first got here, and later the encryption failed but the voter chose to retry + // in such a case the plaintext is already set from previous attempt + if (! (command instanceof RetryEncryptAndCommitBallotCommand)) { + // this is not a retry attempt, so the plaintext is not set yet + // otherwise, we have the plaintext from the previous encryption attempt + state.plaintextBallot = PlaintextBallot.newBuilder() + .setSerialNumber(command.getBallotSerialNumber()) + .addAllAnswers(command.getVotingAnswers()) + .build(); + } + + // keep the encryption and the secrets we used for it + EncryptionAndSecrets encryptionAndSecrets = crypto.encrypt(state.plaintextBallot); + state.signedEncryptedBallot = encryptionAndSecrets.getSignedEncryptedBallot(); + state.secrets = encryptionAndSecrets.getSecrets(); + } + + /** + * a method to execute a Cast Command or an Audit Command + * according to the flag, chooses which finalize ballot task to send to the output device + * @param auditRequested true if we wish to finalize by auditing. false if we finalize by casting the ballot + */ + private void doFinalize (boolean auditRequested) { + if (state.stateIdentifier == VBState.CAST_OR_AUDIT) { + logger.debug("finalizing"); + state.stateIdentifier = VBState.FINALIZING; + if (auditRequested) { + // finalize by auditing + ui.notifyVoterToWaitForFinish(systemMessages.get(StorageManager.WAIT_FOR_AUDIT_MESSAGE), + new WaitForFinishCallback(generateRequestIdentifier(), + state.currentBallotSerialNumber, + this.queue, + systemMessages.get(StorageManager.SOMETHING_WRONG_MESSAGE))); + outputDevice.audit(state.secrets, + new OutputDeviceFinalizeCallback(generateRequestIdentifier(), + state.currentBallotSerialNumber, + this.queue, + systemMessages.get(StorageManager.OUTPUT_DEVICE_FAILURE_MESSAGE))); + } + else { + // finalize by casting the ballot + ui.notifyVoterToWaitForFinish(systemMessages.get(StorageManager.WAIT_FOR_CAST_MESSAGE), + new WaitForFinishCallback(generateRequestIdentifier(), + state.currentBallotSerialNumber, + this.queue, + systemMessages.get(StorageManager.SOMETHING_WRONG_MESSAGE))); + outputDevice.castBallot( + new OutputDeviceFinalizeCallback(generateRequestIdentifier(), + state.currentBallotSerialNumber, + this.queue, + systemMessages.get(StorageManager.OUTPUT_DEVICE_FAILURE_MESSAGE))); + } + } + else { + logger.debug("doFinalize: current state is " + state.stateIdentifier); + // ignore this request + } + } + + + + // an enum to keep the step (of the voting process) in which the VB is currently in + private enum VBState { + NEW_VOTER, + CHOOSE_CHANNEL, + ANSWER_QUESTIONS, + COMMITTING_TO_BALLOT, + CAST_OR_AUDIT, + FINALIZING, + FATAL_ERROR_FORCE_NEW_VOTER, + SHUT_DOWN + } + + + /** + * a class to keep and directly access all the details of the VB controller state. + * naming: + * - the (enum) state identifier of the current step + * - the chosen channel + * - all details of the ballot (both plaintext and encryption) + * - last request identifier (to one of the other component interfaces) + * - serial number of the current ballot + */ + private class ControllerState { + public VBState stateIdentifier; + public byte[] channelIdentifier; + public List channelSpecificQuestions; + public PlaintextBallot plaintextBallot; + public SignedEncryptedBallot signedEncryptedBallot; + public BallotSecrets secrets; + public int lastRequestIdentifier; + public long currentBallotSerialNumber; + + public ControllerState () { + plaintextBallot = null; + signedEncryptedBallot = null; + secrets = null; + lastRequestIdentifier = -1; + channelIdentifier = null; + channelSpecificQuestions = null; + currentBallotSerialNumber = 0; + } + + private void clearPlaintext () { + plaintextBallot = null; + } + + private void clearCiphertext () { + signedEncryptedBallot = null; + secrets = null; + } + + public void clearAndResetState(VBState newStateIdentifier) { + state.clearPlaintext(); + state.clearCiphertext(); + state.stateIdentifier = newStateIdentifier; + state.currentBallotSerialNumber += 1; + } + + } + + + /** + * Creates a new request identifier to identify any call to one of the other component interfaces. + * We limit its value to MAX_REQUEST_IDENTIFIER, so the identifier is kept short. + * @return a new request identifier + */ + private int generateRequestIdentifier() { + ++requestCounter; + if (requestCounter >= MAX_REQUEST_IDENTIFIER) { + requestCounter = 1; + } + return requestCounter; + } + + +} + diff --git a/voting-booth/src/main/java/meerkat/voting/controller/callbacks/CastOrAuditCallback.java b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/CastOrAuditCallback.java new file mode 100644 index 0000000..7226f83 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/CastOrAuditCallback.java @@ -0,0 +1,52 @@ +package meerkat.voting.controller.callbacks; + +import meerkat.protobuf.Voting.UIElement; +import meerkat.voting.controller.commands.*; +import meerkat.voting.ui.VotingBoothUI.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * A controller callback for the cast-or-audit request to the UI. + * Upon getting a FinalizeBallotChoice response from the voter, the callback then registers a new command + * to the controller queue, either a CastCommand or an AuditCommand according to the voter's choice + */ +public class CastOrAuditCallback extends ControllerCallback { + protected final static Logger logger = LoggerFactory.getLogger(CastOrAuditCallback.class); + protected final UIElement unrecognizedFinalizeResponseMessage; + + public CastOrAuditCallback(int requestId, + long ballotSerialNumber, + LinkedBlockingQueue controllerQueue, + UIElement unrecognizedFinalizeResponseMessage) { + super(requestId, ballotSerialNumber, controllerQueue); + this.unrecognizedFinalizeResponseMessage = unrecognizedFinalizeResponseMessage; + } + + @Override + public void onSuccess(FinalizeBallotChoice result) { + if (result == FinalizeBallotChoice.CAST) { + enqueueCommand(new CastCommand(getRequestIdentifier(), getBallotSerialNumber())); + } + else if (result == FinalizeBallotChoice.AUDIT) { + enqueueCommand(new AuditCommand(getRequestIdentifier(), getBallotSerialNumber())); + } + else { + logger.error("CastOrAuditCallback got an unrecognized response: " + result); + onFailure(new IllegalArgumentException("CastOrAuditCallback got an unknown result (" + result + ")")); + } + + } + + @Override + public void onFailure(Throwable t) { + logger.error("CastOrAuditCallback got a failure: " + t); + enqueueCommand(new ReportErrorCommand(getRequestIdentifier(), + getBallotSerialNumber(), + unrecognizedFinalizeResponseMessage)); + } +} + + + diff --git a/voting-booth/src/main/java/meerkat/voting/controller/callbacks/ChannelChoiceCallback.java b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/ChannelChoiceCallback.java new file mode 100644 index 0000000..7b97a82 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/ChannelChoiceCallback.java @@ -0,0 +1,50 @@ +package meerkat.voting.controller.callbacks; + +import meerkat.protobuf.Voting.*; +import meerkat.voting.controller.commands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * A controller callback for the channel-choice request to the UI. + * Upon receiving the answers for the channel-choice questions, the callback registers a new ChannelDeterminedCommand + * to the controller queue, so the controller can then process the answers and set the channel. + * If voter cancelled during the process, a cancelling exception is thrown and a RestartVotingCommand is + * registered through the onFailure() method + */ +public class ChannelChoiceCallback extends ControllerCallback> { + protected final static Logger logger = LoggerFactory.getLogger(ChannelChoiceCallback.class); + protected final UIElement unsuccessfulChannelChoiceMessage; + + public ChannelChoiceCallback(int requestId, + long ballotSerialNumber, + LinkedBlockingQueue controllerQueue, + UIElement unsuccessfulChannelChoiceMessage) { + super(requestId, ballotSerialNumber, controllerQueue); + this.unsuccessfulChannelChoiceMessage = unsuccessfulChannelChoiceMessage; + } + + @Override + public void onSuccess(List result) { + logger.debug("callback for channel choice returned success"); + // register the chosen BallotAnswers to a command in the controller queue + enqueueCommand(new ChannelDeterminedCommand(getRequestIdentifier(), getBallotSerialNumber(), result)); + } + + @Override + public void onFailure(Throwable t) { + if (t instanceof VoterCancelThrowable) { + // voter has cancelled during the UI channel choice process. A VoterCancelThrowable is thrown + logger.debug("ChannelChoiceCallback got a cancellation response"); + enqueueCommand(new RestartVotingCommand(getRequestIdentifier(), getBallotSerialNumber())); + } + else { + logger.error("channel choice initiated a failure: " + t); + enqueueCommand(new ReportErrorCommand(getRequestIdentifier(), + getBallotSerialNumber(), + unsuccessfulChannelChoiceMessage)); + } + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/callbacks/ControllerCallback.java b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/ControllerCallback.java new file mode 100644 index 0000000..18719bd --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/ControllerCallback.java @@ -0,0 +1,41 @@ +package meerkat.voting.controller.callbacks; + +import com.google.common.util.concurrent.FutureCallback; +import meerkat.voting.controller.commands.ControllerCommand; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * The base (abstract) class of all callbacks for requests sent by the controller to other components (ui, output-device) + * It implements the FutureCallback interface + * Its members are: + * - requestIdentifier - uniquely identifies the request which this callback responds + * - ballotSerialNumber - number of ballot which was currently active when request was sent + * - controllerQueue - so the callback can issue and register a new command to the controller, once the request handling is finished + */ +public abstract class ControllerCallback implements FutureCallback { + + private final int requestIdentifier; + private final long ballotSerialNumber; + private LinkedBlockingQueue controllerQueue; + + protected ControllerCallback (int requestId, + long ballotSerialNumber, + LinkedBlockingQueue controllerQueue) { + this.requestIdentifier = requestId; + this.ballotSerialNumber = ballotSerialNumber; + this.controllerQueue = controllerQueue; + } + + protected int getRequestIdentifier () { + return requestIdentifier; + } + + protected long getBallotSerialNumber () { + return ballotSerialNumber; + } + + protected void enqueueCommand (ControllerCommand command) { + controllerQueue.add(command); + } + +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/callbacks/EncryptionFailedCallback.java b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/EncryptionFailedCallback.java new file mode 100644 index 0000000..55906be --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/EncryptionFailedCallback.java @@ -0,0 +1,45 @@ +package meerkat.voting.controller.callbacks; + +import meerkat.voting.controller.commands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * This is quite a special callback. It is not issued in a normal flow of the voting. + * This callback is made only for a request to the UI to choose handling of failure in encryption. + * When encryption/signature fails the voter is asked in the UI whether to retry or abort. + * This specific callback decides, upon the answer to this request, which command to register in the controller's queue + */ +public class EncryptionFailedCallback extends ControllerCallback { + protected final static Logger logger = LoggerFactory.getLogger(EncryptionFailedCallback.class); + + public EncryptionFailedCallback(int requestId, + long ballotSerialNumber, + LinkedBlockingQueue controllerQueue) { + super(requestId, ballotSerialNumber, controllerQueue); + } + + @Override + public void onSuccess(Integer result) { + logger.debug("callback for encryption-failed request is initiated successfully"); + int res = result.intValue(); + if (res == 0) { + logger.debug("voter chose to retry encryption"); + enqueueCommand(new RetryEncryptAndCommitBallotCommand(getRequestIdentifier(), getBallotSerialNumber())); + } + else if (res == 1) { + logger.debug("voter chose to abort the vote"); + enqueueCommand(new RestartVotingCommand(getRequestIdentifier(), getBallotSerialNumber())); + } + else { + onFailure(new IllegalArgumentException("EncryptionFailedCallback got an unknown result (" + res + ")")); + } + } + + @Override + public void onFailure(Throwable t) { + logger.error("Error message execution initiated a failure: " + t); + enqueueCommand(new RestartVotingCommand(getRequestIdentifier(), getBallotSerialNumber())); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/callbacks/ErrorMessageRestartCallback.java b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/ErrorMessageRestartCallback.java new file mode 100644 index 0000000..4d75c1d --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/ErrorMessageRestartCallback.java @@ -0,0 +1,33 @@ +package meerkat.voting.controller.callbacks; + +import meerkat.voting.controller.commands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * This is quite a special callback. It is not issued in a normal flow of the voting. + * This callback is made only for a request to the UI to show the voter an error message. + * Upon approval of the voter, the method onSuccess() of this callback is called, and the voting + * is reset through a command to the controller's queue + */ +public class ErrorMessageRestartCallback extends ControllerCallback { + protected final static Logger logger = LoggerFactory.getLogger(ErrorMessageRestartCallback.class); + + public ErrorMessageRestartCallback(int requestId, + long ballotSerialNumber, + LinkedBlockingQueue controllerQueue) { + super(requestId, ballotSerialNumber, controllerQueue); + } + + @Override + public void onSuccess(Integer i) { + enqueueCommand(new RestartVotingCommand(getRequestIdentifier(), getBallotSerialNumber())); + } + + @Override + public void onFailure(Throwable t) { + logger.error("Error message execution initiated a failure: " + t); + enqueueCommand(new RestartVotingCommand(getRequestIdentifier(), getBallotSerialNumber())); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/callbacks/NewVoterCallback.java b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/NewVoterCallback.java new file mode 100644 index 0000000..17f67be --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/NewVoterCallback.java @@ -0,0 +1,34 @@ +package meerkat.voting.controller.callbacks; + +import meerkat.voting.controller.commands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.concurrent.LinkedBlockingQueue; + + +/** + * A controller callback for the StartSession request to the UI. + * Upon approval of the voter, it registers a new ChannelChoiceCommand to the controller queue (which + * then starts the channel choice process) + */ +public class NewVoterCallback extends ControllerCallback { + protected final static Logger logger = LoggerFactory.getLogger(NewVoterCallback.class); + + public NewVoterCallback(int requestId, + long ballotSerialNumber, + LinkedBlockingQueue controllerQueue) { + super(requestId, ballotSerialNumber, controllerQueue); + } + + @Override + public void onSuccess(Void v) { + logger.debug("callback for new voting returned success"); + enqueueCommand(new ChannelChoiceCommand(getRequestIdentifier(), getBallotSerialNumber())); + } + + @Override + public void onFailure(Throwable t) { + logger.error("New voting session got a failure: " + t); + enqueueCommand(new RestartVotingCommand(getRequestIdentifier(), getBallotSerialNumber())); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/callbacks/OutputDeviceCommitCallback.java b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/OutputDeviceCommitCallback.java new file mode 100644 index 0000000..dd20ec2 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/OutputDeviceCommitCallback.java @@ -0,0 +1,39 @@ +package meerkat.voting.controller.callbacks; + +import meerkat.protobuf.Voting.UIElement; +import meerkat.voting.controller.commands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * A controller callback for the Commit request to the output-device. + * When committing is done, the callback's onSuccess() method is called to register a new ChooseFinalizeOptionCommand + * to the controller + */ +public class OutputDeviceCommitCallback extends ControllerCallback { + protected final static Logger logger = LoggerFactory.getLogger(OutputDeviceCommitCallback.class); + protected final UIElement outputDeviceFailureMessage; + + public OutputDeviceCommitCallback(int requestId, + long ballotSerialNumber, + LinkedBlockingQueue controllerQueue, + UIElement outputDeviceFailureMessage) { + super(requestId, ballotSerialNumber, controllerQueue); + this.outputDeviceFailureMessage = outputDeviceFailureMessage; + } + + @Override + public void onSuccess(Void v) { + logger.debug("callback for output device commit success"); + enqueueCommand(new ChooseFinalizeOptionCommand(getRequestIdentifier(), getBallotSerialNumber())); + } + + @Override + public void onFailure(Throwable t) { + logger.error("OutputDeviceCommitCallback got a failure: " + t); + enqueueCommand(new ReportErrorCommand(getRequestIdentifier(), + getBallotSerialNumber(), + outputDeviceFailureMessage)); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/callbacks/OutputDeviceFinalizeCallback.java b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/OutputDeviceFinalizeCallback.java new file mode 100644 index 0000000..6c5b0d4 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/OutputDeviceFinalizeCallback.java @@ -0,0 +1,39 @@ +package meerkat.voting.controller.callbacks; + +import meerkat.protobuf.Voting.UIElement; +import meerkat.voting.controller.commands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * A controller callback for the Finalize request to the output-device. + * When finalizing (either cast or audit) is done, + * the callback's onSuccess() method is called to register a new command to the controller to restart the voting process + */ +public class OutputDeviceFinalizeCallback extends ControllerCallback { + protected final static Logger logger = LoggerFactory.getLogger(OutputDeviceFinalizeCallback.class); + protected final UIElement outputDeviceFailureMessage; + + public OutputDeviceFinalizeCallback(int requestId, + long ballotSerialNumber, + LinkedBlockingQueue controllerQueue, + UIElement outputDeviceFailureMessage) { + super(requestId, ballotSerialNumber, controllerQueue); + this.outputDeviceFailureMessage = outputDeviceFailureMessage; + } + + @Override + public void onSuccess(Void v) { + logger.debug("callback for output device finalize success"); + enqueueCommand(new RestartVotingCommand(getRequestIdentifier(), getBallotSerialNumber())); + } + + @Override + public void onFailure(Throwable t) { + logger.error("OutputDeviceFinalizeCallback got a failure: " + t); + enqueueCommand(new ReportErrorCommand(getRequestIdentifier(), + getBallotSerialNumber(), + outputDeviceFailureMessage)); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/callbacks/VoterCancelThrowable.java b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/VoterCancelThrowable.java new file mode 100644 index 0000000..38c2c8b --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/VoterCancelThrowable.java @@ -0,0 +1,8 @@ +package meerkat.voting.controller.callbacks; + +/** + * Just a simple unique exception to throw when a voter aborts/cancels the voting during the voting process + */ +public class VoterCancelThrowable extends Throwable { + // +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/callbacks/VotingCallback.java b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/VotingCallback.java new file mode 100644 index 0000000..96c940b --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/VotingCallback.java @@ -0,0 +1,48 @@ +package meerkat.voting.controller.callbacks; + +import meerkat.protobuf.Voting.*; +import meerkat.voting.controller.commands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * A controller callback for the race-voting request to the UI. + * Upon receiving the answers for the race questions, the callback registers a new command to process + * the voter's answers (encrypt and then commit) into the controller's queue. + * If voter cancelled during the process, a cancelling exception is thrown and a RestartVotingCommand is + * registered through the onFailure() method + */ +public class VotingCallback extends ControllerCallback> { + protected final static Logger logger = LoggerFactory.getLogger(VotingCallback.class); + protected final UIElement unsuccessfulVotingMessage; + + public VotingCallback(int requestId, + long ballotSerialNumber, + LinkedBlockingQueue controllerQueue, + UIElement unsuccessfulVotingMessage) { + super(requestId, ballotSerialNumber, controllerQueue); + this.unsuccessfulVotingMessage = unsuccessfulVotingMessage; + } + + @Override + public void onSuccess(List result) { + logger.debug("callback for voting returned success"); + enqueueCommand(new EncryptAndCommitBallotCommand(getRequestIdentifier(), getBallotSerialNumber(), result)); + } + + @Override + public void onFailure(Throwable t) { + if (t instanceof VoterCancelThrowable) { + logger.debug("VotingCallback got a cancellation response"); + enqueueCommand(new RestartVotingCommand(getRequestIdentifier(), getBallotSerialNumber())); + } + else { + logger.error("voting initiated a failure: " + t); + enqueueCommand(new ReportErrorCommand(getRequestIdentifier(), + getBallotSerialNumber(), + unsuccessfulVotingMessage)); + } + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/callbacks/WaitForFinishCallback.java b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/WaitForFinishCallback.java new file mode 100644 index 0000000..f3f0308 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/WaitForFinishCallback.java @@ -0,0 +1,37 @@ +package meerkat.voting.controller.callbacks; + +import meerkat.protobuf.Voting.UIElement; +import meerkat.voting.controller.commands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * This callback is attached to requests to UI which ask the voter to wait for some process to finish. + * It actually asks nothing in the UI, and it is simply attached to the UI request as a place-holder. + * Therefore its onSuccess() method is empty + */ +public class WaitForFinishCallback extends ControllerCallback { + protected final static Logger logger = LoggerFactory.getLogger(WaitForFinishCallback.class); + protected final UIElement somethingWrongMessage; + + public WaitForFinishCallback(int requestId, + long ballotSerialNumber, + LinkedBlockingQueue controllerQueue, + UIElement somethingWrongMessage) { + super(requestId, ballotSerialNumber, controllerQueue); + this.somethingWrongMessage = somethingWrongMessage; + } + + @Override + public void onSuccess(Void v) { + } + + @Override + public void onFailure(Throwable t) { + logger.error("WaitForFinishCallback got a failure: " + t); + enqueueCommand(new ReportErrorCommand(getRequestIdentifier(), + getBallotSerialNumber(), + somethingWrongMessage)); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/commands/AuditCommand.java b/voting-booth/src/main/java/meerkat/voting/controller/commands/AuditCommand.java new file mode 100644 index 0000000..fa7fc4a --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/commands/AuditCommand.java @@ -0,0 +1,10 @@ +package meerkat.voting.controller.commands; + +/** + * a command to audit the ballot + */ +public class AuditCommand extends ControllerCommand { + public AuditCommand(int requestIdentifier, long ballotSerialNumber) { + super(requestIdentifier, ballotSerialNumber); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/commands/CastCommand.java b/voting-booth/src/main/java/meerkat/voting/controller/commands/CastCommand.java new file mode 100644 index 0000000..c4e96bc --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/commands/CastCommand.java @@ -0,0 +1,10 @@ +package meerkat.voting.controller.commands; + +/** + * a command to cast the ballot + */ +public class CastCommand extends ControllerCommand { + public CastCommand(int requestIdentifier, long ballotSerialNumber) { + super(requestIdentifier, ballotSerialNumber); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/commands/ChannelChoiceCommand.java b/voting-booth/src/main/java/meerkat/voting/controller/commands/ChannelChoiceCommand.java new file mode 100644 index 0000000..9d86edb --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/commands/ChannelChoiceCommand.java @@ -0,0 +1,10 @@ +package meerkat.voting.controller.commands; + +/** + * a command to initiate the channel choice flow at the beginning of the voting + */ +public class ChannelChoiceCommand extends ControllerCommand { + public ChannelChoiceCommand(int requestIdentifier, long ballotSerialNumber) { + super(requestIdentifier, ballotSerialNumber); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/commands/ChannelDeterminedCommand.java b/voting-booth/src/main/java/meerkat/voting/controller/commands/ChannelDeterminedCommand.java new file mode 100644 index 0000000..c9b7023 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/commands/ChannelDeterminedCommand.java @@ -0,0 +1,16 @@ +package meerkat.voting.controller.commands; + +import meerkat.protobuf.Voting.*; +import java.util.List; + +/** + * This command is registered in the controller right after the voter answered all the channel choice questions + */ +public class ChannelDeterminedCommand extends ControllerCommand { + public List channelChoiceAnswers; + + public ChannelDeterminedCommand(int requestIdentifier, long ballotSerialNumber, List answers) { + super(requestIdentifier, ballotSerialNumber); + channelChoiceAnswers = answers; + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/commands/ChooseFinalizeOptionCommand.java b/voting-booth/src/main/java/meerkat/voting/controller/commands/ChooseFinalizeOptionCommand.java new file mode 100644 index 0000000..0cdbacc --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/commands/ChooseFinalizeOptionCommand.java @@ -0,0 +1,10 @@ +package meerkat.voting.controller.commands; + +/** + * a command to initiate asking the voter how to finalize (cast-or-audit) the ballot + */ +public class ChooseFinalizeOptionCommand extends ControllerCommand { + public ChooseFinalizeOptionCommand(int requestIdentifier, long ballotSerialNumber) { + super(requestIdentifier, ballotSerialNumber); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/commands/ControllerCommand.java b/voting-booth/src/main/java/meerkat/voting/controller/commands/ControllerCommand.java new file mode 100644 index 0000000..7743723 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/commands/ControllerCommand.java @@ -0,0 +1,23 @@ +package meerkat.voting.controller.commands; + +/** + * This is the base class for the controller commands. + * These commands are registered in a command queue of the controller. + */ +public abstract class ControllerCommand { + protected final int requestIdentifier; + protected final long ballotSerialNumber; + + protected ControllerCommand(int requestIdentifier, long ballotSerialNumber) { + this.requestIdentifier = requestIdentifier; + this.ballotSerialNumber = ballotSerialNumber; + } + + public long getBallotSerialNumber () { + return this.ballotSerialNumber; + } + + public int getRequestIdentifier () { + return this.requestIdentifier; + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/commands/EncryptAndCommitBallotCommand.java b/voting-booth/src/main/java/meerkat/voting/controller/commands/EncryptAndCommitBallotCommand.java new file mode 100644 index 0000000..3917a31 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/commands/EncryptAndCommitBallotCommand.java @@ -0,0 +1,24 @@ +package meerkat.voting.controller.commands; + +import meerkat.protobuf.Voting.BallotAnswer; +import java.util.List; + +/** + * a command registered after voter answered all ballot questions. + * The controller then initiates an encryption-signature-commit flow + */ +public class EncryptAndCommitBallotCommand extends ControllerCommand { + private final List votingAnswers; + + public EncryptAndCommitBallotCommand(int requestIdentifier, + long ballotSerialNumber, + List answers) { + super(requestIdentifier, ballotSerialNumber); + votingAnswers = answers; + } + + public List getVotingAnswers() { + return votingAnswers; + } + +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/commands/ReportErrorCommand.java b/voting-booth/src/main/java/meerkat/voting/controller/commands/ReportErrorCommand.java new file mode 100644 index 0000000..fe3e369 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/commands/ReportErrorCommand.java @@ -0,0 +1,20 @@ +package meerkat.voting.controller.commands; + +import meerkat.protobuf.Voting.*; + +/** + * This command is not a part of the normal flow of the controller. + * It asks the controller to handle (report to voter) some error message + */ +public class ReportErrorCommand extends ControllerCommand { + private final UIElement errorMessage; + + public ReportErrorCommand(int requestIdentifier, long ballotSerialNumber, UIElement errorMessage) { + super(requestIdentifier, ballotSerialNumber); + this.errorMessage = errorMessage; + } + + public UIElement getErrorMessage() { + return errorMessage; + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/commands/RestartVotingCommand.java b/voting-booth/src/main/java/meerkat/voting/controller/commands/RestartVotingCommand.java new file mode 100644 index 0000000..600a163 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/commands/RestartVotingCommand.java @@ -0,0 +1,11 @@ +package meerkat.voting.controller.commands; + + +/** + * a command to restart a voting flow (for a new voter) + */ +public class RestartVotingCommand extends ControllerCommand { + public RestartVotingCommand(int requestIdentifier, long ballotSerialNumber) { + super(requestIdentifier, ballotSerialNumber); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/commands/RetryEncryptAndCommitBallotCommand.java b/voting-booth/src/main/java/meerkat/voting/controller/commands/RetryEncryptAndCommitBallotCommand.java new file mode 100644 index 0000000..b79a55f --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/commands/RetryEncryptAndCommitBallotCommand.java @@ -0,0 +1,15 @@ +package meerkat.voting.controller.commands; + +/** + * This is quite a special command not part of the normal voting flow. + * It extends the base EncryptAndCommitBallotCommand for occasions where first attempt of encryption failed + * and the voter asks to re-try encrypting and committing. + */ +public class RetryEncryptAndCommitBallotCommand extends EncryptAndCommitBallotCommand { + + public RetryEncryptAndCommitBallotCommand(int requestIdentifier, + long ballotSerialNumber) { + super(requestIdentifier, ballotSerialNumber, null); + } + +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/selector/QuestionSelector.java b/voting-booth/src/main/java/meerkat/voting/controller/selector/QuestionSelector.java new file mode 100644 index 0000000..6603b62 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/selector/QuestionSelector.java @@ -0,0 +1,29 @@ +package meerkat.voting.controller.selector; + +import meerkat.protobuf.Voting.*; +import java.util.List; + +/** + * An interface for the question-selection component. + * This component handles the connection between the channel choice questions and the race questions. + * It gets the answers for the channel choice questions and determines which race question to put in the voter's ballot. + * It also creates an identifier for this chosen channel. This identifier should appear in the plaintext of the ballot. + * The channel identifier does not identify a specific voter, but rather it identifies a specific voting channel + */ +public interface QuestionSelector { + + /** + * determines an identifier for the channel of the voter + * @param channelChoiceAnswers The answers given by the voter to the channel choice questions + * @return an identifier of the channel. To be used by selectQuestionsForVoter(). This identifier should also appear on the plaintext of the ballot + */ + public byte[] getChannelIdentifier (List channelChoiceAnswers); + + /** + * determines which race questions to present to the voter according to its channel + * @param channelIdentifier the identifier of this specific channel + * @return the race questions (to present to the voter) + */ + public List selectQuestionsForVoter (byte[] channelIdentifier); + +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/selector/SimpleListCategoriesSelector.java b/voting-booth/src/main/java/meerkat/voting/controller/selector/SimpleListCategoriesSelector.java new file mode 100644 index 0000000..6b8f78b --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/selector/SimpleListCategoriesSelector.java @@ -0,0 +1,176 @@ +package meerkat.voting.controller.selector; + +import meerkat.protobuf.Voting.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.lang.Math; + +/** + * A simple implementation of a QuestionSelector. + * This implementation simply regards every single answer in the channel choice phase as an identifier of a category + * Every category is an array of ballot race questions. + * Data of categories is initialized and stored by a SimpleCategoriesSelectionData protobuf. + * After receiving the answers from a channel choice phase, this class simply gathers all the categories + * chosen and compiles the list of ballot questions to include in the ballot for this voter (a question + * is included in the ballot if its index appears in any chosen category, or in the default category shared by all voters) + */ +public class SimpleListCategoriesSelector implements QuestionSelector { + protected final static Logger logger = LoggerFactory.getLogger(SimpleListCategoriesSelector.class); + + // all the possible race questions + private final BallotQuestion[] allBallotQuestions; + + // this category is presented to any voter (regardless of his answers to the channel choice questions) + private final int[] sharedDefaults; + + // all the categories. + // first index is the channel choice question number + // second index is a possible answer to this question + // categoryChoosers[questionNumber][answerNumber] is an array of indices (to the ballotQuestions array). + // This category of questions is included in the ballot if voter answered this specific answer to this channel choice question + private final int[][][] categoryChoosers; + + + private final static byte QUESTION_SELECTED = (byte)1; + private final static byte QUESTION_NOT_SELECTED = (byte)0; + + + /** + * A very straight-forward constructor for the SimpleListCategoriesSelector + * @param allBallotQuestions all possible race questions for this election + * @param data a protobuf containing all the index categories + */ + public SimpleListCategoriesSelector(List allBallotQuestions, SimpleCategoriesSelectionData data) { + // copies the ballot race question list into a member array + this.allBallotQuestions = new BallotQuestion[allBallotQuestions.size()]; + allBallotQuestions.toArray(this.allBallotQuestions); + + // copies the shared category list (as appears in the protobuf data) into a member array + sharedDefaults = listToIntArray(data.getSharedDefaults().getQuestionIndexList()); + + // copies the category lists (as appear in the protobuf data) into a 3-dimensional member array + int[][][] selectionDataTmp = new int[data.getCategoryChooserList().size()][][]; + int channelChoiceQuestionNumber = 0; + for (CategoryChooser catChooser: data.getCategoryChooserList()) { + selectionDataTmp[channelChoiceQuestionNumber] = new int[catChooser.getCategoryList().size()][]; + int channelChoiceAnswerNumber = 0; + for (Category category: catChooser.getCategoryList()) { + selectionDataTmp[channelChoiceQuestionNumber][channelChoiceAnswerNumber] = listToIntArray(category.getQuestionIndexList()); + ++channelChoiceAnswerNumber; + } + ++channelChoiceQuestionNumber; + } + categoryChoosers = selectionDataTmp; + + // verifies in advance that there are not very suspicious indices in the selection data + assertDataValid(); + } + + /** + * asserts that the selection data does not contain a question index which is beyond the length of + * the ballot race questions array. Otherwise, throws an IndexOutOfBoundsException + */ + private void assertDataValid () { + // find the maximum question index in the selection data + int maxQuestionIndex = -1; + for (int index: sharedDefaults) { + maxQuestionIndex = Math.max(maxQuestionIndex, index); + } + for (int[][] categoryChooser: categoryChoosers) { + for (int[] category: categoryChooser) { + for (int index: category) { + maxQuestionIndex = Math.max(maxQuestionIndex, index); + } + } + } + + // asserts that the maximal question index in the selection data does not overflow the ballot race questions array + int questionsLength = allBallotQuestions.length; + if (maxQuestionIndex >= questionsLength) { + String errorMessage = "Selection data refers to question index " + maxQuestionIndex + " while we have only " + questionsLength + " questions totally"; + logger.error(errorMessage); + throw new IndexOutOfBoundsException(errorMessage); + } + } + + + /** + * an implementation of the QuestionSelector interface method. + * In this selector class the identifier simply marks all the ballot race questions which appear in at least one + * category of the categories chosen by the voter (or in the shared defaults category) in the channel choice round. + * @param channelChoiceAnswers The answers given by the voter to the channel choice questions + * @return the channel identifier + */ + @Override + public byte[] getChannelIdentifier(List channelChoiceAnswers) { + /* + * Currently, this implementation of the QuestionSelector interface returns an over-simplified identifier which + * is merely an array of booleans (which flags the questions to appear in the ballot) + * For elections with more than one possible channel we should return a more printable and recognizable + * identifier to be put in the plaintext of the ballot + */ + byte[] isSelected = new byte[allBallotQuestions.length]; + java.util.Arrays.fill(isSelected, QUESTION_NOT_SELECTED); + + for (int i: sharedDefaults) { + isSelected[i] = QUESTION_SELECTED; + } + + int channelChoiceQuestionNumber = 0; + for (BallotAnswer ballotAnswer: channelChoiceAnswers) { + assertAnswerLengthIsOne(ballotAnswer, channelChoiceQuestionNumber); + for (int i: categoryChoosers[channelChoiceQuestionNumber][(int)ballotAnswer.getAnswer(0)]) { + isSelected[i] = QUESTION_SELECTED; + } + } + + return isSelected; + } + + /** + * Verifies that the ballot answer is of length 1. (We do not yet handle multi-choice questions in the channel choice round). + * Otherwise, throws an exception. + * @param ballotAnswer the answer to verify whose length is one + * @param questionNumber the number of the question (needed only for error message strings) + */ + private void assertAnswerLengthIsOne (BallotAnswer ballotAnswer, int questionNumber) { + if (ballotAnswer.getAnswerCount() != 1) { + String errorMessage = "SimpleListCategoriesSelector expects a single answer for every channel choice question\n"; + errorMessage += "Answer to question number " + (questionNumber+1) + " is"; + for (long i : ballotAnswer.getAnswerList()) { + errorMessage += " " + i; + } + logger.error(errorMessage); + throw new IllegalArgumentException(errorMessage); + } + } + + @Override + public List selectQuestionsForVoter(byte[] channelIdentifier) { + List selectedQuestions = new ArrayList<>(); + for (int i = 0; i < channelIdentifier.length; ++i) { + if (channelIdentifier[i] == QUESTION_SELECTED) { + selectedQuestions.add(allBallotQuestions[i]); + } + } + return selectedQuestions; + } + + /** + * copies a List of Integers into an int[] array of same length + * @param l a list of Integers + * @return an array of ints + */ + private int[] listToIntArray(List l) { + int[] res = new int[l.size()]; + int index = 0; + for (Integer i: l) { + res[index] = i; + ++index; + } + return res; + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/encryptor/VBCryptoManager.java b/voting-booth/src/main/java/meerkat/voting/encryptor/VBCryptoManager.java new file mode 100644 index 0000000..04416f3 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/encryptor/VBCryptoManager.java @@ -0,0 +1,44 @@ +package meerkat.voting.encryptor; + +import meerkat.protobuf.Voting.*; + +import java.io.IOException; +import java.security.SignatureException; + +/** + * An interface for the encryptor component of the voting booth + * It handles both the encryption and the digital signature + */ +public interface VBCryptoManager { + /** + * A simple class for pairing EncrypedBallot together with its matching BallotSecrets + */ + public class EncryptionAndSecrets { + private final SignedEncryptedBallot signedEncryptedBallot; + private final BallotSecrets secrets; + + public EncryptionAndSecrets (SignedEncryptedBallot encryptedBallot, BallotSecrets secrets) { + this.signedEncryptedBallot = encryptedBallot; + this.secrets = secrets; + } + + public SignedEncryptedBallot getSignedEncryptedBallot() { + return signedEncryptedBallot; + } + + public BallotSecrets getSecrets() { + return secrets; + } + } + + + /** + * This function encrypts the plaintext ballot using the booth's keys + * @param plaintextBallot - all plaintext ballot info of the voter + * @return an encryption of the ballot + */ + // TODO: do we seed the random here? + public EncryptionAndSecrets encrypt (PlaintextBallot plaintextBallot) throws SignatureException, IOException; + + +} diff --git a/voting-booth/src/main/java/meerkat/voting/encryptor/VBCryptoManagerImpl.java b/voting-booth/src/main/java/meerkat/voting/encryptor/VBCryptoManagerImpl.java new file mode 100644 index 0000000..83fb8b0 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/encryptor/VBCryptoManagerImpl.java @@ -0,0 +1,68 @@ +package meerkat.voting.encryptor; + +import meerkat.crypto.*; +import meerkat.protobuf.Crypto.*; +import meerkat.protobuf.Voting.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.security.SignatureException; +import java.util.Random; + +/** + * A basic implementation of the VBCryptoManager interface + */ +public class VBCryptoManagerImpl implements VBCryptoManager { + + protected final static Logger logger = LoggerFactory.getLogger(VBCryptoManagerImpl.class); + + private final Random random; //TODO: Random object should be more cryptographycally secure + private final Encryption encryption; + private final DigitalSignature digitalSignature; + + + public VBCryptoManagerImpl (Random rand, Encryption encryption, DigitalSignature digitalSignature) { + this.random = rand; + this.encryption = encryption; + this.digitalSignature = digitalSignature; + } + + + @Override + public EncryptionAndSecrets encrypt(PlaintextBallot plaintextBallot) throws SignatureException, IOException { + + // TODO: do we seed the random here? + + try { + EncryptionRandomness encryptionRandomness = encryption.generateRandomness(random); + BallotSecrets secrets = BallotSecrets.newBuilder() + .setPlaintextBallot(plaintextBallot) + .setEncryptionRandomness(encryptionRandomness) + .build(); + RerandomizableEncryptedMessage encryptedMessage = encryption.encrypt(plaintextBallot, encryptionRandomness); + EncryptedBallot encBallot = EncryptedBallot.newBuilder() + .setSerialNumber(plaintextBallot.getSerialNumber()) + .setData(encryptedMessage) + .build(); + digitalSignature.updateContent(encBallot); + + SignedEncryptedBallot signedEncryptedBallot = SignedEncryptedBallot.newBuilder() + .setEncryptedBallot(encBallot) + .setSignature(digitalSignature.sign()) + .build(); + + // TODO: still has to supply RandomnessGenerationProof as well + + return new EncryptionAndSecrets(signedEncryptedBallot, secrets); + } + catch (IOException e) { + logger.error("encrypt: the encryption component has thrown an exception: " + e); + throw e; + } + catch (SignatureException e) { + logger.error("encrypt: the signature component has thrown an exception: " + e); + throw e; + } + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/output/AsyncRunnableOutputDevice.java b/voting-booth/src/main/java/meerkat/voting/output/AsyncRunnableOutputDevice.java new file mode 100644 index 0000000..0052a2c --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/output/AsyncRunnableOutputDevice.java @@ -0,0 +1,140 @@ +package meerkat.voting.output; + +import com.google.common.util.concurrent.FutureCallback; +import meerkat.protobuf.Voting.BallotSecrets; +import meerkat.protobuf.Voting.PlaintextBallot; +import meerkat.protobuf.Voting.SignedEncryptedBallot; +import meerkat.voting.controller.callbacks.ControllerCallback; +import meerkat.voting.controller.callbacks.OutputDeviceCommitCallback; +import meerkat.voting.controller.callbacks.OutputDeviceFinalizeCallback; +import meerkat.voting.output.outputcommands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ArrayBlockingQueue; + +/** + * This is a base class for simple OutputDevices which run asynchronously (as a separate thread). + * The methods of the BallotOutputDevice simply register a matching OutputCommand in the instance's queue + * The Runnable.run method simply takes the next registered command and calls the matching (abstract) method + */ +public abstract class AsyncRunnableOutputDevice implements BallotOutputDevice, Runnable { + + private Logger logger; + private ArrayBlockingQueue queue; + private volatile boolean shutDownHasBeenCalled; + + public AsyncRunnableOutputDevice() { + logger = LoggerFactory.getLogger(AsyncRunnableOutputDevice.class); + logger.info("AsyncRunnableOutputDevice is constructed"); + queue = new ArrayBlockingQueue<>(1); + shutDownHasBeenCalled = false; + } + + @Override + public void run () { + logger.info("starts running"); + while (! wasShutDownCalled()) { + try { + OutputCommand command = queue.take(); + handleSingleCommand(command); + } + catch (InterruptedException e) { + logger.warn("Interrupted while reading from command queue " + e); + } + } + } + + private boolean wasShutDownCalled () { + return shutDownHasBeenCalled; + } + + @Override + public void callShutDown() { + logger.info("callShutDown command has been called"); + shutDownHasBeenCalled = true; + queue.clear(); + } + + /** + * chooses the next method to run according to the type of the given OutputCommand + * @param command any valid OutputCommand + */ + private void handleSingleCommand(OutputCommand command) { + if (command instanceof CommitOutputCommand) { + doCommitToBallot((CommitOutputCommand)command); + } + else if (command instanceof AuditOutputCommand) { + doAudit((AuditOutputCommand)command); + } + else if (command instanceof CastOutputCommand) { + doCastBallot((CastOutputCommand)command); + } + else if (command instanceof CancelOutputCommand) { + doCancel((CancelOutputCommand)command); + } + else { + String errorMessage = "handleSingleCommand: unknown type of OutputCommand received: " + + command.getClass().getName(); + logger.error(errorMessage); + throw new RuntimeException(errorMessage); + } + } + + + @Override + public void commitToBallot(PlaintextBallot plaintextBallot, + SignedEncryptedBallot signedEncryptedBallot, + FutureCallback callback) { + logger.debug("Output interface call to commit to ballot"); + queue.clear(); + queue.add(new CommitOutputCommand(plaintextBallot, signedEncryptedBallot, (OutputDeviceCommitCallback)callback)); + } + + @Override + public void audit(BallotSecrets ballotSecrets, FutureCallback callback) { + logger.debug("an interface call to audit"); + queue.clear(); + queue.add(new AuditOutputCommand(ballotSecrets, (OutputDeviceFinalizeCallback)callback)); + } + + @Override + public void castBallot(FutureCallback callback) { + logger.debug("an interface call to cast ballot"); + queue.clear(); + queue.add(new CastOutputCommand((OutputDeviceFinalizeCallback)callback)); + } + + @Override + public void cancelBallot(FutureCallback callback) { + logger.debug("an interface call to cancel the output"); + queue.clear(); + queue.add(new CancelOutputCommand((ControllerCallback)callback)); + } + + + /** + * This method should be filled by an extending class. It should have the details of how to commit to a ballot + * @param command a CommitOutputCommand with the details and the callback + */ + abstract void doCommitToBallot(CommitOutputCommand command); + + /** + * This method should be filled by an extending class. It should have the details of how to audit the ballot + * @param command a AuditOutputCommand with the details and the callback + */ + abstract void doAudit(AuditOutputCommand command); + + /** + * This method should be filled by an extending class. It should have the details of how to cast the ballot + * @param command a CastOutputCommand with the details and the callback + */ + abstract void doCastBallot(CastOutputCommand command); + + /** + * This method should be filled by an extending class. It should have the details of how to cancel the ballot output + * @param command a CancelOutputCommand with the details and the callback + */ + abstract void doCancel(CancelOutputCommand command); + +} diff --git a/voting-booth/src/main/java/meerkat/voting/output/BallotOutputDevice.java b/voting-booth/src/main/java/meerkat/voting/output/BallotOutputDevice.java new file mode 100644 index 0000000..3909ce3 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/output/BallotOutputDevice.java @@ -0,0 +1,44 @@ +package meerkat.voting.output; + +import com.google.common.util.concurrent.FutureCallback; +import meerkat.protobuf.Voting.*; + +/** + * An interface for the device in which we output the ballots. + * Probably going to be a printer or an ethernet connection, or both. + */ +public interface BallotOutputDevice { + + /** + * Output the encrypted ballot. This is a commitment before voter chooses casting or auditing + * @param encryptedBallot - the encrypted ballot to commit to + * @param callback - a callback object which expects no return value + */ + public void commitToBallot(PlaintextBallot plaintextBallot, + SignedEncryptedBallot encryptedBallot, + FutureCallback callback); + + /** + * Voter chose 'audit'. Output the ballot secrets to prove correctness of the encryption. + * @param ballotSecrets - the secrets of the encryption + * @param callback - a callback object which expects no return value + */ + public void audit(BallotSecrets ballotSecrets, FutureCallback callback); + + /** + * Voter chose 'cast'. Finalize the ballot for use in the polling station + * @param callback - a callback object which expects no return value + */ + public void castBallot(FutureCallback callback); + + /** + * Cancelling the current ballot. This clears the state of the OutputDevice if the implementation has any such state. + * @param callback - a callback object which expects no return value + */ + public void cancelBallot(FutureCallback callback); + + /** + * A method for shutting down the Output Device + */ + public void callShutDown(); +} diff --git a/voting-booth/src/main/java/meerkat/voting/output/NetworkVirtualPrinter.java b/voting-booth/src/main/java/meerkat/voting/output/NetworkVirtualPrinter.java new file mode 100644 index 0000000..74f9970 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/output/NetworkVirtualPrinter.java @@ -0,0 +1,108 @@ +package meerkat.voting.output; + +import com.google.protobuf.BoolValue; +import com.google.protobuf.ByteString; +import meerkat.protobuf.PollingStation.ScannedData; +import meerkat.protobuf.Voting.SignedEncryptedBallot; +import meerkat.rest.Constants; +import meerkat.rest.ProtobufMessageBodyReader; +import meerkat.rest.ProtobufMessageBodyWriter; +import meerkat.voting.output.outputcommands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.client.*; +import javax.ws.rs.core.Response; +import java.io.IOException; + +import static meerkat.pollingstation.PollingStationConstants.POLLING_STATION_WEB_SCANNER_SCAN_PATH; + +/** + * A ballot output device for the network. It simply sends details over the wire + */ +public class NetworkVirtualPrinter extends AsyncRunnableOutputDevice { + + private static final Logger logger = LoggerFactory.getLogger(NetworkVirtualPrinter.class); + private ByteString channelIdentifier; + private SignedEncryptedBallot signedEncryptedBallot; + private final WebTarget successfulPrintTarget; + + public NetworkVirtualPrinter(String address) { + super(); + logger.info("A NetworkVirtualPrinter is constructed"); + Client client = ClientBuilder.newClient(); + client.register(ProtobufMessageBodyReader.class); + client.register(ProtobufMessageBodyWriter.class); + successfulPrintTarget = client.target(address).path(POLLING_STATION_WEB_SCANNER_SCAN_PATH); + resetState(); + } + + + /** + * The NetworkVirtualPrinter actually does nothing for committing. + * It simply keeps the ballot details for later. + * When the voter chooses to Cast the ballot, these details are sent over the wire. + * @param command a CommitOutputCommand with the signed encryption of the ballot + */ + public void doCommitToBallot(CommitOutputCommand command) { + logger.debug("entered method doCommitToBallot"); + channelIdentifier = command.getChannelIdentifierByteString(); + signedEncryptedBallot = command.getSignedEncryptedBallot(); + command.getCallback().onSuccess(null); + } + + + /** + * The NetworkVirtualPrinter actually does nothing for auditing. + * @param command a AuditOutputCommand with the details and the callback + */ + public void doAudit(AuditOutputCommand command) { + logger.debug("entered method doAudit"); + resetState(); + command.getCallback().onSuccess(null); + } + + + /** + * This is where the magic happens. The signed encrypted ballot is transmitted over the wire + * @param command a CastOutputCommand with the details and the callback + */ + public void doCastBallot(CastOutputCommand command) { + logger.debug("entered method doCastBallot"); + ScannedData scannedData = ScannedData.newBuilder() + .setChannel(channelIdentifier) + .setSignedEncryptedBallot(this.signedEncryptedBallot) + .build(); + + Response response = successfulPrintTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(scannedData, Constants.MEDIATYPE_PROTOBUF)); + BoolValue b = response.readEntity(BoolValue.class); + response.close(); + + resetState(); + + if (b.getValue()) { + command.getCallback().onSuccess(null); + } + else { + command.getCallback().onFailure(new IOException()); + } + } + + + /** + * The NetworkVirtualPrinter actually does nothing for canceling. + * @param command a CancelOutputCommand with the callback + */ + public void doCancel(CancelOutputCommand command) { + logger.debug("entered method doCancel"); + resetState(); + command.getCallback().onSuccess(null); + } + + + private void resetState() { + channelIdentifier = null; + signedEncryptedBallot = null; + } + +} diff --git a/voting-booth/src/main/java/meerkat/voting/output/SystemConsoleOutputDevice.java b/voting-booth/src/main/java/meerkat/voting/output/SystemConsoleOutputDevice.java new file mode 100644 index 0000000..4f50c27 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/output/SystemConsoleOutputDevice.java @@ -0,0 +1,106 @@ +package meerkat.voting.output; + +import com.google.protobuf.ByteString; +import meerkat.protobuf.Crypto.*; +import meerkat.protobuf.Voting.*; +import meerkat.voting.output.outputcommands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A toy OutputDevice class + * outputs everything simply to the System console + */ +public class SystemConsoleOutputDevice extends AsyncRunnableOutputDevice { + + private static final Logger logger = LoggerFactory.getLogger(SystemConsoleOutputDevice.class); + + public SystemConsoleOutputDevice () { + super(); + logger.info("A SystemConsoleOutputDevice is constructed"); + } + + /** + * Committing to the ballot. + * Simply prints to the output stream all the details in the CommitOutputCommand. + * @param command details to commit to, and the callback to call when finished + */ + public void doCommitToBallot(CommitOutputCommand command) { + logger.debug("entered method doCommitToBallot"); + PlaintextBallot plaintextBallot = command.getPlaintext(); + long plaintextSerialNumber = plaintextBallot.getSerialNumber(); + System.out.println("Commitment of Ballot #" + plaintextSerialNumber); + System.out.println("(channel): "); + System.out.println(bytesToString(command.getChannelIdentifierByteString())); + System.out.println("(plaintext): "); + System.out.println(plaintextBallot); + SignedEncryptedBallot signedEncryptedBallot = command.getSignedEncryptedBallot(); + long encryptedSerialNumber = signedEncryptedBallot.getEncryptedBallot().getSerialNumber(); + System.out.println("Commitment of Ballot #" + encryptedSerialNumber + " (ciphertext):"); + if (plaintextSerialNumber != encryptedSerialNumber) { + logger.error("plaintext and encryption serial numbers do not match!! plaintext# = " + + plaintextSerialNumber + ", ciphertext# = " + encryptedSerialNumber); + } + ByteString encryptedData = signedEncryptedBallot.getEncryptedBallot().getData().getData(); + System.out.println(bytesToString(encryptedData)); + command.getCallback().onSuccess(null); + } + + + /** + * auditing the ballot. + * prints to the output stream the ballot secrets (the encryption randomness and its proof of random generation) + * @param command An auditing command with the callback to finally call + */ + public void doAudit(AuditOutputCommand command) { + logger.debug("entered method doAudit"); + System.out.println("Auditing"); + BallotSecrets ballotSecrets = command.getBallotSecrets(); + printEncryptionRandomness(ballotSecrets.getEncryptionRandomness()); + printRandomnessGenerationProof (ballotSecrets.getProof()); + command.getCallback().onSuccess(null); + } + + /** + * Casting the ballot (actually does nothing new) + * @param command a CastOutputCommand with the details and the callback + */ + public void doCastBallot(CastOutputCommand command) { + logger.debug("entered method doCastBallot"); + System.out.println("Ballot finalized for casting!"); + command.getCallback().onSuccess(null); + } + + + /** + * Canceling the ballot (actually does nothing new) + * @param command a CancelOutputCommand with the details and the callback + */ + public void doCancel(CancelOutputCommand command) { + logger.debug("entered method doCancel"); + System.out.println("Ballot cancelled!"); + command.getCallback().onSuccess(null); + } + + + private void printEncryptionRandomness (EncryptionRandomness encryptionRandomness) { + System.out.println("Encryption Randomness = "); + ByteString data = encryptionRandomness.getData(); + System.out.println(bytesToString(data)); + } + + private void printRandomnessGenerationProof (RandomnessGenerationProof proof) { + System.out.println("Proof of randomness generation:"); + ByteString data = proof.getData(); + System.out.println(bytesToString(data)); + } + + + /* + * Returns the UTF8 decoding of byte-string data + */ + private static String bytesToString(ByteString data) { + return data.toStringUtf8(); + } + +} diff --git a/voting-booth/src/main/java/meerkat/voting/output/outputcommands/AuditOutputCommand.java b/voting-booth/src/main/java/meerkat/voting/output/outputcommands/AuditOutputCommand.java new file mode 100644 index 0000000..0d4d6ea --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/output/outputcommands/AuditOutputCommand.java @@ -0,0 +1,22 @@ +package meerkat.voting.output.outputcommands; + +import meerkat.protobuf.Voting.*; +import meerkat.voting.controller.callbacks.ControllerCallback; + +/** + * This OutputCommand supplies the necessary details for outputting Audit information + */ +public class AuditOutputCommand extends OutputCommand { + + private final BallotSecrets ballotSecrets; + + public AuditOutputCommand(BallotSecrets ballotSecrets, ControllerCallback callback) { + super(callback); + this.ballotSecrets = ballotSecrets; + } + + public BallotSecrets getBallotSecrets() { + return ballotSecrets; + } + +} diff --git a/voting-booth/src/main/java/meerkat/voting/output/outputcommands/CancelOutputCommand.java b/voting-booth/src/main/java/meerkat/voting/output/outputcommands/CancelOutputCommand.java new file mode 100644 index 0000000..88fe03f --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/output/outputcommands/CancelOutputCommand.java @@ -0,0 +1,14 @@ +package meerkat.voting.output.outputcommands; + +import meerkat.voting.controller.callbacks.ControllerCallback; + +/** + * This OutputCommand signals the output-device that it should Cancel the rest of the ballot output + */ +public class CancelOutputCommand extends OutputCommand { + + public CancelOutputCommand(ControllerCallback callback) { + super(callback); + } + +} diff --git a/voting-booth/src/main/java/meerkat/voting/output/outputcommands/CastOutputCommand.java b/voting-booth/src/main/java/meerkat/voting/output/outputcommands/CastOutputCommand.java new file mode 100644 index 0000000..5098cd5 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/output/outputcommands/CastOutputCommand.java @@ -0,0 +1,14 @@ +package meerkat.voting.output.outputcommands; + +import meerkat.voting.controller.callbacks.ControllerCallback; + +/** + * This OutputCommand signals the output-device that the voter wishes to Cast the ballot + */ +public class CastOutputCommand extends OutputCommand { + + public CastOutputCommand(ControllerCallback callback) { + super(callback); + } + +} diff --git a/voting-booth/src/main/java/meerkat/voting/output/outputcommands/CommitOutputCommand.java b/voting-booth/src/main/java/meerkat/voting/output/outputcommands/CommitOutputCommand.java new file mode 100644 index 0000000..42c9307 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/output/outputcommands/CommitOutputCommand.java @@ -0,0 +1,34 @@ +package meerkat.voting.output.outputcommands; + +import com.google.protobuf.ByteString; +import meerkat.protobuf.Voting.*; +import meerkat.voting.controller.callbacks.ControllerCallback; + +/** + * This OutputCommand supplies the necessary details for outputting a commit to the ballot + */ +public class CommitOutputCommand extends OutputCommand { + + private final PlaintextBallot plaintextBallot; + private final SignedEncryptedBallot signedEncryptedBallot; + + public CommitOutputCommand(PlaintextBallot plaintextBallot, + SignedEncryptedBallot signedEncryptedBallot, + ControllerCallback callback) { + super(callback); + this.plaintextBallot = plaintextBallot; + this.signedEncryptedBallot = signedEncryptedBallot; + } + + public ByteString getChannelIdentifierByteString() { + return plaintextBallot.getChannelIdentifier(); + } + + public PlaintextBallot getPlaintext() { + return plaintextBallot; + } + + public SignedEncryptedBallot getSignedEncryptedBallot() { + return signedEncryptedBallot; + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/output/outputcommands/OutputCommand.java b/voting-booth/src/main/java/meerkat/voting/output/outputcommands/OutputCommand.java new file mode 100644 index 0000000..89591fb --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/output/outputcommands/OutputCommand.java @@ -0,0 +1,18 @@ +package meerkat.voting.output.outputcommands; + +import meerkat.voting.controller.callbacks.ControllerCallback; + +/** + * Base class for the commands to put in the output-device queue + */ +public abstract class OutputCommand { + protected final ControllerCallback callback; + + protected OutputCommand(ControllerCallback callback) { + this.callback = callback; + } + + public ControllerCallback getCallback () { + return callback; + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/storage/StorageManager.java b/voting-booth/src/main/java/meerkat/voting/storage/StorageManager.java new file mode 100644 index 0000000..4a70f79 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/storage/StorageManager.java @@ -0,0 +1,49 @@ +package meerkat.voting.storage; + +import meerkat.protobuf.Voting.*; + +import java.io.IOException; +import java.util.Map; + +/** + * An interface for the storage component of the voting booth + */ +public interface StorageManager { + + /** + * Detect whether an administration key is inserted to the machine. This determines if we gointo the set-up flow or the voting session flow. + * @return True is a hardware key is inserted. False if not. + */ + public boolean isAdminHardwareKeyInserted(); + + /** + * load the election params from the storage. + * @return the current election params + * @throws IOException + */ + public ElectionParams readElectionParams () throws IOException; + + /** + * write the election parameters protobuf to the storage + * @param params ElectionParams protobuf to save + * @throws IOException + */ + public void writeElectionParams(ElectionParams params) throws IOException; + + + public Map readSystemMessages() throws IOException; + + // These are just static key identifiers for accessing the matching System Messages in the message map + public final static String WAIT_FOR_COMMIT_MESSAGE = "waitForCommit"; + public final static String WAIT_FOR_AUDIT_MESSAGE = "waitForAudit"; + public final static String WAIT_FOR_CAST_MESSAGE = "waitForCast"; + public final static String RESTART_VOTING_BUTTON = "restartVotingButton"; + public final static String UNRECOGNIZED_FINALIZE_RESPONSE_MESSAGE = "unrecognizedFinalizeResponse"; + public final static String UNSUCCESSFUL_CHANNEL_CHOICE_MESSAGE = "unsuccessfulChannelChoice"; + public final static String OUTPUT_DEVICE_FAILURE_MESSAGE = "outputDeviceFailure"; + public final static String UNSUCCESSFUL_VOTING_MESSAGE = "unsuccessfulVoting"; + public final static String SOMETHING_WRONG_MESSAGE = "somethingWrong"; + public final static String ENCRYPTION_FAILED_MESSAGE = "encryptionFailed"; + public final static String RETRY_BUTTON = "retryButton"; + public final static String CANCEL_VOTE_BUTTON = "cancelVoteButton"; +} diff --git a/voting-booth/src/main/java/meerkat/voting/storage/StorageManagerMockup.java b/voting-booth/src/main/java/meerkat/voting/storage/StorageManagerMockup.java new file mode 100644 index 0000000..f556e3b --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/storage/StorageManagerMockup.java @@ -0,0 +1,90 @@ +package meerkat.voting.storage; + +import meerkat.protobuf.Voting.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Map; + +/** + * A mockup for the StorageManager interface + * Currently keeps the ElectionParams in a file in the user's home-directory + */ +public class StorageManagerMockup implements StorageManager { + + private static final Logger logger = LoggerFactory.getLogger(StorageManagerMockup.class); + + public static final String electionParamFullFilename = "/home/hai/meerkat-java/meerkat_election_params_tempfile.dat"; + public static final String systemMessagesFilename = "/home/hai/meerkat-java/meerkat_booth_system_messages.dat"; + + private boolean adminHardwareKeyInserted; + + + public StorageManagerMockup () { + logger.info("A StorageManagerMockup is constructed"); + this.adminHardwareKeyInserted = false; + } + + @Override + public boolean isAdminHardwareKeyInserted() { + logger.info("Entered method isAdminHardwareKeyInserted"); + logger.warn("isAdminHardwareKeyInserted is not yet fully implemented. It does not check the file system. " + + "Rather it just returns a private boolean member"); + return adminHardwareKeyInserted; + } + + @Override + public ElectionParams readElectionParams() throws IOException { + logger.info("Entered method readElectionParams"); + ElectionParams params; + try { + FileInputStream inputStream = new FileInputStream(electionParamFullFilename); + params = ElectionParams.parseFrom(inputStream); + inputStream.close(); + logger.info ("Successfully read election parameter protobuf from a file"); + } + catch (IOException e) { + logger.error("Could not read from the election parameter file: '" + electionParamFullFilename + "'."); + throw e; + } + return params; + } + + @Override + public void writeElectionParams(ElectionParams params) throws IOException { + logger.info("Entered method writeElectionParams"); + try { + FileOutputStream output = new FileOutputStream(electionParamFullFilename); + params.writeTo(output); + output.close(); + logger.info ("Successfully wrote election parameter protobuf to a file"); + } + catch (IOException e) { + logger.error("Could not write to the election parameter file: '" + electionParamFullFilename + "'."); + throw e; + } + } + + + @Override + public Map readSystemMessages() throws IOException { + + logger.info("Entered method readSystemMessages"); + BoothSystemMessages systemMessages; + try { + FileInputStream inputStream = new FileInputStream(systemMessagesFilename); + systemMessages = BoothSystemMessages.parseFrom(inputStream); + inputStream.close(); + logger.info ("Successfully read systemMessages protobuf from a file"); + } + catch (IOException e) { + logger.error("Could not read from the systemMessages file: '" + systemMessagesFilename + "'."); + throw e; + } + return systemMessages.getSystemMessage(); + } + +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/CommandPend.java b/voting-booth/src/main/java/meerkat/voting/ui/CommandPend.java new file mode 100644 index 0000000..a12d3ac --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/CommandPend.java @@ -0,0 +1,53 @@ +package meerkat.voting.ui; + +import java.util.concurrent.ArrayBlockingQueue; + +/** + * A special kind of an ArrayBlockingQueue. + * It is only of size 1, meaning it keeps only one element at most at every point of time. + * The trample function is similar to put/add except that it overrides the previously kept element. + * Other functions are similar to the matching functions in ArrayBlockingQueue. + * For instance, the offer function only "recommends" another element to keep, but if there is a stored element + * already, than this recommendation is ignored. + */ +public class CommandPend { + + private ArrayBlockingQueue queue; + + public CommandPend () { + queue = new ArrayBlockingQueue<>(1); + } + + /** + * overrides the current kept command + * @param cmd a command to override the previous one (if existed) + */ + synchronized public void trample (T cmd) { + queue.clear(); + queue.add(cmd); + } + + /** + * keeps the offered command, but only if there is no other command to handle right now + * @param cmd a command to keep if we currently do not have another + */ + synchronized public void offer (T cmd) { + queue.offer(cmd); + } + + /** + * Retrieves and removes the kept command, waiting if necessary until a command becomes available. + * @return the kept command + * @throws InterruptedException + */ + public T take() throws InterruptedException { + return queue.take(); + } + + /** + * removes the kept command + */ + synchronized public void clear () { + queue.clear(); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/SystemConsoleUI.java b/voting-booth/src/main/java/meerkat/voting/ui/SystemConsoleUI.java new file mode 100644 index 0000000..3b34da8 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/SystemConsoleUI.java @@ -0,0 +1,533 @@ +package meerkat.voting.ui; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.protobuf.ByteString; +import meerkat.protobuf.Voting.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.*; + +import meerkat.voting.controller.callbacks.*; +import meerkat.voting.ui.uicommands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static java.lang.System.in; + + +/** + * an asynchronous thread implementation of the VotingBoothUI interface + * This is a mock-up implementation using just the console as our UI device + */ +public class SystemConsoleUI implements VotingBoothUI, Runnable { + + private static final Logger logger = LoggerFactory.getLogger(SystemConsoleUI.class); + + private BufferedReader bufferedReader; + private CommandPend cmdPend; + private Date startWaitingTime; + + private volatile boolean shutDownHasBeenCalled; + + + public SystemConsoleUI() { + final int tickDurationInMillisec = 10; // period between view update calls + + logger.info("A VB UI console is constructed"); + cmdPend = new CommandPend<>(); + bufferedReader = new BufferedReader(new InputStreamReader(in)); + + startWaitingTime = null; + Timer timer = new Timer(); + timer.scheduleAtFixedRate(new TickerTimerTask(cmdPend), new Date(), tickDurationInMillisec); + + shutDownHasBeenCalled = false; + } + + + /** + * the run() method. Simply loops and takes the UI's pending command and handles it accordingly + */ + @Override + public void run () { + logger.info("UI starts running"); + while (! wasShutDownCalled()) { + try { + UICommand command = cmdPend.take(); + handleSingleCommand(command); + } + catch (InterruptedException e) { + logger.warn ("Interrupted while reading the pending command " + e); + } + } + } + + + @Override + public void callShutDown() { + logger.info("callShutDown command has been called"); + shutDownHasBeenCalled = true; + stopWaiting(); + cmdPend.clear(); + } + + /** + * chooses the next method to run according to the type of the given UICommand. + * Special case for the TickCommand. + * As this command is registered in the queue constantly, we simply ignore this command if the UI is not in + * a waiting state + * @param command any valid UICommand + */ + private void handleSingleCommand(UICommand command) { + if (!(command instanceof TickCommand)) { + if (startWaitingTime != null) { + stopWaiting(); + } + } + + if (command instanceof StartSessionUICommand) { + doShowWelcomeScreen((StartSessionUICommand)command); + } + else if (command instanceof ChannelChoiceUICommand) { + doAskChannelChoiceQuestions((ChannelChoiceUICommand)command); + } + else if (command instanceof RaceVotingUICommand) { + doAskVotingQuestions((RaceVotingUICommand)command); + } + else if (command instanceof CastOrAuditUICommand) { + doCastOrAudit ((CastOrAuditUICommand)command); + } + else if (command instanceof FatalErrorUICommand) { + doFatalError((FatalErrorUICommand)command); + } + else if (command instanceof WaitForFinishUICommand) { + doWaitForFinish((WaitForFinishUICommand)command); + } + else if (command instanceof TickCommand) { + doTick (); + } + else { + String errorMessage = "handleSingleCommand: unknown type of UICommand received: " + + command.getClass().getName(); + logger.error(errorMessage); + throw new RuntimeException(errorMessage); + } + } + + + /** + * start a new session by registering a StartSessionUICommand + * @param callback - a boolean future callback to return when done + */ + @Override + public void startNewVoterSession(FutureCallback callback) { + logger.debug("UI interface call to startNewVoterSession"); + cmdPend.trample(new StartSessionUICommand((NewVoterCallback)callback)); + } + + /** + * welcomes the new voter at the beginning of the session + * @param command a StartSessionUICommand with a acallback + */ + private void doShowWelcomeScreen(StartSessionUICommand command) { + logger.debug("UI entered doShowWelcomeScreen"); + System.out.println("Welcome, new voter!"); + waitForEnter(null); + ControllerCallback callback = command.getCallback(); + callback.onSuccess(null); + } + + /** + * marks that the waiting, for something else to have happened, is finished + */ + private void stopWaiting () { + System.out.println (); + startWaitingTime = null; + + } + + /** + * waits until the ENTER key is pressed in the console + * @param message a message to show the user in the console + */ + private void waitForEnter(String message) { + if (message != null) { + System.out.println(message); + } + System.out.println("\nPress ENTER to proceed.\n"); + String t = null; + while (t == null) { + try { + t = readInputLine(); + } + catch (IOException e) { + String errorMessage = "waitForEnter: threw an IOException: " + e; + logger.error(errorMessage); + System.err.println(errorMessage); + } + } + } + + /** + * call for the channel choice phase by registering a ChannelChoiceUICommand in the queue + * @param questions questions to determine the right voting channel for this voter + * @param callback that's where we store the answers to decide channel upon for the current voter + */ + @Override + public void chooseChannel(List questions, FutureCallback> callback) { + logger.debug("UI interface call to chooseChannel"); + cmdPend.trample(new ChannelChoiceUICommand(questions, (ChannelChoiceCallback)callback)); + } + + /** + * lists the channel choice questions to the voter and gathers the voter's answers + * @param command a ChannelChoiceUICommand with the data and a callback + */ + private void doAskChannelChoiceQuestions (ChannelChoiceUICommand command) { + logger.debug("UI: doAskChannelChoiceQuestions"); + System.out.println("Showing questions for choosing channel:\n"); + try { + List answers = askVoterForAnswers(command.getQuestions()); + command.getCallback().onSuccess(answers); + } + catch (VoterCancelThrowable e) { + command.getCallback().onFailure(e); + } + catch (IOException e) { + String errorMessage = "Channel choice failed due to IOException: " + e; + logger.error (errorMessage); + System.err.println(errorMessage); + command.getCallback().onFailure(e); + } + } + + /** + * call for the race voting question phase by registering a RaceVotingUICommand in the queue + * @param questions all ballot questions to present to the voter + * @param callback the responses to the questions collected by the UI, to send back to the controller. Responses are null if voter chose to cancel session + */ + @Override + public void askVoterQuestions(List questions, FutureCallback> callback) { + logger.debug("UI interface call to chooseChannel"); + cmdPend.trample(new RaceVotingUICommand(questions, (VotingCallback)callback)); + } + + /** + * lists the race voting questions to the voter and gathers the voter's answers + * @param command a RaceVotingUICommand with a callback + */ + private void doAskVotingQuestions (RaceVotingUICommand command) { + logger.debug("UI: doAskVotingQuestions"); + System.out.println("Showing questions for race voting:\n"); + try { + List answers = askVoterForAnswers(command.getQuestions()); + command.getCallback().onSuccess(answers); + } + catch (VoterCancelThrowable e) { + command.getCallback().onFailure(e); + } + catch (IOException e) { + String errorMessage = "Asking voting questions failed due to IOException: " + e; + logger.error (errorMessage); + System.err.println(errorMessage); + command.getCallback().onFailure(e); + } + } + + /** + * call for the cast-or-audit phase by registering a CastOrAuditUICommand in the queue + * @param callback the returned choice of how to finalize the ballot + */ + @Override + public void castOrAudit(FutureCallback callback) { + logger.debug("UI interface call to castOrAudit"); + cmdPend.trample(new CastOrAuditUICommand((CastOrAuditCallback)callback)); + } + + /** + * asks the voter whether to cast or audit the ballot + * @param command a simple CastOrAuditUICommand with the callback + */ + private void doCastOrAudit(CastOrAuditUICommand command) { + logger.debug("UI entered doCastOrAudit"); + System.out.println ("Finalizing your vote. Do you wish to (C)ast or (A)udit?"); + + FinalizeBallotChoice fChoice; + + try { + String s = readInputLine(); + if (s.equals("cast") || s.equals("c")) { + fChoice = FinalizeBallotChoice.CAST; + } + else if (s.equals("audit") || s.equals("a")) { + fChoice = FinalizeBallotChoice.AUDIT; + } + else { + throw new IllegalArgumentException("UI could not understand the answer for cast/audit question '" + s + "'"); + } + ControllerCallback callback = command.getCallback(); + assert (callback instanceof CastOrAuditCallback); + ((CastOrAuditCallback)callback).onSuccess(fChoice); + } + catch (IllegalArgumentException|IOException e) { + String errorMessage = "doCastOrAudit: some error with reading input from console. details: " + e; + logger.error(errorMessage); + command.getCallback().onFailure(e); + } + } + + + /** + * makes the UI (and voter) wait for something else to happen, by registering a WaitForFinishUICommand in the queue + * @param message a message to show the user on the UI device while waiting + * @param callback a success return value of the wait (cancelling returns false) + */ + @Override + public void notifyVoterToWaitForFinish(UIElement message, FutureCallback callback) { + logger.debug("UI interface call to notifyVoterToWaitForFinish"); + cmdPend.trample(new WaitForFinishUICommand(message, (WaitForFinishCallback)callback)); + } + + /** + * Tells the voter (in the console) to wait until some other process is finished + * @param command a simple WaitForFinishUICommand with the callback + */ + public void doWaitForFinish (WaitForFinishUICommand command) { + logger.debug("UI entered doWaitForFinish"); + + startWaitingTime = new Date(); + + UIElement message = command.getMessage(); + String messageString; + if (message.getType() != UIElementDataType.TEXT) { + messageString = "Default message: encountered an error. System halting"; + } else { + messageString = bytesToString(message.getData()); + } + System.out.println(messageString); + System.out.print ("Waiting : ."); + } + + /** + * show an error to the voter. Halts the system until a technician handles it + * @param errorMessage message to show in UI device + * @param callback returns interrupt + */ + @Override + public void showErrorMessageAndHalt(UIElement errorMessage, FutureCallback callback) { + logger.debug("UI interface call to showErrorMessageAndHalt"); + throw new UnsupportedOperationException("Not implemented becuase currently not sure if we ever use it."); + } + + /** + * show an error to the voter. let him press a (chosen) button for handling the error. + * @param errorMessage message to show in UI device + * @param buttonLabels labels for buttons to present to voter + * @param callback the number of the selected button + */ + @Override + public void showErrorMessageWithButtons(UIElement errorMessage, UIElement[] buttonLabels, FutureCallback callback) { + logger.debug("UI interface call to showErrorMessageWithButtons"); + cmdPend.trample(new FatalErrorUICommand(errorMessage, buttonLabels, (ControllerCallback)callback)); + } + + /** + * show an error to the voter. let him press a (chosen) button for handling the error. + * @param command a FatalErrorUICommand with the callback + */ + private void doFatalError (FatalErrorUICommand command) { + logger.debug("UI entered doFatalError"); + + UIElement errorMessage = command.getErrorMessage(); + String errorMessageString; + if (errorMessage.getType() != UIElementDataType.TEXT) { + errorMessageString = "Default message: encountered an error. System halting"; + } else { + errorMessageString = bytesToString(errorMessage.getData()); + } + + UIElement[] buttonLabels = command.getButtonLabels(); + String[] buttonLabelStrings = new String[buttonLabels.length]; + for (int i = 0; i < buttonLabels.length; ++i) { + if (buttonLabels[i].getType() != UIElementDataType.TEXT) { + buttonLabelStrings[i] = ""; + } else { + buttonLabelStrings[i] = bytesToString(errorMessage.getData()); + } + } + + System.out.println(errorMessageString); + for (int i = 0; i < buttonLabelStrings.length; ++i) { + System.out.println("" + i + " - " + buttonLabelStrings[i]); + } + + try { + String s = readInputLine(); + Integer chosenButton = new Integer(s); + command.getCallback().onSuccess(chosenButton); + } + catch (IOException e) { + String err = "doFatalError: some error with reading input from console. details: " + e; + logger.error(err); + command.getCallback().onFailure(e); + } + + } + + + /** + * this method is run when a TickCommand was received while in waiting state + */ + private void doTick () { + if (startWaitingTime != null) { + System.out.print ("."); // still waiting + } + } + + /** + * get an input line from the console + * @return a line from the voter + * @throws IOException + */ + private String readInputLine() throws IOException{ + String s; + try { + s = this.bufferedReader.readLine(); + if (null == s) { + throw new IOException(); + } + } catch (IOException e) { + String errorMessage = "readInputLine: some error with reading input from console. details: " + e; + logger.error(errorMessage); + throw new IOException(e); + } + return s; + } + + + /** + * asserts that the question data matches the types that we can handle in the ConsoleUI + * (better and more sophisticated UI will be able to handle more types of data) + * @param questions list of the questions + */ + private void assertQuestionsAreValid (List questions) { + for (int index = 0; index < questions.size(); ++index) { + BallotQuestion question = questions.get(index); + if (question.getIsMandatory()) { + String errorMessage = "askVoterQuestions: question number " + index + " is marked as mandatory"; + logger.error(errorMessage); + throw new UnsupportedOperationException(errorMessage); + } + if (!isQuestionOnlyText(question)) { + String errorMessage = "askVoterQuestions: question number " + index + " is not only text"; + logger.error(errorMessage); + throw new UnsupportedOperationException(errorMessage); + } + } + } + + + /** + * present the questions to the voter console sequentially. + * Voter may choose at any time to skip a question, go back or even cancel the whole session + * @param questions list of questions to present + * @return list of answers to the questions (at the same order) + * @throws VoterCancelThrowable this is thrown if a voter chose to cancel in the middle of the process + * @throws IOException + */ + private List askVoterForAnswers(List questions) throws VoterCancelThrowable, IOException { + + assertQuestionsAreValid (questions); + + List answers = new ArrayList<>(); + int index = 0; + while (index < questions.size()) { + BallotQuestion question = questions.get(index); + System.out.println("Question number " + index); + showQuestionInConsole(question); + + System.out.println("UI screen: Enter your answer. You can also type '(b)ack' or '(c)ancel' or '(s)kip"); + String s = readInputLine(); + + if ((s.equals("cancel") || s.equals("c")) || (index == 0 && (s.equals("back") || s.equals("b")))) { + throw new VoterCancelThrowable(); + } + else if (s.equals("back") || s.equals("b")) { + --index; + answers.remove(index); + } + else if (s.equals("skip") || s.equals("s")) { + answers.add(translateStringAnswerToProtoBufMessageAnswer("")); + ++index; + } + else { + answers.add(translateStringAnswerToProtoBufMessageAnswer(s)); + ++index; + } + } + return answers; + } + + /** + * present a question in the console to the voter + * @param question a text ballot question + */ + private void showQuestionInConsole(BallotQuestion question) { + if (!isQuestionOnlyText(question)) { + System.err.println("debug: an element in question is not of TEXT type"); + throw new UnsupportedOperationException(); + } + + System.out.println("Question text: " + bytesToString(question.getQuestion().getData())); + System.out.println("Description: " + bytesToString(question.getDescription().getData())); + int answerIndex = 0; + for (UIElement answer : question.getAnswerList()) { + ++answerIndex; + System.out.println("Answer " + answerIndex + ": " + bytesToString(answer.getData())); + } + } + + /** + * checks whether the data of the question is only text. This is the only type we can handle in ConsoleUI + * @param question a ballot question to check + * @return True if the question data is only text + */ + private boolean isQuestionOnlyText (BallotQuestion question) { + boolean isText = true; + if (question.getQuestion().getType() != UIElementDataType.TEXT + || question.getDescription().getType() != UIElementDataType.TEXT) { + isText = false; + } + for (UIElement answer : question.getAnswerList()) { + if (answer.getType() != UIElementDataType.TEXT) { + isText = false; + } + } + return isText; + } + + + private BallotAnswer translateStringAnswerToProtoBufMessageAnswer(String s) { + BallotAnswer.Builder bab = BallotAnswer.newBuilder(); + StringTokenizer st = new StringTokenizer(s); + while (st.hasMoreTokens()) { + bab.addAnswer(Integer.parseInt(st.nextToken())); + } + return bab.build(); + } + + /* + * Returns the UTF8 decoding of byte-string data + */ + private static String bytesToString(ByteString data) { + return data.toStringUtf8(); + } + + + private boolean wasShutDownCalled () { + return shutDownHasBeenCalled; + } + +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/TickerTimerTask.java b/voting-booth/src/main/java/meerkat/voting/ui/TickerTimerTask.java new file mode 100644 index 0000000..42924c2 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/TickerTimerTask.java @@ -0,0 +1,21 @@ +package meerkat.voting.ui; + +import meerkat.voting.ui.uicommands.TickCommand; +import meerkat.voting.ui.uicommands.UICommand; + +import java.util.TimerTask; + +/** + * A thread that sends the UI clock TickCommands in a given constant frequency + */ +class TickerTimerTask extends TimerTask { + private CommandPend cmdPend; + public TickerTimerTask (CommandPend cmdPend) { + this.cmdPend = cmdPend; + } + + @Override + public void run() { + cmdPend.offer(new TickCommand(null)); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/VotingBoothUI.java b/voting-booth/src/main/java/meerkat/voting/ui/VotingBoothUI.java new file mode 100644 index 0000000..c4e1f1e --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/VotingBoothUI.java @@ -0,0 +1,82 @@ +package meerkat.voting.ui; + +import com.google.common.util.concurrent.FutureCallback; +import meerkat.protobuf.Voting.*; + +import java.util.List; + + +/** + * An interface for the UI component of the voting booth + */ +public interface VotingBoothUI { + + /** + * a simple enum for the voter's finalize choice + */ + public enum FinalizeBallotChoice { + CAST, + AUDIT + } + + /** + * shut down the UI + */ + public void callShutDown(); + + /** + * Starts a new session for a voter. Presents whatever initial info is decided to show at the beginning + * @param callback - a boolean future callback to return when done + */ + public void startNewVoterSession (FutureCallback callback); + + /** + * Present a question to the voter to decide on his voting channel. + * @param questions questions to determine the right voting channel for this voter + * @param callback that's where we store the answers to decide channel upon for the current voter + */ + public void chooseChannel (List questions, FutureCallback> callback); + + /** + * Presents the set of questions to the voter. Collect all his responses. + * @param questions all ballot questions to present to the voter + * @param callback the responses to the questions collected by the UI, to send back to the controller. Responses are null if voter chose to cancel session + */ + public void askVoterQuestions (List questions, FutureCallback> callback); + + /** + * Get a response from the voter on how to finalize the ballot. + * @param callback the returned choice of how to finalize the ballot + */ + public void castOrAudit (FutureCallback callback); + + + + // Admin scenario methods + //TODO: the admin scenario still needs some more thinking + + + /** + * present a wait-for-finish screen to the voter + * @param message a message to show the user on the UI device while waiting + * @param callback a success return value of the wait (cancelling returns false) + */ + public void notifyVoterToWaitForFinish (UIElement message, FutureCallback callback); + + /** + * show a fatal error message in the UI. Halts system. Waits for administrator interrupt or reset + * @param errorMessage message to show in UI device + * @param callback returns interrupt + */ + public void showErrorMessageAndHalt (UIElement errorMessage, FutureCallback callback); + + /** + * show an error message and let user press his chosen button for continuation + * @param errorMessage message to show in UI device + * @param buttonLabels labels for buttons to present to voter + * @param callback the number of the selected button + */ + public void showErrorMessageWithButtons (UIElement errorMessage, UIElement[] buttonLabels, + FutureCallback callback); + +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/uicommands/CastOrAuditUICommand.java b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/CastOrAuditUICommand.java new file mode 100644 index 0000000..583a4a2 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/CastOrAuditUICommand.java @@ -0,0 +1,13 @@ +package meerkat.voting.ui.uicommands; + +import meerkat.voting.controller.callbacks.*; +import meerkat.voting.ui.VotingBoothUI.FinalizeBallotChoice; + +/** + * This command signals the UI that the voter should now choose whether to Cast or Audit the ballot + */ +public class CastOrAuditUICommand extends UICommand { + public CastOrAuditUICommand(ControllerCallback callback) { + super(callback); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/uicommands/ChannelChoiceUICommand.java b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/ChannelChoiceUICommand.java new file mode 100644 index 0000000..185e255 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/ChannelChoiceUICommand.java @@ -0,0 +1,24 @@ +package meerkat.voting.ui.uicommands; + +import meerkat.protobuf.Voting.*; +import meerkat.voting.controller.callbacks.*; + +import java.util.List; + +/** + * This command signals the UI to present channel choice questions to the voter and send back the answers + */ +public class ChannelChoiceUICommand extends UICommand> { + + private final List questions; + + public ChannelChoiceUICommand(List questions, ControllerCallback> callback) + { + super(callback); + this.questions = questions; + } + + public List getQuestions () { + return this.questions; + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/uicommands/FatalErrorUICommand.java b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/FatalErrorUICommand.java new file mode 100644 index 0000000..98af0e4 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/FatalErrorUICommand.java @@ -0,0 +1,28 @@ +package meerkat.voting.ui.uicommands; + +import meerkat.protobuf.Voting.UIElement; +import meerkat.voting.controller.callbacks.*; + +/** + * This command signals the UI that a fatal error occurred and it should notify the voter + */ +public class FatalErrorUICommand extends UICommand { + + private final UIElement errorMessage; + private final UIElement[] buttonLabels; + + public FatalErrorUICommand(UIElement errorMessage, UIElement[] buttonLabels, ControllerCallback callback) + { + super(callback); + this.errorMessage = errorMessage; + this.buttonLabels = buttonLabels; + } + + public UIElement getErrorMessage() { + return errorMessage; + } + + public UIElement[] getButtonLabels() { + return buttonLabels; + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/uicommands/RaceVotingUICommand.java b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/RaceVotingUICommand.java new file mode 100644 index 0000000..9b329bc --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/RaceVotingUICommand.java @@ -0,0 +1,24 @@ +package meerkat.voting.ui.uicommands; + +import meerkat.protobuf.Voting.*; +import meerkat.voting.controller.callbacks.*; + +import java.util.List; + +/** + * This command signals the UI to present the race voting questions to the voter + */ +public class RaceVotingUICommand extends UICommand> { + + private final List questions; + + public RaceVotingUICommand(List questions, ControllerCallback> callback) + { + super(callback); + this.questions = questions; + } + + public List getQuestions () { + return this.questions; + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/uicommands/StartSessionUICommand.java b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/StartSessionUICommand.java new file mode 100644 index 0000000..b40bb5d --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/StartSessionUICommand.java @@ -0,0 +1,13 @@ +package meerkat.voting.ui.uicommands; + +import meerkat.voting.controller.callbacks.*; + +/** + * This command signals the UI to present a new session to a voter + */ +public class StartSessionUICommand extends UICommand { + + public StartSessionUICommand(ControllerCallback callback) { + super(callback); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/uicommands/TickCommand.java b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/TickCommand.java new file mode 100644 index 0000000..e9bb2b7 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/TickCommand.java @@ -0,0 +1,15 @@ +package meerkat.voting.ui.uicommands; + +import meerkat.voting.controller.callbacks.*; + +/** + * This is a special UI command which just points out that a tick of the clock occurred + * (so a progress bar can advance while waiting) + */ +public class TickCommand extends UICommand { + + public TickCommand(ControllerCallback callback) { + super(callback); + assert null == callback; + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/uicommands/UICommand.java b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/UICommand.java new file mode 100644 index 0000000..e579133 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/UICommand.java @@ -0,0 +1,18 @@ +package meerkat.voting.ui.uicommands; + +import meerkat.voting.controller.callbacks.*; + +/** + * Base class for the commands to put in the UI queue + */ +public abstract class UICommand { + protected final ControllerCallback callback; + + protected UICommand(ControllerCallback callback) { + this.callback = callback; + } + + public ControllerCallback getCallback () { + return this.callback; + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/uicommands/WaitForFinishUICommand.java b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/WaitForFinishUICommand.java new file mode 100644 index 0000000..5e5d504 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/WaitForFinishUICommand.java @@ -0,0 +1,22 @@ +package meerkat.voting.ui.uicommands; + +import meerkat.protobuf.Voting.*; +import meerkat.voting.controller.callbacks.*; + +/** + * This command signals the UI to wait with an appropriate message until a new command replaces this state + */ +public class WaitForFinishUICommand extends UICommand { + + private final UIElement message; + + public WaitForFinishUICommand(UIElement message, ControllerCallback callback) + { + super(callback); + this.message = message; + } + + public UIElement getMessage () { + return this.message; + } +} diff --git a/voting-booth/src/test/java/meerkat/voting/NetworkVirtualPrinterTest.java b/voting-booth/src/test/java/meerkat/voting/NetworkVirtualPrinterTest.java new file mode 100644 index 0000000..80c9b72 --- /dev/null +++ b/voting-booth/src/test/java/meerkat/voting/NetworkVirtualPrinterTest.java @@ -0,0 +1,240 @@ +package meerkat.voting; + + +import com.google.common.util.concurrent.FutureCallback; + +import meerkat.protobuf.Crypto.*; +import meerkat.protobuf.PollingStation.*; + +import com.google.protobuf.ByteString; +import meerkat.pollingstation.PollingStationScanner; +import meerkat.pollingstation.PollingStationWebScanner; + +import meerkat.protobuf.Voting.*; +import meerkat.voting.controller.callbacks.OutputDeviceCommitCallback; +import meerkat.voting.controller.callbacks.OutputDeviceFinalizeCallback; +import meerkat.voting.output.NetworkVirtualPrinter; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.concurrent.Semaphore; + + +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * A test for the NetworkVirtualPrinter + * coded with too much effort by Hai, based on the PollingStationWebScannerTest coded by Arbel + */ + + + +public class NetworkVirtualPrinterTest { + + private PollingStationScanner.Consumer scanner; + private static final String ADDRESS = "http://localhost"; + private static final String SUB_ADDRESS = ""; + private static final int PORT = 8080; + + private Semaphore semaphore0; + private Semaphore semaphore1; + private Semaphore semaphore2; + private Throwable thrown; + private boolean dataIsAsExpected; + + private NetworkVirtualPrinter networkPrinter; + + + + + + private class ScanHandler implements FutureCallback { + + private final ScannedData expectedData; + + public ScanHandler(ScannedData expectedData) { + this.expectedData = expectedData; + } + + @Override + public void onSuccess(ScannedData result) { + dataIsAsExpected = result.equals(expectedData); + semaphore2.release(); + } + + @Override + public void onFailure(Throwable t) { + dataIsAsExpected = false; + thrown = t; + semaphore2.release(); + } + } + + private class CommitHandler extends OutputDeviceCommitCallback { + + private boolean success; + public String errorMessage; + + public CommitHandler(int requestId, long serialNumber) { + super(requestId, serialNumber, null, null); + errorMessage = null; + success = false; + } + + @Override + public void onSuccess(Void v) { + System.out.println("CommitHandler success"); + success = true; + semaphore0.release(); + } + + @Override + public void onFailure(Throwable t) { + errorMessage = "Commit to ballot failed " + t.getMessage(); + semaphore0.release(); + } + + public boolean gotSuccess() { + return success; + } + } + + private class CastHandler extends OutputDeviceFinalizeCallback { + + private boolean success; + public String errorMessage; + + public CastHandler(int requestId, long serialNumber) { + super(requestId, serialNumber, null, null); + errorMessage = null; + success = false; + } + + @Override + public void onSuccess(Void v) { + System.out.println("CastHandler success"); + success = true; + semaphore1.release(); + } + + @Override + public void onFailure(Throwable t) { + errorMessage = "Cast to ballot failed " + t.getMessage(); + semaphore1.release(); + } + + public boolean gotSuccess() { + return success; + } + } + + @Before + public void init() { + + System.err.println("Setting up Scanner WebApp!"); + + scanner = new PollingStationWebScanner(PORT, SUB_ADDRESS); + + semaphore0 = new Semaphore(0); + semaphore1 = new Semaphore(0); + semaphore2 = new Semaphore(0); + thrown = null; + + try { + scanner.start(); + } catch (Exception e) { + assertThat("Could not start server: " + e.getMessage(), false); + } + + + networkPrinter = new NetworkVirtualPrinter(ADDRESS + ":" + PORT); + Thread outputThread = new Thread(networkPrinter); + outputThread.setName("Meerkat VB-Output Thread"); + outputThread.start(); + } + + @Test + public void testSuccessfulScan() throws InterruptedException { + + // create scannedData + + byte[] channel = {(byte) 1, (byte) 2}; + byte[] encMessageData = {(byte) 50, (byte) 51, (byte) 52}; + byte[] signatureData = {(byte) 93, (byte) 95, (byte) 95}; + byte[] signerId = {(byte) 17, (byte) 18, (byte) 19}; + int serialNumber = 17; + + PlaintextBallot plaintextBallot = PlaintextBallot.newBuilder() + .setChannelIdentifier(ByteString.copyFrom(channel)) + .setSerialNumber(serialNumber) + .build(); + + RerandomizableEncryptedMessage encMessage = RerandomizableEncryptedMessage.newBuilder() + .setData(ByteString.copyFrom(encMessageData)) + .build(); + + EncryptedBallot encryptedBallot = EncryptedBallot.newBuilder() + .setSerialNumber(serialNumber) + .setData(encMessage) + .build(); + + Signature signature = Signature.newBuilder() + .setType(SignatureType.ECDSA) + .setData(ByteString.copyFrom(signatureData)) + .setSignerId(ByteString.copyFrom(signerId)) + .build(); + + SignedEncryptedBallot signedEncryptedBallot = SignedEncryptedBallot.newBuilder() + .setEncryptedBallot(encryptedBallot) + .setSignature(signature) + .build(); + + ScannedData scannedData = ScannedData.newBuilder() + .setChannel(ByteString.copyFrom(channel)) + .setSignedEncryptedBallot(signedEncryptedBallot) + .build(); + + + scanner.subscribe(new ScanHandler(scannedData)); + + //Send scan + + CommitHandler commitHandler = new CommitHandler(0, serialNumber); + networkPrinter.commitToBallot(plaintextBallot, signedEncryptedBallot, commitHandler); + + semaphore0.acquire(); + + CastHandler castHandler = new CastHandler(1, serialNumber); + networkPrinter.castBallot(castHandler); + + semaphore1.acquire(); + + semaphore2.acquire(); + + // Make sure the response was valid + + assertThat("Commit to ballot callback did not receive success", commitHandler.gotSuccess()); + assertThat("Cast ballot callback did not receive success", castHandler.gotSuccess()); + + assertThat("Scanner has thrown an error", thrown == null); + assertThat("Scanned data received was incorrect", dataIsAsExpected); + + } + + @After + public void close() { + + System.err.println("Scanner WebApp shutting down..."); + + try { + scanner.stop(); + } catch (Exception e) { + assertThat("Could not stop server: " + e.getMessage(), false); + } + + networkPrinter.callShutDown(); + + } + +} \ No newline at end of file