Skip to content

Instantly share code, notes, and snippets.

@markcmarino
Created February 26, 2020 18:23
Show Gist options
  • Save markcmarino/65291e30f59dbde4235b56096a3e4d6b to your computer and use it in GitHub Desktop.
Save markcmarino/65291e30f59dbde4235b56096a3e4d6b to your computer and use it in GitHub Desktop.
VoteBox code created by William Marsh, Rice University
/**
* This file is part of VoteBox.
*
* VoteBox is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published by
* the Free Software Foundation.
*
* You should have received a copy of the GNU General Public License
* along with VoteBox, found in the root of any distribution or
* repository containing all or part of VoteBox.
*
* THIS SOFTWARE IS PROVIDED BY WILLIAM MARSH RICE UNIVERSITY, HOUSTON,
* TX AND IS PROVIDED 'AS IS' AND WITHOUT ANY EXPRESS, IMPLIED OR
* STATUTORY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF
* ACCURACY, COMPLETENESS, AND NONINFRINGEMENT. THE SOFTWARE USER SHALL
* INDEMNIFY, DEFEND AND HOLD HARMLESS RICE UNIVERSITY AND ITS FACULTY,
* STAFF AND STUDENTS FROM ANY AND ALL CLAIMS, ACTIONS, DAMAGES, LOSSES,
* LIABILITIES, COSTS AND EXPENSES, INCLUDING ATTORNEYS' FEES AND COURT
* COSTS, DIRECTLY OR INDIRECTLY ARISING OUR OF OR IN CONNECTION WITH
* ACCESS OR USE OF THE SOFTWARE.
*/
package votebox;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Observable;
import java.util.Observer;
import javax.swing.Timer;
import sexpression.*;
import votebox.crypto.*;
import votebox.events.*;
import votebox.middle.*;
import votebox.middle.ballot.Ballot;
import votebox.middle.driver.*;
import votebox.middle.view.*;
import auditorium.*;
import auditorium.Event;
/**
* This is the top level votebox main class. This class organizes and connects
* the components of VoteBox, namely:<br>
* 1) The auditorium network<br>
* 2) The vote storage backend<br>
* 3) The votebox "middle" (that is, the link between the voter and the backend,
* the gui)
*
* @author derrley, cshaw
*/
public class VoteBox {
private final AuditoriumParams _constants;
private final IViewFactory _factory;
private VoteBoxAuditoriumConnector auditorium;
private Driver currentDriver;
private IVoteBoxInactiveUI inactiveUI;
private final int mySerial;
private boolean connected;
private boolean voting;
private boolean override;
private boolean committedBallot;
private boolean finishedVoting;
private int label;
private Event<Integer> labelChangedEvent;
private int protectedCount;
private int publicCount;
private int numConnections;
private byte[] nonce;
private int pageBeforeOverride;
private Timer killVBTimer;
private Timer statusTimer;
/**
* Equivalent to new VoteBox(-1).
*/
public VoteBox(){
this(-1);
}
/**
* Constructs a new instance of a persistant VoteBox booth. This
* implementation runs in the background, on an auditorium network, and
* waits to receive an authorization before launching the VoteBox middle.
* For a standalone implementation, see
* {@link votebox.middle.datacollection.Launcher}.
*
* @param serial
* the serial number of the votebox
*/
public VoteBox(int serial) {
_constants = new AuditoriumParams("vb.conf");
if(serial != -1)
mySerial = serial;
else
mySerial = _constants.getDefaultSerialNumber();
if(mySerial == -1)
throw new RuntimeException("usage: VoteBox <machineID>");
numConnections = 0;
labelChangedEvent = new Event<Integer>();
statusTimer = new Timer(300000, new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (connected) {
auditorium.announce(getStatus());
}
}
});
if (_constants.getViewImplementation().equals("AWT")) {
// run fullscreen on OSX only
_factory = new AWTViewFactory(
!System.getProperty("os.name").equals("Mac OS X"));
} else if (_constants.getViewImplementation().equals("SDL")) {
_factory = new VoteboxSDLViewFactory(_constants);
}else
throw new RuntimeException(
"Unknown view implementation defined in configuration");
}
/**
* Broadcasts this VoteBox booth's status, and resets the status timer
*/
public void broadcastStatus() {
auditorium.announce(getStatus());
statusTimer.restart();
}
/**
* Returns this booth's label, assigned by a supervisor over auditorium. If
* unassigned, will return 0.
*
* @return the label
*/
public int getLabel() {
return label;
}
/**
* Returns this booth's status as a VoteBoxEvent, used for periodic
* broadcasts
*
* @return the status
*/
public VoteBoxEvent getStatus() {
VoteBoxEvent event;
int battery = BatteryStatus.read();
if (voting)
event = new VoteBoxEvent(mySerial, label, "in-use", battery,
protectedCount, publicCount);
else
event = new VoteBoxEvent(mySerial, label, "ready", battery,
protectedCount, publicCount);
return event;
}
/**
* Allows the VoteBox inactive UI (what is shown when a user isn't voting)
* to register for a label changed event, and update itself accordingly
*
* @param obs
* the observer
*/
public void registerForLabelChanged(Observer obs) {
labelChangedEvent.addObserver(obs);
}
/**
* Launch the VoteBox middle. Registers for events that we would want to
* know about (such as cast ballot, so we can send the message over
* auditorium)
*
* @param location
* the location on disk of the ballot
*/
public void run(String location) {
inactiveUI.setVisible(false);
currentDriver = new Driver(location, _factory, _constants.getCastBallotEncryptionEnabled());
voting = true;
currentDriver.run();
//If we're using the commit-challenge model, we need to register for the commit & challenge events.
if(_constants.getUseCommitChallengeModel()){
//Listen for challenges ui events. When received, discard the ballot (as the vote is no longer countable)
// and reply with the random key needed to decrypt this particular vote
currentDriver.getView().registerForChallenge(new Observer() {
/**
* Makes sure that the booth is in a correct state to cast a ballot,
* then announce the cast-ballot message (also increment counters)
*/
public void update(Observable arg0, Object arg1) {
if (!connected)
throw new RuntimeException(
"Attempted to cast ballot when not connected to any machines");
if (!voting || currentDriver == null)
throw new RuntimeException(
"VoteBox attempted to cast ballot, but was not currently voting");
if (finishedVoting)
throw new RuntimeException(
"This machine has already finished voting, but attempted to vote again");
finishedVoting = true;
auditorium.announce(new ChallengeEvent(mySerial,
StringExpression.makeString(nonce),
BallotEncrypter.SINGLETON.getRecentRandom()));
BallotEncrypter.SINGLETON.clear();
}
});
//Listen for commit ui events. When received, send out an encrypted vote.
currentDriver.getView().registerForCommit(new Observer() {
public void update(Observable o, Object arg) {
if (!connected)
throw new RuntimeException(
"Attempted to cast ballot when not connected to any machines");
if (!voting || currentDriver == null)
throw new RuntimeException(
"VoteBox attempted to cast ballot, but was not currently voting");
if (finishedVoting)
throw new RuntimeException(
"This machine has already finished voting, but attempted to vote again");
committedBallot = true;
// arg1 should be the cast ballot structure, check
if (Ballot.BALLOT_PATTERN.match((ASExpression) arg) == NoMatch.SINGLETON)
throw new RuntimeException(
"Incorrectly expected a cast-ballot");
ListExpression ballot = (ListExpression) arg;
try {
auditorium.announce(new CommitBallotEvent(mySerial,
StringExpression.makeString(nonce),
BallotEncrypter.SINGLETON.encrypt(ballot, _constants.getKeyStore().loadKey("public"))));
} catch (AuditoriumCryptoException e) {
Bugout.err("Crypto error trying to commit ballot: "+e.getMessage());
e.printStackTrace();
}
}
});
//Listen for cast ui events.
//Rather than actually send the ballot out, just send the nonce (which can identify the whole
//transaction).
//Clean up the encryptor afterwards so as to destroy the random number needed for challenging.
currentDriver.getView().registerForCastBallot(new Observer(){
public void update(Observable o, Object arg) {
if (!connected)
throw new RuntimeException(
"Attempted to cast ballot when not connected to any machines");
if (!voting || currentDriver == null)
throw new RuntimeException(
"VoteBox attempted to cast ballot, but was not currently voting");
if (finishedVoting)
throw new RuntimeException(
"This machine has already finished voting, but attempted to vote again");
finishedVoting = true;
++publicCount;
++protectedCount;
auditorium.announce(new CastCommittedBallotEvent(mySerial,
StringExpression.makeString(nonce)));
BallotEncrypter.SINGLETON.clear();
}
});
}else{
//If we're not using the challenge-commit model, we still need to handle "cast" ui events.
//Here we role the commit triggered encryption in with casting (provided encryption is enabled).
currentDriver.getView().registerForCastBallot(new Observer() {
/**
* Makes sure that the booth is in a correct state to cast a ballot,
* then announce the cast-ballot message (also increment counters)
*/
public void update(Observable o, Object arg) {
if (!connected)
throw new RuntimeException(
"Attempted to cast ballot when not connected to any machines");
if (!voting || currentDriver == null)
throw new RuntimeException(
"VoteBox attempted to cast ballot, but was not currently voting");
if (finishedVoting)
throw new RuntimeException(
"This machine has already finished voting, but attempted to vote again");
finishedVoting = true;
++publicCount;
++protectedCount;
//If we are not using encryption use the plain old CastBallotEvent
if(!_constants.getCastBallotEncryptionEnabled()){
auditorium.announce(new CastBallotEvent(mySerial,
StringExpression.makeString(nonce),
(ASExpression)arg));
}else{
//Else, use the EncryptedCastBallotEvent with a properly encrypted ballot
try{
BallotEncrypter.SINGLETON.encrypt((ListExpression)arg, _constants.getKeyStore().loadKey("public"));
auditorium.announce(new EncryptedCastBallotEvent(mySerial,
StringExpression.makeString(nonce),
BallotEncrypter.SINGLETON.getRecentEncryptedBallot()));
} catch (AuditoriumCryptoException e) {
System.err.println("Encryption Error: "+e.getMessage());
}
}
BallotEncrypter.SINGLETON.clear();
}
});
}//if
currentDriver.getView().registerForOverrideCancelConfirm(
new Observer() {
/**
* Kill the VB runtime, and announce the confirm message
*/
public void update(Observable o, Object arg) {
if (voting && override && !finishedVoting
&& currentDriver != null) {
auditorium.announce(new OverrideCancelConfirmEvent(
mySerial, nonce));
currentDriver.kill();
currentDriver = null;
nonce = null;
voting = false;
override = false;
broadcastStatus();
inactiveUI.setVisible(true);
} else
throw new RuntimeException(
"Received an override-cancel-confirm event at the incorrect time");
}
});
currentDriver.getView().registerForOverrideCancelDeny(new Observer() {
/**
* Announce the deny message, and return to the page the voter was
* previously on
*/
public void update(Observable o, Object arg) {
if (voting && override && !finishedVoting
&& currentDriver != null) {
auditorium.announce(new OverrideCancelDenyEvent(mySerial,
nonce));
override = false;
currentDriver.getView().drawPage(pageBeforeOverride);
} else
throw new RuntimeException(
"Received an override-cancel-deny event at the incorrect time");
}
});
currentDriver.getView().registerForOverrideCastConfirm(new Observer() {
/**
* Increment counters, and send the ballot in the confirm message.
* Also kill votebox and show the inactive UI
*/
public void update(Observable o, Object arg) {
if (voting && override && !finishedVoting
&& currentDriver != null) {
++publicCount;
++protectedCount;
byte[] ballot = ((ASExpression) arg).toVerbatim();
auditorium.announce(new OverrideCastConfirmEvent(mySerial,
nonce, ballot));
currentDriver.kill();
currentDriver = null;
nonce = null;
voting = false;
override = false;
broadcastStatus();
inactiveUI.setVisible(true);
} else
throw new RuntimeException(
"Received an override-cast-confirm event at the incorrect time");
}
});
currentDriver.getView().registerForOverrideCastDeny(new Observer() {
/**
* Announce the deny message, and return to the page the voter was
* previously on
*/
public void update(Observable o, Object arg) {
if (voting && override && !finishedVoting
&& currentDriver != null) {
auditorium.announce(new OverrideCastDenyEvent(mySerial,
nonce));
override = false;
currentDriver.getView().drawPage(pageBeforeOverride);
} else
throw new RuntimeException(
"Received an override-cast-deny event at the incorrect time");
}
});
}
/**
* Starts Auditorium, registers the listener, and connects to the network.
*/
public void start() {
if(_constants.getViewImplementation().equals("SDL"))
inactiveUI = new VoteBoxSDLInactiveUI(this, _constants);
else
inactiveUI = new VoteBoxInactiveUI(this);
inactiveUI.setVisible(true);
try {
auditorium = new VoteBoxAuditoriumConnector(mySerial,
_constants,
ActivatedEvent.getMatcher(), AssignLabelEvent.getMatcher(),
AuthorizedToCastEvent.getMatcher(), BallotReceivedEvent.getMatcher(),
OverrideCancelEvent.getMatcher(), OverrideCastEvent.getMatcher(),
PollsOpenQEvent.getMatcher(), BallotCountedEvent.getMatcher(),
ChallengeEvent.getMatcher(), ChallengeResponseEvent.getMatcher());
} catch (NetworkException e1) {
//NetworkException represents a recoverable error
// so just note it and continue
System.out.println("Recoverable error occured: "+e1.getMessage());
e1.printStackTrace(System.err);
}
auditorium.addListener(new VoteBoxEventListener() {
public void ballotCounted(BallotCountedEvent e){
if (e.getNode() == mySerial
&& Arrays.equals(e.getNonce(), nonce)) {
if (!finishedVoting)
throw new RuntimeException(
"Someone said the ballot was counted, but this machine hasn't finished voting yet");
if(!_constants.getUseCommitChallengeModel()){
Bugout.err("Received BallotCounted message while not in Challenge-Commit mode!");
return;
}
currentDriver.getView().nextPage();
nonce = null;
voting = false;
finishedVoting = false;
committedBallot = false;
broadcastStatus();
killVBTimer = new Timer(_constants.getViewRestartTimeout(), new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
currentDriver.kill();
currentDriver = null;
inactiveUI.setVisible(true);
killVBTimer = null;
};
});
killVBTimer.setRepeats(false);
killVBTimer.start();
}//if
}
/**
* Handler for the activated message. Look to see if this VoteBox's
* status exists (and is correct), and if not, broadcast its status
*/
public void activated(ActivatedEvent e) {
boolean found = false;
for (StatusEvent ae : e.getStatuses()) {
if (ae.getNode() == mySerial) {
VoteBoxEvent ve = (VoteBoxEvent) ae.getStatus();
VoteBoxEvent status = getStatus();
if (!ve.getStatus().equals(status.getStatus())
|| ve.getBattery() != status.getBattery()
|| ve.getLabel() != status.getLabel()
|| ve.getProtectedCount() != status
.getProtectedCount()
|| ve.getPublicCount() != status
.getPublicCount())
broadcastStatus();
found = true;
}
}
if (!found) broadcastStatus();
}
/**
* Handler for the assign-label message. If it is referring to this
* booth, set the label.
*/
public void assignLabel(AssignLabelEvent e) {
if (e.getNode() == mySerial){
label = e.getLabel();
System.out.println("\tNew Label: "+label);
}//if
labelChangedEvent.notify(label);
}
/**
* Handler for the authorized-to-cast message. If it is for this
* booth, and it is not already voting, unzip the ballot and fire
* the VoteBox runtime. Also announce the new status.
*/
public void authorizedToCast(AuthorizedToCastEvent e) {
if (e.getNode() == mySerial) {
if (voting || currentDriver != null && killVBTimer == null)
throw new RuntimeException(
"VoteBox was authorized-to-cast, but was already voting");
// If last VB runtime is on thank you screen and counting
// down to when it disappears, kill it prematurely without
// showing inactive UI
if (killVBTimer != null && currentDriver != null) {
killVBTimer.stop();
killVBTimer = null;
currentDriver.kill();
currentDriver = null;
}
nonce = e.getNonce();
//Current working directory
File path = new File(System.getProperty("user.dir"));
path = new File(path, "tmp");
path = new File(path, "ballots");
path = new File(path, "ballot" + protectedCount);
path.mkdirs();
try {
FileOutputStream fout = new FileOutputStream(new File(path, "ballot.zip"));
byte[] ballot = e.getBallot();
fout.write(ballot);
Driver.unzip(new File(path, "ballot.zip").getAbsolutePath(), new File(path, "data").getAbsolutePath());
Driver.deleteRecursivelyOnExit(path.getAbsolutePath());
run(new File(path, "data").getAbsolutePath());
broadcastStatus();
} catch (IOException e1) {
throw new RuntimeException(e1);
}
}
}
/**
* Handler for the ballot-received message. Show the next page on
* the VB runtime (the thank you screen), and start a timer that
* kills the runtime after a set amount of time (5 seconds), and
* then shows the inactive screen. Also responds with its status.
*/
public void ballotReceived(BallotReceivedEvent e) {
if (e.getNode() == mySerial
&& Arrays.equals(e.getNonce(), nonce)) {
if (!committedBallot && _constants.getUseCommitChallengeModel())
throw new RuntimeException(
"Someone said the ballot was received, but this machine hasn't committed it yet");
if(!finishedVoting && !_constants.getUseCommitChallengeModel())
throw new RuntimeException(
"Someone said the ballot was received, but this machine hasn't finished voting yet");
currentDriver.getView().nextPage();
if(!_constants.getUseCommitChallengeModel()){
nonce = null;
voting = false;
finishedVoting = false;
committedBallot = false;
broadcastStatus();
killVBTimer = new Timer(_constants.getViewRestartTimeout(), new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
currentDriver.kill();
currentDriver = null;
inactiveUI.setVisible(true);
killVBTimer = null;
};
});
killVBTimer.setRepeats(false);
killVBTimer.start();
}//if
}
}
public void castBallot(CastBallotEvent e) {
// NO-OP
}
/**
* Increment the number of connections
*/
public void joined(JoinEvent e) {
++numConnections;
connected = true;
}
public void lastPollsOpen(LastPollsOpenEvent e) {
// NO-OP
}
/**
* Decrement the number of connections
*/
public void left(LeaveEvent e) {
--numConnections;
if (numConnections == 0) connected = false;
}
/**
* Handler for the override-cancel message. If it is referring to
* this booth, and it is in a state that it can be overridden, send
* the runtime to the proper override page and record the page the
* user was previously on.
*/
public void overrideCancel(OverrideCancelEvent e) {
if (mySerial == e.getNode()
&& Arrays.equals(e.getNonce(), nonce)) {
try {
if (voting && !finishedVoting && currentDriver != null) {
int page = currentDriver.getView().overrideCancel();
if (!override) {
pageBeforeOverride = page;
override = true;
}
} else
throw new RuntimeException(
"Received an override-cancel message when the user wasn't voting");
} catch (IncorrectTypeException e1) {
Bugout.err("Incorrect type in overrideCancel handler");
}
}
}
public void overrideCancelConfirm(OverrideCancelConfirmEvent e) {
// NO-OP
}
public void overrideCancelDeny(OverrideCancelDenyEvent e) {
// NO-OP
}
/**
* Handler for the override-cast message. If it is referring to this
* booth, and it is in a state that it can be overridden, send the
* runtime to the proper override page and record the page the user
* was previously on.
*/
public void overrideCast(OverrideCastEvent e) {
try {
if (voting && !finishedVoting && currentDriver != null) {
int page = currentDriver.getView().overrideCast();
if (!override) {
pageBeforeOverride = page;
override = true;
}
} else
throw new RuntimeException(
"Received an override-cast message when the user wasn't voting");
} catch (IncorrectTypeException e1) {
//We don't want to bail once VoteBox is up and running,
// so report and continue in this case
System.out.println("Incorrect type received in overrideCast event: "+e1.getMessage());
e1.printStackTrace(System.err);
}
}
public void overrideCastConfirm(OverrideCastConfirmEvent e) {
// NO-OP
}
public void overrideCastDeny(OverrideCastDenyEvent e) {
// NO-OP
}
public void pollsClosed(PollsClosedEvent e) {
// NO-OP
}
public void pollsOpen(PollsOpenEvent e) {
// NO-OP
}
/**
* Handler for the polls-open? event. Searches the machine's log,
* and replies with a last-polls-open message if an appropriate
* polls-open message is found.
*/
public void pollsOpenQ(PollsOpenQEvent e) {
if (e.getSerial() != mySerial) {
// TODO: Search the log and extract an appropriate
// polls-open message
ASExpression res = null;
if (res != null && res != NoMatch.SINGLETON) {
VoteBoxEventMatcher matcher = new VoteBoxEventMatcher(
PollsOpenEvent.getMatcher());
PollsOpenEvent event = (PollsOpenEvent) matcher.match(
0, res);
if (event != null
&& event.getKeyword().equals(e.getKeyword()))
auditorium.announce(new LastPollsOpenEvent(
mySerial, event));
}
}
}
public void supervisor(SupervisorEvent e) {
// NO-OP
}
public void votebox(VoteBoxEvent e) {
// NO-OP
}
public void commitBallot(CommitBallotEvent e) {
// NO-OP
}
public void challenge(ChallengeEvent e) {
if(!_constants.getUseCommitChallengeModel()){
Bugout.err("Received Challenge while not using Challenge-Commit model");
return;
}//if
if (e.getSerial() == mySerial
&& Arrays.equals(e.getNonce().toVerbatim(), nonce)) {
if (!finishedVoting)
throw new RuntimeException(
"Someone said this ballot was challenge, but this machine hasn't finished voting yet");
broadcastStatus();
}//if
}
public void challengeResponse(ChallengeResponseEvent e) {
if(!_constants.getUseCommitChallengeModel()){
Bugout.err("Received Challenge Response while not using Challenge-Commit model");
return;
}//if
if (e.getNode() == mySerial
&& e.getNonce().equals(StringExpression.makeString(nonce))) {
if (!finishedVoting)
throw new RuntimeException(
"Someone said this ballot was challenge, but this machine hasn't finished voting yet");
currentDriver.getView().nextPage();
nonce = null;
voting = false;
finishedVoting = false;
committedBallot = false;
broadcastStatus();
killVBTimer = new Timer(_constants.getViewRestartTimeout(), new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
currentDriver.kill();
currentDriver = null;
inactiveUI.setVisible(true);
killVBTimer = null;
};
});
killVBTimer.setRepeats(false);
killVBTimer.start();
}//if
}
});
try {
auditorium.connect();
auditorium.announce(getStatus());
}
catch (NetworkException e1) {
throw new RuntimeException(e1);
}
statusTimer.start();
}
/**
* Main entry point into the program. If an argument is given, it will be
* the serial number, otherwise VoteBox will load a serial from its config file.
*
* @param args
*/
public static void main(String[] args) {
if (args.length == 1)
new VoteBox(Integer.parseInt(args[0])).start();
else
//Tell VoteBox to refer to its config file for the serial number
new VoteBox().start();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment