scenebuilder wasnt working on my laptop for some reason so i implemented the voting system i guess.... still need to test it and refactor it a little bit, i have a lot of duplicate code that i can just put into 2-3 external functions
This commit is contained in:
parent
84381bc165
commit
7ba9e4f5e6
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@ -4,7 +4,7 @@
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_X" default="true" project-jdk-name="openjdk-25" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="openjdk-25" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
55
src/main/java/dev/ksan/voteSystem/HmacKeyRepository.java
Normal file
55
src/main/java/dev/ksan/voteSystem/HmacKeyRepository.java
Normal file
@ -0,0 +1,55 @@
|
||||
package dev.ksan.voteSystem;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
public class HmacKeyRepository {
|
||||
|
||||
private static final Path HMAC_DIR = Path.of("data/hmackeys");
|
||||
private static final ConcurrentHashMap<String, ReentrantReadWriteLock> locks =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
private static ReentrantReadWriteLock getLock(String key) {
|
||||
return locks.computeIfAbsent(key, k -> new ReentrantReadWriteLock());
|
||||
}
|
||||
|
||||
public static void save(String voterId, String pollId, byte[] hmacKey) throws Exception {
|
||||
Path path = resolvePath(voterId, pollId);
|
||||
Files.createDirectories(path.getParent());
|
||||
|
||||
String key = lockKey(voterId, pollId);
|
||||
ReentrantReadWriteLock.WriteLock writeLock = getLock(key).writeLock();
|
||||
writeLock.lock();
|
||||
try {
|
||||
Files.write(path, hmacKey);
|
||||
} finally {
|
||||
writeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] load(String voterId, String pollId) throws Exception {
|
||||
Path path = resolvePath(voterId, pollId);
|
||||
String key = lockKey(voterId, pollId);
|
||||
|
||||
ReentrantReadWriteLock.ReadLock readLock = getLock(key).readLock();
|
||||
readLock.lock();
|
||||
try {
|
||||
if (!Files.exists(path))
|
||||
throw new IllegalStateException("No HMAC key found for voter: "
|
||||
+ voterId + " in poll: " + pollId);
|
||||
return Files.readAllBytes(path);
|
||||
} finally {
|
||||
readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private static Path resolvePath(String voterId, String pollId) {
|
||||
return HMAC_DIR.resolve(pollId).resolve(voterId + ".key");
|
||||
}
|
||||
|
||||
private static String lockKey(String voterId, String pollId) {
|
||||
return pollId + ":" + voterId;
|
||||
}
|
||||
}
|
||||
96
src/main/java/dev/ksan/voteSystem/Poll.java
Normal file
96
src/main/java/dev/ksan/voteSystem/Poll.java
Normal file
@ -0,0 +1,96 @@
|
||||
package dev.ksan.voteSystem;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class Poll implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final String pollId;
|
||||
|
||||
private final String organizerId;
|
||||
|
||||
private String title;
|
||||
|
||||
private String description;
|
||||
|
||||
private LocalDateTime startTime;
|
||||
private LocalDateTime endTime;
|
||||
|
||||
private List<PollOption> options; // 2-5 options
|
||||
private boolean tallied; // true after organizer runs tally
|
||||
|
||||
public Poll(String organizerId, String title, String description,
|
||||
LocalDateTime startTime, LocalDateTime endTime) {
|
||||
this.pollId = UUID.randomUUID().toString();
|
||||
this.organizerId = organizerId;
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.startTime = startTime;
|
||||
this.endTime = endTime;
|
||||
this.options = new ArrayList<>();
|
||||
this.tallied = false;
|
||||
}
|
||||
|
||||
public PollStatus getStatus() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (now.isBefore(startTime))
|
||||
return PollStatus.PENDING;
|
||||
if (now.isAfter(endTime))
|
||||
return PollStatus.FINISHED;
|
||||
return PollStatus.ACTIVE;
|
||||
}
|
||||
|
||||
public void addOption(PollOption option) {
|
||||
if (options.size() >= 5)
|
||||
throw new IllegalStateException("Maximum 5 options allowed.");
|
||||
options.add(option);
|
||||
}
|
||||
|
||||
public boolean isReadyForTally() {
|
||||
return getStatus() == PollStatus.FINISHED && !tallied;
|
||||
}
|
||||
|
||||
public String getPollId() {
|
||||
return pollId;
|
||||
}
|
||||
|
||||
public String getOrganizerId() {
|
||||
return organizerId;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public LocalDateTime getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public LocalDateTime getEndTime() {
|
||||
return endTime;
|
||||
}
|
||||
|
||||
public List<PollOption> getOptions() {
|
||||
return Collections.unmodifiableList(options);
|
||||
}
|
||||
|
||||
public boolean isTallied() {
|
||||
return tallied;
|
||||
}
|
||||
|
||||
public void setTallied(boolean tallied) {
|
||||
this.tallied = tallied;
|
||||
}
|
||||
}
|
||||
26
src/main/java/dev/ksan/voteSystem/PollOption.java
Normal file
26
src/main/java/dev/ksan/voteSystem/PollOption.java
Normal file
@ -0,0 +1,26 @@
|
||||
package dev.ksan.voteSystem;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
|
||||
public class PollOption implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final String optionId;
|
||||
private final String text;
|
||||
|
||||
public PollOption(String text) {
|
||||
this.optionId = UUID.randomUUID().toString();
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public String getOptionId() {
|
||||
return optionId;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
124
src/main/java/dev/ksan/voteSystem/PollRepository.java
Normal file
124
src/main/java/dev/ksan/voteSystem/PollRepository.java
Normal file
@ -0,0 +1,124 @@
|
||||
package dev.ksan.voteSystem;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class PollRepository {
|
||||
|
||||
private static final Path POLL_DIR = Path.of("data/polls");
|
||||
private static final ConcurrentHashMap<String, ReentrantReadWriteLock> locks =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
private static ReentrantReadWriteLock getLock(String pollId) {
|
||||
return locks.computeIfAbsent(pollId, k -> new ReentrantReadWriteLock());
|
||||
}
|
||||
|
||||
public static void save(Poll poll) throws Exception {
|
||||
Files.createDirectories(POLL_DIR);
|
||||
ReentrantReadWriteLock.WriteLock writeLock = getLock(poll.getPollId()).writeLock();
|
||||
writeLock.lock();
|
||||
try {
|
||||
writeToFile(poll, resolvePath(poll.getPollId()));
|
||||
} finally {
|
||||
writeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public static Poll findById(String pollId) throws Exception {
|
||||
ReentrantReadWriteLock.ReadLock readLock = getLock(pollId).readLock();
|
||||
readLock.lock();
|
||||
try {
|
||||
return (Poll) deserialize(resolvePath(pollId));
|
||||
} finally {
|
||||
readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Poll> findAll() throws Exception {
|
||||
if (!Files.exists(POLL_DIR)) return Collections.emptyList();
|
||||
|
||||
List<Poll> results = new ArrayList<>();
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(POLL_DIR, "*.ser")) {
|
||||
for (Path path : stream) {
|
||||
String pollId = path.getFileName().toString().replace(".ser", "");
|
||||
ReentrantReadWriteLock.ReadLock readLock = getLock(pollId).readLock();
|
||||
readLock.lock();
|
||||
try {
|
||||
Object obj = deserialize(path);
|
||||
if (obj instanceof Poll poll) results.add(poll);
|
||||
} finally {
|
||||
readLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
public static List<Poll> findByOrganizer(String organizerId) throws Exception {
|
||||
return findAll().stream()
|
||||
.filter(p -> p.getOrganizerId().equals(organizerId))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static List<Poll> findActive() throws Exception {
|
||||
return findAll().stream()
|
||||
.filter(p -> p.getStatus() == PollStatus.ACTIVE)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static void update(Poll poll) throws Exception {
|
||||
ReentrantReadWriteLock.WriteLock writeLock = getLock(poll.getPollId()).writeLock();
|
||||
writeLock.lock();
|
||||
try {
|
||||
if (!Files.exists(resolvePath(poll.getPollId())))
|
||||
throw new IllegalStateException("Poll not found: " + poll.getPollId());
|
||||
writeToFile(poll, resolvePath(poll.getPollId()));
|
||||
} finally {
|
||||
writeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean exists(String pollId) {
|
||||
return Files.exists(resolvePath(pollId));
|
||||
}
|
||||
|
||||
private static Path resolvePath(String pollId) {
|
||||
return POLL_DIR.resolve(pollId + ".ser");
|
||||
}
|
||||
|
||||
private static void writeToFile(Object obj, Path targetPath) throws Exception {
|
||||
Path tmpPath = targetPath.resolveSibling(targetPath.getFileName() + ".tmp");
|
||||
try (FileOutputStream fos = new FileOutputStream(tmpPath.toFile());
|
||||
FileLock fileLock = fos.getChannel().lock();
|
||||
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
|
||||
oos.writeObject(obj);
|
||||
oos.flush();
|
||||
}
|
||||
Files.move(tmpPath, targetPath,
|
||||
StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
private static Object deserialize(Path path) throws Exception {
|
||||
if (!Files.exists(path)) return null;
|
||||
try (FileInputStream fis = new FileInputStream(path.toFile());
|
||||
FileLock fileLock = fis.getChannel().tryLock(0, Long.MAX_VALUE, true);
|
||||
ObjectInputStream ois = new ObjectInputStream(fis)) {
|
||||
if (fileLock == null)
|
||||
throw new IllegalStateException("Could not acquire read lock on: " + path);
|
||||
return ois.readObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/main/java/dev/ksan/voteSystem/PollStatus.java
Normal file
7
src/main/java/dev/ksan/voteSystem/PollStatus.java
Normal file
@ -0,0 +1,7 @@
|
||||
package dev.ksan.voteSystem;
|
||||
|
||||
public enum PollStatus {
|
||||
ACTIVE,
|
||||
PENDING,
|
||||
FINISHED,
|
||||
}
|
||||
83
src/main/java/dev/ksan/voteSystem/TallyResult.java
Normal file
83
src/main/java/dev/ksan/voteSystem/TallyResult.java
Normal file
@ -0,0 +1,83 @@
|
||||
package dev.ksan.voteSystem;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
|
||||
public class TallyResult implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final String pollId;
|
||||
|
||||
private final String pollTitle;
|
||||
|
||||
private final Map<String, Integer> resultsByOption;
|
||||
|
||||
private final int validVotes;
|
||||
private final int invalidVotes;
|
||||
|
||||
private final LocalDateTime talliedAt;
|
||||
|
||||
private byte[] report;
|
||||
private byte[] reportSignature;
|
||||
|
||||
public TallyResult(String pollId, String pollTitle, Map<String, Integer> resultsByOption,
|
||||
int validVotes, int invalidVotes, LocalDateTime talliedAt) {
|
||||
this.pollId = pollId;
|
||||
this.pollTitle = pollTitle;
|
||||
this.resultsByOption = resultsByOption;
|
||||
this.validVotes = validVotes;
|
||||
this.invalidVotes = invalidVotes;
|
||||
this.talliedAt = talliedAt;
|
||||
}
|
||||
|
||||
public String getPollId() {
|
||||
return pollId;
|
||||
}
|
||||
|
||||
public String getPollTitle() {
|
||||
return pollTitle;
|
||||
}
|
||||
|
||||
public Map<String, Integer> getResultsByOption() {
|
||||
return resultsByOption;
|
||||
}
|
||||
|
||||
public int getValidVotes() {
|
||||
return validVotes;
|
||||
}
|
||||
|
||||
public int getInvalidVotes() {
|
||||
return invalidVotes;
|
||||
}
|
||||
|
||||
public LocalDateTime getTalliedAt() {
|
||||
return talliedAt;
|
||||
}
|
||||
|
||||
public byte[] getReport() {
|
||||
return report;
|
||||
}
|
||||
|
||||
public byte[] getReportSignature() {
|
||||
return reportSignature;
|
||||
}
|
||||
|
||||
public void setReport(byte[] report) {
|
||||
this.report = report;
|
||||
}
|
||||
|
||||
public void setReportSignature(byte[] sig) {
|
||||
this.reportSignature = sig;
|
||||
}
|
||||
|
||||
public void printReport() {
|
||||
System.out.println(new String(report, StandardCharsets.UTF_8));
|
||||
System.out.println("Signature (Base64): " +
|
||||
Base64.getEncoder().encodeToString(reportSignature));
|
||||
}
|
||||
}
|
||||
29
src/main/java/dev/ksan/voteSystem/TallyResultRepository.java
Normal file
29
src/main/java/dev/ksan/voteSystem/TallyResultRepository.java
Normal file
@ -0,0 +1,29 @@
|
||||
package dev.ksan.voteSystem;
|
||||
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class TallyResultRepository {
|
||||
|
||||
private static final Path TALLY_DIR = Path.of("data/tallies");
|
||||
|
||||
public static void save(TallyResult result) throws Exception {
|
||||
Files.createDirectories(TALLY_DIR);
|
||||
Path path = TALLY_DIR.resolve(result.getPollId() + ".ser");
|
||||
try (ObjectOutputStream oos = new ObjectOutputStream(
|
||||
Files.newOutputStream(path))) {
|
||||
oos.writeObject(result);
|
||||
}
|
||||
}
|
||||
|
||||
public static TallyResult findByPoll(String pollId) throws Exception {
|
||||
Path path = TALLY_DIR.resolve(pollId + ".ser");
|
||||
if (!Files.exists(path)) return null;
|
||||
try (ObjectInputStream ois = new ObjectInputStream(
|
||||
Files.newInputStream(path))) {
|
||||
return (TallyResult) ois.readObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
169
src/main/java/dev/ksan/voteSystem/TallyService.java
Normal file
169
src/main/java/dev/ksan/voteSystem/TallyService.java
Normal file
@ -0,0 +1,169 @@
|
||||
package dev.ksan.voteSystem;
|
||||
|
||||
import dev.ksan.Users.Organizer;
|
||||
import dev.ksan.Users.UserRepository;
|
||||
import dev.ksan.Users.Voter;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class TallyService {
|
||||
|
||||
private static final String AES_MODE = "AES/CBC/PKCS5Padding";
|
||||
private static final String RSA_MODE = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
|
||||
|
||||
public static TallyResult tallyPoll(
|
||||
Poll poll,
|
||||
Organizer organizer,
|
||||
PrivateKey organizerPrivateKey
|
||||
) throws Exception {
|
||||
|
||||
if (poll.getStatus() != PollStatus.FINISHED)
|
||||
throw new IllegalStateException("Poll has not finished yet.");
|
||||
|
||||
if (poll.isTallied())
|
||||
throw new IllegalStateException("Poll has already been tallied.");
|
||||
|
||||
if (!poll.getOrganizerId().equals(organizer.getId()))
|
||||
throw new IllegalStateException("This poll does not belong to this organizer.");
|
||||
|
||||
List<Vote> votes = VoteRepository.findAllByPoll(poll.getPollId());
|
||||
|
||||
Map<String, Integer> counts = new LinkedHashMap<>();
|
||||
for (PollOption option : poll.getOptions()) {
|
||||
counts.put(option.getOptionId(), 0);
|
||||
}
|
||||
|
||||
int validVotes = 0;
|
||||
int invalidVotes = 0;
|
||||
|
||||
for (Vote vote : votes) {
|
||||
|
||||
Voter voter = UserRepository.findVoter(vote.getVoterId());
|
||||
if (voter == null) {
|
||||
invalidVotes++;
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean signatureValid = verifySignature(
|
||||
vote.getEncryptedVote(),
|
||||
vote.getPollId(),
|
||||
vote.getVoterId(),
|
||||
vote.getSignature(),
|
||||
voter.getPublicKey()
|
||||
);
|
||||
|
||||
if (!signatureValid) {
|
||||
System.out.println("Invalid signature for voter: " + vote.getVoterId());
|
||||
invalidVotes++;
|
||||
continue;
|
||||
}
|
||||
|
||||
Cipher rsaCipher = Cipher.getInstance(RSA_MODE, "BC");
|
||||
rsaCipher.init(Cipher.DECRYPT_MODE, organizerPrivateKey);
|
||||
byte[] aesKeyBytes = rsaCipher.doFinal(vote.getEncryptedSymmetricKey());
|
||||
SecretKey aesKey = new SecretKeySpec(aesKeyBytes, "AES");
|
||||
|
||||
Cipher aesCipher = Cipher.getInstance(AES_MODE);
|
||||
aesCipher.init(Cipher.DECRYPT_MODE, aesKey, new IvParameterSpec(vote.getIv()));
|
||||
String optionId = new String(
|
||||
aesCipher.doFinal(vote.getEncryptedVote()), StandardCharsets.UTF_8);
|
||||
|
||||
if (counts.containsKey(optionId)) {
|
||||
counts.merge(optionId, 1, Integer::sum);
|
||||
validVotes++;
|
||||
} else {
|
||||
invalidVotes++;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Integer> resultsByText = new LinkedHashMap<>();
|
||||
for (PollOption option : poll.getOptions()) {
|
||||
resultsByText.put(option.getText(), counts.get(option.getOptionId()));
|
||||
}
|
||||
|
||||
TallyResult result = new TallyResult(
|
||||
poll.getPollId(),
|
||||
poll.getTitle(),
|
||||
resultsByText,
|
||||
validVotes,
|
||||
invalidVotes,
|
||||
LocalDateTime.now()
|
||||
);
|
||||
|
||||
byte[] report = generateReport(result, poll, organizer);
|
||||
byte[] reportSignature = signReport(report, organizerPrivateKey);
|
||||
result.setReport(report);
|
||||
result.setReportSignature(reportSignature);
|
||||
|
||||
poll.setTallied(true);
|
||||
PollRepository.update(poll);
|
||||
|
||||
TallyResultRepository.save(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean verifySignature(
|
||||
byte[] encryptedVote, String pollId,
|
||||
String voterId, byte[] signature,
|
||||
PublicKey voterPublicKey
|
||||
) {
|
||||
try {
|
||||
byte[] data = buildSignableData(encryptedVote, pollId, voterId);
|
||||
Signature sig = Signature.getInstance("SHA256withRSA", "BC");
|
||||
sig.initVerify(voterPublicKey);
|
||||
sig.update(data);
|
||||
return sig.verify(signature);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] buildSignableData(
|
||||
byte[] encryptedVote, String pollId, String voterId) {
|
||||
byte[] pollIdBytes = pollId.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] voterIdBytes = voterId.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] combined = new byte[encryptedVote.length + pollIdBytes.length + voterIdBytes.length];
|
||||
System.arraycopy(encryptedVote, 0, combined, 0, encryptedVote.length);
|
||||
System.arraycopy(pollIdBytes, 0, combined, encryptedVote.length, pollIdBytes.length);
|
||||
System.arraycopy(voterIdBytes, 0, combined,
|
||||
encryptedVote.length + pollIdBytes.length, voterIdBytes.length);
|
||||
return combined;
|
||||
}
|
||||
|
||||
private static byte[] generateReport(
|
||||
TallyResult result, Poll poll, Organizer organizer) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("=== TALLY REPORT ===\n");
|
||||
sb.append("Poll : ").append(poll.getTitle()).append("\n");
|
||||
sb.append("Organizer: ").append(organizer.getOrganizationName()).append("\n");
|
||||
sb.append("Period : ").append(poll.getStartTime())
|
||||
.append(" -> ").append(poll.getEndTime()).append("\n");
|
||||
sb.append("Tallied : ").append(result.getTalliedAt()).append("\n");
|
||||
sb.append("--------------------\n");
|
||||
result.getResultsByOption().forEach((option, count) ->
|
||||
sb.append(option).append(": ").append(count).append(" votes\n"));
|
||||
sb.append("--------------------\n");
|
||||
sb.append("Valid votes : ").append(result.getValidVotes()).append("\n");
|
||||
sb.append("Invalid votes: ").append(result.getInvalidVotes()).append("\n");
|
||||
return sb.toString().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static byte[] signReport(byte[] report, PrivateKey key) throws Exception {
|
||||
Signature sig = Signature.getInstance("SHA256withRSA", "BC");
|
||||
sig.initSign(key);
|
||||
sig.update(report);
|
||||
return sig.sign();
|
||||
}
|
||||
}
|
||||
79
src/main/java/dev/ksan/voteSystem/Vote.java
Normal file
79
src/main/java/dev/ksan/voteSystem/Vote.java
Normal file
@ -0,0 +1,79 @@
|
||||
package dev.ksan.voteSystem;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public class Vote implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final String voteId;
|
||||
private final String pollId;
|
||||
private final String voterId;
|
||||
|
||||
private final byte[] encryptedVote;
|
||||
|
||||
private final byte[] encryptedSymmetricKey;
|
||||
|
||||
// AES IV needed for decryption
|
||||
private final byte[] iv;
|
||||
|
||||
// voter digital signature over (encryptedVote + pollId + voterId)
|
||||
private final byte[] signature;
|
||||
|
||||
private final byte[] metadataHmac;
|
||||
|
||||
private final LocalDateTime castTime;
|
||||
|
||||
public Vote(String pollId, String voterId,
|
||||
byte[] encryptedVote, byte[] encryptedSymmetricKey,
|
||||
byte[] iv, byte[] signature, byte[] metadataHmac) {
|
||||
this.voteId = UUID.randomUUID().toString();
|
||||
this.pollId = pollId;
|
||||
this.voterId = voterId;
|
||||
this.encryptedVote = encryptedVote;
|
||||
this.encryptedSymmetricKey = encryptedSymmetricKey;
|
||||
this.iv = iv;
|
||||
this.signature = signature;
|
||||
this.metadataHmac = metadataHmac;
|
||||
this.castTime = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public String getVoteId() {
|
||||
return voteId;
|
||||
}
|
||||
|
||||
public String getPollId() {
|
||||
return pollId;
|
||||
}
|
||||
|
||||
public String getVoterId() {
|
||||
return voterId;
|
||||
}
|
||||
|
||||
public byte[] getEncryptedVote() {
|
||||
return encryptedVote;
|
||||
}
|
||||
|
||||
public byte[] getEncryptedSymmetricKey() {
|
||||
return encryptedSymmetricKey;
|
||||
}
|
||||
|
||||
public byte[] getIv() {
|
||||
return iv;
|
||||
}
|
||||
|
||||
public byte[] getSignature() {
|
||||
return signature;
|
||||
}
|
||||
|
||||
public byte[] getMetadataHmac() {
|
||||
return metadataHmac;
|
||||
}
|
||||
|
||||
public LocalDateTime getCastTime() {
|
||||
return castTime;
|
||||
}
|
||||
}
|
||||
116
src/main/java/dev/ksan/voteSystem/VoteRepository.java
Normal file
116
src/main/java/dev/ksan/voteSystem/VoteRepository.java
Normal file
@ -0,0 +1,116 @@
|
||||
package dev.ksan.voteSystem;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
public class VoteRepository {
|
||||
|
||||
private static final Path VOTE_DIR = Path.of("data/votes");
|
||||
private static final ConcurrentHashMap<String, ReentrantReadWriteLock> locks =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
private static ReentrantReadWriteLock getLock(String key) {
|
||||
return locks.computeIfAbsent(key, k -> new ReentrantReadWriteLock());
|
||||
}
|
||||
|
||||
public static void save(Vote vote) throws Exception {
|
||||
Path path = resolvePath(vote.getPollId(), vote.getVoterId());
|
||||
Files.createDirectories(path.getParent());
|
||||
|
||||
String key = lockKey(vote.getPollId(), vote.getVoterId());
|
||||
ReentrantReadWriteLock.WriteLock writeLock = getLock(key).writeLock();
|
||||
writeLock.lock();
|
||||
try {
|
||||
if (Files.exists(path))
|
||||
throw new IllegalStateException("Vote already exists for this voter in this poll.");
|
||||
writeToFile(vote, path);
|
||||
} finally {
|
||||
writeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public static Vote findByVoterAndPoll(String voterId, String pollId) {
|
||||
Path path = resolvePath(pollId, voterId);
|
||||
String key = lockKey(pollId, voterId);
|
||||
|
||||
ReentrantReadWriteLock.ReadLock readLock = getLock(key).readLock();
|
||||
readLock.lock();
|
||||
try {
|
||||
return (Vote) deserialize(path);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
} finally {
|
||||
readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Vote> findAllByPoll(String pollId) throws Exception {
|
||||
Path pollVoteDir = VOTE_DIR.resolve(pollId);
|
||||
if (!Files.exists(pollVoteDir)) return Collections.emptyList();
|
||||
|
||||
List<Vote> votes = new ArrayList<>();
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(pollVoteDir, "*.ser")) {
|
||||
for (Path path : stream) {
|
||||
String voterId = path.getFileName().toString().replace(".ser", "");
|
||||
String key = lockKey(pollId, voterId);
|
||||
|
||||
ReentrantReadWriteLock.ReadLock readLock = getLock(key).readLock();
|
||||
readLock.lock();
|
||||
try {
|
||||
Object obj = deserialize(path);
|
||||
if (obj instanceof Vote vote) votes.add(vote);
|
||||
} finally {
|
||||
readLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
return votes;
|
||||
}
|
||||
|
||||
public static int countByPoll(String pollId) throws Exception {
|
||||
return findAllByPoll(pollId).size();
|
||||
}
|
||||
|
||||
private static Path resolvePath(String pollId, String voterId) {
|
||||
return VOTE_DIR.resolve(pollId).resolve(voterId + ".ser");
|
||||
}
|
||||
|
||||
private static String lockKey(String pollId, String voterId) {
|
||||
return pollId + ":" + voterId;
|
||||
}
|
||||
|
||||
private static void writeToFile(Object obj, Path targetPath) throws Exception {
|
||||
Path tmpPath = targetPath.resolveSibling(targetPath.getFileName() + ".tmp");
|
||||
try (FileOutputStream fos = new FileOutputStream(tmpPath.toFile());
|
||||
FileLock fileLock = fos.getChannel().lock();
|
||||
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
|
||||
oos.writeObject(obj);
|
||||
oos.flush();
|
||||
}
|
||||
Files.move(tmpPath, targetPath,
|
||||
StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
private static Object deserialize(Path path) throws Exception {
|
||||
if (!Files.exists(path)) return null;
|
||||
try (FileInputStream fis = new FileInputStream(path.toFile());
|
||||
FileLock fileLock = fis.getChannel().tryLock(0, Long.MAX_VALUE, true);
|
||||
ObjectInputStream ois = new ObjectInputStream(fis)) {
|
||||
if (fileLock == null)
|
||||
throw new IllegalStateException("Could not acquire read lock on: " + path);
|
||||
return ois.readObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
122
src/main/java/dev/ksan/voteSystem/VotingService.java
Normal file
122
src/main/java/dev/ksan/voteSystem/VotingService.java
Normal file
@ -0,0 +1,122 @@
|
||||
package dev.ksan.voteSystem;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.*;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import dev.ksan.Users.Voter;
|
||||
|
||||
public class VotingService {
|
||||
|
||||
private static final String AES = "AES";
|
||||
private static final String AES_MODE = "AES/CBC/PKCS5Padding";
|
||||
private static final String RSA_MODE = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
|
||||
private static final String HMAC_ALGO = "HmacSHA256";
|
||||
private static final String BC = "BC";
|
||||
|
||||
public static Vote castVote(
|
||||
String selectedOptionId,
|
||||
Poll poll,
|
||||
Voter voter,
|
||||
PrivateKey voterPrivateKey,
|
||||
PublicKey organizerPublicKey) throws Exception {
|
||||
|
||||
if (poll.getStatus() != PollStatus.ACTIVE)
|
||||
throw new IllegalStateException("Poll is not active.");
|
||||
|
||||
if (hasVoted(voter.getUsername(), poll.getPollId()))
|
||||
throw new IllegalStateException("You have already voted in this poll.");
|
||||
|
||||
KeyGenerator keyGen = KeyGenerator.getInstance(AES, BC);
|
||||
keyGen.init(256, new SecureRandom());
|
||||
SecretKey aesKey = keyGen.generateKey();
|
||||
|
||||
byte[] iv = new byte[16];
|
||||
new SecureRandom().nextBytes(iv);
|
||||
|
||||
Cipher aesCipher = Cipher.getInstance(AES_MODE);
|
||||
aesCipher.init(Cipher.ENCRYPT_MODE, aesKey, new IvParameterSpec(iv));
|
||||
byte[] encryptedVote = aesCipher.doFinal(selectedOptionId.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
Cipher rsaCipher = Cipher.getInstance(RSA_MODE, BC);
|
||||
rsaCipher.init(Cipher.ENCRYPT_MODE, organizerPublicKey);
|
||||
byte[] encryptedSymmetricKey = rsaCipher.doFinal(aesKey.getEncoded());
|
||||
|
||||
byte[] dataToSign = buildSignableData(encryptedVote, poll.getPollId(), voter.getUsername());
|
||||
byte[] signature = signData(dataToSign, voterPrivateKey);
|
||||
|
||||
byte[] hmacKey = generateHmacKey();
|
||||
byte[] metadata = buildMetadata(poll.getPollId(), voter.getUsername());
|
||||
byte[] metadataHmac = computeHmac(metadata, hmacKey);
|
||||
|
||||
HmacKeyRepository.save(voter.getUsername(), poll.getPollId(), hmacKey);
|
||||
|
||||
Vote vote = new Vote(
|
||||
poll.getPollId(),
|
||||
voter.getUsername(),
|
||||
encryptedVote,
|
||||
encryptedSymmetricKey,
|
||||
iv,
|
||||
signature,
|
||||
metadataHmac);
|
||||
|
||||
VoteRepository.save(vote);
|
||||
return vote;
|
||||
}
|
||||
|
||||
public static boolean verifyVote(String voterId, String pollId) throws Exception {
|
||||
Vote vote = VoteRepository.findByVoterAndPoll(voterId, pollId);
|
||||
if (vote == null)
|
||||
return false;
|
||||
|
||||
byte[] hmacKey = HmacKeyRepository.load(voterId, pollId);
|
||||
byte[] metadata = buildMetadata(pollId, voterId);
|
||||
byte[] expectedHmac = computeHmac(metadata, hmacKey);
|
||||
|
||||
return MessageDigest.isEqual(expectedHmac, vote.getMetadataHmac());
|
||||
}
|
||||
|
||||
public static boolean hasVoted(String voterId, String pollId) {
|
||||
return VoteRepository.findByVoterAndPoll(voterId, pollId) != null;
|
||||
}
|
||||
|
||||
private static byte[] buildSignableData(byte[] encryptedVote,
|
||||
String pollId, String voterId) {
|
||||
byte[] pollIdBytes = pollId.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] voterIdBytes = voterId.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] combined = new byte[encryptedVote.length + pollIdBytes.length + voterIdBytes.length];
|
||||
System.arraycopy(encryptedVote, 0, combined, 0, encryptedVote.length);
|
||||
System.arraycopy(pollIdBytes, 0, combined, encryptedVote.length, pollIdBytes.length);
|
||||
System.arraycopy(voterIdBytes, 0, combined, encryptedVote.length + pollIdBytes.length, voterIdBytes.length);
|
||||
return combined;
|
||||
}
|
||||
|
||||
private static byte[] buildMetadata(String pollId, String voterId) {
|
||||
return (pollId + "|" + voterId).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static byte[] signData(byte[] data, PrivateKey key) throws Exception {
|
||||
Signature sig = Signature.getInstance("SHA256withRSA", BC);
|
||||
sig.initSign(key);
|
||||
sig.update(data);
|
||||
return sig.sign();
|
||||
}
|
||||
|
||||
private static byte[] generateHmacKey() {
|
||||
byte[] key = new byte[32];
|
||||
new SecureRandom().nextBytes(key);
|
||||
return key;
|
||||
}
|
||||
|
||||
private static byte[] computeHmac(byte[] data, byte[] key) throws Exception {
|
||||
Mac mac = Mac.getInstance(HMAC_ALGO);
|
||||
mac.init(new SecretKeySpec(key, HMAC_ALGO));
|
||||
return mac.doFinal(data);
|
||||
}
|
||||
}
|
||||
0
src/main/resources/fxml/OrganizerView.fxml
Normal file
0
src/main/resources/fxml/OrganizerView.fxml
Normal file
0
src/main/resources/fxml/VoterView.fxml
Normal file
0
src/main/resources/fxml/VoterView.fxml
Normal file
Loading…
x
Reference in New Issue
Block a user