From 22711c30f708901c83de203b33e79a16e75c8985 Mon Sep 17 00:00:00 2001 From: Ksan Date: Fri, 27 Mar 2026 17:38:17 +0100 Subject: [PATCH] added registration --- .gitignore | 7 +- .idea/misc.xml | 2 +- build.gradle.kts | 3 +- plan | 9 ++ src/main/java/dev/ksan/CASystem/CA.java | 5 +- .../java/dev/ksan/CASystem/CAInitializer.java | 65 ++++++++++ .../dev/ksan/CASystem/KeystoreService.java | 28 ++++- .../ksan/CASystem/RegistrationService.java | 47 ++++---- .../dev/ksan/CASystem/TruststoreService.java | 1 + src/main/java/dev/ksan/ConsoleApp.java | 26 ++++ src/main/java/dev/ksan/Main.java | 71 ++++++----- src/main/java/dev/ksan/RegistrationUI.java | 114 ++++++++++++++++++ src/main/java/dev/ksan/Users/Organizer.java | 12 +- .../java/dev/ksan/Users/UserRepository.java | 18 ++- src/main/java/dev/ksan/Users/Voter.java | 8 +- src/main/java/dev/ksan/temp/AppConfig.java | 17 +++ 16 files changed, 347 insertions(+), 86 deletions(-) create mode 100644 plan create mode 100644 src/main/java/dev/ksan/CASystem/CAInitializer.java create mode 100644 src/main/java/dev/ksan/ConsoleApp.java create mode 100644 src/main/java/dev/ksan/RegistrationUI.java create mode 100644 src/main/java/dev/ksan/temp/AppConfig.java diff --git a/.gitignore b/.gitignore index 1fac4d5..ce2526f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,9 @@ + +keystores/ +data/ + .gradle + build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ @@ -40,4 +45,4 @@ bin/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store diff --git a/.idea/misc.xml b/.idea/misc.xml index 8852426..1bd18cd 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 6fc6099..2b42cc5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,7 +17,8 @@ dependencies { 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") -} + + implementation("commons-logging:commons-logging:1.2")} tasks.test { useJUnitPlatform() diff --git a/plan b/plan new file mode 100644 index 0000000..d93bf09 --- /dev/null +++ b/plan @@ -0,0 +1,9 @@ +gui +registration screen user type choice, and then needed fields, after registration user gets to download/save his certificate +login screen drag/select file location and then unlock the p12 cert, then login screen. + +organizer screen - panel with all current polls, section bellow it for finished polls sorted by latest, above all that , user can select the previus polls to see vote graphs + +voter screen - only can see ongoing polls and after selection once he gets a radio button select, then after voting he gets notification? on success?? + + diff --git a/src/main/java/dev/ksan/CASystem/CA.java b/src/main/java/dev/ksan/CASystem/CA.java index ac79568..5e9185c 100644 --- a/src/main/java/dev/ksan/CASystem/CA.java +++ b/src/main/java/dev/ksan/CASystem/CA.java @@ -7,6 +7,7 @@ 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.JcaX509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; @@ -73,7 +74,7 @@ public class CA { KeyPair keyPair = KeyService.generateRSAKeyPair(); - X500Name issuer = new X500Name(issuerCert.getSubjectX500Principal().getName()); + X500Name issuer = new JcaX509CertificateHolder(issuerCert).getSubject(); X500Name subject = new X500Name("CN=" + type + ", O=CA, OU=VotingSystem, L=BL, ST=BA"); BigInteger serial = BigInteger.valueOf(System.currentTimeMillis()); @@ -175,7 +176,7 @@ public class CA { PublicKey userKey ) throws Exception { - X500Name issuer = new X500Name(caCert.getSubjectX500Principal().getName()); + X500Name issuer = new JcaX509CertificateHolder(caCert).getSubject(); X500Name subject = new X500Name("CN=" + username); BigInteger serial = new BigInteger(64, new SecureRandom()); diff --git a/src/main/java/dev/ksan/CASystem/CAInitializer.java b/src/main/java/dev/ksan/CASystem/CAInitializer.java new file mode 100644 index 0000000..18ce893 --- /dev/null +++ b/src/main/java/dev/ksan/CASystem/CAInitializer.java @@ -0,0 +1,65 @@ +package dev.ksan.CASystem; + +import dev.ksan.temp.AppConfig; + +import java.nio.file.Files; +import java.nio.file.Path; + +public class CAInitializer { + + private static final Path ROOT_CA_PATH = Path.of("keystores/ca/root-ca.p12"); + private static final char[] CA_PASSWORD = AppConfig.getCaKeystorePassword(); + private static final char[] TRUSTSTORE_PASSWORD = AppConfig.getTruststorePassword(); + + public static void initializeIfNeeded() { + if (Files.exists(ROOT_CA_PATH)) { + System.out.println("CA hierarchy already initialized."); + return; + } + + try { + System.out.println("Initializing CA hierarchy for the first time..."); + + // 1. Root CA + CAResult rootCA = CA.generateRootCA(); + + // 2. Sub-CAs signed by Root + CAResult organizerCA = CA.generateSubCA( + CAType.ORGANIZER, + rootCA.getCertificate(), + rootCA.getKeyPair().getPrivate() + ); + CAResult voterCA = CA.generateSubCA( + CAType.VOTER, + rootCA.getCertificate(), + rootCA.getKeyPair().getPrivate() + ); + + // 3. Save CA keystores (private keys protected) + KeystoreService.saveCAKeyStore("root-ca", + rootCA.getKeyPair().getPrivate(), rootCA.getCertificate(), null, CA_PASSWORD); + + KeystoreService.saveCAKeyStore("organizer-ca", + organizerCA.getKeyPair().getPrivate(), organizerCA.getCertificate(), + rootCA.getCertificate(), CA_PASSWORD); + + KeystoreService.saveCAKeyStore("voter-ca", + voterCA.getKeyPair().getPrivate(), voterCA.getCertificate(), + rootCA.getCertificate(), CA_PASSWORD); + + // 4. Save truststore (public certs only) + TruststoreService.createTruststore( + rootCA.getCertificate(), + organizerCA.getCertificate(), + voterCA.getCertificate(), + TRUSTSTORE_PASSWORD + ); + + System.out.println("CA hierarchy initialized successfully."); + + } catch (Exception e) { + System.err.println("FATAL: Could not initialize CA hierarchy: " + e.getMessage()); + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/ksan/CASystem/KeystoreService.java b/src/main/java/dev/ksan/CASystem/KeystoreService.java index 1b7e474..5fcc2d2 100644 --- a/src/main/java/dev/ksan/CASystem/KeystoreService.java +++ b/src/main/java/dev/ksan/CASystem/KeystoreService.java @@ -24,8 +24,8 @@ public class KeystoreService { public static void saveUserKeystore(String username, PrivateKey privateKey, - X509Certificate caCert, X509Certificate userCert, + X509Certificate caCert , char[] keystorePassword) throws Exception { KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE); @@ -61,7 +61,29 @@ public class KeystoreService { PrivateKey privateKey, X509Certificate caCert, X509Certificate issuerCert, - char[] keystorePassword) throws Exception {} + char[] keystorePassword) throws Exception { + + + KeyStore ks = KeyStore.getInstance(KEYSTORE_TYPE); + ks.load(null, null); + + Certificate[] chain = issuerCert != null ? + new Certificate[]{caCert, issuerCert} : + new Certificate[]{caCert}; + + ks.setKeyEntry( KEY_ALIAS, privateKey, keystorePassword, chain); + + Path path = CA_KEYSTORE_DIR.resolve(caName + ".p12"); + Files.createDirectories(path.getParent()); + + try(FileOutputStream fos = new FileOutputStream(path.toFile())) { + ks.store(fos, keystorePassword); + } + + + + + } private static KeyStore loadKeyStore(Path path, char[] password) throws Exception { KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE); @@ -89,8 +111,6 @@ public class KeystoreService { return Base64.getEncoder().encodeToString(hash).toCharArray(); - - } diff --git a/src/main/java/dev/ksan/CASystem/RegistrationService.java b/src/main/java/dev/ksan/CASystem/RegistrationService.java index 9be0708..739e9be 100644 --- a/src/main/java/dev/ksan/CASystem/RegistrationService.java +++ b/src/main/java/dev/ksan/CASystem/RegistrationService.java @@ -4,6 +4,7 @@ import dev.ksan.Users.Organizer; import dev.ksan.Users.User; import dev.ksan.Users.UserRepository; import dev.ksan.Users.Voter; +import dev.ksan.temp.AppConfig; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import java.security.KeyPair; import java.security.PrivateKey; @@ -13,10 +14,8 @@ 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(); + private static final char[] CA_KEYSTORE_PASSWORD = AppConfig.getCaKeystorePassword(); + private static final char[] TRUSTSTORE_PASSWORD = AppConfig.getTruststorePassword(); public static void registerOrganizer(Organizer organizer, String rawPassword) throws Exception { @@ -40,27 +39,33 @@ public class RegistrationService { organizer.setCertificate(cert); - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12); - String hashedPassword = encoder.encode(rawPassword); + try { - organizer.setHashedPassword(hashedPassword); - char[] ksPassword = KeystoreService.deriveKeystorePassword( - rawPassword, - organizer.getId().toString() - ); + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12); + String hashedPassword = encoder.encode(rawPassword); - KeystoreService.saveUserKeystore( - organizer.getId().toString(), - keyPair.getPrivate(), - cert, - organizerCACert, - ksPassword - ); - if (UserRepository.voterExists(organizer.getId().toString())) { - throw new IllegalStateException("Id already taken: " + organizer.getId()); + 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); + } catch (Exception e) { + e.printStackTrace(); } - UserRepository.save(organizer); } public static void registerVoter(Voter voter, String rawPassword) diff --git a/src/main/java/dev/ksan/CASystem/TruststoreService.java b/src/main/java/dev/ksan/CASystem/TruststoreService.java index 6db9d9a..9fdfeec 100644 --- a/src/main/java/dev/ksan/CASystem/TruststoreService.java +++ b/src/main/java/dev/ksan/CASystem/TruststoreService.java @@ -28,6 +28,7 @@ public class TruststoreService { ts.setCertificateEntry("voter-ca", voterCACert); Files.createDirectories(TRUSTSTORE_PATH.getParent()); + try (OutputStream os = Files.newOutputStream(TRUSTSTORE_PATH)) { ts.store(os, truststorePassword); } diff --git a/src/main/java/dev/ksan/ConsoleApp.java b/src/main/java/dev/ksan/ConsoleApp.java new file mode 100644 index 0000000..f6c3713 --- /dev/null +++ b/src/main/java/dev/ksan/ConsoleApp.java @@ -0,0 +1,26 @@ +package dev.ksan; + +import java.util.Scanner; + +// TODO temp class for testing functionality + +public class ConsoleApp { + + private final Scanner scanner = new Scanner(System.in); + + public void start() { + while (true) { + System.out.println("\n=== E-Voting System ==="); + System.out.println("1. Register"); + System.out.println("0. Exit"); + System.out.print("Choose: "); + + switch (scanner.nextLine().trim()) { + case "1" -> new RegistrationUI(scanner).show(); + case "2" -> System.out.println("Login not yet implemented"); + case "0" -> { System.out.println("Goodbye."); return; } + default -> System.out.println("Invalid option."); + } + } + } +} \ 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 2f1ddeb..32739de 100644 --- a/src/main/java/dev/ksan/Main.java +++ b/src/main/java/dev/ksan/Main.java @@ -1,9 +1,6 @@ package dev.ksan; -import dev.ksan.CASystem.CA; -import dev.ksan.CASystem.CAResult; -import dev.ksan.CASystem.CAType; -import dev.ksan.CASystem.KeyService; +import dev.ksan.CASystem.*; import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.KeyPair; @@ -14,40 +11,48 @@ import static dev.ksan.CASystem.CA.publicKeyToPem; public class Main { + static{ + Security.addProvider(new BouncyCastleProvider()); + } public static void main(String args[]) throws Exception { - Security.addProvider(new BouncyCastleProvider()); - System.out.println("added bouncycastle provider"); - KeyPair key =KeyService.generateRSAKeyPair(); - System.out.println(key.getPrivate()); - System.out.println(key.getPublic()); +// System.out.println("added bouncycastle provider"); +// KeyPair key =KeyService.generateRSAKeyPair(); +// System.out.println(key.getPrivate()); +// System.out.println(key.getPublic()); +// +// +// +// 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())); + CAInitializer.initializeIfNeeded(); - 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())); + ConsoleApp app = new ConsoleApp(); + app.start(); } } diff --git a/src/main/java/dev/ksan/RegistrationUI.java b/src/main/java/dev/ksan/RegistrationUI.java new file mode 100644 index 0000000..faf557b --- /dev/null +++ b/src/main/java/dev/ksan/RegistrationUI.java @@ -0,0 +1,114 @@ +package dev.ksan; + +import dev.ksan.CASystem.RegistrationService; +import dev.ksan.Users.Organizer; +import dev.ksan.Users.UserRepository; +import dev.ksan.Users.Voter; + +import java.util.Scanner; +import java.util.UUID; + +public class RegistrationUI { + + private final Scanner scanner; + + public RegistrationUI(Scanner scanner) { + this.scanner = scanner; + } + + public void show() { + System.out.println("\n-- Register --"); + System.out.println("1. Register as Organizer"); + System.out.println("2. Register as Voter"); + System.out.print("Choose: "); + + switch (scanner.nextLine().trim()) { + case "1" -> registerOrganizer(); + case "2" -> registerVoter(); + default -> System.out.println("Invalid option."); + } + } + + private void registerOrganizer() { + try { + System.out.print("Organization name: "); + String orgName = scanner.nextLine().trim(); + + System.out.print("Identification number: "); + String idNumber = scanner.nextLine().trim(); + + if (UserRepository.organizerExists(idNumber)) { + System.out.println("An organizer with that ID already exists."); + return; + } + + String password = readAndConfirmPassword(); + if (password == null) return; // passwords didn't match + + Organizer organizer = new Organizer(orgName, (idNumber)); + + System.out.println("Registering, please wait..."); + RegistrationService.registerOrganizer(organizer, password); + + System.out.println("Organizer registered successfully."); + System.out.println("Your keystore is saved at: keystores/users/" + idNumber + ".p12"); + + } catch (Exception e) { + System.out.println("Registration failed: " + e.getMessage()); + e.printStackTrace(); + } + } + + private void registerVoter() { + try { + System.out.print("First name: "); + String firstName = scanner.nextLine().trim(); + + System.out.print("Last name: "); + String lastName = scanner.nextLine().trim(); + + System.out.print("Username: "); + String username = scanner.nextLine().trim(); + + if (UserRepository.voterExists(username)) { + System.out.println("That username is already taken."); + return; + } + + String password = readAndConfirmPassword(); + if (password == null) return; + + Voter voter = new Voter(firstName, lastName, username); + + System.out.println("Registering, please wait..."); + RegistrationService.registerVoter(voter, password); + + System.out.println("Voter registered successfully."); + System.out.println("Your keystore is saved at: keystores/users/" + username + ".p12"); + + } catch (Exception e) { + System.out.println("Registration failed: " + e.getMessage()); + } + } + + // Ask for password twice to catch typos + private String readAndConfirmPassword() { + System.out.print("Password: "); + String password = scanner.nextLine(); + + System.out.print("Confirm password: "); + String confirm = scanner.nextLine(); + + if (!password.equals(confirm)) { + System.out.println("Passwords do not match."); + return null; + } + + if (password.length() < 3) { + System.out.println("Password must be at least 8 characters."); + return null; + } + + return password; + } +} \ No newline at end of file diff --git a/src/main/java/dev/ksan/Users/Organizer.java b/src/main/java/dev/ksan/Users/Organizer.java index 69f87a4..b2d7061 100644 --- a/src/main/java/dev/ksan/Users/Organizer.java +++ b/src/main/java/dev/ksan/Users/Organizer.java @@ -13,14 +13,12 @@ public class Organizer extends User { @Serial private static final long serialVersionUID = 1L; private String name; - private UUID id; - private String password; + private String id; - public Organizer(String name, UUID id, String password) { + public Organizer(String name, String id) { super(); this.name = name; this.id = id; - this.password = password; } @@ -28,13 +26,9 @@ public class Organizer extends User { return name; } - public UUID getId() { + public String getId() { return id; } - public String getPassword() { - return password; - } - public String getOrganizationName() { return getName(); diff --git a/src/main/java/dev/ksan/Users/UserRepository.java b/src/main/java/dev/ksan/Users/UserRepository.java index 70f14c2..57a4e69 100644 --- a/src/main/java/dev/ksan/Users/UserRepository.java +++ b/src/main/java/dev/ksan/Users/UserRepository.java @@ -4,11 +4,10 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; 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.nio.file.*; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -125,16 +124,15 @@ public class UserRepository { 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)) { - + // Open a temporary file and write the object + try (ObjectOutputStream oos = new ObjectOutputStream( + Files.newOutputStream(tmpPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))) { oos.writeObject(user); oos.flush(); - // fileLock released automatically on close } - Files.move(tmpPath, targetPath, StandardCopyOption.ATOMIC_MOVE, + Files.move(tmpPath, targetPath, + StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); } diff --git a/src/main/java/dev/ksan/Users/Voter.java b/src/main/java/dev/ksan/Users/Voter.java index 89560b6..6b60be1 100644 --- a/src/main/java/dev/ksan/Users/Voter.java +++ b/src/main/java/dev/ksan/Users/Voter.java @@ -6,14 +6,14 @@ public class Voter extends User { @Serial private static final long serialVersionUID = 1L; private String name; + private String lastName; private String username; - private String password; - public Voter(String name, String username, String password){ + public Voter(String name, String username, String lastName){ super(); this.name = name; this.username = username; - this.password = password; + this.lastName = lastName; } public String getName(){ @@ -24,6 +24,6 @@ public class Voter extends User { } public String getPassword(){ - return password; + return lastName; } } diff --git a/src/main/java/dev/ksan/temp/AppConfig.java b/src/main/java/dev/ksan/temp/AppConfig.java new file mode 100644 index 0000000..a233c98 --- /dev/null +++ b/src/main/java/dev/ksan/temp/AppConfig.java @@ -0,0 +1,17 @@ +package dev.ksan.temp; + +import java.util.Arrays; + +public class AppConfig { + + private static final char[] CA_KEYSTORE_PASSWORD = "ca-keystore-password-123".toCharArray(); + private static final char[] TRUSTSTORE_PASSWORD = "truststore-password-123".toCharArray(); + + public static char[] getCaKeystorePassword() { + return Arrays.copyOf(CA_KEYSTORE_PASSWORD, CA_KEYSTORE_PASSWORD.length); + } + + public static char[] getTruststorePassword() { + return Arrays.copyOf(TRUSTSTORE_PASSWORD, TRUSTSTORE_PASSWORD.length); + } +} \ No newline at end of file