I'm writing a somewhat secure voting system. I'm aware that it won't be really secure because it's on the internet so I'm looking for feedback on any logic errors or glaring mistakes in the security implementation.
The backend is ExpressionEngine CMS.
So, in this system user is allowed to vote anonymously or not anonymously. And in both cases are allowed to change the vote as long as the poll is open. After the vote ends user can verify cast vote by assigned ticket (anonymously) or by name.
One thing I know I haven't taken into account is detection of tampering in the tables. I guess I should have a fingerprint hash of all cast votes in another database, but I'm not sure how to do it.
I have two tables:
pt_voters_new: voters_id (int), entry_id (int), member_id (int), change_vote (int), iv (varchar), salt (varchar)
pt_votes_new: votes_id (varchar), entry_id (int), vote (int), member_id (int), enc (varchar)
I have a FORM that submits the chosen vote and other choices via an AJAX call. Here is the main code:
<?php
function crypto_rand($min, $max) {
$range = $max - $min;
$length = (int) (log($range,2) / 8) + 1;
$num = hexdec(bin2hex(openssl_random_pseudo_bytes($length,$s))) % $range;
return $num + $min;
}
// str_makerand is used for creating random ticket, that user can use to verify vote later (note mt_rand is not cryptographically secure)
function str_makerand ($minlength, $maxlength, $useupper, $usespecial, $usenumbers) {
$charset = "ACDEFGHJKLMNPQRTUVWXY";
if ($useupper) $charset .= "ACDEFGHJKLMNPQSTUVWXY";
if ($usenumbers) $charset .= "34679";
if ($usespecial) $charset .= "~@#$%^*()_+-={}|][";
for ($i=0; $i<$maxlength; $i++) $key .= $charset[(crypto_rand(0,(strlen($charset)-1)))];
return $key;
}
// Check that members group are allowed to vote
if (($this->EE->session->userdata('group_id') == 1) || ($this->EE->session->userdata('group_id') == 5) || ($this->EE->session->userdata('group_id') == 6) || ($this->EE->session->userdata('group_id') == 7) || ($this->EE->session->userdata('group_id') == 8) || ($this->EE->session->userdata('group_id') == 9) || ($this->EE->session->userdata('group_id') == 10) || ($this->EE->session->userdata('group_id') == 11)) {
// Function for logging
function logvote ($text) {
$handle = fopen("/dana/data/www.parlamentet.dk/votelog/logtest.txt", "a+");
fwrite($handle, $text);
fwrite($handle, "\r\n");
fclose($handle);
}
// Get password_compat for bcrypt.
require('/dana/data/www.parlamentet.dk/scripts/password.php');
// Get DB connection
require('/dana/data/www.parlamentet.dk/scripts/db.php');
// Check of form XID token
$member_id = (int)$this->EE->session->userdata('member_id'); // Users ID from ExpressionEngine session data
$ip_address = $this->EE->session->userdata('ip_address'); // Users IP address, only used for blocking check and error handling
$XID = $_POST['XID'];
$this->EE =& get_instance();
$this->EE->load->library('functions');
if (ee()->security->secure_forms_check($XID) === FALSE) {
echo '
<p style="text-align: center"><strong>AFSTEMNING</strong></p>
<p><strong>Fejl:</strong> Sikkerhedscheck fejlede. Forsøg igen eller kontakt os.</p>
<p><strong>Fejlkode:</strong> 001</p>
';
$log = $member_id." failed securitycheck 001 (from $ip_address)";
logvote($log);
exit();
}
$all_query_ok = TRUE; // Control variable for queries
// $time_now = time() - 3600;
$time_now = time();
$entry_id = (int)$_POST['entry_id']; // Unique entry ID
$vote = (int)$_POST['stemme']; // What user voted - 1, 2 or 3
if ($vote == '') {
echo '
<p style="text-align: center"><strong>AFSTEMNING</strong></p>
<p>Din stemme er <strong>IKKE</strong> registreret, da du ikke valgte noget at stemme. Genindlæs siden for at stemme korrekt.</p>
<p><strong>Fejlkode:</strong> 002</p>
';
$log = $member_id." did not choose voteoptions 002";
logvote($log);
exit();
}
if (($vote > 3) || ($vote < 1)) { // Allowed range of votes
echo '
<p style="text-align: center"><strong>AFSTEMNING</strong></p>
<p><strong>Fejl:</strong> Sikkerhedscheck fejlede. Forsøg igen eller kontakt os.</p>
<p><strong>Fejlkode:</strong> 003</p>
';
$log = $member_id." somehow voted for something strange 003";
logvote($log);
exit();
}
$anonymous_vote = (int)$_POST['anonymous_vote']; // Is it an anonymous vote?
$change_vote = (int)$_POST['change_vote']; // Should user be allowed to change vote later?
$changing_vote = (int)$_POST['changing_vote']; // Is this vote a change of a previous vote?
$password = $_POST['password']; // One time password to change the vote later
if ($change_vote === 1) {
// Blowfish hash of password (bcrypt)
$hash = password_hash($password, PASSWORD_BCRYPT, array("cost" => 10)); // Kan sættes til PASSWORD_DEFAULT (cost 4 - 31)
// Verify hash
if (password_verify($password, $hash) === FALSE) {
echo '
<p style="text-align: center"><strong>AFSTEMNING</strong></p>
<p>Din stemme er <strong>IKKE</strong> registreret, da der opstod en fejl. Forsøg venligst igen!</p>
<p><strong>Fejlkode:</strong> 004</p>
';
$log = $member_id." failed password check 004";
logvote($log);
exit();
}
// Create key from password
$key_from_pw = hash('sha256', $password);
// Get IV
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC);
$iv = mcrypt_create_iv($iv_size, MCRYPT_DEV_URANDOM);
$iv_base64 = base64_encode($iv);
// Data about vote
$data = json_encode(array('member_id'=>$member_id, 'entry_id'=>$entry_id, 'hash'=>$key_from_pw));
// Encrypt data
$enc = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key_from_pw, json_encode($data), MCRYPT_MODE_CBC, $iv);
$enc_base64 = base64_encode($enc);
}
else {
$change_vote = 0;
$enc_base64 = '';
$iv = '';
$hash = '';
}
// Check if user already voted
$result = mysqli_query($link, "SELECT voters_id FROM pt_voters WHERE member_id = '$member_id' AND entry_id='$entry_id'");
$num_results = mysqli_num_rows($result);
// Check if poll is still open
$result = mysqli_query($link, "SELECT field_id_18,field_id_44,status FROM exp_channel_data,exp_channel_titles WHERE exp_channel_titles.entry_id='$entry_id' AND exp_channel_titles.entry_id=exp_channel_data.entry_id");
$row = mysqli_fetch_array($result);
$slut_ft = (int)$row["field_id_18"]; // Endtime for the vote in Danish Parliament
if ($slut_ft === 0) { // Change this logic!
$slut_ft = 9378133526;
}
$slut_pt = $row["field_id_44"]; // Sluttidspunkt for Parlamentets egne forslag
$status = $row["status"];
if ((($num_results === 0) || ($changing_vote === 1)) && (($time_now <= $slut_ft) || ($time_now <= $slut_pt)) && ($status == 'Fremsat')) { // User has not yet voted or is allowed to change vote
if ($changing_vote === 1) { // Verify password if user is changing vote
$result = mysqli_query($link, "SELECT iv,hash,voters_id FROM pt_voters WHERE member_id = '$member_id' AND entry_id = '$entry_id'");
$row = mysqli_fetch_array($result);
$hash_check = $row['hash'];
$voters_id = $row['voters_id'];
// $hash = password_hash($password, PASSWORD_BCRYPT, array("cost" => 10, "salt" => $salt_check));
if (password_verify($password, $hash_check) === FALSE) {
print '
<p style="text-align: center"><strong>AFSTEMNING</strong></p>
<p>Forkert kodeord! Du kan genindlæse siden og forsøge igen!</p>
<p><strong>Fejlkode:</strong> 005</p>
';
$log = $member_id." failed password check 005";
logvote($log);
exit();
}
$key_from_pw = hash('sha256', $password);
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC);
$old_iv_base64 = $row['iv'];
$old_iv = base64_decode($old_iv_base64);
$data = json_encode(array('member_id'=>$member_id, 'entry_id'=>$entry_id, 'hash'=>$key_from_pw));
$enc_test = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key_from_pw, json_encode($data), MCRYPT_MODE_CBC, $old_iv);
$enc_test_base64 = base64_encode($enc_test);
$result = mysqli_query($link, "SELECT votes_id FROM pt_votes WHERE enc='$enc_test_base64'");
$num_results = mysqli_num_rows($result);
if ($num_results === 1) { // OK - data checked out
// Removing all data from previous vote
$row = mysqli_fetch_array($result);
$votes_id = $row['votes_id'];
mysqli_query($link, "DELETE FROM pt_votes WHERE votes_id='$votes_id'") ? null : $all_query_ok=FALSE;
mysqli_query($link, "DELETE FROM pt_voters WHERE entry_id='$entry_id' AND member_id='$member_id'") ? null : $all_query_ok=FALSE;
}
else {
print '
<p style="text-align: center"><strong>AFSTEMNING</strong></p>
<p>Forkert kodeord! Du kan genindlæse siden og forsøge igen!</p>
<p><strong>Fejlkode:</strong> 007</p>
';
$log = $member_id." did not enter correct password 007";
logvote($log);
exit();
}
}
$num_results_2 = 1;
while ($num_results_2 != 0) {
$votes_id = str_makerand(10,10,1,0,1); // Random ID for vote
$result_2 = mysqli_query($link, "SELECT * FROM pt_votes WHERE votes_id='$votes_id'"); // Check if ID exists
$num_results_2 = mysqli_num_rows($result_2);
if ($num_results_2 === 0) {
// ID OK - casting vote
if ($anonymous_vote === 1) {
// $query = "INSERT INTO pt_votes (votes_id,entry_id,vote,vote_time,enc) VALUES ('$votes_id','$entry_id','$vote','$time_now','$enc_base64')";
$query = "INSERT INTO pt_votes (votes_id,entry_id,vote,enc) VALUES ('$votes_id','$entry_id','$vote','$enc_base64')";
}
else {
// $query = "INSERT INTO pt_votes (votes_id,entry_id,vote,vote_time,enc,member_id) VALUES ('$votes_id','$entry_id','$vote','$time_now','$enc_base64','$member_id')";
$query = "INSERT INTO pt_votes (votes_id,entry_id,vote,enc,member_id) VALUES ('$votes_id','$entry_id','$vote','$enc_base64','$member_id')";
}
mysqli_query($link, $query) ? null : $all_query_ok=FALSE;
// Shuffle table
$query = "ALTER TABLE pt_votes ORDER BY votes_id ASC";
mysqli_query($link, $query) ? null : $all_query_ok=FALSE;
// Getting ID for voters_id
$num_results_3 = 1;
while ($num_results_3 != 0) {
$voters_id = crypto_rand(100,1000000000000);
$result_3 = mysqli_query($link, "SELECT * FROM pt_voters WHERE voters_id='$voters_id'"); // Check for existance of voters_id
$num_results_3 = mysqli_num_rows($result_3);
if ($num_results_3 == 0) {
// $query = "INSERT INTO pt_voters (voters_id,entry_id,member_id,ip_address,change_vote,iv,salt) VALUES ('$voters_id','$entry_id','$member_id','$ip_address','$change_vote','$iv_base64','$salt')";
$query = "INSERT INTO pt_voters (voters_id,entry_id,member_id,change_vote,iv,hash) VALUES ('$voters_id','$entry_id','$member_id','$change_vote','$iv_base64','$hash')";
mysqli_query($link, $query) ? null : $all_query_ok=FALSE;
// Shuffle table
$query = "ALTER TABLE pt_voters ORDER BY voters_id ASC";
mysqli_query($link, $query) ? null : $all_query_ok=FALSE;
}
}
if ($all_query_ok) {
mysqli_commit($link);
}
else {
mysqli_rollback($link);
echo '
<p style="text-align: center"><strong>AFSTEMNING</strong></p>
<p>Der opstod en fejl! Du kan genindlæse siden og forsøge igen!</p>
<p><strong>Fejlkode:</strong> 008</p>
';
mysqli_close($link);
$log = $member_id." made the SQL fail! 008";
logvote($log);
exit();
}
// Everything OK. Inform user and write to logfile, print and/or insert posts in S3
$action = ' voted';
if ($changing_vote === 1) {
$action = ' changed vote';
}
if ($anonymous_vote === 1) {
$action .= ' anonymously';
}
if ($change_vote === 1) {
$action .= ' and is allowed to change vote';
}
$log = $member_id.$action;
logvote($log);
if ($anonymous_vote === 1) {
echo '
<p style="text-align: center;"><strong>AFSTEMNING</strong></p>
<p style="text-align: justify;">Din stemme er registreret anonymt. Du kan bruge nedenstående ID til at verificere stemmens korrekthed, når afstemningen er slut. Bemærk at
ID ikke er tilknyttet dig i vores system, så du skal selv huske det, hvis du ønsker at verificere. Det vises ikke igen, når du forlader denne side.</p>
<div id="printablereceipt">
<p style="text-align: center; font-family: Courier New, monospace; letter-spacing: 3px; font-size: 150%;"><strong>'.$votes_id.'</strong></p>
<div id="QRimage" style="text-align:center;"></div>
</div>
<p style="text-align: center;"><a class="btn btn-success" style="color: white;" href="#" onclick="printReceipt();"><i class="icon-print icon-large"></i> UDSKRIV</a></p>
';
$QRcode = 'https://www.parlamentet.dk/parlamentet/detaljer/'.$entry_id.'#v'.$votes_id;
echo "
<script>
var image = qr.image({
background: '#f5f5f5',
size: 7,
level: 'L',
value: '".$QRcode."'
});
if (image) {
document.getElementById('QRimage').appendChild(image);
}
</script>
";
}
else {
echo '
<p style="text-align: center;"><strong>AFSTEMNING</strong></p>
<p style="text-align: justify;">Din stemme er registreret uden anonymitet. Når afstemningen er slut, vil dit navn vises med din stemme, så du kan verificere stemmen. Du kan også bruge nedenstående ID til at verificere stemmen.</p>
<p style="text-align: center; font-family: Courier New, monospace; letter-spacing: 3px; font-size: 150%;"><strong>'.$votes_id.'</strong></p>
<div id="QRimage" style="text-align:center;"></div>
</div>
<p style="text-align: center;"><a class="btn btn-success" style="color: white;" href="#" onclick="printReceipt();"><i class="icon-print icon-large"></i> UDSKRIV</a></p>
';
$QRcode = 'https://www.parlamentet.dk/parlamentet/detaljer/'.$entry_id.'#v'.$votes_id;
echo "
<script>
var image = qr.image({
background: '#f5f5f5',
size: 7,
level: 'L',
value: '".$QRcode."'
});
if (image) {
document.getElementById('QRimage').appendChild(image);
}
</script>
";
}
}
}
}
else {
echo '
<p style="text-align: center"><strong>AFSTEMNING</strong></p>
<p><strong>Fejl:</strong> Du har allerede stemt i denne afstemning eller afstemningen er ikke åben.</p>
<p><strong>Fejlkode:</strong> 009</p>
';
$log = $member_id." tried to vote but failed 009";
logvote($log);
exit();
}
}
else {
echo '
<p style="text-align: center"><strong>AFSTEMNING</strong></p>
<p><strong>Fejl:</strong> Du har ikke rettigheder til at stemme.</p>
<p><strong>Fejlkode:</strong> 010</p>
';
$log = $member_id." does not have rights to vote 010";
logvote($log);
exit();
}
?>
Help much appreciated.