Ballot was created due to a confluence of two factors.
First, I wanted to replace the voting scripts KDE used (Elektion), for a few reasons:
So, I thought for a while about whether I could make a system that fixed at least some of these flaws and provided better security while doing so. Which leads me to the second factor: Identity. Now that KDE has a centralized authentication system, we can take advantage of that to ensure that the person voting is the right person. Ballot is the result.
That said, if anyone wants to lend their CSS skills to make it look nicer without introducing complexity, I'm open for it.
P.S.: This goes for the code too. I know that if you're a Ruby god the code will probably make you scream bloody murder, but it's relatively modular due to Sinatra's architecture and thus not terribly difficult to understand (if you understand the whole, you can look at the parts and verify that they are behaving correctly).
Before getting into the below, more specific sections, a word: Ballot must be run over HTTPS. All pages, all the time. You simply cannot have a secure voting site that is waiting for any eavesdropper that's around to pick out your traffic.
There are some factors built into Ballot (covered below) that can help ensure that your session isn't highjacked, if for some reason you have only the login page over HTTPS, but if the black helicopters are watching your username and password and secret keys fly by in cleartext, that doesn't help much. Not to mention that if vote pages are in cleartext, people will see your vote selections anyways (except in the case of scramble votes, below). Run over HTTPS.
The only actions in Ballot that do not go through some sort of authentication check are:
That's it. Any other page checks your credentials and responds appropriately, usually by redirecting you back to the login page if the credential check fails. This includes the logout page, which ensures that the person attempting to logout is actually the person they claim that they are (so that an attacker can't cause you problems by logging you out repeatedly). Therefore, other than accessing those static pages, there is no action you can perform in Ballot that does not require a credential check.
Once you log in, which verifies your given information against Identity, your password is forgotten. Your rights to use the system are then stored in a credential cookie that is freshly set on every single access of an authenticated page (see Session Highjacking Prevention, below).
Votes are loaded from .vote files, which are JSON-syntax files containing vote data. Votes are never loaded based on vote names given by the user, to avoid directory traversal or other similar attacks. When a user requests a vote, an exact string match against the system's known votes is performed. If the vote exists, an authorization check is performed to ensure that the user belongs to one of the LDAP groups/users to which the vote explicitly allows access. This authorization check is performed every single time you perform a GET or POST on a vote.
In order to prevent session hijacking, a rotating cookie (a "One Time Cookie", based off the similar concept of a "One Time Password") is used. This cookie has a few parts:
The values of the hex prefix, hex suffix, and the random float in the hash are stored server-side and associated with that user. When an action is performed on the server, the cookie's presented hash is matched against a reconstructed hash from the server's stored random values for that user. If it does not match, the user is not authorized and the action is prevented. If it matches, a new random value is generated and the user's cookie re-set with a new prefix, SHA256 (with a new random float), and new suffix.
A few factors make it very difficult for an attacker to make use of this cookie, especially under HTTPS (see the Attack Vectors subsection Hardcore Session Hijacking for more information). An attacker may know that there is a SHA256 hash embedded in the broader string, but won't know where it is, since both the size of the overall string and the SHA256's position inside the string is generated anew every access. Additionally, since the IP address of the connection is embedded in the SHA256 hash, and the SHA256 hash is reconstructed on the server during the check with the current connection's IP address, an attacker wishing to make use of a discovered cookie would need to either be in behind the same NAT area or have some other way to spoof the original connection's IP address.
Finally, since each cookie value is good for only one access of the system, it does mean that no two users will be able to use the system under the same account at the same time; this means that a user in a situation where cookie stealing is possible that suddenly finds themselves unauthenticated can know that their session may have been hijacked.
In order to allow users to change their votes with a minimal of attack vector risk, the user's record of voting is obfuscated. The information about votes is split up into two directories. One records which users have voted. When a user votes, their username is hashed with a vote-specific key (so that the same user won't have the same identifier in two different votes) and a file is created indicating that that user has voted (this directory can be made public or not depending on the desires of those running the system).
The other directory contains the votes themselves. The file values contain the selected voting options; the file names are a hash of the username and their vote-specific private key.
How this works is that when a user attempts to vote, their username/vote key hash is created and a check is run to see if a file with this hash value exists in the user directory. If the hash doesn't exist, the vote is recorded and all is well. However, if the hash does exist, then the system knows that it must match the user's given private key, hashed with their username, against the file names in the vote directory. If such a hash is found, the user gave the correct private key and the changed vote is recorded. If it isn't found, the user's supplied private key is incorrect and their attempt to change the vote is denied.
The system is capable of supporting, on a per-user basis, YubiKeys for second-factor authentication. If turned on globally, users will see a login field asking for a YubiKey one-time password (OTP). If their account has a YubiKey public identifier associated with it, this field becomes required. This OTP is then checked against Yubico's servers (currently, checking aginst other servers is not supported). If the OTP is not valid, the user is not allowed to log in.
For the ultimate in paranoia, votes can be scrambled. Scramble votes replace the normal vote options with random integer values between 1 and 10,000,000, randomizes the order in which the values are shown on the web page, and changes the value and order on every single page view. In scramble votes, the user is shown a button that can be pressed to send an SMS to the phone number they have recorded in Identity (right now this is disabled, as my testing was only with a personal Google Voice account, which worked). Pressing the button causes a refresh of the page (thus re-scrambling the values) and sends the decoded values for that one page view to the user's phone. The user can then select their desired vote value.
Scramble votes are really only useful in cases where you can't use HTTPS or cases where HTTPS is being intercepted (see the Attack Vectors subsection Hardcore Session Hijacking for more information).
In nearly all cases, the cookie randomization feature is enough to prevent session hijacking, in combination with HTTPS. However, you could have a problem if you either cannot use HTTPS, or if your employer/government/etc has put into place a nasty system (they exist) whereby you are required to have trusted the certificate of a root certification authority controlled by your employer/goverment/etc and also required to go through them for HTTPS, giving them the perfect avenue for a man-in-the-middle attack. In such a case, they can actually intercept your HTTPS calls, creating valid certificates on-the-fly to the server's endpoint so that your browser thinks everything is fine but meanwhile they're sniffing your decoded traffic.
Note that the IP address of the attacker will still have to match the user's originating IP address, so this provides some level of security. Additionally, SSL attacks (for instance, BEAST) currently require a great deal of time to decrypt a user's session cookie (on the order of many seconds per byte). In a normal user session (especially in a scenario where the user logs out properly), by the time that such an attack could be carried out, the user's cookie will have changed multiple times, and the decrypted cookie will no longer be valid.
If all else fails, you can either deal with it (keeping in mind to be wary of your authenticated access to the system suddenly disappearing) or you can use scramble votes. Assuming that you trust your telecom provider. :-)
If you don't trust your sysadmins, you're lost. That is almost universally true. The clever sysadmin can -- especially with open-source software such as this, and especially with root access to a filesystem -- nearly always find a way to defeat all sorts of security measures. There are various things a sysadmin could do to figure out who is voting what:
The lesson is: it wouldn't be hard. (It isn't hard on the current system either; see below.) So don't ever use this software -- or any centrally-hosted voting software -- if you don't trust the person running the server that is hosting it.
The vectors are actually quite similar, and essentially boil down to the same point made above: you have to trust your sysadmin.