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: