Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,13 @@
<version>3.20.0</version>
<scope>compile</scope>
</dependency>
<!-- Source: https://mvnrepository.com/artifact/commons-codec/commons-codec -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.20.0</version>
<scope>compile</scope>
</dependency>
</dependencies>

<build>
Expand Down
97 changes: 88 additions & 9 deletions src/main/java/org/example/cyberwatch/config/DataInitializer.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package org.example.cyberwatch.config;

import org.example.cyberwatch.config.security.EncryptionService;
import org.example.cyberwatch.features.form.model.EmploymentForm;
import org.example.cyberwatch.features.form.repository.EmploymentFormRepository;
import org.example.cyberwatch.features.staff.model.Staff;
import org.example.cyberwatch.features.staff.repository.StaffRepository;
import org.example.cyberwatch.shared.model.enums.Department;
import org.example.cyberwatch.shared.model.enums.Role;
import org.example.cyberwatch.features.ticket.model.Ticket;
import org.example.cyberwatch.features.ticket.repository.TicketRepository;
import org.example.cyberwatch.shared.model.enums.*;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
Expand All @@ -15,11 +18,16 @@
public class DataInitializer implements CommandLineRunner {

private final StaffRepository staffRepository;
private final EmploymentFormRepository employmentFormRepository;
private final TicketRepository ticketRepository;
private final EncryptionService encryptionService;
private final PasswordEncoder passwordEncoder;

public DataInitializer(StaffRepository staffRepository, EncryptionService encryptionService, PasswordEncoder passwordEncoder) {
public DataInitializer(StaffRepository staffRepository, EmploymentFormRepository employmentFormRepository,
TicketRepository ticketRepository, EncryptionService encryptionService, PasswordEncoder passwordEncoder) {
this.staffRepository = staffRepository;
this.employmentFormRepository = employmentFormRepository;
this.ticketRepository = ticketRepository;
this.encryptionService = encryptionService;
this.passwordEncoder = passwordEncoder;
}
Expand All @@ -30,26 +38,97 @@ public void run(String... args) {
return;
}

// Skapa admin-användare
List<Staff> admins = staffRepository.saveAll(List.of(
createStaff("19900101-0101", "Eric", "Thilen", "ericthilen2003@gmail.com", "0701111111", Role.ADMIN, Department.BACKEND, "testPass123", "ONLINE"),
createStaff("19900505-0505", "Caroline", "Nordbrandt", "nordbrandtcaroline@gmail.com", "0702222222", Role.ADMIN, Department.DEVOPS, "testPass1234", "ONLINE"),
createStaff("19900707-0707", "Alice", "Wersen", "alicewersen@hotmail.com", "0703333333", Role.ADMIN, Department.HR, "testPass12345", "OFFLINE"),
createStaff("19900909-0909", "Younes", "Lamia", "younescool94@gmail.com", "0704444444", Role.ADMIN, Department.FRONTEND, "testPass1234", "OFFLINE")
));

// Fler dummies för att testa filter och scroll
staffRepository.saveAll(List.of(
createStaff("19900101-0101", "Eric", "Thilen", "ericthilen2003@gmail.com", "0701111111", Role.ADMIN, Department.BACKEND, "testPass123"),
createStaff("19900505-0505", "Caroline", "Nordbrandt", "nordbrandtcaroline@gmail.com", "0702222222", Role.ADMIN, Department.DEVOPS, "testPass1234"),
createStaff("19900707-0707", "Alice", "Wersen", "alicewersen@hotmail.com", "0703333333", Role.ADMIN, Department.HR, "testPass12345"),
createStaff("19900909-0909", "Younes", "Lamia", "younescool94@gmail.com", "0704444444", Role.ADMIN, Department.FRONTEND, "testPass1234")
createStaff("19850212-1212", "Johan", "Andersson", "johan.a@cyberwatch.se", "0705555555", Role.CTO, Department.BACKEND, "pass123", "BUSY"),
createStaff("19920314-3412", "Sofia", "Lindgren", "sofia.l@cyberwatch.se", "0706666666", Role.HR, Department.HR, "pass123", "OFFLINE"),
createStaff("19881120-5678", "Marcus", "Ek", "marcus.ek@cyberwatch.se", "0707777777", Role.CEO, Department.MANAGEMENT, "pass123", "AWAY"),
createStaff("19950606-9999", "Linnea", "Berg", "linnea.b@cyberwatch.se", "0708888888", Role.CONSULTANT, Department.FRONTEND, "pass123", "OFFLINE"),
createStaff("19910825-4433", "Niklas", "Sjöberg", "niklas.s@cyberwatch.se", "0709999999", Role.CONSULTANT, Department.DEVOPS, "pass123", "AWAY"),
createStaff("19931201-1010", "Elena", "Popova", "elena.p@cyberwatch.se", "0701010101", Role.CONSULTANT, Department.BACKEND, "pass123", "OFFLINE"),
createStaff("19870412-2233", "Mikael", "Vesterberg", "mikael.v@cyberwatch.se", "0721112233", Role.PROJECT_MANAGER, Department.DEVOPS, "testPass123", "BUSY"),
createStaff("19941030-4455", "Sara", "Lundin", "sara.l@cyberwatch.se", "0734445566", Role.CONSULTANT, Department.BACKEND, "testPass123", "BUSY"),
createStaff("19910228-6677", "David", "Holm", "david.h@cyberwatch.se", "0767778899", Role.HR, Department.HR, "testPass123", "ONLINE"),
createStaff("19890515-8899", "Emma", "Sjölin", "emma.s@cyberwatch.se", "0708889900", Role.PROJECT_MANAGER, Department.FRONTEND, "testPass123", "ONLINE"),
createStaff("19960820-1122", "Lucas", "Karlsson", "lucas.k@cyberwatch.se", "0791112233", Role.CONSULTANT, Department.BACKEND, "testPass123", "OFFLINE")
));

// Skapa 3 pending employment forms
Staff hrStaff = admins.getFirst(); // Eric (Admin)
employmentFormRepository.saveAll(List.of(
createEmploymentForm("19800315-2525", "Peter", "Bergström", "peter.bergstrom@cyberwatch.se", "0715151515", Role.CONSULTANT, Department.BACKEND, hrStaff),
createEmploymentForm("19870723-3636", "Victoria", "Ström", "victoria.strom@cyberwatch.se", "0726262626", Role.PROJECT_MANAGER, Department.FRONTEND, hrStaff),
createEmploymentForm("19920411-4747", "Martin", "Nordin", "martin.nordin@cyberwatch.se", "0737373737", Role.CONSULTANT, Department.DEVOPS, hrStaff)
));
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Skapa 10 tickets
List<Staff> allStaff = staffRepository.findAll();
ticketRepository.saveAll(List.of(
createTicket("TICKET-1001", "Fix login bug on mobile", "Login button not responding on iOS 17", Status.IN_PROGRESS, Priority.HIGH, IssueType.SOFTWARE, allStaff.get(4), List.of(allStaff.get(7))),
createTicket("TICKET-1002", "Add dark mode feature", "Implement dark theme for better accessibility", Status.SUBMITTED, Priority.MEDIUM, IssueType.SOFTWARE, allStaff.get(5), List.of(allStaff.get(9))),
createTicket("TICKET-1003", "Database optimization", "Query performance issues in staff list endpoint", Status.IN_PROGRESS, Priority.HIGH, IssueType.SOFTWARE, allStaff.get(6), List.of()),
createTicket("TICKET-1004", "Update API documentation", "Document new endpoints for ticket filtering", Status.SUBMITTED, Priority.LOW, IssueType.OTHER, allStaff.get(7), List.of()),
createTicket("TICKET-1005", "Memory leak in S3 upload", "Application consuming too much memory during large file uploads", Status.RESOLVED, Priority.HIGH, IssueType.SOFTWARE, allStaff.get(8), List.of(allStaff.get(5))),
createTicket("TICKET-1006", "Enhance error messages", "Make error messages more user-friendly", Status.SUBMITTED, Priority.MEDIUM, IssueType.SOFTWARE, allStaff.get(9), List.of(allStaff.get(8))),
createTicket("TICKET-1007", "SSL certificate expiring", "Renew SSL certificate before expiration", Status.IN_PROGRESS, Priority.CRITICAL, IssueType.SECURITY, allStaff.get(4), List.of(allStaff.get(6))),
createTicket("TICKET-1008", "Add password recovery", "Implement forgot password functionality", Status.SUBMITTED, Priority.MEDIUM, IssueType.SOFTWARE, allStaff.get(10), List.of(allStaff.get(7))),
createTicket("TICKET-1009", "Network latency issues", "Reported high latency in VPN connection", Status.CLOSED, Priority.LOW, IssueType.NETWORK, allStaff.get(5), List.of()),
createTicket("TICKET-1010", "Update security headers", "Add missing security headers to HTTP responses", Status.IN_PROGRESS, Priority.HIGH, IssueType.SECURITY, allStaff.get(6), List.of(allStaff.get(4)))
));
}

private Staff createStaff(String ssn, String firstName, String lastName, String email, String phone, Role role, Department department, String rawPassword) {
private Staff createStaff(String ssn, String firstName, String lastName, String email, String phone,
Role role, Department department, String rawPassword, String status) {
Staff staff = new Staff();
staff.setSocialSecurityNumber(encryptionService.encrypt(ssn));
staff.setSsnHash(encryptionService.hmac(ssn));
staff.setFirstName(firstName);
staff.setLastName(lastName);
staff.setEmail(email);
staff.setPhoneNumber(phone);
staff.setRole(role);
staff.setDepartment(department);
staff.setStatus("OFFLINE");
staff.setStatus(status);
staff.setPassword(passwordEncoder.encode(rawPassword));
return staff;
}

private EmploymentForm createEmploymentForm(String ssn, String firstName, String lastName, String email,
String phone, Role role, Department department, Staff createdBy) {
EmploymentForm form = new EmploymentForm();
form.setSocialSecurityNumber(encryptionService.encrypt(ssn));
form.setSsnHash(encryptionService.hmac(ssn));
form.setFirstName(firstName);
form.setLastName(lastName);
form.setEmail(email);
form.setPhoneNumber(phone);
form.setRole(role);
form.setDepartment(department);
form.setStatus(ApprovalStatus.PENDING);
form.setCreatedBy(createdBy);
return form;
}

private Ticket createTicket(String ticketCode, String title, String description, Status status, Priority priority, IssueType issueType,
Staff createdBy, List<Staff> assignedStaff) {
Ticket ticket = new Ticket();
ticket.setTicketCode(ticketCode);
ticket.setTitle(title);
ticket.setDescription(description);
ticket.setStatus(status);
ticket.setPriority(priority);
ticket.setIssueType(issueType);
ticket.setCreatedBy(createdBy);
ticket.setAssignedStaff(assignedStaff);
return ticket;
}
}

58 changes: 53 additions & 5 deletions src/main/java/org/example/cyberwatch/config/EncryptionConfig.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.example.cyberwatch.config;

import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -9,14 +10,61 @@
@Configuration
public class EncryptionConfig {

@Value("${app.encryption.key}")
private String encryptionKey;
@Value("${app.encryption.password}")
private String encryptionPass;

@Value("${app.encryption.salt}")
private String salt;
@Value("${app.encryption.pepper}")
private String pepper;

//Vi implementerade fail-fast validation för alla krypteringsnycklar. Applikationen startar INTE om någon nyckel är tom eller för svag.
@PostConstruct
public void validateEncryptionKeys() {
if (encryptionPass == null || encryptionPass.isBlank()) {
throw new IllegalArgumentException(
"CRITICAL: app.encryption.password is missing or blank. " +
"AES-256 encryption disabled. Set a strong password in environment variables."
);
}

if (pepper == null || pepper.isBlank()) {
throw new IllegalArgumentException(
"CRITICAL: app.encryption.pepper is missing or blank. " +
"Key derivation weakened. Set a strong pepper value in environment variables."
);
}

// Minsta entropy-krav för password: 32 tecken
if (encryptionPass.length() < 32) {
throw new IllegalArgumentException(
"CRITICAL: app.encryption.password is too short (" + encryptionPass.length() + " chars). " +
"Requires minimum 32 characters for AES-256 security. " +
"Use a strong random password like: openssl rand -base64 24"
);
}

// Minsta entropy-krav för pepper: 16 tecken
if (pepper.length() < 16) {
throw new IllegalArgumentException(
"CRITICAL: app.encryption.pepper is too short (" + pepper.length() + " chars). " +
"Requires minimum 16 characters. Use: openssl rand -hex 8"
);
}
}

/**
* pass + pepper = key, delux kör dem genom en nyckelderivationsfunktion (PBKDF2)
* för att skapa den faktiska AES-nyckeln som vi sedan använder för att kunna låsa upp/låsa personnummer
* delux lägger till ett randomiserat salt-värde(Nonce/IV) vid varje anrop av encrypt
* delux-objektet lever i minnet under hela applikationens körning - Spring hanterar livscykeln som en singleton Bean,
* så det randomiserade saltet är unikt per kryptering och gör att samma input ger olika output varje gång
* Det håller internt koll på:
* encryptionPass + pepper → för att kunna kryptera/dekryptera och sätter in/plockar ut IV(saltet) vid encrypt/decrypt
* Logiken för att generera ett nytt slumpmässigt IV/salt vid varje encrypt()-anrop
*
* Säkerhet: Om password eller pepper är tom/svag kastas IllegalArgumentException vid startup (@PostConstruct)
*/
@Bean
public TextEncryptor textEncryptor() {
return Encryptors.text(encryptionKey, salt);
return Encryptors.delux(encryptionPass, pepper);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package org.example.cyberwatch.config.security;

import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.apache.commons.codec.digest.HmacAlgorithms;
import org.apache.commons.codec.digest.HmacUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.encrypt.TextEncryptor;
import org.springframework.stereotype.Service;

Expand All @@ -10,22 +14,62 @@ public class EncryptionService {

private final TextEncryptor textEncryptor;

@Value("${app.encryption.hmac-key}") // En separat fast nyckel för sökning
private String hmacKey;

//Fail-fas validering. Detta garanterar att blind indexet alltid är starkt och predictable duplicates är omöjliga.
@PostConstruct
public void validateHmacKey() {
if (hmacKey == null || hmacKey.isBlank()) {
throw new IllegalArgumentException(
"CRITICAL: app.encryption.hmac-key is missing or blank. " +
"Blind index security compromised. Set a strong, non-empty key in environment variables."
);
}

// Minsta entropy-krav: 16 tecken (128 bits)
if (hmacKey.length() < 16) {
throw new IllegalArgumentException(
"CRITICAL: app.encryption.hmac-key is too short (" + hmacKey.length() + " chars). " +
"Requires minimum 16 characters for adequate entropy. " +
"Use a strong random key like: " +
"openssl rand -hex 16"
);
}
}

/**
* Delegerar till delux som genererar ett unikt IV, krypterar med AES-256 nycken
* och returnerar IV + krypterad data som en enda sträng, det är denna som sedan lagras i databasen.
*/
public String encrypt(String data) {
return textEncryptor.encrypt(data);
}

/**
* Delegerar till delux som plockar ut IV från strängen och dekrypterar
* med AES-256 för att återge klartexten.
*/
public String decrypt(String data) {
return textEncryptor.decrypt(data);
}

public String maskLastFour(String encrypted) {
if (encrypted == null || encrypted.isBlank()) {
/**
* Skapar ett "Blind Index" (deterministisk hash, sökbar).
* Används för att jämföra om personnummer redan finns i databasen.
*/
public String hmac(String data) {
if (data == null) return null;
return new HmacUtils(HmacAlgorithms.HMAC_SHA_256, hmacKey).hmacHex(data);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

public String maskLastFour(String plainText) {
if (plainText == null || plainText.isBlank()) {
return "****";
}
String decrypted = textEncryptor.decrypt(encrypted);
if (decrypted.length() <= 4) {
if (plainText.length() <= 4) {
return "****";
}
return decrypted.substring(0, decrypted.length() - 4) + "****";
return plainText.substring(0, plainText.length() - 4) + "****";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
public class CreateEmploymentDTO {

@NotBlank(message = "Social security number cannot be blank")
@Pattern(regexp = "\\d{6}-\\d{4}|\\d{8}-\\d{4}", message = "Social security number must be in format YYMMDD-NNNN or YYYYMMDD-NNNN")
@Pattern(regexp = "^(19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])-\\d{4}$",
message = "Social security number must match format YYYYMMDD-NNNN")
private String socialSecurityNumber;

@NotBlank(message = "First name cannot be blank")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public EmploymentFormDTO toDTO(EmploymentForm entity) {
public EmploymentForm toEntity(CreateEmploymentDTO dto) {
if (dto == null) return null;
EmploymentForm entity = new EmploymentForm();
entity.setSocialSecurityNumber(dto.getSocialSecurityNumber());
// Note: socialSecurityNumber and ssnHash are handled separately in service layer for encryption/hashing
entity.setFirstName(dto.getFirstName());
entity.setLastName(dto.getLastName());
entity.setEmail(dto.getEmail());
Expand All @@ -49,7 +49,7 @@ public EmploymentForm toEntity(CreateEmploymentDTO dto) {
public void updateEntity(UpdateEmploymentDTO dto, EmploymentForm entity) {
if (dto == null || entity == null) return;

entity.setSocialSecurityNumber(dto.getSocialSecurityNumber());
// Note: socialSecurityNumber and ssnHash are handled separately in service layer for encryption/hashing
entity.setFirstName(dto.getFirstName());
entity.setLastName(dto.getLastName());
entity.setEmail(dto.getEmail());
Expand All @@ -61,7 +61,9 @@ public void updateEntity(UpdateEmploymentDTO dto, EmploymentForm entity) {
public Staff formToStaff(EmploymentForm form) {
if (form == null) return null;
Staff staff = new Staff();
// Copy already-encrypted SSN and its hash from approved form
staff.setSocialSecurityNumber(form.getSocialSecurityNumber());
staff.setSsnHash(form.getSsnHash());
staff.setFirstName(form.getFirstName());
staff.setLastName(form.getLastName());
staff.setEmail(form.getEmail());
Expand Down
Loading
Loading