diff --git a/.idea/misc.xml b/.idea/misc.xml index 8fb11b1..8852426 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -4,7 +4,7 @@ - + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 5f462a1..6fc6099 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 { diff --git a/src/main/java/dev/ksan/CASystem/CA.java b/src/main/java/dev/ksan/CASystem/CA.java new file mode 100644 index 0000000..ac79568 --- /dev/null +++ b/src/main/java/dev/ksan/CASystem/CA.java @@ -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)); + } + + +} diff --git a/src/main/java/dev/ksan/CASystem/CAResult.java b/src/main/java/dev/ksan/CASystem/CAResult.java new file mode 100644 index 0000000..3b831b8 --- /dev/null +++ b/src/main/java/dev/ksan/CASystem/CAResult.java @@ -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; + } +} diff --git a/src/main/java/dev/ksan/CASystem/CAType.java b/src/main/java/dev/ksan/CASystem/CAType.java new file mode 100644 index 0000000..c26d03f --- /dev/null +++ b/src/main/java/dev/ksan/CASystem/CAType.java @@ -0,0 +1,6 @@ +package dev.ksan.CASystem; + +public enum CAType { + ORGANIZER, + VOTER +} diff --git a/src/main/java/dev/ksan/CASystem/KeyService.java b/src/main/java/dev/ksan/CASystem/KeyService.java index e2f9196..5ab3b48 100644 --- a/src/main/java/dev/ksan/CASystem/KeyService.java +++ b/src/main/java/dev/ksan/CASystem/KeyService.java @@ -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()); + } + } + } diff --git a/src/main/java/dev/ksan/CASystem/KeystoreService.java b/src/main/java/dev/ksan/CASystem/KeystoreService.java new file mode 100644 index 0000000..1b7e474 --- /dev/null +++ b/src/main/java/dev/ksan/CASystem/KeystoreService.java @@ -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; + } +} diff --git a/src/main/java/dev/ksan/CASystem/RegistrationService.java b/src/main/java/dev/ksan/CASystem/RegistrationService.java new file mode 100644 index 0000000..9be0708 --- /dev/null +++ b/src/main/java/dev/ksan/CASystem/RegistrationService.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/dev/ksan/CASystem/RootCA.java b/src/main/java/dev/ksan/CASystem/RootCA.java deleted file mode 100644 index 8d43bd3..0000000 --- a/src/main/java/dev/ksan/CASystem/RootCA.java +++ /dev/null @@ -1,4 +0,0 @@ -package dev.ksan.CASystem; - -public class RootCA { -} diff --git a/src/main/java/dev/ksan/CASystem/TruststoreService.java b/src/main/java/dev/ksan/CASystem/TruststoreService.java new file mode 100644 index 0000000..6db9d9a --- /dev/null +++ b/src/main/java/dev/ksan/CASystem/TruststoreService.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/dev/ksan/Main.java b/src/main/java/dev/ksan/Main.java index 4ba2ba6..2f1ddeb 100644 --- a/src/main/java/dev/ksan/Main.java +++ b/src/main/java/dev/ksan/Main.java @@ -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())); + } } diff --git a/src/main/java/dev/ksan/Users/Organizer.java b/src/main/java/dev/ksan/Users/Organizer.java index 134f5e7..69f87a4 100644 --- a/src/main/java/dev/ksan/Users/Organizer.java +++ b/src/main/java/dev/ksan/Users/Organizer.java @@ -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(); + } } diff --git a/src/main/java/dev/ksan/Users/User.java b/src/main/java/dev/ksan/Users/User.java index 344c5eb..99c31a5 100644 --- a/src/main/java/dev/ksan/Users/User.java +++ b/src/main/java/dev/ksan/Users/User.java @@ -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) } diff --git a/src/main/java/dev/ksan/Users/UserRepository.java b/src/main/java/dev/ksan/Users/UserRepository.java new file mode 100644 index 0000000..70f14c2 --- /dev/null +++ b/src/main/java/dev/ksan/Users/UserRepository.java @@ -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 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 findAllOrganizers() throws Exception { + return findAll(ORGANIZER_DIR, Organizer.class, "organizer:"); + } + + public static List 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 List findAll( + Path dir, Class type, String lockPrefix) throws Exception { + + if (!Files.exists(dir)) return Collections.emptyList(); + + List results = new ArrayList<>(); + try (DirectoryStream 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()); + } +} \ No newline at end of file diff --git a/src/main/java/dev/ksan/Users/Voter.java b/src/main/java/dev/ksan/Users/Voter.java index 69892fe..89560b6 100644 --- a/src/main/java/dev/ksan/Users/Voter.java +++ b/src/main/java/dev/ksan/Users/Voter.java @@ -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; + } }