This commit is contained in:
Ksan 2026-03-25 17:34:49 +01:00
parent e866f771a1
commit 9fa1a7d914
15 changed files with 915 additions and 14 deletions

2
.idea/misc.xml generated
View File

@ -4,7 +4,7 @@
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="openjdk-25" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="openjdk-25" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@ -13,7 +13,10 @@ dependencies {
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
implementation("org.bouncycastle:bcprov-jdk18on:1.83")
implementation("org.bouncycastle:bcpkix-jdk18on:1.83")
implementation("org.springframework.security:spring-security-crypto:7.1.0-M2")
}
tasks.test {

View File

@ -0,0 +1,224 @@
package dev.ksan.CASystem;
import dev.ksan.Users.Organizer;
import dev.ksan.Users.User;
import dev.ksan.Users.Voter;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.*;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemWriter;
import java.io.StringWriter;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.Date;
public class CA {
public static CAResult generateRootCA() throws Exception {
KeyPair keyPair = KeyService.generateRSAKeyPair();
X500Name issuer = new X500Name("CN=RootCA, O=CA, OU=CA, L=BL, ST=BA");
X500Name subject = issuer; //self-signed
BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
Date notBefore = new Date();
Date notAfter = new Date(notBefore.getTime() + 3650L * 24 * 60 * 60 * 1000); // 10 year
JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(issuer,
serial,
notBefore,
notAfter,
subject,
keyPair.getPublic());
certBuilder.addExtension(
Extension.basicConstraints,
true,
new BasicConstraints(true)
);
certBuilder.addExtension(
Extension.keyUsage,
true,
new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign)
);
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
.setProvider("BC").build(keyPair.getPrivate());
X509CertificateHolder certHolder = certBuilder.build(signer);
X509Certificate certificate = new JcaX509CertificateConverter()
.setProvider("BC").getCertificate(certHolder);
certificate.verify(certificate.getPublicKey());
return new CAResult(certificate, keyPair);
}
public static CAResult generateSubCA(CAType type, X509Certificate issuerCert, PrivateKey issuerKey) throws Exception {
KeyPair keyPair = KeyService.generateRSAKeyPair();
X500Name issuer = new X500Name(issuerCert.getSubjectX500Principal().getName());
X500Name subject = new X500Name("CN=" + type + ", O=CA, OU=VotingSystem, L=BL, ST=BA");
BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
Date notBefore = new Date();
Date notAfter = new Date(notBefore.getTime() + 365L * 24 * 60 * 60 * 1000);
JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(issuer,
serial,
notBefore,
notAfter,
subject,
keyPair.getPublic());
certBuilder.addExtension(
Extension.basicConstraints,
true,
new BasicConstraints(true)
);
certBuilder.addExtension(
Extension.keyUsage,
true,
new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign)
);
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
.setProvider("BC").build(issuerKey);
X509CertificateHolder certHolder = certBuilder.build(signer);
X509Certificate cert = new JcaX509CertificateConverter()
.setProvider("BC").getCertificate(certHolder);
return new CAResult(cert, keyPair);
}
public static String publicKeyToPem(PublicKey publicKey) throws Exception {
try (StringWriter stringWriter = new StringWriter();
PemWriter pemWriter = new PemWriter(stringWriter)) {
pemWriter.writeObject(new PemObject("PUBLIC KEY", publicKey.getEncoded()));
pemWriter.flush(); //idk why but why not
return stringWriter.toString();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
public static boolean validateCertificate(
X509Certificate cert,
X509Certificate caCert
) {
try {
cert.checkValidity();
cert.verify(caCert.getPublicKey());
return true;
} catch (Exception e) {
return false;
}
}
// TODO delete probably idk
public static X509Certificate registerUser(
User user,
X509Certificate organizerCACert,
PrivateKey organizerCAKey,
X509Certificate voterCACert,
PrivateKey voterCAKey
) throws Exception {
if (user instanceof Organizer organizer) {
String cn = organizer.getName() + "-" + organizer.getId();
return generateUserCertificate(cn, organizerCACert, organizerCAKey, user.getPublicKey());
} else if (user instanceof Voter voter) {
String cn = voter.getUsername();
return generateUserCertificate(cn, voterCACert, voterCAKey, user.getPublicKey());
} else {
throw new IllegalArgumentException("Unknown user type: " + user.getClass().getName());
}
}
public static X509Certificate generateUserCertificate(
String username,
X509Certificate caCert,
PrivateKey caKey,
PublicKey userKey
) throws Exception {
X500Name issuer = new X500Name(caCert.getSubjectX500Principal().getName());
X500Name subject = new X500Name("CN=" + username);
BigInteger serial = new BigInteger(64, new SecureRandom());
Date notBefore = new Date();
Date notAfter = new Date(notBefore.getTime() + 365L * 24 * 60 * 60 * 1000);
JcaX509v3CertificateBuilder builder =
new JcaX509v3CertificateBuilder(
issuer,
serial,
notBefore,
notAfter,
subject,
userKey
);
builder.addExtension(
Extension.basicConstraints,
false,
new BasicConstraints(false)
);
builder.addExtension(
Extension.keyUsage,
true,
new KeyUsage(KeyUsage.digitalSignature)
);
builder.addExtension(
Extension.extendedKeyUsage,
false,
new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth)
);
ContentSigner signer =
new JcaContentSignerBuilder("SHA256withRSA")
.setProvider("BC").build(caKey);
return new JcaX509CertificateConverter()
.setProvider("BC")
.getCertificate(builder.build(signer));
}
}

View File

@ -0,0 +1,22 @@
package dev.ksan.CASystem;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
public class CAResult {
X509Certificate certificate;
KeyPair keyPair;
public CAResult(X509Certificate cert, KeyPair keyPair) {
this.certificate = cert;
this.keyPair = keyPair;
}
public X509Certificate getCertificate() {
return certificate;
}
public KeyPair getKeyPair() {
return keyPair;
}
}

View File

@ -0,0 +1,6 @@
package dev.ksan.CASystem;
public enum CAType {
ORGANIZER,
VOTER
}

View File

@ -1,13 +1,42 @@
package dev.ksan.CASystem;
import java.io.FileOutputStream;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
public class KeyService {
public static KeyPair generateRSAKeyPair() throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", "BC");
generator.initialize(2048, new SecureRandom());
return generator.generateKeyPair();
}
public static void storeUserKeyPair(
String alias,
KeyPair keyPair,
X509Certificate cert,
String password
) throws Exception {
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(null, null);
ks.setKeyEntry(
alias,
keyPair.getPrivate(),
password.toCharArray(),
new Certificate[]{cert}
);
try (FileOutputStream fos =
new FileOutputStream(alias + ".p12")) {
ks.store(fos, password.toCharArray());
}
}
}

View File

@ -0,0 +1,112 @@
package dev.ksan.CASystem;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Base64;
public class KeystoreService {
private static String KEYSTORE_TYPE = "PKCS12";
private static String KEY_ALIAS = "key";
private static final Path USER_KEYSORE_DIR = Path.of("keystores/users");
private static final Path CA_KEYSTORE_DIR = Path.of("keystores/ca");
public static void saveUserKeystore(String username,
PrivateKey privateKey,
X509Certificate caCert,
X509Certificate userCert,
char[] keystorePassword) throws Exception {
KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);
keyStore.load(null, null);
Certificate[] chain = {userCert, caCert};
keyStore.setKeyEntry(KEY_ALIAS, privateKey, keystorePassword, chain);
Path path = USER_KEYSORE_DIR.resolve(username + ".p12");
Files.createDirectories(path.getParent());
try(FileOutputStream fos = new FileOutputStream(path.toFile())) {
keyStore.store(fos, keystorePassword);
}
}
public static PrivateKey getPrivateKey(String username, char[] keystorePassword) throws Exception {
KeyStore keystore = loadKeyStore(USER_KEYSORE_DIR.resolve(username+".p12"), keystorePassword);
return (PrivateKey) keystore.getKey(KEY_ALIAS, keystorePassword);
}
public static X509Certificate getCertificate(String username, char[] keystorePassword) throws Exception {
KeyStore keyStore = loadKeyStore(USER_KEYSORE_DIR.resolve(username+".p12"), keystorePassword);
return (X509Certificate) keyStore.getCertificate(KEY_ALIAS);
}
public static void saveCAKeyStore(String caName,
PrivateKey privateKey,
X509Certificate caCert,
X509Certificate issuerCert,
char[] keystorePassword) throws Exception {}
private static KeyStore loadKeyStore(Path path, char[] password) throws Exception {
KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);
try(FileInputStream fis = new FileInputStream(path.toFile())) {
keyStore.load(fis, password);
}
return keyStore;
}
public static char[] deriveKeystorePassword(String rawPassword, String username) throws Exception {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
PBEKeySpec spec = new PBEKeySpec(
rawPassword.toCharArray(),
username.getBytes(StandardCharsets.UTF_8),
100_000,
256
);
byte[] hash = factory.generateSecret(spec).getEncoded();
return Base64.getEncoder().encodeToString(hash).toCharArray();
}
public static PrivateKey loadCAPrivateKey(
String caName,
char[] caPassword
) throws Exception {
KeyStore ks = loadKeystore(CA_KEYSTORE_DIR.resolve(caName + ".p12"), caPassword);
return (PrivateKey) ks.getKey(KEY_ALIAS, caPassword);
}
private static KeyStore loadKeystore(Path path, char[] password) throws Exception {
KeyStore ks = KeyStore.getInstance(KEYSTORE_TYPE);
try (InputStream is = Files.newInputStream(path)) {
ks.load(is, password);
}
return ks;
}
}

View File

@ -0,0 +1,109 @@
package dev.ksan.CASystem;
import dev.ksan.Users.Organizer;
import dev.ksan.Users.User;
import dev.ksan.Users.UserRepository;
import dev.ksan.Users.Voter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
public class RegistrationService {
// CA certs are loaded from truststore (public, accessible before login)
// CA private keys are loaded from CA keystores (protected)
private static final char[] CA_KEYSTORE_PASSWORD =
System.getenv("CA_KEYSTORE_PASSWORD").toCharArray();
private static final char[] TRUSTSTORE_PASSWORD =
System.getenv("TRUSTSTORE_PASSWORD").toCharArray();
public static void registerOrganizer(Organizer organizer, String rawPassword)
throws Exception {
KeyPair keyPair = KeyService.generateRSAKeyPair();
X509Certificate organizerCACert = TruststoreService
.getTrustedCert("organizer-ca", TRUSTSTORE_PASSWORD);
PrivateKey organizerCAKey = KeystoreService
.loadCAPrivateKey("organizer-ca", CA_KEYSTORE_PASSWORD);
String cn = organizer.getName() + "-" + organizer.getId();
X509Certificate cert = CA.generateUserCertificate(
cn,
organizerCACert,
organizerCAKey,
keyPair.getPublic()
);
organizer.setCertificate(cert);
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
String hashedPassword = encoder.encode(rawPassword);
organizer.setHashedPassword(hashedPassword);
char[] ksPassword = KeystoreService.deriveKeystorePassword(
rawPassword,
organizer.getId().toString()
);
KeystoreService.saveUserKeystore(
organizer.getId().toString(),
keyPair.getPrivate(),
cert,
organizerCACert,
ksPassword
);
if (UserRepository.voterExists(organizer.getId().toString())) {
throw new IllegalStateException("Id already taken: " + organizer.getId());
}
UserRepository.save(organizer);
}
public static void registerVoter(Voter voter, String rawPassword)
throws Exception {
KeyPair keyPair = KeyService.generateRSAKeyPair();
X509Certificate voterCACert = TruststoreService
.getTrustedCert("voter-ca", TRUSTSTORE_PASSWORD);
PrivateKey voterCAKey = KeystoreService
.loadCAPrivateKey("voter-ca", CA_KEYSTORE_PASSWORD);
String cn = voter.getUsername();
X509Certificate cert = CA.generateUserCertificate(
cn,
voterCACert,
voterCAKey,
keyPair.getPublic()
);
voter.setCertificate(cert);
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
String hashedPassword = encoder.encode(rawPassword);
voter.setHashedPassword(hashedPassword);
char[] ksPassword = KeystoreService.deriveKeystorePassword(
rawPassword,
voter.getUsername()
);
KeystoreService.saveUserKeystore(
voter.getUsername(),
keyPair.getPrivate(),
cert,
voterCACert,
ksPassword
);
if (UserRepository.voterExists(voter.getUsername())) {
throw new IllegalStateException("Username already taken: " + voter.getUsername());
}
UserRepository.save(voter);
}
}

View File

@ -1,4 +0,0 @@
package dev.ksan.CASystem;
public class RootCA {
}

View File

@ -0,0 +1,86 @@
package dev.ksan.CASystem;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.io.InputStream;
public class TruststoreService {
private static final String TRUSTSTORE_TYPE = "PKCS12";
private static final Path TRUSTSTORE_PATH = Path.of("keystores/truststore.p12");
// Build truststore on first run (during CA initialization)
public static void createTruststore(
X509Certificate rootCACert,
X509Certificate organizerCACert,
X509Certificate voterCACert,
char[] truststorePassword
) throws Exception {
KeyStore ts = KeyStore.getInstance(TRUSTSTORE_TYPE);
ts.load(null, null);
ts.setCertificateEntry("root-ca", rootCACert);
ts.setCertificateEntry("organizer-ca", organizerCACert);
ts.setCertificateEntry("voter-ca", voterCACert);
Files.createDirectories(TRUSTSTORE_PATH.getParent());
try (OutputStream os = Files.newOutputStream(TRUSTSTORE_PATH)) {
ts.store(os, truststorePassword);
}
}
public static X509Certificate getTrustedCert(
String alias, // "root-ca", "organizer-ca", "voter-ca"
char[] truststorePassword
) throws Exception {
KeyStore ts = loadTruststore(truststorePassword);
return (X509Certificate) ts.getCertificate(alias);
}
// Validate a user cert against the correct sub-CA
public static boolean validateUserCertificate(
X509Certificate userCert,
CAType caType, // ORGANIZER or VOTER
char[] truststorePassword
) throws Exception {
String alias = switch (caType) {
case ORGANIZER -> "organizer-ca";
case VOTER -> "voter-ca";
};
X509Certificate caCert = getTrustedCert(alias, truststorePassword);
try {
userCert.checkValidity(); // time validity
userCert.verify(caCert.getPublicKey()); // issued by correct CA
verifyCertificateChain(caCert, truststorePassword); // CA itself is trusted
return true;
} catch (Exception e) {
return false;
}
}
// Verify sub-CA was signed by Root CA
private static void verifyCertificateChain(
X509Certificate subCACert,
char[] truststorePassword
) throws Exception {
X509Certificate rootCert = getTrustedCert("root-ca", truststorePassword);
subCACert.verify(rootCert.getPublicKey());
}
private static KeyStore loadTruststore(char[] password) throws Exception {
KeyStore ts = KeyStore.getInstance(TRUSTSTORE_TYPE);
try (InputStream is = Files.newInputStream(TRUSTSTORE_PATH)) {
ts.load(is, password);
}
return ts;
}
}

View File

@ -1,14 +1,20 @@
package dev.ksan;
import dev.ksan.CASystem.CA;
import dev.ksan.CASystem.CAResult;
import dev.ksan.CASystem.CAType;
import dev.ksan.CASystem.KeyService;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.KeyPair;
import java.security.Security;
import java.security.cert.X509Certificate;
import static dev.ksan.CASystem.CA.publicKeyToPem;
public class Main {
static void main() throws Exception {
public static void main(String args[]) throws Exception {
Security.addProvider(new BouncyCastleProvider());
System.out.println("added bouncycastle provider");
@ -20,5 +26,28 @@ public class Main {
int maxKeySize = javax.crypto.Cipher.getMaxAllowedKeyLength("AES");
System.out.println("Max Key Size for AES : " + maxKeySize);
System.out.println("test cert");
CAResult cert = CA.generateRootCA();
System.out.println(cert.getCertificate().toString());
System.out.println("test cert");
System.out.println("test cert");
System.out.println("test cert");
System.out.println("test cert");
CAResult prvi = CA.generateSubCA(CAType.VOTER ,cert.getCertificate(),cert.getKeyPair().getPrivate());
System.out.println(prvi.getCertificate().toString());
String publicKeyPem = publicKeyToPem(cert.getCertificate().getPublicKey());
System.out.println("Public Key PEM:\n" + publicKeyPem);
System.out.println(CA.validateCertificate(prvi.getCertificate(),cert.getCertificate()));
System.out.println(CA.validateCertificate(cert.getCertificate(),cert.getCertificate()));
System.out.println(CA.validateCertificate(cert.getCertificate(),prvi.getCertificate()));
}
}

View File

@ -1,7 +1,42 @@
package dev.ksan.Users;
import dev.ksan.CASystem.CA;
import dev.ksan.CASystem.KeyService;
import dev.ksan.CASystem.KeystoreService;
import java.io.Serial;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.util.UUID;
public class Organizer extends User {
UUID id ;
@Serial
private static final long serialVersionUID = 1L;
private String name;
private UUID id;
private String password;
public Organizer(String name, UUID id, String password) {
super();
this.name = name;
this.id = id;
this.password = password;
}
public String getName() {
return name;
}
public UUID getId() {
return id;
}
public String getPassword() {
return password;
}
public String getOrganizationName() {
return getName();
}
}

View File

@ -1,9 +1,39 @@
package dev.ksan.Users;
public class User {
private String username;
private String password;
import java.io.Serial;
import java.io.Serializable;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
public abstract class User implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String hashedPassword;
private int failedLoginAttempts;
private boolean revoked;
private X509Certificate certificate;
public User(){
//generate key
}
public PublicKey getPublicKey() {
return certificate.getPublicKey();
}
public void setCertificate(X509Certificate certificate) {
this.certificate = certificate;
}
public X509Certificate getCertificate() {
return certificate;
}
public void setHashedPassword(String hashedPassword) {
this.hashedPassword = hashedPassword;
}
//keypair i digitalni cert (da li za sve korisnike il samo za glasaca??? kontam da bi trebalo za sve)
}

View File

@ -0,0 +1,196 @@
package dev.ksan.Users;
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 UserRepository {
private static final Path USER_DIR = Path.of("data/users");
private static final Path ORGANIZER_DIR = USER_DIR.resolve("organizers");
private static final Path VOTER_DIR = USER_DIR.resolve("voters");
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(User user) throws Exception {
String key = resolveKey(user);
Path path = resolveUserPath(user);
Files.createDirectories(path.getParent());
ReentrantReadWriteLock.WriteLock writeLock = getLock(key).writeLock();
writeLock.lock();
try {
writeToFile(user, path);
} finally {
writeLock.unlock();
}
}
public static Organizer findOrganizer(String identificationNumber) throws Exception {
Path path = ORGANIZER_DIR.resolve(identificationNumber + ".ser");
ReentrantReadWriteLock.ReadLock readLock = getLock("organizer:" + identificationNumber).readLock();
readLock.lock();
try {
return (Organizer) deserialize(path);
} finally {
readLock.unlock();
}
}
public static Voter findVoter(String username) throws Exception {
Path path = VOTER_DIR.resolve(username + ".ser");
ReentrantReadWriteLock.ReadLock readLock = getLock("voter:" + username).readLock();
readLock.lock();
try {
return (Voter) deserialize(path);
} finally {
readLock.unlock();
}
}
public static List<Organizer> findAllOrganizers() throws Exception {
return findAll(ORGANIZER_DIR, Organizer.class, "organizer:");
}
public static List<Voter> findAllVoters() throws Exception {
return findAll(VOTER_DIR, Voter.class, "voter:");
}
public static boolean organizerExists(String identificationNumber) {
ReentrantReadWriteLock.ReadLock readLock = getLock("organizer:" + identificationNumber).readLock();
readLock.lock();
try {
return Files.exists(ORGANIZER_DIR.resolve(identificationNumber + ".ser"));
} finally {
readLock.unlock();
}
}
public static boolean voterExists(String username) {
ReentrantReadWriteLock.ReadLock readLock = getLock("voter:" + username).readLock();
readLock.lock();
try {
return Files.exists(VOTER_DIR.resolve(username + ".ser"));
} finally {
readLock.unlock();
}
}
public static void update(User user) throws Exception {
String key = resolveKey(user);
Path path = resolveUserPath(user);
ReentrantReadWriteLock.WriteLock writeLock = getLock(key).writeLock();
writeLock.lock();
try {
if (!Files.exists(path)) {
throw new IllegalStateException("Cannot update non-existent user: " + key);
}
writeToFile(user, path);
} finally {
writeLock.unlock();
}
}
public static void delete(User user) throws Exception {
String key = resolveKey(user);
Path path = resolveUserPath(user);
ReentrantReadWriteLock.WriteLock writeLock = getLock(key).writeLock();
writeLock.lock();
try {
Files.deleteIfExists(path);
locks.remove(key); // clean up lock entry
} finally {
writeLock.unlock();
}
}
private static void writeToFile(User user, 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(user);
oos.flush();
// fileLock released automatically on close
}
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); // shared read lock
ObjectInputStream ois = new ObjectInputStream(fis)) {
if (fileLock == null) {
throw new IllegalStateException("Could not acquire read lock on: " + path);
}
return ois.readObject();
}
}
private static <T extends User> List<T> findAll(
Path dir, Class<T> type, String lockPrefix) throws Exception {
if (!Files.exists(dir)) return Collections.emptyList();
List<T> results = new ArrayList<>();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.ser")) {
for (Path path : stream) {
// Extract key from filename
String filename = path.getFileName().toString().replace(".ser", "");
String key = lockPrefix + filename;
ReentrantReadWriteLock.ReadLock readLock = getLock(key).readLock();
readLock.lock();
try {
Object obj = deserialize(path);
if (type.isInstance(obj)) {
results.add(type.cast(obj));
}
} finally {
readLock.unlock();
}
}
}
return results;
}
private static Path resolveUserPath(User user) {
if (user instanceof Organizer o) {
return ORGANIZER_DIR.resolve(o.getId() + ".ser");
} else if (user instanceof Voter v) {
return VOTER_DIR.resolve(v.getUsername() + ".ser");
}
throw new IllegalArgumentException("Unknown user type: " + user.getClass().getName());
}
private static String resolveKey(User user) {
if (user instanceof Organizer o) return "organizer:" + o.getId();
if (user instanceof Voter v) return "voter:" + v.getUsername();
throw new IllegalArgumentException("Unknown user type: " + user.getClass().getName());
}
}

View File

@ -1,5 +1,29 @@
package dev.ksan.Users;
import java.io.Serial;
public class Voter extends User {
String name;
@Serial
private static final long serialVersionUID = 1L;
private String name;
private String username;
private String password;
public Voter(String name, String username, String password){
super();
this.name = name;
this.username = username;
this.password = password;
}
public String getName(){
return name;
}
public String getUsername(){
return username;
}
public String getPassword(){
return password;
}
}