added login logic, still needs some work

This commit is contained in:
Ksan 2026-03-30 16:53:20 +02:00
parent 7ba9e4f5e6
commit 80ca3240f1
4 changed files with 301 additions and 6 deletions

View File

@ -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) {

View File

@ -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<BigInteger> 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<BigInteger> 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<BigInteger> 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<BigInteger> 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<BigInteger> serials = new ArrayList<>();
Collection<?> revoked = crlHolder.getRevokedCertificates();
if (revoked != null) {
for (Object obj : revoked) {
X509CRLEntryHolder entry = (X509CRLEntryHolder) obj;
serials.add(entry.getSerialNumber());
}
}
return serials;
}
}

View File

@ -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";
}
}

View File

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