From e1f4f5cee3e21a754772bd2b464516eca5e35e6d Mon Sep 17 00:00:00 2001 From: Dimitrije Gasic Date: Tue, 30 Sep 2025 21:33:37 +0200 Subject: [PATCH 1/4] Integrate wrapping with key generation, add key rotation, update models and api --- MiniKms/pom.xml | 16 ++++ .../controller/KeyManagementController.java | 48 ++++++++--- .../java/ftn/security/minikms/dto/KeyDTO.java | 10 ++- .../ftn/security/minikms/dto/KeyMapper.java | 9 ++ .../security/minikms/dto/KeyMetadataDTO.java | 21 +++++ .../security/minikms/entity/KeyMaterial.java | 30 +++++++ .../minikms/entity/KeyMetadataEntity.java | 55 ++++++++++++ .../minikms/entity/WrappedKeyEntity.java | 45 ++++------ .../repository/KeyMetadataRepository.java | 9 ++ .../repository/WrappedKeyRepository.java | 4 - .../ftn/security/minikms/service/AES.java | 21 ----- .../security/minikms/service/AESService.java | 16 ++++ .../ftn/security/minikms/service/HMAC.java | 22 ----- .../security/minikms/service/HMACService.java | 16 ++++ .../minikms/service/ICryptoService.java | 9 ++ .../security/minikms/service/KeyService.java | 84 +++++++++++++++---- .../ftn/security/minikms/service/RSA.java | 21 ----- .../security/minikms/service/RSAService.java | 15 ++++ .../minikms/service/RootKeyManager.java | 19 ++++- 19 files changed, 337 insertions(+), 133 deletions(-) create mode 100644 MiniKms/src/main/java/ftn/security/minikms/dto/KeyMapper.java create mode 100644 MiniKms/src/main/java/ftn/security/minikms/dto/KeyMetadataDTO.java create mode 100644 MiniKms/src/main/java/ftn/security/minikms/entity/KeyMaterial.java create mode 100644 MiniKms/src/main/java/ftn/security/minikms/entity/KeyMetadataEntity.java create mode 100644 MiniKms/src/main/java/ftn/security/minikms/repository/KeyMetadataRepository.java delete mode 100644 MiniKms/src/main/java/ftn/security/minikms/service/AES.java create mode 100644 MiniKms/src/main/java/ftn/security/minikms/service/AESService.java delete mode 100644 MiniKms/src/main/java/ftn/security/minikms/service/HMAC.java create mode 100644 MiniKms/src/main/java/ftn/security/minikms/service/HMACService.java create mode 100644 MiniKms/src/main/java/ftn/security/minikms/service/ICryptoService.java delete mode 100644 MiniKms/src/main/java/ftn/security/minikms/service/RSA.java create mode 100644 MiniKms/src/main/java/ftn/security/minikms/service/RSAService.java diff --git a/MiniKms/pom.xml b/MiniKms/pom.xml index 4a815af..7f02f1a 100644 --- a/MiniKms/pom.xml +++ b/MiniKms/pom.xml @@ -60,6 +60,17 @@ spring-security-test test + + org.mapstruct + mapstruct + 1.6.0 + + + org.mapstruct + mapstruct-processor + 1.6.0 + provided + @@ -73,6 +84,11 @@ org.projectlombok lombok + + org.mapstruct + mapstruct-processor + 1.6.0 + 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..f7b1fbd 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,57 @@ 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.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.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, KeyMapper mapper) { + this.keyService = keyService; + this.mapper = mapper; + } @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) throws GeneralSecurityException { + try { + var created = keyService.createKey(dto.getAlias(), dto.getKeyType(), dto.getAllowedOperations()); + 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); + public ResponseEntity rotateKey(@RequestBody KeyDTO dto) throws GeneralSecurityException { + try { + var created = keyService.rotateKey(dto.getId()); + return ResponseEntity.status(HttpStatus.CREATED).body(mapper.toDto(created)); + } catch (InvalidParameterException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } } - @PutMapping("/delete/{id}") - public ResponseEntity deleteKey(@PathVariable String id){ - keyService.deleteKey(id); - return new ResponseEntity<>(id, HttpStatus.OK); + + @PutMapping("/{id}") + public ResponseEntity deleteKey(@PathVariable UUID id) { + try { + keyService.deleteKey(id); + return ResponseEntity.noContent().build(); + } catch (InvalidParameterException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } } } 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..d31e2ad 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,22 @@ package ftn.security.minikms.dto; +import ftn.security.minikms.enumeration.KeyOperation; +import ftn.security.minikms.enumeration.KeyType; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.util.List; +import java.util.UUID; + @Getter @Setter @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..c407bf9 --- /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.KeyMetadataEntity; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface KeyMapper { + KeyMetadataDTO toDto(KeyMetadataEntity entity); +} 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/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/KeyMetadataEntity.java b/MiniKms/src/main/java/ftn/security/minikms/entity/KeyMetadataEntity.java new file mode 100644 index 0000000..200a8cb --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/entity/KeyMetadataEntity.java @@ -0,0 +1,55 @@ +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 java.time.Instant; +import java.util.List; +import java.util.UUID; + +@Data +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +@Entity +public class KeyMetadataEntity { + @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) + private List allowedOperations; + + private Instant createdAt; + private Instant rotatedAt; + + @OneToMany(mappedBy = "metadata", cascade = CascadeType.ALL, orphanRemoval = true) + private List versions; + + public static KeyMetadataEntity of(String alias, KeyType keyType, List allowedOperations) { + var entity = new KeyMetadataEntity(); + entity.alias = alias; + entity.primaryVersion = 0; + entity.keyType = keyType; + entity.allowedOperations = allowedOperations; + 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/WrappedKeyEntity.java b/MiniKms/src/main/java/ftn/security/minikms/entity/WrappedKeyEntity.java index 35639c3..970ca6a 100644 --- a/MiniKms/src/main/java/ftn/security/minikms/entity/WrappedKeyEntity.java +++ b/MiniKms/src/main/java/ftn/security/minikms/entity/WrappedKeyEntity.java @@ -1,49 +1,36 @@ 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; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; @Data +@EqualsAndHashCode(onlyExplicitlyIncluded = true) @Entity public class WrappedKeyEntity { + @EqualsAndHashCode.Include @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; + @Embedded + private KeyMaterial wrappedMaterial; - private Instant createdAt; + @ManyToOne + @JoinColumn(name = "key_metadata_id") + @OnDelete(action = OnDeleteAction.CASCADE) + private KeyMetadataEntity metadata; - public static WrappedKeyEntity of(UUID logicalKey, String alias, Integer version, KeyType keyType, byte[] wrappedKey, List allowedOperations) { + public static WrappedKeyEntity of(KeyMaterial wrappedMaterial, KeyMetadataEntity metadata) { 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(); + entity.version = 1; + entity.wrappedMaterial = wrappedMaterial; + entity.metadata = metadata; return entity; } -} \ No newline at end of file +} 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..100ad96 --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/repository/KeyMetadataRepository.java @@ -0,0 +1,9 @@ +package ftn.security.minikms.repository; + +import ftn.security.minikms.entity.KeyMetadataEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface KeyMetadataRepository extends JpaRepository { +} 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..9fed80b 100644 --- a/MiniKms/src/main/java/ftn/security/minikms/repository/WrappedKeyRepository.java +++ b/MiniKms/src/main/java/ftn/security/minikms/repository/WrappedKeyRepository.java @@ -3,9 +3,5 @@ import ftn.security.minikms.entity.WrappedKeyEntity; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; -import java.util.UUID; - public interface WrappedKeyRepository extends JpaRepository { - Optional findFirstByLogicalKeyOrderByVersionDesc(UUID logicalKey); } 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..6dbdbd8 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,79 @@ package ftn.security.minikms.service; +import ftn.security.minikms.entity.KeyMetadataEntity; +import ftn.security.minikms.entity.WrappedKeyEntity; +import ftn.security.minikms.enumeration.KeyOperation; +import ftn.security.minikms.enumeration.KeyType; +import ftn.security.minikms.repository.KeyMetadataRepository; +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 RootKeyManager rootKeyManager; + private final Map cryptoServices; + + public KeyService( + KeyMetadataRepository metadataRepository, + WrappedKeyRepository keyRepository, + RootKeyManager rootKeyManager) { + this.metadataRepository = metadataRepository; + this.keyRepository = keyRepository; + this.rootKeyManager = rootKeyManager; + this.cryptoServices = Map.of( + KeyType.SYMMETRIC, new AESService(), + KeyType.ASYMMETRIC, new RSAService(), + KeyType.HMAC, new HMACService() + ); + } + + public KeyMetadataEntity createKey(String alias, KeyType keyType, List allowedOperations) + throws InvalidParameterException, GeneralSecurityException { + var metadata = metadataRepository.save(KeyMetadataEntity.of(alias, keyType, allowedOperations)); + return createNewKeyVersion(metadata, 1); + } + + public void deleteKey(UUID id) throws InvalidParameterException { + if (!metadataRepository.existsById(id)) + throw new InvalidParameterException("Key with given id does not exist"); + + metadataRepository.deleteById(id); + } + + public KeyMetadataEntity rotateKey(UUID id) throws InvalidParameterException, GeneralSecurityException { + var metadata = findMetadataById(id); + var nextVersion = metadata.getPrimaryVersion() + 1; + return createNewKeyVersion(metadata, nextVersion); } - public void deleteKey(String Id){ - //delete from database + + private KeyMetadataEntity createNewKeyVersion(KeyMetadataEntity 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(WrappedKeyEntity.of(material, metadata)); + metadata.updatePrimaryVersion(key.getVersion()); // Set the latest version as primary + return metadataRepository.save(metadata); } - 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 KeyMetadataEntity findMetadataById(UUID id) throws InvalidParameterException { + return metadataRepository.findById(id).orElseThrow(() -> + new InvalidParameterException("Key with given id 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); + } } From c8f274c72cca69738ccb8dc7422f62d7933c9251 Mon Sep 17 00:00:00 2001 From: Dimitrije Gasic Date: Tue, 30 Sep 2025 23:38:30 +0200 Subject: [PATCH 2/4] Add mapper, security config, docker compose and update app properties --- MiniKms/pom.xml | 26 +++++++++++-------- .../config/JsonDeserializationConfig.java | 15 +++++++++++ .../minikms/config/SecurityConfig.java | 19 ++++++++++++++ .../controller/KeyManagementController.java | 10 +++---- .../java/ftn/security/minikms/dto/KeyDTO.java | 9 ++----- .../ftn/security/minikms/dto/KeyMapper.java | 4 +-- .../security/minikms/enumeration/KeyType.java | 4 ++- .../security/minikms/service/KeyService.java | 2 +- .../src/main/resources/application.properties | 12 ++++++++- docker-compose.yml | 18 +++++++++++++ 10 files changed, 91 insertions(+), 28 deletions(-) create mode 100644 MiniKms/src/main/java/ftn/security/minikms/config/JsonDeserializationConfig.java create mode 100644 MiniKms/src/main/java/ftn/security/minikms/config/SecurityConfig.java create mode 100644 docker-compose.yml diff --git a/MiniKms/pom.xml b/MiniKms/pom.xml index 7f02f1a..e085f81 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,21 +49,30 @@ org.springframework.boot spring-boot-starter-data-jpa + + org.springframework.boot + spring-boot-starter-web + org.springframework.security spring-security-test test - org.mapstruct - mapstruct - 1.6.0 + org.postgresql + postgresql + 42.7.8 + + + org.projectlombok + lombok + 1.18.42 + provided org.mapstruct - mapstruct-processor + mapstruct 1.6.0 - provided @@ -83,6 +86,7 @@ org.projectlombok lombok + 1.18.40 org.mapstruct 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..cdcfc7c --- /dev/null +++ b/MiniKms/src/main/java/ftn/security/minikms/config/SecurityConfig.java @@ -0,0 +1,19 @@ +package ftn.security.minikms.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests((authz) -> authz + .anyRequest().permitAll() + ); + return http.build(); + } +} 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 f7b1fbd..b3f2b87 100644 --- a/MiniKms/src/main/java/ftn/security/minikms/controller/KeyManagementController.java +++ b/MiniKms/src/main/java/ftn/security/minikms/controller/KeyManagementController.java @@ -3,6 +3,7 @@ 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; @@ -19,9 +20,9 @@ public class KeyManagementController { private final KeyMapper mapper; @Autowired - public KeyManagementController(KeyService keyService, KeyMapper mapper) { + public KeyManagementController(KeyService keyService) { this.keyService = keyService; - this.mapper = mapper; + this.mapper = Mappers.getMapper(KeyMapper.class); } @PostMapping("/create") @@ -44,8 +45,8 @@ public ResponseEntity rotateKey(@RequestBody KeyDTO dto) throws GeneralSecuri } } - @PutMapping("/{id}") - public ResponseEntity deleteKey(@PathVariable UUID id) { + @DeleteMapping("/{id}") + public ResponseEntity deleteKey(@PathVariable UUID id) { try { keyService.deleteKey(id); return ResponseEntity.noContent().build(); @@ -53,5 +54,4 @@ public ResponseEntity deleteKey(@PathVariable UUID id) { return ResponseEntity.badRequest().body(e.getMessage()); } } - } 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 d31e2ad..35e33b3 100644 --- a/MiniKms/src/main/java/ftn/security/minikms/dto/KeyDTO.java +++ b/MiniKms/src/main/java/ftn/security/minikms/dto/KeyDTO.java @@ -2,18 +2,13 @@ import ftn.security.minikms.enumeration.KeyOperation; import ftn.security.minikms.enumeration.KeyType; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import java.util.List; import java.util.UUID; -@Getter -@Setter +@Data @NoArgsConstructor -@AllArgsConstructor public class KeyDTO { private UUID id; private String alias; diff --git a/MiniKms/src/main/java/ftn/security/minikms/dto/KeyMapper.java b/MiniKms/src/main/java/ftn/security/minikms/dto/KeyMapper.java index c407bf9..84f780d 100644 --- a/MiniKms/src/main/java/ftn/security/minikms/dto/KeyMapper.java +++ b/MiniKms/src/main/java/ftn/security/minikms/dto/KeyMapper.java @@ -3,7 +3,7 @@ import ftn.security.minikms.entity.KeyMetadataEntity; import org.mapstruct.Mapper; -@Mapper(componentModel = "spring") +@Mapper public interface KeyMapper { - KeyMetadataDTO toDto(KeyMetadataEntity entity); + KeyMetadataDTO toDto(KeyMetadataEntity key); } diff --git a/MiniKms/src/main/java/ftn/security/minikms/enumeration/KeyType.java b/MiniKms/src/main/java/ftn/security/minikms/enumeration/KeyType.java index 8a6d2eb..fc7afc3 100644 --- a/MiniKms/src/main/java/ftn/security/minikms/enumeration/KeyType.java +++ b/MiniKms/src/main/java/ftn/security/minikms/enumeration/KeyType.java @@ -1,5 +1,7 @@ package ftn.security.minikms.enumeration; +import com.fasterxml.jackson.annotation.JsonCreator; + public enum KeyType { - SYMMETRIC, ASYMMETRIC, HMAC + SYMMETRIC, ASYMMETRIC, HMAC; } 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 6dbdbd8..c6d77d9 100644 --- a/MiniKms/src/main/java/ftn/security/minikms/service/KeyService.java +++ b/MiniKms/src/main/java/ftn/security/minikms/service/KeyService.java @@ -68,7 +68,7 @@ private KeyMetadataEntity createNewKeyVersion(KeyMetadataEntity metadata, Intege material.setKey(wrapped); var key = keyRepository.save(WrappedKeyEntity.of(material, metadata)); - metadata.updatePrimaryVersion(key.getVersion()); // Set the latest version as primary + metadata.updatePrimaryVersion(version); // Set the latest version as primary return metadataRepository.save(metadata); } diff --git a/MiniKms/src/main/resources/application.properties b/MiniKms/src/main/resources/application.properties index d3bcf13..0638d99 100644 --- a/MiniKms/src/main/resources/application.properties +++ b/MiniKms/src/main/resources/application.properties @@ -1,3 +1,13 @@ 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.show-sql=true +spring.jpa.open-in-view=false +spring.jpa.properties.hibernate.format_sql=true 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: From 8e3c36bf17f7cd234b92325588333b7e470ffea7 Mon Sep 17 00:00:00 2001 From: Dimitrije Gasic Date: Wed, 1 Oct 2025 02:32:32 +0200 Subject: [PATCH 3/4] Add ssl, users, sql init script, jwt auth and rbac --- MiniKms/pom.xml | 17 +++++ .../minikms/config/SecurityConfig.java | 70 ++++++++++++++++-- .../minikms/controller/AuthController.java | 40 ++++++++++ .../controller/KeyManagementController.java | 19 +++-- .../ftn/security/minikms/dto/AuthDTO.java | 9 +++ .../ftn/security/minikms/dto/KeyMapper.java | 4 +- .../ftn/security/minikms/dto/TokenDTO.java | 10 +++ ...eyMetadataEntity.java => KeyMetadata.java} | 18 ++++- .../ftn/security/minikms/entity/User.java | 24 ++++++ ...{WrappedKeyEntity.java => WrappedKey.java} | 11 +-- .../security/minikms/enumeration/KeyType.java | 4 +- .../minikms/enumeration/UserRole.java | 5 ++ .../repository/KeyMetadataRepository.java | 7 +- .../minikms/repository/UserRepository.java | 10 +++ .../repository/WrappedKeyRepository.java | 4 +- .../security/minikms/service/KeyService.java | 37 +++++---- .../service/auth/JwtAuthenticationFilter.java | 62 ++++++++++++++++ .../minikms/service/auth/JwtService.java | 32 ++++++++ .../auth/MiniKmsUserDetailsService.java | 35 +++++++++ .../src/main/resources/application.properties | 20 ++++- MiniKms/src/main/resources/data.sql | 5 ++ MiniKms/src/main/resources/keystore.p12 | Bin 0 -> 2714 bytes 22 files changed, 399 insertions(+), 44 deletions(-) create mode 100644 MiniKms/src/main/java/ftn/security/minikms/controller/AuthController.java create mode 100644 MiniKms/src/main/java/ftn/security/minikms/dto/AuthDTO.java create mode 100644 MiniKms/src/main/java/ftn/security/minikms/dto/TokenDTO.java rename MiniKms/src/main/java/ftn/security/minikms/entity/{KeyMetadataEntity.java => KeyMetadata.java} (69%) create mode 100644 MiniKms/src/main/java/ftn/security/minikms/entity/User.java rename MiniKms/src/main/java/ftn/security/minikms/entity/{WrappedKeyEntity.java => WrappedKey.java} (72%) create mode 100644 MiniKms/src/main/java/ftn/security/minikms/enumeration/UserRole.java create mode 100644 MiniKms/src/main/java/ftn/security/minikms/repository/UserRepository.java create mode 100644 MiniKms/src/main/java/ftn/security/minikms/service/auth/JwtAuthenticationFilter.java create mode 100644 MiniKms/src/main/java/ftn/security/minikms/service/auth/JwtService.java create mode 100644 MiniKms/src/main/java/ftn/security/minikms/service/auth/MiniKmsUserDetailsService.java create mode 100644 MiniKms/src/main/resources/data.sql create mode 100644 MiniKms/src/main/resources/keystore.p12 diff --git a/MiniKms/pom.xml b/MiniKms/pom.xml index e085f81..25837cc 100644 --- a/MiniKms/pom.xml +++ b/MiniKms/pom.xml @@ -58,6 +58,23 @@ 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 diff --git a/MiniKms/src/main/java/ftn/security/minikms/config/SecurityConfig.java b/MiniKms/src/main/java/ftn/security/minikms/config/SecurityConfig.java index cdcfc7c..3bbb21d 100644 --- a/MiniKms/src/main/java/ftn/security/minikms/config/SecurityConfig.java +++ b/MiniKms/src/main/java/ftn/security/minikms/config/SecurityConfig.java @@ -1,19 +1,79 @@ 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.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.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 { - http.csrf(AbstractHttpConfigurer::disable) - .authorizeHttpRequests((authz) -> authz - .anyRequest().permitAll() - ); - return http.build(); + return http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth + .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("*")); + config.setAllowCredentials(true); + + 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 b3f2b87..e821ae7 100644 --- a/MiniKms/src/main/java/ftn/security/minikms/controller/KeyManagementController.java +++ b/MiniKms/src/main/java/ftn/security/minikms/controller/KeyManagementController.java @@ -11,6 +11,7 @@ import java.security.GeneralSecurityException; import java.security.InvalidParameterException; +import java.security.Principal; import java.util.UUID; @RestController @@ -26,9 +27,11 @@ public KeyManagementController(KeyService keyService) { } @PostMapping("/create") - public ResponseEntity createKey(@RequestBody KeyDTO dto) throws GeneralSecurityException { + 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()); + 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()); @@ -36,9 +39,11 @@ public ResponseEntity createKey(@RequestBody KeyDTO dto) throws GeneralSecuri } @PostMapping("/rotate") - public ResponseEntity rotateKey(@RequestBody KeyDTO dto) throws GeneralSecurityException { + public ResponseEntity rotateKey(@RequestBody KeyDTO dto, Principal principal) throws GeneralSecurityException { + var username = principal.getName(); + try { - var created = keyService.rotateKey(dto.getId()); + 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()); @@ -46,9 +51,11 @@ public ResponseEntity rotateKey(@RequestBody KeyDTO dto) throws GeneralSecuri } @DeleteMapping("/{id}") - public ResponseEntity deleteKey(@PathVariable UUID id) { + public ResponseEntity deleteKey(@PathVariable UUID id, Principal principal) { + var username = principal.getName(); + try { - keyService.deleteKey(id); + 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/KeyMapper.java b/MiniKms/src/main/java/ftn/security/minikms/dto/KeyMapper.java index 84f780d..aa08e42 100644 --- a/MiniKms/src/main/java/ftn/security/minikms/dto/KeyMapper.java +++ b/MiniKms/src/main/java/ftn/security/minikms/dto/KeyMapper.java @@ -1,9 +1,9 @@ package ftn.security.minikms.dto; -import ftn.security.minikms.entity.KeyMetadataEntity; +import ftn.security.minikms.entity.KeyMetadata; import org.mapstruct.Mapper; @Mapper public interface KeyMapper { - KeyMetadataDTO toDto(KeyMetadataEntity key); + KeyMetadataDTO toDto(KeyMetadata key); } 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/KeyMetadataEntity.java b/MiniKms/src/main/java/ftn/security/minikms/entity/KeyMetadata.java similarity index 69% rename from MiniKms/src/main/java/ftn/security/minikms/entity/KeyMetadataEntity.java rename to MiniKms/src/main/java/ftn/security/minikms/entity/KeyMetadata.java index 200a8cb..95b5109 100644 --- a/MiniKms/src/main/java/ftn/security/minikms/entity/KeyMetadataEntity.java +++ b/MiniKms/src/main/java/ftn/security/minikms/entity/KeyMetadata.java @@ -5,6 +5,8 @@ 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; @@ -13,7 +15,8 @@ @Data @EqualsAndHashCode(onlyExplicitlyIncluded = true) @Entity -public class KeyMetadataEntity { +@Table(name = "keys") +public class KeyMetadata { @EqualsAndHashCode.Include @Id @GeneratedValue(strategy = GenerationType.UUID) @@ -27,20 +30,27 @@ public class KeyMetadataEntity { @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; + private List versions; - public static KeyMetadataEntity of(String alias, KeyType keyType, List allowedOperations) { - var entity = new KeyMetadataEntity(); + 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; } 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/WrappedKeyEntity.java b/MiniKms/src/main/java/ftn/security/minikms/entity/WrappedKey.java similarity index 72% rename from MiniKms/src/main/java/ftn/security/minikms/entity/WrappedKeyEntity.java rename to MiniKms/src/main/java/ftn/security/minikms/entity/WrappedKey.java index 970ca6a..143c64e 100644 --- a/MiniKms/src/main/java/ftn/security/minikms/entity/WrappedKeyEntity.java +++ b/MiniKms/src/main/java/ftn/security/minikms/entity/WrappedKey.java @@ -10,7 +10,8 @@ @Data @EqualsAndHashCode(onlyExplicitlyIncluded = true) @Entity -public class WrappedKeyEntity { +@Table(name = "key_versions") +public class WrappedKey { @EqualsAndHashCode.Include @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -22,12 +23,12 @@ public class WrappedKeyEntity { private KeyMaterial wrappedMaterial; @ManyToOne - @JoinColumn(name = "key_metadata_id") + @JoinColumn(name = "key_metadata_id", nullable = false) @OnDelete(action = OnDeleteAction.CASCADE) - private KeyMetadataEntity metadata; + private KeyMetadata metadata; - public static WrappedKeyEntity of(KeyMaterial wrappedMaterial, KeyMetadataEntity metadata) { - var entity = new WrappedKeyEntity(); + public static WrappedKey of(KeyMaterial wrappedMaterial, KeyMetadata metadata) { + var entity = new WrappedKey(); entity.version = 1; entity.wrappedMaterial = wrappedMaterial; entity.metadata = metadata; diff --git a/MiniKms/src/main/java/ftn/security/minikms/enumeration/KeyType.java b/MiniKms/src/main/java/ftn/security/minikms/enumeration/KeyType.java index fc7afc3..8a6d2eb 100644 --- a/MiniKms/src/main/java/ftn/security/minikms/enumeration/KeyType.java +++ b/MiniKms/src/main/java/ftn/security/minikms/enumeration/KeyType.java @@ -1,7 +1,5 @@ package ftn.security.minikms.enumeration; -import com.fasterxml.jackson.annotation.JsonCreator; - public enum KeyType { - SYMMETRIC, ASYMMETRIC, HMAC; + SYMMETRIC, ASYMMETRIC, HMAC } 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 index 100ad96..da1e440 100644 --- a/MiniKms/src/main/java/ftn/security/minikms/repository/KeyMetadataRepository.java +++ b/MiniKms/src/main/java/ftn/security/minikms/repository/KeyMetadataRepository.java @@ -1,9 +1,12 @@ package ftn.security.minikms.repository; -import ftn.security.minikms.entity.KeyMetadataEntity; +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 { +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 9fed80b..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,7 +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; -public interface WrappedKeyRepository extends JpaRepository { +public interface WrappedKeyRepository extends JpaRepository { } 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 c6d77d9..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,10 +1,12 @@ package ftn.security.minikms.service; -import ftn.security.minikms.entity.KeyMetadataEntity; -import ftn.security.minikms.entity.WrappedKeyEntity; +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; @@ -18,15 +20,19 @@ public class KeyService { 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(), @@ -35,26 +41,29 @@ KeyType.HMAC, new HMACService() ); } - public KeyMetadataEntity createKey(String alias, KeyType keyType, List allowedOperations) + public KeyMetadata createKey(String alias, KeyType keyType, List allowedOperations, String username) throws InvalidParameterException, GeneralSecurityException { - var metadata = metadataRepository.save(KeyMetadataEntity.of(alias, keyType, allowedOperations)); + var user = findUserByUsername(username); + var metadata = metadataRepository.save(KeyMetadata.of(alias, keyType, allowedOperations, user)); return createNewKeyVersion(metadata, 1); } - public void deleteKey(UUID id) throws InvalidParameterException { - if (!metadataRepository.existsById(id)) - throw new InvalidParameterException("Key with given id does not exist"); + public void deleteKey(UUID id, String username) throws InvalidParameterException { + if (!metadataRepository.existsByIdAndUserUsername(id, username)) + throw new InvalidParameterException(NOT_AUTHORIZED_MSG); metadataRepository.deleteById(id); } - public KeyMetadataEntity rotateKey(UUID id) throws InvalidParameterException, GeneralSecurityException { - var metadata = findMetadataById(id); + 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); } - private KeyMetadataEntity createNewKeyVersion(KeyMetadataEntity metadata, Integer version) throws GeneralSecurityException { + private KeyMetadata createNewKeyVersion(KeyMetadata metadata, Integer version) throws GeneralSecurityException { var id = metadata.getId(); var keyType = metadata.getKeyType(); @@ -67,13 +76,13 @@ private KeyMetadataEntity createNewKeyVersion(KeyMetadataEntity metadata, Intege material.setKey(wrapped); - var key = keyRepository.save(WrappedKeyEntity.of(material, metadata)); + var key = keyRepository.save(WrappedKey.of(material, metadata)); metadata.updatePrimaryVersion(version); // Set the latest version as primary return metadataRepository.save(metadata); } - private KeyMetadataEntity findMetadataById(UUID id) throws InvalidParameterException { - return metadataRepository.findById(id).orElseThrow(() -> - new InvalidParameterException("Key with given id does not exist")); + 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/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 0638d99..1274bc2 100644 --- a/MiniKms/src/main/resources/application.properties +++ b/MiniKms/src/main/resources/application.properties @@ -7,7 +7,25 @@ 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.show-sql=true 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 0000000000000000000000000000000000000000..c7d48b5d0351d7d7c8f02448dc691a27ab9e224c GIT binary patch literal 2714 zcma);S5y;-5{47fBp@9`I?@D!Bp}jz3q_YEQlyAfC7~mNLI{dLsDkwD5*DIp0BNEi zRU$=$%lMA_A8^_nzHzpYDB_Gc*4`GmqaHBp#m41Oy@Ra6KrqTs%5{_cRa& zEW*Q4U_4ymM3zP3Aus>Sf{?&?2=PSDJBcSymj7(A!huXhc!=JKsD4;SO zr)5RTgL&3$bspQIpEx zATUr4%yIra#FdA#-HJf3vPpj(x(~*KKTFX+Y=(72ykj+tG)peIp?_&HoM%wZM>8mG z?XtfMJ${umh$K+I+W1VMAkdTk>y|}cLFU_d|eL-p7!SSy9zVu0XGd;6svnq6V zUo@Ik!S2Da51i5Zf&*DNDHn;^9&(w@IZeTS)oKQ6Kv*NN4e{|yTqQs!e!oMJFdVG# zqI{s4+V;h?TE*0N>rxM0js+v8qN!m{W%-Y8FQau#GE}M3j$#zGe8r48Njx@C$@1q% zm}Skl;1lN$76_A{2ocK`D~qiPYz7!>pt;hXjm?j>|5UvDQ2N1emEffdlYX2m^`cXK zcT@k&cssaC>~tQxo~H{?`#rDY@xZgIatT|$H@ktALVFdGks=urF#BODK3Y;opzg!d z@*1Ch=W^^9l9HFghR9KINv{cCfq2eDCFk&_^$$HykG83+3;3#aFXp8V-tOB% z>tFvwAm2}^a}GBm^augME)cU+IH`b)GvbXRJcrN4P3yU2k}(qxDW%~AwZ->k5~E)q z%e+m5=#p`UPHzT$I7|uxIvm|ZSaO!c4c6CHdup##OSzf5XNK>u@%F#FJkr!MwEMAFBErLhfDM%G!k!S*rfgJw}Fu{nc_pPfmT+V)}EsEoe@GcAXXNvtN6! z&EqWTk7puRiEa^r2?`Q3oc0F1z_+OjjW+lBlY*POfgellOOR$#SQ_Y_gNTWBK_bkHBg7TP=u5imqq z2$pi?`fzZj1_JlpB6qi^cuQR;kueb;A$eTnr!!sMB@?HQh7>0fBJZgwQ2*G z_hpos{T5Ycpc-z2sH2KaezUbvHHm3Zi-hP}3ezpKdilFSd;@q_bMiCb7cI6r4MEFq z;`aiG-yXRH`~E)LJM!Gz$$$=Ud6|C;hJERNttoma%~$!TEmPS46^K-O|0fOQ#CIH6 zgU_ZL5uKUj}yt22?#hz*Zy&U{|nSjqtu~`$JdyWJ$}>;NP47;DqIly ze?a}p`I2wLRzoEtX4Obj-oOus2hk;b4!;Odrk8M-!`CaIv9~FH zbGWQc;x-RPN{iJ_P&&AUdnfbt)Oo?rw6KFzfq*!W?)kY4;eMxnxwA@IvlAZ!+;nL5 z(xyXb$*DYA!9Fso&+%%D}TCllO5J@byM*g=>8y-)7PlUT_Ktaq#z zj^N0>ZuNyQFuP2~m`js9hsbNA3)U;m6w>ECng;jOMCix4QEEoKP~QqBemK_({xs}A!|RSPmGg2|F^zTdGD(F=-T2!RHMUc*Bt zh+T9>!}5pH!Q$WCR`6% z7DH2WJ#i(H9>&4KLCfeMy{JC`=-0kXB%9sb)0eNw8z7PsMyp|d+OGM#^c;Z_$@fjk z=FOM!4E{hvozmq!h4CD|gj?EBLpFi;2&t7sa)6-4j8dENJI(C!L(Ckzr?bqGOSD_t zk606L$fKj7h)6S$FWiN}47+skc`~0eiZg{Q%|!^I`bu*!L9d$Cc9_bFS{HO*%y?i+ zt4}P}Lvj`j7#cyg7Eu$*-=&70ayg@KLF^7-orgc$Bl-*!$LmyyoKTrr)-y$*Dq`eQ zx%G1#Z)+4@g$V5u>viuAgIV^@{jl)!R0&M8$K7Z@po(2i6n!37<o%9YQ*Z=c1Jd~F0%hw84XX3SJV?dVsiESx z;%q5=HnI6!ZEWx_o5y!j*3zk)K+TP*Eabcnt!ny1C(5BN66}#!d5hbI)Sfq2mc1~# z5_Fl*e@L=bJG6xpv(Q3BzPLO8?P-i@6W(RyLVZ+0e`)1JncwvQ_oRH%BBj$CaZXcs zjf0?{_PuD7HF98I*DZ--Ogu#2J`cIY-XxoWu& zly^xs-%f)5xRN{Lk_2tzI>S~nR;a$LvJADIB7mo3_r0h2&9}2Nv0~v}jJBpLk{K6b ziBi|KlcbRmyt8F8yDZv^5{H!J^k>M+NEGt)-~SpD5CQ;;Ehp7Tr$qFEmRy~19edU< uXq_QGj7KpG=kGs}!8B@Tfq81v=-M=8@6do5P8X9~FSs9jS-<-WG5-SV!sPk@ literal 0 HcmV?d00001 From 7cd0b24a5b3e2026631e40c513bf5dc6969ee801 Mon Sep 17 00:00:00 2001 From: Dimitrije Gasic Date: Wed, 1 Oct 2025 02:45:30 +0200 Subject: [PATCH 4/4] Additionaly restrict session policy and headers list --- .../security/minikms/config/SecurityConfig.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/MiniKms/src/main/java/ftn/security/minikms/config/SecurityConfig.java b/MiniKms/src/main/java/ftn/security/minikms/config/SecurityConfig.java index 3bbb21d..e2839bf 100644 --- a/MiniKms/src/main/java/ftn/security/minikms/config/SecurityConfig.java +++ b/MiniKms/src/main/java/ftn/security/minikms/config/SecurityConfig.java @@ -8,10 +8,12 @@ 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; @@ -39,13 +41,23 @@ public SecurityConfig( @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, @@ -69,8 +81,8 @@ 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("*")); - config.setAllowCredentials(true); + config.setAllowedHeaders(List.of("Authorization", "Content-Type")); + config.setAllowCredentials(false); var source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config);