diff --git a/src/main/java/dev/ksan/CASystem/CAInitializer.java b/src/main/java/dev/ksan/CASystem/CAInitializer.java index 18ce893..6d66302 100644 --- a/src/main/java/dev/ksan/CASystem/CAInitializer.java +++ b/src/main/java/dev/ksan/CASystem/CAInitializer.java @@ -11,8 +11,9 @@ public class CAInitializer { private static final char[] CA_PASSWORD = AppConfig.getCaKeystorePassword(); private static final char[] TRUSTSTORE_PASSWORD = AppConfig.getTruststorePassword(); - public static void initializeIfNeeded() { + public static void initializeIfNeeded() throws Exception { if (Files.exists(ROOT_CA_PATH)) { + CRLService.initializeIfNeeded(); System.out.println("CA hierarchy already initialized."); return; } @@ -20,10 +21,8 @@ public class CAInitializer { 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(), @@ -35,7 +34,6 @@ public class CAInitializer { rootCA.getKeyPair().getPrivate() ); - // 3. Save CA keystores (private keys protected) KeystoreService.saveCAKeyStore("root-ca", rootCA.getKeyPair().getPrivate(), rootCA.getCertificate(), null, CA_PASSWORD); @@ -47,7 +45,6 @@ public class CAInitializer { voterCA.getKeyPair().getPrivate(), voterCA.getCertificate(), rootCA.getCertificate(), CA_PASSWORD); - // 4. Save truststore (public certs only) TruststoreService.createTruststore( rootCA.getCertificate(), organizerCA.getCertificate(), @@ -55,6 +52,8 @@ public class CAInitializer { TRUSTSTORE_PASSWORD ); + CRLService.initializeIfNeeded(); + System.out.println("CA hierarchy initialized successfully."); } catch (Exception e) { diff --git a/src/main/java/dev/ksan/CASystem/CRLService.java b/src/main/java/dev/ksan/CASystem/CRLService.java new file mode 100644 index 0000000..4be6efb --- /dev/null +++ b/src/main/java/dev/ksan/CASystem/CRLService.java @@ -0,0 +1,140 @@ +package dev.ksan.CASystem; + +import dev.ksan.temp.AppConfig; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.CRLReason; +import org.bouncycastle.cert.X509CRLEntryHolder; +import org.bouncycastle.cert.X509CRLHolder; +import org.bouncycastle.cert.X509v2CRLBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; + +public class CRLService { + + private static final Path CRL_DIR = Path.of("data/crl"); + private static final String ORGANIZER_CRL = "organizer-ca.crl"; + private static final String VOTER_CRL = "voter-ca.crl"; + + public static void initializeIfNeeded() throws Exception { + Files.createDirectories(CRL_DIR); + + if (!Files.exists(CRL_DIR.resolve(ORGANIZER_CRL))) { + createEmptyCRL(CAType.ORGANIZER); + System.out.println("Organizer CRL initialized."); + } + + if (!Files.exists(CRL_DIR.resolve(VOTER_CRL))) { + createEmptyCRL(CAType.VOTER); + System.out.println("Voter CRL initialized."); + } + } + + public static void revokeCertificate( + X509Certificate certToRevoke, + CAType caType + ) throws Exception { + + PrivateKey caKey = KeystoreService.loadCAPrivateKey( + caType == CAType.ORGANIZER ? "organizer-ca" : "voter-ca", + AppConfig.getCaKeystorePassword() + ); + + X509Certificate caCert = TruststoreService.getTrustedCert( + caType == CAType.ORGANIZER ? "organizer-ca" : "voter-ca", + AppConfig.getTruststorePassword() + ); + + List revokedSerials = loadRevokedSerials(caType); + + revokedSerials.add(certToRevoke.getSerialNumber()); + + buildAndSaveCRL(revokedSerials, caCert, caKey, caType); + + System.out.println("Certificate revoked. Serial: " + certToRevoke.getSerialNumber()); + } + + public static boolean isRevoked(X509Certificate cert, CAType caType) { + try { + List revokedSerials = loadRevokedSerials(caType); + return revokedSerials.contains(cert.getSerialNumber()); + } catch (Exception e) { + return true; + } + } + + private static void createEmptyCRL(CAType caType) throws Exception { + PrivateKey caKey = KeystoreService.loadCAPrivateKey( + caType == CAType.ORGANIZER ? "organizer-ca" : "voter-ca", + AppConfig.getCaKeystorePassword() + ); + X509Certificate caCert = TruststoreService.getTrustedCert( + caType == CAType.ORGANIZER ? "organizer-ca" : "voter-ca", + AppConfig.getTruststorePassword() + ); + buildAndSaveCRL(new ArrayList<>(), caCert, caKey, caType); + } + + private static void buildAndSaveCRL( + List revokedSerials, + X509Certificate caCert, + PrivateKey caKey, + CAType caType + ) throws Exception { + + X500Name issuerName = new JcaX509CertificateHolder(caCert).getSubject(); + + Date now = new Date(); + Date nextUpdate = new Date(now.getTime() + 24L * 60 * 60 * 1000); // 24h + + X509v2CRLBuilder crlBuilder = new X509v2CRLBuilder(issuerName, now); + crlBuilder.setNextUpdate(nextUpdate); + + for (BigInteger serial : revokedSerials) { + crlBuilder.addCRLEntry(serial, now, CRLReason.unspecified); + } + + ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA") + .setProvider("BC").build(caKey); + + X509CRLHolder crlHolder = crlBuilder.build(signer); + + Path crlPath = CRL_DIR.resolve( + caType == CAType.ORGANIZER ? ORGANIZER_CRL : VOTER_CRL); + + Files.write(crlPath, crlHolder.getEncoded()); + } + + + private static List loadRevokedSerials(CAType caType) throws Exception { + Path crlPath = CRL_DIR.resolve( + caType == CAType.ORGANIZER ? ORGANIZER_CRL : VOTER_CRL); + + if (!Files.exists(crlPath)) return new ArrayList<>(); + + byte[] crlBytes = Files.readAllBytes(crlPath); + X509CRLHolder crlHolder = new X509CRLHolder(crlBytes); + + List serials = new ArrayList<>(); + + Collection revoked = crlHolder.getRevokedCertificates(); + if (revoked != null) { + for (Object obj : revoked) { + X509CRLEntryHolder entry = (X509CRLEntryHolder) obj; + serials.add(entry.getSerialNumber()); + } + } + + return serials; + } +} \ No newline at end of file diff --git a/src/main/java/dev/ksan/CASystem/LoginService.java b/src/main/java/dev/ksan/CASystem/LoginService.java new file mode 100644 index 0000000..0455ce3 --- /dev/null +++ b/src/main/java/dev/ksan/CASystem/LoginService.java @@ -0,0 +1,137 @@ +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 dev.ksan.temp.AppConfig; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; +import org.springframework.security.crypto.bcrypt.BCrypt; + +import java.security.cert.X509Certificate; + +public class LoginService { + + private static User currentUser = null; + + public static CAType validateCertificate(X509Certificate submittedCert) throws Exception { + + // try organizer CA first, then voter CA + if (TruststoreService.validateUserCertificate( + submittedCert, CAType.ORGANIZER, AppConfig.getTruststorePassword())) { + + if (CRLService.isRevoked(submittedCert, CAType.ORGANIZER)) + throw new SecurityException("Certificate has been revoked."); + + return CAType.ORGANIZER; + } + + if (TruststoreService.validateUserCertificate( + submittedCert, CAType.VOTER, AppConfig.getTruststorePassword())) { + + if (CRLService.isRevoked(submittedCert, CAType.VOTER)) + throw new SecurityException("Certificate has been revoked."); + + return CAType.VOTER; + } + + throw new SecurityException("Certificate is not valid or not issued by this system."); + } + + public static User validateCredentials( + String usernameOrId, + String rawPassword, + X509Certificate submittedCert, + CAType caType + ) throws Exception { + + User user = loadUser(usernameOrId, caType); + + if (user == null) + throw new SecurityException("User not found."); + + if (user.isRevoked()) + throw new SecurityException("Account is revoked."); + + if (!certBelongsToUser(submittedCert, user)) { + handleFailedAttempt(user, caType); + throw new SecurityException("Certificate does not belong to this user."); + } + + if (!BCrypt.checkpw(rawPassword, user.getHashedPassword())) { + handleFailedAttempt(user, caType); + throw new SecurityException("Invalid password. Attempts remaining: " + + (3 - user.getFailedLoginAttempts())); + } + + user.setFailedLoginAttempts(0); + UserRepository.update(user); + + currentUser = user; + return user; + } + + //TODO + public static void logout() { + System.out.println("Goodbye, " + getDisplayName(currentUser)); + currentUser = null; + } + + public static User getCurrentUser() { return currentUser; } + + public static boolean isLoggedIn() { return currentUser != null; } + + private static boolean certBelongsToUser(X509Certificate cert, User user) throws Exception { + + X500Name subject = new JcaX509CertificateHolder(cert).getSubject(); + String cn = subject.getRDNs(BCStyle.CN)[0] + .getFirst().getValue().toString(); + + if (user instanceof Organizer organizer) { + + String expectedCN = organizer.getOrganizationName() + + "-" + organizer.getId(); + return cn.equals(expectedCN); + } + + if (user instanceof Voter voter) { + + return cn.equals(voter.getUsername()); + } + + return false; + } + + private static void handleFailedAttempt(User user, CAType caType) throws Exception { + + user.setFailedLoginAttempts(user.getFailedLoginAttempts() + 1); + + if (user.getFailedLoginAttempts() >= 3) { + + user.setRevoked(true); + CRLService.revokeCertificate(user.getCertificate(), caType); + UserRepository.update(user); + + throw new SecurityException( + "Too many failed attempts. Account and certificate have been revoked."); + + } + + UserRepository.update(user); + } + + private static User loadUser(String usernameOrId, CAType caType) throws Exception { + return switch (caType) { + case ORGANIZER -> UserRepository.findOrganizer(usernameOrId); + case VOTER -> UserRepository.findVoter(usernameOrId); + }; + } + + private static String getDisplayName(User user) { + if (user instanceof Organizer o) return o.getOrganizationName(); + if (user instanceof Voter v) return v.getUsername(); + return "Unknown"; + } +} \ No newline at end of file diff --git a/src/main/java/dev/ksan/Users/User.java b/src/main/java/dev/ksan/Users/User.java index 99c31a5..1e175f2 100644 --- a/src/main/java/dev/ksan/Users/User.java +++ b/src/main/java/dev/ksan/Users/User.java @@ -10,7 +10,7 @@ public abstract class User implements Serializable { private static final long serialVersionUID = 1L; private String hashedPassword; - private int failedLoginAttempts; + private int failedLoginAttempts = 0; private boolean revoked; private X509Certificate certificate; @@ -36,4 +36,23 @@ public abstract class User implements Serializable { this.hashedPassword = hashedPassword; } + public boolean isRevoked() { + return revoked; + } + + public String getHashedPassword() { + return hashedPassword; + } + + public int getFailedLoginAttempts() { + return failedLoginAttempts; + } + + public void setFailedLoginAttempts(int i) { + this.failedLoginAttempts = i; + } + + public void setRevoked(boolean b) { + this.revoked = b; + } }