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:
Ksan 2026-03-29 23:43:03 +02:00
parent 84381bc165
commit 7ba9e4f5e6
14 changed files with 907 additions and 1 deletions

2
.idea/misc.xml generated
View File

@ -4,7 +4,7 @@
<component name="FrameworkDetectionExcludesConfiguration"> <component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" /> <file type="web" url="file://$PROJECT_DIR$" />
</component> </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" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </project>

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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();
}
}
}

View File

@ -0,0 +1,7 @@
package dev.ksan.voteSystem;
public enum PollStatus {
ACTIVE,
PENDING,
FINISHED,
}

View 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));
}
}

View 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();
}
}
}

View 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();
}
}

View 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;
}
}

View 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();
}
}
}

View 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);
}
}

View File