diff --git a/MiniKms/pom.xml b/MiniKms/pom.xml index 4a815af..25837cc 100644 --- a/MiniKms/pom.xml +++ b/MiniKms/pom.xml @@ -34,18 +34,12 @@ org.springframework.boot spring-boot-starter-security - org.springframework.boot spring-boot-devtools runtime true - - org.projectlombok - lombok - true - org.springframework.boot spring-boot-starter-test @@ -55,11 +49,48 @@ org.springframework.boot spring-boot-starter-data-jpa + + org.springframework.boot + spring-boot-starter-web + org.springframework.security spring-security-test test + + io.jsonwebtoken + jjwt-api + 0.13.0 + + + io.jsonwebtoken + jjwt-impl + 0.13.0 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.13.0 + runtime + + + org.postgresql + postgresql + 42.7.8 + + + org.projectlombok + lombok + 1.18.42 + provided + + + org.mapstruct + mapstruct + 1.6.0 + @@ -72,6 +103,12 @@ org.projectlombok lombok + 1.18.40 + + + org.mapstruct + mapstruct-processor + 1.6.0 diff --git a/MiniKms/src/main/java/ftn/security/minikms/config/JsonDeserializationConfig.java b/MiniKms/src/main/java/ftn/security/minikms/config/JsonDeserializationConfig.java new file mode 100644 index 0000000..52d1538 --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/config/JsonDeserializationConfig.java @@ -0,0 +1,15 @@ +package ftn.security.minikms.config; + +import com.fasterxml.jackson.databind.MapperFeature; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JsonDeserializationConfig { + @Bean + public Jackson2ObjectMapperBuilderCustomizer caseInsensitiveEnums() { + return builder -> builder + .featuresToEnable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS); + } +} diff --git a/MiniKms/src/main/java/ftn/security/minikms/config/SecurityConfig.java b/MiniKms/src/main/java/ftn/security/minikms/config/SecurityConfig.java new file mode 100644 index 0000000..e2839bf --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/config/SecurityConfig.java @@ -0,0 +1,91 @@ +package ftn.security.minikms.config; + +import ftn.security.minikms.service.auth.JwtAuthenticationFilter; +import ftn.security.minikms.service.auth.MiniKmsUserDetailsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + private final String jwtSecret; + private final MiniKmsUserDetailsService service; + + @Autowired + public SecurityConfig( + @Value("${jwt.secret}") String jwtSecret, + MiniKmsUserDetailsService service) { + this.jwtSecret = jwtSecret; + this.service = service; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http + // Stateless, token-only API: no cookies => CSRF not applicable + .csrf(AbstractHttpConfigurer::disable) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .cors(Customizer.withDefaults()) + + .formLogin(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .logout(AbstractHttpConfigurer::disable) + + .authorizeHttpRequests(auth -> auth + .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() + .requestMatchers("/api/v1/auth/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/v1/keys/**").authenticated() // Allow all roles to GET + .requestMatchers("/api/v1/keys/**").hasRole("MANAGER") + .anyRequest().authenticated() + ) + + .userDetailsService(service) + .addFilterBefore(new JwtAuthenticationFilter( + jwtSecret, + service + ), UsernamePasswordAuthenticationFilter.class) + .build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { + return configuration.getAuthenticationManager(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + var config = new CorsConfiguration(); + config.setAllowedOrigins(List.of("http://localhost:4200", "http://localhost")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + config.setAllowedHeaders(List.of("Authorization", "Content-Type")); + config.setAllowCredentials(false); + + var source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } +} diff --git a/MiniKms/src/main/java/ftn/security/minikms/controller/AuthController.java b/MiniKms/src/main/java/ftn/security/minikms/controller/AuthController.java new file mode 100644 index 0000000..bc5d0a4 --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/controller/AuthController.java @@ -0,0 +1,40 @@ +package ftn.security.minikms.controller; + +import ftn.security.minikms.dto.AuthDTO; +import ftn.security.minikms.dto.TokenDTO; +import ftn.security.minikms.service.auth.JwtService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.AuthenticationException; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/auth") +public class AuthController { + private final AuthenticationManager authManager; + private final JwtService jwtService; + + @Autowired + public AuthController(AuthenticationManager authManager, JwtService jwtService) { + this.authManager = authManager; + this.jwtService = jwtService; + } + + @PostMapping + public ResponseEntity auth(@RequestBody AuthDTO dto) { + try { + var auth = authManager.authenticate( + new UsernamePasswordAuthenticationToken(dto.getUsername(), dto.getPassword()) + ); + var token = jwtService.generateToken(auth.getName()); + return ResponseEntity.ok(new TokenDTO(token)); + } catch (AuthenticationException e) { + return ResponseEntity.status(401).body("Invalid credentials"); + } + } +} diff --git a/MiniKms/src/main/java/ftn/security/minikms/controller/KeyManagementController.java b/MiniKms/src/main/java/ftn/security/minikms/controller/KeyManagementController.java index e5f1f2d..e821ae7 100644 --- a/MiniKms/src/main/java/ftn/security/minikms/controller/KeyManagementController.java +++ b/MiniKms/src/main/java/ftn/security/minikms/controller/KeyManagementController.java @@ -1,35 +1,64 @@ package ftn.security.minikms.controller; import ftn.security.minikms.dto.KeyDTO; +import ftn.security.minikms.dto.KeyMapper; import ftn.security.minikms.service.KeyService; +import org.mapstruct.factory.Mappers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.security.NoSuchAlgorithmException; +import java.security.GeneralSecurityException; +import java.security.InvalidParameterException; +import java.security.Principal; +import java.util.UUID; @RestController @RequestMapping(value = "/api/v1/keys") public class KeyManagementController { + private final KeyService keyService; + private final KeyMapper mapper; + @Autowired - private KeyService keyService; + public KeyManagementController(KeyService keyService) { + this.keyService = keyService; + this.mapper = Mappers.getMapper(KeyMapper.class); + } @PostMapping("/create") - public ResponseEntity createKey(@RequestBody KeyDTO dto) throws NoSuchAlgorithmException { - String id = keyService.createKey(dto.getKeyType()); - dto.setId(id); - return new ResponseEntity<>(dto, HttpStatus.CREATED); + public ResponseEntity createKey(@RequestBody KeyDTO dto, Principal principal) throws GeneralSecurityException { + var username = principal.getName(); + + try { + var created = keyService.createKey(dto.getAlias(), dto.getKeyType(), dto.getAllowedOperations(), username); + return ResponseEntity.status(HttpStatus.CREATED).body(mapper.toDto(created)); + } catch (InvalidParameterException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } } + @PostMapping("/rotate") - public ResponseEntity rotateKey(@RequestBody KeyDTO dto){ - keyService.rotateKey(dto.getKeyType(), dto.getId()); - return new ResponseEntity<>(dto, HttpStatus.CREATED); - } - @PutMapping("/delete/{id}") - public ResponseEntity deleteKey(@PathVariable String id){ - keyService.deleteKey(id); - return new ResponseEntity<>(id, HttpStatus.OK); + public ResponseEntity rotateKey(@RequestBody KeyDTO dto, Principal principal) throws GeneralSecurityException { + var username = principal.getName(); + + try { + var created = keyService.rotateKey(dto.getId(), username); + return ResponseEntity.status(HttpStatus.CREATED).body(mapper.toDto(created)); + } catch (InvalidParameterException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } } + @DeleteMapping("/{id}") + public ResponseEntity deleteKey(@PathVariable UUID id, Principal principal) { + var username = principal.getName(); + + try { + keyService.deleteKey(id, username); + return ResponseEntity.noContent().build(); + } catch (InvalidParameterException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } + } } diff --git a/MiniKms/src/main/java/ftn/security/minikms/dto/AuthDTO.java b/MiniKms/src/main/java/ftn/security/minikms/dto/AuthDTO.java new file mode 100644 index 0000000..7727799 --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/dto/AuthDTO.java @@ -0,0 +1,9 @@ +package ftn.security.minikms.dto; + +import lombok.Data; + +@Data +public class AuthDTO { + private String username; + private String password; +} diff --git a/MiniKms/src/main/java/ftn/security/minikms/dto/KeyDTO.java b/MiniKms/src/main/java/ftn/security/minikms/dto/KeyDTO.java index 3624f34..35e33b3 100644 --- a/MiniKms/src/main/java/ftn/security/minikms/dto/KeyDTO.java +++ b/MiniKms/src/main/java/ftn/security/minikms/dto/KeyDTO.java @@ -1,16 +1,17 @@ package ftn.security.minikms.dto; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import ftn.security.minikms.enumeration.KeyOperation; +import ftn.security.minikms.enumeration.KeyType; +import lombok.*; -@Getter -@Setter +import java.util.List; +import java.util.UUID; + +@Data @NoArgsConstructor -@AllArgsConstructor public class KeyDTO { - private String id; + private UUID id; private String alias; - private String keyType; + private KeyType keyType; + private List allowedOperations; } diff --git a/MiniKms/src/main/java/ftn/security/minikms/dto/KeyMapper.java b/MiniKms/src/main/java/ftn/security/minikms/dto/KeyMapper.java new file mode 100644 index 0000000..aa08e42 --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/dto/KeyMapper.java @@ -0,0 +1,9 @@ +package ftn.security.minikms.dto; + +import ftn.security.minikms.entity.KeyMetadata; +import org.mapstruct.Mapper; + +@Mapper +public interface KeyMapper { + KeyMetadataDTO toDto(KeyMetadata key); +} diff --git a/MiniKms/src/main/java/ftn/security/minikms/dto/KeyMetadataDTO.java b/MiniKms/src/main/java/ftn/security/minikms/dto/KeyMetadataDTO.java new file mode 100644 index 0000000..2efea71 --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/dto/KeyMetadataDTO.java @@ -0,0 +1,21 @@ +package ftn.security.minikms.dto; + +import ftn.security.minikms.enumeration.KeyOperation; +import ftn.security.minikms.enumeration.KeyType; +import lombok.*; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +@Data +@NoArgsConstructor +public class KeyMetadataDTO { + private UUID id; + private String alias; + private Integer primaryVersion; + private KeyType keyType; + private List allowedOperations; + private Instant createdAt; + private Instant rotatedAt; +} diff --git a/MiniKms/src/main/java/ftn/security/minikms/dto/TokenDTO.java b/MiniKms/src/main/java/ftn/security/minikms/dto/TokenDTO.java new file mode 100644 index 0000000..23dbdae --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/dto/TokenDTO.java @@ -0,0 +1,10 @@ +package ftn.security.minikms.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class TokenDTO { + private String token; +} diff --git a/MiniKms/src/main/java/ftn/security/minikms/entity/KeyMaterial.java b/MiniKms/src/main/java/ftn/security/minikms/entity/KeyMaterial.java new file mode 100644 index 0000000..56a7dfd --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/entity/KeyMaterial.java @@ -0,0 +1,30 @@ +package ftn.security.minikms.entity; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.Lob; +import lombok.Data; + +import javax.crypto.SecretKey; +import java.security.KeyPair; + +@Data +@Embeddable +public class KeyMaterial { + @Lob + private byte[] key; + @Lob + private byte[] publicKey; + + public static KeyMaterial of(SecretKey key) { + var material = new KeyMaterial(); + material.setKey(key.getEncoded()); + return material; + } + + public static KeyMaterial of(KeyPair pair) { + var material = new KeyMaterial(); + material.setKey(pair.getPrivate().getEncoded()); + material.setPublicKey(pair.getPublic().getEncoded()); + return material; + } +} diff --git a/MiniKms/src/main/java/ftn/security/minikms/entity/KeyMetadata.java b/MiniKms/src/main/java/ftn/security/minikms/entity/KeyMetadata.java new file mode 100644 index 0000000..95b5109 --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/entity/KeyMetadata.java @@ -0,0 +1,65 @@ +package ftn.security.minikms.entity; + +import ftn.security.minikms.enumeration.KeyOperation; +import ftn.security.minikms.enumeration.KeyType; +import jakarta.persistence.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +@Data +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +@Entity +@Table(name = "keys") +public class KeyMetadata { + @EqualsAndHashCode.Include + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + private String alias; + private Integer primaryVersion; + + @Enumerated(EnumType.STRING) + private KeyType keyType; + + @ElementCollection(fetch = FetchType.EAGER) + @Enumerated(EnumType.STRING) + @CollectionTable(name = "key_allowed_operations") + private List allowedOperations; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + private User user; + + private Instant createdAt; + private Instant rotatedAt; + + @OneToMany(mappedBy = "metadata", cascade = CascadeType.ALL, orphanRemoval = true) + private List versions; + + public static KeyMetadata of(String alias, KeyType keyType, List allowedOperations, User user) { + var entity = new KeyMetadata(); + entity.alias = alias; + entity.primaryVersion = 0; + entity.keyType = keyType; + entity.allowedOperations = allowedOperations; + entity.user = user; + entity.createdAt = Instant.now(); + return entity; + } + + public void updatePrimaryVersion(Integer version) { + primaryVersion = version; + + if (version > 1) { + rotatedAt = Instant.now(); + } + } +} diff --git a/MiniKms/src/main/java/ftn/security/minikms/entity/User.java b/MiniKms/src/main/java/ftn/security/minikms/entity/User.java new file mode 100644 index 0000000..13105f7 --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/entity/User.java @@ -0,0 +1,24 @@ +package ftn.security.minikms.entity; + +import ftn.security.minikms.enumeration.UserRole; +import jakarta.persistence.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +@Entity +@Table(name = "users") +public class User { + @EqualsAndHashCode.Include + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String username; + private String password; + + @Enumerated(EnumType.STRING) + private UserRole role; +} diff --git a/MiniKms/src/main/java/ftn/security/minikms/entity/WrappedKey.java b/MiniKms/src/main/java/ftn/security/minikms/entity/WrappedKey.java new file mode 100644 index 0000000..143c64e --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/entity/WrappedKey.java @@ -0,0 +1,37 @@ +package ftn.security.minikms.entity; + +import jakarta.persistence.*; +import jakarta.persistence.Id; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +@Data +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +@Entity +@Table(name = "key_versions") +public class WrappedKey { + @EqualsAndHashCode.Include + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Integer version; + + @Embedded + private KeyMaterial wrappedMaterial; + + @ManyToOne + @JoinColumn(name = "key_metadata_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + private KeyMetadata metadata; + + public static WrappedKey of(KeyMaterial wrappedMaterial, KeyMetadata metadata) { + var entity = new WrappedKey(); + entity.version = 1; + entity.wrappedMaterial = wrappedMaterial; + entity.metadata = metadata; + return entity; + } +} diff --git a/MiniKms/src/main/java/ftn/security/minikms/entity/WrappedKeyEntity.java b/MiniKms/src/main/java/ftn/security/minikms/entity/WrappedKeyEntity.java deleted file mode 100644 index 35639c3..0000000 --- a/MiniKms/src/main/java/ftn/security/minikms/entity/WrappedKeyEntity.java +++ /dev/null @@ -1,49 +0,0 @@ -package ftn.security.minikms.entity; - -import ftn.security.minikms.enumeration.KeyOperation; -import ftn.security.minikms.enumeration.KeyType; -import jakarta.persistence.*; -import jakarta.persistence.Id; -import lombok.Data; - -import java.time.Instant; -import java.util.List; -import java.util.UUID; - -@Data -@Entity -public class WrappedKeyEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, updatable = false) - private UUID logicalKey; - - private String alias; - private Integer version; - - @Enumerated(EnumType.STRING) - private KeyType keyType; - - @Lob - private byte[] wrappedKey; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - private List allowedOperations; - - private Instant createdAt; - - public static WrappedKeyEntity of(UUID logicalKey, String alias, Integer version, KeyType keyType, byte[] wrappedKey, List allowedOperations) { - var entity = new WrappedKeyEntity(); - entity.logicalKey = logicalKey; - entity.alias = alias; - entity.version = version; - entity.keyType = keyType; - entity.wrappedKey = wrappedKey; - entity.allowedOperations = allowedOperations; - entity.createdAt = Instant.now(); - return entity; - } -} \ No newline at end of file diff --git a/MiniKms/src/main/java/ftn/security/minikms/enumeration/UserRole.java b/MiniKms/src/main/java/ftn/security/minikms/enumeration/UserRole.java new file mode 100644 index 0000000..485af3c --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/enumeration/UserRole.java @@ -0,0 +1,5 @@ +package ftn.security.minikms.enumeration; + +public enum UserRole { + MANAGER, USER, AUDITOR +} diff --git a/MiniKms/src/main/java/ftn/security/minikms/repository/KeyMetadataRepository.java b/MiniKms/src/main/java/ftn/security/minikms/repository/KeyMetadataRepository.java new file mode 100644 index 0000000..da1e440 --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/repository/KeyMetadataRepository.java @@ -0,0 +1,12 @@ +package ftn.security.minikms.repository; + +import ftn.security.minikms.entity.KeyMetadata; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface KeyMetadataRepository extends JpaRepository { + boolean existsByIdAndUserUsername(UUID id, String username); + Optional findByIdAndUserUsername(UUID id, String username); +} diff --git a/MiniKms/src/main/java/ftn/security/minikms/repository/UserRepository.java b/MiniKms/src/main/java/ftn/security/minikms/repository/UserRepository.java new file mode 100644 index 0000000..f9c605d --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/repository/UserRepository.java @@ -0,0 +1,10 @@ +package ftn.security.minikms.repository; + +import ftn.security.minikms.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); +} diff --git a/MiniKms/src/main/java/ftn/security/minikms/repository/WrappedKeyRepository.java b/MiniKms/src/main/java/ftn/security/minikms/repository/WrappedKeyRepository.java index 208ff8b..1ba4b75 100644 --- a/MiniKms/src/main/java/ftn/security/minikms/repository/WrappedKeyRepository.java +++ b/MiniKms/src/main/java/ftn/security/minikms/repository/WrappedKeyRepository.java @@ -1,11 +1,7 @@ package ftn.security.minikms.repository; -import ftn.security.minikms.entity.WrappedKeyEntity; +import ftn.security.minikms.entity.WrappedKey; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; -import java.util.UUID; - -public interface WrappedKeyRepository extends JpaRepository { - Optional findFirstByLogicalKeyOrderByVersionDesc(UUID logicalKey); +public interface WrappedKeyRepository extends JpaRepository { } diff --git a/MiniKms/src/main/java/ftn/security/minikms/service/AES.java b/MiniKms/src/main/java/ftn/security/minikms/service/AES.java deleted file mode 100644 index f36c1a1..0000000 --- a/MiniKms/src/main/java/ftn/security/minikms/service/AES.java +++ /dev/null @@ -1,21 +0,0 @@ -package ftn.security.minikms.service; - -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; -import java.security.NoSuchAlgorithmException; - -public class AES { - public static String createKey() throws NoSuchAlgorithmException { - SecretKey key = generateKey(); - //save key - return "keyId"; - } - private static SecretKey generateKey() throws NoSuchAlgorithmException { - KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); - keyGenerator.init(256); - return keyGenerator.generateKey(); - } - public static void rotateKey(String Id){ - //rotate - } -} diff --git a/MiniKms/src/main/java/ftn/security/minikms/service/AESService.java b/MiniKms/src/main/java/ftn/security/minikms/service/AESService.java new file mode 100644 index 0000000..0f42948 --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/service/AESService.java @@ -0,0 +1,16 @@ +package ftn.security.minikms.service; + +import ftn.security.minikms.entity.KeyMaterial; + +import javax.crypto.KeyGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +class AESService implements ICryptoService { + public KeyMaterial generateKey() throws NoSuchAlgorithmException { + KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); + keyGenerator.init(256, SecureRandom.getInstanceStrong()); + var key = keyGenerator.generateKey(); + return KeyMaterial.of(key); + } +} diff --git a/MiniKms/src/main/java/ftn/security/minikms/service/HMAC.java b/MiniKms/src/main/java/ftn/security/minikms/service/HMAC.java deleted file mode 100644 index 16bd809..0000000 --- a/MiniKms/src/main/java/ftn/security/minikms/service/HMAC.java +++ /dev/null @@ -1,22 +0,0 @@ -package ftn.security.minikms.service; - -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; - -public class HMAC { - public static String createKey() throws NoSuchAlgorithmException { - SecretKey key = generateKey(); - //save key - return "keyId"; - } - private static SecretKey generateKey() throws NoSuchAlgorithmException { - KeyGenerator keyGenerator = KeyGenerator.getInstance("HmacSHA512"); - keyGenerator.init(256, SecureRandom.getInstanceStrong()); - return keyGenerator.generateKey(); - } - public static void rotateKey(String Id){ - //rotate - } -} diff --git a/MiniKms/src/main/java/ftn/security/minikms/service/HMACService.java b/MiniKms/src/main/java/ftn/security/minikms/service/HMACService.java new file mode 100644 index 0000000..516b2d7 --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/service/HMACService.java @@ -0,0 +1,16 @@ +package ftn.security.minikms.service; + +import ftn.security.minikms.entity.KeyMaterial; + +import javax.crypto.KeyGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +class HMACService implements ICryptoService { + public KeyMaterial generateKey() throws NoSuchAlgorithmException { + KeyGenerator keyGenerator = KeyGenerator.getInstance("HmacSHA512"); + keyGenerator.init(256, SecureRandom.getInstanceStrong()); + var key = keyGenerator.generateKey(); + return KeyMaterial.of(key); + } +} diff --git a/MiniKms/src/main/java/ftn/security/minikms/service/ICryptoService.java b/MiniKms/src/main/java/ftn/security/minikms/service/ICryptoService.java new file mode 100644 index 0000000..c2cd9ef --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/service/ICryptoService.java @@ -0,0 +1,9 @@ +package ftn.security.minikms.service; + +import ftn.security.minikms.entity.KeyMaterial; + +import java.security.NoSuchAlgorithmException; + +public interface ICryptoService { + KeyMaterial generateKey() throws NoSuchAlgorithmException; +} diff --git a/MiniKms/src/main/java/ftn/security/minikms/service/KeyService.java b/MiniKms/src/main/java/ftn/security/minikms/service/KeyService.java index 8415f34..4c56e64 100644 --- a/MiniKms/src/main/java/ftn/security/minikms/service/KeyService.java +++ b/MiniKms/src/main/java/ftn/security/minikms/service/KeyService.java @@ -1,29 +1,88 @@ package ftn.security.minikms.service; +import ftn.security.minikms.entity.KeyMetadata; +import ftn.security.minikms.entity.User; +import ftn.security.minikms.entity.WrappedKey; +import ftn.security.minikms.enumeration.KeyOperation; +import ftn.security.minikms.enumeration.KeyType; +import ftn.security.minikms.repository.KeyMetadataRepository; +import ftn.security.minikms.repository.UserRepository; +import ftn.security.minikms.repository.WrappedKeyRepository; import org.springframework.stereotype.Service; +import java.security.GeneralSecurityException; import java.security.InvalidParameterException; -import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.Map; +import java.util.UUID; @Service public class KeyService { - public String createKey(String keyType) throws NoSuchAlgorithmException, InvalidParameterException { - return switch (keyType) { - case "symmetric" -> AES.createKey(); - case "asymmetric" -> RSA.createKey(); - case "hmac" -> HMAC.createKey(); - default -> throw new InvalidParameterException(); - }; + private final KeyMetadataRepository metadataRepository; + private final WrappedKeyRepository keyRepository; + private final UserRepository userRepository; + private final RootKeyManager rootKeyManager; + private final Map cryptoServices; + private static final String NOT_AUTHORIZED_MSG = "You do not own a key with given id"; + + public KeyService( + KeyMetadataRepository metadataRepository, + WrappedKeyRepository keyRepository, + UserRepository userRepository, + RootKeyManager rootKeyManager) { + this.metadataRepository = metadataRepository; + this.keyRepository = keyRepository; + this.userRepository = userRepository; + this.rootKeyManager = rootKeyManager; + this.cryptoServices = Map.of( + KeyType.SYMMETRIC, new AESService(), + KeyType.ASYMMETRIC, new RSAService(), + KeyType.HMAC, new HMACService() + ); + } + + public KeyMetadata createKey(String alias, KeyType keyType, List allowedOperations, String username) + throws InvalidParameterException, GeneralSecurityException { + var user = findUserByUsername(username); + var metadata = metadataRepository.save(KeyMetadata.of(alias, keyType, allowedOperations, user)); + return createNewKeyVersion(metadata, 1); + } + + public void deleteKey(UUID id, String username) throws InvalidParameterException { + if (!metadataRepository.existsByIdAndUserUsername(id, username)) + throw new InvalidParameterException(NOT_AUTHORIZED_MSG); + + metadataRepository.deleteById(id); } - public void deleteKey(String Id){ - //delete from database + + public KeyMetadata rotateKey(UUID id, String username) throws InvalidParameterException, GeneralSecurityException { + var metadata = metadataRepository.findByIdAndUserUsername(id, username) + .orElseThrow(() -> new InvalidParameterException(NOT_AUTHORIZED_MSG)); + + var nextVersion = metadata.getPrimaryVersion() + 1; + return createNewKeyVersion(metadata, nextVersion); } - public void rotateKey(String keyType, String keyId) throws InvalidParameterException { - switch(keyType){ - case "symmetric" -> AES.rotateKey(keyId); - case "asymmetric" -> RSA.rotateKey(keyId); - case "hmac" -> HMAC.rotateKey(keyId); - default -> throw new InvalidParameterException(); - } + + private KeyMetadata createNewKeyVersion(KeyMetadata metadata, Integer version) throws GeneralSecurityException { + var id = metadata.getId(); + var keyType = metadata.getKeyType(); + + var material = cryptoServices.get(keyType).generateKey(); + var secretKey = material.getKey(); + + // Not wrapping public key, just secret + var wrapped = rootKeyManager.wrap(secretKey, id, version); + if (secretKey != null) java.util.Arrays.fill(secretKey, (byte) 0); // Zeroizing sensitive in memory data + + material.setKey(wrapped); + + var key = keyRepository.save(WrappedKey.of(material, metadata)); + metadata.updatePrimaryVersion(version); // Set the latest version as primary + return metadataRepository.save(metadata); + } + + private User findUserByUsername(String username) throws InvalidParameterException { + return userRepository.findByUsername(username).orElseThrow(() -> + new InvalidParameterException("User with given username does not exist")); } } diff --git a/MiniKms/src/main/java/ftn/security/minikms/service/RSA.java b/MiniKms/src/main/java/ftn/security/minikms/service/RSA.java deleted file mode 100644 index ab8fe75..0000000 --- a/MiniKms/src/main/java/ftn/security/minikms/service/RSA.java +++ /dev/null @@ -1,21 +0,0 @@ -package ftn.security.minikms.service; - -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; - -public class RSA { - public static String createKey() throws NoSuchAlgorithmException { - KeyPair key = generateKey(); - //save key - return "keyId"; - } - private static KeyPair generateKey() throws NoSuchAlgorithmException { - KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); - generator.initialize(2048); - return generator.generateKeyPair(); - } - public static void rotateKey(String Id){ - //rotate - } -} diff --git a/MiniKms/src/main/java/ftn/security/minikms/service/RSAService.java b/MiniKms/src/main/java/ftn/security/minikms/service/RSAService.java new file mode 100644 index 0000000..83a54ec --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/service/RSAService.java @@ -0,0 +1,15 @@ +package ftn.security.minikms.service; + +import ftn.security.minikms.entity.KeyMaterial; + +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; + +class RSAService implements ICryptoService { + public KeyMaterial generateKey() throws NoSuchAlgorithmException { + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(2048); + var pair = generator.generateKeyPair(); + return KeyMaterial.of(pair); + } +} diff --git a/MiniKms/src/main/java/ftn/security/minikms/service/RootKeyManager.java b/MiniKms/src/main/java/ftn/security/minikms/service/RootKeyManager.java index c99ee82..103de23 100644 --- a/MiniKms/src/main/java/ftn/security/minikms/service/RootKeyManager.java +++ b/MiniKms/src/main/java/ftn/security/minikms/service/RootKeyManager.java @@ -9,6 +9,7 @@ import javax.crypto.spec.SecretKeySpec; import java.security.GeneralSecurityException; import java.util.Base64; +import java.util.UUID; @Service public class RootKeyManager { @@ -24,13 +25,14 @@ public RootKeyManager(@Value("${ROOT_KEY}") String base64Key) { this.rootKey = new SecretKeySpec(raw, "AES"); } - public byte[] wrap(byte[] plaintextKey, byte[] aad) throws GeneralSecurityException { + public byte[] wrap(byte[] plaintextKey, UUID id, Integer version) throws GeneralSecurityException { var iv = new byte[12]; RNG.nextBytes(iv); + var aad = getAad(id, version); var c = Cipher.getInstance("AES/GCM/NoPadding"); c.init(Cipher.ENCRYPT_MODE, rootKey, new GCMParameterSpec(128, iv)); - if (aad != null && aad.length > 0) c.updateAAD(aad); + if (aad.length > 0) c.updateAAD(aad); var ct = c.doFinal(plaintextKey); var out = new byte[iv.length + ct.length]; @@ -39,14 +41,23 @@ public byte[] wrap(byte[] plaintextKey, byte[] aad) throws GeneralSecurityExcept return out; } - public byte[] unwrap(byte[] blob, byte[] aad) throws GeneralSecurityException { + public byte[] unwrap(byte[] blob, UUID id, Integer version) throws GeneralSecurityException { if (blob.length < 12 + 16) throw new GeneralSecurityException("Blob too short"); + var aad = getAad(id, version); var iv = java.util.Arrays.copyOfRange(blob, 0, 12); var ct = java.util.Arrays.copyOfRange(blob, 12, blob.length); var c = Cipher.getInstance("AES/GCM/NoPadding"); c.init(Cipher.DECRYPT_MODE, rootKey, new GCMParameterSpec(128, iv)); - if (aad != null && aad.length > 0) c.updateAAD(aad); + if (aad.length > 0) c.updateAAD(aad); return c.doFinal(ct); } + + private static byte[] getAad(UUID id, Integer version) { + if (id == null || version == null) { + throw new IllegalArgumentException("id and version must not be null"); + } + var aadString = id + ":" + version; + return aadString.getBytes(java.nio.charset.StandardCharsets.UTF_8); + } } diff --git a/MiniKms/src/main/java/ftn/security/minikms/service/auth/JwtAuthenticationFilter.java b/MiniKms/src/main/java/ftn/security/minikms/service/auth/JwtAuthenticationFilter.java new file mode 100644 index 0000000..4928eec --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/service/auth/JwtAuthenticationFilter.java @@ -0,0 +1,62 @@ +package ftn.security.minikms.service.auth; + +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.lang.NonNull; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.crypto.SecretKey; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final SecretKey jwtSecret; + private final MiniKmsUserDetailsService userDetailsService; + + public JwtAuthenticationFilter( + String jwtSecret, + MiniKmsUserDetailsService userDetailsService) { + this.jwtSecret = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8)); + this.userDetailsService = userDetailsService; + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + String header = request.getHeader("Authorization"); + String token; + String username = null; + + if (header != null && header.startsWith("Bearer ")) { + token = header.substring(7); + try { + var jws = io.jsonwebtoken.Jwts.parser() + .verifyWith(jwtSecret) + .build() + .parseSignedClaims(token); + + username = jws.getPayload().getSubject(); + } catch (io.jsonwebtoken.JwtException ignored) { + } + } + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + var auth = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(auth); + } + + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/MiniKms/src/main/java/ftn/security/minikms/service/auth/JwtService.java b/MiniKms/src/main/java/ftn/security/minikms/service/auth/JwtService.java new file mode 100644 index 0000000..619b9c7 --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/service/auth/JwtService.java @@ -0,0 +1,32 @@ +package ftn.security.minikms.service.auth; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +@Service +public class JwtService { + private final SecretKey secretKey; + private final Long expirationMillis; + + public JwtService( + @Value("${jwt.secret}") String secret, + @Value("${jwt.expiration}") Long expirationMillis) { + this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + this.expirationMillis = expirationMillis; + } + + public String generateToken(String username) { + return Jwts.builder() + .subject(username) + .issuedAt(new Date()) + .expiration(new Date(System.currentTimeMillis() + expirationMillis)) + .signWith(secretKey) + .compact(); + } +} diff --git a/MiniKms/src/main/java/ftn/security/minikms/service/auth/MiniKmsUserDetailsService.java b/MiniKms/src/main/java/ftn/security/minikms/service/auth/MiniKmsUserDetailsService.java new file mode 100644 index 0000000..4dc02c9 --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/service/auth/MiniKmsUserDetailsService.java @@ -0,0 +1,35 @@ +package ftn.security.minikms.service.auth; + +import ftn.security.minikms.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.Collections; + +@Service +public class MiniKmsUserDetailsService implements UserDetailsService { + private final UserRepository repository; + + @Autowired + public MiniKmsUserDetailsService(UserRepository repository) { + this.repository = repository; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + var user = repository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + var roleName = "ROLE_" + user.getRole().name(); + + return new User( + user.getUsername(), + user.getPassword(), + Collections.singletonList(new SimpleGrantedAuthority(roleName)) + ); + } +} diff --git a/MiniKms/src/main/resources/application.properties b/MiniKms/src/main/resources/application.properties index d3bcf13..1274bc2 100644 --- a/MiniKms/src/main/resources/application.properties +++ b/MiniKms/src/main/resources/application.properties @@ -1,3 +1,31 @@ spring.application.name=MiniKms -ROOT_KEY=your-base64-encoded-key-here +# Development values, won't be used in prod +ROOT_KEY=onX6qBcMY+LtSTtDSWIS8xhnCkM8v/mozZhYL56dkf8= + +spring.datasource.url=jdbc:postgresql://localhost:5432/minikms +spring.datasource.username=minikms +spring.datasource.password=minikms +spring.datasource.driver-class-name=org.postgresql.Driver + +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.open-in-view=false +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.defer-datasource-initialization=true + +spring.sql.init.mode=always + +server.port=8443 +server.ssl.key-store=classpath:keystore.p12 +server.ssl.key-store-password=minikms +server.ssl.key-store-type=PKCS12 +server.ssl.key-alias=minikms + +# Debugging +#spring.jpa.show-sql=true +#logging.level.org.springframework.security=DEBUG +#logging.level.io.jsonwebtoken=DEBUG + +# 1 hour +jwt.expiration=3600000 +jwt.secret=Dubt4z4Lba9fc82KES/2uRcxOR9LcTTwxh7UuxE4f9Q= diff --git a/MiniKms/src/main/resources/data.sql b/MiniKms/src/main/resources/data.sql new file mode 100644 index 0000000..cfa8d8f --- /dev/null +++ b/MiniKms/src/main/resources/data.sql @@ -0,0 +1,5 @@ +INSERT INTO users (id, username, password, role) +VALUES + (1, 'manager', '$2a$12$NwD7kQ.LEMq1eIkVEmSHJenFpdS9ZkDo/zrY8omrn7f23tnsCq1Ta', 'MANAGER'), + (2, 'user', '$2a$12$WhhSJhSvdc72B5qlHvYXVuTgSD/nV.c03REujUPGWNvU7Mw/pXISu', 'USER'), + (3, 'auditor', '$2a$12$lM16Cg0RCf9libTi56wXfOrypGzAuV/ylrXJZsl7q8W9W0A/O42VC', 'AUDITOR'); diff --git a/MiniKms/src/main/resources/keystore.p12 b/MiniKms/src/main/resources/keystore.p12 new file mode 100644 index 0000000..c7d48b5 Binary files /dev/null and b/MiniKms/src/main/resources/keystore.p12 differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8437019 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +services: + db: + image: postgres:18 + restart: always + + environment: + POSTGRES_DB: minikms + POSTGRES_USER: minikms + POSTGRES_PASSWORD: minikms + + ports: + - "5432:5432" + + volumes: + - pgdata:/var/lib/postgresql/data + +volumes: + pgdata: