From 01511c4311e08f650a71eda696dd75f97e47a18b Mon Sep 17 00:00:00 2001 From: sayf Date: Fri, 20 Dec 2024 13:35:42 +0100 Subject: [PATCH 01/30] POM.xml organizing --- pom.xml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pom.xml b/pom.xml index 8ebf211..4f068c7 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,10 @@ Demo project for Spring Boot 21 + 3.1.0 + 4.8.6 + 1.79 + 3.6.0 @@ -33,7 +37,7 @@ jakarta.validation jakarta.validation-api - 3.1.0 + ${jakarta.validation-api.version} org.springframework.boot @@ -48,13 +52,13 @@ com.github.spotbugs spotbugs-annotations - 4.8.6 + ${spotbugs-annotations.version} compile org.bouncycastle bcprov-jdk18on - 1.79 + ${bcprov-jdk18on.version} @@ -117,7 +121,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.8.6.6 + ${spotbugs-annotations.version}.6 spotbugs-exclude.xml @@ -134,7 +138,7 @@ org.apache.maven.plugins maven-checkstyle-plugin - 3.6.0 + ${maven-checkstyle-plugin.version} checkstyle.xml true @@ -157,9 +161,6 @@ org.springframework.boot spring-boot-maven-plugin - - - org.projectlombok @@ -197,7 +198,7 @@ org.apache.maven.plugins maven-checkstyle-plugin - 3.6.0 + ${maven-checkstyle-plugin.version} From 175e21a6b609d30ae69d9383746d8a4861bcafcb Mon Sep 17 00:00:00 2001 From: sayf Date: Fri, 20 Dec 2024 16:46:37 +0100 Subject: [PATCH 02/30] first step refactoring --- .../PseudoniemenServiceProperties.java | 18 ++-- .../ictu/service/AesGcmCryptographerImpl.java | 87 +++++++++---------- .../exception/TokenPrivateKeyException.java | 9 ++ .../java/nl/ictu/utils/Base64Wrapper.java | 28 ++++++ .../java/nl/ictu/utils/MessageDigestUtil.java | 17 ++++ .../ictu/service/TestAesGcmCryptographer.java | 25 ++++-- 6 files changed, 126 insertions(+), 58 deletions(-) create mode 100644 src/main/java/nl/ictu/service/exception/TokenPrivateKeyException.java create mode 100644 src/main/java/nl/ictu/utils/Base64Wrapper.java create mode 100644 src/main/java/nl/ictu/utils/MessageDigestUtil.java diff --git a/src/main/java/nl/ictu/configuration/PseudoniemenServiceProperties.java b/src/main/java/nl/ictu/configuration/PseudoniemenServiceProperties.java index ae00690..9fb7372 100644 --- a/src/main/java/nl/ictu/configuration/PseudoniemenServiceProperties.java +++ b/src/main/java/nl/ictu/configuration/PseudoniemenServiceProperties.java @@ -1,21 +1,29 @@ package nl.ictu.configuration; -import lombok.Getter; -import lombok.Setter; +import jakarta.annotation.PostConstruct; +import lombok.Data; import lombok.experimental.Accessors; +import nl.ictu.service.exception.TokenPrivateKeyException; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; @Component @ConfigurationProperties(prefix = "pseudoniemenservice") -@Getter -@Setter +@Data @Accessors(chain = true) -public class PseudoniemenServiceProperties { +public final class PseudoniemenServiceProperties { private String tokenPrivateKey; private String identifierPrivateKey; + @PostConstruct + public void validate() { + + if (!StringUtils.hasText(tokenPrivateKey)) { + throw new TokenPrivateKeyException("Please set a private token key"); + } + } } diff --git a/src/main/java/nl/ictu/service/AesGcmCryptographerImpl.java b/src/main/java/nl/ictu/service/AesGcmCryptographerImpl.java index 21bf2da..a3d76da 100644 --- a/src/main/java/nl/ictu/service/AesGcmCryptographerImpl.java +++ b/src/main/java/nl/ictu/service/AesGcmCryptographerImpl.java @@ -1,12 +1,13 @@ package nl.ictu.service; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import lombok.SneakyThrows; -import nl.ictu.configuration.PseudoniemenServiceProperties; -import nl.ictu.utils.ByteArrayUtils; -import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; +import static nl.ictu.service.AESHelper.IV_LENGTH; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; @@ -14,15 +15,14 @@ import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; -import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; -import java.util.Base64; - -import static nl.ictu.service.AESHelper.IV_LENGTH; +import lombok.SneakyThrows; +import nl.ictu.configuration.PseudoniemenServiceProperties; +import nl.ictu.service.exception.TokenPrivateKeyException; +import nl.ictu.utils.Base64Wrapper; +import nl.ictu.utils.ByteArrayUtils; +import nl.ictu.utils.MessageDigestUtil; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; /** * Advanced Encryption Standard Galois/Counter Mode (AES-GCM). @@ -32,32 +32,31 @@ @Service public class AesGcmCryptographerImpl implements AesGcmCryptographer { - //private SecretKey secretKey; - - private final Base64.Encoder base64Encoder = Base64.getEncoder(); + private final Base64Wrapper base64Wrapper; - private final Base64.Decoder base64Decoder = Base64.getDecoder(); - - private final MessageDigest sha256Digest; + private final MessageDigestUtil messageDigestUtil; private final PseudoniemenServiceProperties pseudoniemenServiceProperties; @SuppressFBWarnings("CT_CONSTRUCTOR_THROW") @SneakyThrows - public AesGcmCryptographerImpl(final PseudoniemenServiceProperties pseudoniemenServicePropertiesArg) { - - pseudoniemenServiceProperties = pseudoniemenServicePropertiesArg; + public AesGcmCryptographerImpl( + final PseudoniemenServiceProperties properties, + final Base64Wrapper base64, + final MessageDigestUtil messageDigest) { - sha256Digest = MessageDigest.getInstance("SHA-256"); + this.pseudoniemenServiceProperties = properties; + this.base64Wrapper = base64; + this.messageDigestUtil = messageDigest; if (!StringUtils.hasText(pseudoniemenServiceProperties.getTokenPrivateKey())) { - throw new RuntimeException("Please set a private token key"); + throw new TokenPrivateKeyException("Please set a private token key"); } - } @Override - public String encrypt(final String plaintext, final String salt) throws IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { + public String encrypt(final String plaintext, final String salt) + throws IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { final Cipher cipher = AESHelper.createCipher(); @@ -67,39 +66,40 @@ public String encrypt(final String plaintext, final String salt) throws IllegalB cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec); - byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); - - byte[] encryptedWithIV = new byte[IV_LENGTH + ciphertext.length]; + final var ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); - System.arraycopy(gcmParameterSpec.getIV(), 0, encryptedWithIV, 0, IV_LENGTH); - System.arraycopy(ciphertext, 0, encryptedWithIV, IV_LENGTH, ciphertext.length); + final var gcmIV = gcmParameterSpec.getIV(); + final var encryptedWithIV = ByteArrayUtils.concat(gcmIV, ciphertext); - return base64Encoder.encodeToString(encryptedWithIV); + return base64Wrapper.encodeToString(encryptedWithIV); } private SecretKey createSecretKey(final String salt) { - byte[] keyBytes = base64Decoder.decode(pseudoniemenServiceProperties.getTokenPrivateKey()); + final var keyBytes = base64Wrapper.decode( + pseudoniemenServiceProperties.getTokenPrivateKey()); - byte[] saltBytes = salt.getBytes(StandardCharsets.UTF_8); + final var saltBytes = salt.getBytes(StandardCharsets.UTF_8); - byte[] salterSecretBytes = ByteArrayUtils.concat(keyBytes, saltBytes); + final var salterSecretBytes = ByteArrayUtils.concat(keyBytes, saltBytes); - byte[] key = sha256Digest.digest(salterSecretBytes); + final var key = messageDigestUtil.getMessageDigestSha256().digest(salterSecretBytes); return new SecretKeySpec(key, "AES"); } @Override - public String decrypt(final String ciphertextWithIv, final String salt) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + public String decrypt(final String ciphertextWithIv, final String salt) + throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { final Cipher cipher = AESHelper.createCipher(); - final byte[] encryptedWithIV = base64Decoder.decode(ciphertextWithIv); + final var encryptedWithIV = base64Wrapper.decode(ciphertextWithIv); - byte[] iv = Arrays.copyOfRange(encryptedWithIV, 0, IV_LENGTH); - byte[] ciphertext = Arrays.copyOfRange(encryptedWithIV, IV_LENGTH, encryptedWithIV.length); + final var iv = Arrays.copyOfRange(encryptedWithIV, 0, IV_LENGTH); + final var ciphertext = Arrays.copyOfRange(encryptedWithIV, IV_LENGTH, + encryptedWithIV.length); final GCMParameterSpec gcmParameterSpec = AESHelper.createIVfromValues(iv); @@ -107,10 +107,9 @@ public String decrypt(final String ciphertextWithIv, final String salt) throws N cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec); - byte[] decryptedText = cipher.doFinal(ciphertext); + final var decryptedText = cipher.doFinal(ciphertext); return new String(decryptedText, StandardCharsets.UTF_8); } - } diff --git a/src/main/java/nl/ictu/service/exception/TokenPrivateKeyException.java b/src/main/java/nl/ictu/service/exception/TokenPrivateKeyException.java new file mode 100644 index 0000000..e844d27 --- /dev/null +++ b/src/main/java/nl/ictu/service/exception/TokenPrivateKeyException.java @@ -0,0 +1,9 @@ +package nl.ictu.service.exception; + +public class TokenPrivateKeyException extends RuntimeException { + + public TokenPrivateKeyException(final String message) { + + super(message); + } +} diff --git a/src/main/java/nl/ictu/utils/Base64Wrapper.java b/src/main/java/nl/ictu/utils/Base64Wrapper.java new file mode 100644 index 0000000..0907b97 --- /dev/null +++ b/src/main/java/nl/ictu/utils/Base64Wrapper.java @@ -0,0 +1,28 @@ +package nl.ictu.utils; + +import java.util.Base64; +import java.util.Base64.Decoder; +import java.util.Base64.Encoder; +import org.springframework.stereotype.Component; + +@Component +public final class Base64Wrapper { + + public static final Decoder DECODER = Base64.getDecoder(); + public static final Encoder ENCODER = Base64.getEncoder(); + + public byte[] decode(final String toDecode) { + + return DECODER.decode(toDecode); + } + + public byte[] encode(final byte[] toEncode) { + + return ENCODER.encode(toEncode); + } + + public String encodeToString(final byte[] toEncode) { + + return ENCODER.encodeToString(toEncode); + } +} diff --git a/src/main/java/nl/ictu/utils/MessageDigestUtil.java b/src/main/java/nl/ictu/utils/MessageDigestUtil.java new file mode 100644 index 0000000..6ea9d15 --- /dev/null +++ b/src/main/java/nl/ictu/utils/MessageDigestUtil.java @@ -0,0 +1,17 @@ +package nl.ictu.utils; + +import java.security.MessageDigest; +import lombok.SneakyThrows; +import org.springframework.stereotype.Component; + +@Component +public final class MessageDigestUtil { + + public static final String SHA_256 = "SHA-256"; + + @SneakyThrows + public MessageDigest getMessageDigestSha256() { + + return MessageDigest.getInstance(SHA_256); + } +} diff --git a/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java b/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java index b1e691a..8505386 100644 --- a/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java +++ b/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java @@ -1,16 +1,17 @@ package nl.ictu.service; -import lombok.extern.slf4j.Slf4j; -import nl.ictu.configuration.PseudoniemenServiceProperties; -import org.junit.jupiter.api.Test; -import org.springframework.test.context.ActiveProfiles; +import static org.assertj.core.api.Assertions.assertThat; import java.util.Arrays; import java.util.HashSet; import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; +import lombok.extern.slf4j.Slf4j; +import nl.ictu.configuration.PseudoniemenServiceProperties; +import nl.ictu.utils.Base64Wrapper; +import nl.ictu.utils.MessageDigestUtil; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; /** * Class for tesing {@link AesGcmCryptographerImpl} @@ -20,9 +21,15 @@ @ActiveProfiles("test") public class TestAesGcmCryptographer { - private final AesGcmCryptographer aesGcmCryptographer = new AesGcmCryptographerImpl(new PseudoniemenServiceProperties().setTokenPrivateKey("bFUyS1FRTVpON0pCSFFRRGdtSllSeUQ1MlRna2txVmI=")); - - private final Set testStrings = new HashSet<>(Arrays.asList("a", "bb", "dsv", "ghad", "dhaht", "uDg5Av", "d93fdvv", "dj83hzHo", "38iKawKv9", "dk(gkzm)Mh", "gjk)s3$g9cQ")); + private final AesGcmCryptographer aesGcmCryptographer = new AesGcmCryptographerImpl( + new PseudoniemenServiceProperties().setTokenPrivateKey( + "bFUyS1FRTVpON0pCSFFRRGdtSllSeUQ1MlRna2txVmI="), + new Base64Wrapper(), + new MessageDigestUtil() + ); + private final Set testStrings = new HashSet<>( + Arrays.asList("a", "bb", "dsv", "ghad", "dhaht", "uDg5Av", "d93fdvv", "dj83hzHo", + "38iKawKv9", "dk(gkzm)Mh", "gjk)s3$g9cQ")); @Test public void testEncyptDecryptForDifferentStringLengths() { From 8a0dc0d3783330c05442fa599d28f776a544645b Mon Sep 17 00:00:00 2001 From: sayf Date: Fri, 20 Dec 2024 16:53:27 +0100 Subject: [PATCH 03/30] code styling --- .../nl/ictu/service/AesGcmCryptographerImpl.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/nl/ictu/service/AesGcmCryptographerImpl.java b/src/main/java/nl/ictu/service/AesGcmCryptographerImpl.java index a3d76da..0de500d 100644 --- a/src/main/java/nl/ictu/service/AesGcmCryptographerImpl.java +++ b/src/main/java/nl/ictu/service/AesGcmCryptographerImpl.java @@ -58,11 +58,11 @@ public AesGcmCryptographerImpl( public String encrypt(final String plaintext, final String salt) throws IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { - final Cipher cipher = AESHelper.createCipher(); + final var cipher = AESHelper.createCipher(); - final GCMParameterSpec gcmParameterSpec = AESHelper.generateIV(); + final var gcmParameterSpec = AESHelper.generateIV(); - final SecretKey secretKey = createSecretKey(salt); + final var secretKey = createSecretKey(salt); cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec); @@ -93,7 +93,7 @@ private SecretKey createSecretKey(final String salt) { public String decrypt(final String ciphertextWithIv, final String salt) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { - final Cipher cipher = AESHelper.createCipher(); + final var cipher = AESHelper.createCipher(); final var encryptedWithIV = base64Wrapper.decode(ciphertextWithIv); @@ -101,9 +101,9 @@ public String decrypt(final String ciphertextWithIv, final String salt) final var ciphertext = Arrays.copyOfRange(encryptedWithIV, IV_LENGTH, encryptedWithIV.length); - final GCMParameterSpec gcmParameterSpec = AESHelper.createIVfromValues(iv); + final var gcmParameterSpec = AESHelper.createIVfromValues(iv); - final SecretKey secretKey = createSecretKey(salt); + final var secretKey = createSecretKey(salt); cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec); From 5e5bdba81a09883536de319072749db89e3b9db6 Mon Sep 17 00:00:00 2001 From: sayf Date: Sat, 21 Dec 2024 13:10:32 +0100 Subject: [PATCH 04/30] More refactoring, better constructors --- spotbugs-exclude.xml | 2 +- .../PseudoniemenServiceProperties.java | 3 + .../nl/ictu/service/AesGcmCryptographer.java | 22 +++- .../ictu/service/AesGcmCryptographerImpl.java | 45 +------- .../service/AesGcmSivCryptographerImpl.java | 109 ++++++------------ .../nl/ictu/service/IdentifierConverter.java | 4 +- .../ictu/service/IdentifierConverterImpl.java | 2 - .../IdentifierPrivateKeyException.java | 9 ++ .../nl/ictu/{service => utils}/AESHelper.java | 26 +++-- .../ictu/service/TestAesGcmCryptographer.java | 7 +- .../service/TestAesGcmSivCryptographer.java | 30 +++-- 11 files changed, 108 insertions(+), 151 deletions(-) create mode 100644 src/main/java/nl/ictu/service/exception/IdentifierPrivateKeyException.java rename src/main/java/nl/ictu/{service => utils}/AESHelper.java (82%) diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index 14d7383..0f4a5e4 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -4,6 +4,6 @@ - + \ No newline at end of file diff --git a/src/main/java/nl/ictu/configuration/PseudoniemenServiceProperties.java b/src/main/java/nl/ictu/configuration/PseudoniemenServiceProperties.java index 9fb7372..6bc7ad5 100644 --- a/src/main/java/nl/ictu/configuration/PseudoniemenServiceProperties.java +++ b/src/main/java/nl/ictu/configuration/PseudoniemenServiceProperties.java @@ -24,6 +24,9 @@ public void validate() { if (!StringUtils.hasText(tokenPrivateKey)) { throw new TokenPrivateKeyException("Please set a private token key"); } + if (!StringUtils.hasText(identifierPrivateKey)) { + throw new RuntimeException("Please set a private identifier key"); + } } } diff --git a/src/main/java/nl/ictu/service/AesGcmCryptographer.java b/src/main/java/nl/ictu/service/AesGcmCryptographer.java index 5e83c89..4e1ad7b 100644 --- a/src/main/java/nl/ictu/service/AesGcmCryptographer.java +++ b/src/main/java/nl/ictu/service/AesGcmCryptographer.java @@ -1,15 +1,27 @@ package nl.ictu.service; -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; public interface AesGcmCryptographer { - String encrypt(String plaintext, String salt) throws IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException; + String encrypt(String plaintext, String salt) + throws IllegalBlockSizeException, + BadPaddingException, + InvalidAlgorithmParameterException, + InvalidKeyException, + NoSuchAlgorithmException, + NoSuchPaddingException; - String decrypt(String ciphertext, String salt) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException; + String decrypt(String ciphertext, String salt) + throws NoSuchPaddingException, + NoSuchAlgorithmException, + InvalidAlgorithmParameterException, + InvalidKeyException, + IllegalBlockSizeException, + BadPaddingException; } diff --git a/src/main/java/nl/ictu/service/AesGcmCryptographerImpl.java b/src/main/java/nl/ictu/service/AesGcmCryptographerImpl.java index 0de500d..5a58926 100644 --- a/src/main/java/nl/ictu/service/AesGcmCryptographerImpl.java +++ b/src/main/java/nl/ictu/service/AesGcmCryptographerImpl.java @@ -1,8 +1,7 @@ package nl.ictu.service; -import static nl.ictu.service.AESHelper.IV_LENGTH; +import static nl.ictu.utils.AESHelper.IV_LENGTH; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; @@ -13,16 +12,14 @@ import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; -import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; -import lombok.SneakyThrows; +import lombok.RequiredArgsConstructor; import nl.ictu.configuration.PseudoniemenServiceProperties; -import nl.ictu.service.exception.TokenPrivateKeyException; +import nl.ictu.utils.AESHelper; import nl.ictu.utils.Base64Wrapper; import nl.ictu.utils.ByteArrayUtils; import nl.ictu.utils.MessageDigestUtil; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; /** * Advanced Encryption Standard Galois/Counter Mode (AES-GCM). @@ -30,47 +27,24 @@ @SuppressWarnings("DesignForExtension") @Service +@RequiredArgsConstructor public class AesGcmCryptographerImpl implements AesGcmCryptographer { private final Base64Wrapper base64Wrapper; - private final MessageDigestUtil messageDigestUtil; - private final PseudoniemenServiceProperties pseudoniemenServiceProperties; - @SuppressFBWarnings("CT_CONSTRUCTOR_THROW") - @SneakyThrows - public AesGcmCryptographerImpl( - final PseudoniemenServiceProperties properties, - final Base64Wrapper base64, - final MessageDigestUtil messageDigest) { - - this.pseudoniemenServiceProperties = properties; - this.base64Wrapper = base64; - this.messageDigestUtil = messageDigest; - - if (!StringUtils.hasText(pseudoniemenServiceProperties.getTokenPrivateKey())) { - throw new TokenPrivateKeyException("Please set a private token key"); - } - } - @Override public String encrypt(final String plaintext, final String salt) throws IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { final var cipher = AESHelper.createCipher(); - final var gcmParameterSpec = AESHelper.generateIV(); - final var secretKey = createSecretKey(salt); - cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec); - final var ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); - final var gcmIV = gcmParameterSpec.getIV(); final var encryptedWithIV = ByteArrayUtils.concat(gcmIV, ciphertext); - return base64Wrapper.encodeToString(encryptedWithIV); } @@ -78,13 +52,9 @@ private SecretKey createSecretKey(final String salt) { final var keyBytes = base64Wrapper.decode( pseudoniemenServiceProperties.getTokenPrivateKey()); - final var saltBytes = salt.getBytes(StandardCharsets.UTF_8); - final var salterSecretBytes = ByteArrayUtils.concat(keyBytes, saltBytes); - final var key = messageDigestUtil.getMessageDigestSha256().digest(salterSecretBytes); - return new SecretKeySpec(key, "AES"); } @@ -94,21 +64,14 @@ public String decrypt(final String ciphertextWithIv, final String salt) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { final var cipher = AESHelper.createCipher(); - final var encryptedWithIV = base64Wrapper.decode(ciphertextWithIv); - final var iv = Arrays.copyOfRange(encryptedWithIV, 0, IV_LENGTH); final var ciphertext = Arrays.copyOfRange(encryptedWithIV, IV_LENGTH, encryptedWithIV.length); - final var gcmParameterSpec = AESHelper.createIVfromValues(iv); - final var secretKey = createSecretKey(salt); - cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec); - final var decryptedText = cipher.doFinal(ciphertext); - return new String(decryptedText, StandardCharsets.UTF_8); } diff --git a/src/main/java/nl/ictu/service/AesGcmSivCryptographerImpl.java b/src/main/java/nl/ictu/service/AesGcmSivCryptographerImpl.java index 2ecfb19..a395c7d 100644 --- a/src/main/java/nl/ictu/service/AesGcmSivCryptographerImpl.java +++ b/src/main/java/nl/ictu/service/AesGcmSivCryptographerImpl.java @@ -1,25 +1,21 @@ package nl.ictu.service; import com.fasterxml.jackson.core.JsonProcessingException; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import lombok.SneakyThrows; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import nl.ictu.Identifier; import nl.ictu.configuration.PseudoniemenServiceProperties; +import nl.ictu.utils.AESHelper; +import nl.ictu.utils.Base64Wrapper; +import nl.ictu.utils.MessageDigestUtil; import org.bouncycastle.crypto.InvalidCipherTextException; -import org.bouncycastle.crypto.MultiBlockCipher; -import org.bouncycastle.crypto.engines.AESEngine; import org.bouncycastle.crypto.modes.GCMSIVBlockCipher; import org.bouncycastle.crypto.params.AEADParameters; import org.bouncycastle.crypto.params.KeyParameter; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.util.Arrays; -import java.util.Base64; /** * Advanced Encryption Standard Galois/Counter Mode synthetic initialization vector. @@ -28,100 +24,61 @@ @Slf4j @SuppressWarnings("DesignForExtension") @Service +@RequiredArgsConstructor public class AesGcmSivCryptographerImpl implements AesGcmSivCryptographer { public static final int MAC_SIZE = 128; - - private final PseudoniemenServiceProperties pseudoniemenServiceProperties; - private static final int NONCE_LENTH = 12; - private final Base64.Encoder base64Encoder = Base64.getEncoder(); - - private final Base64.Decoder base64Decoder = Base64.getDecoder(); - - private final MultiBlockCipher aesEngine; - - private final MessageDigest sha256Digest; - + private final PseudoniemenServiceProperties pseudoniemenServiceProperties; + private final MessageDigestUtil messageDigestUtil; private final IdentifierConverter identifierConverter; - - @SuppressFBWarnings("CT_CONSTRUCTOR_THROW") - @SneakyThrows - public AesGcmSivCryptographerImpl(final PseudoniemenServiceProperties pseudoniemenServicePropertiesArg, final IdentifierConverter identifierConverterArg) { - - pseudoniemenServiceProperties = pseudoniemenServicePropertiesArg; - identifierConverter = identifierConverterArg; - - aesEngine = AESEngine.newInstance(); - sha256Digest = MessageDigest.getInstance("SHA-256"); - - if (!StringUtils.hasText(pseudoniemenServiceProperties.getIdentifierPrivateKey())) { - throw new RuntimeException("Please set a private identifier key"); - } - - } + private final Base64Wrapper base64Wrapper; private AEADParameters createSecretKey(final String salt) { - final byte[] nonce16 = sha256Digest.digest(salt.getBytes(StandardCharsets.UTF_8)); - - byte[] nonce12 = Arrays.copyOf(nonce16, NONCE_LENTH); - + final var nonce16 = messageDigestUtil.getMessageDigestSha256() + .digest(salt.getBytes(StandardCharsets.UTF_8)); + final var nonce12 = Arrays.copyOf(nonce16, NONCE_LENTH); final String identifierPrivateKey = pseudoniemenServiceProperties.getIdentifierPrivateKey(); - - final KeyParameter keyParameter = new KeyParameter(base64Decoder.decode(identifierPrivateKey)); - + final KeyParameter keyParameter = new KeyParameter( + base64Wrapper.decode(identifierPrivateKey)); return new AEADParameters(keyParameter, MAC_SIZE, nonce12); } @Override - public String encrypt(final Identifier identifier, final String salt) throws InvalidCipherTextException, IOException { - - final String plaintext = identifierConverter.encode(identifier); - - final GCMSIVBlockCipher cipher = new GCMSIVBlockCipher(aesEngine); + public String encrypt(final Identifier identifier, final String salt) + throws InvalidCipherTextException, IOException { + final var plaintext = identifierConverter.encode(identifier); + final var cipher = new GCMSIVBlockCipher(AESHelper.getAESEngine()); cipher.init(true, createSecretKey(salt)); - - final byte[] plainTextBytes = plaintext.getBytes(StandardCharsets.UTF_8); - - final byte[] ciphertext = new byte[cipher.getOutputSize(plainTextBytes.length)]; - - final int outputLength = cipher.processBytes(plainTextBytes, 0, plainTextBytes.length, ciphertext, 0); - + final var plainTextBytes = plaintext.getBytes(StandardCharsets.UTF_8); + final var ciphertext = new byte[cipher.getOutputSize(plainTextBytes.length)]; + final var outputLength = cipher.processBytes(plainTextBytes, 0, plainTextBytes.length, + ciphertext, 0); cipher.doFinal(ciphertext, outputLength); - cipher.reset(); - - return base64Encoder.encodeToString(ciphertext); + return base64Wrapper.encodeToString(ciphertext); } @Override - public Identifier decrypt(final String ciphertextString, final String salt) throws InvalidCipherTextException, JsonProcessingException { - - final GCMSIVBlockCipher cipher = new GCMSIVBlockCipher(aesEngine); + public Identifier decrypt(final String ciphertextString, final String salt) + throws InvalidCipherTextException, JsonProcessingException { + final var cipher = new GCMSIVBlockCipher(AESHelper.getAESEngine()); cipher.init(false, createSecretKey(salt)); - - final byte[] ciphertext = base64Decoder.decode(ciphertextString); - - final byte[] plaintext = new byte[cipher.getOutputSize(ciphertext.length)]; - - final int outputLength = cipher.processBytes(ciphertext, 0, ciphertext.length, plaintext, 0); - + final var ciphertext = base64Wrapper.decode(ciphertextString); + final var plaintext = new byte[cipher.getOutputSize(ciphertext.length)]; + final var outputLength = cipher.processBytes(ciphertext, 0, ciphertext.length, plaintext, + 0); cipher.doFinal(plaintext, outputLength); - cipher.reset(); - - final String encodedIdentifier = new String(plaintext, StandardCharsets.UTF_8); - + final var encodedIdentifier = new String(plaintext, StandardCharsets.UTF_8); return identifierConverter.decode(encodedIdentifier); - } - } diff --git a/src/main/java/nl/ictu/service/IdentifierConverter.java b/src/main/java/nl/ictu/service/IdentifierConverter.java index 0292ec4..584b6c6 100644 --- a/src/main/java/nl/ictu/service/IdentifierConverter.java +++ b/src/main/java/nl/ictu/service/IdentifierConverter.java @@ -1,14 +1,12 @@ package nl.ictu.service; import com.fasterxml.jackson.core.JsonProcessingException; -import nl.ictu.Identifier; - import java.io.IOException; +import nl.ictu.Identifier; public interface IdentifierConverter { String encode(Identifier identifier) throws IOException; Identifier decode(String encodedIdentifier) throws JsonProcessingException; - } diff --git a/src/main/java/nl/ictu/service/IdentifierConverterImpl.java b/src/main/java/nl/ictu/service/IdentifierConverterImpl.java index 542969d..8116981 100644 --- a/src/main/java/nl/ictu/service/IdentifierConverterImpl.java +++ b/src/main/java/nl/ictu/service/IdentifierConverterImpl.java @@ -29,6 +29,4 @@ public String encode(final Identifier identifier) throws IOException { public Identifier decode(final String encodedIdentifier) throws JsonProcessingException { return objectMapper.readValue(encodedIdentifier, Identifier.class); } - - } diff --git a/src/main/java/nl/ictu/service/exception/IdentifierPrivateKeyException.java b/src/main/java/nl/ictu/service/exception/IdentifierPrivateKeyException.java new file mode 100644 index 0000000..660d8cd --- /dev/null +++ b/src/main/java/nl/ictu/service/exception/IdentifierPrivateKeyException.java @@ -0,0 +1,9 @@ +package nl.ictu.service.exception; + +public class IdentifierPrivateKeyException extends RuntimeException { + + public IdentifierPrivateKeyException(final String message) { + + super(message); + } +} diff --git a/src/main/java/nl/ictu/service/AESHelper.java b/src/main/java/nl/ictu/utils/AESHelper.java similarity index 82% rename from src/main/java/nl/ictu/service/AESHelper.java rename to src/main/java/nl/ictu/utils/AESHelper.java index 3c6cdcb..ad57557 100644 --- a/src/main/java/nl/ictu/service/AESHelper.java +++ b/src/main/java/nl/ictu/utils/AESHelper.java @@ -1,26 +1,27 @@ -package nl.ictu.service; +package nl.ictu.utils; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.GCMParameterSpec; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; +import org.bouncycastle.crypto.MultiBlockCipher; +import org.bouncycastle.crypto.engines.AESEngine; public final class AESHelper { - private AESHelper() { - } - public static final int IV_LENGTH = 12; - private static final int TAG_LENGTH = 128; - private static final String CIPHER = "AES/GCM/NoPadding"; - private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + private AESHelper() { + + } + // Method to generate a random Initialization Vector (IV) public static GCMParameterSpec generateIV() { + byte[] iv = new byte[IV_LENGTH]; // AES block size is 16 bytes SECURE_RANDOM.nextBytes(iv); @@ -29,11 +30,18 @@ public static GCMParameterSpec generateIV() { } public static GCMParameterSpec createIVfromValues(final byte[] iv) { + return new GCMParameterSpec(TAG_LENGTH, iv); } public static Cipher createCipher() throws NoSuchPaddingException, NoSuchAlgorithmException { + return Cipher.getInstance(CIPHER); } + public static MultiBlockCipher getAESEngine() { + + return AESEngine.newInstance(); + } + } diff --git a/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java b/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java index 8505386..ffaeda1 100644 --- a/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java +++ b/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java @@ -22,10 +22,11 @@ public class TestAesGcmCryptographer { private final AesGcmCryptographer aesGcmCryptographer = new AesGcmCryptographerImpl( - new PseudoniemenServiceProperties().setTokenPrivateKey( - "bFUyS1FRTVpON0pCSFFRRGdtSllSeUQ1MlRna2txVmI="), new Base64Wrapper(), - new MessageDigestUtil() + new MessageDigestUtil(), + new PseudoniemenServiceProperties().setTokenPrivateKey( + "bFUyS1FRTVpON0pCSFFRRGdtSllSeUQ1MlRna2txVmI=") + ); private final Set testStrings = new HashSet<>( Arrays.asList("a", "bb", "dsv", "ghad", "dhaht", "uDg5Av", "d93fdvv", "dj83hzHo", diff --git a/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java b/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java index 29407d0..0a84869 100644 --- a/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java +++ b/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java @@ -1,19 +1,20 @@ package nl.ictu.service; +import static org.assertj.core.api.Assertions.assertThat; + import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; import lombok.extern.slf4j.Slf4j; import nl.ictu.Identifier; import nl.ictu.configuration.PseudoniemenServiceProperties; +import nl.ictu.utils.Base64Wrapper; +import nl.ictu.utils.MessageDigestUtil; import org.junit.jupiter.api.Test; import org.springframework.test.context.ActiveProfiles; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - /** * Class for tesing {@link AesGcmCryptographerImpl} */ @@ -23,11 +24,16 @@ public class TestAesGcmSivCryptographer { private final AesGcmSivCryptographer aesGcmSivCryptographer = new AesGcmSivCryptographerImpl( - new PseudoniemenServiceProperties().setIdentifierPrivateKey("QTBtVEhLN3EwMHJ3QXN1ZUFqNzVrT3hDQTBIWWNIZTU="), - new IdentifierConverterImpl(new ObjectMapper()) + new PseudoniemenServiceProperties().setIdentifierPrivateKey( + "QTBtVEhLN3EwMHJ3QXN1ZUFqNzVrT3hDQTBIWWNIZTU="), + new MessageDigestUtil(), + new IdentifierConverterImpl(new ObjectMapper()), + new Base64Wrapper() ); - private final Set testStrings = new HashSet<>(Arrays.asList("a", "bb", "dsv", "ghad", "dhaht", "uDg5Av", "d93fdvv", "dj83hzHo", "38iKawKv9", "dk(gkzm)Mh", "gjk)s3$g9cQ")); + private final Set testStrings = new HashSet<>( + Arrays.asList("a", "bb", "dsv", "ghad", "dhaht", "uDg5Av", "d93fdvv", "dj83hzHo", + "38iKawKv9", "dk(gkzm)Mh", "gjk)s3$g9cQ")); @Test public void testEncyptDecryptForDifferentStringLengths() { @@ -38,8 +44,10 @@ public void testEncyptDecryptForDifferentStringLengths() { final Identifier identifier = new Identifier(); identifier.setBsn(plain); - final String crypted = aesGcmSivCryptographer.encrypt(identifier, "helloHowAreyo12345678"); - final Identifier actual = aesGcmSivCryptographer.decrypt(crypted, "helloHowAreyo12345678"); + final String crypted = aesGcmSivCryptographer.encrypt(identifier, + "helloHowAreyo12345678"); + final Identifier actual = aesGcmSivCryptographer.decrypt(crypted, + "helloHowAreyo12345678"); assertThat(actual.getBsn()).isEqualTo(plain); } catch (final Exception e) { throw new RuntimeException(e); From 860d74493d02d286f6cc80ff3f02509992af0e17 Mon Sep 17 00:00:00 2001 From: sayf Date: Sat, 21 Dec 2024 22:02:03 +0100 Subject: [PATCH 05/30] More refactoring, prepare to split responsibilities --- src/main/java/nl/ictu/Token.java | 16 ++--- .../controller/v1/ExchangeIdentifier.java | 41 +++++++------ .../java/nl/ictu/controller/v1/GetToken.java | 61 +++++++++++-------- .../ictu/service/AesGcmCryptographerImpl.java | 1 - .../java/nl/ictu/service/GetTokenService.java | 15 +++++ 5 files changed, 79 insertions(+), 55 deletions(-) create mode 100644 src/main/java/nl/ictu/service/GetTokenService.java diff --git a/src/main/java/nl/ictu/Token.java b/src/main/java/nl/ictu/Token.java index 38c4d21..c47fd21 100644 --- a/src/main/java/nl/ictu/Token.java +++ b/src/main/java/nl/ictu/Token.java @@ -1,15 +1,15 @@ package nl.ictu; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import lombok.Getter; -import lombok.Setter; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; -@Getter -@Setter -public final class Token { +@Data +@Builder +@AllArgsConstructor +public class Token { - @SuppressFBWarnings("SS_SHOULD_BE_STATIC") - private String version = "v1"; + private String version; private String bsn; private String recipientOIN; private Long creationDate; diff --git a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifier.java b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifier.java index 4271028..6ee5070 100644 --- a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifier.java +++ b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifier.java @@ -1,5 +1,10 @@ package nl.ictu.controller.v1; +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; +import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; + +import java.io.IOException; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import nl.ictu.Identifier; @@ -7,18 +12,11 @@ import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierRequest; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierResponse; import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; -import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes; import nl.ictu.service.AesGcmSivCryptographer; import org.bouncycastle.crypto.InvalidCipherTextException; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; -import java.io.IOException; - -import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; -import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; -import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; - @RequiredArgsConstructor @RestController public final class ExchangeIdentifier implements ExchangeIdentifierApi, VersionOneController { @@ -27,21 +25,24 @@ public final class ExchangeIdentifier implements ExchangeIdentifierApi, VersionO @Override @SneakyThrows - public ResponseEntity exchangeIdentifier(final String callerOIN, final WsExchangeIdentifierRequest wsExchangeIdentifierForIdentifierRequest) { - - final WsIdentifier wsIdentifierRequest = wsExchangeIdentifierForIdentifierRequest.getIdentifier(); - - final String recipientOIN = wsExchangeIdentifierForIdentifierRequest.getRecipientOIN(); + public ResponseEntity exchangeIdentifier(final String callerOIN, + final WsExchangeIdentifierRequest wsExchangeIdentifierForIdentifierRequest) { - final WsIdentifierTypes recipientIdentifierType = wsExchangeIdentifierForIdentifierRequest.getRecipientIdentifierType(); + final var wsIdentifierRequest = wsExchangeIdentifierForIdentifierRequest.getIdentifier(); + final var recipientOIN = wsExchangeIdentifierForIdentifierRequest.getRecipientOIN(); + final var recipientIdentifierType = wsExchangeIdentifierForIdentifierRequest.getRecipientIdentifierType(); - if (BSN.equals(wsIdentifierRequest.getType()) && ORGANISATION_PSEUDO.equals(recipientIdentifierType)) { + if (BSN.equals(wsIdentifierRequest.getType()) && ORGANISATION_PSEUDO.equals( + recipientIdentifierType)) { // from BSN to Org Pseudo - return ResponseEntity.ok(convertBsnToPseudo(wsIdentifierRequest.getValue(), recipientOIN)); + return ResponseEntity.ok( + convertBsnToPseudo(wsIdentifierRequest.getValue(), recipientOIN)); - } else if (ORGANISATION_PSEUDO.equals(wsIdentifierRequest.getType()) && BSN.equals(recipientIdentifierType)) { + } else if (ORGANISATION_PSEUDO.equals(wsIdentifierRequest.getType()) && BSN.equals( + recipientIdentifierType)) { // from BSN to Org Pseudo - return ResponseEntity.ok(convertPseudoToBEsn(wsIdentifierRequest.getValue(), recipientOIN)); + return ResponseEntity.ok( + convertPseudoToBEsn(wsIdentifierRequest.getValue(), recipientOIN)); } else { return ResponseEntity.status(UNPROCESSABLE_ENTITY).build(); @@ -50,7 +51,8 @@ public ResponseEntity exchangeIdentifier(final Str } - private WsExchangeIdentifierResponse convertBsnToPseudo(final String bsn, final String oin) throws IOException, InvalidCipherTextException { + private WsExchangeIdentifierResponse convertBsnToPseudo(final String bsn, final String oin) + throws IOException, InvalidCipherTextException { final Identifier identifier = new Identifier(); @@ -71,7 +73,8 @@ private WsExchangeIdentifierResponse convertBsnToPseudo(final String bsn, final } - private WsExchangeIdentifierResponse convertPseudoToBEsn(final String pseudo, final String oin) throws IOException, InvalidCipherTextException { + private WsExchangeIdentifierResponse convertPseudoToBEsn(final String pseudo, final String oin) + throws IOException, InvalidCipherTextException { final Identifier identifier = aesGcmSivCryptographer.decrypt(pseudo, oin); diff --git a/src/main/java/nl/ictu/controller/v1/GetToken.java b/src/main/java/nl/ictu/controller/v1/GetToken.java index 0a2395c..92d5613 100644 --- a/src/main/java/nl/ictu/controller/v1/GetToken.java +++ b/src/main/java/nl/ictu/controller/v1/GetToken.java @@ -1,8 +1,10 @@ package nl.ictu.controller.v1; +import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; + +import com.fasterxml.jackson.core.JsonProcessingException; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; -import nl.ictu.Identifier; import nl.ictu.Token; import nl.ictu.pseudoniemenservice.generated.server.api.GetTokenApi; import nl.ictu.pseudoniemenservice.generated.server.model.WsGetTokenRequest; @@ -10,57 +12,62 @@ import nl.ictu.service.AesGcmCryptographer; import nl.ictu.service.AesGcmSivCryptographer; import nl.ictu.service.TokenConverter; +import org.bouncycastle.crypto.InvalidCipherTextException; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; -import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; - @RestController @RequiredArgsConstructor public final class GetToken implements GetTokenApi, VersionOneController { private final AesGcmCryptographer aesGcmCryptographer; - private final AesGcmSivCryptographer aesGcmSivCryptographer; - private final TokenConverter tokenConverter; + @Override @SneakyThrows - public ResponseEntity getToken(final String callerOIN, final WsGetTokenRequest wsGetTokenRequest) { + public ResponseEntity getToken(final String callerOIN, + final WsGetTokenRequest wsGetTokenRequest) { // check is callerOIN allowed to communicatie with sinkOIN final WsGetTokenResponse wsGetToken200Response = new WsGetTokenResponse(); - final Token token = new Token(); - - token.setCreationDate(System.currentTimeMillis()); - token.setRecipientOIN(wsGetTokenRequest.getRecipientOIN()); - - if (wsGetTokenRequest.getIdentifier() != null) { - switch (wsGetTokenRequest.getIdentifier().getType()) { - case BSN -> token.setBsn(wsGetTokenRequest.getIdentifier().getValue()); - case ORGANISATION_PSEUDO -> { - - final String orgPseudoEncryptedString = wsGetTokenRequest.getIdentifier().getValue(); - - final Identifier decodedIdentifier = aesGcmSivCryptographer.decrypt(orgPseudoEncryptedString, wsGetTokenRequest.getRecipientOIN()); - - token.setBsn(decodedIdentifier.getBsn()); - - } + final var creationDate = System.currentTimeMillis(); + final var recipientOIN = wsGetTokenRequest.getRecipientOIN(); + final var identifier = wsGetTokenRequest.getIdentifier(); + final var tokenBuilder = Token.builder(); + + if (identifier != null) { + switch (identifier.getType()) { + case BSN -> tokenBuilder + .bsn(identifier.getValue()) + .creationDate(creationDate) + .recipientOIN(recipientOIN); + case ORGANISATION_PSEUDO -> tokenBuilder + .bsn(mapDecryptedBsn(identifier.getValue(), recipientOIN)) + .creationDate(creationDate) + .recipientOIN(recipientOIN); default -> { return ResponseEntity.status(UNPROCESSABLE_ENTITY).build(); } } } - final String plainTextToken = tokenConverter.encode(token); - - wsGetToken200Response.token(aesGcmCryptographer.encrypt(plainTextToken, wsGetTokenRequest.getRecipientOIN())); - + final var plainTextToken = tokenConverter.encode(tokenBuilder.build()); + final var encryptedToken = aesGcmCryptographer.encrypt(plainTextToken, recipientOIN); + wsGetToken200Response.token(encryptedToken); return ResponseEntity.ok(wsGetToken200Response); } + private String mapDecryptedBsn(final String bsnValue, final String recipientOIN) + throws InvalidCipherTextException, JsonProcessingException { + + final var orgPseudoEncryptedString = bsnValue; + final var decodedIdentifier = aesGcmSivCryptographer.decrypt( + orgPseudoEncryptedString, + recipientOIN); + return decodedIdentifier.getBsn(); + } } diff --git a/src/main/java/nl/ictu/service/AesGcmCryptographerImpl.java b/src/main/java/nl/ictu/service/AesGcmCryptographerImpl.java index 5a58926..c6ed71d 100644 --- a/src/main/java/nl/ictu/service/AesGcmCryptographerImpl.java +++ b/src/main/java/nl/ictu/service/AesGcmCryptographerImpl.java @@ -73,6 +73,5 @@ public String decrypt(final String ciphertextWithIv, final String salt) cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec); final var decryptedText = cipher.doFinal(ciphertext); return new String(decryptedText, StandardCharsets.UTF_8); - } } diff --git a/src/main/java/nl/ictu/service/GetTokenService.java b/src/main/java/nl/ictu/service/GetTokenService.java new file mode 100644 index 0000000..8f3d930 --- /dev/null +++ b/src/main/java/nl/ictu/service/GetTokenService.java @@ -0,0 +1,15 @@ +/* +package nl.ictu.service; + +import nl.ictu.Token; +import org.springframework.stereotype.Service; + +@Service +public class GetTokenService { + + public Token getToken(){ + return null; + } + +} +*/ From 4e8002b817389ac1eca5c275a93fbb905d9cf003 Mon Sep 17 00:00:00 2001 From: sayf Date: Sat, 21 Dec 2024 22:40:08 +0100 Subject: [PATCH 06/30] More refactoring, prepare to split responsibilities --- ...java => ExchangeIdentifierController.java} | 2 +- ...oken.java => ExchangeTokenController.java} | 2 +- ...{GetToken.java => GetTokenController.java} | 50 ++++++++++--------- 3 files changed, 29 insertions(+), 25 deletions(-) rename src/main/java/nl/ictu/controller/v1/{ExchangeIdentifier.java => ExchangeIdentifierController.java} (97%) rename src/main/java/nl/ictu/controller/v1/{ExchangeToken.java => ExchangeTokenController.java} (96%) rename src/main/java/nl/ictu/controller/v1/{GetToken.java => GetTokenController.java} (53%) diff --git a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifier.java b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java similarity index 97% rename from src/main/java/nl/ictu/controller/v1/ExchangeIdentifier.java rename to src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java index 6ee5070..60b2a37 100644 --- a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifier.java +++ b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java @@ -19,7 +19,7 @@ @RequiredArgsConstructor @RestController -public final class ExchangeIdentifier implements ExchangeIdentifierApi, VersionOneController { +public final class ExchangeIdentifierController implements ExchangeIdentifierApi, VersionOneController { private final AesGcmSivCryptographer aesGcmSivCryptographer; diff --git a/src/main/java/nl/ictu/controller/v1/ExchangeToken.java b/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java similarity index 96% rename from src/main/java/nl/ictu/controller/v1/ExchangeToken.java rename to src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java index 116fec4..29580e6 100644 --- a/src/main/java/nl/ictu/controller/v1/ExchangeToken.java +++ b/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java @@ -22,7 +22,7 @@ @Slf4j @RequiredArgsConstructor @RestController -public final class ExchangeToken implements ExchangeTokenApi, VersionOneController { +public final class ExchangeTokenController implements ExchangeTokenApi, VersionOneController { private final AesGcmCryptographer aesGcmCryptographer; diff --git a/src/main/java/nl/ictu/controller/v1/GetToken.java b/src/main/java/nl/ictu/controller/v1/GetTokenController.java similarity index 53% rename from src/main/java/nl/ictu/controller/v1/GetToken.java rename to src/main/java/nl/ictu/controller/v1/GetTokenController.java index 92d5613..b9b8ff1 100644 --- a/src/main/java/nl/ictu/controller/v1/GetToken.java +++ b/src/main/java/nl/ictu/controller/v1/GetTokenController.java @@ -1,5 +1,7 @@ package nl.ictu.controller.v1; +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; import com.fasterxml.jackson.core.JsonProcessingException; @@ -18,8 +20,9 @@ @RestController @RequiredArgsConstructor -public final class GetToken implements GetTokenApi, VersionOneController { +public final class GetTokenController implements GetTokenApi, VersionOneController { + public static final String V_1 = "v1"; private final AesGcmCryptographer aesGcmCryptographer; private final AesGcmSivCryptographer aesGcmSivCryptographer; private final TokenConverter tokenConverter; @@ -32,41 +35,42 @@ public ResponseEntity getToken(final String callerOIN, // check is callerOIN allowed to communicatie with sinkOIN - final WsGetTokenResponse wsGetToken200Response = new WsGetTokenResponse(); - final var creationDate = System.currentTimeMillis(); final var recipientOIN = wsGetTokenRequest.getRecipientOIN(); final var identifier = wsGetTokenRequest.getIdentifier(); - final var tokenBuilder = Token.builder(); - if (identifier != null) { - switch (identifier.getType()) { - case BSN -> tokenBuilder - .bsn(identifier.getValue()) - .creationDate(creationDate) - .recipientOIN(recipientOIN); - case ORGANISATION_PSEUDO -> tokenBuilder - .bsn(mapDecryptedBsn(identifier.getValue(), recipientOIN)) - .creationDate(creationDate) - .recipientOIN(recipientOIN); - default -> { - return ResponseEntity.status(UNPROCESSABLE_ENTITY).build(); - } + var bsn = ""; + if (identifier != null + && identifier.getValue() != null + && (BSN.equals(identifier.getType()) || ORGANISATION_PSEUDO.equals(identifier.getType()))) { + final String bsnValue = identifier.getValue(); + if (BSN.equals(identifier.getType())) { + bsn = bsnValue; + } else if (ORGANISATION_PSEUDO.equals(identifier.getType())) { + bsn = mapDecryptedBsn(bsnValue, recipientOIN); } + final var token = Token.builder() + .version(V_1) + .bsn(bsn) + .creationDate(creationDate) + .recipientOIN(recipientOIN) + .build(); + final var plainTextToken = tokenConverter.encode(token); + final var encryptedToken = aesGcmCryptographer.encrypt(plainTextToken, recipientOIN); + final WsGetTokenResponse wsGetToken200Response = new WsGetTokenResponse(); + wsGetToken200Response.token(encryptedToken); + return ResponseEntity.ok(wsGetToken200Response); } - final var plainTextToken = tokenConverter.encode(tokenBuilder.build()); - final var encryptedToken = aesGcmCryptographer.encrypt(plainTextToken, recipientOIN); - wsGetToken200Response.token(encryptedToken); - return ResponseEntity.ok(wsGetToken200Response); + return ResponseEntity.status(UNPROCESSABLE_ENTITY).build(); + } private String mapDecryptedBsn(final String bsnValue, final String recipientOIN) throws InvalidCipherTextException, JsonProcessingException { - final var orgPseudoEncryptedString = bsnValue; final var decodedIdentifier = aesGcmSivCryptographer.decrypt( - orgPseudoEncryptedString, + bsnValue, recipientOIN); return decodedIdentifier.getBsn(); } From 0cca93b9e9b14b0995bacd001f034a8f003846c5 Mon Sep 17 00:00:00 2001 From: sayf Date: Mon, 23 Dec 2024 13:02:00 +0100 Subject: [PATCH 07/30] More refactoring, prepare to split responsibilities --- pom.xml | 8 +- .../v1/ExchangeIdentifierController.java | 2 +- .../v1/ExchangeTokenController.java | 6 +- .../controller/v1/GetTokenController.java | 49 +---------- .../java/nl/ictu/service/GetTokenService.java | 15 ---- .../nl/ictu/service/v1/GetTokenService.java | 88 +++++++++++++++++++ .../{ => v1/crypto}/AesGcmCryptographer.java | 2 +- .../crypto}/AesGcmCryptographerImpl.java | 2 +- .../crypto}/AesGcmSivCryptographer.java | 2 +- .../crypto}/AesGcmSivCryptographerImpl.java | 2 +- .../{ => v1/crypto}/IdentifierConverter.java | 2 +- .../crypto}/IdentifierConverterImpl.java | 2 +- .../{ => v1/crypto}/TokenConverter.java | 2 +- .../{ => v1/crypto}/TokenConverterImpl.java | 2 +- .../ictu/service/TestAesGcmCryptographer.java | 2 + .../service/TestAesGcmSivCryptographer.java | 4 + 16 files changed, 117 insertions(+), 73 deletions(-) delete mode 100644 src/main/java/nl/ictu/service/GetTokenService.java create mode 100644 src/main/java/nl/ictu/service/v1/GetTokenService.java rename src/main/java/nl/ictu/service/{ => v1/crypto}/AesGcmCryptographer.java (96%) rename src/main/java/nl/ictu/service/{ => v1/crypto}/AesGcmCryptographerImpl.java (98%) rename src/main/java/nl/ictu/service/{ => v1/crypto}/AesGcmSivCryptographer.java (92%) rename src/main/java/nl/ictu/service/{ => v1/crypto}/AesGcmSivCryptographerImpl.java (98%) rename src/main/java/nl/ictu/service/{ => v1/crypto}/IdentifierConverter.java (89%) rename src/main/java/nl/ictu/service/{ => v1/crypto}/IdentifierConverterImpl.java (96%) rename src/main/java/nl/ictu/service/{ => v1/crypto}/TokenConverter.java (88%) rename src/main/java/nl/ictu/service/{ => v1/crypto}/TokenConverterImpl.java (96%) diff --git a/pom.xml b/pom.xml index 4f068c7..98f2b60 100644 --- a/pom.xml +++ b/pom.xml @@ -98,12 +98,18 @@ false true none - none + + + true + + + true + io.github.git-commit-id git-commit-id-maven-plugin diff --git a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java index 60b2a37..020f81b 100644 --- a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java +++ b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java @@ -12,7 +12,7 @@ import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierRequest; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierResponse; import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; -import nl.ictu.service.AesGcmSivCryptographer; +import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; import org.bouncycastle.crypto.InvalidCipherTextException; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; diff --git a/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java b/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java index 29580e6..a3a06d1 100644 --- a/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java +++ b/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java @@ -9,9 +9,9 @@ import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenRequest; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenResponse; import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; -import nl.ictu.service.AesGcmCryptographer; -import nl.ictu.service.AesGcmSivCryptographer; -import nl.ictu.service.TokenConverter; +import nl.ictu.service.v1.crypto.AesGcmCryptographer; +import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; +import nl.ictu.service.v1.crypto.TokenConverter; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; diff --git a/src/main/java/nl/ictu/controller/v1/GetTokenController.java b/src/main/java/nl/ictu/controller/v1/GetTokenController.java index b9b8ff1..ec862f4 100644 --- a/src/main/java/nl/ictu/controller/v1/GetTokenController.java +++ b/src/main/java/nl/ictu/controller/v1/GetTokenController.java @@ -1,20 +1,13 @@ package nl.ictu.controller.v1; -import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; -import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; -import com.fasterxml.jackson.core.JsonProcessingException; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; -import nl.ictu.Token; import nl.ictu.pseudoniemenservice.generated.server.api.GetTokenApi; import nl.ictu.pseudoniemenservice.generated.server.model.WsGetTokenRequest; import nl.ictu.pseudoniemenservice.generated.server.model.WsGetTokenResponse; -import nl.ictu.service.AesGcmCryptographer; -import nl.ictu.service.AesGcmSivCryptographer; -import nl.ictu.service.TokenConverter; -import org.bouncycastle.crypto.InvalidCipherTextException; +import nl.ictu.service.v1.GetTokenService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; @@ -22,56 +15,22 @@ @RequiredArgsConstructor public final class GetTokenController implements GetTokenApi, VersionOneController { - public static final String V_1 = "v1"; - private final AesGcmCryptographer aesGcmCryptographer; - private final AesGcmSivCryptographer aesGcmSivCryptographer; - private final TokenConverter tokenConverter; + private final GetTokenService getTokenService; @Override @SneakyThrows public ResponseEntity getToken(final String callerOIN, final WsGetTokenRequest wsGetTokenRequest) { - // check is callerOIN allowed to communicatie with sinkOIN - final var creationDate = System.currentTimeMillis(); - final var recipientOIN = wsGetTokenRequest.getRecipientOIN(); - final var identifier = wsGetTokenRequest.getIdentifier(); - - var bsn = ""; - if (identifier != null - && identifier.getValue() != null - && (BSN.equals(identifier.getType()) || ORGANISATION_PSEUDO.equals(identifier.getType()))) { - final String bsnValue = identifier.getValue(); - if (BSN.equals(identifier.getType())) { - bsn = bsnValue; - } else if (ORGANISATION_PSEUDO.equals(identifier.getType())) { - bsn = mapDecryptedBsn(bsnValue, recipientOIN); - } - final var token = Token.builder() - .version(V_1) - .bsn(bsn) - .creationDate(creationDate) - .recipientOIN(recipientOIN) - .build(); - final var plainTextToken = tokenConverter.encode(token); - final var encryptedToken = aesGcmCryptographer.encrypt(plainTextToken, recipientOIN); - final WsGetTokenResponse wsGetToken200Response = new WsGetTokenResponse(); - wsGetToken200Response.token(encryptedToken); + final var wsGetToken200Response = getTokenService.getWsGetTokenResponse(wsGetTokenRequest); + if (wsGetToken200Response != null) { return ResponseEntity.ok(wsGetToken200Response); } - return ResponseEntity.status(UNPROCESSABLE_ENTITY).build(); } - private String mapDecryptedBsn(final String bsnValue, final String recipientOIN) - throws InvalidCipherTextException, JsonProcessingException { - final var decodedIdentifier = aesGcmSivCryptographer.decrypt( - bsnValue, - recipientOIN); - return decodedIdentifier.getBsn(); - } } diff --git a/src/main/java/nl/ictu/service/GetTokenService.java b/src/main/java/nl/ictu/service/GetTokenService.java deleted file mode 100644 index 8f3d930..0000000 --- a/src/main/java/nl/ictu/service/GetTokenService.java +++ /dev/null @@ -1,15 +0,0 @@ -/* -package nl.ictu.service; - -import nl.ictu.Token; -import org.springframework.stereotype.Service; - -@Service -public class GetTokenService { - - public Token getToken(){ - return null; - } - -} -*/ diff --git a/src/main/java/nl/ictu/service/v1/GetTokenService.java b/src/main/java/nl/ictu/service/v1/GetTokenService.java new file mode 100644 index 0000000..b001fa0 --- /dev/null +++ b/src/main/java/nl/ictu/service/v1/GetTokenService.java @@ -0,0 +1,88 @@ +package nl.ictu.service.v1; + +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; + +import com.fasterxml.jackson.core.JsonProcessingException; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import nl.ictu.Token; +import nl.ictu.pseudoniemenservice.generated.server.model.WsGetTokenRequest; +import nl.ictu.pseudoniemenservice.generated.server.model.WsGetTokenResponse; +import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; +import nl.ictu.service.v1.crypto.AesGcmCryptographer; +import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; +import nl.ictu.service.v1.crypto.TokenConverter; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public final class GetTokenService { + + public static final String V_1 = "v1"; + private final AesGcmCryptographer aesGcmCryptographer; + private final AesGcmSivCryptographer aesGcmSivCryptographer; + private final TokenConverter tokenConverter; + + + public WsGetTokenResponse getWsGetTokenResponse( + final WsGetTokenRequest wsGetTokenRequest) + throws IOException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { + + final var creationDate = System.currentTimeMillis(); + final var recipientOIN = wsGetTokenRequest.getRecipientOIN(); + final var identifier = wsGetTokenRequest.getIdentifier(); + // check is callerOIN allowed to communicatie with sinkOIN + if (identifier != null && identifier.getValue() != null && identifier.getType() != null) { + + final String bsn = mapBsn(identifier, recipientOIN); + if (bsn != null) { + return createEncryptedToken(bsn, creationDate, recipientOIN); + } + } + return null; + } + + @SneakyThrows + private String mapBsn(final WsIdentifier identifier, final String recipientOIN) { + + final String bsnValue = identifier.getValue(); + if (BSN.equals(identifier.getType())) { + return bsnValue; + } else if (ORGANISATION_PSEUDO.equals(identifier.getType())) { + return mapDecryptedBsn(bsnValue, recipientOIN); + } + return null; + } + + private WsGetTokenResponse createEncryptedToken(final String bsn, final long creationDate, + final String recipientOIN) + throws IOException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { + + final var plainTextToken = tokenConverter.encode(Token.builder() + .version(V_1) + .bsn(bsn) + .creationDate(creationDate) + .recipientOIN(recipientOIN) + .build()); + final var encryptedToken = aesGcmCryptographer.encrypt(plainTextToken, recipientOIN); + final var wsGetToken200Response = new WsGetTokenResponse(); + wsGetToken200Response.token(encryptedToken); + return wsGetToken200Response; + } + + private String mapDecryptedBsn(final String bsnValue, final String recipientOIN) + throws InvalidCipherTextException, JsonProcessingException { + + final var decodedIdentifier = aesGcmSivCryptographer.decrypt(bsnValue, recipientOIN); + return decodedIdentifier.getBsn(); + } +} diff --git a/src/main/java/nl/ictu/service/AesGcmCryptographer.java b/src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographer.java similarity index 96% rename from src/main/java/nl/ictu/service/AesGcmCryptographer.java rename to src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographer.java index 4e1ad7b..4101be0 100644 --- a/src/main/java/nl/ictu/service/AesGcmCryptographer.java +++ b/src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographer.java @@ -1,4 +1,4 @@ -package nl.ictu.service; +package nl.ictu.service.v1.crypto; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; diff --git a/src/main/java/nl/ictu/service/AesGcmCryptographerImpl.java b/src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographerImpl.java similarity index 98% rename from src/main/java/nl/ictu/service/AesGcmCryptographerImpl.java rename to src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographerImpl.java index c6ed71d..100330c 100644 --- a/src/main/java/nl/ictu/service/AesGcmCryptographerImpl.java +++ b/src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographerImpl.java @@ -1,4 +1,4 @@ -package nl.ictu.service; +package nl.ictu.service.v1.crypto; import static nl.ictu.utils.AESHelper.IV_LENGTH; diff --git a/src/main/java/nl/ictu/service/AesGcmSivCryptographer.java b/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java similarity index 92% rename from src/main/java/nl/ictu/service/AesGcmSivCryptographer.java rename to src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java index 56872fe..d0076fa 100644 --- a/src/main/java/nl/ictu/service/AesGcmSivCryptographer.java +++ b/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java @@ -1,4 +1,4 @@ -package nl.ictu.service; +package nl.ictu.service.v1.crypto; import com.fasterxml.jackson.core.JsonProcessingException; import nl.ictu.Identifier; diff --git a/src/main/java/nl/ictu/service/AesGcmSivCryptographerImpl.java b/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographerImpl.java similarity index 98% rename from src/main/java/nl/ictu/service/AesGcmSivCryptographerImpl.java rename to src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographerImpl.java index a395c7d..7d6da30 100644 --- a/src/main/java/nl/ictu/service/AesGcmSivCryptographerImpl.java +++ b/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographerImpl.java @@ -1,4 +1,4 @@ -package nl.ictu.service; +package nl.ictu.service.v1.crypto; import com.fasterxml.jackson.core.JsonProcessingException; import java.io.IOException; diff --git a/src/main/java/nl/ictu/service/IdentifierConverter.java b/src/main/java/nl/ictu/service/v1/crypto/IdentifierConverter.java similarity index 89% rename from src/main/java/nl/ictu/service/IdentifierConverter.java rename to src/main/java/nl/ictu/service/v1/crypto/IdentifierConverter.java index 584b6c6..fcb5952 100644 --- a/src/main/java/nl/ictu/service/IdentifierConverter.java +++ b/src/main/java/nl/ictu/service/v1/crypto/IdentifierConverter.java @@ -1,4 +1,4 @@ -package nl.ictu.service; +package nl.ictu.service.v1.crypto; import com.fasterxml.jackson.core.JsonProcessingException; import java.io.IOException; diff --git a/src/main/java/nl/ictu/service/IdentifierConverterImpl.java b/src/main/java/nl/ictu/service/v1/crypto/IdentifierConverterImpl.java similarity index 96% rename from src/main/java/nl/ictu/service/IdentifierConverterImpl.java rename to src/main/java/nl/ictu/service/v1/crypto/IdentifierConverterImpl.java index 8116981..911eeff 100644 --- a/src/main/java/nl/ictu/service/IdentifierConverterImpl.java +++ b/src/main/java/nl/ictu/service/v1/crypto/IdentifierConverterImpl.java @@ -1,4 +1,4 @@ -package nl.ictu.service; +package nl.ictu.service.v1.crypto; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/src/main/java/nl/ictu/service/TokenConverter.java b/src/main/java/nl/ictu/service/v1/crypto/TokenConverter.java similarity index 88% rename from src/main/java/nl/ictu/service/TokenConverter.java rename to src/main/java/nl/ictu/service/v1/crypto/TokenConverter.java index 8c8d539..2c3aecc 100644 --- a/src/main/java/nl/ictu/service/TokenConverter.java +++ b/src/main/java/nl/ictu/service/v1/crypto/TokenConverter.java @@ -1,4 +1,4 @@ -package nl.ictu.service; +package nl.ictu.service.v1.crypto; import com.fasterxml.jackson.core.JsonProcessingException; import nl.ictu.Token; diff --git a/src/main/java/nl/ictu/service/TokenConverterImpl.java b/src/main/java/nl/ictu/service/v1/crypto/TokenConverterImpl.java similarity index 96% rename from src/main/java/nl/ictu/service/TokenConverterImpl.java rename to src/main/java/nl/ictu/service/v1/crypto/TokenConverterImpl.java index 4d8b346..635f9a7 100644 --- a/src/main/java/nl/ictu/service/TokenConverterImpl.java +++ b/src/main/java/nl/ictu/service/v1/crypto/TokenConverterImpl.java @@ -1,4 +1,4 @@ -package nl.ictu.service; +package nl.ictu.service.v1.crypto; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java b/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java index ffaeda1..ddd74b2 100644 --- a/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java +++ b/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java @@ -8,6 +8,8 @@ import java.util.Set; import lombok.extern.slf4j.Slf4j; import nl.ictu.configuration.PseudoniemenServiceProperties; +import nl.ictu.service.v1.crypto.AesGcmCryptographer; +import nl.ictu.service.v1.crypto.AesGcmCryptographerImpl; import nl.ictu.utils.Base64Wrapper; import nl.ictu.utils.MessageDigestUtil; import org.junit.jupiter.api.Test; diff --git a/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java b/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java index 0a84869..d56d0c4 100644 --- a/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java +++ b/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java @@ -10,6 +10,10 @@ import lombok.extern.slf4j.Slf4j; import nl.ictu.Identifier; import nl.ictu.configuration.PseudoniemenServiceProperties; +import nl.ictu.service.v1.crypto.AesGcmCryptographerImpl; +import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; +import nl.ictu.service.v1.crypto.AesGcmSivCryptographerImpl; +import nl.ictu.service.v1.crypto.IdentifierConverterImpl; import nl.ictu.utils.Base64Wrapper; import nl.ictu.utils.MessageDigestUtil; import org.junit.jupiter.api.Test; From e37080d5f6123b3ddeadde9b982fb2dcf76cb760 Mon Sep 17 00:00:00 2001 From: sayf Date: Mon, 23 Dec 2024 14:27:29 +0100 Subject: [PATCH 08/30] More refactoring to responsibility --- .../exception/InvalidOINException.java | 9 +++++ .../v1/ExchangeIdentifierController.java | 19 --------- .../v1/ExchangeTokenController.java | 40 ++++++------------- .../controller/v1/GetTokenController.java | 8 ++-- .../nl/ictu/service/v1/GetTokenService.java | 12 ++---- .../v1/crypto/AesGcmSivCryptographer.java | 3 +- 6 files changed, 29 insertions(+), 62 deletions(-) create mode 100644 src/main/java/nl/ictu/controller/exception/InvalidOINException.java diff --git a/src/main/java/nl/ictu/controller/exception/InvalidOINException.java b/src/main/java/nl/ictu/controller/exception/InvalidOINException.java new file mode 100644 index 0000000..6a168e0 --- /dev/null +++ b/src/main/java/nl/ictu/controller/exception/InvalidOINException.java @@ -0,0 +1,9 @@ +package nl.ictu.controller.exception; + +public class InvalidOINException extends RuntimeException { + + public InvalidOINException(final String message) { + + super(message); + } +} diff --git a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java index 020f81b..2ce3ce8 100644 --- a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java +++ b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java @@ -31,63 +31,44 @@ public ResponseEntity exchangeIdentifier(final Str final var wsIdentifierRequest = wsExchangeIdentifierForIdentifierRequest.getIdentifier(); final var recipientOIN = wsExchangeIdentifierForIdentifierRequest.getRecipientOIN(); final var recipientIdentifierType = wsExchangeIdentifierForIdentifierRequest.getRecipientIdentifierType(); - if (BSN.equals(wsIdentifierRequest.getType()) && ORGANISATION_PSEUDO.equals( recipientIdentifierType)) { // from BSN to Org Pseudo return ResponseEntity.ok( convertBsnToPseudo(wsIdentifierRequest.getValue(), recipientOIN)); - } else if (ORGANISATION_PSEUDO.equals(wsIdentifierRequest.getType()) && BSN.equals( recipientIdentifierType)) { // from BSN to Org Pseudo return ResponseEntity.ok( convertPseudoToBEsn(wsIdentifierRequest.getValue(), recipientOIN)); - } else { return ResponseEntity.status(UNPROCESSABLE_ENTITY).build(); } - - } private WsExchangeIdentifierResponse convertBsnToPseudo(final String bsn, final String oin) throws IOException, InvalidCipherTextException { final Identifier identifier = new Identifier(); - identifier.setBsn(bsn); - final String oinNencyptedIdentifier = aesGcmSivCryptographer.encrypt(identifier, oin); - final WsExchangeIdentifierResponse wsExchangeTokenForIdentifier200Response = new WsExchangeIdentifierResponse(); - final WsIdentifier wsIdentifierResponse = new WsIdentifier(); - wsIdentifierResponse.setType(ORGANISATION_PSEUDO); wsIdentifierResponse.setValue(oinNencyptedIdentifier); - wsExchangeTokenForIdentifier200Response.setIdentifier(wsIdentifierResponse); - return wsExchangeTokenForIdentifier200Response; - } private WsExchangeIdentifierResponse convertPseudoToBEsn(final String pseudo, final String oin) throws IOException, InvalidCipherTextException { final Identifier identifier = aesGcmSivCryptographer.decrypt(pseudo, oin); - final WsExchangeIdentifierResponse wsExchangeTokenForIdentifier200Response = new WsExchangeIdentifierResponse(); - final WsIdentifier wsIdentifierResponse = new WsIdentifier(); - wsIdentifierResponse.setType(BSN); wsIdentifierResponse.setValue(identifier.getBsn()); - wsExchangeTokenForIdentifier200Response.setIdentifier(wsIdentifierResponse); - return wsExchangeTokenForIdentifier200Response; - } } diff --git a/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java b/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java index a3a06d1..08dd95e 100644 --- a/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java +++ b/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java @@ -1,10 +1,14 @@ package nl.ictu.controller.v1; +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; +import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; + import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import nl.ictu.Identifier; -import nl.ictu.Token; +import nl.ictu.controller.exception.InvalidOINException; import nl.ictu.pseudoniemenservice.generated.server.api.ExchangeTokenApi; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenRequest; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenResponse; @@ -15,64 +19,44 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; -import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; -import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; -import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; - @Slf4j @RequiredArgsConstructor @RestController public final class ExchangeTokenController implements ExchangeTokenApi, VersionOneController { private final AesGcmCryptographer aesGcmCryptographer; - private final AesGcmSivCryptographer aesGcmSivCryptographer; - private final TokenConverter tokenConverter; @Override @SneakyThrows - public ResponseEntity exchangeToken(final String callerOIN, final WsExchangeTokenRequest wsExchangeTokenForIdentifierRequest) { - - final String encodedToken = aesGcmCryptographer.decrypt(wsExchangeTokenForIdentifierRequest.getToken(), callerOIN); - - final Token token = tokenConverter.decode(encodedToken); + public ResponseEntity exchangeToken(final String callerOIN, + final WsExchangeTokenRequest wsExchangeTokenForIdentifierRequest) { + final var encodedToken = aesGcmCryptographer.decrypt(wsExchangeTokenForIdentifierRequest.getToken(), callerOIN); + final var token = tokenConverter.decode(encodedToken); if (!callerOIN.equals(token.getRecipientOIN())) { - throw new RuntimeException("Sink OIN not the same"); + throw new InvalidOINException("Sink OIN not the same"); } - - final WsExchangeTokenResponse wsExchangeTokenForIdentifier200Response = new WsExchangeTokenResponse(); - - final WsIdentifier wsIdentifier = new WsIdentifier(); - + final var wsExchangeTokenForIdentifier200Response = new WsExchangeTokenResponse(); + final var wsIdentifier = new WsIdentifier(); switch (wsExchangeTokenForIdentifierRequest.getIdentifierType()) { case BSN -> { wsIdentifier.setType(BSN); wsIdentifier.setValue(token.getBsn()); } case ORGANISATION_PSEUDO -> { - final Identifier identifier = new Identifier(); - identifier.setBsn(token.getBsn()); - final String encrypt = aesGcmSivCryptographer.encrypt(identifier, callerOIN); - wsIdentifier.setType(ORGANISATION_PSEUDO); wsIdentifier.setValue(encrypt); - } default -> { return ResponseEntity.status(UNPROCESSABLE_ENTITY).build(); } - } - - wsExchangeTokenForIdentifier200Response.setIdentifier(wsIdentifier); - return ResponseEntity.ok(wsExchangeTokenForIdentifier200Response); - } } diff --git a/src/main/java/nl/ictu/controller/v1/GetTokenController.java b/src/main/java/nl/ictu/controller/v1/GetTokenController.java index ec862f4..cb5d442 100644 --- a/src/main/java/nl/ictu/controller/v1/GetTokenController.java +++ b/src/main/java/nl/ictu/controller/v1/GetTokenController.java @@ -23,14 +23,12 @@ public final class GetTokenController implements GetTokenApi, VersionOneControll public ResponseEntity getToken(final String callerOIN, final WsGetTokenRequest wsGetTokenRequest) { - - final var wsGetToken200Response = getTokenService.getWsGetTokenResponse(wsGetTokenRequest); + final var recipientOIN = wsGetTokenRequest.getRecipientOIN(); + final var identifier = wsGetTokenRequest.getIdentifier(); + final var wsGetToken200Response = getTokenService.getWsGetTokenResponse(recipientOIN, identifier); if (wsGetToken200Response != null) { return ResponseEntity.ok(wsGetToken200Response); } return ResponseEntity.status(UNPROCESSABLE_ENTITY).build(); - } - - } diff --git a/src/main/java/nl/ictu/service/v1/GetTokenService.java b/src/main/java/nl/ictu/service/v1/GetTokenService.java index b001fa0..8339194 100644 --- a/src/main/java/nl/ictu/service/v1/GetTokenService.java +++ b/src/main/java/nl/ictu/service/v1/GetTokenService.java @@ -14,7 +14,6 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import nl.ictu.Token; -import nl.ictu.pseudoniemenservice.generated.server.model.WsGetTokenRequest; import nl.ictu.pseudoniemenservice.generated.server.model.WsGetTokenResponse; import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; import nl.ictu.service.v1.crypto.AesGcmCryptographer; @@ -32,16 +31,13 @@ public final class GetTokenService { private final AesGcmSivCryptographer aesGcmSivCryptographer; private final TokenConverter tokenConverter; - - public WsGetTokenResponse getWsGetTokenResponse( - final WsGetTokenRequest wsGetTokenRequest) - throws IOException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { + @SneakyThrows + public WsGetTokenResponse getWsGetTokenResponse(final String recipientOIN, final WsIdentifier identifier) { final var creationDate = System.currentTimeMillis(); - final var recipientOIN = wsGetTokenRequest.getRecipientOIN(); - final var identifier = wsGetTokenRequest.getIdentifier(); + // check is callerOIN allowed to communicatie with sinkOIN - if (identifier != null && identifier.getValue() != null && identifier.getType() != null) { + if (identifier != null) { final String bsn = mapBsn(identifier, recipientOIN); if (bsn != null) { diff --git a/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java b/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java index d0076fa..826386f 100644 --- a/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java +++ b/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java @@ -1,11 +1,10 @@ package nl.ictu.service.v1.crypto; import com.fasterxml.jackson.core.JsonProcessingException; +import java.io.IOException; import nl.ictu.Identifier; import org.bouncycastle.crypto.InvalidCipherTextException; -import java.io.IOException; - public interface AesGcmSivCryptographer { String encrypt(Identifier identifier, String salt) throws InvalidCipherTextException, IOException; From 9ed0b30da80ae3896a2603b0c1a17f2f1710fd6b Mon Sep 17 00:00:00 2001 From: sayf Date: Mon, 23 Dec 2024 14:35:02 +0100 Subject: [PATCH 09/30] More refactoring to remove unnecessary code --- .../v1/crypto/AesGcmCryptographer.java | 82 ++++++++++++++---- .../v1/crypto/AesGcmCryptographerImpl.java | 77 ----------------- .../v1/crypto/AesGcmSivCryptographer.java | 75 ++++++++++++++++- .../v1/crypto/AesGcmSivCryptographerImpl.java | 84 ------------------- .../v1/crypto/IdentifierConverter.java | 25 +++++- .../v1/crypto/IdentifierConverterImpl.java | 32 ------- .../service/v1/crypto/TokenConverter.java | 28 ++++++- .../service/v1/crypto/TokenConverterImpl.java | 33 -------- .../ictu/service/TestAesGcmCryptographer.java | 5 +- .../service/TestAesGcmSivCryptographer.java | 11 ++- 10 files changed, 190 insertions(+), 262 deletions(-) delete mode 100644 src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographerImpl.java delete mode 100644 src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographerImpl.java delete mode 100644 src/main/java/nl/ictu/service/v1/crypto/IdentifierConverterImpl.java delete mode 100644 src/main/java/nl/ictu/service/v1/crypto/TokenConverterImpl.java diff --git a/src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographer.java b/src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographer.java index 4101be0..217eadc 100644 --- a/src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographer.java +++ b/src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographer.java @@ -1,27 +1,75 @@ package nl.ictu.service.v1.crypto; +import static nl.ictu.utils.AESHelper.IV_LENGTH; + +import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.util.Arrays; import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import lombok.RequiredArgsConstructor; +import nl.ictu.configuration.PseudoniemenServiceProperties; +import nl.ictu.utils.AESHelper; +import nl.ictu.utils.Base64Wrapper; +import nl.ictu.utils.ByteArrayUtils; +import nl.ictu.utils.MessageDigestUtil; +import org.springframework.stereotype.Service; + +/** + * Advanced Encryption Standard Galois/Counter Mode (AES-GCM). + */ + +@SuppressWarnings("DesignForExtension") +@Service +@RequiredArgsConstructor +public class AesGcmCryptographer { + + private final Base64Wrapper base64Wrapper; + private final MessageDigestUtil messageDigestUtil; + private final PseudoniemenServiceProperties pseudoniemenServiceProperties; + + public String encrypt(final String plaintext, final String salt) + throws IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { + + final var cipher = AESHelper.createCipher(); + final var gcmParameterSpec = AESHelper.generateIV(); + final var secretKey = createSecretKey(salt); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec); + final var ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); + final var gcmIV = gcmParameterSpec.getIV(); + final var encryptedWithIV = ByteArrayUtils.concat(gcmIV, ciphertext); + return base64Wrapper.encodeToString(encryptedWithIV); + } + + private SecretKey createSecretKey(final String salt) { + + final var keyBytes = base64Wrapper.decode( + pseudoniemenServiceProperties.getTokenPrivateKey()); + final var saltBytes = salt.getBytes(StandardCharsets.UTF_8); + final var salterSecretBytes = ByteArrayUtils.concat(keyBytes, saltBytes); + final var key = messageDigestUtil.getMessageDigestSha256().digest(salterSecretBytes); + return new SecretKeySpec(key, "AES"); + + } + + public String decrypt(final String ciphertextWithIv, final String salt) + throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { -public interface AesGcmCryptographer { - - String encrypt(String plaintext, String salt) - throws IllegalBlockSizeException, - BadPaddingException, - InvalidAlgorithmParameterException, - InvalidKeyException, - NoSuchAlgorithmException, - NoSuchPaddingException; - - String decrypt(String ciphertext, String salt) - throws NoSuchPaddingException, - NoSuchAlgorithmException, - InvalidAlgorithmParameterException, - InvalidKeyException, - IllegalBlockSizeException, - BadPaddingException; + final var cipher = AESHelper.createCipher(); + final var encryptedWithIV = base64Wrapper.decode(ciphertextWithIv); + final var iv = Arrays.copyOfRange(encryptedWithIV, 0, IV_LENGTH); + final var ciphertext = Arrays.copyOfRange(encryptedWithIV, IV_LENGTH, + encryptedWithIV.length); + final var gcmParameterSpec = AESHelper.createIVfromValues(iv); + final var secretKey = createSecretKey(salt); + cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec); + final var decryptedText = cipher.doFinal(ciphertext); + return new String(decryptedText, StandardCharsets.UTF_8); + } } diff --git a/src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographerImpl.java b/src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographerImpl.java deleted file mode 100644 index 100330c..0000000 --- a/src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographerImpl.java +++ /dev/null @@ -1,77 +0,0 @@ -package nl.ictu.service.v1.crypto; - -import static nl.ictu.utils.AESHelper.IV_LENGTH; - -import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import lombok.RequiredArgsConstructor; -import nl.ictu.configuration.PseudoniemenServiceProperties; -import nl.ictu.utils.AESHelper; -import nl.ictu.utils.Base64Wrapper; -import nl.ictu.utils.ByteArrayUtils; -import nl.ictu.utils.MessageDigestUtil; -import org.springframework.stereotype.Service; - -/** - * Advanced Encryption Standard Galois/Counter Mode (AES-GCM). - */ - -@SuppressWarnings("DesignForExtension") -@Service -@RequiredArgsConstructor -public class AesGcmCryptographerImpl implements AesGcmCryptographer { - - private final Base64Wrapper base64Wrapper; - private final MessageDigestUtil messageDigestUtil; - private final PseudoniemenServiceProperties pseudoniemenServiceProperties; - - @Override - public String encrypt(final String plaintext, final String salt) - throws IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { - - final var cipher = AESHelper.createCipher(); - final var gcmParameterSpec = AESHelper.generateIV(); - final var secretKey = createSecretKey(salt); - cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec); - final var ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); - final var gcmIV = gcmParameterSpec.getIV(); - final var encryptedWithIV = ByteArrayUtils.concat(gcmIV, ciphertext); - return base64Wrapper.encodeToString(encryptedWithIV); - } - - private SecretKey createSecretKey(final String salt) { - - final var keyBytes = base64Wrapper.decode( - pseudoniemenServiceProperties.getTokenPrivateKey()); - final var saltBytes = salt.getBytes(StandardCharsets.UTF_8); - final var salterSecretBytes = ByteArrayUtils.concat(keyBytes, saltBytes); - final var key = messageDigestUtil.getMessageDigestSha256().digest(salterSecretBytes); - return new SecretKeySpec(key, "AES"); - - } - - @Override - public String decrypt(final String ciphertextWithIv, final String salt) - throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { - - final var cipher = AESHelper.createCipher(); - final var encryptedWithIV = base64Wrapper.decode(ciphertextWithIv); - final var iv = Arrays.copyOfRange(encryptedWithIV, 0, IV_LENGTH); - final var ciphertext = Arrays.copyOfRange(encryptedWithIV, IV_LENGTH, - encryptedWithIV.length); - final var gcmParameterSpec = AESHelper.createIVfromValues(iv); - final var secretKey = createSecretKey(salt); - cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec); - final var decryptedText = cipher.doFinal(ciphertext); - return new String(decryptedText, StandardCharsets.UTF_8); - } -} diff --git a/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java b/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java index 826386f..f092016 100644 --- a/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java +++ b/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java @@ -2,12 +2,81 @@ import com.fasterxml.jackson.core.JsonProcessingException; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import nl.ictu.Identifier; +import nl.ictu.configuration.PseudoniemenServiceProperties; +import nl.ictu.utils.AESHelper; +import nl.ictu.utils.Base64Wrapper; +import nl.ictu.utils.MessageDigestUtil; import org.bouncycastle.crypto.InvalidCipherTextException; +import org.bouncycastle.crypto.modes.GCMSIVBlockCipher; +import org.bouncycastle.crypto.params.AEADParameters; +import org.bouncycastle.crypto.params.KeyParameter; +import org.springframework.stereotype.Service; -public interface AesGcmSivCryptographer { +/** + * Advanced Encryption Standard Galois/Counter Mode synthetic initialization vector. + */ - String encrypt(Identifier identifier, String salt) throws InvalidCipherTextException, IOException; +@Slf4j +@SuppressWarnings("DesignForExtension") +@Service +@RequiredArgsConstructor +public class AesGcmSivCryptographer { - Identifier decrypt(String ciphertext, String salt) throws InvalidCipherTextException, JsonProcessingException; + public static final int MAC_SIZE = 128; + private static final int NONCE_LENTH = 12; + + private final PseudoniemenServiceProperties pseudoniemenServiceProperties; + private final MessageDigestUtil messageDigestUtil; + private final IdentifierConverter identifierConverter; + private final Base64Wrapper base64Wrapper; + + private AEADParameters createSecretKey(final String salt) { + + final var nonce16 = messageDigestUtil.getMessageDigestSha256() + .digest(salt.getBytes(StandardCharsets.UTF_8)); + final var nonce12 = Arrays.copyOf(nonce16, NONCE_LENTH); + final String identifierPrivateKey = pseudoniemenServiceProperties.getIdentifierPrivateKey(); + final KeyParameter keyParameter = new KeyParameter( + base64Wrapper.decode(identifierPrivateKey)); + return new AEADParameters(keyParameter, MAC_SIZE, nonce12); + + } + + public String encrypt(final Identifier identifier, final String salt) + throws InvalidCipherTextException, IOException { + + final var plaintext = identifierConverter.encode(identifier); + final var cipher = new GCMSIVBlockCipher(AESHelper.getAESEngine()); + cipher.init(true, createSecretKey(salt)); + final var plainTextBytes = plaintext.getBytes(StandardCharsets.UTF_8); + final var ciphertext = new byte[cipher.getOutputSize(plainTextBytes.length)]; + final var outputLength = cipher.processBytes(plainTextBytes, 0, plainTextBytes.length, + ciphertext, 0); + cipher.doFinal(ciphertext, outputLength); + cipher.reset(); + return base64Wrapper.encodeToString(ciphertext); + + } + + public Identifier decrypt(final String ciphertextString, final String salt) + throws InvalidCipherTextException, JsonProcessingException { + + final var cipher = new GCMSIVBlockCipher(AESHelper.getAESEngine()); + cipher.init(false, createSecretKey(salt)); + final var ciphertext = base64Wrapper.decode(ciphertextString); + final var plaintext = new byte[cipher.getOutputSize(ciphertext.length)]; + final var outputLength = cipher.processBytes(ciphertext, 0, ciphertext.length, plaintext, + 0); + cipher.doFinal(plaintext, outputLength); + cipher.reset(); + final var encodedIdentifier = new String(plaintext, StandardCharsets.UTF_8); + return identifierConverter.decode(encodedIdentifier); + } } + + diff --git a/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographerImpl.java b/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographerImpl.java deleted file mode 100644 index 7d6da30..0000000 --- a/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographerImpl.java +++ /dev/null @@ -1,84 +0,0 @@ -package nl.ictu.service.v1.crypto; - -import com.fasterxml.jackson.core.JsonProcessingException; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import nl.ictu.Identifier; -import nl.ictu.configuration.PseudoniemenServiceProperties; -import nl.ictu.utils.AESHelper; -import nl.ictu.utils.Base64Wrapper; -import nl.ictu.utils.MessageDigestUtil; -import org.bouncycastle.crypto.InvalidCipherTextException; -import org.bouncycastle.crypto.modes.GCMSIVBlockCipher; -import org.bouncycastle.crypto.params.AEADParameters; -import org.bouncycastle.crypto.params.KeyParameter; -import org.springframework.stereotype.Service; - -/** - * Advanced Encryption Standard Galois/Counter Mode synthetic initialization vector. - */ - -@Slf4j -@SuppressWarnings("DesignForExtension") -@Service -@RequiredArgsConstructor -public class AesGcmSivCryptographerImpl implements AesGcmSivCryptographer { - - public static final int MAC_SIZE = 128; - private static final int NONCE_LENTH = 12; - - private final PseudoniemenServiceProperties pseudoniemenServiceProperties; - private final MessageDigestUtil messageDigestUtil; - private final IdentifierConverter identifierConverter; - private final Base64Wrapper base64Wrapper; - - private AEADParameters createSecretKey(final String salt) { - - final var nonce16 = messageDigestUtil.getMessageDigestSha256() - .digest(salt.getBytes(StandardCharsets.UTF_8)); - final var nonce12 = Arrays.copyOf(nonce16, NONCE_LENTH); - final String identifierPrivateKey = pseudoniemenServiceProperties.getIdentifierPrivateKey(); - final KeyParameter keyParameter = new KeyParameter( - base64Wrapper.decode(identifierPrivateKey)); - return new AEADParameters(keyParameter, MAC_SIZE, nonce12); - - } - - @Override - public String encrypt(final Identifier identifier, final String salt) - throws InvalidCipherTextException, IOException { - - final var plaintext = identifierConverter.encode(identifier); - final var cipher = new GCMSIVBlockCipher(AESHelper.getAESEngine()); - cipher.init(true, createSecretKey(salt)); - final var plainTextBytes = plaintext.getBytes(StandardCharsets.UTF_8); - final var ciphertext = new byte[cipher.getOutputSize(plainTextBytes.length)]; - final var outputLength = cipher.processBytes(plainTextBytes, 0, plainTextBytes.length, - ciphertext, 0); - cipher.doFinal(ciphertext, outputLength); - cipher.reset(); - return base64Wrapper.encodeToString(ciphertext); - - } - - @Override - public Identifier decrypt(final String ciphertextString, final String salt) - throws InvalidCipherTextException, JsonProcessingException { - - final var cipher = new GCMSIVBlockCipher(AESHelper.getAESEngine()); - cipher.init(false, createSecretKey(salt)); - final var ciphertext = base64Wrapper.decode(ciphertextString); - final var plaintext = new byte[cipher.getOutputSize(ciphertext.length)]; - final var outputLength = cipher.processBytes(ciphertext, 0, ciphertext.length, plaintext, - 0); - cipher.doFinal(plaintext, outputLength); - cipher.reset(); - final var encodedIdentifier = new String(plaintext, StandardCharsets.UTF_8); - return identifierConverter.decode(encodedIdentifier); - } -} - - diff --git a/src/main/java/nl/ictu/service/v1/crypto/IdentifierConverter.java b/src/main/java/nl/ictu/service/v1/crypto/IdentifierConverter.java index fcb5952..4cc7932 100644 --- a/src/main/java/nl/ictu/service/v1/crypto/IdentifierConverter.java +++ b/src/main/java/nl/ictu/service/v1/crypto/IdentifierConverter.java @@ -1,12 +1,31 @@ package nl.ictu.service.v1.crypto; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; +import java.io.StringWriter; +import lombok.RequiredArgsConstructor; import nl.ictu.Identifier; +import org.springframework.aot.hint.annotation.RegisterReflectionForBinding; +import org.springframework.stereotype.Service; -public interface IdentifierConverter { +@SuppressWarnings("DesignForExtension") +@Service +@RequiredArgsConstructor +@RegisterReflectionForBinding({Identifier.class}) +public class IdentifierConverter { - String encode(Identifier identifier) throws IOException; + private final ObjectMapper objectMapper; - Identifier decode(String encodedIdentifier) throws JsonProcessingException; + public String encode(final Identifier identifier) throws IOException { + + final StringWriter stringWriter = new StringWriter(); + objectMapper.writeValue(stringWriter, identifier); + return stringWriter.toString(); + } + + public Identifier decode(final String encodedIdentifier) throws JsonProcessingException { + + return objectMapper.readValue(encodedIdentifier, Identifier.class); + } } diff --git a/src/main/java/nl/ictu/service/v1/crypto/IdentifierConverterImpl.java b/src/main/java/nl/ictu/service/v1/crypto/IdentifierConverterImpl.java deleted file mode 100644 index 911eeff..0000000 --- a/src/main/java/nl/ictu/service/v1/crypto/IdentifierConverterImpl.java +++ /dev/null @@ -1,32 +0,0 @@ -package nl.ictu.service.v1.crypto; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; -import nl.ictu.Identifier; -import org.springframework.aot.hint.annotation.RegisterReflectionForBinding; -import org.springframework.stereotype.Service; - -import java.io.IOException; -import java.io.StringWriter; - -@SuppressWarnings("DesignForExtension") -@Service -@RequiredArgsConstructor -@RegisterReflectionForBinding({Identifier.class}) -public class IdentifierConverterImpl implements IdentifierConverter { - - private final ObjectMapper objectMapper; - - @Override - public String encode(final Identifier identifier) throws IOException { - final StringWriter stringWriter = new StringWriter(); - objectMapper.writeValue(stringWriter, identifier); - return stringWriter.toString(); - } - - @Override - public Identifier decode(final String encodedIdentifier) throws JsonProcessingException { - return objectMapper.readValue(encodedIdentifier, Identifier.class); - } -} diff --git a/src/main/java/nl/ictu/service/v1/crypto/TokenConverter.java b/src/main/java/nl/ictu/service/v1/crypto/TokenConverter.java index 2c3aecc..b6d3c00 100644 --- a/src/main/java/nl/ictu/service/v1/crypto/TokenConverter.java +++ b/src/main/java/nl/ictu/service/v1/crypto/TokenConverter.java @@ -1,12 +1,32 @@ package nl.ictu.service.v1.crypto; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.io.StringWriter; +import lombok.RequiredArgsConstructor; import nl.ictu.Token; +import org.springframework.aot.hint.annotation.RegisterReflectionForBinding; +import org.springframework.stereotype.Service; -import java.io.IOException; +@SuppressWarnings("DesignForExtension") +@Service +@RequiredArgsConstructor +@RegisterReflectionForBinding({Token.class}) +public class TokenConverter { + + private final ObjectMapper objectMapper; + + public String encode(final Token token) throws IOException { + + final StringWriter stringWriter = new StringWriter(); + objectMapper.writeValue(stringWriter, token); + return stringWriter.toString(); + } + + public Token decode(final String encodedToken) throws JsonProcessingException { -public interface TokenConverter { - String encode(Token token) throws IOException; + return objectMapper.readValue(encodedToken, Token.class); + } - Token decode(String encodedToken) throws JsonProcessingException; } diff --git a/src/main/java/nl/ictu/service/v1/crypto/TokenConverterImpl.java b/src/main/java/nl/ictu/service/v1/crypto/TokenConverterImpl.java deleted file mode 100644 index 635f9a7..0000000 --- a/src/main/java/nl/ictu/service/v1/crypto/TokenConverterImpl.java +++ /dev/null @@ -1,33 +0,0 @@ -package nl.ictu.service.v1.crypto; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; -import nl.ictu.Token; -import org.springframework.aot.hint.annotation.RegisterReflectionForBinding; -import org.springframework.stereotype.Service; - -import java.io.IOException; -import java.io.StringWriter; - -@SuppressWarnings("DesignForExtension") -@Service -@RequiredArgsConstructor -@RegisterReflectionForBinding({Token.class}) -public class TokenConverterImpl implements TokenConverter { - - private final ObjectMapper objectMapper; - - @Override - public String encode(final Token token) throws IOException { - final StringWriter stringWriter = new StringWriter(); - objectMapper.writeValue(stringWriter, token); - return stringWriter.toString(); - } - - @Override - public Token decode(final String encodedToken) throws JsonProcessingException { - return objectMapper.readValue(encodedToken, Token.class); - } - -} diff --git a/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java b/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java index ddd74b2..3f74503 100644 --- a/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java +++ b/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java @@ -9,21 +9,20 @@ import lombok.extern.slf4j.Slf4j; import nl.ictu.configuration.PseudoniemenServiceProperties; import nl.ictu.service.v1.crypto.AesGcmCryptographer; -import nl.ictu.service.v1.crypto.AesGcmCryptographerImpl; import nl.ictu.utils.Base64Wrapper; import nl.ictu.utils.MessageDigestUtil; import org.junit.jupiter.api.Test; import org.springframework.test.context.ActiveProfiles; /** - * Class for tesing {@link AesGcmCryptographerImpl} + * Class for tesing {@link AesGcmCryptographer} */ @Slf4j @ActiveProfiles("test") public class TestAesGcmCryptographer { - private final AesGcmCryptographer aesGcmCryptographer = new AesGcmCryptographerImpl( + private final AesGcmCryptographer aesGcmCryptographer = new AesGcmCryptographer( new Base64Wrapper(), new MessageDigestUtil(), new PseudoniemenServiceProperties().setTokenPrivateKey( diff --git a/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java b/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java index d56d0c4..1319706 100644 --- a/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java +++ b/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java @@ -10,28 +10,27 @@ import lombok.extern.slf4j.Slf4j; import nl.ictu.Identifier; import nl.ictu.configuration.PseudoniemenServiceProperties; -import nl.ictu.service.v1.crypto.AesGcmCryptographerImpl; +import nl.ictu.service.v1.crypto.AesGcmCryptographer; import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; -import nl.ictu.service.v1.crypto.AesGcmSivCryptographerImpl; -import nl.ictu.service.v1.crypto.IdentifierConverterImpl; +import nl.ictu.service.v1.crypto.IdentifierConverter; import nl.ictu.utils.Base64Wrapper; import nl.ictu.utils.MessageDigestUtil; import org.junit.jupiter.api.Test; import org.springframework.test.context.ActiveProfiles; /** - * Class for tesing {@link AesGcmCryptographerImpl} + * Class for tesing {@link AesGcmCryptographer} */ @Slf4j @ActiveProfiles("test") public class TestAesGcmSivCryptographer { - private final AesGcmSivCryptographer aesGcmSivCryptographer = new AesGcmSivCryptographerImpl( + private final AesGcmSivCryptographer aesGcmSivCryptographer = new AesGcmSivCryptographer( new PseudoniemenServiceProperties().setIdentifierPrivateKey( "QTBtVEhLN3EwMHJ3QXN1ZUFqNzVrT3hDQTBIWWNIZTU="), new MessageDigestUtil(), - new IdentifierConverterImpl(new ObjectMapper()), + new IdentifierConverter(new ObjectMapper()), new Base64Wrapper() ); From 119bdb7de3379cf33904aaa4a38b476b89bafd47 Mon Sep 17 00:00:00 2001 From: sayf Date: Mon, 23 Dec 2024 14:38:06 +0100 Subject: [PATCH 10/30] More refactoring to remove unnecessary code --- src/test/java/nl/ictu/service/TestAesGcmCryptographer.java | 6 +++--- .../java/nl/ictu/service/TestAesGcmSivCryptographer.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java b/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java index 3f74503..113a1b7 100644 --- a/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java +++ b/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java @@ -20,7 +20,7 @@ @Slf4j @ActiveProfiles("test") -public class TestAesGcmCryptographer { +class TestAesGcmCryptographer { private final AesGcmCryptographer aesGcmCryptographer = new AesGcmCryptographer( new Base64Wrapper(), @@ -34,7 +34,7 @@ public class TestAesGcmCryptographer { "38iKawKv9", "dk(gkzm)Mh", "gjk)s3$g9cQ")); @Test - public void testEncyptDecryptForDifferentStringLengths() { + void testEncyptDecryptForDifferentStringLengths() { testStrings.forEach(plain -> { @@ -52,7 +52,7 @@ public void testEncyptDecryptForDifferentStringLengths() { // Test to ensure ciphertext is different for the same plaintext due to IV randomness @Test - public void testCiphertextIsDifferentForSamePlaintext() throws Exception { + void testCiphertextIsDifferentForSamePlaintext() throws Exception { // The same plaintext message String plaintext = "This is a test message to ensure ciphertext is different!"; diff --git a/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java b/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java index 1319706..d005d2e 100644 --- a/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java +++ b/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java @@ -24,7 +24,7 @@ @Slf4j @ActiveProfiles("test") -public class TestAesGcmSivCryptographer { +class TestAesGcmSivCryptographer { private final AesGcmSivCryptographer aesGcmSivCryptographer = new AesGcmSivCryptographer( new PseudoniemenServiceProperties().setIdentifierPrivateKey( @@ -39,7 +39,7 @@ public class TestAesGcmSivCryptographer { "38iKawKv9", "dk(gkzm)Mh", "gjk)s3$g9cQ")); @Test - public void testEncyptDecryptForDifferentStringLengths() { + void testEncyptDecryptForDifferentStringLengths() { testStrings.forEach(plain -> { @@ -62,7 +62,7 @@ public void testEncyptDecryptForDifferentStringLengths() { // Test to ensure ciphertext is different for the same plaintext due to IV randomness @Test - public void testCiphertextIsTheSameForSamePlaintext() throws Exception { + void testCiphertextIsTheSameForSamePlaintext() throws Exception { // The same plaintext message String plaintext = "This is a test message to ensure ciphertext is different!"; From 46254c7f39e7e391570dff42de59bbb41d6c9e68 Mon Sep 17 00:00:00 2001 From: sayf Date: Mon, 23 Dec 2024 15:48:54 +0100 Subject: [PATCH 11/30] More refactoring to become more SOLID --- pom.xml | 3 + .../PseudoniemenServiceProperties.java | 3 +- .../v1/ExchangeTokenController.java | 42 ++---------- .../InvalidWsIdentifierTokenException.java | 8 +++ .../ictu/service/v1/ExchangeTokenService.java | 60 +++++++++++++++++ .../nl/ictu/service/v1/GetTokenService.java | 22 ++----- .../service/v1/map/EncryptedBsnMapper.java | 30 +++++++++ .../service/v1/validate/OINValidator.java | 22 +++++++ .../nl/ictu/TestingWebApplicationTests.java | 66 ++++++++----------- .../service/TestAesGcmSivCryptographer.java | 1 - 10 files changed, 165 insertions(+), 92 deletions(-) create mode 100644 src/main/java/nl/ictu/service/exception/InvalidWsIdentifierTokenException.java create mode 100644 src/main/java/nl/ictu/service/v1/ExchangeTokenService.java create mode 100644 src/main/java/nl/ictu/service/v1/map/EncryptedBsnMapper.java create mode 100644 src/main/java/nl/ictu/service/v1/validate/OINValidator.java diff --git a/pom.xml b/pom.xml index 98f2b60..79520f8 100644 --- a/pom.xml +++ b/pom.xml @@ -104,6 +104,9 @@ true + @lombok.Builder @lombok.NoArgsConstructor + @lombok.AllArgsConstructor + diff --git a/src/main/java/nl/ictu/configuration/PseudoniemenServiceProperties.java b/src/main/java/nl/ictu/configuration/PseudoniemenServiceProperties.java index 6bc7ad5..d34e53d 100644 --- a/src/main/java/nl/ictu/configuration/PseudoniemenServiceProperties.java +++ b/src/main/java/nl/ictu/configuration/PseudoniemenServiceProperties.java @@ -3,6 +3,7 @@ import jakarta.annotation.PostConstruct; import lombok.Data; import lombok.experimental.Accessors; +import nl.ictu.service.exception.IdentifierPrivateKeyException; import nl.ictu.service.exception.TokenPrivateKeyException; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @@ -25,7 +26,7 @@ public void validate() { throw new TokenPrivateKeyException("Please set a private token key"); } if (!StringUtils.hasText(identifierPrivateKey)) { - throw new RuntimeException("Please set a private identifier key"); + throw new IdentifierPrivateKeyException("Please set a private identifier key"); } } } diff --git a/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java b/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java index 08dd95e..c6003b1 100644 --- a/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java +++ b/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java @@ -1,21 +1,14 @@ package nl.ictu.controller.v1; -import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; -import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import nl.ictu.Identifier; -import nl.ictu.controller.exception.InvalidOINException; import nl.ictu.pseudoniemenservice.generated.server.api.ExchangeTokenApi; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenRequest; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenResponse; -import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; -import nl.ictu.service.v1.crypto.AesGcmCryptographer; -import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; -import nl.ictu.service.v1.crypto.TokenConverter; +import nl.ictu.service.v1.ExchangeTokenService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; @@ -24,39 +17,18 @@ @RestController public final class ExchangeTokenController implements ExchangeTokenApi, VersionOneController { - private final AesGcmCryptographer aesGcmCryptographer; - private final AesGcmSivCryptographer aesGcmSivCryptographer; - private final TokenConverter tokenConverter; + private final ExchangeTokenService exchangeTokenService; @Override @SneakyThrows public ResponseEntity exchangeToken(final String callerOIN, final WsExchangeTokenRequest wsExchangeTokenForIdentifierRequest) { - final var encodedToken = aesGcmCryptographer.decrypt(wsExchangeTokenForIdentifierRequest.getToken(), callerOIN); - final var token = tokenConverter.decode(encodedToken); - if (!callerOIN.equals(token.getRecipientOIN())) { - throw new InvalidOINException("Sink OIN not the same"); + try { + final var wsExchangeTokenResponse = exchangeTokenService.exchangeToken(callerOIN, wsExchangeTokenForIdentifierRequest); + return ResponseEntity.ok(wsExchangeTokenResponse); + } catch (Exception ex) { + return ResponseEntity.status(UNPROCESSABLE_ENTITY).build(); } - final var wsExchangeTokenForIdentifier200Response = new WsExchangeTokenResponse(); - final var wsIdentifier = new WsIdentifier(); - switch (wsExchangeTokenForIdentifierRequest.getIdentifierType()) { - case BSN -> { - wsIdentifier.setType(BSN); - wsIdentifier.setValue(token.getBsn()); - } - case ORGANISATION_PSEUDO -> { - final Identifier identifier = new Identifier(); - identifier.setBsn(token.getBsn()); - final String encrypt = aesGcmSivCryptographer.encrypt(identifier, callerOIN); - wsIdentifier.setType(ORGANISATION_PSEUDO); - wsIdentifier.setValue(encrypt); - } - default -> { - return ResponseEntity.status(UNPROCESSABLE_ENTITY).build(); - } - } - wsExchangeTokenForIdentifier200Response.setIdentifier(wsIdentifier); - return ResponseEntity.ok(wsExchangeTokenForIdentifier200Response); } } diff --git a/src/main/java/nl/ictu/service/exception/InvalidWsIdentifierTokenException.java b/src/main/java/nl/ictu/service/exception/InvalidWsIdentifierTokenException.java new file mode 100644 index 0000000..9e25a9d --- /dev/null +++ b/src/main/java/nl/ictu/service/exception/InvalidWsIdentifierTokenException.java @@ -0,0 +1,8 @@ +package nl.ictu.service.exception; + +public class InvalidWsIdentifierTokenException extends RuntimeException { + public InvalidWsIdentifierTokenException(final String message) { + + super(message); + } +} diff --git a/src/main/java/nl/ictu/service/v1/ExchangeTokenService.java b/src/main/java/nl/ictu/service/v1/ExchangeTokenService.java new file mode 100644 index 0000000..18d4063 --- /dev/null +++ b/src/main/java/nl/ictu/service/v1/ExchangeTokenService.java @@ -0,0 +1,60 @@ +package nl.ictu.service.v1; + +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import nl.ictu.Identifier; +import nl.ictu.controller.exception.InvalidOINException; +import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenRequest; +import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenResponse; +import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; +import nl.ictu.service.exception.InvalidWsIdentifierTokenException; +import nl.ictu.service.v1.crypto.AesGcmCryptographer; +import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; +import nl.ictu.service.v1.crypto.TokenConverter; +import nl.ictu.service.v1.validate.OINValidator; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RequiredArgsConstructor +@RestController +public final class ExchangeTokenService { + + private final AesGcmCryptographer aesGcmCryptographer; + private final AesGcmSivCryptographer aesGcmSivCryptographer; + private final TokenConverter tokenConverter; + private final OINValidator oinValidator; + + @SneakyThrows + public WsExchangeTokenResponse exchangeToken(final String callerOIN, + final WsExchangeTokenRequest wsExchangeTokenForIdentifierRequest) { + + final var encodedToken = aesGcmCryptographer.decrypt(wsExchangeTokenForIdentifierRequest.getToken(), callerOIN); + final var token = tokenConverter.decode(encodedToken); + if (!oinValidator.isValid(callerOIN, token)) { + throw new InvalidOINException("Sink OIN not the same"); + } + + final var wsIdentifier = new WsIdentifier(); + switch (wsExchangeTokenForIdentifierRequest.getIdentifierType()) { + case BSN -> { + wsIdentifier.setType(BSN); + wsIdentifier.setValue(token.getBsn()); + } + case ORGANISATION_PSEUDO -> { + final Identifier identifier = new Identifier(); + identifier.setBsn(token.getBsn()); + final String encrypt = aesGcmSivCryptographer.encrypt(identifier, callerOIN); + wsIdentifier.setType(ORGANISATION_PSEUDO); + wsIdentifier.setValue(encrypt); + } + default -> throw new InvalidWsIdentifierTokenException("Invalid identifier cannot be processed."); + } + return WsExchangeTokenResponse.builder() + .identifier(wsIdentifier) + .build(); + } +} diff --git a/src/main/java/nl/ictu/service/v1/GetTokenService.java b/src/main/java/nl/ictu/service/v1/GetTokenService.java index 8339194..af8a0ad 100644 --- a/src/main/java/nl/ictu/service/v1/GetTokenService.java +++ b/src/main/java/nl/ictu/service/v1/GetTokenService.java @@ -3,7 +3,6 @@ import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; -import com.fasterxml.jackson.core.JsonProcessingException; import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; @@ -17,9 +16,8 @@ import nl.ictu.pseudoniemenservice.generated.server.model.WsGetTokenResponse; import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; import nl.ictu.service.v1.crypto.AesGcmCryptographer; -import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; import nl.ictu.service.v1.crypto.TokenConverter; -import org.bouncycastle.crypto.InvalidCipherTextException; +import nl.ictu.service.v1.map.EncryptedBsnMapper; import org.springframework.stereotype.Service; @Service @@ -28,8 +26,8 @@ public final class GetTokenService { public static final String V_1 = "v1"; private final AesGcmCryptographer aesGcmCryptographer; - private final AesGcmSivCryptographer aesGcmSivCryptographer; private final TokenConverter tokenConverter; + private final EncryptedBsnMapper encryptedBsnMapper; @SneakyThrows public WsGetTokenResponse getWsGetTokenResponse(final String recipientOIN, final WsIdentifier identifier) { @@ -38,7 +36,6 @@ public WsGetTokenResponse getWsGetTokenResponse(final String recipientOIN, final // check is callerOIN allowed to communicatie with sinkOIN if (identifier != null) { - final String bsn = mapBsn(identifier, recipientOIN); if (bsn != null) { return createEncryptedToken(bsn, creationDate, recipientOIN); @@ -54,7 +51,7 @@ private String mapBsn(final WsIdentifier identifier, final String recipientOIN) if (BSN.equals(identifier.getType())) { return bsnValue; } else if (ORGANISATION_PSEUDO.equals(identifier.getType())) { - return mapDecryptedBsn(bsnValue, recipientOIN); + return encryptedBsnMapper.map(bsnValue, recipientOIN); } return null; } @@ -70,15 +67,8 @@ private WsGetTokenResponse createEncryptedToken(final String bsn, final long cre .recipientOIN(recipientOIN) .build()); final var encryptedToken = aesGcmCryptographer.encrypt(plainTextToken, recipientOIN); - final var wsGetToken200Response = new WsGetTokenResponse(); - wsGetToken200Response.token(encryptedToken); - return wsGetToken200Response; - } - - private String mapDecryptedBsn(final String bsnValue, final String recipientOIN) - throws InvalidCipherTextException, JsonProcessingException { - - final var decodedIdentifier = aesGcmSivCryptographer.decrypt(bsnValue, recipientOIN); - return decodedIdentifier.getBsn(); + return WsGetTokenResponse.builder() + .token(encryptedToken) + .build(); } } diff --git a/src/main/java/nl/ictu/service/v1/map/EncryptedBsnMapper.java b/src/main/java/nl/ictu/service/v1/map/EncryptedBsnMapper.java new file mode 100644 index 0000000..aa55da1 --- /dev/null +++ b/src/main/java/nl/ictu/service/v1/map/EncryptedBsnMapper.java @@ -0,0 +1,30 @@ +package nl.ictu.service.v1.map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import lombok.RequiredArgsConstructor; +import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class EncryptedBsnMapper { + + private final AesGcmSivCryptographer aesGcmSivCryptographer; + + /** + * Maps the encrypted business service number to its decrypted value using the given recipient OIN. + * + * @param bsnValue the encrypted business service number to be decrypted + * @param recipientOIN the recipient OIN key used for decryption + * @return the decrypted business service number + * @throws InvalidCipherTextException if an error occurs during decryption + * @throws JsonProcessingException if an error occurs during JSON processing + */ + public String map(final String bsnValue, final String recipientOIN) + throws InvalidCipherTextException, JsonProcessingException { + + final var decodedIdentifier = aesGcmSivCryptographer.decrypt(bsnValue, recipientOIN); + return decodedIdentifier.getBsn(); + } +} diff --git a/src/main/java/nl/ictu/service/v1/validate/OINValidator.java b/src/main/java/nl/ictu/service/v1/validate/OINValidator.java new file mode 100644 index 0000000..fc3ab34 --- /dev/null +++ b/src/main/java/nl/ictu/service/v1/validate/OINValidator.java @@ -0,0 +1,22 @@ +package nl.ictu.service.v1.validate; + +import lombok.RequiredArgsConstructor; +import nl.ictu.Token; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class OINValidator { + + /** + * Determines if the caller's OIN matches the recipient OIN from the token. + * + * @param callerOIN the OIN of the caller + * @param token the Token object containing recipient OIN + * @return true if the caller's OIN matches the recipient OIN, false otherwise + */ + public boolean isValid(final String callerOIN, final Token token) { + + return callerOIN.equals(token.getRecipientOIN()); + } +} diff --git a/src/test/java/nl/ictu/TestingWebApplicationTests.java b/src/test/java/nl/ictu/TestingWebApplicationTests.java index 09211e0..c0c0cde 100644 --- a/src/test/java/nl/ictu/TestingWebApplicationTests.java +++ b/src/test/java/nl/ictu/TestingWebApplicationTests.java @@ -1,5 +1,11 @@ package nl.ictu; +import static java.util.Map.of; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.MapEntry.entry; + +import java.util.List; +import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; @@ -11,17 +17,9 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.test.context.ActiveProfiles; import org.springframework.util.CollectionUtils; -import java.util.List; -import java.util.Map; - -import static java.util.Map.of; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.data.MapEntry.entry; - @Slf4j @ActiveProfiles("test") @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @@ -34,54 +32,44 @@ class TestingWebApplicationTests { private TestRestTemplate restTemplate; @Test - public void contextLoads() { + void contextLoads() { + } @Test - public void testActuatorHealthEndpoint() { + void testActuatorHealthEndpoint() { final int actuatorPort = environment.getProperty("local.management.port", Integer.class); - - assertThat( - restTemplate - .getForObject("http://localhost:" + actuatorPort + "/actuator/health", String.class) + assertThat(restTemplate.getForObject("http://localhost:" + actuatorPort + "/actuator/health", String.class) ).contains("{\"status\":\"UP\"}"); } @Test - public void testGetAtokenExchangeForBSN() { + void testGetAtokenExchangeForBSN() { // get a token - final Map getTokenBody = Map.of("recipientOIN", "54321543215432154321", "identifier", Map.of("type", "BSN", "value", "012345679")); - - final HttpEntity httpEntityGetToken = new HttpEntity(getTokenBody, new HttpHeaders(CollectionUtils.toMultiValueMap(of("callerOIN", List.of("0912345012345012345012345"))))); - - final ResponseEntity tokenExchange = restTemplate.exchange("/v1/getToken", HttpMethod.POST, httpEntityGetToken, Map.class); - + final var getTokenBody = Map.of("recipientOIN", "54321543215432154321", "identifier", Map.of("type", "BSN", "value", "012345679")); + final var httpEntityGetToken = new HttpEntity(getTokenBody, + new HttpHeaders(CollectionUtils.toMultiValueMap(of("callerOIN", List.of("0912345012345012345012345"))))); + final var tokenExchange = restTemplate.exchange("/v1/getToken", HttpMethod.POST, httpEntityGetToken, Map.class); assertThat(tokenExchange.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(tokenExchange) - .extracting("body") - .asInstanceOf(InstanceOfAssertFactories.map(String.class, Void.class)) - .containsKey("token"); - + .extracting("body") + .asInstanceOf(InstanceOfAssertFactories.map(String.class, Void.class)) + .containsKey("token"); // change token for identifier - - final String token = (String) tokenExchange.getBody().get("token"); - - final Map exchangeTokenBody = Map.of("token", token, "identifierType", "BSN"); - - final HttpEntity httpEntityExchangeToken = new HttpEntity(exchangeTokenBody, new HttpHeaders(CollectionUtils.toMultiValueMap(of("callerOIN", List.of("54321543215432154321"))))); - - final ResponseEntity identifierExchange = restTemplate.exchange("/v1/exchangeToken", HttpMethod.POST, httpEntityExchangeToken, Map.class); - + final var token = (String) tokenExchange.getBody().get("token"); + final var exchangeTokenBody = Map.of("token", token, "identifierType", "BSN"); + final var httpEntityExchangeToken = new HttpEntity(exchangeTokenBody, + new HttpHeaders(CollectionUtils.toMultiValueMap(of("callerOIN", List.of("54321543215432154321"))))); + final var identifierExchange = restTemplate.exchange("/v1/exchangeToken", HttpMethod.POST, httpEntityExchangeToken, + Map.class); assertThat(identifierExchange.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(identifierExchange) - .extracting("body") - .asInstanceOf(InstanceOfAssertFactories.map(String.class, Map.class)) - .containsExactly(entry("identifier", Map.of("type", "BSN", "value", "012345679"))); + .extracting("body") + .asInstanceOf(InstanceOfAssertFactories.map(String.class, Map.class)) + .containsExactly(entry("identifier", Map.of("type", "BSN", "value", "012345679"))); } diff --git a/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java b/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java index d005d2e..867fdd1 100644 --- a/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java +++ b/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java @@ -46,7 +46,6 @@ void testEncyptDecryptForDifferentStringLengths() { try { final Identifier identifier = new Identifier(); identifier.setBsn(plain); - final String crypted = aesGcmSivCryptographer.encrypt(identifier, "helloHowAreyo12345678"); final Identifier actual = aesGcmSivCryptographer.decrypt(crypted, From 809e10f61d680c0f88e88ef051b98a558a98139d Mon Sep 17 00:00:00 2001 From: sayf Date: Mon, 23 Dec 2024 16:16:38 +0100 Subject: [PATCH 12/30] Making sense of the madness --- src/main/java/nl/ictu/Identifier.java | 19 ++++---- .../v1/ExchangeIdentifierController.java | 43 +++++++++---------- .../ictu/service/v1/ExchangeTokenService.java | 8 ++-- .../ictu/service/v1/map/BsnPseudoMapper.java | 9 ++++ .../service/TestAesGcmSivCryptographer.java | 16 +++---- 5 files changed, 53 insertions(+), 42 deletions(-) create mode 100644 src/main/java/nl/ictu/service/v1/map/BsnPseudoMapper.java diff --git a/src/main/java/nl/ictu/Identifier.java b/src/main/java/nl/ictu/Identifier.java index c255372..647d829 100644 --- a/src/main/java/nl/ictu/Identifier.java +++ b/src/main/java/nl/ictu/Identifier.java @@ -1,14 +1,17 @@ package nl.ictu; -import lombok.Getter; -import lombok.Setter; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; -@Getter -@Setter -public final class Identifier { - @edu.umd.cs.findbugs.annotations.SuppressFBWarnings("SS_SHOULD_BE_STATIC") - private String version = "v1"; - private String bsn; +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Identifier { + private String version; + private String bsn; } diff --git a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java index 2ce3ce8..8c29ec4 100644 --- a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java +++ b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java @@ -21,6 +21,7 @@ @RestController public final class ExchangeIdentifierController implements ExchangeIdentifierApi, VersionOneController { + public static final String V_1 = "v1"; private final AesGcmSivCryptographer aesGcmSivCryptographer; @Override @@ -35,40 +36,38 @@ public ResponseEntity exchangeIdentifier(final Str recipientIdentifierType)) { // from BSN to Org Pseudo return ResponseEntity.ok( - convertBsnToPseudo(wsIdentifierRequest.getValue(), recipientOIN)); + mapBsnToPseudo(wsIdentifierRequest.getValue(), recipientOIN)); } else if (ORGANISATION_PSEUDO.equals(wsIdentifierRequest.getType()) && BSN.equals( recipientIdentifierType)) { // from BSN to Org Pseudo return ResponseEntity.ok( - convertPseudoToBEsn(wsIdentifierRequest.getValue(), recipientOIN)); - } else { - return ResponseEntity.status(UNPROCESSABLE_ENTITY).build(); + mapPseudoToBEsn(wsIdentifierRequest.getValue(), recipientOIN)); } + return ResponseEntity.status(UNPROCESSABLE_ENTITY).build(); } - private WsExchangeIdentifierResponse convertBsnToPseudo(final String bsn, final String oin) + private WsExchangeIdentifierResponse mapBsnToPseudo(final String bsn, final String oin) throws IOException, InvalidCipherTextException { - final Identifier identifier = new Identifier(); - identifier.setBsn(bsn); - final String oinNencyptedIdentifier = aesGcmSivCryptographer.encrypt(identifier, oin); - final WsExchangeIdentifierResponse wsExchangeTokenForIdentifier200Response = new WsExchangeIdentifierResponse(); - final WsIdentifier wsIdentifierResponse = new WsIdentifier(); - wsIdentifierResponse.setType(ORGANISATION_PSEUDO); - wsIdentifierResponse.setValue(oinNencyptedIdentifier); - wsExchangeTokenForIdentifier200Response.setIdentifier(wsIdentifierResponse); - return wsExchangeTokenForIdentifier200Response; + return WsExchangeIdentifierResponse.builder() + .identifier(WsIdentifier.builder() + .type(ORGANISATION_PSEUDO) + .value(aesGcmSivCryptographer.encrypt(Identifier.builder() + .version(V_1) + .bsn(bsn) + .build(), oin)) + .build()) + .build(); } - private WsExchangeIdentifierResponse convertPseudoToBEsn(final String pseudo, final String oin) + private WsExchangeIdentifierResponse mapPseudoToBEsn(final String pseudo, final String oin) throws IOException, InvalidCipherTextException { - final Identifier identifier = aesGcmSivCryptographer.decrypt(pseudo, oin); - final WsExchangeIdentifierResponse wsExchangeTokenForIdentifier200Response = new WsExchangeIdentifierResponse(); - final WsIdentifier wsIdentifierResponse = new WsIdentifier(); - wsIdentifierResponse.setType(BSN); - wsIdentifierResponse.setValue(identifier.getBsn()); - wsExchangeTokenForIdentifier200Response.setIdentifier(wsIdentifierResponse); - return wsExchangeTokenForIdentifier200Response; + return WsExchangeIdentifierResponse.builder() + .identifier(WsIdentifier.builder() + .type(BSN) + .value(aesGcmSivCryptographer.decrypt(pseudo, oin).getBsn()) + .build()) + .build(); } } diff --git a/src/main/java/nl/ictu/service/v1/ExchangeTokenService.java b/src/main/java/nl/ictu/service/v1/ExchangeTokenService.java index 18d4063..a45f142 100644 --- a/src/main/java/nl/ictu/service/v1/ExchangeTokenService.java +++ b/src/main/java/nl/ictu/service/v1/ExchangeTokenService.java @@ -23,6 +23,7 @@ @RestController public final class ExchangeTokenService { + public static final String V_1 = "v1"; private final AesGcmCryptographer aesGcmCryptographer; private final AesGcmSivCryptographer aesGcmSivCryptographer; private final TokenConverter tokenConverter; @@ -45,9 +46,10 @@ public WsExchangeTokenResponse exchangeToken(final String callerOIN, wsIdentifier.setValue(token.getBsn()); } case ORGANISATION_PSEUDO -> { - final Identifier identifier = new Identifier(); - identifier.setBsn(token.getBsn()); - final String encrypt = aesGcmSivCryptographer.encrypt(identifier, callerOIN); + final String encrypt = aesGcmSivCryptographer.encrypt(Identifier.builder() + .version(V_1) + .bsn(token.getBsn()) + .build(), callerOIN); wsIdentifier.setType(ORGANISATION_PSEUDO); wsIdentifier.setValue(encrypt); } diff --git a/src/main/java/nl/ictu/service/v1/map/BsnPseudoMapper.java b/src/main/java/nl/ictu/service/v1/map/BsnPseudoMapper.java new file mode 100644 index 0000000..d011072 --- /dev/null +++ b/src/main/java/nl/ictu/service/v1/map/BsnPseudoMapper.java @@ -0,0 +1,9 @@ +/* +package nl.ictu.service.v1.map; + +public class BsnPseudoMapper { + + + +} +*/ diff --git a/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java b/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java index 867fdd1..d9c7828 100644 --- a/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java +++ b/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java @@ -44,9 +44,9 @@ void testEncyptDecryptForDifferentStringLengths() { testStrings.forEach(plain -> { try { - final Identifier identifier = new Identifier(); - identifier.setBsn(plain); - final String crypted = aesGcmSivCryptographer.encrypt(identifier, + final String crypted = aesGcmSivCryptographer.encrypt(Identifier.builder() + .bsn(plain) + .build(), "helloHowAreyo12345678"); final Identifier actual = aesGcmSivCryptographer.decrypt(crypted, "helloHowAreyo12345678"); @@ -64,13 +64,11 @@ void testEncyptDecryptForDifferentStringLengths() { void testCiphertextIsTheSameForSamePlaintext() throws Exception { // The same plaintext message - String plaintext = "This is a test message to ensure ciphertext is different!"; + final var plaintext = "This is a test message to ensure ciphertext is different!"; + final var identifier = Identifier.builder().bsn(plaintext).build(); - final Identifier identifier = new Identifier(); - identifier.setBsn(plaintext); - - String encryptedMessage1 = aesGcmSivCryptographer.encrypt(identifier, "aniceSaltGorYu"); - String encryptedMessage2 = aesGcmSivCryptographer.encrypt(identifier, "aniceSaltGorYu"); + final var encryptedMessage1 = aesGcmSivCryptographer.encrypt(identifier, "aniceSaltGorYu"); + final var encryptedMessage2 = aesGcmSivCryptographer.encrypt(identifier, "aniceSaltGorYu"); // Assert that the two ciphertexts are different assertThat(encryptedMessage1).isEqualTo(encryptedMessage2); From b22f4ef6d848c087e89665fb0201c2e08baf38b9 Mon Sep 17 00:00:00 2001 From: sayf Date: Mon, 23 Dec 2024 21:06:46 +0100 Subject: [PATCH 13/30] More refactoring, split responsibilities --- .../v1/ExchangeIdentifierController.java | 43 ++++--------------- .../ictu/service/v1/map/BsnPseudoMapper.java | 39 ++++++++++++++++- .../ictu/service/v1/map/PseudoBsnMapper.java | 43 +++++++++++++++++++ 3 files changed, 88 insertions(+), 37 deletions(-) create mode 100644 src/main/java/nl/ictu/service/v1/map/PseudoBsnMapper.java diff --git a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java index 8c29ec4..29411d7 100644 --- a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java +++ b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java @@ -4,25 +4,23 @@ import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; -import java.io.IOException; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; -import nl.ictu.Identifier; import nl.ictu.pseudoniemenservice.generated.server.api.ExchangeIdentifierApi; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierRequest; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierResponse; -import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; -import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; -import org.bouncycastle.crypto.InvalidCipherTextException; +import nl.ictu.service.v1.map.BsnPseudoMapper; +import nl.ictu.service.v1.map.PseudoBsnMapper; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor @RestController -public final class ExchangeIdentifierController implements ExchangeIdentifierApi, VersionOneController { +public final class ExchangeIdentifierController implements ExchangeIdentifierApi, + VersionOneController { - public static final String V_1 = "v1"; - private final AesGcmSivCryptographer aesGcmSivCryptographer; + private final BsnPseudoMapper bsnPseudoMapper; + private final PseudoBsnMapper pseudoBsnMapper; @Override @SneakyThrows @@ -34,40 +32,15 @@ public ResponseEntity exchangeIdentifier(final Str final var recipientIdentifierType = wsExchangeIdentifierForIdentifierRequest.getRecipientIdentifierType(); if (BSN.equals(wsIdentifierRequest.getType()) && ORGANISATION_PSEUDO.equals( recipientIdentifierType)) { - // from BSN to Org Pseudo return ResponseEntity.ok( - mapBsnToPseudo(wsIdentifierRequest.getValue(), recipientOIN)); + bsnPseudoMapper.map(wsIdentifierRequest.getValue(), recipientOIN)); } else if (ORGANISATION_PSEUDO.equals(wsIdentifierRequest.getType()) && BSN.equals( recipientIdentifierType)) { - // from BSN to Org Pseudo return ResponseEntity.ok( - mapPseudoToBEsn(wsIdentifierRequest.getValue(), recipientOIN)); + pseudoBsnMapper.map(wsIdentifierRequest.getValue(), recipientOIN)); } return ResponseEntity.status(UNPROCESSABLE_ENTITY).build(); } - private WsExchangeIdentifierResponse mapBsnToPseudo(final String bsn, final String oin) - throws IOException, InvalidCipherTextException { - return WsExchangeIdentifierResponse.builder() - .identifier(WsIdentifier.builder() - .type(ORGANISATION_PSEUDO) - .value(aesGcmSivCryptographer.encrypt(Identifier.builder() - .version(V_1) - .bsn(bsn) - .build(), oin)) - .build()) - .build(); - } - - private WsExchangeIdentifierResponse mapPseudoToBEsn(final String pseudo, final String oin) - throws IOException, InvalidCipherTextException { - - return WsExchangeIdentifierResponse.builder() - .identifier(WsIdentifier.builder() - .type(BSN) - .value(aesGcmSivCryptographer.decrypt(pseudo, oin).getBsn()) - .build()) - .build(); - } } diff --git a/src/main/java/nl/ictu/service/v1/map/BsnPseudoMapper.java b/src/main/java/nl/ictu/service/v1/map/BsnPseudoMapper.java index d011072..ba8728f 100644 --- a/src/main/java/nl/ictu/service/v1/map/BsnPseudoMapper.java +++ b/src/main/java/nl/ictu/service/v1/map/BsnPseudoMapper.java @@ -1,9 +1,44 @@ -/* package nl.ictu.service.v1.map; +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; + +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import nl.ictu.Identifier; +import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierResponse; +import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; +import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor public class BsnPseudoMapper { + public static final String V_1 = "v1"; + private final AesGcmSivCryptographer aesGcmSivCryptographer; + /** + * Maps a given BSN (Burger Service Nummer) and OIN (Organisatie-identificatienummer) to a + * {@link WsExchangeIdentifierResponse} containing a pseudo-anonymous identifier. + * + * @param bsn the BSN to be encrypted and included in the identifier + * @param oin the OIN used as the salt for encryption + * @return a {@link WsExchangeIdentifierResponse} containing the pseudo-anonymous identifier + * @throws IOException if an I/O error occurs during the encryption process + * @throws InvalidCipherTextException if encryption fails due to invalid cipher text + */ + public WsExchangeIdentifierResponse map(final String bsn, final String oin) + throws IOException, InvalidCipherTextException { + return WsExchangeIdentifierResponse.builder() + .identifier(WsIdentifier.builder() + .type(ORGANISATION_PSEUDO) + .value(aesGcmSivCryptographer.encrypt(Identifier.builder() + .version(V_1) + .bsn(bsn) + .build(), oin)) + .build()) + .build(); + } } -*/ diff --git a/src/main/java/nl/ictu/service/v1/map/PseudoBsnMapper.java b/src/main/java/nl/ictu/service/v1/map/PseudoBsnMapper.java new file mode 100644 index 0000000..e6d5604 --- /dev/null +++ b/src/main/java/nl/ictu/service/v1/map/PseudoBsnMapper.java @@ -0,0 +1,43 @@ +package nl.ictu.service.v1.map; + + +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; + +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierResponse; +import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; +import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class PseudoBsnMapper { + + private final AesGcmSivCryptographer aesGcmSivCryptographer; + + /** + * Maps a given pseudonym and organizational identification number (OIN) to a + * {@link WsExchangeIdentifierResponse}. The pseudonym is decrypted using the + * provided OIN to derive the corresponding BSN (Burger Service Nummer) value. + * + * @param pseudo the pseudonym string to be decrypted + * @param oin the organizational identification number used as a decryption key + * @return a {@link WsExchangeIdentifierResponse} containing the decrypted BSN encapsulated in a {@link WsIdentifier} + * @throws IOException if an I/O error occurs during the decryption process + * @throws InvalidCipherTextException if decryption fails due to invalid cipher text + */ + public WsExchangeIdentifierResponse map(final String pseudo, final String oin) + throws IOException, InvalidCipherTextException { + + return WsExchangeIdentifierResponse.builder() + + .identifier(WsIdentifier.builder() + .type(BSN) + .value(aesGcmSivCryptographer.decrypt(pseudo, oin).getBsn()) + .build()) + .build(); + } + +} From 76b45a0dbd54e52d6b3bd3a6b49bbbc783a3cc47 Mon Sep 17 00:00:00 2001 From: sayf Date: Mon, 23 Dec 2024 21:40:16 +0100 Subject: [PATCH 14/30] More refactoring, split responsibilities --- .../v1/ExchangeIdentifierController.java | 33 ++++++------ .../v1/ExchangeTokenController.java | 9 ++++ .../controller/v1/GetTokenController.java | 8 +++ .../service/v1/ExchangeIdentifierService.java | 50 +++++++++++++++++++ .../ictu/service/v1/ExchangeTokenService.java | 9 ++++ .../nl/ictu/service/v1/GetTokenService.java | 9 ++++ 6 files changed, 99 insertions(+), 19 deletions(-) create mode 100644 src/main/java/nl/ictu/service/v1/ExchangeIdentifierService.java diff --git a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java index 29411d7..3462cc9 100644 --- a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java +++ b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java @@ -1,7 +1,5 @@ package nl.ictu.controller.v1; -import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; -import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; import lombok.RequiredArgsConstructor; @@ -9,8 +7,7 @@ import nl.ictu.pseudoniemenservice.generated.server.api.ExchangeIdentifierApi; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierRequest; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierResponse; -import nl.ictu.service.v1.map.BsnPseudoMapper; -import nl.ictu.service.v1.map.PseudoBsnMapper; +import nl.ictu.service.v1.ExchangeIdentifierService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; @@ -19,28 +16,26 @@ public final class ExchangeIdentifierController implements ExchangeIdentifierApi, VersionOneController { - private final BsnPseudoMapper bsnPseudoMapper; - private final PseudoBsnMapper pseudoBsnMapper; + private final ExchangeIdentifierService service; + /** + * Exchanges an identifier based on the provided caller OIN and request data. + * + * @param callerOIN The OIN of the caller initiating the request. + * @param wsExchangeIdentifierForIdentifierRequest The request object containing the identifier and additional data for the exchange process. + * @return A ResponseEntity containing a WsExchangeIdentifierResponse if the exchange is successful, + * or a ResponseEntity with HTTP status UNPROCESSABLE_ENTITY if the exchange fails. + */ @Override @SneakyThrows public ResponseEntity exchangeIdentifier(final String callerOIN, final WsExchangeIdentifierRequest wsExchangeIdentifierForIdentifierRequest) { - final var wsIdentifierRequest = wsExchangeIdentifierForIdentifierRequest.getIdentifier(); - final var recipientOIN = wsExchangeIdentifierForIdentifierRequest.getRecipientOIN(); - final var recipientIdentifierType = wsExchangeIdentifierForIdentifierRequest.getRecipientIdentifierType(); - if (BSN.equals(wsIdentifierRequest.getType()) && ORGANISATION_PSEUDO.equals( - recipientIdentifierType)) { - return ResponseEntity.ok( - bsnPseudoMapper.map(wsIdentifierRequest.getValue(), recipientOIN)); - } else if (ORGANISATION_PSEUDO.equals(wsIdentifierRequest.getType()) && BSN.equals( - recipientIdentifierType)) { - return ResponseEntity.ok( - pseudoBsnMapper.map(wsIdentifierRequest.getValue(), recipientOIN)); + final var identifier = service.exchangeIdentifier(callerOIN, + wsExchangeIdentifierForIdentifierRequest); + if (identifier != null) { + return ResponseEntity.ok(identifier); } return ResponseEntity.status(UNPROCESSABLE_ENTITY).build(); } - - } diff --git a/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java b/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java index c6003b1..a91e87b 100644 --- a/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java +++ b/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java @@ -19,6 +19,15 @@ public final class ExchangeTokenController implements ExchangeTokenApi, VersionO private final ExchangeTokenService exchangeTokenService; + /** + * Handles the exchange of a token and returns the corresponding identifier in a response. + * This method validates the caller's OIN, processes the incoming token using + * the specified identifier type, and constructs a response accordingly. + * + * @param callerOIN The identifier of the requesting organization (OIN). + * @param wsExchangeTokenForIdentifierRequest The request containing the token and identifier type details. + * @return A response entity containing the converted identifier or a status indicating failure. + */ @Override @SneakyThrows public ResponseEntity exchangeToken(final String callerOIN, diff --git a/src/main/java/nl/ictu/controller/v1/GetTokenController.java b/src/main/java/nl/ictu/controller/v1/GetTokenController.java index cb5d442..e1cf642 100644 --- a/src/main/java/nl/ictu/controller/v1/GetTokenController.java +++ b/src/main/java/nl/ictu/controller/v1/GetTokenController.java @@ -18,6 +18,14 @@ public final class GetTokenController implements GetTokenApi, VersionOneControll private final GetTokenService getTokenService; + /** + * Retrieves a token based on the provided caller identifier and request details. + * + * @param callerOIN The identifier of the caller organization initiating the request. + * @param wsGetTokenRequest The request object containing the recipient organization identifier and additional details. + * @return A ResponseEntity containing the token if the request is successful, + * or a ResponseEntity with a status of UNPROCESSABLE_ENTITY if the token cannot be retrieved. + */ @Override @SneakyThrows public ResponseEntity getToken(final String callerOIN, diff --git a/src/main/java/nl/ictu/service/v1/ExchangeIdentifierService.java b/src/main/java/nl/ictu/service/v1/ExchangeIdentifierService.java new file mode 100644 index 0000000..b7508c7 --- /dev/null +++ b/src/main/java/nl/ictu/service/v1/ExchangeIdentifierService.java @@ -0,0 +1,50 @@ +package nl.ictu.service.v1; + +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierRequest; +import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierResponse; +import nl.ictu.service.v1.map.BsnPseudoMapper; +import nl.ictu.service.v1.map.PseudoBsnMapper; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public final class ExchangeIdentifierService { + + private final BsnPseudoMapper bsnPseudoMapper; + private final PseudoBsnMapper pseudoBsnMapper; + + + /** + * Processes the exchange of an identifier between different types based on specific mappings + * and returns the corresponding response. + * + * @param callerOIN The originating identification number of the caller. + * @param wsExchangeIdentifierForIdentifierRequest The request object containing details + * of the identifier to be exchanged, + * including its value, type, recipient OIN, + * and recipient identifier type. + * @return A {@link WsExchangeIdentifierResponse} containing the exchanged identifier. + * Returns null if no appropriate mapping exists for the provided inputs. + */ + @SneakyThrows + public WsExchangeIdentifierResponse exchangeIdentifier(final String callerOIN, + final WsExchangeIdentifierRequest wsExchangeIdentifierForIdentifierRequest) { + + final var wsIdentifierRequest = wsExchangeIdentifierForIdentifierRequest.getIdentifier(); + final var recipientOIN = wsExchangeIdentifierForIdentifierRequest.getRecipientOIN(); + final var recipientIdentifierType = wsExchangeIdentifierForIdentifierRequest.getRecipientIdentifierType(); + if (BSN.equals(wsIdentifierRequest.getType()) && ORGANISATION_PSEUDO.equals( + recipientIdentifierType)) { + return bsnPseudoMapper.map(wsIdentifierRequest.getValue(), recipientOIN); + } else if (ORGANISATION_PSEUDO.equals(wsIdentifierRequest.getType()) && BSN.equals( + recipientIdentifierType)) { + return pseudoBsnMapper.map(wsIdentifierRequest.getValue(), recipientOIN); + } + return null; + } +} diff --git a/src/main/java/nl/ictu/service/v1/ExchangeTokenService.java b/src/main/java/nl/ictu/service/v1/ExchangeTokenService.java index a45f142..f61abbc 100644 --- a/src/main/java/nl/ictu/service/v1/ExchangeTokenService.java +++ b/src/main/java/nl/ictu/service/v1/ExchangeTokenService.java @@ -29,6 +29,15 @@ public final class ExchangeTokenService { private final TokenConverter tokenConverter; private final OINValidator oinValidator; + /** + * Exchanges a token for an identifier based on the provided request and caller OIN. + * + * @param callerOIN the originating organization's identification number used for validation + * @param wsExchangeTokenForIdentifierRequest the request containing the token and identifier type + * @return a WsExchangeTokenResponse containing the generated or resolved identifier + * @throws InvalidOINException if the caller OIN is not valid or does not match the token + * @throws InvalidWsIdentifierTokenException if the identifier type in the request is invalid or cannot be processed + */ @SneakyThrows public WsExchangeTokenResponse exchangeToken(final String callerOIN, final WsExchangeTokenRequest wsExchangeTokenForIdentifierRequest) { diff --git a/src/main/java/nl/ictu/service/v1/GetTokenService.java b/src/main/java/nl/ictu/service/v1/GetTokenService.java index af8a0ad..ce67800 100644 --- a/src/main/java/nl/ictu/service/v1/GetTokenService.java +++ b/src/main/java/nl/ictu/service/v1/GetTokenService.java @@ -29,6 +29,15 @@ public final class GetTokenService { private final TokenConverter tokenConverter; private final EncryptedBsnMapper encryptedBsnMapper; + /** + * Generates an encrypted token response based on the given recipient OIN and identifier. + * Validates the identifier type and maps it to the corresponding BSN before + * creating the encrypted token. + * + * @param recipientOIN the recipient's organizational identification number + * @param identifier the identifier containing value and type information + * @return a {@link WsGetTokenResponse} containing the encrypted token, or null if the identifier is invalid or BSN mapping fails + */ @SneakyThrows public WsGetTokenResponse getWsGetTokenResponse(final String recipientOIN, final WsIdentifier identifier) { From a41e7da2969389889c43b8e694d46c215212febb Mon Sep 17 00:00:00 2001 From: sayf Date: Mon, 23 Dec 2024 21:56:43 +0100 Subject: [PATCH 15/30] More refactoring, split responsibilities --- .../ictu/service/v1/ExchangeTokenService.java | 61 +++++++++++++------ .../nl/ictu/service/v1/GetTokenService.java | 6 +- .../{TokenConverter.java => TokenCoder.java} | 4 +- 3 files changed, 47 insertions(+), 24 deletions(-) rename src/main/java/nl/ictu/service/v1/crypto/{TokenConverter.java => TokenCoder.java} (90%) diff --git a/src/main/java/nl/ictu/service/v1/ExchangeTokenService.java b/src/main/java/nl/ictu/service/v1/ExchangeTokenService.java index f61abbc..64ac36c 100644 --- a/src/main/java/nl/ictu/service/v1/ExchangeTokenService.java +++ b/src/main/java/nl/ictu/service/v1/ExchangeTokenService.java @@ -3,10 +3,12 @@ import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; +import java.io.IOException; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import nl.ictu.Identifier; +import nl.ictu.Token; import nl.ictu.controller.exception.InvalidOINException; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenRequest; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenResponse; @@ -14,8 +16,9 @@ import nl.ictu.service.exception.InvalidWsIdentifierTokenException; import nl.ictu.service.v1.crypto.AesGcmCryptographer; import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; -import nl.ictu.service.v1.crypto.TokenConverter; +import nl.ictu.service.v1.crypto.TokenCoder; import nl.ictu.service.v1.validate.OINValidator; +import org.bouncycastle.crypto.InvalidCipherTextException; import org.springframework.web.bind.annotation.RestController; @Slf4j @@ -26,46 +29,66 @@ public final class ExchangeTokenService { public static final String V_1 = "v1"; private final AesGcmCryptographer aesGcmCryptographer; private final AesGcmSivCryptographer aesGcmSivCryptographer; - private final TokenConverter tokenConverter; + private final TokenCoder tokenCoder; private final OINValidator oinValidator; /** * Exchanges a token for an identifier based on the provided request and caller OIN. * - * @param callerOIN the originating organization's identification number used for validation - * @param wsExchangeTokenForIdentifierRequest the request containing the token and identifier type + * @param callerOIN the originating organization's identification + * number used for validation + * @param wsExchangeTokenForIdentifierRequest the request containing the token and identifier + * type * @return a WsExchangeTokenResponse containing the generated or resolved identifier - * @throws InvalidOINException if the caller OIN is not valid or does not match the token - * @throws InvalidWsIdentifierTokenException if the identifier type in the request is invalid or cannot be processed + * @throws InvalidOINException if the caller OIN is not valid or does not match + * the token + * @throws InvalidWsIdentifierTokenException if the identifier type in the request is invalid or + * cannot be processed */ @SneakyThrows public WsExchangeTokenResponse exchangeToken(final String callerOIN, final WsExchangeTokenRequest wsExchangeTokenForIdentifierRequest) { final var encodedToken = aesGcmCryptographer.decrypt(wsExchangeTokenForIdentifierRequest.getToken(), callerOIN); - final var token = tokenConverter.decode(encodedToken); + final var token = tokenCoder.decode(encodedToken); if (!oinValidator.isValid(callerOIN, token)) { throw new InvalidOINException("Sink OIN not the same"); } - - final var wsIdentifier = new WsIdentifier(); switch (wsExchangeTokenForIdentifierRequest.getIdentifierType()) { case BSN -> { - wsIdentifier.setType(BSN); - wsIdentifier.setValue(token.getBsn()); + return mapBsnToken(token); } case ORGANISATION_PSEUDO -> { - final String encrypt = aesGcmSivCryptographer.encrypt(Identifier.builder() - .version(V_1) - .bsn(token.getBsn()) - .build(), callerOIN); - wsIdentifier.setType(ORGANISATION_PSEUDO); - wsIdentifier.setValue(encrypt); + return mapOrganisationPseudoToken(callerOIN, token); } - default -> throw new InvalidWsIdentifierTokenException("Invalid identifier cannot be processed."); + default -> throw new InvalidWsIdentifierTokenException( + "Invalid identifier cannot be processed."); } + } + + private static WsExchangeTokenResponse mapBsnToken(final Token token) { + + return WsExchangeTokenResponse.builder() + .identifier(WsIdentifier.builder() + .type(BSN) + .value(token.getBsn()) + .build()) + .build(); + } + + private WsExchangeTokenResponse mapOrganisationPseudoToken(final String callerOIN, + final Token token) throws InvalidCipherTextException, IOException { + + final var tokenIdentifier = Identifier.builder() + .version(V_1) + .bsn(token.getBsn()) + .build(); + final var encrypt = aesGcmSivCryptographer.encrypt(tokenIdentifier, callerOIN); return WsExchangeTokenResponse.builder() - .identifier(wsIdentifier) + .identifier(WsIdentifier.builder() + .type(ORGANISATION_PSEUDO) + .value(encrypt) + .build()) .build(); } } diff --git a/src/main/java/nl/ictu/service/v1/GetTokenService.java b/src/main/java/nl/ictu/service/v1/GetTokenService.java index ce67800..dcc3f6a 100644 --- a/src/main/java/nl/ictu/service/v1/GetTokenService.java +++ b/src/main/java/nl/ictu/service/v1/GetTokenService.java @@ -16,7 +16,7 @@ import nl.ictu.pseudoniemenservice.generated.server.model.WsGetTokenResponse; import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; import nl.ictu.service.v1.crypto.AesGcmCryptographer; -import nl.ictu.service.v1.crypto.TokenConverter; +import nl.ictu.service.v1.crypto.TokenCoder; import nl.ictu.service.v1.map.EncryptedBsnMapper; import org.springframework.stereotype.Service; @@ -26,7 +26,7 @@ public final class GetTokenService { public static final String V_1 = "v1"; private final AesGcmCryptographer aesGcmCryptographer; - private final TokenConverter tokenConverter; + private final TokenCoder tokenCoder; private final EncryptedBsnMapper encryptedBsnMapper; /** @@ -69,7 +69,7 @@ private WsGetTokenResponse createEncryptedToken(final String bsn, final long cre final String recipientOIN) throws IOException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { - final var plainTextToken = tokenConverter.encode(Token.builder() + final var plainTextToken = tokenCoder.encode(Token.builder() .version(V_1) .bsn(bsn) .creationDate(creationDate) diff --git a/src/main/java/nl/ictu/service/v1/crypto/TokenConverter.java b/src/main/java/nl/ictu/service/v1/crypto/TokenCoder.java similarity index 90% rename from src/main/java/nl/ictu/service/v1/crypto/TokenConverter.java rename to src/main/java/nl/ictu/service/v1/crypto/TokenCoder.java index b6d3c00..f8715d2 100644 --- a/src/main/java/nl/ictu/service/v1/crypto/TokenConverter.java +++ b/src/main/java/nl/ictu/service/v1/crypto/TokenCoder.java @@ -13,13 +13,13 @@ @Service @RequiredArgsConstructor @RegisterReflectionForBinding({Token.class}) -public class TokenConverter { +public class TokenCoder { private final ObjectMapper objectMapper; public String encode(final Token token) throws IOException { - final StringWriter stringWriter = new StringWriter(); + final var stringWriter = new StringWriter(); objectMapper.writeValue(stringWriter, token); return stringWriter.toString(); } From a4de7f4c314dfe1c398c542e82f7a738d150d4c5 Mon Sep 17 00:00:00 2001 From: sayf Date: Mon, 23 Dec 2024 21:59:33 +0100 Subject: [PATCH 16/30] More refactoring for better naming --- .../v1/ExchangeIdentifierController.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java index 3462cc9..73afa94 100644 --- a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java +++ b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java @@ -21,18 +21,18 @@ public final class ExchangeIdentifierController implements ExchangeIdentifierApi /** * Exchanges an identifier based on the provided caller OIN and request data. * - * @param callerOIN The OIN of the caller initiating the request. - * @param wsExchangeIdentifierForIdentifierRequest The request object containing the identifier and additional data for the exchange process. - * @return A ResponseEntity containing a WsExchangeIdentifierResponse if the exchange is successful, - * or a ResponseEntity with HTTP status UNPROCESSABLE_ENTITY if the exchange fails. + * @param callerOIN The OIN of the caller initiating the request. + * @param wsExchangeRequest The request object containing the identifier and additional data for + * the exchange process. + * @return A ResponseEntity containing a WsExchangeIdentifierResponse if the exchange is + * successful, or a ResponseEntity with HTTP status UNPROCESSABLE_ENTITY if the exchange fails. */ @Override @SneakyThrows public ResponseEntity exchangeIdentifier(final String callerOIN, - final WsExchangeIdentifierRequest wsExchangeIdentifierForIdentifierRequest) { + final WsExchangeIdentifierRequest wsExchangeRequest) { - final var identifier = service.exchangeIdentifier(callerOIN, - wsExchangeIdentifierForIdentifierRequest); + final var identifier = service.exchangeIdentifier(callerOIN, wsExchangeRequest); if (identifier != null) { return ResponseEntity.ok(identifier); } From 8f08c02b5e7e68e9e1cd050dd253d39c05a3ab90 Mon Sep 17 00:00:00 2001 From: sayf Date: Mon, 23 Dec 2024 22:12:11 +0100 Subject: [PATCH 17/30] More refactoring to improve singe responsibilities --- .../v1/ExchangeTokenController.java | 1 - .../ictu/service/v1/ExchangeTokenService.java | 50 ++++--------------- .../ictu/service/v1/map/BsnTokenMapper.java | 31 ++++++++++++ .../v1/map/OrganisationPseudoTokenMapper.java | 47 +++++++++++++++++ 4 files changed, 87 insertions(+), 42 deletions(-) create mode 100644 src/main/java/nl/ictu/service/v1/map/BsnTokenMapper.java create mode 100644 src/main/java/nl/ictu/service/v1/map/OrganisationPseudoTokenMapper.java diff --git a/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java b/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java index a91e87b..4eda1bd 100644 --- a/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java +++ b/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java @@ -32,7 +32,6 @@ public final class ExchangeTokenController implements ExchangeTokenApi, VersionO @SneakyThrows public ResponseEntity exchangeToken(final String callerOIN, final WsExchangeTokenRequest wsExchangeTokenForIdentifierRequest) { - try { final var wsExchangeTokenResponse = exchangeTokenService.exchangeToken(callerOIN, wsExchangeTokenForIdentifierRequest); return ResponseEntity.ok(wsExchangeTokenResponse); diff --git a/src/main/java/nl/ictu/service/v1/ExchangeTokenService.java b/src/main/java/nl/ictu/service/v1/ExchangeTokenService.java index 64ac36c..1dda797 100644 --- a/src/main/java/nl/ictu/service/v1/ExchangeTokenService.java +++ b/src/main/java/nl/ictu/service/v1/ExchangeTokenService.java @@ -1,24 +1,17 @@ package nl.ictu.service.v1; -import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; -import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; - -import java.io.IOException; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import nl.ictu.Identifier; -import nl.ictu.Token; import nl.ictu.controller.exception.InvalidOINException; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenRequest; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenResponse; -import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; import nl.ictu.service.exception.InvalidWsIdentifierTokenException; import nl.ictu.service.v1.crypto.AesGcmCryptographer; -import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; import nl.ictu.service.v1.crypto.TokenCoder; +import nl.ictu.service.v1.map.BsnTokenMapper; +import nl.ictu.service.v1.map.OrganisationPseudoTokenMapper; import nl.ictu.service.v1.validate.OINValidator; -import org.bouncycastle.crypto.InvalidCipherTextException; import org.springframework.web.bind.annotation.RestController; @Slf4j @@ -26,11 +19,11 @@ @RestController public final class ExchangeTokenService { - public static final String V_1 = "v1"; private final AesGcmCryptographer aesGcmCryptographer; - private final AesGcmSivCryptographer aesGcmSivCryptographer; private final TokenCoder tokenCoder; private final OINValidator oinValidator; + private final OrganisationPseudoTokenMapper organisationPseudoTokenMapper; + private final BsnTokenMapper bsnTokenMapper; /** * Exchanges a token for an identifier based on the provided request and caller OIN. @@ -49,46 +42,21 @@ public final class ExchangeTokenService { public WsExchangeTokenResponse exchangeToken(final String callerOIN, final WsExchangeTokenRequest wsExchangeTokenForIdentifierRequest) { - final var encodedToken = aesGcmCryptographer.decrypt(wsExchangeTokenForIdentifierRequest.getToken(), callerOIN); + final var encodedToken = aesGcmCryptographer.decrypt( + wsExchangeTokenForIdentifierRequest.getToken(), callerOIN); final var token = tokenCoder.decode(encodedToken); if (!oinValidator.isValid(callerOIN, token)) { - throw new InvalidOINException("Sink OIN not the same"); + throw new InvalidOINException("CallerOIN and token are mismatched."); } switch (wsExchangeTokenForIdentifierRequest.getIdentifierType()) { case BSN -> { - return mapBsnToken(token); + return bsnTokenMapper.map(token); } case ORGANISATION_PSEUDO -> { - return mapOrganisationPseudoToken(callerOIN, token); + return organisationPseudoTokenMapper.map(callerOIN, token); } default -> throw new InvalidWsIdentifierTokenException( "Invalid identifier cannot be processed."); } } - - private static WsExchangeTokenResponse mapBsnToken(final Token token) { - - return WsExchangeTokenResponse.builder() - .identifier(WsIdentifier.builder() - .type(BSN) - .value(token.getBsn()) - .build()) - .build(); - } - - private WsExchangeTokenResponse mapOrganisationPseudoToken(final String callerOIN, - final Token token) throws InvalidCipherTextException, IOException { - - final var tokenIdentifier = Identifier.builder() - .version(V_1) - .bsn(token.getBsn()) - .build(); - final var encrypt = aesGcmSivCryptographer.encrypt(tokenIdentifier, callerOIN); - return WsExchangeTokenResponse.builder() - .identifier(WsIdentifier.builder() - .type(ORGANISATION_PSEUDO) - .value(encrypt) - .build()) - .build(); - } } diff --git a/src/main/java/nl/ictu/service/v1/map/BsnTokenMapper.java b/src/main/java/nl/ictu/service/v1/map/BsnTokenMapper.java new file mode 100644 index 0000000..cbe8920 --- /dev/null +++ b/src/main/java/nl/ictu/service/v1/map/BsnTokenMapper.java @@ -0,0 +1,31 @@ +package nl.ictu.service.v1.map; + +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; + +import lombok.RequiredArgsConstructor; +import nl.ictu.Token; +import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenResponse; +import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class BsnTokenMapper { + + /** + * Maps a given Token to a WsExchangeTokenResponse. Populates the response with a WsIdentifier + * containing the BSN value from the provided token. + * + * @param token the Token object containing BSN and other data + * @return a WsExchangeTokenResponse containing the identifier with the BSN value + */ + public WsExchangeTokenResponse map(final Token token) { + + return WsExchangeTokenResponse.builder() + .identifier(WsIdentifier.builder() + .type(BSN) + .value(token.getBsn()) + .build()) + .build(); + } +} diff --git a/src/main/java/nl/ictu/service/v1/map/OrganisationPseudoTokenMapper.java b/src/main/java/nl/ictu/service/v1/map/OrganisationPseudoTokenMapper.java new file mode 100644 index 0000000..d5aafc7 --- /dev/null +++ b/src/main/java/nl/ictu/service/v1/map/OrganisationPseudoTokenMapper.java @@ -0,0 +1,47 @@ +package nl.ictu.service.v1.map; + +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; + +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import nl.ictu.Identifier; +import nl.ictu.Token; +import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenResponse; +import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; +import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class OrganisationPseudoTokenMapper { + + public static final String V_1 = "v1"; + private final AesGcmSivCryptographer aesGcmSivCryptographer; + + + /** + * Maps the provided callerOIN and Token into a WsExchangeTokenResponse object. + * + * @param callerOIN the originating identification number of the caller + * @param token the Token object containing the required information such as BSN + * @return a WsExchangeTokenResponse containing an encrypted identifier + * @throws InvalidCipherTextException if there is an issue with the encryption process + * @throws IOException if there is an I/O error during encryption + */ + public WsExchangeTokenResponse map(final String callerOIN, + final Token token) throws InvalidCipherTextException, IOException { + + final var tokenIdentifier = Identifier.builder() + .version(V_1) + .bsn(token.getBsn()) + .build(); + final var encryptedIdentifier = aesGcmSivCryptographer.encrypt(tokenIdentifier, callerOIN); + return WsExchangeTokenResponse.builder() + .identifier(WsIdentifier.builder() + .type(ORGANISATION_PSEUDO) + .value(encryptedIdentifier) + .build()) + .build(); + } +} From cabbffdcac9c1002f846d1b535149c4ef597b594 Mon Sep 17 00:00:00 2001 From: sayf Date: Mon, 23 Dec 2024 22:32:11 +0100 Subject: [PATCH 18/30] Added mutation testing coverage with PiTest --- pom.xml | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/pom.xml b/pom.xml index 79520f8..c0d8987 100644 --- a/pom.xml +++ b/pom.xml @@ -19,6 +19,11 @@ 4.8.6 1.79 3.6.0 + + 75 + 75 + 1.2.1 + 1.17.3 @@ -218,6 +223,51 @@ + + + pitest + + + + pitest-maven + + ${pit.coverageThreshold} + ${pit.mutationThreshold} + + nl.ictu.* + + + nl.ictu.* + + + nl.ictu.pseudoniemenservice.generated.* + + 5000 + false + + + + org.pitest + pitest-junit5-plugin + 1.2.1 + + + + + + mutationCoverage + + pit-report + test + + + org.pitest + ${pitest-maven.version} + + + + + From 6dddccfe84b9f7705b17cd42729498924f495e86 Mon Sep 17 00:00:00 2001 From: sayf Date: Tue, 24 Dec 2024 16:10:52 +0100 Subject: [PATCH 19/30] Making controllers and services more uniform --- .../v1/ExchangeIdentifierController.java | 19 +++++++++------- .../v1/ExchangeTokenController.java | 13 +++++------ .../controller/v1/GetTokenController.java | 19 +++++++++------- ...validWsIdentifierRequestTypeException.java | 8 +++++++ .../WsGetTokenProcessingException.java | 8 +++++++ .../service/v1/ExchangeIdentifierService.java | 3 ++- .../nl/ictu/service/v1/GetTokenService.java | 22 ++++++++++--------- .../v1/crypto/AesGcmSivCryptographer.java | 6 ++--- .../service/v1/map/EncryptedBsnMapper.java | 9 ++------ 9 files changed, 63 insertions(+), 44 deletions(-) create mode 100644 src/main/java/nl/ictu/service/exception/InvalidWsIdentifierRequestTypeException.java create mode 100644 src/main/java/nl/ictu/service/exception/WsGetTokenProcessingException.java diff --git a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java index 73afa94..82f28ce 100644 --- a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java +++ b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; import nl.ictu.pseudoniemenservice.generated.server.api.ExchangeIdentifierApi; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierRequest; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierResponse; @@ -11,8 +12,9 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; -@RequiredArgsConstructor +@Slf4j @RestController +@RequiredArgsConstructor public final class ExchangeIdentifierController implements ExchangeIdentifierApi, VersionOneController { @@ -22,20 +24,21 @@ public final class ExchangeIdentifierController implements ExchangeIdentifierApi * Exchanges an identifier based on the provided caller OIN and request data. * * @param callerOIN The OIN of the caller initiating the request. - * @param wsExchangeRequest The request object containing the identifier and additional data for - * the exchange process. - * @return A ResponseEntity containing a WsExchangeIdentifierResponse if the exchange is - * successful, or a ResponseEntity with HTTP status UNPROCESSABLE_ENTITY if the exchange fails. + * @param wsExchangeRequest The request object containing the identifier and additional data for the exchange process. + * @return A ResponseEntity containing a WsExchangeIdentifierResponse if the exchange is successful, or a ResponseEntity with HTTP status + * UNPROCESSABLE_ENTITY if the exchange fails. */ @Override @SneakyThrows public ResponseEntity exchangeIdentifier(final String callerOIN, final WsExchangeIdentifierRequest wsExchangeRequest) { - final var identifier = service.exchangeIdentifier(callerOIN, wsExchangeRequest); - if (identifier != null) { + try { + final var identifier = service.exchangeIdentifier(callerOIN, wsExchangeRequest); return ResponseEntity.ok(identifier); + } catch (Exception ex) { + log.warn(ex.getMessage()); + return ResponseEntity.status(UNPROCESSABLE_ENTITY).build(); } - return ResponseEntity.status(UNPROCESSABLE_ENTITY).build(); } } diff --git a/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java b/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java index 4eda1bd..bcf2108 100644 --- a/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java +++ b/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java @@ -3,7 +3,6 @@ import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import nl.ictu.pseudoniemenservice.generated.server.api.ExchangeTokenApi; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenRequest; @@ -20,23 +19,23 @@ public final class ExchangeTokenController implements ExchangeTokenApi, VersionO private final ExchangeTokenService exchangeTokenService; /** - * Handles the exchange of a token and returns the corresponding identifier in a response. - * This method validates the caller's OIN, processes the incoming token using - * the specified identifier type, and constructs a response accordingly. + * Handles the exchange of a token and returns the corresponding identifier in a response. This method validates the caller's OIN, processes the + * incoming token using the specified identifier type, and constructs a response accordingly. * - * @param callerOIN The identifier of the requesting organization (OIN). + * @param callerOIN The identifier of the requesting organization (OIN). * @param wsExchangeTokenForIdentifierRequest The request containing the token and identifier type details. * @return A response entity containing the converted identifier or a status indicating failure. */ @Override - @SneakyThrows public ResponseEntity exchangeToken(final String callerOIN, final WsExchangeTokenRequest wsExchangeTokenForIdentifierRequest) { + try { final var wsExchangeTokenResponse = exchangeTokenService.exchangeToken(callerOIN, wsExchangeTokenForIdentifierRequest); return ResponseEntity.ok(wsExchangeTokenResponse); } catch (Exception ex) { - return ResponseEntity.status(UNPROCESSABLE_ENTITY).build(); + log.warn(ex.getMessage()); } + return ResponseEntity.status(UNPROCESSABLE_ENTITY).build(); } } diff --git a/src/main/java/nl/ictu/controller/v1/GetTokenController.java b/src/main/java/nl/ictu/controller/v1/GetTokenController.java index e1cf642..cbf1eb5 100644 --- a/src/main/java/nl/ictu/controller/v1/GetTokenController.java +++ b/src/main/java/nl/ictu/controller/v1/GetTokenController.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; import nl.ictu.pseudoniemenservice.generated.server.api.GetTokenApi; import nl.ictu.pseudoniemenservice.generated.server.model.WsGetTokenRequest; import nl.ictu.pseudoniemenservice.generated.server.model.WsGetTokenResponse; @@ -11,6 +12,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; +@Slf4j @RestController @RequiredArgsConstructor public final class GetTokenController implements GetTokenApi, VersionOneController { @@ -21,21 +23,22 @@ public final class GetTokenController implements GetTokenApi, VersionOneControll /** * Retrieves a token based on the provided caller identifier and request details. * - * @param callerOIN The identifier of the caller organization initiating the request. + * @param callerOIN The identifier of the caller organization initiating the request. * @param wsGetTokenRequest The request object containing the recipient organization identifier and additional details. - * @return A ResponseEntity containing the token if the request is successful, - * or a ResponseEntity with a status of UNPROCESSABLE_ENTITY if the token cannot be retrieved. + * @return A ResponseEntity containing the token if the request is successful, or a ResponseEntity with a status of UNPROCESSABLE_ENTITY if the + * token cannot be retrieved. */ @Override @SneakyThrows public ResponseEntity getToken(final String callerOIN, final WsGetTokenRequest wsGetTokenRequest) { - final var recipientOIN = wsGetTokenRequest.getRecipientOIN(); - final var identifier = wsGetTokenRequest.getIdentifier(); - final var wsGetToken200Response = getTokenService.getWsGetTokenResponse(recipientOIN, identifier); - if (wsGetToken200Response != null) { - return ResponseEntity.ok(wsGetToken200Response); + try { + final var recipientOIN = wsGetTokenRequest.getRecipientOIN(); + final var identifier = wsGetTokenRequest.getIdentifier(); + return ResponseEntity.ok(getTokenService.getWsGetTokenResponse(recipientOIN, identifier)); + } catch (Exception ex) { + log.warn(ex.getMessage()); } return ResponseEntity.status(UNPROCESSABLE_ENTITY).build(); } diff --git a/src/main/java/nl/ictu/service/exception/InvalidWsIdentifierRequestTypeException.java b/src/main/java/nl/ictu/service/exception/InvalidWsIdentifierRequestTypeException.java new file mode 100644 index 0000000..f56f23e --- /dev/null +++ b/src/main/java/nl/ictu/service/exception/InvalidWsIdentifierRequestTypeException.java @@ -0,0 +1,8 @@ +package nl.ictu.service.exception; + +public class InvalidWsIdentifierRequestTypeException extends RuntimeException { + public InvalidWsIdentifierRequestTypeException(final String message) { + + super(message); + } +} diff --git a/src/main/java/nl/ictu/service/exception/WsGetTokenProcessingException.java b/src/main/java/nl/ictu/service/exception/WsGetTokenProcessingException.java new file mode 100644 index 0000000..0c61f39 --- /dev/null +++ b/src/main/java/nl/ictu/service/exception/WsGetTokenProcessingException.java @@ -0,0 +1,8 @@ +package nl.ictu.service.exception; + +public class WsGetTokenProcessingException extends RuntimeException { + public WsGetTokenProcessingException(final String message) { + + super(message); + } +} diff --git a/src/main/java/nl/ictu/service/v1/ExchangeIdentifierService.java b/src/main/java/nl/ictu/service/v1/ExchangeIdentifierService.java index b7508c7..7be2f8c 100644 --- a/src/main/java/nl/ictu/service/v1/ExchangeIdentifierService.java +++ b/src/main/java/nl/ictu/service/v1/ExchangeIdentifierService.java @@ -7,6 +7,7 @@ import lombok.SneakyThrows; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierRequest; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierResponse; +import nl.ictu.service.exception.InvalidWsIdentifierRequestTypeException; import nl.ictu.service.v1.map.BsnPseudoMapper; import nl.ictu.service.v1.map.PseudoBsnMapper; import org.springframework.stereotype.Service; @@ -45,6 +46,6 @@ public WsExchangeIdentifierResponse exchangeIdentifier(final String callerOIN, recipientIdentifierType)) { return pseudoBsnMapper.map(wsIdentifierRequest.getValue(), recipientOIN); } - return null; + throw new InvalidWsIdentifierRequestTypeException("Invalid WsIdentifierRequest type cannot be processed."); } } diff --git a/src/main/java/nl/ictu/service/v1/GetTokenService.java b/src/main/java/nl/ictu/service/v1/GetTokenService.java index dcc3f6a..055d2f9 100644 --- a/src/main/java/nl/ictu/service/v1/GetTokenService.java +++ b/src/main/java/nl/ictu/service/v1/GetTokenService.java @@ -12,14 +12,17 @@ import javax.crypto.NoSuchPaddingException; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; import nl.ictu.Token; import nl.ictu.pseudoniemenservice.generated.server.model.WsGetTokenResponse; import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; +import nl.ictu.service.exception.WsGetTokenProcessingException; import nl.ictu.service.v1.crypto.AesGcmCryptographer; import nl.ictu.service.v1.crypto.TokenCoder; import nl.ictu.service.v1.map.EncryptedBsnMapper; import org.springframework.stereotype.Service; +@Slf4j @Service @RequiredArgsConstructor public final class GetTokenService { @@ -30,12 +33,11 @@ public final class GetTokenService { private final EncryptedBsnMapper encryptedBsnMapper; /** - * Generates an encrypted token response based on the given recipient OIN and identifier. - * Validates the identifier type and maps it to the corresponding BSN before - * creating the encrypted token. + * Generates an encrypted token response based on the given recipient OIN and identifier. Validates the identifier type and maps it to the + * corresponding BSN before creating the encrypted token. * * @param recipientOIN the recipient's organizational identification number - * @param identifier the identifier containing value and type information + * @param identifier the identifier containing value and type information * @return a {@link WsGetTokenResponse} containing the encrypted token, or null if the identifier is invalid or BSN mapping fails */ @SneakyThrows @@ -44,16 +46,16 @@ public WsGetTokenResponse getWsGetTokenResponse(final String recipientOIN, final final var creationDate = System.currentTimeMillis(); // check is callerOIN allowed to communicatie with sinkOIN - if (identifier != null) { + try { final String bsn = mapBsn(identifier, recipientOIN); - if (bsn != null) { - return createEncryptedToken(bsn, creationDate, recipientOIN); - } + return createEncryptedToken(bsn, creationDate, recipientOIN); + } catch (Exception ex) { + final var exceptionMessage = ex.getMessage(); + log.warn(exceptionMessage); + throw new WsGetTokenProcessingException(exceptionMessage); } - return null; } - @SneakyThrows private String mapBsn(final WsIdentifier identifier, final String recipientOIN) { final String bsnValue = identifier.getValue(); diff --git a/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java b/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java index f092016..6147c09 100644 --- a/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java +++ b/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java @@ -1,10 +1,10 @@ package nl.ictu.service.v1.crypto; -import com.fasterxml.jackson.core.JsonProcessingException; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import nl.ictu.Identifier; import nl.ictu.configuration.PseudoniemenServiceProperties; @@ -63,8 +63,8 @@ public String encrypt(final Identifier identifier, final String salt) } - public Identifier decrypt(final String ciphertextString, final String salt) - throws InvalidCipherTextException, JsonProcessingException { + @SneakyThrows + public Identifier decrypt(final String ciphertextString, final String salt) { final var cipher = new GCMSIVBlockCipher(AESHelper.getAESEngine()); cipher.init(false, createSecretKey(salt)); diff --git a/src/main/java/nl/ictu/service/v1/map/EncryptedBsnMapper.java b/src/main/java/nl/ictu/service/v1/map/EncryptedBsnMapper.java index aa55da1..f337141 100644 --- a/src/main/java/nl/ictu/service/v1/map/EncryptedBsnMapper.java +++ b/src/main/java/nl/ictu/service/v1/map/EncryptedBsnMapper.java @@ -1,9 +1,7 @@ package nl.ictu.service.v1.map; -import com.fasterxml.jackson.core.JsonProcessingException; import lombok.RequiredArgsConstructor; import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; -import org.bouncycastle.crypto.InvalidCipherTextException; import org.springframework.stereotype.Component; @Component @@ -15,14 +13,11 @@ public class EncryptedBsnMapper { /** * Maps the encrypted business service number to its decrypted value using the given recipient OIN. * - * @param bsnValue the encrypted business service number to be decrypted + * @param bsnValue the encrypted business service number to be decrypted * @param recipientOIN the recipient OIN key used for decryption * @return the decrypted business service number - * @throws InvalidCipherTextException if an error occurs during decryption - * @throws JsonProcessingException if an error occurs during JSON processing */ - public String map(final String bsnValue, final String recipientOIN) - throws InvalidCipherTextException, JsonProcessingException { + public String map(final String bsnValue, final String recipientOIN) { final var decodedIdentifier = aesGcmSivCryptographer.decrypt(bsnValue, recipientOIN); return decodedIdentifier.getBsn(); From 42ce262e4806745390a17f5c53a7a6ea81bf3dec Mon Sep 17 00:00:00 2001 From: sayf Date: Tue, 24 Dec 2024 22:23:01 +0100 Subject: [PATCH 20/30] refined error handling --- .../controller/GlobalExceptionHandler.java | 130 ++++++++++++++++++ .../v1/ExchangeIdentifierController.java | 19 +-- .../v1/ExchangeTokenController.java | 23 ++-- .../controller/v1/GetTokenController.java | 23 +--- .../GlobalExceptionHandlerTest.java | 82 +++++++++++ .../ictu/controller/stub/StubController.java | 28 ++++ .../nl/ictu/controller/stub/StubService.java | 11 ++ .../v1/ExchangeTokenControllerTest.java | 86 ++++++++++++ 8 files changed, 360 insertions(+), 42 deletions(-) create mode 100644 src/main/java/nl/ictu/controller/GlobalExceptionHandler.java create mode 100644 src/test/java/nl/ictu/controller/GlobalExceptionHandlerTest.java create mode 100644 src/test/java/nl/ictu/controller/stub/StubController.java create mode 100644 src/test/java/nl/ictu/controller/stub/StubService.java create mode 100644 src/test/java/nl/ictu/controller/v1/ExchangeTokenControllerTest.java diff --git a/src/main/java/nl/ictu/controller/GlobalExceptionHandler.java b/src/main/java/nl/ictu/controller/GlobalExceptionHandler.java new file mode 100644 index 0000000..be69668 --- /dev/null +++ b/src/main/java/nl/ictu/controller/GlobalExceptionHandler.java @@ -0,0 +1,130 @@ +package nl.ictu.controller; + +import lombok.extern.slf4j.Slf4j; +import nl.ictu.controller.exception.InvalidOINException; +import nl.ictu.service.exception.IdentifierPrivateKeyException; +import nl.ictu.service.exception.InvalidWsIdentifierRequestTypeException; +import nl.ictu.service.exception.InvalidWsIdentifierTokenException; +import nl.ictu.service.exception.TokenPrivateKeyException; +import nl.ictu.service.exception.WsGetTokenProcessingException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +@Slf4j +@ControllerAdvice +public class GlobalExceptionHandler { + + /** + * Catch-all for any unhandled exceptions. + */ + @ExceptionHandler(Exception.class) + @ResponseBody + public ResponseEntity handleGenericException(Exception ex) { + + log.error("Unexpected error occurred", ex); + return new ResponseEntity<>( + "An unexpected error occurred: " + ex.getMessage(), + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + + /** + * Handles exceptions of type IdentifierPrivateKeyException and returns an appropriate HTTP + * response with the exception message. + * + * @param ex the IdentifierPrivateKeyException to be handled + * @return a ResponseEntity containing the exception message and an UNPROCESSABLE_ENTITY (422) + * status + */ + @ExceptionHandler(IdentifierPrivateKeyException.class) + @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) + @ResponseBody + public String handleIdentifierPrivateKeyException(IdentifierPrivateKeyException ex) { + + return ex.getMessage(); + } + + /** + * Handles exceptions of type InvalidWsIdentifierRequestTypeException and returns the exception + * message. This handler sets the HTTP response status to UNPROCESSABLE_ENTITY (422). + * + * @param ex the InvalidWsIdentifierRequestTypeException to be handled + * @return the exception message as a String + */ + @ExceptionHandler(InvalidWsIdentifierRequestTypeException.class) + @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) + @ResponseBody + public String handleInvalidWsIdentifierRequestTypeException( + InvalidWsIdentifierRequestTypeException ex) { + + return ex.getMessage(); + } + + /** + * Handles exceptions of type InvalidWsIdentifierTokenException and returns an appropriate HTTP + * response with the exception message. + * + * @param ex the InvalidWsIdentifierTokenException to be handled + * @return the exception message as a String + */ + @ExceptionHandler(InvalidWsIdentifierTokenException.class) + @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) + @ResponseBody + public String handleInvalidWsIdentifierTokenException( + InvalidWsIdentifierTokenException ex) { + + return ex.getMessage(); + } + + /** + * Handles exceptions of type TokenPrivateKeyException and returns an appropriate HTTP response + * with the exception message. + * + * @param ex the TokenPrivateKeyException to be handled + * @return the exception message as a String + */ + @ExceptionHandler(TokenPrivateKeyException.class) + @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) + @ResponseBody + public String handleTokenPrivateKeyException( + TokenPrivateKeyException ex) { + + return ex.getMessage(); + } + + /** + * Handles exceptions of type WsGetTokenProcessingException and returns an appropriate HTTP + * response with the exception message. + * + * @param ex the WsGetTokenProcessingException to be handled + * @return the exception message as a String + */ + @ExceptionHandler(WsGetTokenProcessingException.class) + @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) + @ResponseBody + public String handleWsGetTokenProcessingException( + WsGetTokenProcessingException ex) { + + return ex.getMessage(); + } + + /** + * Handles exceptions of type InvalidOINException and returns the exception message. + * This handler sets the HTTP response status to UNPROCESSABLE_ENTITY (422). + * + * @param ex the InvalidOINException to be handled + * @return the exception message as a String + */ + @ExceptionHandler(InvalidOINException.class) + @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) + @ResponseBody + public String handleInvalidOINException( + InvalidOINException ex) { + + return ex.getMessage(); + } +} diff --git a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java index 82f28ce..651c83f 100644 --- a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java +++ b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java @@ -1,7 +1,5 @@ package nl.ictu.controller.v1; -import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; - import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -24,21 +22,16 @@ public final class ExchangeIdentifierController implements ExchangeIdentifierApi * Exchanges an identifier based on the provided caller OIN and request data. * * @param callerOIN The OIN of the caller initiating the request. - * @param wsExchangeRequest The request object containing the identifier and additional data for the exchange process. - * @return A ResponseEntity containing a WsExchangeIdentifierResponse if the exchange is successful, or a ResponseEntity with HTTP status - * UNPROCESSABLE_ENTITY if the exchange fails. + * @param wsExchangeRequest The request object containing the identifier and additional data for + * the exchange process. + * @return A ResponseEntity containing a WsExchangeIdentifierResponse if the exchange is + * successful, or a ResponseEntity with HTTP status UNPROCESSABLE_ENTITY if the exchange fails. */ @Override - @SneakyThrows public ResponseEntity exchangeIdentifier(final String callerOIN, final WsExchangeIdentifierRequest wsExchangeRequest) { - try { - final var identifier = service.exchangeIdentifier(callerOIN, wsExchangeRequest); - return ResponseEntity.ok(identifier); - } catch (Exception ex) { - log.warn(ex.getMessage()); - return ResponseEntity.status(UNPROCESSABLE_ENTITY).build(); - } + final var identifier = service.exchangeIdentifier(callerOIN, wsExchangeRequest); + return ResponseEntity.ok(identifier); } } diff --git a/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java b/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java index bcf2108..b49a65c 100644 --- a/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java +++ b/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java @@ -1,7 +1,5 @@ package nl.ictu.controller.v1; -import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; - import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import nl.ictu.pseudoniemenservice.generated.server.api.ExchangeTokenApi; @@ -19,23 +17,22 @@ public final class ExchangeTokenController implements ExchangeTokenApi, VersionO private final ExchangeTokenService exchangeTokenService; /** - * Handles the exchange of a token and returns the corresponding identifier in a response. This method validates the caller's OIN, processes the - * incoming token using the specified identifier type, and constructs a response accordingly. + * Handles the exchange of a token and returns the corresponding identifier in a response. This + * method validates the caller's OIN, processes the incoming token using the specified + * identifier type, and constructs a response accordingly. * - * @param callerOIN The identifier of the requesting organization (OIN). - * @param wsExchangeTokenForIdentifierRequest The request containing the token and identifier type details. + * @param callerOIN The identifier of the requesting organization + * (OIN). + * @param wsExchangeTokenForIdentifierRequest The request containing the token and identifier + * type details. * @return A response entity containing the converted identifier or a status indicating failure. */ @Override public ResponseEntity exchangeToken(final String callerOIN, final WsExchangeTokenRequest wsExchangeTokenForIdentifierRequest) { - try { - final var wsExchangeTokenResponse = exchangeTokenService.exchangeToken(callerOIN, wsExchangeTokenForIdentifierRequest); - return ResponseEntity.ok(wsExchangeTokenResponse); - } catch (Exception ex) { - log.warn(ex.getMessage()); - } - return ResponseEntity.status(UNPROCESSABLE_ENTITY).build(); + final var wsExchangeTokenResponse = exchangeTokenService.exchangeToken(callerOIN, + wsExchangeTokenForIdentifierRequest); + return ResponseEntity.ok(wsExchangeTokenResponse); } } diff --git a/src/main/java/nl/ictu/controller/v1/GetTokenController.java b/src/main/java/nl/ictu/controller/v1/GetTokenController.java index cbf1eb5..d6ed3f7 100644 --- a/src/main/java/nl/ictu/controller/v1/GetTokenController.java +++ b/src/main/java/nl/ictu/controller/v1/GetTokenController.java @@ -1,9 +1,6 @@ package nl.ictu.controller.v1; -import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; - import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import nl.ictu.pseudoniemenservice.generated.server.api.GetTokenApi; import nl.ictu.pseudoniemenservice.generated.server.model.WsGetTokenRequest; @@ -17,29 +14,23 @@ @RequiredArgsConstructor public final class GetTokenController implements GetTokenApi, VersionOneController { - private final GetTokenService getTokenService; /** * Retrieves a token based on the provided caller identifier and request details. * * @param callerOIN The identifier of the caller organization initiating the request. - * @param wsGetTokenRequest The request object containing the recipient organization identifier and additional details. - * @return A ResponseEntity containing the token if the request is successful, or a ResponseEntity with a status of UNPROCESSABLE_ENTITY if the - * token cannot be retrieved. + * @param wsGetTokenRequest The request object containing the recipient organization identifier + * and additional details. + * @return A ResponseEntity containing the token if the request is successful, or a + * ResponseEntity with a status of UNPROCESSABLE_ENTITY if the token cannot be retrieved. */ @Override - @SneakyThrows public ResponseEntity getToken(final String callerOIN, final WsGetTokenRequest wsGetTokenRequest) { - try { - final var recipientOIN = wsGetTokenRequest.getRecipientOIN(); - final var identifier = wsGetTokenRequest.getIdentifier(); - return ResponseEntity.ok(getTokenService.getWsGetTokenResponse(recipientOIN, identifier)); - } catch (Exception ex) { - log.warn(ex.getMessage()); - } - return ResponseEntity.status(UNPROCESSABLE_ENTITY).build(); + final var recipientOIN = wsGetTokenRequest.getRecipientOIN(); + final var identifier = wsGetTokenRequest.getIdentifier(); + return ResponseEntity.ok(getTokenService.getWsGetTokenResponse(recipientOIN, identifier)); } } diff --git a/src/test/java/nl/ictu/controller/GlobalExceptionHandlerTest.java b/src/test/java/nl/ictu/controller/GlobalExceptionHandlerTest.java new file mode 100644 index 0000000..fe57ec7 --- /dev/null +++ b/src/test/java/nl/ictu/controller/GlobalExceptionHandlerTest.java @@ -0,0 +1,82 @@ +package nl.ictu.controller; + +import static org.mockito.Mockito.doThrow; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.List; +import nl.ictu.controller.exception.InvalidOINException; +import nl.ictu.controller.stub.StubController; +import nl.ictu.controller.stub.StubService; +import nl.ictu.service.exception.IdentifierPrivateKeyException; +import nl.ictu.service.exception.InvalidWsIdentifierRequestTypeException; +import nl.ictu.service.exception.InvalidWsIdentifierTokenException; +import nl.ictu.service.exception.TokenPrivateKeyException; +import nl.ictu.service.exception.WsGetTokenProcessingException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest({GlobalExceptionHandler.class, StubController.class}) +class GlobalExceptionHandlerTest { + + public static final String SERVICE_ERROR_MESSAGE = "Service error"; + @Autowired + private MockMvc mockMvc; + @MockBean + private StubService stubService; + + // Test for handleGenericException + @Test + void handleGenericException_ShouldReturnInternalServerErrorWithMessage() throws Exception { + + mockMvc.perform(get("/non-existent-endpoint")) // Assuming no controller is mapped to this + .andExpect(status().isInternalServerError()) + .andExpect(content().contentType("text/plain;charset=UTF-8")) + .andExpect(content().string( + "An unexpected error occurred: No static resource non-existent-endpoint.")); + } + + @Test + @DisplayName("exchangeToken() -> 422 UNPROCESSABLE_ENTITY on exception") + void exchangeToken_ShouldReturnUnprocessableEntity() throws Exception { + // GIVEN: a stubbed controller and service + // WHEN: the service throws an exception + final var exceptions = List.of( + + new IdentifierPrivateKeyException(SERVICE_ERROR_MESSAGE), + new InvalidWsIdentifierRequestTypeException(SERVICE_ERROR_MESSAGE), + new InvalidWsIdentifierTokenException(SERVICE_ERROR_MESSAGE), + new TokenPrivateKeyException(SERVICE_ERROR_MESSAGE), + new WsGetTokenProcessingException(SERVICE_ERROR_MESSAGE), + new InvalidOINException(SERVICE_ERROR_MESSAGE) + ); + exceptions.forEach(this::testExceptionHandlingBehavior); + } + + /** + * Tests the behavior of exception handling by simulating a scenario where the stub service + * throws the given RuntimeException. This test verifies that when an exception is thrown + * by the service, the system responds with the appropriate HTTP status code. + * + * @param ex the RuntimeException to be thrown by the stub service during the test + */ + private void testExceptionHandlingBehavior(final Exception ex) { + + try { + doThrow(ex) + .when(stubService) + .throwAStubbedException(); + // THEN: perform the POST request + mockMvc.perform(get("/stubby")) + .andExpect(status().isUnprocessableEntity()) + .andExpect(content().string(ex.getMessage())); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/src/test/java/nl/ictu/controller/stub/StubController.java b/src/test/java/nl/ictu/controller/stub/StubController.java new file mode 100644 index 0000000..3499e22 --- /dev/null +++ b/src/test/java/nl/ictu/controller/stub/StubController.java @@ -0,0 +1,28 @@ +package nl.ictu.controller.stub; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +/** + * This controller contains endpoints for interacting with stubbed services. + * It demonstrates a simple REST controller setup with a GET endpoint.This stubbed controller is + * used to test behavior exception handling in congestion with the GlobalExceptionHandler class + */ +@RestController +public class StubController { + + @Autowired + public StubService service; + + @RequestMapping( + method = RequestMethod.GET, + value = "/stubby") + public ResponseEntity get() { + + service.throwAStubbedException(); + return ResponseEntity.ok("stubbed body"); + } +} diff --git a/src/test/java/nl/ictu/controller/stub/StubService.java b/src/test/java/nl/ictu/controller/stub/StubService.java new file mode 100644 index 0000000..4dccfa5 --- /dev/null +++ b/src/test/java/nl/ictu/controller/stub/StubService.java @@ -0,0 +1,11 @@ +package nl.ictu.controller.stub; + +import org.springframework.stereotype.Service; + +@Service +public class StubService { + + public void throwAStubbedException(){ + // mockito will be used to throw a mocked exception from this stubb + } +} diff --git a/src/test/java/nl/ictu/controller/v1/ExchangeTokenControllerTest.java b/src/test/java/nl/ictu/controller/v1/ExchangeTokenControllerTest.java new file mode 100644 index 0000000..70892ed --- /dev/null +++ b/src/test/java/nl/ictu/controller/v1/ExchangeTokenControllerTest.java @@ -0,0 +1,86 @@ +package nl.ictu.controller.v1; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import nl.ictu.controller.exception.InvalidOINException; +import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenRequest; +import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenResponse; +import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; +import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes; +import nl.ictu.service.v1.ExchangeTokenService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(ExchangeTokenController.class) +public class ExchangeTokenControllerTest { + + @Autowired + private MockMvc mockMvc; + @MockBean + private ExchangeTokenService exchangeTokenService; + @Autowired + private ObjectMapper objectMapper; + + @Test + @DisplayName("exchangeToken() -> 200 OK on success") + void exchangeToken_ShouldReturnOk() throws Exception { + // GIVEN: a request payload + WsExchangeTokenRequest requestPayload = new WsExchangeTokenRequest(); + requestPayload.setToken("testToken"); + requestPayload.setIdentifierType(WsIdentifierTypes.BSN); + // AND: a mock service response + WsExchangeTokenResponse responsePayload = new WsExchangeTokenResponse(); + responsePayload.setIdentifier(WsIdentifier.builder() + .type(WsIdentifierTypes.BSN) + .value("convertedIdentifier") + .build()); + // WHEN: the service is called, return the response payload + when(exchangeTokenService.exchangeToken(eq("TEST_OIN"), any(WsExchangeTokenRequest.class))) + .thenReturn(responsePayload); + // THEN: perform the POST request + mockMvc.perform( + post("/v1/exchangeToken") + .header("callerOIN", "TEST_OIN") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestPayload)) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.identifier.value").value("convertedIdentifier")) + .andExpect(jsonPath("$.identifier.type").value("BSN")); + } + + @Test + @DisplayName("exchangeToken() -> 422 UNPROCESSABLE_ENTITY on exception") + void exchangeToken_ShouldReturnUnprocessableEntity() throws Exception { + // GIVEN: a request payload + WsExchangeTokenRequest requestPayload = new WsExchangeTokenRequest(); + requestPayload.setToken("testToken"); + requestPayload.setIdentifierType(WsIdentifierTypes.ORGANISATION_PSEUDO); + // WHEN: the service throws an exception + doThrow(new InvalidOINException("Service error")) + .when(exchangeTokenService) + .exchangeToken(eq("FAIL_OIN"), any(WsExchangeTokenRequest.class)); + // THEN: perform the POST request + mockMvc.perform( + post("/v1/exchangeToken") + .header("callerOIN", "FAIL_OIN") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestPayload)) + ) + .andExpect(status().isUnprocessableEntity()); + } +} From 0c85a8b798a2fe6392d4744aa916842c07e6a840 Mon Sep 17 00:00:00 2001 From: sayf Date: Tue, 24 Dec 2024 23:31:41 +0100 Subject: [PATCH 21/30] More coverage and unit testing --- .../controller/GlobalExceptionHandler.java | 27 +++---- .../v1/ExchangeIdentifierController.java | 1 - .../java/nl/ictu/{ => model}/Identifier.java | 2 +- src/main/java/nl/ictu/{ => model}/Token.java | 2 +- .../nl/ictu/service/v1/GetTokenService.java | 2 +- .../v1/crypto/AesGcmSivCryptographer.java | 2 +- .../v1/crypto/IdentifierConverter.java | 2 +- .../nl/ictu/service/v1/crypto/TokenCoder.java | 2 +- .../ictu/service/v1/map/BsnPseudoMapper.java | 2 +- .../ictu/service/v1/map/BsnTokenMapper.java | 2 +- .../v1/map/OrganisationPseudoTokenMapper.java | 4 +- .../service/v1/validate/OINValidator.java | 2 +- src/main/java/nl/ictu/utils/AESHelper.java | 2 +- .../PseudoniemenServicePropertiesTest.java | 43 +++++++++++ .../service/TestAesGcmSivCryptographer.java | 2 +- .../service/v1/map/BsnPseudoMapperTest.java | 70 +++++++++++++++++ .../service/v1/map/BsnTokenMapperTest.java | 54 +++++++++++++ .../v1/map/EncryptedBsnMapperTest.java | 38 ++++++++++ .../service/v1/map/PseudoBsnMapperTest.java | 48 ++++++++++++ .../java/nl/ictu/utils/AESHelperTest.java | 72 ++++++++++++++++++ .../java/nl/ictu/utils/Base64WrapperTest.java | 75 +++++++++++++++++++ .../nl/ictu/utils/ByteArrayUtilsTest.java | 65 ++++++++++++++++ .../nl/ictu/utils/MessageDigestUtilTest.java | 29 +++++++ 23 files changed, 521 insertions(+), 27 deletions(-) rename src/main/java/nl/ictu/{ => model}/Identifier.java (91%) rename src/main/java/nl/ictu/{ => model}/Token.java (91%) create mode 100644 src/test/java/nl/ictu/configuration/PseudoniemenServicePropertiesTest.java create mode 100644 src/test/java/nl/ictu/service/v1/map/BsnPseudoMapperTest.java create mode 100644 src/test/java/nl/ictu/service/v1/map/BsnTokenMapperTest.java create mode 100644 src/test/java/nl/ictu/service/v1/map/EncryptedBsnMapperTest.java create mode 100644 src/test/java/nl/ictu/service/v1/map/PseudoBsnMapperTest.java create mode 100644 src/test/java/nl/ictu/utils/AESHelperTest.java create mode 100644 src/test/java/nl/ictu/utils/Base64WrapperTest.java create mode 100644 src/test/java/nl/ictu/utils/ByteArrayUtilsTest.java create mode 100644 src/test/java/nl/ictu/utils/MessageDigestUtilTest.java diff --git a/src/main/java/nl/ictu/controller/GlobalExceptionHandler.java b/src/main/java/nl/ictu/controller/GlobalExceptionHandler.java index be69668..190f519 100644 --- a/src/main/java/nl/ictu/controller/GlobalExceptionHandler.java +++ b/src/main/java/nl/ictu/controller/GlobalExceptionHandler.java @@ -19,11 +19,15 @@ public class GlobalExceptionHandler { /** - * Catch-all for any unhandled exceptions. + * Handles generic exceptions and returns an appropriate HTTP response with an error message. + * + * @param ex the Exception to be handled + * @return a ResponseEntity containing a generic error message and an INTERNAL_SERVER_ERROR + * (500) status */ @ExceptionHandler(Exception.class) @ResponseBody - public ResponseEntity handleGenericException(Exception ex) { + public ResponseEntity handleGenericException(final Exception ex) { log.error("Unexpected error occurred", ex); return new ResponseEntity<>( @@ -43,7 +47,7 @@ public ResponseEntity handleGenericException(Exception ex) { @ExceptionHandler(IdentifierPrivateKeyException.class) @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) @ResponseBody - public String handleIdentifierPrivateKeyException(IdentifierPrivateKeyException ex) { + public String handleIdentifierPrivateKeyException(final IdentifierPrivateKeyException ex) { return ex.getMessage(); } @@ -59,7 +63,7 @@ public String handleIdentifierPrivateKeyException(IdentifierPrivateKeyException @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) @ResponseBody public String handleInvalidWsIdentifierRequestTypeException( - InvalidWsIdentifierRequestTypeException ex) { + final InvalidWsIdentifierRequestTypeException ex) { return ex.getMessage(); } @@ -75,7 +79,7 @@ public String handleInvalidWsIdentifierRequestTypeException( @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) @ResponseBody public String handleInvalidWsIdentifierTokenException( - InvalidWsIdentifierTokenException ex) { + final InvalidWsIdentifierTokenException ex) { return ex.getMessage(); } @@ -90,8 +94,7 @@ public String handleInvalidWsIdentifierTokenException( @ExceptionHandler(TokenPrivateKeyException.class) @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) @ResponseBody - public String handleTokenPrivateKeyException( - TokenPrivateKeyException ex) { + public String handleTokenPrivateKeyException(final TokenPrivateKeyException ex) { return ex.getMessage(); } @@ -106,15 +109,14 @@ public String handleTokenPrivateKeyException( @ExceptionHandler(WsGetTokenProcessingException.class) @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) @ResponseBody - public String handleWsGetTokenProcessingException( - WsGetTokenProcessingException ex) { + public String handleWsGetTokenProcessingException(final WsGetTokenProcessingException ex) { return ex.getMessage(); } /** - * Handles exceptions of type InvalidOINException and returns the exception message. - * This handler sets the HTTP response status to UNPROCESSABLE_ENTITY (422). + * Handles exceptions of type InvalidOINException and returns the exception message. This + * handler sets the HTTP response status to UNPROCESSABLE_ENTITY (422). * * @param ex the InvalidOINException to be handled * @return the exception message as a String @@ -122,8 +124,7 @@ public String handleWsGetTokenProcessingException( @ExceptionHandler(InvalidOINException.class) @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) @ResponseBody - public String handleInvalidOINException( - InvalidOINException ex) { + public String handleInvalidOINException(final InvalidOINException ex) { return ex.getMessage(); } diff --git a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java index 651c83f..a223726 100644 --- a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java +++ b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java @@ -1,7 +1,6 @@ package nl.ictu.controller.v1; import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import nl.ictu.pseudoniemenservice.generated.server.api.ExchangeIdentifierApi; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierRequest; diff --git a/src/main/java/nl/ictu/Identifier.java b/src/main/java/nl/ictu/model/Identifier.java similarity index 91% rename from src/main/java/nl/ictu/Identifier.java rename to src/main/java/nl/ictu/model/Identifier.java index 647d829..1fc0f6b 100644 --- a/src/main/java/nl/ictu/Identifier.java +++ b/src/main/java/nl/ictu/model/Identifier.java @@ -1,4 +1,4 @@ -package nl.ictu; +package nl.ictu.model; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/nl/ictu/Token.java b/src/main/java/nl/ictu/model/Token.java similarity index 91% rename from src/main/java/nl/ictu/Token.java rename to src/main/java/nl/ictu/model/Token.java index c47fd21..c70398e 100644 --- a/src/main/java/nl/ictu/Token.java +++ b/src/main/java/nl/ictu/model/Token.java @@ -1,4 +1,4 @@ -package nl.ictu; +package nl.ictu.model; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/nl/ictu/service/v1/GetTokenService.java b/src/main/java/nl/ictu/service/v1/GetTokenService.java index 055d2f9..851f14a 100644 --- a/src/main/java/nl/ictu/service/v1/GetTokenService.java +++ b/src/main/java/nl/ictu/service/v1/GetTokenService.java @@ -13,7 +13,7 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import nl.ictu.Token; +import nl.ictu.model.Token; import nl.ictu.pseudoniemenservice.generated.server.model.WsGetTokenResponse; import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; import nl.ictu.service.exception.WsGetTokenProcessingException; diff --git a/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java b/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java index 6147c09..ba98e25 100644 --- a/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java +++ b/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java @@ -6,7 +6,7 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import nl.ictu.Identifier; +import nl.ictu.model.Identifier; import nl.ictu.configuration.PseudoniemenServiceProperties; import nl.ictu.utils.AESHelper; import nl.ictu.utils.Base64Wrapper; diff --git a/src/main/java/nl/ictu/service/v1/crypto/IdentifierConverter.java b/src/main/java/nl/ictu/service/v1/crypto/IdentifierConverter.java index 4cc7932..b54ca28 100644 --- a/src/main/java/nl/ictu/service/v1/crypto/IdentifierConverter.java +++ b/src/main/java/nl/ictu/service/v1/crypto/IdentifierConverter.java @@ -5,7 +5,7 @@ import java.io.IOException; import java.io.StringWriter; import lombok.RequiredArgsConstructor; -import nl.ictu.Identifier; +import nl.ictu.model.Identifier; import org.springframework.aot.hint.annotation.RegisterReflectionForBinding; import org.springframework.stereotype.Service; diff --git a/src/main/java/nl/ictu/service/v1/crypto/TokenCoder.java b/src/main/java/nl/ictu/service/v1/crypto/TokenCoder.java index f8715d2..3f7b9e1 100644 --- a/src/main/java/nl/ictu/service/v1/crypto/TokenCoder.java +++ b/src/main/java/nl/ictu/service/v1/crypto/TokenCoder.java @@ -5,7 +5,7 @@ import java.io.IOException; import java.io.StringWriter; import lombok.RequiredArgsConstructor; -import nl.ictu.Token; +import nl.ictu.model.Token; import org.springframework.aot.hint.annotation.RegisterReflectionForBinding; import org.springframework.stereotype.Service; diff --git a/src/main/java/nl/ictu/service/v1/map/BsnPseudoMapper.java b/src/main/java/nl/ictu/service/v1/map/BsnPseudoMapper.java index ba8728f..21f47a5 100644 --- a/src/main/java/nl/ictu/service/v1/map/BsnPseudoMapper.java +++ b/src/main/java/nl/ictu/service/v1/map/BsnPseudoMapper.java @@ -4,7 +4,7 @@ import java.io.IOException; import lombok.RequiredArgsConstructor; -import nl.ictu.Identifier; +import nl.ictu.model.Identifier; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierResponse; import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; diff --git a/src/main/java/nl/ictu/service/v1/map/BsnTokenMapper.java b/src/main/java/nl/ictu/service/v1/map/BsnTokenMapper.java index cbe8920..1194075 100644 --- a/src/main/java/nl/ictu/service/v1/map/BsnTokenMapper.java +++ b/src/main/java/nl/ictu/service/v1/map/BsnTokenMapper.java @@ -3,7 +3,7 @@ import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; import lombok.RequiredArgsConstructor; -import nl.ictu.Token; +import nl.ictu.model.Token; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenResponse; import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; import org.springframework.stereotype.Component; diff --git a/src/main/java/nl/ictu/service/v1/map/OrganisationPseudoTokenMapper.java b/src/main/java/nl/ictu/service/v1/map/OrganisationPseudoTokenMapper.java index d5aafc7..e42cd56 100644 --- a/src/main/java/nl/ictu/service/v1/map/OrganisationPseudoTokenMapper.java +++ b/src/main/java/nl/ictu/service/v1/map/OrganisationPseudoTokenMapper.java @@ -4,8 +4,8 @@ import java.io.IOException; import lombok.RequiredArgsConstructor; -import nl.ictu.Identifier; -import nl.ictu.Token; +import nl.ictu.model.Identifier; +import nl.ictu.model.Token; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenResponse; import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; diff --git a/src/main/java/nl/ictu/service/v1/validate/OINValidator.java b/src/main/java/nl/ictu/service/v1/validate/OINValidator.java index fc3ab34..5e46289 100644 --- a/src/main/java/nl/ictu/service/v1/validate/OINValidator.java +++ b/src/main/java/nl/ictu/service/v1/validate/OINValidator.java @@ -1,7 +1,7 @@ package nl.ictu.service.v1.validate; import lombok.RequiredArgsConstructor; -import nl.ictu.Token; +import nl.ictu.model.Token; import org.springframework.stereotype.Component; @Component diff --git a/src/main/java/nl/ictu/utils/AESHelper.java b/src/main/java/nl/ictu/utils/AESHelper.java index ad57557..d5191f4 100644 --- a/src/main/java/nl/ictu/utils/AESHelper.java +++ b/src/main/java/nl/ictu/utils/AESHelper.java @@ -11,7 +11,7 @@ public final class AESHelper { public static final int IV_LENGTH = 12; - private static final int TAG_LENGTH = 128; + public static final int TAG_LENGTH = 128; private static final String CIPHER = "AES/GCM/NoPadding"; private static final SecureRandom SECURE_RANDOM = new SecureRandom(); diff --git a/src/test/java/nl/ictu/configuration/PseudoniemenServicePropertiesTest.java b/src/test/java/nl/ictu/configuration/PseudoniemenServicePropertiesTest.java new file mode 100644 index 0000000..926a384 --- /dev/null +++ b/src/test/java/nl/ictu/configuration/PseudoniemenServicePropertiesTest.java @@ -0,0 +1,43 @@ +package nl.ictu.configuration; + +import nl.ictu.service.exception.IdentifierPrivateKeyException; +import nl.ictu.service.exception.TokenPrivateKeyException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class PseudoniemenServicePropertiesTest { + + @Test + void validate_WhenTokenPrivateKeyIsEmpty_ThrowsTokenPrivateKeyException() { + // GIVEN + PseudoniemenServiceProperties props = new PseudoniemenServiceProperties() + .setTokenPrivateKey("") + .setIdentifierPrivateKey("someIdentifierKey"); + + // WHEN & THEN + assertThrows(TokenPrivateKeyException.class, props::validate); + } + + @Test + void validate_WhenIdentifierPrivateKeyIsEmpty_ThrowsIdentifierPrivateKeyException() { + // GIVEN + PseudoniemenServiceProperties props = new PseudoniemenServiceProperties() + .setTokenPrivateKey("someTokenKey") + .setIdentifierPrivateKey(""); + + // WHEN & THEN + assertThrows(IdentifierPrivateKeyException.class, props::validate); + } + + @Test + void validate_WhenBothKeysAreSet_NoExceptionIsThrown() { + // GIVEN + PseudoniemenServiceProperties props = new PseudoniemenServiceProperties() + .setTokenPrivateKey("someTokenKey") + .setIdentifierPrivateKey("someIdentifierKey"); + + // WHEN & THEN + assertDoesNotThrow(props::validate); + } +} diff --git a/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java b/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java index d9c7828..741f33f 100644 --- a/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java +++ b/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java @@ -8,7 +8,7 @@ import java.util.HashSet; import java.util.Set; import lombok.extern.slf4j.Slf4j; -import nl.ictu.Identifier; +import nl.ictu.model.Identifier; import nl.ictu.configuration.PseudoniemenServiceProperties; import nl.ictu.service.v1.crypto.AesGcmCryptographer; import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; diff --git a/src/test/java/nl/ictu/service/v1/map/BsnPseudoMapperTest.java b/src/test/java/nl/ictu/service/v1/map/BsnPseudoMapperTest.java new file mode 100644 index 0000000..3748c73 --- /dev/null +++ b/src/test/java/nl/ictu/service/v1/map/BsnPseudoMapperTest.java @@ -0,0 +1,70 @@ +package nl.ictu.service.v1.map; + +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import nl.ictu.model.Identifier; +import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierResponse; +import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class BsnPseudoMapperTest { + + @Mock + private AesGcmSivCryptographer aesGcmSivCryptographer; + @InjectMocks + private BsnPseudoMapper bsnPseudoMapper; + + @Test + void map_ShouldReturnWsExchangeIdentifierResponse_WhenEncryptionSucceeds() throws Exception { + // GIVEN + String bsn = "123456789"; + String oin = "OIN_X"; + String encryptedValue = "encryptedBsn123"; + // Mock the cryptographer to return a known "encrypted" value + when(aesGcmSivCryptographer.encrypt(any(Identifier.class), eq(oin))) + .thenReturn(encryptedValue); + // WHEN + WsExchangeIdentifierResponse response = bsnPseudoMapper.map(bsn, oin); + // THEN + assertNotNull(response); + assertNotNull(response.getIdentifier()); + assertEquals(ORGANISATION_PSEUDO, response.getIdentifier().getType()); + assertEquals(encryptedValue, response.getIdentifier().getValue()); + } + + @Test + void map_ShouldThrowIOException_WhenEncryptThrowsIOException() throws Exception { + // GIVEN + String bsn = "987654321"; + String oin = "OIN_IO"; + when(aesGcmSivCryptographer.encrypt(any(Identifier.class), eq(oin))) + .thenThrow(new IOException("Simulated I/O error")); + // WHEN & THEN + assertThrows(IOException.class, () -> bsnPseudoMapper.map(bsn, oin)); + } + + @Test + void map_ShouldThrowInvalidCipherTextException_WhenEncryptThrowsInvalidCipherTextException() + throws Exception { + // GIVEN + String bsn = "111222333"; + String oin = "OIN_CIPHER"; + when(aesGcmSivCryptographer.encrypt(any(Identifier.class), eq(oin))) + .thenThrow(new InvalidCipherTextException("Simulated cipher error")); + // WHEN & THEN + assertThrows(InvalidCipherTextException.class, () -> bsnPseudoMapper.map(bsn, oin)); + } +} diff --git a/src/test/java/nl/ictu/service/v1/map/BsnTokenMapperTest.java b/src/test/java/nl/ictu/service/v1/map/BsnTokenMapperTest.java new file mode 100644 index 0000000..4a0b9d7 --- /dev/null +++ b/src/test/java/nl/ictu/service/v1/map/BsnTokenMapperTest.java @@ -0,0 +1,54 @@ +package nl.ictu.service.v1.map; + +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import nl.ictu.model.Token; +import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class BsnTokenMapperTest { + + private BsnTokenMapper bsnTokenMapper; + + @BeforeEach + void setUp() { + + bsnTokenMapper = new BsnTokenMapper(); + } + + @Test + void map_ShouldReturnResponseWithBsnIdentifier() { + // GIVEN + final var token = Token.builder() + .bsn("123456789") + .build(); // Suppose your Token class has setBsn(...) method + // WHEN + WsExchangeTokenResponse response = bsnTokenMapper.map(token); + // THEN + assertNotNull(response, "Response should not be null"); + assertNotNull(response.getIdentifier(), "Identifier should not be null"); + assertEquals(BSN, response.getIdentifier().getType(), "Identifier type should be BSN"); + assertEquals("123456789", response.getIdentifier().getValue(), + "Identifier value should match token’s BSN"); + } + + @Test + void map_ShouldHandleNullBsnGracefully() { + // GIVEN + final var token = Token.builder().build(); // No BSN set + // WHEN + WsExchangeTokenResponse response = bsnTokenMapper.map(token); + // THEN + assertNotNull(response, "Response should not be null even if BSN is null"); + assertNotNull(response.getIdentifier(), "Identifier should not be null"); + assertEquals(BSN, response.getIdentifier().getType(), + "Identifier type should still be BSN"); + // Since token.getBsn() is null, we expect null in the identifier's value + assertNull(response.getIdentifier().getValue(), + "Value should be null if token’s BSN is null"); + } +} diff --git a/src/test/java/nl/ictu/service/v1/map/EncryptedBsnMapperTest.java b/src/test/java/nl/ictu/service/v1/map/EncryptedBsnMapperTest.java new file mode 100644 index 0000000..0166c5f --- /dev/null +++ b/src/test/java/nl/ictu/service/v1/map/EncryptedBsnMapperTest.java @@ -0,0 +1,38 @@ +package nl.ictu.service.v1.map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +import nl.ictu.model.Identifier; +import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class EncryptedBsnMapperTest { + + @Mock + private AesGcmSivCryptographer aesGcmSivCryptographer; + @InjectMocks + private EncryptedBsnMapper encryptedBsnMapper; + + @Test + void map_ShouldReturnDecryptedBsn_WhenDecryptSucceeds() { + // GIVEN + String encryptedBsn = "someEncryptedValue"; + String recipientOin = "testOIN"; + // Suppose decrypt returns an Identifier with "123456789" as BSN + Identifier decryptedIdentifier = Identifier.builder() + .bsn("123456789") + .build(); + when(aesGcmSivCryptographer.decrypt(encryptedBsn, recipientOin)) + .thenReturn(decryptedIdentifier); + // WHEN + String result = encryptedBsnMapper.map(encryptedBsn, recipientOin); + // THEN + assertEquals("123456789", result); + } +} diff --git a/src/test/java/nl/ictu/service/v1/map/PseudoBsnMapperTest.java b/src/test/java/nl/ictu/service/v1/map/PseudoBsnMapperTest.java new file mode 100644 index 0000000..3aa3497 --- /dev/null +++ b/src/test/java/nl/ictu/service/v1/map/PseudoBsnMapperTest.java @@ -0,0 +1,48 @@ +package nl.ictu.service.v1.map; + +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.when; + +import nl.ictu.model.Identifier; +import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierResponse; +import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Unit tests for {@link PseudoBsnMapper}. + */ +@ExtendWith(MockitoExtension.class) +class PseudoBsnMapperTest { + + @Mock + private AesGcmSivCryptographer aesGcmSivCryptographer; + @InjectMocks + private PseudoBsnMapper pseudoBsnMapper; + + @Test + void map_ShouldReturnDecryptedBsn_WhenDecryptionSucceeds() throws Exception { + // GIVEN + String pseudo = "someEncryptedString"; + String oin = "TEST_OIN"; + // Suppose the decrypted Identifier has BSN "123456789" + Identifier decryptedIdentifier = Identifier.builder() + .bsn("123456789") + .build(); + when(aesGcmSivCryptographer.decrypt(pseudo, oin)) + .thenReturn(decryptedIdentifier); + // WHEN + WsExchangeIdentifierResponse response = pseudoBsnMapper.map(pseudo, oin); + // THEN + assertNotNull(response, "Response should not be null"); + assertNotNull(response.getIdentifier(), "Identifier should not be null"); + assertEquals(BSN, response.getIdentifier().getType(), "Type should be BSN"); + assertEquals("123456789", response.getIdentifier().getValue(), + "Decrypted BSN value should match"); + } +} diff --git a/src/test/java/nl/ictu/utils/AESHelperTest.java b/src/test/java/nl/ictu/utils/AESHelperTest.java new file mode 100644 index 0000000..cccf91f --- /dev/null +++ b/src/test/java/nl/ictu/utils/AESHelperTest.java @@ -0,0 +1,72 @@ +package nl.ictu.utils; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.security.NoSuchAlgorithmException; +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.GCMParameterSpec; +import org.bouncycastle.crypto.MultiBlockCipher; +import org.bouncycastle.crypto.engines.AESEngine; +import org.junit.jupiter.api.Test; + +class AESHelperTest { + + @Test + void generateIV_ShouldReturnGCMParameterSpec_WithNonNullIV() { + // WHEN + GCMParameterSpec gcmParameterSpec = AESHelper.generateIV(); + // THEN + assertNotNull(gcmParameterSpec, "GCMParameterSpec should not be null"); + assertEquals(AESHelper.TAG_LENGTH, gcmParameterSpec.getTLen(), + "Tag length should be 128 (bits)"); + // The IV array is extracted from gcmParameterSpec + byte[] iv = gcmParameterSpec.getIV(); + assertNotNull(iv, "IV should not be null"); + assertEquals(AESHelper.IV_LENGTH, iv.length, + "IV length should be " + AESHelper.IV_LENGTH); + } + + @Test + void createIVfromValues_ShouldReturnGCMParameterSpec_FromGivenIV() { + // GIVEN + byte[] ivSource = new byte[AESHelper.IV_LENGTH]; + // Fill the array with deterministic data for test + for (int i = 0; i < ivSource.length; i++) { + ivSource[i] = (byte) i; + } + // WHEN + GCMParameterSpec spec = AESHelper.createIVfromValues(ivSource); + // THEN + assertNotNull(spec, "GCMParameterSpec should not be null"); + assertEquals(AESHelper.TAG_LENGTH, spec.getTLen(), + "Tag length should be 128 (bits)"); + assertArrayEquals(ivSource, spec.getIV(), + "IV array in GCMParameterSpec should match the input"); + } + + @Test + void createCipher_ShouldReturnAesGcmNoPaddingCipher() + throws NoSuchPaddingException, NoSuchAlgorithmException { + // WHEN + Cipher cipher = AESHelper.createCipher(); + // THEN + assertNotNull(cipher, "Cipher should not be null"); + // Depending on the JVM/provider, the algorithm name can be uppercase or some variation, + // but typically you'd expect "AES/GCM/NoPadding". + assertEquals("AES/GCM/NoPadding", cipher.getAlgorithm(), + "Cipher algorithm should match AES/GCM/NoPadding"); + } + + @Test + void getAESEngine_ShouldReturnNonNullAESEngineInstance() { + // WHEN + MultiBlockCipher engine = AESHelper.getAESEngine(); + // THEN + assertNotNull(engine, "Engine should not be null"); + assertInstanceOf(AESEngine.class, engine, "Engine should be an instance of AESEngine"); + } +} diff --git a/src/test/java/nl/ictu/utils/Base64WrapperTest.java b/src/test/java/nl/ictu/utils/Base64WrapperTest.java new file mode 100644 index 0000000..aec3e00 --- /dev/null +++ b/src/test/java/nl/ictu/utils/Base64WrapperTest.java @@ -0,0 +1,75 @@ +package nl.ictu.utils; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class Base64WrapperTest { + + private Base64Wrapper base64Wrapper; + + @BeforeEach + void setUp() { + base64Wrapper = new Base64Wrapper(); + } + + @Test + @DisplayName("encode() -> Should encode bytes to Base64 bytes") + void encode_ShouldEncodeBytesToBase64Bytes() { + // GIVEN + byte[] input = "Hello".getBytes(StandardCharsets.UTF_8); + + // WHEN + byte[] result = base64Wrapper.encode(input); + + // THEN + String resultAsString = new String(result, StandardCharsets.UTF_8); + assertEquals("SGVsbG8=", resultAsString, + "Expected Base64 encoding of 'Hello' to be 'SGVsbG8='"); + } + + @Test + @DisplayName("encodeToString() -> Should encode bytes to Base64 string") + void encodeToString_ShouldEncodeBytesToBase64String() { + // GIVEN + byte[] input = "Hello".getBytes(StandardCharsets.UTF_8); + + // WHEN + String base64String = base64Wrapper.encodeToString(input); + + // THEN + assertEquals("SGVsbG8=", base64String, + "Expected Base64 encoding of 'Hello' to be 'SGVsbG8='"); + } + + @Test + @DisplayName("decode() -> Should decode Base64 string to bytes") + void decode_ShouldDecodeBase64StringToBytes() { + // GIVEN + String base64String = "SGVsbG8="; + + // WHEN + byte[] decoded = base64Wrapper.decode(base64String); + + // THEN + String decodedAsString = new String(decoded, StandardCharsets.UTF_8); + assertEquals("Hello", decodedAsString, + "Expected Base64 decoding of 'SGVsbG8=' to be 'Hello'"); + } + + @Test + @DisplayName("decode() -> Should throw IllegalArgumentException on invalid Base64") + void decode_ShouldThrowException_WhenInvalidBase64String() { + // GIVEN + String invalidBase64 = "Not valid base64!!!"; + + // WHEN & THEN + // Base64.getDecoder().decode(...) throws IllegalArgumentException on invalid input + assertThrows(IllegalArgumentException.class, + () -> base64Wrapper.decode(invalidBase64), + "Expected decode() to throw IllegalArgumentException for invalid Base64 string"); + } +} diff --git a/src/test/java/nl/ictu/utils/ByteArrayUtilsTest.java b/src/test/java/nl/ictu/utils/ByteArrayUtilsTest.java new file mode 100644 index 0000000..cb60e52 --- /dev/null +++ b/src/test/java/nl/ictu/utils/ByteArrayUtilsTest.java @@ -0,0 +1,65 @@ +package nl.ictu.utils; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ByteArrayUtilsTest { + + @Test + @DisplayName("concat() -> should concatenate two non-empty arrays") + void concat_ShouldConcatenateTwoArrays() { + // GIVEN + byte[] a = {1, 2, 3}; + byte[] b = {4, 5, 6}; + byte[] expected = {1, 2, 3, 4, 5, 6}; + + // WHEN + byte[] result = ByteArrayUtils.concat(a, b); + + // THEN + assertArrayEquals(expected, result); + } + + @Test + @DisplayName("concat() -> should return empty array if both inputs are empty") + void concat_ShouldHandleTwoEmptyArrays() { + // GIVEN + byte[] a = {}; + byte[] b = {}; + byte[] expected = {}; + + // WHEN + byte[] result = ByteArrayUtils.concat(a, b); + + // THEN + assertArrayEquals(expected, result); + } + + @Test + @DisplayName("concat() -> should handle empty array on either side") + void concat_ShouldHandleOneEmptyArray() { + // GIVEN + byte[] a = {1, 2, 3}; + byte[] b = {}; + byte[] expected1 = {1, 2, 3}; + + // WHEN + byte[] result1 = ByteArrayUtils.concat(a, b); + + // THEN + assertArrayEquals(expected1, result1); + + // GIVEN + byte[] c = {}; + byte[] d = {4, 5, 6}; + byte[] expected2 = {4, 5, 6}; + + // WHEN + byte[] result2 = ByteArrayUtils.concat(c, d); + + // THEN + assertArrayEquals(expected2, result2); + } +} diff --git a/src/test/java/nl/ictu/utils/MessageDigestUtilTest.java b/src/test/java/nl/ictu/utils/MessageDigestUtilTest.java new file mode 100644 index 0000000..de1b27f --- /dev/null +++ b/src/test/java/nl/ictu/utils/MessageDigestUtilTest.java @@ -0,0 +1,29 @@ +package nl.ictu.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.security.MessageDigest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class MessageDigestUtilTest { + + private MessageDigestUtil messageDigestUtil; + + @BeforeEach + void setUp() { + messageDigestUtil = new MessageDigestUtil(); + } + + @Test + void getMessageDigestSha256_ShouldReturnSha256Digest() { + // WHEN + MessageDigest digest = messageDigestUtil.getMessageDigestSha256(); + + // THEN + assertNotNull(digest, "MessageDigest should not be null"); + assertEquals("SHA-256", digest.getAlgorithm(), + "Expected the digest algorithm to be SHA-256"); + } +} From b21f43dfcb6cbaadff7eb87fbb1bbd4af4dcc67d Mon Sep 17 00:00:00 2001 From: sayf Date: Tue, 24 Dec 2024 23:39:34 +0100 Subject: [PATCH 22/30] More coverage and unit testing --- .../v1/crypto/AesGcmCryptographer.java | 51 +++++++++++++++++-- .../v1/crypto/AesGcmSivCryptographer.java | 38 +++++++++++--- .../v1/crypto/IdentifierConverter.java | 19 +++++-- .../nl/ictu/service/v1/crypto/TokenCoder.java | 20 ++++++-- 4 files changed, 108 insertions(+), 20 deletions(-) diff --git a/src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographer.java b/src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographer.java index 217eadc..b1bff91 100644 --- a/src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographer.java +++ b/src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographer.java @@ -19,14 +19,12 @@ import nl.ictu.utils.Base64Wrapper; import nl.ictu.utils.ByteArrayUtils; import nl.ictu.utils.MessageDigestUtil; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; /** * Advanced Encryption Standard Galois/Counter Mode (AES-GCM). */ - -@SuppressWarnings("DesignForExtension") -@Service +@Component @RequiredArgsConstructor public class AesGcmCryptographer { @@ -34,6 +32,24 @@ public class AesGcmCryptographer { private final MessageDigestUtil messageDigestUtil; private final PseudoniemenServiceProperties pseudoniemenServiceProperties; + /** + * Encrypts the given plaintext using the Advanced Encryption Standard in Galois/Counter Mode + * (AES-GCM) and a provided salt. The resulting ciphertext is Base64 encoded and includes the IV + * used during encryption, concatenated with the encrypted data. + * + * @param plaintext the plaintext message to be encrypted + * @param salt the salt value used to derive the encryption key + * @return the Base64 encoded ciphertext, including the IV + * @throws IllegalBlockSizeException if the block size is invalid during the encryption + * process + * @throws BadPaddingException if there are issues with padding during + * encryption + * @throws InvalidAlgorithmParameterException if the provided algorithm parameters are invalid + * @throws InvalidKeyException if the encryption key is invalid + * @throws NoSuchAlgorithmException if the requested encryption algorithm is not + * available + * @throws NoSuchPaddingException if the requested padding scheme is not available + */ public String encrypt(final String plaintext, final String salt) throws IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { @@ -47,6 +63,14 @@ public String encrypt(final String plaintext, final String salt) return base64Wrapper.encodeToString(encryptedWithIV); } + /** + * Creates a secret encryption key by combining a base64-decoded private key with a given salt, + * and hashing the result using SHA-256. + * + * @param salt the salt value used to modify the private key and derive the final encryption + * key + * @return a SecretKey instance derived from the combined and hashed input + */ private SecretKey createSecretKey(final String salt) { final var keyBytes = base64Wrapper.decode( @@ -55,9 +79,26 @@ private SecretKey createSecretKey(final String salt) { final var salterSecretBytes = ByteArrayUtils.concat(keyBytes, saltBytes); final var key = messageDigestUtil.getMessageDigestSha256().digest(salterSecretBytes); return new SecretKeySpec(key, "AES"); - } + /** + * Decrypts the given Base64 encoded ciphertext, which includes the initialization vector (IV), + * using the Advanced Encryption Standard in Galois/Counter Mode (AES-GCM) with a provided + * salt. + * + * @param ciphertextWithIv the Base64 encoded encrypted data, including the IV + * @param salt the salt value used to derive the decryption key + * @return the decrypted plaintext as a UTF-8 string + * @throws NoSuchPaddingException if the requested padding scheme is not available + * @throws NoSuchAlgorithmException if the requested encryption algorithm is not + * available + * @throws InvalidAlgorithmParameterException if the provided algorithm parameters are invalid + * @throws InvalidKeyException if the decryption key is invalid + * @throws IllegalBlockSizeException if the block size is invalid during the decryption + * process + * @throws BadPaddingException if there are issues with padding during + * decryption + */ public String decrypt(final String ciphertextWithIv, final String salt) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { diff --git a/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java b/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java index ba98e25..20122b9 100644 --- a/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java +++ b/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java @@ -6,8 +6,8 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import nl.ictu.model.Identifier; import nl.ictu.configuration.PseudoniemenServiceProperties; +import nl.ictu.model.Identifier; import nl.ictu.utils.AESHelper; import nl.ictu.utils.Base64Wrapper; import nl.ictu.utils.MessageDigestUtil; @@ -15,26 +15,30 @@ import org.bouncycastle.crypto.modes.GCMSIVBlockCipher; import org.bouncycastle.crypto.params.AEADParameters; import org.bouncycastle.crypto.params.KeyParameter; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; /** * Advanced Encryption Standard Galois/Counter Mode synthetic initialization vector. */ - @Slf4j -@SuppressWarnings("DesignForExtension") -@Service +@Component @RequiredArgsConstructor public class AesGcmSivCryptographer { public static final int MAC_SIZE = 128; private static final int NONCE_LENTH = 12; - private final PseudoniemenServiceProperties pseudoniemenServiceProperties; private final MessageDigestUtil messageDigestUtil; private final IdentifierConverter identifierConverter; private final Base64Wrapper base64Wrapper; + /** + * Creates AEADParameters using the given salt to generate a nonce and a private key for the + * encryption process. + * + * @param salt the salt used to derive the nonce for the encryption process + * @return AEADParameters containing the key, MAC size, and nonce for encryption + */ private AEADParameters createSecretKey(final String salt) { final var nonce16 = messageDigestUtil.getMessageDigestSha256() @@ -44,9 +48,19 @@ private AEADParameters createSecretKey(final String salt) { final KeyParameter keyParameter = new KeyParameter( base64Wrapper.decode(identifierPrivateKey)); return new AEADParameters(keyParameter, MAC_SIZE, nonce12); - } + /** + * Encrypts the given {@code Identifier} using a salt and returns the resulting Base64-encoded + * ciphertext. This method leverages AES-GCM-SIV encryption for secure and authenticated + * encryption. + * + * @param identifier the identifier object to be encrypted + * @param salt a string used to derive a nonce and key for encryption + * @return the Base64-encoded string representation of the ciphertext + * @throws InvalidCipherTextException if encryption process fails + * @throws IOException if an I/O error occurs during encryption + */ public String encrypt(final Identifier identifier, final String salt) throws InvalidCipherTextException, IOException { @@ -60,9 +74,17 @@ public String encrypt(final Identifier identifier, final String salt) cipher.doFinal(ciphertext, outputLength); cipher.reset(); return base64Wrapper.encodeToString(ciphertext); - } + /** + * Decrypts the given Base64-encoded ciphertext string using the provided salt. This method uses + * AES-GCM-SIV decryption to securely retrieve the original plaintext. + * + * @param ciphertextString the Base64-encoded string containing the ciphertext to be decrypted + * @param salt a string used to derive the nonce and key for decryption + * @return the decrypted {@code Identifier} object + * @throws InvalidCipherTextException if decryption fails or the ciphertext is invalid + */ @SneakyThrows public Identifier decrypt(final String ciphertextString, final String salt) { diff --git a/src/main/java/nl/ictu/service/v1/crypto/IdentifierConverter.java b/src/main/java/nl/ictu/service/v1/crypto/IdentifierConverter.java index b54ca28..4a488fd 100644 --- a/src/main/java/nl/ictu/service/v1/crypto/IdentifierConverter.java +++ b/src/main/java/nl/ictu/service/v1/crypto/IdentifierConverter.java @@ -7,16 +7,22 @@ import lombok.RequiredArgsConstructor; import nl.ictu.model.Identifier; import org.springframework.aot.hint.annotation.RegisterReflectionForBinding; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; -@SuppressWarnings("DesignForExtension") -@Service +@Component @RequiredArgsConstructor @RegisterReflectionForBinding({Identifier.class}) public class IdentifierConverter { private final ObjectMapper objectMapper; + /** + * Encodes the given Identifier object into its JSON representation as a string. + * + * @param identifier the Identifier object to be encoded + * @return the JSON string representation of the given Identifier object + * @throws IOException if an I/O error occurs during encoding + */ public String encode(final Identifier identifier) throws IOException { final StringWriter stringWriter = new StringWriter(); @@ -24,6 +30,13 @@ public String encode(final Identifier identifier) throws IOException { return stringWriter.toString(); } + /** + * Decodes a JSON string into an Identifier object. + * + * @param encodedIdentifier the JSON string representation of an Identifier object + * @return the deserialized Identifier object + * @throws JsonProcessingException if an error occurs while processing the JSON string + */ public Identifier decode(final String encodedIdentifier) throws JsonProcessingException { return objectMapper.readValue(encodedIdentifier, Identifier.class); diff --git a/src/main/java/nl/ictu/service/v1/crypto/TokenCoder.java b/src/main/java/nl/ictu/service/v1/crypto/TokenCoder.java index 3f7b9e1..9c9c878 100644 --- a/src/main/java/nl/ictu/service/v1/crypto/TokenCoder.java +++ b/src/main/java/nl/ictu/service/v1/crypto/TokenCoder.java @@ -7,16 +7,22 @@ import lombok.RequiredArgsConstructor; import nl.ictu.model.Token; import org.springframework.aot.hint.annotation.RegisterReflectionForBinding; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; -@SuppressWarnings("DesignForExtension") -@Service +@Component @RequiredArgsConstructor @RegisterReflectionForBinding({Token.class}) public class TokenCoder { private final ObjectMapper objectMapper; + /** + * Encodes the given Token object to its JSON string representation. + * + * @param token the Token object to be encoded + * @return the JSON string representation of the given Token object + * @throws IOException if an I/O error occurs during encoding + */ public String encode(final Token token) throws IOException { final var stringWriter = new StringWriter(); @@ -24,9 +30,15 @@ public String encode(final Token token) throws IOException { return stringWriter.toString(); } + /** + * Decodes a JSON-encoded token string into a Token object. + * + * @param encodedToken the JSON string representation of a Token + * @return the decoded Token object + * @throws JsonProcessingException if the JSON string cannot be parsed into a Token object + */ public Token decode(final String encodedToken) throws JsonProcessingException { return objectMapper.readValue(encodedToken, Token.class); } - } From cc517dcd4d15b5e6ad658e5b32370b01e27aa8f7 Mon Sep 17 00:00:00 2001 From: sayf Date: Tue, 24 Dec 2024 23:45:49 +0100 Subject: [PATCH 23/30] More cleanup --- checkstyle.xml | 308 +++++++++--------- .../ictu/PseudoniemenServiceApplication.java | 10 +- .../nl/ictu/TestingWebApplicationTests.java | 7 +- .../GlobalExceptionHandlerTest.java | 2 +- .../v1/ExchangeTokenControllerTest.java | 2 +- 5 files changed, 157 insertions(+), 172 deletions(-) diff --git a/checkstyle.xml b/checkstyle.xml index c93c84c..30e5676 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -1,7 +1,7 @@ + "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" + "https://checkstyle.org/dtds/configuration_1_3.dtd"> - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/main/java/nl/ictu/PseudoniemenServiceApplication.java b/src/main/java/nl/ictu/PseudoniemenServiceApplication.java index 0daa17c..9d4e3bd 100644 --- a/src/main/java/nl/ictu/PseudoniemenServiceApplication.java +++ b/src/main/java/nl/ictu/PseudoniemenServiceApplication.java @@ -1,18 +1,15 @@ package nl.ictu; - import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.security.Security; import lombok.NoArgsConstructor; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import java.security.NoSuchAlgorithmException; -import java.security.Security; - @SuppressWarnings({"HideUtilityClassConstructor"}) @SuppressFBWarnings(value = "EI_EXPOSE_STATIC_REP2", - justification = "nl.ictu.PseudoniemenServiceApplication$$SpringCGLIB$$0") + justification = "nl.ictu.PseudoniemenServiceApplication$$SpringCGLIB$$0") @SpringBootApplication @NoArgsConstructor public class PseudoniemenServiceApplication { @@ -21,7 +18,8 @@ public class PseudoniemenServiceApplication { Security.addProvider(new BouncyCastleProvider()); } - public static void main(final String[] args) throws NoSuchAlgorithmException { + public static void main(final String[] args) { + SpringApplication.run(PseudoniemenServiceApplication.class, args); } } diff --git a/src/test/java/nl/ictu/TestingWebApplicationTests.java b/src/test/java/nl/ictu/TestingWebApplicationTests.java index c0c0cde..9f2afd9 100644 --- a/src/test/java/nl/ictu/TestingWebApplicationTests.java +++ b/src/test/java/nl/ictu/TestingWebApplicationTests.java @@ -31,11 +31,6 @@ class TestingWebApplicationTests { @Autowired private TestRestTemplate restTemplate; - @Test - void contextLoads() { - - } - @Test void testActuatorHealthEndpoint() { @@ -50,7 +45,7 @@ void testGetAtokenExchangeForBSN() { // get a token final var getTokenBody = Map.of("recipientOIN", "54321543215432154321", "identifier", Map.of("type", "BSN", "value", "012345679")); - final var httpEntityGetToken = new HttpEntity(getTokenBody, + final var httpEntityGetToken = new HttpEntity<>(getTokenBody, new HttpHeaders(CollectionUtils.toMultiValueMap(of("callerOIN", List.of("0912345012345012345012345"))))); final var tokenExchange = restTemplate.exchange("/v1/getToken", HttpMethod.POST, httpEntityGetToken, Map.class); assertThat(tokenExchange.getStatusCode()).isEqualTo(HttpStatus.OK); diff --git a/src/test/java/nl/ictu/controller/GlobalExceptionHandlerTest.java b/src/test/java/nl/ictu/controller/GlobalExceptionHandlerTest.java index fe57ec7..0086e61 100644 --- a/src/test/java/nl/ictu/controller/GlobalExceptionHandlerTest.java +++ b/src/test/java/nl/ictu/controller/GlobalExceptionHandlerTest.java @@ -43,7 +43,7 @@ void handleGenericException_ShouldReturnInternalServerErrorWithMessage() throws @Test @DisplayName("exchangeToken() -> 422 UNPROCESSABLE_ENTITY on exception") - void exchangeToken_ShouldReturnUnprocessableEntity() throws Exception { + void exchangeToken_ShouldReturnUnprocessableEntity() { // GIVEN: a stubbed controller and service // WHEN: the service throws an exception final var exceptions = List.of( diff --git a/src/test/java/nl/ictu/controller/v1/ExchangeTokenControllerTest.java b/src/test/java/nl/ictu/controller/v1/ExchangeTokenControllerTest.java index 70892ed..f3bee53 100644 --- a/src/test/java/nl/ictu/controller/v1/ExchangeTokenControllerTest.java +++ b/src/test/java/nl/ictu/controller/v1/ExchangeTokenControllerTest.java @@ -25,7 +25,7 @@ import org.springframework.test.web.servlet.MockMvc; @WebMvcTest(ExchangeTokenController.class) -public class ExchangeTokenControllerTest { +class ExchangeTokenControllerTest { @Autowired private MockMvc mockMvc; From 424023087076cfe7f9bb48737b077a375365e4ee Mon Sep 17 00:00:00 2001 From: sayf Date: Wed, 25 Dec 2024 09:24:32 +0100 Subject: [PATCH 24/30] More unit testing --- checkstyle.xml | 1 - .../v1/crypto/AesGcmCryptographer.java | 24 ++--- .../v1/crypto/AesGcmSivCryptographer.java | 12 +-- src/main/java/nl/ictu/utils/AESHelper.java | 47 --------- src/main/java/nl/ictu/utils/AesUtility.java | 67 +++++++++++++ .../java/nl/ictu/utils/Base64Wrapper.java | 18 ++++ .../java/nl/ictu/utils/ByteArrayUtil.java | 23 +++++ .../java/nl/ictu/utils/ByteArrayUtils.java | 14 --- .../java/nl/ictu/utils/MessageDigestUtil.java | 17 ---- .../nl/ictu/utils/MessageDigestWrapper.java | 22 +++++ .../ictu/service/TestAesGcmCryptographer.java | 4 +- .../service/TestAesGcmSivCryptographer.java | 4 +- .../v1/ExchangeIdentifierServiceTest.java | 99 +++++++++++++++++++ .../OrganisationPseudoTokenMapperTest.java | 74 ++++++++++++++ .../service/v1/validate/OINValidatorTest.java | 64 ++++++++++++ .../java/nl/ictu/utils/AESHelperTest.java | 18 ++-- .../java/nl/ictu/utils/AesUtilityTest.java | 73 ++++++++++++++ .../nl/ictu/utils/ByteArrayUtilsTest.java | 8 +- ...est.java => MessageDigestWrapperTest.java} | 8 +- 19 files changed, 479 insertions(+), 118 deletions(-) delete mode 100644 src/main/java/nl/ictu/utils/AESHelper.java create mode 100644 src/main/java/nl/ictu/utils/AesUtility.java create mode 100644 src/main/java/nl/ictu/utils/ByteArrayUtil.java delete mode 100644 src/main/java/nl/ictu/utils/ByteArrayUtils.java delete mode 100644 src/main/java/nl/ictu/utils/MessageDigestUtil.java create mode 100644 src/main/java/nl/ictu/utils/MessageDigestWrapper.java create mode 100644 src/test/java/nl/ictu/service/v1/ExchangeIdentifierServiceTest.java create mode 100644 src/test/java/nl/ictu/service/v1/map/OrganisationPseudoTokenMapperTest.java create mode 100644 src/test/java/nl/ictu/service/v1/validate/OINValidatorTest.java create mode 100644 src/test/java/nl/ictu/utils/AesUtilityTest.java rename src/test/java/nl/ictu/utils/{MessageDigestUtilTest.java => MessageDigestWrapperTest.java} (72%) diff --git a/checkstyle.xml b/checkstyle.xml index 30e5676..5bde2a4 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -173,7 +173,6 @@ - diff --git a/src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographer.java b/src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographer.java index b1bff91..ac016c1 100644 --- a/src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographer.java +++ b/src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographer.java @@ -1,6 +1,6 @@ package nl.ictu.service.v1.crypto; -import static nl.ictu.utils.AESHelper.IV_LENGTH; +import static nl.ictu.utils.AesUtility.IV_LENGTH; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; @@ -15,10 +15,10 @@ import javax.crypto.spec.SecretKeySpec; import lombok.RequiredArgsConstructor; import nl.ictu.configuration.PseudoniemenServiceProperties; -import nl.ictu.utils.AESHelper; +import nl.ictu.utils.AesUtility; import nl.ictu.utils.Base64Wrapper; -import nl.ictu.utils.ByteArrayUtils; -import nl.ictu.utils.MessageDigestUtil; +import nl.ictu.utils.ByteArrayUtil; +import nl.ictu.utils.MessageDigestWrapper; import org.springframework.stereotype.Component; /** @@ -29,7 +29,7 @@ public class AesGcmCryptographer { private final Base64Wrapper base64Wrapper; - private final MessageDigestUtil messageDigestUtil; + private final MessageDigestWrapper messageDigestWrapper; private final PseudoniemenServiceProperties pseudoniemenServiceProperties; /** @@ -53,13 +53,13 @@ public class AesGcmCryptographer { public String encrypt(final String plaintext, final String salt) throws IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { - final var cipher = AESHelper.createCipher(); - final var gcmParameterSpec = AESHelper.generateIV(); + final var cipher = AesUtility.createCipher(); + final var gcmParameterSpec = AesUtility.generateIV(); final var secretKey = createSecretKey(salt); cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec); final var ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); final var gcmIV = gcmParameterSpec.getIV(); - final var encryptedWithIV = ByteArrayUtils.concat(gcmIV, ciphertext); + final var encryptedWithIV = ByteArrayUtil.concat(gcmIV, ciphertext); return base64Wrapper.encodeToString(encryptedWithIV); } @@ -76,8 +76,8 @@ private SecretKey createSecretKey(final String salt) { final var keyBytes = base64Wrapper.decode( pseudoniemenServiceProperties.getTokenPrivateKey()); final var saltBytes = salt.getBytes(StandardCharsets.UTF_8); - final var salterSecretBytes = ByteArrayUtils.concat(keyBytes, saltBytes); - final var key = messageDigestUtil.getMessageDigestSha256().digest(salterSecretBytes); + final var salterSecretBytes = ByteArrayUtil.concat(keyBytes, saltBytes); + final var key = messageDigestWrapper.getMessageDigestInstance().digest(salterSecretBytes); return new SecretKeySpec(key, "AES"); } @@ -102,12 +102,12 @@ private SecretKey createSecretKey(final String salt) { public String decrypt(final String ciphertextWithIv, final String salt) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { - final var cipher = AESHelper.createCipher(); + final var cipher = AesUtility.createCipher(); final var encryptedWithIV = base64Wrapper.decode(ciphertextWithIv); final var iv = Arrays.copyOfRange(encryptedWithIV, 0, IV_LENGTH); final var ciphertext = Arrays.copyOfRange(encryptedWithIV, IV_LENGTH, encryptedWithIV.length); - final var gcmParameterSpec = AESHelper.createIVfromValues(iv); + final var gcmParameterSpec = AesUtility.createIVfromValues(iv); final var secretKey = createSecretKey(salt); cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec); final var decryptedText = cipher.doFinal(ciphertext); diff --git a/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java b/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java index 20122b9..e775d2e 100644 --- a/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java +++ b/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java @@ -8,9 +8,9 @@ import lombok.extern.slf4j.Slf4j; import nl.ictu.configuration.PseudoniemenServiceProperties; import nl.ictu.model.Identifier; -import nl.ictu.utils.AESHelper; +import nl.ictu.utils.AesUtility; import nl.ictu.utils.Base64Wrapper; -import nl.ictu.utils.MessageDigestUtil; +import nl.ictu.utils.MessageDigestWrapper; import org.bouncycastle.crypto.InvalidCipherTextException; import org.bouncycastle.crypto.modes.GCMSIVBlockCipher; import org.bouncycastle.crypto.params.AEADParameters; @@ -28,7 +28,7 @@ public class AesGcmSivCryptographer { public static final int MAC_SIZE = 128; private static final int NONCE_LENTH = 12; private final PseudoniemenServiceProperties pseudoniemenServiceProperties; - private final MessageDigestUtil messageDigestUtil; + private final MessageDigestWrapper messageDigestWrapper; private final IdentifierConverter identifierConverter; private final Base64Wrapper base64Wrapper; @@ -41,7 +41,7 @@ public class AesGcmSivCryptographer { */ private AEADParameters createSecretKey(final String salt) { - final var nonce16 = messageDigestUtil.getMessageDigestSha256() + final var nonce16 = messageDigestWrapper.getMessageDigestInstance() .digest(salt.getBytes(StandardCharsets.UTF_8)); final var nonce12 = Arrays.copyOf(nonce16, NONCE_LENTH); final String identifierPrivateKey = pseudoniemenServiceProperties.getIdentifierPrivateKey(); @@ -65,7 +65,7 @@ public String encrypt(final Identifier identifier, final String salt) throws InvalidCipherTextException, IOException { final var plaintext = identifierConverter.encode(identifier); - final var cipher = new GCMSIVBlockCipher(AESHelper.getAESEngine()); + final var cipher = new GCMSIVBlockCipher(AesUtility.getAESEngine()); cipher.init(true, createSecretKey(salt)); final var plainTextBytes = plaintext.getBytes(StandardCharsets.UTF_8); final var ciphertext = new byte[cipher.getOutputSize(plainTextBytes.length)]; @@ -88,7 +88,7 @@ public String encrypt(final Identifier identifier, final String salt) @SneakyThrows public Identifier decrypt(final String ciphertextString, final String salt) { - final var cipher = new GCMSIVBlockCipher(AESHelper.getAESEngine()); + final var cipher = new GCMSIVBlockCipher(AesUtility.getAESEngine()); cipher.init(false, createSecretKey(salt)); final var ciphertext = base64Wrapper.decode(ciphertextString); final var plaintext = new byte[cipher.getOutputSize(ciphertext.length)]; diff --git a/src/main/java/nl/ictu/utils/AESHelper.java b/src/main/java/nl/ictu/utils/AESHelper.java deleted file mode 100644 index d5191f4..0000000 --- a/src/main/java/nl/ictu/utils/AESHelper.java +++ /dev/null @@ -1,47 +0,0 @@ -package nl.ictu.utils; - -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import javax.crypto.Cipher; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.spec.GCMParameterSpec; -import org.bouncycastle.crypto.MultiBlockCipher; -import org.bouncycastle.crypto.engines.AESEngine; - -public final class AESHelper { - - public static final int IV_LENGTH = 12; - public static final int TAG_LENGTH = 128; - private static final String CIPHER = "AES/GCM/NoPadding"; - private static final SecureRandom SECURE_RANDOM = new SecureRandom(); - - private AESHelper() { - - } - - // Method to generate a random Initialization Vector (IV) - public static GCMParameterSpec generateIV() { - - byte[] iv = new byte[IV_LENGTH]; // AES block size is 16 bytes - SECURE_RANDOM.nextBytes(iv); - - return new GCMParameterSpec(TAG_LENGTH, iv); - - } - - public static GCMParameterSpec createIVfromValues(final byte[] iv) { - - return new GCMParameterSpec(TAG_LENGTH, iv); - } - - public static Cipher createCipher() throws NoSuchPaddingException, NoSuchAlgorithmException { - - return Cipher.getInstance(CIPHER); - } - - public static MultiBlockCipher getAESEngine() { - - return AESEngine.newInstance(); - } - -} diff --git a/src/main/java/nl/ictu/utils/AesUtility.java b/src/main/java/nl/ictu/utils/AesUtility.java new file mode 100644 index 0000000..e360f4f --- /dev/null +++ b/src/main/java/nl/ictu/utils/AesUtility.java @@ -0,0 +1,67 @@ +package nl.ictu.utils; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.GCMParameterSpec; +import lombok.experimental.UtilityClass; +import org.bouncycastle.crypto.MultiBlockCipher; +import org.bouncycastle.crypto.engines.AESEngine; + +@UtilityClass +public class AesUtility { + + public static final int IV_LENGTH = 12; + public static final int TAG_LENGTH = 128; + private static final String CIPHER = "AES/GCM/NoPadding"; + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + /** + * Generates a random Initialization Vector (IV) for use with AES-GCM encryption. + * + * @return a GCMParameterSpec containing the randomly generated IV and the specified tag length + */ + // Method to generate a random Initialization Vector (IV) + public static GCMParameterSpec generateIV() { + + byte[] iv = new byte[IV_LENGTH]; // AES block size is 16 bytes + SECURE_RANDOM.nextBytes(iv); + return new GCMParameterSpec(TAG_LENGTH, iv); + } + + /** + * Creates a {@link GCMParameterSpec} instance using the specified initialization vector (IV). + * + * @param iv the byte array representing the initialization vector; must not be null, and its + * length should match the expected IV length for AES-GCM + * @return a GCMParameterSpec initialized with the provided IV and the predefined tag length + */ + public static GCMParameterSpec createIVfromValues(final byte[] iv) { + + return new GCMParameterSpec(TAG_LENGTH, iv); + } + + /** + * Creates and initializes a {@link Cipher} instance using the AES/GCM/NoPadding + * transformation. + * + * @return a Cipher instance initialized with the AES/GCM/NoPadding transformation + * @throws NoSuchPaddingException if the requested padding scheme is not available + * @throws NoSuchAlgorithmException if the AES algorithm in GCM mode is not available + */ + public static Cipher createCipher() throws NoSuchPaddingException, NoSuchAlgorithmException { + + return Cipher.getInstance(CIPHER); + } + + /** + * Returns an instance of {@link MultiBlockCipher} configured as an AES engine. + * + * @return a new instance of a {@link MultiBlockCipher} configured to use AES encryption + */ + public static MultiBlockCipher getAESEngine() { + + return AESEngine.newInstance(); + } +} diff --git a/src/main/java/nl/ictu/utils/Base64Wrapper.java b/src/main/java/nl/ictu/utils/Base64Wrapper.java index 0907b97..3e00ebf 100644 --- a/src/main/java/nl/ictu/utils/Base64Wrapper.java +++ b/src/main/java/nl/ictu/utils/Base64Wrapper.java @@ -11,16 +11,34 @@ public final class Base64Wrapper { public static final Decoder DECODER = Base64.getDecoder(); public static final Encoder ENCODER = Base64.getEncoder(); + /** + * Decodes a Base64-encoded string into its original byte array representation. + * + * @param toDecode the Base64-encoded string to be decoded + * @return a byte array containing the decoded data + */ public byte[] decode(final String toDecode) { return DECODER.decode(toDecode); } + /** + * Encodes a byte array into its Base64-encoded byte array representation. + * + * @param toEncode the byte array to be encoded + * @return a byte array containing the Base64-encoded representation of the input byte array + */ public byte[] encode(final byte[] toEncode) { return ENCODER.encode(toEncode); } + /** + * Encodes the given byte array into a Base64-encoded string. + * + * @param toEncode the byte array to be encoded + * @return a String containing the Base64-encoded representation of the input byte array + */ public String encodeToString(final byte[] toEncode) { return ENCODER.encodeToString(toEncode); diff --git a/src/main/java/nl/ictu/utils/ByteArrayUtil.java b/src/main/java/nl/ictu/utils/ByteArrayUtil.java new file mode 100644 index 0000000..ca2b6d3 --- /dev/null +++ b/src/main/java/nl/ictu/utils/ByteArrayUtil.java @@ -0,0 +1,23 @@ +package nl.ictu.utils; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class ByteArrayUtil { + + /** + * Concatenates two byte arrays into a single byte array. + * + * @param a the first byte array + * @param b the second byte array + * @return a new byte array containing all the elements of the first array followed by all the + * elements of the second array + */ + public static byte[] concat(final byte[] a, final byte[] b) { + + byte[] c = new byte[a.length + b.length]; + System.arraycopy(a, 0, c, 0, a.length); + System.arraycopy(b, 0, c, a.length, b.length); + return c; + } +} diff --git a/src/main/java/nl/ictu/utils/ByteArrayUtils.java b/src/main/java/nl/ictu/utils/ByteArrayUtils.java deleted file mode 100644 index 88222ea..0000000 --- a/src/main/java/nl/ictu/utils/ByteArrayUtils.java +++ /dev/null @@ -1,14 +0,0 @@ -package nl.ictu.utils; - -public final class ByteArrayUtils { - - private ByteArrayUtils() { - } - - public static byte[] concat(final byte[] a, final byte[] b) { - byte[] c = new byte[a.length + b.length]; - System.arraycopy(a, 0, c, 0, a.length); - System.arraycopy(b, 0, c, a.length, b.length); - return c; - } -} diff --git a/src/main/java/nl/ictu/utils/MessageDigestUtil.java b/src/main/java/nl/ictu/utils/MessageDigestUtil.java deleted file mode 100644 index 6ea9d15..0000000 --- a/src/main/java/nl/ictu/utils/MessageDigestUtil.java +++ /dev/null @@ -1,17 +0,0 @@ -package nl.ictu.utils; - -import java.security.MessageDigest; -import lombok.SneakyThrows; -import org.springframework.stereotype.Component; - -@Component -public final class MessageDigestUtil { - - public static final String SHA_256 = "SHA-256"; - - @SneakyThrows - public MessageDigest getMessageDigestSha256() { - - return MessageDigest.getInstance(SHA_256); - } -} diff --git a/src/main/java/nl/ictu/utils/MessageDigestWrapper.java b/src/main/java/nl/ictu/utils/MessageDigestWrapper.java new file mode 100644 index 0000000..488b9e4 --- /dev/null +++ b/src/main/java/nl/ictu/utils/MessageDigestWrapper.java @@ -0,0 +1,22 @@ +package nl.ictu.utils; + +import java.security.MessageDigest; +import lombok.SneakyThrows; +import org.springframework.stereotype.Component; + +@Component +public final class MessageDigestWrapper { + + public static final String SHA_256 = "SHA-256"; + + /** + * Creates and returns a new instance of the MessageDigest configured for the SHA-256 algorithm. + * + * @return a MessageDigest instance initialized to use the SHA-256 algorithm + */ + @SneakyThrows + public MessageDigest getMessageDigestInstance() { + + return MessageDigest.getInstance(SHA_256); + } +} diff --git a/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java b/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java index 113a1b7..90eb3cf 100644 --- a/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java +++ b/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java @@ -10,7 +10,7 @@ import nl.ictu.configuration.PseudoniemenServiceProperties; import nl.ictu.service.v1.crypto.AesGcmCryptographer; import nl.ictu.utils.Base64Wrapper; -import nl.ictu.utils.MessageDigestUtil; +import nl.ictu.utils.MessageDigestWrapper; import org.junit.jupiter.api.Test; import org.springframework.test.context.ActiveProfiles; @@ -24,7 +24,7 @@ class TestAesGcmCryptographer { private final AesGcmCryptographer aesGcmCryptographer = new AesGcmCryptographer( new Base64Wrapper(), - new MessageDigestUtil(), + new MessageDigestWrapper(), new PseudoniemenServiceProperties().setTokenPrivateKey( "bFUyS1FRTVpON0pCSFFRRGdtSllSeUQ1MlRna2txVmI=") diff --git a/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java b/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java index 741f33f..5fb9bda 100644 --- a/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java +++ b/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java @@ -14,7 +14,7 @@ import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; import nl.ictu.service.v1.crypto.IdentifierConverter; import nl.ictu.utils.Base64Wrapper; -import nl.ictu.utils.MessageDigestUtil; +import nl.ictu.utils.MessageDigestWrapper; import org.junit.jupiter.api.Test; import org.springframework.test.context.ActiveProfiles; @@ -29,7 +29,7 @@ class TestAesGcmSivCryptographer { private final AesGcmSivCryptographer aesGcmSivCryptographer = new AesGcmSivCryptographer( new PseudoniemenServiceProperties().setIdentifierPrivateKey( "QTBtVEhLN3EwMHJ3QXN1ZUFqNzVrT3hDQTBIWWNIZTU="), - new MessageDigestUtil(), + new MessageDigestWrapper(), new IdentifierConverter(new ObjectMapper()), new Base64Wrapper() ); diff --git a/src/test/java/nl/ictu/service/v1/ExchangeIdentifierServiceTest.java b/src/test/java/nl/ictu/service/v1/ExchangeIdentifierServiceTest.java new file mode 100644 index 0000000..123bc44 --- /dev/null +++ b/src/test/java/nl/ictu/service/v1/ExchangeIdentifierServiceTest.java @@ -0,0 +1,99 @@ +package nl.ictu.service.v1; + +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierRequest; +import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierResponse; +import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; +import nl.ictu.service.exception.InvalidWsIdentifierRequestTypeException; +import nl.ictu.service.v1.map.BsnPseudoMapper; +import nl.ictu.service.v1.map.PseudoBsnMapper; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Unit tests for {@link ExchangeIdentifierService}. + */ +@ExtendWith(MockitoExtension.class) +class ExchangeIdentifierServiceTest { + + @Mock + private BsnPseudoMapper bsnPseudoMapper; + @Mock + private PseudoBsnMapper pseudoBsnMapper; + @InjectMocks + private ExchangeIdentifierService exchangeIdentifierService; + + @Test + @DisplayName("exchangeIdentifier() -> BSN -> ORGANISATION_PSEUDO path") + void testExchangeIdentifier_BsnToOrgPseudo() throws Exception { + // GIVEN + var request = new WsExchangeIdentifierRequest() + .identifier(new WsIdentifier().type(BSN).value("123456789")) + .recipientOIN("TEST_OIN") + .recipientIdentifierType(ORGANISATION_PSEUDO); + // We mock BsnPseudoMapper to return a WsExchangeIdentifierResponse + var mockedResponse = new WsExchangeIdentifierResponse(); + mockedResponse.setIdentifier( + new WsIdentifier().type(ORGANISATION_PSEUDO).value("encryptedValue")); + when(bsnPseudoMapper.map("123456789", "TEST_OIN")).thenReturn(mockedResponse); + // WHEN + WsExchangeIdentifierResponse actualResponse = + exchangeIdentifierService.exchangeIdentifier("CALLER_OIN", request); + // THEN + // Verify the returned response is what the mapper gave back + org.junit.jupiter.api.Assertions.assertNotNull(actualResponse); + org.junit.jupiter.api.Assertions.assertNotNull(actualResponse.getIdentifier()); + org.junit.jupiter.api.Assertions.assertEquals(ORGANISATION_PSEUDO, + actualResponse.getIdentifier().getType()); + org.junit.jupiter.api.Assertions.assertEquals("encryptedValue", + actualResponse.getIdentifier().getValue()); + } + + @Test + @DisplayName("exchangeIdentifier() -> ORGANISATION_PSEUDO -> BSN path") + void testExchangeIdentifier_OrgPseudoToBsn() throws Exception { + // GIVEN + var request = new WsExchangeIdentifierRequest() + .identifier(new WsIdentifier().type(ORGANISATION_PSEUDO).value("somePseudo")) + .recipientOIN("TEST_OIN") + .recipientIdentifierType(BSN); + // We mock PseudoBsnMapper to return a WsExchangeIdentifierResponse + var mockedResponse = new WsExchangeIdentifierResponse(); + mockedResponse.setIdentifier(new WsIdentifier().type(BSN).value("decryptedBsn")); + when(pseudoBsnMapper.map("somePseudo", "TEST_OIN")).thenReturn(mockedResponse); + // WHEN + WsExchangeIdentifierResponse actualResponse = + exchangeIdentifierService.exchangeIdentifier("CALLER_OIN", request); + // THEN + org.junit.jupiter.api.Assertions.assertNotNull(actualResponse); + org.junit.jupiter.api.Assertions.assertNotNull(actualResponse.getIdentifier()); + org.junit.jupiter.api.Assertions.assertEquals(BSN, + actualResponse.getIdentifier().getType()); + org.junit.jupiter.api.Assertions.assertEquals("decryptedBsn", + actualResponse.getIdentifier().getValue()); + } + + @Test + @DisplayName("exchangeIdentifier() -> throws InvalidWsIdentifierRequestTypeException for unsupported mappings") + void testExchangeIdentifier_UnsupportedMapping_ThrowsException() { + // GIVEN + var request = new WsExchangeIdentifierRequest() + // Let's say we do something like BSN -> BSN or ORG_PSEUDO -> ORG_PSEUDO + .identifier(new WsIdentifier().type(BSN).value("12345")) + .recipientOIN("TEST_OIN") + .recipientIdentifierType(BSN); + // WHEN & THEN + assertThrows( + InvalidWsIdentifierRequestTypeException.class, + () -> exchangeIdentifierService.exchangeIdentifier("CALLER_OIN", request) + ); + } +} diff --git a/src/test/java/nl/ictu/service/v1/map/OrganisationPseudoTokenMapperTest.java b/src/test/java/nl/ictu/service/v1/map/OrganisationPseudoTokenMapperTest.java new file mode 100644 index 0000000..ba8a20f --- /dev/null +++ b/src/test/java/nl/ictu/service/v1/map/OrganisationPseudoTokenMapperTest.java @@ -0,0 +1,74 @@ +package nl.ictu.service.v1.map; + +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import nl.ictu.model.Identifier; +import nl.ictu.model.Token; +import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenResponse; +import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Unit tests for {@link OrganisationPseudoTokenMapper}. + */ +@ExtendWith(MockitoExtension.class) +class OrganisationPseudoTokenMapperTest { + + @Mock + private AesGcmSivCryptographer aesGcmSivCryptographer; + @InjectMocks + private OrganisationPseudoTokenMapper organisationPseudoTokenMapper; + + @Test + void map_ShouldReturnEncryptedTokenResponse_WhenEncryptionSucceeds() throws Exception { + // GIVEN + String callerOIN = "TEST_OIN"; + Token token = Token.builder().bsn("123456789").build(); + String encryptedValue = "encryptedBSN"; + // We mock the cryptographer to return a known "encrypted" value + when(aesGcmSivCryptographer.encrypt(any(Identifier.class), eq(callerOIN))) + .thenReturn(encryptedValue); + // WHEN + WsExchangeTokenResponse response = organisationPseudoTokenMapper.map(callerOIN, token); + // THEN + assertEquals(ORGANISATION_PSEUDO, response.getIdentifier().getType()); + assertEquals(encryptedValue, response.getIdentifier().getValue()); + } + + @Test + void map_ShouldThrowInvalidCipherTextException_WhenEncryptionFails() throws Exception { + // GIVEN + String callerOIN = "FAILING_OIN"; + Token token = Token.builder().bsn("987654321").build(); + // We mock an InvalidCipherTextException + when(aesGcmSivCryptographer.encrypt(any(Identifier.class), eq(callerOIN))) + .thenThrow(new InvalidCipherTextException("Simulated cipher error")); + // WHEN & THEN + assertThrows(InvalidCipherTextException.class, + () -> organisationPseudoTokenMapper.map(callerOIN, token)); + } + + @Test + void map_ShouldThrowIOException_WhenEncryptionThrowsIOException() throws Exception { + // GIVEN + String callerOIN = "IO_EXCEPTION_OIN"; + Token token = Token.builder().bsn("555555555").build(); + // We mock an IOException + when(aesGcmSivCryptographer.encrypt(any(Identifier.class), eq(callerOIN))) + .thenThrow(new IOException("Simulated I/O error")); + // WHEN & THEN + assertThrows(IOException.class, + () -> organisationPseudoTokenMapper.map(callerOIN, token)); + } +} diff --git a/src/test/java/nl/ictu/service/v1/validate/OINValidatorTest.java b/src/test/java/nl/ictu/service/v1/validate/OINValidatorTest.java new file mode 100644 index 0000000..ccf1262 --- /dev/null +++ b/src/test/java/nl/ictu/service/v1/validate/OINValidatorTest.java @@ -0,0 +1,64 @@ +package nl.ictu.service.v1.validate; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import nl.ictu.model.Token; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class OINValidatorTest { + + private OINValidator oinValidator; + + @BeforeEach + void setUp() { + oinValidator = new OINValidator(); + } + + @Test + @DisplayName("isValid() returns true when callerOIN matches token's recipientOIN") + void isValid_ReturnsTrue_WhenOINsMatch() { + // GIVEN + String callerOIN = "TEST_OIN_123"; + Token token = Token.builder().recipientOIN("TEST_OIN_123").build(); + + // WHEN + boolean result = oinValidator.isValid(callerOIN, token); + + // THEN + assertTrue(result, "Expected isValid() to return true for matching OINs"); + } + + @Test + @DisplayName("isValid() returns false when callerOIN does not match token's recipientOIN") + void isValid_ReturnsFalse_WhenOINsDoNotMatch() { + // GIVEN + String callerOIN = "TEST_OIN_ABC"; + Token token = Token.builder().recipientOIN("TEST_OIN_XYZ").build(); + + // WHEN + boolean result = oinValidator.isValid(callerOIN, token); + + // THEN + assertFalse(result, "Expected isValid() to return false for non-matching OINs"); + } + + // OPTIONAL: If you want to test null behavior + @Test + @DisplayName("isValid() returns false or throws if token recipientOIN is null") + void isValid_Behavior_WhenTokenRecipientOINIsNull() { + // GIVEN + String callerOIN = "NON_NULL_OIN"; + Token token = Token.builder().build(); + + // WHEN + // This will return false if callerOIN is not null, + // or might throw NullPointerException if you rely on the equals contract + boolean result = oinValidator.isValid(callerOIN, token); + + // THEN + assertFalse(result, "Expected isValid() to return false if token's recipientOIN is null"); + } +} diff --git a/src/test/java/nl/ictu/utils/AESHelperTest.java b/src/test/java/nl/ictu/utils/AESHelperTest.java index cccf91f..ebd0201 100644 --- a/src/test/java/nl/ictu/utils/AESHelperTest.java +++ b/src/test/java/nl/ictu/utils/AESHelperTest.java @@ -18,31 +18,31 @@ class AESHelperTest { @Test void generateIV_ShouldReturnGCMParameterSpec_WithNonNullIV() { // WHEN - GCMParameterSpec gcmParameterSpec = AESHelper.generateIV(); + GCMParameterSpec gcmParameterSpec = AesUtility.generateIV(); // THEN assertNotNull(gcmParameterSpec, "GCMParameterSpec should not be null"); - assertEquals(AESHelper.TAG_LENGTH, gcmParameterSpec.getTLen(), + assertEquals(AesUtility.TAG_LENGTH, gcmParameterSpec.getTLen(), "Tag length should be 128 (bits)"); // The IV array is extracted from gcmParameterSpec byte[] iv = gcmParameterSpec.getIV(); assertNotNull(iv, "IV should not be null"); - assertEquals(AESHelper.IV_LENGTH, iv.length, - "IV length should be " + AESHelper.IV_LENGTH); + assertEquals(AesUtility.IV_LENGTH, iv.length, + "IV length should be " + AesUtility.IV_LENGTH); } @Test void createIVfromValues_ShouldReturnGCMParameterSpec_FromGivenIV() { // GIVEN - byte[] ivSource = new byte[AESHelper.IV_LENGTH]; + byte[] ivSource = new byte[AesUtility.IV_LENGTH]; // Fill the array with deterministic data for test for (int i = 0; i < ivSource.length; i++) { ivSource[i] = (byte) i; } // WHEN - GCMParameterSpec spec = AESHelper.createIVfromValues(ivSource); + GCMParameterSpec spec = AesUtility.createIVfromValues(ivSource); // THEN assertNotNull(spec, "GCMParameterSpec should not be null"); - assertEquals(AESHelper.TAG_LENGTH, spec.getTLen(), + assertEquals(AesUtility.TAG_LENGTH, spec.getTLen(), "Tag length should be 128 (bits)"); assertArrayEquals(ivSource, spec.getIV(), "IV array in GCMParameterSpec should match the input"); @@ -52,7 +52,7 @@ void createIVfromValues_ShouldReturnGCMParameterSpec_FromGivenIV() { void createCipher_ShouldReturnAesGcmNoPaddingCipher() throws NoSuchPaddingException, NoSuchAlgorithmException { // WHEN - Cipher cipher = AESHelper.createCipher(); + Cipher cipher = AesUtility.createCipher(); // THEN assertNotNull(cipher, "Cipher should not be null"); // Depending on the JVM/provider, the algorithm name can be uppercase or some variation, @@ -64,7 +64,7 @@ void createCipher_ShouldReturnAesGcmNoPaddingCipher() @Test void getAESEngine_ShouldReturnNonNullAESEngineInstance() { // WHEN - MultiBlockCipher engine = AESHelper.getAESEngine(); + MultiBlockCipher engine = AesUtility.getAESEngine(); // THEN assertNotNull(engine, "Engine should not be null"); assertInstanceOf(AESEngine.class, engine, "Engine should be an instance of AESEngine"); diff --git a/src/test/java/nl/ictu/utils/AesUtilityTest.java b/src/test/java/nl/ictu/utils/AesUtilityTest.java new file mode 100644 index 0000000..0515b68 --- /dev/null +++ b/src/test/java/nl/ictu/utils/AesUtilityTest.java @@ -0,0 +1,73 @@ +package nl.ictu.utils; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.security.NoSuchAlgorithmException; +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.GCMParameterSpec; +import org.bouncycastle.crypto.MultiBlockCipher; +import org.bouncycastle.crypto.engines.AESEngine; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class AesUtilityTest { + + @Test + @DisplayName("generateIV() -> should return GCMParameterSpec with correct IV & tag length") + void generateIV_ShouldReturnGCMParameterSpec() { + // WHEN + GCMParameterSpec spec = AesUtility.generateIV(); + // THEN + assertNotNull(spec, "GCMParameterSpec should not be null"); + assertEquals(AesUtility.TAG_LENGTH, spec.getTLen(), + "Tag length should be " + AesUtility.TAG_LENGTH + " bits"); + assertNotNull(spec.getIV(), "IV should not be null"); + assertEquals(AesUtility.IV_LENGTH, spec.getIV().length, + "IV length should be " + AesUtility.IV_LENGTH); + } + + @Test + @DisplayName("createIVfromValues() -> should build GCMParameterSpec from provided IV") + void createIVfromValues_ShouldReturnGCMParameterSpecFromGivenIV() { + // GIVEN: a deterministic IV of length 12 + byte[] ivBytes = new byte[AesUtility.IV_LENGTH]; + for (int i = 0; i < ivBytes.length; i++) { + ivBytes[i] = (byte) i; + } + // WHEN + GCMParameterSpec spec = AesUtility.createIVfromValues(ivBytes); + // THEN + assertNotNull(spec, "GCMParameterSpec should not be null"); + assertEquals(AesUtility.TAG_LENGTH, spec.getTLen(), + "Tag length should be " + AesUtility.TAG_LENGTH + " bits"); + assertArrayEquals(ivBytes, spec.getIV(), + "IV in GCMParameterSpec should match the input array"); + } + + @Test + @DisplayName("createCipher() -> should return Cipher for AES/GCM/NoPadding") + void createCipher_ShouldReturnAesGcmNoPadding() + throws NoSuchPaddingException, NoSuchAlgorithmException { + // WHEN + Cipher cipher = AesUtility.createCipher(); + // THEN + assertNotNull(cipher, "Cipher should not be null"); + assertEquals("AES/GCM/NoPadding", cipher.getAlgorithm(), + "Expected the cipher algorithm to be AES/GCM/NoPadding"); + } + + @Test + @DisplayName("getAESEngine() -> should return instance of AESEngine") + void getAESEngine_ShouldReturnAesEngine() { + // WHEN + MultiBlockCipher engine = AesUtility.getAESEngine(); + // THEN + assertNotNull(engine, "Engine should not be null"); + assertTrue(engine instanceof AESEngine, + "Engine should be an instance of AESEngine"); + } +} diff --git a/src/test/java/nl/ictu/utils/ByteArrayUtilsTest.java b/src/test/java/nl/ictu/utils/ByteArrayUtilsTest.java index cb60e52..d2ebac3 100644 --- a/src/test/java/nl/ictu/utils/ByteArrayUtilsTest.java +++ b/src/test/java/nl/ictu/utils/ByteArrayUtilsTest.java @@ -16,7 +16,7 @@ void concat_ShouldConcatenateTwoArrays() { byte[] expected = {1, 2, 3, 4, 5, 6}; // WHEN - byte[] result = ByteArrayUtils.concat(a, b); + byte[] result = ByteArrayUtil.concat(a, b); // THEN assertArrayEquals(expected, result); @@ -31,7 +31,7 @@ void concat_ShouldHandleTwoEmptyArrays() { byte[] expected = {}; // WHEN - byte[] result = ByteArrayUtils.concat(a, b); + byte[] result = ByteArrayUtil.concat(a, b); // THEN assertArrayEquals(expected, result); @@ -46,7 +46,7 @@ void concat_ShouldHandleOneEmptyArray() { byte[] expected1 = {1, 2, 3}; // WHEN - byte[] result1 = ByteArrayUtils.concat(a, b); + byte[] result1 = ByteArrayUtil.concat(a, b); // THEN assertArrayEquals(expected1, result1); @@ -57,7 +57,7 @@ void concat_ShouldHandleOneEmptyArray() { byte[] expected2 = {4, 5, 6}; // WHEN - byte[] result2 = ByteArrayUtils.concat(c, d); + byte[] result2 = ByteArrayUtil.concat(c, d); // THEN assertArrayEquals(expected2, result2); diff --git a/src/test/java/nl/ictu/utils/MessageDigestUtilTest.java b/src/test/java/nl/ictu/utils/MessageDigestWrapperTest.java similarity index 72% rename from src/test/java/nl/ictu/utils/MessageDigestUtilTest.java rename to src/test/java/nl/ictu/utils/MessageDigestWrapperTest.java index de1b27f..be7544b 100644 --- a/src/test/java/nl/ictu/utils/MessageDigestUtilTest.java +++ b/src/test/java/nl/ictu/utils/MessageDigestWrapperTest.java @@ -7,19 +7,19 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -class MessageDigestUtilTest { +class MessageDigestWrapperTest { - private MessageDigestUtil messageDigestUtil; + private MessageDigestWrapper messageDigestWrapper; @BeforeEach void setUp() { - messageDigestUtil = new MessageDigestUtil(); + messageDigestWrapper = new MessageDigestWrapper(); } @Test void getMessageDigestSha256_ShouldReturnSha256Digest() { // WHEN - MessageDigest digest = messageDigestUtil.getMessageDigestSha256(); + MessageDigest digest = messageDigestWrapper.getMessageDigestInstance(); // THEN assertNotNull(digest, "MessageDigest should not be null"); From e9e05e7460489f895551e06b8541ddae05c8bd6d Mon Sep 17 00:00:00 2001 From: sayf Date: Thu, 26 Dec 2024 01:34:06 +0100 Subject: [PATCH 25/30] 95% mutation coverage and some good old SOLID refactoring --- .../PseudoniemenServiceProperties.java | 13 ++ .../controller/GlobalExceptionHandler.java | 2 +- .../v1/ExchangeIdentifierController.java | 4 +- .../v1/ExchangeTokenController.java | 2 +- .../controller/v1/GetTokenController.java | 6 +- .../v1 => }/crypto/AesGcmCryptographer.java | 22 +++- .../crypto/AesGcmSivCryptographer.java | 3 +- .../v1 => }/crypto/IdentifierConverter.java | 2 +- .../{service/v1 => }/crypto/TokenCoder.java | 2 +- .../{v1 => }/ExchangeIdentifierService.java | 27 ++-- .../{v1 => }/ExchangeTokenService.java | 15 +-- .../java/nl/ictu/service/GetTokenService.java | 46 +++++++ .../exception/InvalidOINException.java | 2 +- .../service/{v1 => }/map/BsnPseudoMapper.java | 4 +- .../service/{v1 => }/map/BsnTokenMapper.java | 2 +- .../{v1 => }/map/EncryptedBsnMapper.java | 4 +- .../map/OrganisationPseudoTokenMapper.java | 4 +- .../service/{v1 => }/map/PseudoBsnMapper.java | 4 +- .../service/map/WsGetTokenResponseMapper.java | 55 ++++++++ .../service/map/WsIdentifierOinBsnMapper.java | 37 ++++++ .../nl/ictu/service/v1/GetTokenService.java | 85 ------------- .../{v1 => }/validate/OINValidator.java | 2 +- .../nl/ictu/TestingWebApplicationTests.java | 26 ++-- .../PseudoniemenServicePropertiesTest.java | 8 +- .../GlobalExceptionHandlerTest.java | 7 +- .../ictu/controller/stub/StubController.java | 6 +- .../nl/ictu/controller/stub/StubService.java | 2 +- .../v1/ExchangeIdentifierControllerTest.java | 58 +++++++++ .../v1/ExchangeTokenControllerTest.java | 4 +- .../ExchangeIdentifierServiceTest.java | 12 +- .../service/ExchangeTokenServiceTest.java | 118 ++++++++++++++++++ .../nl/ictu/service/GetTokenServiceTest.java | 83 ++++++++++++ .../ictu/service/TestAesGcmCryptographer.java | 12 +- .../service/TestAesGcmSivCryptographer.java | 18 +-- .../{v1 => }/map/BsnPseudoMapperTest.java | 4 +- .../{v1 => }/map/BsnTokenMapperTest.java | 2 +- .../{v1 => }/map/EncryptedBsnMapperTest.java | 4 +- .../OrganisationPseudoTokenMapperTest.java | 4 +- .../{v1 => }/map/PseudoBsnMapperTest.java | 4 +- .../map/WsGetTokenResponseMapperTest.java | 114 +++++++++++++++++ .../map/WsIdentifierOinBsnMapperTest.java | 57 +++++++++ .../{v1 => }/validate/OINValidatorTest.java | 13 +- .../java/nl/ictu/utils/AesUtilityTest.java | 5 +- .../java/nl/ictu/utils/Base64WrapperTest.java | 11 +- .../nl/ictu/utils/ByteArrayUtilsTest.java | 9 -- .../ictu/utils/MessageDigestWrapperTest.java | 2 +- 46 files changed, 699 insertions(+), 227 deletions(-) rename src/main/java/nl/ictu/{service/v1 => }/crypto/AesGcmCryptographer.java (88%) rename src/main/java/nl/ictu/{service/v1 => }/crypto/AesGcmSivCryptographer.java (97%) rename src/main/java/nl/ictu/{service/v1 => }/crypto/IdentifierConverter.java (97%) rename src/main/java/nl/ictu/{service/v1 => }/crypto/TokenCoder.java (97%) rename src/main/java/nl/ictu/service/{v1 => }/ExchangeIdentifierService.java (69%) rename src/main/java/nl/ictu/service/{v1 => }/ExchangeTokenService.java (88%) create mode 100644 src/main/java/nl/ictu/service/GetTokenService.java rename src/main/java/nl/ictu/{controller => service}/exception/InvalidOINException.java (79%) rename src/main/java/nl/ictu/service/{v1 => }/map/BsnPseudoMapper.java (95%) rename src/main/java/nl/ictu/service/{v1 => }/map/BsnTokenMapper.java (97%) rename src/main/java/nl/ictu/service/{v1 => }/map/EncryptedBsnMapper.java (89%) rename src/main/java/nl/ictu/service/{v1 => }/map/OrganisationPseudoTokenMapper.java (95%) rename src/main/java/nl/ictu/service/{v1 => }/map/PseudoBsnMapper.java (95%) create mode 100644 src/main/java/nl/ictu/service/map/WsGetTokenResponseMapper.java create mode 100644 src/main/java/nl/ictu/service/map/WsIdentifierOinBsnMapper.java delete mode 100644 src/main/java/nl/ictu/service/v1/GetTokenService.java rename src/main/java/nl/ictu/service/{v1 => }/validate/OINValidator.java (94%) create mode 100644 src/test/java/nl/ictu/controller/v1/ExchangeIdentifierControllerTest.java rename src/test/java/nl/ictu/service/{v1 => }/ExchangeIdentifierServiceTest.java (93%) create mode 100644 src/test/java/nl/ictu/service/ExchangeTokenServiceTest.java create mode 100644 src/test/java/nl/ictu/service/GetTokenServiceTest.java rename src/test/java/nl/ictu/service/{v1 => }/map/BsnPseudoMapperTest.java (96%) rename src/test/java/nl/ictu/service/{v1 => }/map/BsnTokenMapperTest.java (98%) rename src/test/java/nl/ictu/service/{v1 => }/map/EncryptedBsnMapperTest.java (93%) rename src/test/java/nl/ictu/service/{v1 => }/map/OrganisationPseudoTokenMapperTest.java (97%) rename src/test/java/nl/ictu/service/{v1 => }/map/PseudoBsnMapperTest.java (95%) create mode 100644 src/test/java/nl/ictu/service/map/WsGetTokenResponseMapperTest.java create mode 100644 src/test/java/nl/ictu/service/map/WsIdentifierOinBsnMapperTest.java rename src/test/java/nl/ictu/service/{v1 => }/validate/OINValidatorTest.java (90%) diff --git a/src/main/java/nl/ictu/configuration/PseudoniemenServiceProperties.java b/src/main/java/nl/ictu/configuration/PseudoniemenServiceProperties.java index d34e53d..142c982 100644 --- a/src/main/java/nl/ictu/configuration/PseudoniemenServiceProperties.java +++ b/src/main/java/nl/ictu/configuration/PseudoniemenServiceProperties.java @@ -19,6 +19,19 @@ public final class PseudoniemenServiceProperties { private String identifierPrivateKey; + /** + * Validates that the required private keys for the token and identifier are set. + * + * This method performs a post-construction validation of the `PseudoniemenServiceProperties` object to ensure that + * the `tokenPrivateKey` and `identifierPrivateKey` are properly configured. If either of these properties is not set + * or is empty, specific exceptions are thrown: + * + * - If `tokenPrivateKey` is null or empty, a {@link TokenPrivateKeyException} is thrown. + * - If `identifierPrivateKey` is null or empty, a {@link IdentifierPrivateKeyException} is thrown. + * + * @throws TokenPrivateKeyException if the `tokenPrivateKey` is missing or empty. + * @throws IdentifierPrivateKeyException if the `identifierPrivateKey` is missing or empty. + */ @PostConstruct public void validate() { diff --git a/src/main/java/nl/ictu/controller/GlobalExceptionHandler.java b/src/main/java/nl/ictu/controller/GlobalExceptionHandler.java index 190f519..6c3be76 100644 --- a/src/main/java/nl/ictu/controller/GlobalExceptionHandler.java +++ b/src/main/java/nl/ictu/controller/GlobalExceptionHandler.java @@ -1,7 +1,7 @@ package nl.ictu.controller; import lombok.extern.slf4j.Slf4j; -import nl.ictu.controller.exception.InvalidOINException; +import nl.ictu.service.exception.InvalidOINException; import nl.ictu.service.exception.IdentifierPrivateKeyException; import nl.ictu.service.exception.InvalidWsIdentifierRequestTypeException; import nl.ictu.service.exception.InvalidWsIdentifierTokenException; diff --git a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java index a223726..911fff2 100644 --- a/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java +++ b/src/main/java/nl/ictu/controller/v1/ExchangeIdentifierController.java @@ -5,7 +5,7 @@ import nl.ictu.pseudoniemenservice.generated.server.api.ExchangeIdentifierApi; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierRequest; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierResponse; -import nl.ictu.service.v1.ExchangeIdentifierService; +import nl.ictu.service.ExchangeIdentifierService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; @@ -30,7 +30,7 @@ public final class ExchangeIdentifierController implements ExchangeIdentifierApi public ResponseEntity exchangeIdentifier(final String callerOIN, final WsExchangeIdentifierRequest wsExchangeRequest) { - final var identifier = service.exchangeIdentifier(callerOIN, wsExchangeRequest); + final var identifier = service.exchangeIdentifier(wsExchangeRequest); return ResponseEntity.ok(identifier); } } diff --git a/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java b/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java index b49a65c..3bd8c9a 100644 --- a/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java +++ b/src/main/java/nl/ictu/controller/v1/ExchangeTokenController.java @@ -5,7 +5,7 @@ import nl.ictu.pseudoniemenservice.generated.server.api.ExchangeTokenApi; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenRequest; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenResponse; -import nl.ictu.service.v1.ExchangeTokenService; +import nl.ictu.service.ExchangeTokenService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; diff --git a/src/main/java/nl/ictu/controller/v1/GetTokenController.java b/src/main/java/nl/ictu/controller/v1/GetTokenController.java index d6ed3f7..a7a94cf 100644 --- a/src/main/java/nl/ictu/controller/v1/GetTokenController.java +++ b/src/main/java/nl/ictu/controller/v1/GetTokenController.java @@ -5,7 +5,7 @@ import nl.ictu.pseudoniemenservice.generated.server.api.GetTokenApi; import nl.ictu.pseudoniemenservice.generated.server.model.WsGetTokenRequest; import nl.ictu.pseudoniemenservice.generated.server.model.WsGetTokenResponse; -import nl.ictu.service.v1.GetTokenService; +import nl.ictu.service.GetTokenService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; @@ -31,6 +31,8 @@ public ResponseEntity getToken(final String callerOIN, final var recipientOIN = wsGetTokenRequest.getRecipientOIN(); final var identifier = wsGetTokenRequest.getIdentifier(); - return ResponseEntity.ok(getTokenService.getWsGetTokenResponse(recipientOIN, identifier)); + final var wsGetTokenResponse = getTokenService.getWsGetTokenResponse( + recipientOIN, identifier); + return ResponseEntity.ok(wsGetTokenResponse); } } diff --git a/src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographer.java b/src/main/java/nl/ictu/crypto/AesGcmCryptographer.java similarity index 88% rename from src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographer.java rename to src/main/java/nl/ictu/crypto/AesGcmCryptographer.java index ac016c1..db8fb9c 100644 --- a/src/main/java/nl/ictu/service/v1/crypto/AesGcmCryptographer.java +++ b/src/main/java/nl/ictu/crypto/AesGcmCryptographer.java @@ -1,4 +1,4 @@ -package nl.ictu.service.v1.crypto; +package nl.ictu.crypto; import static nl.ictu.utils.AesUtility.IV_LENGTH; @@ -51,8 +51,19 @@ public class AesGcmCryptographer { * @throws NoSuchPaddingException if the requested padding scheme is not available */ public String encrypt(final String plaintext, final String salt) - throws IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { + throws IllegalBlockSizeException, + BadPaddingException, + InvalidAlgorithmParameterException, + InvalidKeyException, + NoSuchAlgorithmException, + NoSuchPaddingException { + if (plaintext == null || plaintext.isEmpty()) { + throw new IllegalArgumentException("Plaintext cannot be null or empty"); + } + if (salt == null || salt.isEmpty()) { + throw new IllegalArgumentException("Salt cannot be null or empty"); + } final var cipher = AesUtility.createCipher(); final var gcmParameterSpec = AesUtility.generateIV(); final var secretKey = createSecretKey(salt); @@ -100,7 +111,12 @@ private SecretKey createSecretKey(final String salt) { * decryption */ public String decrypt(final String ciphertextWithIv, final String salt) - throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + throws NoSuchPaddingException, + NoSuchAlgorithmException, + InvalidAlgorithmParameterException, + InvalidKeyException, + IllegalBlockSizeException, + BadPaddingException { final var cipher = AesUtility.createCipher(); final var encryptedWithIV = base64Wrapper.decode(ciphertextWithIv); diff --git a/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java b/src/main/java/nl/ictu/crypto/AesGcmSivCryptographer.java similarity index 97% rename from src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java rename to src/main/java/nl/ictu/crypto/AesGcmSivCryptographer.java index e775d2e..2b7e194 100644 --- a/src/main/java/nl/ictu/service/v1/crypto/AesGcmSivCryptographer.java +++ b/src/main/java/nl/ictu/crypto/AesGcmSivCryptographer.java @@ -1,4 +1,4 @@ -package nl.ictu.service.v1.crypto; +package nl.ictu.crypto; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -83,7 +83,6 @@ public String encrypt(final Identifier identifier, final String salt) * @param ciphertextString the Base64-encoded string containing the ciphertext to be decrypted * @param salt a string used to derive the nonce and key for decryption * @return the decrypted {@code Identifier} object - * @throws InvalidCipherTextException if decryption fails or the ciphertext is invalid */ @SneakyThrows public Identifier decrypt(final String ciphertextString, final String salt) { diff --git a/src/main/java/nl/ictu/service/v1/crypto/IdentifierConverter.java b/src/main/java/nl/ictu/crypto/IdentifierConverter.java similarity index 97% rename from src/main/java/nl/ictu/service/v1/crypto/IdentifierConverter.java rename to src/main/java/nl/ictu/crypto/IdentifierConverter.java index 4a488fd..2332b61 100644 --- a/src/main/java/nl/ictu/service/v1/crypto/IdentifierConverter.java +++ b/src/main/java/nl/ictu/crypto/IdentifierConverter.java @@ -1,4 +1,4 @@ -package nl.ictu.service.v1.crypto; +package nl.ictu.crypto; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/src/main/java/nl/ictu/service/v1/crypto/TokenCoder.java b/src/main/java/nl/ictu/crypto/TokenCoder.java similarity index 97% rename from src/main/java/nl/ictu/service/v1/crypto/TokenCoder.java rename to src/main/java/nl/ictu/crypto/TokenCoder.java index 9c9c878..1be9651 100644 --- a/src/main/java/nl/ictu/service/v1/crypto/TokenCoder.java +++ b/src/main/java/nl/ictu/crypto/TokenCoder.java @@ -1,4 +1,4 @@ -package nl.ictu.service.v1.crypto; +package nl.ictu.crypto; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/src/main/java/nl/ictu/service/v1/ExchangeIdentifierService.java b/src/main/java/nl/ictu/service/ExchangeIdentifierService.java similarity index 69% rename from src/main/java/nl/ictu/service/v1/ExchangeIdentifierService.java rename to src/main/java/nl/ictu/service/ExchangeIdentifierService.java index 7be2f8c..5031a35 100644 --- a/src/main/java/nl/ictu/service/v1/ExchangeIdentifierService.java +++ b/src/main/java/nl/ictu/service/ExchangeIdentifierService.java @@ -1,4 +1,4 @@ -package nl.ictu.service.v1; +package nl.ictu.service; import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; @@ -8,8 +8,8 @@ import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierRequest; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierResponse; import nl.ictu.service.exception.InvalidWsIdentifierRequestTypeException; -import nl.ictu.service.v1.map.BsnPseudoMapper; -import nl.ictu.service.v1.map.PseudoBsnMapper; +import nl.ictu.service.map.BsnPseudoMapper; +import nl.ictu.service.map.PseudoBsnMapper; import org.springframework.stereotype.Service; @RequiredArgsConstructor @@ -19,21 +19,19 @@ public final class ExchangeIdentifierService { private final BsnPseudoMapper bsnPseudoMapper; private final PseudoBsnMapper pseudoBsnMapper; - /** * Processes the exchange of an identifier between different types based on specific mappings * and returns the corresponding response. - * - * @param callerOIN The originating identification number of the caller. - * @param wsExchangeIdentifierForIdentifierRequest The request object containing details - * of the identifier to be exchanged, - * including its value, type, recipient OIN, - * and recipient identifier type. - * @return A {@link WsExchangeIdentifierResponse} containing the exchanged identifier. - * Returns null if no appropriate mapping exists for the provided inputs. + * caller. + * @param wsExchangeIdentifierForIdentifierRequest The request object containing details of the + * identifier to be exchanged, including its + * value, type, recipient OIN, and recipient + * identifier type. + * @return A {@link WsExchangeIdentifierResponse} containing the exchanged identifier. Returns + * null if no appropriate mapping exists for the provided inputs. */ @SneakyThrows - public WsExchangeIdentifierResponse exchangeIdentifier(final String callerOIN, + public WsExchangeIdentifierResponse exchangeIdentifier( final WsExchangeIdentifierRequest wsExchangeIdentifierForIdentifierRequest) { final var wsIdentifierRequest = wsExchangeIdentifierForIdentifierRequest.getIdentifier(); @@ -46,6 +44,7 @@ public WsExchangeIdentifierResponse exchangeIdentifier(final String callerOIN, recipientIdentifierType)) { return pseudoBsnMapper.map(wsIdentifierRequest.getValue(), recipientOIN); } - throw new InvalidWsIdentifierRequestTypeException("Invalid WsIdentifierRequest type cannot be processed."); + throw new InvalidWsIdentifierRequestTypeException( + "Invalid WsIdentifierRequest type cannot be processed."); } } diff --git a/src/main/java/nl/ictu/service/v1/ExchangeTokenService.java b/src/main/java/nl/ictu/service/ExchangeTokenService.java similarity index 88% rename from src/main/java/nl/ictu/service/v1/ExchangeTokenService.java rename to src/main/java/nl/ictu/service/ExchangeTokenService.java index 1dda797..3ae27ff 100644 --- a/src/main/java/nl/ictu/service/v1/ExchangeTokenService.java +++ b/src/main/java/nl/ictu/service/ExchangeTokenService.java @@ -1,17 +1,17 @@ -package nl.ictu.service.v1; +package nl.ictu.service; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import nl.ictu.controller.exception.InvalidOINException; +import nl.ictu.service.exception.InvalidOINException; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenRequest; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenResponse; import nl.ictu.service.exception.InvalidWsIdentifierTokenException; -import nl.ictu.service.v1.crypto.AesGcmCryptographer; -import nl.ictu.service.v1.crypto.TokenCoder; -import nl.ictu.service.v1.map.BsnTokenMapper; -import nl.ictu.service.v1.map.OrganisationPseudoTokenMapper; -import nl.ictu.service.v1.validate.OINValidator; +import nl.ictu.crypto.AesGcmCryptographer; +import nl.ictu.crypto.TokenCoder; +import nl.ictu.service.map.BsnTokenMapper; +import nl.ictu.service.map.OrganisationPseudoTokenMapper; +import nl.ictu.service.validate.OINValidator; import org.springframework.web.bind.annotation.RestController; @Slf4j @@ -25,6 +25,7 @@ public final class ExchangeTokenService { private final OrganisationPseudoTokenMapper organisationPseudoTokenMapper; private final BsnTokenMapper bsnTokenMapper; + /** * Exchanges a token for an identifier based on the provided request and caller OIN. * diff --git a/src/main/java/nl/ictu/service/GetTokenService.java b/src/main/java/nl/ictu/service/GetTokenService.java new file mode 100644 index 0000000..e9a1b6d --- /dev/null +++ b/src/main/java/nl/ictu/service/GetTokenService.java @@ -0,0 +1,46 @@ +package nl.ictu.service; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import nl.ictu.pseudoniemenservice.generated.server.model.WsGetTokenResponse; +import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; +import nl.ictu.service.exception.WsGetTokenProcessingException; +import nl.ictu.service.map.WsGetTokenResponseMapper; +import nl.ictu.service.map.WsIdentifierOinBsnMapper; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public final class GetTokenService { + + private final WsIdentifierOinBsnMapper wsIdentifierOinBsnMapper; + private final WsGetTokenResponseMapper wsGetTokenResponseMapper; + + /** + * Generates an encrypted token response based on the given recipient OIN and identifier. + * Validates the identifier type and maps it to the corresponding BSN before creating the + * encrypted token. + * + * @param recipientOIN the recipient's organizational identification number + * @param identifier the identifier containing value and type information + * @return a {@link WsGetTokenResponse} containing the encrypted token, or null if the + * identifier is invalid or BSN mapping fails + */ + @SneakyThrows + public WsGetTokenResponse getWsGetTokenResponse(final String recipientOIN, + final WsIdentifier identifier) { + + final var creationDate = System.currentTimeMillis(); + // check is callerOIN allowed to communicatie with sinkOIN + try { + final var bsn = wsIdentifierOinBsnMapper.map(identifier, recipientOIN); + return wsGetTokenResponseMapper.map(bsn, creationDate, recipientOIN); + } catch (Exception ex) { + final var exceptionMessage = ex.getMessage(); + log.warn(exceptionMessage); + throw new WsGetTokenProcessingException(exceptionMessage); + } + } +} diff --git a/src/main/java/nl/ictu/controller/exception/InvalidOINException.java b/src/main/java/nl/ictu/service/exception/InvalidOINException.java similarity index 79% rename from src/main/java/nl/ictu/controller/exception/InvalidOINException.java rename to src/main/java/nl/ictu/service/exception/InvalidOINException.java index 6a168e0..a6d0132 100644 --- a/src/main/java/nl/ictu/controller/exception/InvalidOINException.java +++ b/src/main/java/nl/ictu/service/exception/InvalidOINException.java @@ -1,4 +1,4 @@ -package nl.ictu.controller.exception; +package nl.ictu.service.exception; public class InvalidOINException extends RuntimeException { diff --git a/src/main/java/nl/ictu/service/v1/map/BsnPseudoMapper.java b/src/main/java/nl/ictu/service/map/BsnPseudoMapper.java similarity index 95% rename from src/main/java/nl/ictu/service/v1/map/BsnPseudoMapper.java rename to src/main/java/nl/ictu/service/map/BsnPseudoMapper.java index 21f47a5..4968cc0 100644 --- a/src/main/java/nl/ictu/service/v1/map/BsnPseudoMapper.java +++ b/src/main/java/nl/ictu/service/map/BsnPseudoMapper.java @@ -1,4 +1,4 @@ -package nl.ictu.service.v1.map; +package nl.ictu.service.map; import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; @@ -7,7 +7,7 @@ import nl.ictu.model.Identifier; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierResponse; import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; -import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; +import nl.ictu.crypto.AesGcmSivCryptographer; import org.bouncycastle.crypto.InvalidCipherTextException; import org.springframework.stereotype.Component; diff --git a/src/main/java/nl/ictu/service/v1/map/BsnTokenMapper.java b/src/main/java/nl/ictu/service/map/BsnTokenMapper.java similarity index 97% rename from src/main/java/nl/ictu/service/v1/map/BsnTokenMapper.java rename to src/main/java/nl/ictu/service/map/BsnTokenMapper.java index 1194075..d71ddb2 100644 --- a/src/main/java/nl/ictu/service/v1/map/BsnTokenMapper.java +++ b/src/main/java/nl/ictu/service/map/BsnTokenMapper.java @@ -1,4 +1,4 @@ -package nl.ictu.service.v1.map; +package nl.ictu.service.map; import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; diff --git a/src/main/java/nl/ictu/service/v1/map/EncryptedBsnMapper.java b/src/main/java/nl/ictu/service/map/EncryptedBsnMapper.java similarity index 89% rename from src/main/java/nl/ictu/service/v1/map/EncryptedBsnMapper.java rename to src/main/java/nl/ictu/service/map/EncryptedBsnMapper.java index f337141..99f0d59 100644 --- a/src/main/java/nl/ictu/service/v1/map/EncryptedBsnMapper.java +++ b/src/main/java/nl/ictu/service/map/EncryptedBsnMapper.java @@ -1,7 +1,7 @@ -package nl.ictu.service.v1.map; +package nl.ictu.service.map; import lombok.RequiredArgsConstructor; -import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; +import nl.ictu.crypto.AesGcmSivCryptographer; import org.springframework.stereotype.Component; @Component diff --git a/src/main/java/nl/ictu/service/v1/map/OrganisationPseudoTokenMapper.java b/src/main/java/nl/ictu/service/map/OrganisationPseudoTokenMapper.java similarity index 95% rename from src/main/java/nl/ictu/service/v1/map/OrganisationPseudoTokenMapper.java rename to src/main/java/nl/ictu/service/map/OrganisationPseudoTokenMapper.java index e42cd56..c9f33c0 100644 --- a/src/main/java/nl/ictu/service/v1/map/OrganisationPseudoTokenMapper.java +++ b/src/main/java/nl/ictu/service/map/OrganisationPseudoTokenMapper.java @@ -1,4 +1,4 @@ -package nl.ictu.service.v1.map; +package nl.ictu.service.map; import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; @@ -8,7 +8,7 @@ import nl.ictu.model.Token; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenResponse; import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; -import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; +import nl.ictu.crypto.AesGcmSivCryptographer; import org.bouncycastle.crypto.InvalidCipherTextException; import org.springframework.stereotype.Component; diff --git a/src/main/java/nl/ictu/service/v1/map/PseudoBsnMapper.java b/src/main/java/nl/ictu/service/map/PseudoBsnMapper.java similarity index 95% rename from src/main/java/nl/ictu/service/v1/map/PseudoBsnMapper.java rename to src/main/java/nl/ictu/service/map/PseudoBsnMapper.java index e6d5604..e1a1f68 100644 --- a/src/main/java/nl/ictu/service/v1/map/PseudoBsnMapper.java +++ b/src/main/java/nl/ictu/service/map/PseudoBsnMapper.java @@ -1,4 +1,4 @@ -package nl.ictu.service.v1.map; +package nl.ictu.service.map; import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; @@ -7,7 +7,7 @@ import lombok.RequiredArgsConstructor; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierResponse; import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; -import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; +import nl.ictu.crypto.AesGcmSivCryptographer; import org.bouncycastle.crypto.InvalidCipherTextException; import org.springframework.stereotype.Component; diff --git a/src/main/java/nl/ictu/service/map/WsGetTokenResponseMapper.java b/src/main/java/nl/ictu/service/map/WsGetTokenResponseMapper.java new file mode 100644 index 0000000..12b983e --- /dev/null +++ b/src/main/java/nl/ictu/service/map/WsGetTokenResponseMapper.java @@ -0,0 +1,55 @@ +package nl.ictu.service.map; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import lombok.RequiredArgsConstructor; +import nl.ictu.crypto.AesGcmCryptographer; +import nl.ictu.crypto.TokenCoder; +import nl.ictu.model.Token; +import nl.ictu.pseudoniemenservice.generated.server.model.WsGetTokenResponse; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class WsGetTokenResponseMapper { + + public static final String V_1 = "v1"; + private final AesGcmCryptographer aesGcmCryptographer; + private final TokenCoder tokenCoder; + + /** + * Maps input parameters to a WsGetTokenResponse object. This involves creating a Token object + * with the given input parameters, encoding it, encrypting the encoded result, and incorporating + * it into a WsGetTokenResponse object. + * + * @param bsn the BSN value to be included in the Token object + * @param creationDate the creation date to be included in the Token object + * @param recipientOIN the recipient OIN value used in the Token object and for encryption + * @return a WsGetTokenResponse object containing the encrypted token + * @throws IOException if an I/O error occurs during the encoding process + * @throws IllegalBlockSizeException if the block size is invalid during the encryption process + * @throws BadPaddingException if an error occurs with padding during encryption + * @throws InvalidAlgorithmParameterException if algorithm parameters are invalid + * @throws InvalidKeyException if the encryption key is invalid + * @throws NoSuchAlgorithmException if the encryption algorithm is not available + * @throws NoSuchPaddingException if the padding scheme is not available + */ + public WsGetTokenResponse map(final String bsn, final long creationDate, + final String recipientOIN) + throws IOException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { + + return WsGetTokenResponse.builder() + .token(aesGcmCryptographer.encrypt(tokenCoder.encode(Token.builder() + .version(V_1) + .bsn(bsn) + .creationDate(creationDate) + .recipientOIN(recipientOIN) + .build()), recipientOIN)) + .build(); + } +} diff --git a/src/main/java/nl/ictu/service/map/WsIdentifierOinBsnMapper.java b/src/main/java/nl/ictu/service/map/WsIdentifierOinBsnMapper.java new file mode 100644 index 0000000..3a1bf67 --- /dev/null +++ b/src/main/java/nl/ictu/service/map/WsIdentifierOinBsnMapper.java @@ -0,0 +1,37 @@ +package nl.ictu.service.map; + +import lombok.RequiredArgsConstructor; +import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class WsIdentifierOinBsnMapper { + + private final EncryptedBsnMapper encryptedBsnMapper; + + /** + * Maps a given {@link WsIdentifier} object to the corresponding representation based on its type. + * If the type is BSN, it returns the value as-is. If the type is ORGANISATION_PSEUDO, it applies + * a mapping operation using an encrypted BSN mapper. + * + * @param identifier the {@link WsIdentifier} to be mapped, containing the identifier's value and type + * @param recipientOIN the recipient organization identification number (OIN) used for mapping in case of ORGANISATION_PSEUDO type + * @return the mapped value of the identifier based on its type + * @throws IllegalArgumentException if the identifier type is unsupported + */ + public String map(final WsIdentifier identifier, final String recipientOIN) { + + final String bsnValue = identifier.getValue(); + switch (identifier.getType()) { + case BSN -> { + return bsnValue; + } + case ORGANISATION_PSEUDO -> { + return encryptedBsnMapper.map(bsnValue, recipientOIN); + } + default -> throw new IllegalArgumentException( + "Unsupported identifier type: " + identifier.getType()); + } + } +} diff --git a/src/main/java/nl/ictu/service/v1/GetTokenService.java b/src/main/java/nl/ictu/service/v1/GetTokenService.java deleted file mode 100644 index 851f14a..0000000 --- a/src/main/java/nl/ictu/service/v1/GetTokenService.java +++ /dev/null @@ -1,85 +0,0 @@ -package nl.ictu.service.v1; - -import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; -import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; - -import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import nl.ictu.model.Token; -import nl.ictu.pseudoniemenservice.generated.server.model.WsGetTokenResponse; -import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; -import nl.ictu.service.exception.WsGetTokenProcessingException; -import nl.ictu.service.v1.crypto.AesGcmCryptographer; -import nl.ictu.service.v1.crypto.TokenCoder; -import nl.ictu.service.v1.map.EncryptedBsnMapper; -import org.springframework.stereotype.Service; - -@Slf4j -@Service -@RequiredArgsConstructor -public final class GetTokenService { - - public static final String V_1 = "v1"; - private final AesGcmCryptographer aesGcmCryptographer; - private final TokenCoder tokenCoder; - private final EncryptedBsnMapper encryptedBsnMapper; - - /** - * Generates an encrypted token response based on the given recipient OIN and identifier. Validates the identifier type and maps it to the - * corresponding BSN before creating the encrypted token. - * - * @param recipientOIN the recipient's organizational identification number - * @param identifier the identifier containing value and type information - * @return a {@link WsGetTokenResponse} containing the encrypted token, or null if the identifier is invalid or BSN mapping fails - */ - @SneakyThrows - public WsGetTokenResponse getWsGetTokenResponse(final String recipientOIN, final WsIdentifier identifier) { - - final var creationDate = System.currentTimeMillis(); - - // check is callerOIN allowed to communicatie with sinkOIN - try { - final String bsn = mapBsn(identifier, recipientOIN); - return createEncryptedToken(bsn, creationDate, recipientOIN); - } catch (Exception ex) { - final var exceptionMessage = ex.getMessage(); - log.warn(exceptionMessage); - throw new WsGetTokenProcessingException(exceptionMessage); - } - } - - private String mapBsn(final WsIdentifier identifier, final String recipientOIN) { - - final String bsnValue = identifier.getValue(); - if (BSN.equals(identifier.getType())) { - return bsnValue; - } else if (ORGANISATION_PSEUDO.equals(identifier.getType())) { - return encryptedBsnMapper.map(bsnValue, recipientOIN); - } - return null; - } - - private WsGetTokenResponse createEncryptedToken(final String bsn, final long creationDate, - final String recipientOIN) - throws IOException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { - - final var plainTextToken = tokenCoder.encode(Token.builder() - .version(V_1) - .bsn(bsn) - .creationDate(creationDate) - .recipientOIN(recipientOIN) - .build()); - final var encryptedToken = aesGcmCryptographer.encrypt(plainTextToken, recipientOIN); - return WsGetTokenResponse.builder() - .token(encryptedToken) - .build(); - } -} diff --git a/src/main/java/nl/ictu/service/v1/validate/OINValidator.java b/src/main/java/nl/ictu/service/validate/OINValidator.java similarity index 94% rename from src/main/java/nl/ictu/service/v1/validate/OINValidator.java rename to src/main/java/nl/ictu/service/validate/OINValidator.java index 5e46289..ba37080 100644 --- a/src/main/java/nl/ictu/service/v1/validate/OINValidator.java +++ b/src/main/java/nl/ictu/service/validate/OINValidator.java @@ -1,4 +1,4 @@ -package nl.ictu.service.v1.validate; +package nl.ictu.service.validate; import lombok.RequiredArgsConstructor; import nl.ictu.model.Token; diff --git a/src/test/java/nl/ictu/TestingWebApplicationTests.java b/src/test/java/nl/ictu/TestingWebApplicationTests.java index 9f2afd9..5091f2d 100644 --- a/src/test/java/nl/ictu/TestingWebApplicationTests.java +++ b/src/test/java/nl/ictu/TestingWebApplicationTests.java @@ -27,7 +27,6 @@ class TestingWebApplicationTests { @Autowired private Environment environment; - @Autowired private TestRestTemplate restTemplate; @@ -35,19 +34,22 @@ class TestingWebApplicationTests { void testActuatorHealthEndpoint() { final int actuatorPort = environment.getProperty("local.management.port", Integer.class); - assertThat(restTemplate.getForObject("http://localhost:" + actuatorPort + "/actuator/health", String.class) + assertThat( + restTemplate.getForObject("http://localhost:" + actuatorPort + "/actuator/health", + String.class) ).contains("{\"status\":\"UP\"}"); } @Test void testGetAtokenExchangeForBSN() { - // get a token - - final var getTokenBody = Map.of("recipientOIN", "54321543215432154321", "identifier", Map.of("type", "BSN", "value", "012345679")); + final var getTokenBody = Map.of("recipientOIN", "54321543215432154321", "identifier", + Map.of("type", "BSN", "value", "012345679")); final var httpEntityGetToken = new HttpEntity<>(getTokenBody, - new HttpHeaders(CollectionUtils.toMultiValueMap(of("callerOIN", List.of("0912345012345012345012345"))))); - final var tokenExchange = restTemplate.exchange("/v1/getToken", HttpMethod.POST, httpEntityGetToken, Map.class); + new HttpHeaders(CollectionUtils.toMultiValueMap( + of("callerOIN", List.of("0912345012345012345012345"))))); + final var tokenExchange = restTemplate.exchange("/v1/getToken", HttpMethod.POST, + httpEntityGetToken, Map.class); assertThat(tokenExchange.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(tokenExchange) .extracting("body") @@ -56,16 +58,16 @@ void testGetAtokenExchangeForBSN() { // change token for identifier final var token = (String) tokenExchange.getBody().get("token"); final var exchangeTokenBody = Map.of("token", token, "identifierType", "BSN"); - final var httpEntityExchangeToken = new HttpEntity(exchangeTokenBody, - new HttpHeaders(CollectionUtils.toMultiValueMap(of("callerOIN", List.of("54321543215432154321"))))); - final var identifierExchange = restTemplate.exchange("/v1/exchangeToken", HttpMethod.POST, httpEntityExchangeToken, + final var httpEntityExchangeToken = new HttpEntity<>(exchangeTokenBody, + new HttpHeaders(CollectionUtils.toMultiValueMap( + of("callerOIN", List.of("54321543215432154321"))))); + final var identifierExchange = restTemplate.exchange("/v1/exchangeToken", HttpMethod.POST, + httpEntityExchangeToken, Map.class); assertThat(identifierExchange.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(identifierExchange) .extracting("body") .asInstanceOf(InstanceOfAssertFactories.map(String.class, Map.class)) .containsExactly(entry("identifier", Map.of("type", "BSN", "value", "012345679"))); - } - } \ No newline at end of file diff --git a/src/test/java/nl/ictu/configuration/PseudoniemenServicePropertiesTest.java b/src/test/java/nl/ictu/configuration/PseudoniemenServicePropertiesTest.java index 926a384..775a346 100644 --- a/src/test/java/nl/ictu/configuration/PseudoniemenServicePropertiesTest.java +++ b/src/test/java/nl/ictu/configuration/PseudoniemenServicePropertiesTest.java @@ -1,11 +1,12 @@ package nl.ictu.configuration; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + import nl.ictu.service.exception.IdentifierPrivateKeyException; import nl.ictu.service.exception.TokenPrivateKeyException; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - class PseudoniemenServicePropertiesTest { @Test @@ -14,7 +15,6 @@ void validate_WhenTokenPrivateKeyIsEmpty_ThrowsTokenPrivateKeyException() { PseudoniemenServiceProperties props = new PseudoniemenServiceProperties() .setTokenPrivateKey("") .setIdentifierPrivateKey("someIdentifierKey"); - // WHEN & THEN assertThrows(TokenPrivateKeyException.class, props::validate); } @@ -25,7 +25,6 @@ void validate_WhenIdentifierPrivateKeyIsEmpty_ThrowsIdentifierPrivateKeyExceptio PseudoniemenServiceProperties props = new PseudoniemenServiceProperties() .setTokenPrivateKey("someTokenKey") .setIdentifierPrivateKey(""); - // WHEN & THEN assertThrows(IdentifierPrivateKeyException.class, props::validate); } @@ -36,7 +35,6 @@ void validate_WhenBothKeysAreSet_NoExceptionIsThrown() { PseudoniemenServiceProperties props = new PseudoniemenServiceProperties() .setTokenPrivateKey("someTokenKey") .setIdentifierPrivateKey("someIdentifierKey"); - // WHEN & THEN assertDoesNotThrow(props::validate); } diff --git a/src/test/java/nl/ictu/controller/GlobalExceptionHandlerTest.java b/src/test/java/nl/ictu/controller/GlobalExceptionHandlerTest.java index 0086e61..10dd150 100644 --- a/src/test/java/nl/ictu/controller/GlobalExceptionHandlerTest.java +++ b/src/test/java/nl/ictu/controller/GlobalExceptionHandlerTest.java @@ -6,10 +6,10 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.util.List; -import nl.ictu.controller.exception.InvalidOINException; import nl.ictu.controller.stub.StubController; import nl.ictu.controller.stub.StubService; import nl.ictu.service.exception.IdentifierPrivateKeyException; +import nl.ictu.service.exception.InvalidOINException; import nl.ictu.service.exception.InvalidWsIdentifierRequestTypeException; import nl.ictu.service.exception.InvalidWsIdentifierTokenException; import nl.ictu.service.exception.TokenPrivateKeyException; @@ -47,7 +47,6 @@ void exchangeToken_ShouldReturnUnprocessableEntity() { // GIVEN: a stubbed controller and service // WHEN: the service throws an exception final var exceptions = List.of( - new IdentifierPrivateKeyException(SERVICE_ERROR_MESSAGE), new InvalidWsIdentifierRequestTypeException(SERVICE_ERROR_MESSAGE), new InvalidWsIdentifierTokenException(SERVICE_ERROR_MESSAGE), @@ -60,8 +59,8 @@ void exchangeToken_ShouldReturnUnprocessableEntity() { /** * Tests the behavior of exception handling by simulating a scenario where the stub service - * throws the given RuntimeException. This test verifies that when an exception is thrown - * by the service, the system responds with the appropriate HTTP status code. + * throws the given RuntimeException. This test verifies that when an exception is thrown by the + * service, the system responds with the appropriate HTTP status code. * * @param ex the RuntimeException to be thrown by the stub service during the test */ diff --git a/src/test/java/nl/ictu/controller/stub/StubController.java b/src/test/java/nl/ictu/controller/stub/StubController.java index 3499e22..ed61620 100644 --- a/src/test/java/nl/ictu/controller/stub/StubController.java +++ b/src/test/java/nl/ictu/controller/stub/StubController.java @@ -7,9 +7,9 @@ import org.springframework.web.bind.annotation.RestController; /** - * This controller contains endpoints for interacting with stubbed services. - * It demonstrates a simple REST controller setup with a GET endpoint.This stubbed controller is - * used to test behavior exception handling in congestion with the GlobalExceptionHandler class + * This controller contains endpoints for interacting with stubbed services. It demonstrates a + * simple REST controller setup with a GET endpoint.This stubbed controller is used to test behavior + * exception handling in congestion with the GlobalExceptionHandler class */ @RestController public class StubController { diff --git a/src/test/java/nl/ictu/controller/stub/StubService.java b/src/test/java/nl/ictu/controller/stub/StubService.java index 4dccfa5..c3db35f 100644 --- a/src/test/java/nl/ictu/controller/stub/StubService.java +++ b/src/test/java/nl/ictu/controller/stub/StubService.java @@ -5,7 +5,7 @@ @Service public class StubService { - public void throwAStubbedException(){ + public void throwAStubbedException() { // mockito will be used to throw a mocked exception from this stubb } } diff --git a/src/test/java/nl/ictu/controller/v1/ExchangeIdentifierControllerTest.java b/src/test/java/nl/ictu/controller/v1/ExchangeIdentifierControllerTest.java new file mode 100644 index 0000000..99752b1 --- /dev/null +++ b/src/test/java/nl/ictu/controller/v1/ExchangeIdentifierControllerTest.java @@ -0,0 +1,58 @@ +package nl.ictu.controller.v1; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierRequest; +import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierResponse; +import nl.ictu.service.ExchangeIdentifierService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ResponseEntity; + +@ExtendWith(MockitoExtension.class) +class ExchangeIdentifierControllerTest { + + @Mock + private ExchangeIdentifierService service; + @InjectMocks + private ExchangeIdentifierController controller; + + @Test + @DisplayName("exchangeIdentifier() -> Returns 200 OK with response on success") + void testExchangeIdentifier_Success() { + // GIVEN + String callerOIN = "123456789"; + WsExchangeIdentifierRequest request = new WsExchangeIdentifierRequest(); + WsExchangeIdentifierResponse expectedResponse = new WsExchangeIdentifierResponse(); + when(service.exchangeIdentifier(request)).thenReturn(expectedResponse); + // WHEN + ResponseEntity response = + controller.exchangeIdentifier(callerOIN, request); + // THEN + assertEquals(ResponseEntity.ok(expectedResponse), response); + verify(service).exchangeIdentifier(request); // Ensure service method is called + } + + @Test + @DisplayName("exchangeIdentifier() -> Throws exception when service fails") + void testExchangeIdentifier_ServiceThrowsException() { + // GIVEN + String callerOIN = "123456789"; + WsExchangeIdentifierRequest request = new WsExchangeIdentifierRequest(); + RuntimeException exception = new RuntimeException("Service error"); + when(service.exchangeIdentifier(request)).thenThrow(exception); + // WHEN & THEN + RuntimeException thrownException = + assertThrows(RuntimeException.class, + () -> controller.exchangeIdentifier(callerOIN, request)); + assertEquals("Service error", thrownException.getMessage()); + verify(service).exchangeIdentifier(request); // Ensure service method is called + } +} diff --git a/src/test/java/nl/ictu/controller/v1/ExchangeTokenControllerTest.java b/src/test/java/nl/ictu/controller/v1/ExchangeTokenControllerTest.java index f3bee53..d03fb08 100644 --- a/src/test/java/nl/ictu/controller/v1/ExchangeTokenControllerTest.java +++ b/src/test/java/nl/ictu/controller/v1/ExchangeTokenControllerTest.java @@ -10,12 +10,12 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; -import nl.ictu.controller.exception.InvalidOINException; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenRequest; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenResponse; import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes; -import nl.ictu.service.v1.ExchangeTokenService; +import nl.ictu.service.ExchangeTokenService; +import nl.ictu.service.exception.InvalidOINException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/test/java/nl/ictu/service/v1/ExchangeIdentifierServiceTest.java b/src/test/java/nl/ictu/service/ExchangeIdentifierServiceTest.java similarity index 93% rename from src/test/java/nl/ictu/service/v1/ExchangeIdentifierServiceTest.java rename to src/test/java/nl/ictu/service/ExchangeIdentifierServiceTest.java index 123bc44..0efae9c 100644 --- a/src/test/java/nl/ictu/service/v1/ExchangeIdentifierServiceTest.java +++ b/src/test/java/nl/ictu/service/ExchangeIdentifierServiceTest.java @@ -1,4 +1,4 @@ -package nl.ictu.service.v1; +package nl.ictu.service; import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; @@ -9,8 +9,8 @@ import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierResponse; import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; import nl.ictu.service.exception.InvalidWsIdentifierRequestTypeException; -import nl.ictu.service.v1.map.BsnPseudoMapper; -import nl.ictu.service.v1.map.PseudoBsnMapper; +import nl.ictu.service.map.BsnPseudoMapper; +import nl.ictu.service.map.PseudoBsnMapper; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -46,7 +46,7 @@ void testExchangeIdentifier_BsnToOrgPseudo() throws Exception { when(bsnPseudoMapper.map("123456789", "TEST_OIN")).thenReturn(mockedResponse); // WHEN WsExchangeIdentifierResponse actualResponse = - exchangeIdentifierService.exchangeIdentifier("CALLER_OIN", request); + exchangeIdentifierService.exchangeIdentifier(request); // THEN // Verify the returned response is what the mapper gave back org.junit.jupiter.api.Assertions.assertNotNull(actualResponse); @@ -71,7 +71,7 @@ void testExchangeIdentifier_OrgPseudoToBsn() throws Exception { when(pseudoBsnMapper.map("somePseudo", "TEST_OIN")).thenReturn(mockedResponse); // WHEN WsExchangeIdentifierResponse actualResponse = - exchangeIdentifierService.exchangeIdentifier("CALLER_OIN", request); + exchangeIdentifierService.exchangeIdentifier(request); // THEN org.junit.jupiter.api.Assertions.assertNotNull(actualResponse); org.junit.jupiter.api.Assertions.assertNotNull(actualResponse.getIdentifier()); @@ -93,7 +93,7 @@ void testExchangeIdentifier_UnsupportedMapping_ThrowsException() { // WHEN & THEN assertThrows( InvalidWsIdentifierRequestTypeException.class, - () -> exchangeIdentifierService.exchangeIdentifier("CALLER_OIN", request) + () -> exchangeIdentifierService.exchangeIdentifier(request) ); } } diff --git a/src/test/java/nl/ictu/service/ExchangeTokenServiceTest.java b/src/test/java/nl/ictu/service/ExchangeTokenServiceTest.java new file mode 100644 index 0000000..33b2591 --- /dev/null +++ b/src/test/java/nl/ictu/service/ExchangeTokenServiceTest.java @@ -0,0 +1,118 @@ +package nl.ictu.service; + +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import nl.ictu.crypto.AesGcmCryptographer; +import nl.ictu.crypto.TokenCoder; +import nl.ictu.model.Token; +import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenRequest; +import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenResponse; +import nl.ictu.service.exception.InvalidOINException; +import nl.ictu.service.map.BsnTokenMapper; +import nl.ictu.service.map.OrganisationPseudoTokenMapper; +import nl.ictu.service.validate.OINValidator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ExchangeTokenServiceTest { + + private final String callerOIN = "123456789"; + private final String encryptedToken = "encryptedTokenValue"; + private final String decodedToken = "decodedTokenValue"; + @Mock + private AesGcmCryptographer aesGcmCryptographer; + @Mock + private TokenCoder tokenCoder; + @Mock + private OINValidator oinValidator; + @Mock + private OrganisationPseudoTokenMapper organisationPseudoTokenMapper; + @Mock + private BsnTokenMapper bsnTokenMapper; + @InjectMocks + private ExchangeTokenService exchangeTokenService; + private Token mockToken; + + @BeforeEach + void setUp() { + // Setup mock token object + mockToken = Token.builder().build(); + mockToken.setRecipientOIN(callerOIN); + mockToken.setBsn("987654321"); + } + + @Test + @DisplayName("exchangeToken() -> BSN identifier") + void testExchangeToken_BsnIdentifier() throws Exception { + // GIVEN + var request = new WsExchangeTokenRequest().token(encryptedToken).identifierType(BSN); + // Stubbing dependencies + when(aesGcmCryptographer.decrypt(encryptedToken, callerOIN)).thenReturn(decodedToken); + when(tokenCoder.decode(decodedToken)).thenReturn(mockToken); + when(oinValidator.isValid(callerOIN, mockToken)).thenReturn(true); + var expectedResponse = new WsExchangeTokenResponse(); + when(bsnTokenMapper.map(mockToken)).thenReturn(expectedResponse); + // WHEN + WsExchangeTokenResponse actualResponse = + exchangeTokenService.exchangeToken(callerOIN, request); + // THEN + verify(aesGcmCryptographer).decrypt(encryptedToken, callerOIN); + verify(tokenCoder).decode(decodedToken); + verify(oinValidator).isValid(callerOIN, mockToken); + verify(bsnTokenMapper).map(mockToken); + org.junit.jupiter.api.Assertions.assertEquals(expectedResponse, actualResponse); + } + + @Test + @DisplayName("exchangeToken() -> ORGANISATION_PSEUDO identifier") + void testExchangeToken_OrganisationPseudoIdentifier() throws Exception { + // GIVEN + var request = + new WsExchangeTokenRequest().token(encryptedToken) + .identifierType(ORGANISATION_PSEUDO); + // Stubbing dependencies + when(aesGcmCryptographer.decrypt(encryptedToken, callerOIN)).thenReturn(decodedToken); + when(tokenCoder.decode(decodedToken)).thenReturn(mockToken); + when(oinValidator.isValid(callerOIN, mockToken)).thenReturn(true); + var expectedResponse = new WsExchangeTokenResponse(); + when(organisationPseudoTokenMapper.map(callerOIN, mockToken)).thenReturn(expectedResponse); + // WHEN + WsExchangeTokenResponse actualResponse = + exchangeTokenService.exchangeToken(callerOIN, request); + // THEN + verify(aesGcmCryptographer).decrypt(encryptedToken, callerOIN); + verify(tokenCoder).decode(decodedToken); + verify(oinValidator).isValid(callerOIN, mockToken); + verify(organisationPseudoTokenMapper).map(callerOIN, mockToken); + org.junit.jupiter.api.Assertions.assertEquals(expectedResponse, actualResponse); + } + + @Test + @DisplayName("exchangeToken() -> Invalid OIN") + void testExchangeToken_InvalidOIN() throws Exception { + // GIVEN + var request = new WsExchangeTokenRequest().token(encryptedToken).identifierType(BSN); + // Stubbing dependencies + when(aesGcmCryptographer.decrypt(encryptedToken, callerOIN)).thenReturn(decodedToken); + when(tokenCoder.decode(decodedToken)).thenReturn(mockToken); + when(oinValidator.isValid(callerOIN, mockToken)).thenReturn(false); // Invalid OIN + // WHEN & THEN + assertThrows(InvalidOINException.class, + () -> exchangeTokenService.exchangeToken(callerOIN, request)); + verify(aesGcmCryptographer).decrypt(encryptedToken, callerOIN); + verify(tokenCoder).decode(decodedToken); + verify(oinValidator).isValid(callerOIN, mockToken); + verifyNoInteractions(bsnTokenMapper, organisationPseudoTokenMapper); + } +} diff --git a/src/test/java/nl/ictu/service/GetTokenServiceTest.java b/src/test/java/nl/ictu/service/GetTokenServiceTest.java new file mode 100644 index 0000000..a664a27 --- /dev/null +++ b/src/test/java/nl/ictu/service/GetTokenServiceTest.java @@ -0,0 +1,83 @@ +package nl.ictu.service; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import nl.ictu.pseudoniemenservice.generated.server.model.WsGetTokenResponse; +import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; +import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes; +import nl.ictu.service.exception.WsGetTokenProcessingException; +import nl.ictu.service.map.WsGetTokenResponseMapper; +import nl.ictu.service.map.WsIdentifierOinBsnMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class GetTokenServiceTest { + + private final String recipientOIN = "123456789"; + private final String bsn = "987654321"; + @Mock + private WsIdentifierOinBsnMapper wsIdentifierOinBsnMapper; + @Mock + private WsGetTokenResponseMapper wsGetTokenResponseMapper; + @InjectMocks + private GetTokenService getTokenService; + + @BeforeEach + void setUp() { + // This section can be used to initialize common test data if needed + } + + @Test + @DisplayName("getWsGetTokenResponse() -> Valid input") + void testGetWsGetTokenResponse_ValidInput() throws Exception { + // GIVEN + var identifier = WsIdentifier.builder() + .type(WsIdentifierTypes.BSN) + .value(bsn) + .build(); + var expectedResponse = new WsGetTokenResponse(); + // Stubbing dependencies + when(wsIdentifierOinBsnMapper.map(identifier, recipientOIN)).thenReturn(bsn); + when(wsGetTokenResponseMapper.map(eq(bsn), anyLong(), eq(recipientOIN))).thenReturn( + expectedResponse); + // WHEN + WsGetTokenResponse actualResponse = + getTokenService.getWsGetTokenResponse(recipientOIN, identifier); + // THEN + verify(wsIdentifierOinBsnMapper).map(identifier, recipientOIN); + verify(wsGetTokenResponseMapper).map(eq(bsn), anyLong(), eq(recipientOIN)); + org.junit.jupiter.api.Assertions.assertEquals(expectedResponse, actualResponse); + } + + @Test + @DisplayName("getWsGetTokenResponse() -> Unexpected error during processing") + void testGetWsGetTokenResponse_UnexpectedError() { + // GIVEN + var identifier = WsIdentifier.builder() + .type(WsIdentifierTypes.BSN) + .value(bsn) + .build(); + var exceptionMessage = "Unexpected processing error"; + // Stubbing dependencies + when(wsIdentifierOinBsnMapper.map(identifier, recipientOIN)).thenThrow( + new RuntimeException(exceptionMessage)); + // WHEN & THEN + Exception exception = assertThrows(WsGetTokenProcessingException.class, + () -> getTokenService.getWsGetTokenResponse(recipientOIN, identifier)); + // Assert exception message + org.junit.jupiter.api.Assertions.assertEquals(exceptionMessage, exception.getMessage()); + verify(wsIdentifierOinBsnMapper).map(identifier, recipientOIN); + verifyNoInteractions(wsGetTokenResponseMapper); + } +} diff --git a/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java b/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java index 90eb3cf..5f99068 100644 --- a/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java +++ b/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java @@ -1,6 +1,5 @@ package nl.ictu.service; - import static org.assertj.core.api.Assertions.assertThat; import java.util.Arrays; @@ -8,7 +7,7 @@ import java.util.Set; import lombok.extern.slf4j.Slf4j; import nl.ictu.configuration.PseudoniemenServiceProperties; -import nl.ictu.service.v1.crypto.AesGcmCryptographer; +import nl.ictu.crypto.AesGcmCryptographer; import nl.ictu.utils.Base64Wrapper; import nl.ictu.utils.MessageDigestWrapper; import org.junit.jupiter.api.Test; @@ -17,7 +16,6 @@ /** * Class for tesing {@link AesGcmCryptographer} */ - @Slf4j @ActiveProfiles("test") class TestAesGcmCryptographer { @@ -27,7 +25,6 @@ class TestAesGcmCryptographer { new MessageDigestWrapper(), new PseudoniemenServiceProperties().setTokenPrivateKey( "bFUyS1FRTVpON0pCSFFRRGdtSllSeUQ1MlRna2txVmI=") - ); private final Set testStrings = new HashSet<>( Arrays.asList("a", "bb", "dsv", "ghad", "dhaht", "uDg5Av", "d93fdvv", "dj83hzHo", @@ -37,7 +34,6 @@ class TestAesGcmCryptographer { void testEncyptDecryptForDifferentStringLengths() { testStrings.forEach(plain -> { - try { final String crypted = aesGcmCryptographer.encrypt(plain, "helloHowAreyo12345678"); final String actual = aesGcmCryptographer.decrypt(crypted, "helloHowAreyo12345678"); @@ -46,22 +42,16 @@ void testEncyptDecryptForDifferentStringLengths() { throw new RuntimeException(e); } }); - } - // Test to ensure ciphertext is different for the same plaintext due to IV randomness @Test void testCiphertextIsDifferentForSamePlaintext() throws Exception { - // The same plaintext message String plaintext = "This is a test message to ensure ciphertext is different!"; - String encryptedMessage1 = aesGcmCryptographer.encrypt(plaintext, "aniceSaltGorYu"); String encryptedMessage2 = aesGcmCryptographer.encrypt(plaintext, "aniceSaltGorYu"); - // Assert that the two ciphertexts are different assertThat(encryptedMessage1).isNotEqualTo(encryptedMessage2); } - } diff --git a/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java b/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java index 5fb9bda..b9b41a3 100644 --- a/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java +++ b/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java @@ -1,6 +1,5 @@ package nl.ictu.service; - import static org.assertj.core.api.Assertions.assertThat; import com.fasterxml.jackson.databind.ObjectMapper; @@ -8,11 +7,11 @@ import java.util.HashSet; import java.util.Set; import lombok.extern.slf4j.Slf4j; -import nl.ictu.model.Identifier; import nl.ictu.configuration.PseudoniemenServiceProperties; -import nl.ictu.service.v1.crypto.AesGcmCryptographer; -import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; -import nl.ictu.service.v1.crypto.IdentifierConverter; +import nl.ictu.crypto.AesGcmCryptographer; +import nl.ictu.crypto.AesGcmSivCryptographer; +import nl.ictu.crypto.IdentifierConverter; +import nl.ictu.model.Identifier; import nl.ictu.utils.Base64Wrapper; import nl.ictu.utils.MessageDigestWrapper; import org.junit.jupiter.api.Test; @@ -21,7 +20,6 @@ /** * Class for tesing {@link AesGcmCryptographer} */ - @Slf4j @ActiveProfiles("test") class TestAesGcmSivCryptographer { @@ -33,7 +31,6 @@ class TestAesGcmSivCryptographer { new IdentifierConverter(new ObjectMapper()), new Base64Wrapper() ); - private final Set testStrings = new HashSet<>( Arrays.asList("a", "bb", "dsv", "ghad", "dhaht", "uDg5Av", "d93fdvv", "dj83hzHo", "38iKawKv9", "dk(gkzm)Mh", "gjk)s3$g9cQ")); @@ -42,7 +39,6 @@ class TestAesGcmSivCryptographer { void testEncyptDecryptForDifferentStringLengths() { testStrings.forEach(plain -> { - try { final String crypted = aesGcmSivCryptographer.encrypt(Identifier.builder() .bsn(plain) @@ -55,23 +51,17 @@ void testEncyptDecryptForDifferentStringLengths() { throw new RuntimeException(e); } }); - } - // Test to ensure ciphertext is different for the same plaintext due to IV randomness @Test void testCiphertextIsTheSameForSamePlaintext() throws Exception { - // The same plaintext message final var plaintext = "This is a test message to ensure ciphertext is different!"; final var identifier = Identifier.builder().bsn(plaintext).build(); - final var encryptedMessage1 = aesGcmSivCryptographer.encrypt(identifier, "aniceSaltGorYu"); final var encryptedMessage2 = aesGcmSivCryptographer.encrypt(identifier, "aniceSaltGorYu"); - // Assert that the two ciphertexts are different assertThat(encryptedMessage1).isEqualTo(encryptedMessage2); } - } diff --git a/src/test/java/nl/ictu/service/v1/map/BsnPseudoMapperTest.java b/src/test/java/nl/ictu/service/map/BsnPseudoMapperTest.java similarity index 96% rename from src/test/java/nl/ictu/service/v1/map/BsnPseudoMapperTest.java rename to src/test/java/nl/ictu/service/map/BsnPseudoMapperTest.java index 3748c73..35f75a0 100644 --- a/src/test/java/nl/ictu/service/v1/map/BsnPseudoMapperTest.java +++ b/src/test/java/nl/ictu/service/map/BsnPseudoMapperTest.java @@ -1,4 +1,4 @@ -package nl.ictu.service.v1.map; +package nl.ictu.service.map; import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -9,9 +9,9 @@ import static org.mockito.Mockito.when; import java.io.IOException; +import nl.ictu.crypto.AesGcmSivCryptographer; import nl.ictu.model.Identifier; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierResponse; -import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; import org.bouncycastle.crypto.InvalidCipherTextException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/src/test/java/nl/ictu/service/v1/map/BsnTokenMapperTest.java b/src/test/java/nl/ictu/service/map/BsnTokenMapperTest.java similarity index 98% rename from src/test/java/nl/ictu/service/v1/map/BsnTokenMapperTest.java rename to src/test/java/nl/ictu/service/map/BsnTokenMapperTest.java index 4a0b9d7..e54bc10 100644 --- a/src/test/java/nl/ictu/service/v1/map/BsnTokenMapperTest.java +++ b/src/test/java/nl/ictu/service/map/BsnTokenMapperTest.java @@ -1,4 +1,4 @@ -package nl.ictu.service.v1.map; +package nl.ictu.service.map; import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/src/test/java/nl/ictu/service/v1/map/EncryptedBsnMapperTest.java b/src/test/java/nl/ictu/service/map/EncryptedBsnMapperTest.java similarity index 93% rename from src/test/java/nl/ictu/service/v1/map/EncryptedBsnMapperTest.java rename to src/test/java/nl/ictu/service/map/EncryptedBsnMapperTest.java index 0166c5f..51f2e7a 100644 --- a/src/test/java/nl/ictu/service/v1/map/EncryptedBsnMapperTest.java +++ b/src/test/java/nl/ictu/service/map/EncryptedBsnMapperTest.java @@ -1,10 +1,10 @@ -package nl.ictu.service.v1.map; +package nl.ictu.service.map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; +import nl.ictu.crypto.AesGcmSivCryptographer; import nl.ictu.model.Identifier; -import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; diff --git a/src/test/java/nl/ictu/service/v1/map/OrganisationPseudoTokenMapperTest.java b/src/test/java/nl/ictu/service/map/OrganisationPseudoTokenMapperTest.java similarity index 97% rename from src/test/java/nl/ictu/service/v1/map/OrganisationPseudoTokenMapperTest.java rename to src/test/java/nl/ictu/service/map/OrganisationPseudoTokenMapperTest.java index ba8a20f..4b96789 100644 --- a/src/test/java/nl/ictu/service/v1/map/OrganisationPseudoTokenMapperTest.java +++ b/src/test/java/nl/ictu/service/map/OrganisationPseudoTokenMapperTest.java @@ -1,4 +1,4 @@ -package nl.ictu.service.v1.map; +package nl.ictu.service.map; import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -8,10 +8,10 @@ import static org.mockito.Mockito.when; import java.io.IOException; +import nl.ictu.crypto.AesGcmSivCryptographer; import nl.ictu.model.Identifier; import nl.ictu.model.Token; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenResponse; -import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; import org.bouncycastle.crypto.InvalidCipherTextException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/src/test/java/nl/ictu/service/v1/map/PseudoBsnMapperTest.java b/src/test/java/nl/ictu/service/map/PseudoBsnMapperTest.java similarity index 95% rename from src/test/java/nl/ictu/service/v1/map/PseudoBsnMapperTest.java rename to src/test/java/nl/ictu/service/map/PseudoBsnMapperTest.java index 3aa3497..6406ed5 100644 --- a/src/test/java/nl/ictu/service/v1/map/PseudoBsnMapperTest.java +++ b/src/test/java/nl/ictu/service/map/PseudoBsnMapperTest.java @@ -1,13 +1,13 @@ -package nl.ictu.service.v1.map; +package nl.ictu.service.map; import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.Mockito.when; +import nl.ictu.crypto.AesGcmSivCryptographer; import nl.ictu.model.Identifier; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierResponse; -import nl.ictu.service.v1.crypto.AesGcmSivCryptographer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; diff --git a/src/test/java/nl/ictu/service/map/WsGetTokenResponseMapperTest.java b/src/test/java/nl/ictu/service/map/WsGetTokenResponseMapperTest.java new file mode 100644 index 0000000..4d4a145 --- /dev/null +++ b/src/test/java/nl/ictu/service/map/WsGetTokenResponseMapperTest.java @@ -0,0 +1,114 @@ +package nl.ictu.service.map; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.security.InvalidKeyException; +import nl.ictu.crypto.AesGcmCryptographer; +import nl.ictu.crypto.TokenCoder; +import nl.ictu.model.Token; +import nl.ictu.pseudoniemenservice.generated.server.model.WsGetTokenResponse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class WsGetTokenResponseMapperTest { + + private final String bsn = "987654321"; + private final long creationDate = System.currentTimeMillis(); + private final String recipientOIN = "123456789"; + private final String encodedToken = "encoded-token"; + @Mock + private AesGcmCryptographer aesGcmCryptographer; + @Mock + private TokenCoder tokenCoder; + @InjectMocks + private WsGetTokenResponseMapper wsGetTokenResponseMapper; + + @Test + @DisplayName("map() -> Successfully maps parameters to WsGetTokenResponse") + void testMap_Success() throws Exception { + + final var encryptedToken = "encrypted-token"; + // GIVEN + Token token = Token.builder() + .version(WsGetTokenResponseMapper.V_1) + .bsn(bsn) + .creationDate(creationDate) + .recipientOIN(recipientOIN) + .build(); + when(tokenCoder.encode(token)).thenReturn(encodedToken); + when(aesGcmCryptographer.encrypt(encodedToken, recipientOIN)).thenReturn(encryptedToken); + // WHEN + WsGetTokenResponse response = wsGetTokenResponseMapper.map(bsn, creationDate, recipientOIN); + // THEN + verify(tokenCoder).encode(token); + verify(aesGcmCryptographer).encrypt(encodedToken, recipientOIN); + org.junit.jupiter.api.Assertions.assertEquals(encryptedToken, response.getToken()); + } + + @Test + @DisplayName("map() -> Fails due to IOException during token encoding") + void testMap_EncodingIOException() throws Exception { + // GIVEN + Token token = Token.builder() + .version(WsGetTokenResponseMapper.V_1) + .bsn(bsn) + .creationDate(creationDate) + .recipientOIN(recipientOIN) + .build(); + when(tokenCoder.encode(token)).thenThrow(new IOException("Encoding failed")); + // WHEN & THEN + assertThrows(IOException.class, + () -> wsGetTokenResponseMapper.map(bsn, creationDate, recipientOIN)); + verify(tokenCoder).encode(token); + verifyNoInteractions(aesGcmCryptographer); + } + + @Test + @DisplayName("map() -> Fails due to encryption errors") + void testMap_EncryptionError() throws Exception { + // GIVEN + Token token = Token.builder() + .version(WsGetTokenResponseMapper.V_1) + .bsn(bsn) + .creationDate(creationDate) + .recipientOIN(recipientOIN) + .build(); + when(tokenCoder.encode(token)).thenReturn(encodedToken); + when(aesGcmCryptographer.encrypt(encodedToken, recipientOIN)) + .thenThrow(new InvalidKeyException("Invalid encryption key")); + // WHEN & THEN + assertThrows(InvalidKeyException.class, + () -> wsGetTokenResponseMapper.map(bsn, creationDate, recipientOIN)); + verify(tokenCoder).encode(token); + verify(aesGcmCryptographer).encrypt(encodedToken, recipientOIN); + } + + @Test + @DisplayName("map() -> Fails due to unexpected runtime exceptions") + void testMap_UnexpectedError() throws Exception { + // GIVEN + Token token = Token.builder() + .version(WsGetTokenResponseMapper.V_1) + .bsn(bsn) + .creationDate(creationDate) + .recipientOIN(recipientOIN) + .build(); + when(tokenCoder.encode(token)).thenReturn(encodedToken); + when(aesGcmCryptographer.encrypt(encodedToken, recipientOIN)).thenThrow( + new RuntimeException("Unexpected error")); + // WHEN & THEN + assertThrows(RuntimeException.class, + () -> wsGetTokenResponseMapper.map(bsn, creationDate, recipientOIN)); + verify(tokenCoder).encode(token); + verify(aesGcmCryptographer).encrypt(encodedToken, recipientOIN); + } +} diff --git a/src/test/java/nl/ictu/service/map/WsIdentifierOinBsnMapperTest.java b/src/test/java/nl/ictu/service/map/WsIdentifierOinBsnMapperTest.java new file mode 100644 index 0000000..f66f8a9 --- /dev/null +++ b/src/test/java/nl/ictu/service/map/WsIdentifierOinBsnMapperTest.java @@ -0,0 +1,57 @@ +package nl.ictu.service.map; + +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; +import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifier; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class WsIdentifierOinBsnMapperTest { + + @Mock + private EncryptedBsnMapper encryptedBsnMapper; + @InjectMocks + private WsIdentifierOinBsnMapper wsIdentifierOinBsnMapper; + + @Test + @DisplayName("map() -> Returns BSN value directly for BSN type") + void testMap_BsnType() { + // GIVEN + String bsnValue = "987654321"; + WsIdentifier identifier = new WsIdentifier().type(BSN).value(bsnValue); + // WHEN + String result = wsIdentifierOinBsnMapper.map(identifier, "123456789"); + // THEN + assertEquals(bsnValue, result); + verifyNoInteractions(encryptedBsnMapper); // Ensure EncryptedBsnMapper is not called + } + + @Test + @DisplayName("map() -> Returns encrypted value for ORGANISATION_PSEUDO type") + void testMap_OrganisationPseudoType() { + // GIVEN + String bsnValue = "987654321"; + String recipientOIN = "123456789"; + String encryptedValue = "encrypted-value"; + WsIdentifier identifier = new WsIdentifier() + .type(ORGANISATION_PSEUDO) + .value(bsnValue); + when(encryptedBsnMapper.map(bsnValue, recipientOIN)).thenReturn(encryptedValue); + // WHEN + String result = wsIdentifierOinBsnMapper.map(identifier, recipientOIN); + // THEN + assertEquals(encryptedValue, result); + verify(encryptedBsnMapper).map(bsnValue, + recipientOIN); // Ensure EncryptedBsnMapper is called + } +} diff --git a/src/test/java/nl/ictu/service/v1/validate/OINValidatorTest.java b/src/test/java/nl/ictu/service/validate/OINValidatorTest.java similarity index 90% rename from src/test/java/nl/ictu/service/v1/validate/OINValidatorTest.java rename to src/test/java/nl/ictu/service/validate/OINValidatorTest.java index ccf1262..448ad3d 100644 --- a/src/test/java/nl/ictu/service/v1/validate/OINValidatorTest.java +++ b/src/test/java/nl/ictu/service/validate/OINValidatorTest.java @@ -1,4 +1,4 @@ -package nl.ictu.service.v1.validate; +package nl.ictu.service.validate; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -14,6 +14,7 @@ class OINValidatorTest { @BeforeEach void setUp() { + oinValidator = new OINValidator(); } @@ -22,11 +23,9 @@ void setUp() { void isValid_ReturnsTrue_WhenOINsMatch() { // GIVEN String callerOIN = "TEST_OIN_123"; - Token token = Token.builder().recipientOIN("TEST_OIN_123").build(); - + Token token = Token.builder().recipientOIN("TEST_OIN_123").build(); // WHEN boolean result = oinValidator.isValid(callerOIN, token); - // THEN assertTrue(result, "Expected isValid() to return true for matching OINs"); } @@ -36,11 +35,9 @@ void isValid_ReturnsTrue_WhenOINsMatch() { void isValid_ReturnsFalse_WhenOINsDoNotMatch() { // GIVEN String callerOIN = "TEST_OIN_ABC"; - Token token = Token.builder().recipientOIN("TEST_OIN_XYZ").build(); - + Token token = Token.builder().recipientOIN("TEST_OIN_XYZ").build(); // WHEN boolean result = oinValidator.isValid(callerOIN, token); - // THEN assertFalse(result, "Expected isValid() to return false for non-matching OINs"); } @@ -52,12 +49,10 @@ void isValid_Behavior_WhenTokenRecipientOINIsNull() { // GIVEN String callerOIN = "NON_NULL_OIN"; Token token = Token.builder().build(); - // WHEN // This will return false if callerOIN is not null, // or might throw NullPointerException if you rely on the equals contract boolean result = oinValidator.isValid(callerOIN, token); - // THEN assertFalse(result, "Expected isValid() to return false if token's recipientOIN is null"); } diff --git a/src/test/java/nl/ictu/utils/AesUtilityTest.java b/src/test/java/nl/ictu/utils/AesUtilityTest.java index 0515b68..0773060 100644 --- a/src/test/java/nl/ictu/utils/AesUtilityTest.java +++ b/src/test/java/nl/ictu/utils/AesUtilityTest.java @@ -2,8 +2,8 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; import java.security.NoSuchAlgorithmException; import javax.crypto.Cipher; @@ -67,7 +67,6 @@ void getAESEngine_ShouldReturnAesEngine() { MultiBlockCipher engine = AesUtility.getAESEngine(); // THEN assertNotNull(engine, "Engine should not be null"); - assertTrue(engine instanceof AESEngine, - "Engine should be an instance of AESEngine"); + assertInstanceOf(AESEngine.class, engine, "Engine should be an instance of AESEngine"); } } diff --git a/src/test/java/nl/ictu/utils/Base64WrapperTest.java b/src/test/java/nl/ictu/utils/Base64WrapperTest.java index aec3e00..f4accbe 100644 --- a/src/test/java/nl/ictu/utils/Base64WrapperTest.java +++ b/src/test/java/nl/ictu/utils/Base64WrapperTest.java @@ -1,6 +1,7 @@ package nl.ictu.utils; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.BeforeEach; @@ -13,6 +14,7 @@ class Base64WrapperTest { @BeforeEach void setUp() { + base64Wrapper = new Base64Wrapper(); } @@ -21,10 +23,8 @@ void setUp() { void encode_ShouldEncodeBytesToBase64Bytes() { // GIVEN byte[] input = "Hello".getBytes(StandardCharsets.UTF_8); - // WHEN byte[] result = base64Wrapper.encode(input); - // THEN String resultAsString = new String(result, StandardCharsets.UTF_8); assertEquals("SGVsbG8=", resultAsString, @@ -36,10 +36,8 @@ void encode_ShouldEncodeBytesToBase64Bytes() { void encodeToString_ShouldEncodeBytesToBase64String() { // GIVEN byte[] input = "Hello".getBytes(StandardCharsets.UTF_8); - // WHEN String base64String = base64Wrapper.encodeToString(input); - // THEN assertEquals("SGVsbG8=", base64String, "Expected Base64 encoding of 'Hello' to be 'SGVsbG8='"); @@ -50,10 +48,8 @@ void encodeToString_ShouldEncodeBytesToBase64String() { void decode_ShouldDecodeBase64StringToBytes() { // GIVEN String base64String = "SGVsbG8="; - // WHEN byte[] decoded = base64Wrapper.decode(base64String); - // THEN String decodedAsString = new String(decoded, StandardCharsets.UTF_8); assertEquals("Hello", decodedAsString, @@ -65,7 +61,6 @@ void decode_ShouldDecodeBase64StringToBytes() { void decode_ShouldThrowException_WhenInvalidBase64String() { // GIVEN String invalidBase64 = "Not valid base64!!!"; - // WHEN & THEN // Base64.getDecoder().decode(...) throws IllegalArgumentException on invalid input assertThrows(IllegalArgumentException.class, diff --git a/src/test/java/nl/ictu/utils/ByteArrayUtilsTest.java b/src/test/java/nl/ictu/utils/ByteArrayUtilsTest.java index d2ebac3..ffee23a 100644 --- a/src/test/java/nl/ictu/utils/ByteArrayUtilsTest.java +++ b/src/test/java/nl/ictu/utils/ByteArrayUtilsTest.java @@ -14,10 +14,8 @@ void concat_ShouldConcatenateTwoArrays() { byte[] a = {1, 2, 3}; byte[] b = {4, 5, 6}; byte[] expected = {1, 2, 3, 4, 5, 6}; - // WHEN byte[] result = ByteArrayUtil.concat(a, b); - // THEN assertArrayEquals(expected, result); } @@ -29,10 +27,8 @@ void concat_ShouldHandleTwoEmptyArrays() { byte[] a = {}; byte[] b = {}; byte[] expected = {}; - // WHEN byte[] result = ByteArrayUtil.concat(a, b); - // THEN assertArrayEquals(expected, result); } @@ -44,21 +40,16 @@ void concat_ShouldHandleOneEmptyArray() { byte[] a = {1, 2, 3}; byte[] b = {}; byte[] expected1 = {1, 2, 3}; - // WHEN byte[] result1 = ByteArrayUtil.concat(a, b); - // THEN assertArrayEquals(expected1, result1); - // GIVEN byte[] c = {}; byte[] d = {4, 5, 6}; byte[] expected2 = {4, 5, 6}; - // WHEN byte[] result2 = ByteArrayUtil.concat(c, d); - // THEN assertArrayEquals(expected2, result2); } diff --git a/src/test/java/nl/ictu/utils/MessageDigestWrapperTest.java b/src/test/java/nl/ictu/utils/MessageDigestWrapperTest.java index be7544b..0f5517c 100644 --- a/src/test/java/nl/ictu/utils/MessageDigestWrapperTest.java +++ b/src/test/java/nl/ictu/utils/MessageDigestWrapperTest.java @@ -13,6 +13,7 @@ class MessageDigestWrapperTest { @BeforeEach void setUp() { + messageDigestWrapper = new MessageDigestWrapper(); } @@ -20,7 +21,6 @@ void setUp() { void getMessageDigestSha256_ShouldReturnSha256Digest() { // WHEN MessageDigest digest = messageDigestWrapper.getMessageDigestInstance(); - // THEN assertNotNull(digest, "MessageDigest should not be null"); assertEquals("SHA-256", digest.getAlgorithm(), From 4d3c8d072f489d1ffd3e221223ba855fa4fc2f20 Mon Sep 17 00:00:00 2001 From: sayf Date: Thu, 26 Dec 2024 01:41:23 +0100 Subject: [PATCH 26/30] 95% mutation coverage and some good old SOLID refactoring --- .../ictu/controller/IndexControllerTest.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/test/java/nl/ictu/controller/IndexControllerTest.java diff --git a/src/test/java/nl/ictu/controller/IndexControllerTest.java b/src/test/java/nl/ictu/controller/IndexControllerTest.java new file mode 100644 index 0000000..d97e34e --- /dev/null +++ b/src/test/java/nl/ictu/controller/IndexControllerTest.java @@ -0,0 +1,26 @@ +package nl.ictu.controller; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +@ExtendWith(SpringExtension.class) +class IndexControllerTest { + + private final IndexController controller = new IndexController(); + private final MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + + @Test + @DisplayName("GET / -> Redirects to Swagger UI") + void testRedirectToSwaggerUi() throws Exception { + // WHEN & THEN + mockMvc.perform( + org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get("/")) + .andExpect(MockMvcResultMatchers.status().is3xxRedirection()) + .andExpect(MockMvcResultMatchers.redirectedUrl("/swagger-ui/index.html")); + } +} From 2b97626862edee6d98b1b9c1b5d13ac35a8d49ff Mon Sep 17 00:00:00 2001 From: sayf Date: Thu, 26 Dec 2024 10:18:33 +0100 Subject: [PATCH 27/30] Uniformity in unit testing and integration tests --- .../nl/ictu/TestingWebApplicationTests.java | 16 +++++- .../PseudoniemenServicePropertiesTest.java | 14 +++++ .../GlobalExceptionHandlerTest.java | 11 +++- .../ictu/controller/IndexControllerTest.java | 6 ++- .../v1/ExchangeIdentifierControllerTest.java | 12 ++++- .../v1/ExchangeTokenControllerTest.java | 12 ++++- .../ExchangeIdentifierServiceTest.java | 19 +++++-- .../service/ExchangeTokenServiceTest.java | 34 ++++++++----- .../nl/ictu/service/GetTokenServiceTest.java | 32 ++++++++---- .../ictu/service/TestAesGcmCryptographer.java | 21 ++++++-- .../service/TestAesGcmSivCryptographer.java | 24 +++++++-- .../ictu/service/map/BsnPseudoMapperTest.java | 23 +++++++-- .../ictu/service/map/BsnTokenMapperTest.java | 18 +++++-- .../service/map/EncryptedBsnMapperTest.java | 21 +++++--- .../OrganisationPseudoTokenMapperTest.java | 51 ++++++++++++------- .../ictu/service/map/PseudoBsnMapperTest.java | 8 ++- .../map/WsGetTokenResponseMapperTest.java | 28 ++++++++-- .../map/WsIdentifierOinBsnMapperTest.java | 16 ++++-- .../service/validate/OINValidatorTest.java | 22 +++++--- .../java/nl/ictu/utils/AESHelperTest.java | 21 ++++++++ .../java/nl/ictu/utils/AesUtilityTest.java | 24 +++++++-- .../java/nl/ictu/utils/Base64WrapperTest.java | 26 +++++++--- .../nl/ictu/utils/ByteArrayUtilsTest.java | 18 +++++-- .../ictu/utils/MessageDigestWrapperTest.java | 7 ++- 24 files changed, 379 insertions(+), 105 deletions(-) diff --git a/src/test/java/nl/ictu/TestingWebApplicationTests.java b/src/test/java/nl/ictu/TestingWebApplicationTests.java index 5091f2d..cbb7dbc 100644 --- a/src/test/java/nl/ictu/TestingWebApplicationTests.java +++ b/src/test/java/nl/ictu/TestingWebApplicationTests.java @@ -8,6 +8,7 @@ import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -31,8 +32,12 @@ class TestingWebApplicationTests { private TestRestTemplate restTemplate; @Test + @DisplayName(""" + Given the Spring Boot application is running with actuator enabled + When accessing the /actuator/health endpoint + Then the response should contain a status of 'UP' + """) void testActuatorHealthEndpoint() { - final int actuatorPort = environment.getProperty("local.management.port", Integer.class); assertThat( restTemplate.getForObject("http://localhost:" + actuatorPort + "/actuator/health", @@ -41,6 +46,12 @@ void testActuatorHealthEndpoint() { } @Test + @DisplayName(""" + Given a request to get a token with a BSN identifier + When sending the request to /v1/getToken + Then the response should include a token + And the token can be used to exchange for the identifier type BSN + """) void testGetAtokenExchangeForBSN() { // get a token final var getTokenBody = Map.of("recipientOIN", "54321543215432154321", "identifier", @@ -55,6 +66,7 @@ void testGetAtokenExchangeForBSN() { .extracting("body") .asInstanceOf(InstanceOfAssertFactories.map(String.class, Void.class)) .containsKey("token"); + // change token for identifier final var token = (String) tokenExchange.getBody().get("token"); final var exchangeTokenBody = Map.of("token", token, "identifierType", "BSN"); @@ -70,4 +82,4 @@ void testGetAtokenExchangeForBSN() { .asInstanceOf(InstanceOfAssertFactories.map(String.class, Map.class)) .containsExactly(entry("identifier", Map.of("type", "BSN", "value", "012345679"))); } -} \ No newline at end of file +} diff --git a/src/test/java/nl/ictu/configuration/PseudoniemenServicePropertiesTest.java b/src/test/java/nl/ictu/configuration/PseudoniemenServicePropertiesTest.java index 775a346..acd6080 100644 --- a/src/test/java/nl/ictu/configuration/PseudoniemenServicePropertiesTest.java +++ b/src/test/java/nl/ictu/configuration/PseudoniemenServicePropertiesTest.java @@ -5,11 +5,17 @@ import nl.ictu.service.exception.IdentifierPrivateKeyException; import nl.ictu.service.exception.TokenPrivateKeyException; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; class PseudoniemenServicePropertiesTest { @Test + @DisplayName(""" + Given an empty token private key + When validating + When TokenPrivateKeyException is thrown + """) void validate_WhenTokenPrivateKeyIsEmpty_ThrowsTokenPrivateKeyException() { // GIVEN PseudoniemenServiceProperties props = new PseudoniemenServiceProperties() @@ -20,6 +26,10 @@ void validate_WhenTokenPrivateKeyIsEmpty_ThrowsTokenPrivateKeyException() { } @Test + @DisplayName(""" + Given an empty identifier private key + When validating, then IdentifierPrivateKeyException is thrown + """) void validate_WhenIdentifierPrivateKeyIsEmpty_ThrowsIdentifierPrivateKeyException() { // GIVEN PseudoniemenServiceProperties props = new PseudoniemenServiceProperties() @@ -30,6 +40,10 @@ void validate_WhenIdentifierPrivateKeyIsEmpty_ThrowsIdentifierPrivateKeyExceptio } @Test + @DisplayName(""" + Given both keys are set when validating + Then no exception is thrown + """) void validate_WhenBothKeysAreSet_NoExceptionIsThrown() { // GIVEN PseudoniemenServiceProperties props = new PseudoniemenServiceProperties() diff --git a/src/test/java/nl/ictu/controller/GlobalExceptionHandlerTest.java b/src/test/java/nl/ictu/controller/GlobalExceptionHandlerTest.java index 10dd150..9a3eccb 100644 --- a/src/test/java/nl/ictu/controller/GlobalExceptionHandlerTest.java +++ b/src/test/java/nl/ictu/controller/GlobalExceptionHandlerTest.java @@ -32,6 +32,11 @@ class GlobalExceptionHandlerTest { // Test for handleGenericException @Test + @DisplayName(""" + Given an invalid endpoint + When a GET request is made + Then an internal server error is returned with an appropriate message + """) void handleGenericException_ShouldReturnInternalServerErrorWithMessage() throws Exception { mockMvc.perform(get("/non-existent-endpoint")) // Assuming no controller is mapped to this @@ -42,7 +47,11 @@ void handleGenericException_ShouldReturnInternalServerErrorWithMessage() throws } @Test - @DisplayName("exchangeToken() -> 422 UNPROCESSABLE_ENTITY on exception") + @DisplayName(""" + Given a stubbed controller and service + When the service throws various exceptions + Then the system responds with UNPROCESSABLE_ENTITY and the exception message + """) void exchangeToken_ShouldReturnUnprocessableEntity() { // GIVEN: a stubbed controller and service // WHEN: the service throws an exception diff --git a/src/test/java/nl/ictu/controller/IndexControllerTest.java b/src/test/java/nl/ictu/controller/IndexControllerTest.java index d97e34e..ad11828 100644 --- a/src/test/java/nl/ictu/controller/IndexControllerTest.java +++ b/src/test/java/nl/ictu/controller/IndexControllerTest.java @@ -15,7 +15,11 @@ class IndexControllerTest { private final MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); @Test - @DisplayName("GET / -> Redirects to Swagger UI") + @DisplayName(""" + Given a request to the root endpoint + When performing a GET request + Then the response redirects to Swagger UI + """) void testRedirectToSwaggerUi() throws Exception { // WHEN & THEN mockMvc.perform( diff --git a/src/test/java/nl/ictu/controller/v1/ExchangeIdentifierControllerTest.java b/src/test/java/nl/ictu/controller/v1/ExchangeIdentifierControllerTest.java index 99752b1..efa139a 100644 --- a/src/test/java/nl/ictu/controller/v1/ExchangeIdentifierControllerTest.java +++ b/src/test/java/nl/ictu/controller/v1/ExchangeIdentifierControllerTest.java @@ -25,7 +25,11 @@ class ExchangeIdentifierControllerTest { private ExchangeIdentifierController controller; @Test - @DisplayName("exchangeIdentifier() -> Returns 200 OK with response on success") + @DisplayName(""" + Given a valid request and service response + When calling exchangeIdentifier() + Then it returns 200 OK with the expected response + """) void testExchangeIdentifier_Success() { // GIVEN String callerOIN = "123456789"; @@ -41,7 +45,11 @@ void testExchangeIdentifier_Success() { } @Test - @DisplayName("exchangeIdentifier() -> Throws exception when service fails") + @DisplayName(""" + Given a valid request and service throws an exception + When calling exchangeIdentifier() + Then it throws the same exception with the correct message + """) void testExchangeIdentifier_ServiceThrowsException() { // GIVEN String callerOIN = "123456789"; diff --git a/src/test/java/nl/ictu/controller/v1/ExchangeTokenControllerTest.java b/src/test/java/nl/ictu/controller/v1/ExchangeTokenControllerTest.java index d03fb08..685ec2c 100644 --- a/src/test/java/nl/ictu/controller/v1/ExchangeTokenControllerTest.java +++ b/src/test/java/nl/ictu/controller/v1/ExchangeTokenControllerTest.java @@ -35,7 +35,11 @@ class ExchangeTokenControllerTest { private ObjectMapper objectMapper; @Test - @DisplayName("exchangeToken() -> 200 OK on success") + @DisplayName(""" + Given a valid token and identifier type + When calling exchangeToken() + Then it returns 200 OK with the expected identifier in the response + """) void exchangeToken_ShouldReturnOk() throws Exception { // GIVEN: a request payload WsExchangeTokenRequest requestPayload = new WsExchangeTokenRequest(); @@ -64,7 +68,11 @@ void exchangeToken_ShouldReturnOk() throws Exception { } @Test - @DisplayName("exchangeToken() -> 422 UNPROCESSABLE_ENTITY on exception") + @DisplayName(""" + Given an invalid token and identifier type + When calling exchangeToken() + Then it returns 422 UNPROCESSABLE_ENTITY with the appropriate error + """) void exchangeToken_ShouldReturnUnprocessableEntity() throws Exception { // GIVEN: a request payload WsExchangeTokenRequest requestPayload = new WsExchangeTokenRequest(); diff --git a/src/test/java/nl/ictu/service/ExchangeIdentifierServiceTest.java b/src/test/java/nl/ictu/service/ExchangeIdentifierServiceTest.java index 0efae9c..227e1f7 100644 --- a/src/test/java/nl/ictu/service/ExchangeIdentifierServiceTest.java +++ b/src/test/java/nl/ictu/service/ExchangeIdentifierServiceTest.java @@ -32,7 +32,11 @@ class ExchangeIdentifierServiceTest { private ExchangeIdentifierService exchangeIdentifierService; @Test - @DisplayName("exchangeIdentifier() -> BSN -> ORGANISATION_PSEUDO path") + @DisplayName(""" + Given a BSN identifier and recipientIdentifierType ORGANISATION_PSEUDO + When exchangeIdentifier() is called + Then it should return a response with ORGANISATION_PSEUDO type and encrypted value + """) void testExchangeIdentifier_BsnToOrgPseudo() throws Exception { // GIVEN var request = new WsExchangeIdentifierRequest() @@ -48,7 +52,6 @@ void testExchangeIdentifier_BsnToOrgPseudo() throws Exception { WsExchangeIdentifierResponse actualResponse = exchangeIdentifierService.exchangeIdentifier(request); // THEN - // Verify the returned response is what the mapper gave back org.junit.jupiter.api.Assertions.assertNotNull(actualResponse); org.junit.jupiter.api.Assertions.assertNotNull(actualResponse.getIdentifier()); org.junit.jupiter.api.Assertions.assertEquals(ORGANISATION_PSEUDO, @@ -58,7 +61,11 @@ void testExchangeIdentifier_BsnToOrgPseudo() throws Exception { } @Test - @DisplayName("exchangeIdentifier() -> ORGANISATION_PSEUDO -> BSN path") + @DisplayName(""" + Given an ORGANISATION_PSEUDO identifier and recipientIdentifierType BSN + When exchangeIdentifier() is called + Then it should return a response with BSN type and decrypted value + """) void testExchangeIdentifier_OrgPseudoToBsn() throws Exception { // GIVEN var request = new WsExchangeIdentifierRequest() @@ -82,7 +89,11 @@ void testExchangeIdentifier_OrgPseudoToBsn() throws Exception { } @Test - @DisplayName("exchangeIdentifier() -> throws InvalidWsIdentifierRequestTypeException for unsupported mappings") + @DisplayName(""" + Given a request with unsupported identifier mapping (BSN -> BSN or ORG_PSEUDO -> ORG_PSEUDO) + When exchangeIdentifier() is called + Then it should throw InvalidWsIdentifierRequestTypeException + """) void testExchangeIdentifier_UnsupportedMapping_ThrowsException() { // GIVEN var request = new WsExchangeIdentifierRequest() diff --git a/src/test/java/nl/ictu/service/ExchangeTokenServiceTest.java b/src/test/java/nl/ictu/service/ExchangeTokenServiceTest.java index 33b2591..82c8b20 100644 --- a/src/test/java/nl/ictu/service/ExchangeTokenServiceTest.java +++ b/src/test/java/nl/ictu/service/ExchangeTokenServiceTest.java @@ -30,6 +30,7 @@ class ExchangeTokenServiceTest { private final String callerOIN = "123456789"; private final String encryptedToken = "encryptedTokenValue"; private final String decodedToken = "decodedTokenValue"; + @Mock private AesGcmCryptographer aesGcmCryptographer; @Mock @@ -42,18 +43,22 @@ class ExchangeTokenServiceTest { private BsnTokenMapper bsnTokenMapper; @InjectMocks private ExchangeTokenService exchangeTokenService; + private Token mockToken; @BeforeEach void setUp() { - // Setup mock token object mockToken = Token.builder().build(); mockToken.setRecipientOIN(callerOIN); mockToken.setBsn("987654321"); } @Test - @DisplayName("exchangeToken() -> BSN identifier") + @DisplayName(""" + Given a BSN identifier + When exchangeToken() is called + Then it should return a valid response mapped by BsnTokenMapper + """) void testExchangeToken_BsnIdentifier() throws Exception { // GIVEN var request = new WsExchangeTokenRequest().token(encryptedToken).identifierType(BSN); @@ -64,8 +69,7 @@ void testExchangeToken_BsnIdentifier() throws Exception { var expectedResponse = new WsExchangeTokenResponse(); when(bsnTokenMapper.map(mockToken)).thenReturn(expectedResponse); // WHEN - WsExchangeTokenResponse actualResponse = - exchangeTokenService.exchangeToken(callerOIN, request); + WsExchangeTokenResponse actualResponse = exchangeTokenService.exchangeToken(callerOIN, request); // THEN verify(aesGcmCryptographer).decrypt(encryptedToken, callerOIN); verify(tokenCoder).decode(decodedToken); @@ -75,12 +79,14 @@ void testExchangeToken_BsnIdentifier() throws Exception { } @Test - @DisplayName("exchangeToken() -> ORGANISATION_PSEUDO identifier") + @DisplayName(""" + Given an ORGANISATION_PSEUDO identifier + When exchangeToken() is called + Then it should return a valid response mapped by OrganisationPseudoTokenMapper + """) void testExchangeToken_OrganisationPseudoIdentifier() throws Exception { // GIVEN - var request = - new WsExchangeTokenRequest().token(encryptedToken) - .identifierType(ORGANISATION_PSEUDO); + var request = new WsExchangeTokenRequest().token(encryptedToken).identifierType(ORGANISATION_PSEUDO); // Stubbing dependencies when(aesGcmCryptographer.decrypt(encryptedToken, callerOIN)).thenReturn(decodedToken); when(tokenCoder.decode(decodedToken)).thenReturn(mockToken); @@ -88,8 +94,7 @@ void testExchangeToken_OrganisationPseudoIdentifier() throws Exception { var expectedResponse = new WsExchangeTokenResponse(); when(organisationPseudoTokenMapper.map(callerOIN, mockToken)).thenReturn(expectedResponse); // WHEN - WsExchangeTokenResponse actualResponse = - exchangeTokenService.exchangeToken(callerOIN, request); + WsExchangeTokenResponse actualResponse = exchangeTokenService.exchangeToken(callerOIN, request); // THEN verify(aesGcmCryptographer).decrypt(encryptedToken, callerOIN); verify(tokenCoder).decode(decodedToken); @@ -99,7 +104,11 @@ void testExchangeToken_OrganisationPseudoIdentifier() throws Exception { } @Test - @DisplayName("exchangeToken() -> Invalid OIN") + @DisplayName(""" + Given an invalid OIN + When exchangeToken() is called + Then it should throw InvalidOINException + """) void testExchangeToken_InvalidOIN() throws Exception { // GIVEN var request = new WsExchangeTokenRequest().token(encryptedToken).identifierType(BSN); @@ -108,8 +117,7 @@ void testExchangeToken_InvalidOIN() throws Exception { when(tokenCoder.decode(decodedToken)).thenReturn(mockToken); when(oinValidator.isValid(callerOIN, mockToken)).thenReturn(false); // Invalid OIN // WHEN & THEN - assertThrows(InvalidOINException.class, - () -> exchangeTokenService.exchangeToken(callerOIN, request)); + assertThrows(InvalidOINException.class, () -> exchangeTokenService.exchangeToken(callerOIN, request)); verify(aesGcmCryptographer).decrypt(encryptedToken, callerOIN); verify(tokenCoder).decode(decodedToken); verify(oinValidator).isValid(callerOIN, mockToken); diff --git a/src/test/java/nl/ictu/service/GetTokenServiceTest.java b/src/test/java/nl/ictu/service/GetTokenServiceTest.java index a664a27..0ebd617 100644 --- a/src/test/java/nl/ictu/service/GetTokenServiceTest.java +++ b/src/test/java/nl/ictu/service/GetTokenServiceTest.java @@ -26,20 +26,27 @@ class GetTokenServiceTest { private final String recipientOIN = "123456789"; private final String bsn = "987654321"; + @Mock private WsIdentifierOinBsnMapper wsIdentifierOinBsnMapper; + @Mock private WsGetTokenResponseMapper wsGetTokenResponseMapper; + @InjectMocks private GetTokenService getTokenService; @BeforeEach void setUp() { - // This section can be used to initialize common test data if needed + // Initialize common test data if needed } @Test - @DisplayName("getWsGetTokenResponse() -> Valid input") + @DisplayName(""" + Given a valid identifier of type BSN + When getWsGetTokenResponse() is called + Then it should return a valid response + """) void testGetWsGetTokenResponse_ValidInput() throws Exception { // GIVEN var identifier = WsIdentifier.builder() @@ -47,13 +54,14 @@ void testGetWsGetTokenResponse_ValidInput() throws Exception { .value(bsn) .build(); var expectedResponse = new WsGetTokenResponse(); + // Stubbing dependencies when(wsIdentifierOinBsnMapper.map(identifier, recipientOIN)).thenReturn(bsn); - when(wsGetTokenResponseMapper.map(eq(bsn), anyLong(), eq(recipientOIN))).thenReturn( - expectedResponse); + when(wsGetTokenResponseMapper.map(eq(bsn), anyLong(), eq(recipientOIN))).thenReturn(expectedResponse); + // WHEN - WsGetTokenResponse actualResponse = - getTokenService.getWsGetTokenResponse(recipientOIN, identifier); + WsGetTokenResponse actualResponse = getTokenService.getWsGetTokenResponse(recipientOIN, identifier); + // THEN verify(wsIdentifierOinBsnMapper).map(identifier, recipientOIN); verify(wsGetTokenResponseMapper).map(eq(bsn), anyLong(), eq(recipientOIN)); @@ -61,7 +69,11 @@ void testGetWsGetTokenResponse_ValidInput() throws Exception { } @Test - @DisplayName("getWsGetTokenResponse() -> Unexpected error during processing") + @DisplayName(""" + Given an unexpected error during processing + When getWsGetTokenResponse() is called + Then it should throw WsGetTokenProcessingException with the correct message + """) void testGetWsGetTokenResponse_UnexpectedError() { // GIVEN var identifier = WsIdentifier.builder() @@ -69,12 +81,14 @@ void testGetWsGetTokenResponse_UnexpectedError() { .value(bsn) .build(); var exceptionMessage = "Unexpected processing error"; + // Stubbing dependencies - when(wsIdentifierOinBsnMapper.map(identifier, recipientOIN)).thenThrow( - new RuntimeException(exceptionMessage)); + when(wsIdentifierOinBsnMapper.map(identifier, recipientOIN)).thenThrow(new RuntimeException(exceptionMessage)); + // WHEN & THEN Exception exception = assertThrows(WsGetTokenProcessingException.class, () -> getTokenService.getWsGetTokenResponse(recipientOIN, identifier)); + // Assert exception message org.junit.jupiter.api.Assertions.assertEquals(exceptionMessage, exception.getMessage()); verify(wsIdentifierOinBsnMapper).map(identifier, recipientOIN); diff --git a/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java b/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java index 5f99068..4c370c9 100644 --- a/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java +++ b/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java @@ -10,11 +10,12 @@ import nl.ictu.crypto.AesGcmCryptographer; import nl.ictu.utils.Base64Wrapper; import nl.ictu.utils.MessageDigestWrapper; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.test.context.ActiveProfiles; /** - * Class for tesing {@link AesGcmCryptographer} + * Class for testing {@link AesGcmCryptographer} */ @Slf4j @ActiveProfiles("test") @@ -31,12 +32,20 @@ class TestAesGcmCryptographer { "38iKawKv9", "dk(gkzm)Mh", "gjk)s3$g9cQ")); @Test + @DisplayName(""" + Given a set of test strings + When encrypting and decrypting each string with a specific key + Then the decrypted string should be equal to the original plain string + """) void testEncyptDecryptForDifferentStringLengths() { testStrings.forEach(plain -> { try { + // GIVEN final String crypted = aesGcmCryptographer.encrypt(plain, "helloHowAreyo12345678"); + // WHEN final String actual = aesGcmCryptographer.decrypt(crypted, "helloHowAreyo12345678"); + // THEN assertThat(actual).isEqualTo(plain); } catch (final Exception e) { throw new RuntimeException(e); @@ -44,13 +53,19 @@ void testEncyptDecryptForDifferentStringLengths() { }); } - // Test to ensure ciphertext is different for the same plaintext due to IV randomness @Test + @DisplayName(""" + Given the same plaintext message and encryption key + When encrypting the message twice + Then the resulting ciphertexts should be different due to IV randomness + """) void testCiphertextIsDifferentForSamePlaintext() throws Exception { - // The same plaintext message + // GIVEN String plaintext = "This is a test message to ensure ciphertext is different!"; + // WHEN String encryptedMessage1 = aesGcmCryptographer.encrypt(plaintext, "aniceSaltGorYu"); String encryptedMessage2 = aesGcmCryptographer.encrypt(plaintext, "aniceSaltGorYu"); + // THEN // Assert that the two ciphertexts are different assertThat(encryptedMessage1).isNotEqualTo(encryptedMessage2); } diff --git a/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java b/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java index b9b41a3..4c94bbc 100644 --- a/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java +++ b/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java @@ -8,17 +8,17 @@ import java.util.Set; import lombok.extern.slf4j.Slf4j; import nl.ictu.configuration.PseudoniemenServiceProperties; -import nl.ictu.crypto.AesGcmCryptographer; import nl.ictu.crypto.AesGcmSivCryptographer; import nl.ictu.crypto.IdentifierConverter; import nl.ictu.model.Identifier; import nl.ictu.utils.Base64Wrapper; import nl.ictu.utils.MessageDigestWrapper; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.test.context.ActiveProfiles; /** - * Class for tesing {@link AesGcmCryptographer} + * Class for testing {@link AesGcmSivCryptographer} */ @Slf4j @ActiveProfiles("test") @@ -36,16 +36,24 @@ class TestAesGcmSivCryptographer { "38iKawKv9", "dk(gkzm)Mh", "gjk)s3$g9cQ")); @Test + @DisplayName(""" + Given a set of test strings + When encrypting and decrypting each string with a specific key + Then the decrypted identifier's BSN should be equal to the original plain string + """) void testEncyptDecryptForDifferentStringLengths() { testStrings.forEach(plain -> { try { + // GIVEN final String crypted = aesGcmSivCryptographer.encrypt(Identifier.builder() .bsn(plain) .build(), "helloHowAreyo12345678"); + // WHEN final Identifier actual = aesGcmSivCryptographer.decrypt(crypted, "helloHowAreyo12345678"); + // THEN assertThat(actual.getBsn()).isEqualTo(plain); } catch (final Exception e) { throw new RuntimeException(e); @@ -53,15 +61,21 @@ void testEncyptDecryptForDifferentStringLengths() { }); } - // Test to ensure ciphertext is different for the same plaintext due to IV randomness @Test + @DisplayName(""" + Given the same plaintext message and encryption key + When encrypting the message twice + Then the resulting ciphertexts should be the same due to SIV mode + """) void testCiphertextIsTheSameForSamePlaintext() throws Exception { - // The same plaintext message + // GIVEN final var plaintext = "This is a test message to ensure ciphertext is different!"; final var identifier = Identifier.builder().bsn(plaintext).build(); + // WHEN final var encryptedMessage1 = aesGcmSivCryptographer.encrypt(identifier, "aniceSaltGorYu"); final var encryptedMessage2 = aesGcmSivCryptographer.encrypt(identifier, "aniceSaltGorYu"); - // Assert that the two ciphertexts are different + // THEN + // Assert that the two ciphertexts are the same assertThat(encryptedMessage1).isEqualTo(encryptedMessage2); } } diff --git a/src/test/java/nl/ictu/service/map/BsnPseudoMapperTest.java b/src/test/java/nl/ictu/service/map/BsnPseudoMapperTest.java index 35f75a0..84974e5 100644 --- a/src/test/java/nl/ictu/service/map/BsnPseudoMapperTest.java +++ b/src/test/java/nl/ictu/service/map/BsnPseudoMapperTest.java @@ -13,6 +13,7 @@ import nl.ictu.model.Identifier; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierResponse; import org.bouncycastle.crypto.InvalidCipherTextException; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -28,12 +29,16 @@ class BsnPseudoMapperTest { private BsnPseudoMapper bsnPseudoMapper; @Test - void map_ShouldReturnWsExchangeIdentifierResponse_WhenEncryptionSucceeds() throws Exception { + @DisplayName(""" + Given a valid BSN and OIN + When encryption succeeds + Then a valid WsExchangeIdentifierResponse is returned + """) + void map_WhenEncryptionSucceeds_ShouldReturnWsExchangeIdentifierResponse() throws Exception { // GIVEN String bsn = "123456789"; String oin = "OIN_X"; String encryptedValue = "encryptedBsn123"; - // Mock the cryptographer to return a known "encrypted" value when(aesGcmSivCryptographer.encrypt(any(Identifier.class), eq(oin))) .thenReturn(encryptedValue); // WHEN @@ -46,7 +51,12 @@ void map_ShouldReturnWsExchangeIdentifierResponse_WhenEncryptionSucceeds() throw } @Test - void map_ShouldThrowIOException_WhenEncryptThrowsIOException() throws Exception { + @DisplayName(""" + Given a BSN and OIN + When encryption throws IOException + Then an IOException is thrown + """) + void map_WhenEncryptThrowsIOException_ShouldThrowIOException() throws Exception { // GIVEN String bsn = "987654321"; String oin = "OIN_IO"; @@ -57,7 +67,12 @@ void map_ShouldThrowIOException_WhenEncryptThrowsIOException() throws Exception } @Test - void map_ShouldThrowInvalidCipherTextException_WhenEncryptThrowsInvalidCipherTextException() + @DisplayName(""" + Given a BSN and OIN + When encryption throws InvalidCipherTextException + Then an InvalidCipherTextException is thrown + """) + void map_WhenEncryptThrowsInvalidCipherTextException_ShouldThrowInvalidCipherTextException() throws Exception { // GIVEN String bsn = "111222333"; diff --git a/src/test/java/nl/ictu/service/map/BsnTokenMapperTest.java b/src/test/java/nl/ictu/service/map/BsnTokenMapperTest.java index e54bc10..7adcc81 100644 --- a/src/test/java/nl/ictu/service/map/BsnTokenMapperTest.java +++ b/src/test/java/nl/ictu/service/map/BsnTokenMapperTest.java @@ -8,6 +8,7 @@ import nl.ictu.model.Token; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenResponse; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; class BsnTokenMapperTest { @@ -21,11 +22,16 @@ void setUp() { } @Test - void map_ShouldReturnResponseWithBsnIdentifier() { + @DisplayName(""" + Given a token with a valid BSN + When mapped + Then the response contains an identifier with the correct BSN type and value + """) + void map_WhenTokenHasValidBsn_ShouldReturnResponseWithBsnIdentifier() { // GIVEN final var token = Token.builder() .bsn("123456789") - .build(); // Suppose your Token class has setBsn(...) method + .build(); // WHEN WsExchangeTokenResponse response = bsnTokenMapper.map(token); // THEN @@ -37,7 +43,12 @@ void map_ShouldReturnResponseWithBsnIdentifier() { } @Test - void map_ShouldHandleNullBsnGracefully() { + @DisplayName(""" + Given a token without a BSN + When mapped + Then the response contains an identifier with BSN type but a null value + """) + void map_WhenTokenHasNoBsn_ShouldHandleNullBsnGracefully() { // GIVEN final var token = Token.builder().build(); // No BSN set // WHEN @@ -47,7 +58,6 @@ void map_ShouldHandleNullBsnGracefully() { assertNotNull(response.getIdentifier(), "Identifier should not be null"); assertEquals(BSN, response.getIdentifier().getType(), "Identifier type should still be BSN"); - // Since token.getBsn() is null, we expect null in the identifier's value assertNull(response.getIdentifier().getValue(), "Value should be null if token’s BSN is null"); } diff --git a/src/test/java/nl/ictu/service/map/EncryptedBsnMapperTest.java b/src/test/java/nl/ictu/service/map/EncryptedBsnMapperTest.java index 51f2e7a..1f1b66e 100644 --- a/src/test/java/nl/ictu/service/map/EncryptedBsnMapperTest.java +++ b/src/test/java/nl/ictu/service/map/EncryptedBsnMapperTest.java @@ -5,6 +5,7 @@ import nl.ictu.crypto.AesGcmSivCryptographer; import nl.ictu.model.Identifier; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -20,19 +21,25 @@ class EncryptedBsnMapperTest { private EncryptedBsnMapper encryptedBsnMapper; @Test - void map_ShouldReturnDecryptedBsn_WhenDecryptSucceeds() { + @DisplayName(""" + Given an encrypted BSN and a recipient OIN + When decryption succeeds + Then the decrypted BSN is returned + """) + void map_WhenDecryptSucceeds_ShouldReturnDecryptedBsn() { // GIVEN - String encryptedBsn = "someEncryptedValue"; - String recipientOin = "testOIN"; - // Suppose decrypt returns an Identifier with "123456789" as BSN - Identifier decryptedIdentifier = Identifier.builder() - .bsn("123456789") + final String encryptedBsn = "someEncryptedValue"; + final String recipientOin = "testOIN"; + final String expectedBsn = "123456789"; + final var decryptedIdentifier = Identifier.builder() + .bsn(expectedBsn) .build(); when(aesGcmSivCryptographer.decrypt(encryptedBsn, recipientOin)) .thenReturn(decryptedIdentifier); // WHEN String result = encryptedBsnMapper.map(encryptedBsn, recipientOin); // THEN - assertEquals("123456789", result); + assertEquals(expectedBsn, result, + "The decrypted BSN should match the expected value"); } } diff --git a/src/test/java/nl/ictu/service/map/OrganisationPseudoTokenMapperTest.java b/src/test/java/nl/ictu/service/map/OrganisationPseudoTokenMapperTest.java index 4b96789..6f0bcde 100644 --- a/src/test/java/nl/ictu/service/map/OrganisationPseudoTokenMapperTest.java +++ b/src/test/java/nl/ictu/service/map/OrganisationPseudoTokenMapperTest.java @@ -13,6 +13,7 @@ import nl.ictu.model.Token; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeTokenResponse; import org.bouncycastle.crypto.InvalidCipherTextException; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -31,44 +32,60 @@ class OrganisationPseudoTokenMapperTest { private OrganisationPseudoTokenMapper organisationPseudoTokenMapper; @Test - void map_ShouldReturnEncryptedTokenResponse_WhenEncryptionSucceeds() throws Exception { + @DisplayName(""" + Given a valid token and caller OIN + When encryption succeeds + Then the response should contain the encrypted identifier + """) + void map_WhenEncryptionSucceeds_ShouldReturnEncryptedTokenResponse() throws Exception { // GIVEN - String callerOIN = "TEST_OIN"; - Token token = Token.builder().bsn("123456789").build(); - String encryptedValue = "encryptedBSN"; - // We mock the cryptographer to return a known "encrypted" value + final String callerOIN = "TEST_OIN"; + final Token token = Token.builder().bsn("123456789").build(); + final String encryptedValue = "encryptedBSN"; when(aesGcmSivCryptographer.encrypt(any(Identifier.class), eq(callerOIN))) .thenReturn(encryptedValue); // WHEN WsExchangeTokenResponse response = organisationPseudoTokenMapper.map(callerOIN, token); // THEN - assertEquals(ORGANISATION_PSEUDO, response.getIdentifier().getType()); - assertEquals(encryptedValue, response.getIdentifier().getValue()); + assertEquals(ORGANISATION_PSEUDO, response.getIdentifier().getType(), + "The identifier type should be ORGANISATION_PSEUDO"); + assertEquals(encryptedValue, response.getIdentifier().getValue(), + "The identifier value should match the encrypted BSN"); } @Test - void map_ShouldThrowInvalidCipherTextException_WhenEncryptionFails() throws Exception { + @DisplayName(""" + Given a valid token and caller OIN + When encryption fails with InvalidCipherTextException + Then an InvalidCipherTextException should be thrown + """) + void map_WhenEncryptionFails_ShouldThrowInvalidCipherTextException() throws Exception { // GIVEN - String callerOIN = "FAILING_OIN"; - Token token = Token.builder().bsn("987654321").build(); - // We mock an InvalidCipherTextException + final String callerOIN = "FAILING_OIN"; + final Token token = Token.builder().bsn("987654321").build(); when(aesGcmSivCryptographer.encrypt(any(Identifier.class), eq(callerOIN))) .thenThrow(new InvalidCipherTextException("Simulated cipher error")); // WHEN & THEN assertThrows(InvalidCipherTextException.class, - () -> organisationPseudoTokenMapper.map(callerOIN, token)); + () -> organisationPseudoTokenMapper.map(callerOIN, token), + "Expected InvalidCipherTextException to be thrown"); } @Test - void map_ShouldThrowIOException_WhenEncryptionThrowsIOException() throws Exception { + @DisplayName(""" + Given a valid token and caller OIN + When encryption fails with IOException + Then an IOException should be thrown + """) + void map_WhenEncryptionThrowsIOException_ShouldThrowIOException() throws Exception { // GIVEN - String callerOIN = "IO_EXCEPTION_OIN"; - Token token = Token.builder().bsn("555555555").build(); - // We mock an IOException + final String callerOIN = "IO_EXCEPTION_OIN"; + final Token token = Token.builder().bsn("555555555").build(); when(aesGcmSivCryptographer.encrypt(any(Identifier.class), eq(callerOIN))) .thenThrow(new IOException("Simulated I/O error")); // WHEN & THEN assertThrows(IOException.class, - () -> organisationPseudoTokenMapper.map(callerOIN, token)); + () -> organisationPseudoTokenMapper.map(callerOIN, token), + "Expected IOException to be thrown"); } } diff --git a/src/test/java/nl/ictu/service/map/PseudoBsnMapperTest.java b/src/test/java/nl/ictu/service/map/PseudoBsnMapperTest.java index 6406ed5..c479f9f 100644 --- a/src/test/java/nl/ictu/service/map/PseudoBsnMapperTest.java +++ b/src/test/java/nl/ictu/service/map/PseudoBsnMapperTest.java @@ -8,6 +8,7 @@ import nl.ictu.crypto.AesGcmSivCryptographer; import nl.ictu.model.Identifier; import nl.ictu.pseudoniemenservice.generated.server.model.WsExchangeIdentifierResponse; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -26,7 +27,12 @@ class PseudoBsnMapperTest { private PseudoBsnMapper pseudoBsnMapper; @Test - void map_ShouldReturnDecryptedBsn_WhenDecryptionSucceeds() throws Exception { + @DisplayName(""" + Given a valid pseudo and OIN + When decryption succeeds + Then the response should contain the decrypted BSN + """) + void map_WhenDecryptionSucceeds_ShouldReturnDecryptedBsn() throws Exception { // GIVEN String pseudo = "someEncryptedString"; String oin = "TEST_OIN"; diff --git a/src/test/java/nl/ictu/service/map/WsGetTokenResponseMapperTest.java b/src/test/java/nl/ictu/service/map/WsGetTokenResponseMapperTest.java index 4d4a145..dd25638 100644 --- a/src/test/java/nl/ictu/service/map/WsGetTokenResponseMapperTest.java +++ b/src/test/java/nl/ictu/service/map/WsGetTokenResponseMapperTest.java @@ -25,17 +25,23 @@ class WsGetTokenResponseMapperTest { private final long creationDate = System.currentTimeMillis(); private final String recipientOIN = "123456789"; private final String encodedToken = "encoded-token"; + @Mock private AesGcmCryptographer aesGcmCryptographer; + @Mock private TokenCoder tokenCoder; + @InjectMocks private WsGetTokenResponseMapper wsGetTokenResponseMapper; @Test - @DisplayName("map() -> Successfully maps parameters to WsGetTokenResponse") + @DisplayName(""" + Given a valid bsn, creation date, and recipient OIN + When token encoding and encryption succeed + Then the response should contain the encrypted token + """) void testMap_Success() throws Exception { - final var encryptedToken = "encrypted-token"; // GIVEN Token token = Token.builder() @@ -55,7 +61,11 @@ void testMap_Success() throws Exception { } @Test - @DisplayName("map() -> Fails due to IOException during token encoding") + @DisplayName(""" + Given a valid bsn, creation date, and recipient OIN + When token encoding fails with IOException + Then an IOException should be thrown + """) void testMap_EncodingIOException() throws Exception { // GIVEN Token token = Token.builder() @@ -73,7 +83,11 @@ void testMap_EncodingIOException() throws Exception { } @Test - @DisplayName("map() -> Fails due to encryption errors") + @DisplayName(""" + Given a valid bsn, creation date, and recipient OIN + When encryption fails with InvalidKeyException + Then an InvalidKeyException should be thrown + """) void testMap_EncryptionError() throws Exception { // GIVEN Token token = Token.builder() @@ -93,7 +107,11 @@ void testMap_EncryptionError() throws Exception { } @Test - @DisplayName("map() -> Fails due to unexpected runtime exceptions") + @DisplayName(""" + Given a valid bsn, creation date, and recipient OIN + When encryption fails with a runtime exception + Then a RuntimeException should be thrown + """) void testMap_UnexpectedError() throws Exception { // GIVEN Token token = Token.builder() diff --git a/src/test/java/nl/ictu/service/map/WsIdentifierOinBsnMapperTest.java b/src/test/java/nl/ictu/service/map/WsIdentifierOinBsnMapperTest.java index f66f8a9..e7f125e 100644 --- a/src/test/java/nl/ictu/service/map/WsIdentifierOinBsnMapperTest.java +++ b/src/test/java/nl/ictu/service/map/WsIdentifierOinBsnMapperTest.java @@ -20,11 +20,16 @@ class WsIdentifierOinBsnMapperTest { @Mock private EncryptedBsnMapper encryptedBsnMapper; + @InjectMocks private WsIdentifierOinBsnMapper wsIdentifierOinBsnMapper; @Test - @DisplayName("map() -> Returns BSN value directly for BSN type") + @DisplayName(""" + Given a WsIdentifier of type BSN with a BSN value + When the map() method is called + Then the BSN value should be returned directly + """) void testMap_BsnType() { // GIVEN String bsnValue = "987654321"; @@ -37,7 +42,11 @@ void testMap_BsnType() { } @Test - @DisplayName("map() -> Returns encrypted value for ORGANISATION_PSEUDO type") + @DisplayName(""" + Given a WsIdentifier of type ORGANISATION_PSEUDO with a BSN value + When the map() method is called + Then the encrypted value should be returned + """) void testMap_OrganisationPseudoType() { // GIVEN String bsnValue = "987654321"; @@ -51,7 +60,6 @@ void testMap_OrganisationPseudoType() { String result = wsIdentifierOinBsnMapper.map(identifier, recipientOIN); // THEN assertEquals(encryptedValue, result); - verify(encryptedBsnMapper).map(bsnValue, - recipientOIN); // Ensure EncryptedBsnMapper is called + verify(encryptedBsnMapper).map(bsnValue, recipientOIN); // Ensure EncryptedBsnMapper is called } } diff --git a/src/test/java/nl/ictu/service/validate/OINValidatorTest.java b/src/test/java/nl/ictu/service/validate/OINValidatorTest.java index 448ad3d..d96dc7a 100644 --- a/src/test/java/nl/ictu/service/validate/OINValidatorTest.java +++ b/src/test/java/nl/ictu/service/validate/OINValidatorTest.java @@ -14,12 +14,15 @@ class OINValidatorTest { @BeforeEach void setUp() { - oinValidator = new OINValidator(); } @Test - @DisplayName("isValid() returns true when callerOIN matches token's recipientOIN") + @DisplayName(""" + Given a caller OIN and a token with matching recipientOIN + When isValid() is called + Then it should return true + """) void isValid_ReturnsTrue_WhenOINsMatch() { // GIVEN String callerOIN = "TEST_OIN_123"; @@ -31,7 +34,11 @@ void isValid_ReturnsTrue_WhenOINsMatch() { } @Test - @DisplayName("isValid() returns false when callerOIN does not match token's recipientOIN") + @DisplayName(""" + Given a caller OIN and a token with non-matching recipientOIN + When isValid() is called + Then it should return false + """) void isValid_ReturnsFalse_WhenOINsDoNotMatch() { // GIVEN String callerOIN = "TEST_OIN_ABC"; @@ -42,16 +49,17 @@ void isValid_ReturnsFalse_WhenOINsDoNotMatch() { assertFalse(result, "Expected isValid() to return false for non-matching OINs"); } - // OPTIONAL: If you want to test null behavior @Test - @DisplayName("isValid() returns false or throws if token recipientOIN is null") + @DisplayName(""" + Given a caller OIN and a token with a null recipientOIN + When isValid() is called + Then it should return false + """) void isValid_Behavior_WhenTokenRecipientOINIsNull() { // GIVEN String callerOIN = "NON_NULL_OIN"; Token token = Token.builder().build(); // WHEN - // This will return false if callerOIN is not null, - // or might throw NullPointerException if you rely on the equals contract boolean result = oinValidator.isValid(callerOIN, token); // THEN assertFalse(result, "Expected isValid() to return false if token's recipientOIN is null"); diff --git a/src/test/java/nl/ictu/utils/AESHelperTest.java b/src/test/java/nl/ictu/utils/AESHelperTest.java index ebd0201..8d146ae 100644 --- a/src/test/java/nl/ictu/utils/AESHelperTest.java +++ b/src/test/java/nl/ictu/utils/AESHelperTest.java @@ -11,11 +11,17 @@ import javax.crypto.spec.GCMParameterSpec; import org.bouncycastle.crypto.MultiBlockCipher; import org.bouncycastle.crypto.engines.AESEngine; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; class AESHelperTest { @Test + @DisplayName(""" + Given no input + When generating an IV using AesUtility.generateIV() + Then a non-null GCMParameterSpec should be returned with the correct IV length and tag length + """) void generateIV_ShouldReturnGCMParameterSpec_WithNonNullIV() { // WHEN GCMParameterSpec gcmParameterSpec = AesUtility.generateIV(); @@ -31,6 +37,11 @@ void generateIV_ShouldReturnGCMParameterSpec_WithNonNullIV() { } @Test + @DisplayName(""" + Given a byte array of IV values + When creating a GCMParameterSpec using AesUtility.createIVfromValues() + Then the resulting GCMParameterSpec should match the input IV values + """) void createIVfromValues_ShouldReturnGCMParameterSpec_FromGivenIV() { // GIVEN byte[] ivSource = new byte[AesUtility.IV_LENGTH]; @@ -49,6 +60,11 @@ void createIVfromValues_ShouldReturnGCMParameterSpec_FromGivenIV() { } @Test + @DisplayName(""" + Given no input + When creating a Cipher instance using AesUtility.createCipher() + Then the resulting Cipher should be of type AES/GCM/NoPadding + """) void createCipher_ShouldReturnAesGcmNoPaddingCipher() throws NoSuchPaddingException, NoSuchAlgorithmException { // WHEN @@ -62,6 +78,11 @@ void createCipher_ShouldReturnAesGcmNoPaddingCipher() } @Test + @DisplayName(""" + Given no input + When retrieving the AES engine using AesUtility.getAESEngine() + Then the resulting engine should be an instance of AESEngine + """) void getAESEngine_ShouldReturnNonNullAESEngineInstance() { // WHEN MultiBlockCipher engine = AesUtility.getAESEngine(); diff --git a/src/test/java/nl/ictu/utils/AesUtilityTest.java b/src/test/java/nl/ictu/utils/AesUtilityTest.java index 0773060..ab461ce 100644 --- a/src/test/java/nl/ictu/utils/AesUtilityTest.java +++ b/src/test/java/nl/ictu/utils/AesUtilityTest.java @@ -17,7 +17,11 @@ class AesUtilityTest { @Test - @DisplayName("generateIV() -> should return GCMParameterSpec with correct IV & tag length") + @DisplayName(""" + Given no input + When generating an IV using AesUtility.generateIV() + Then a non-null GCMParameterSpec should be returned with the correct IV length and tag length + """) void generateIV_ShouldReturnGCMParameterSpec() { // WHEN GCMParameterSpec spec = AesUtility.generateIV(); @@ -31,7 +35,11 @@ void generateIV_ShouldReturnGCMParameterSpec() { } @Test - @DisplayName("createIVfromValues() -> should build GCMParameterSpec from provided IV") + @DisplayName(""" + Given a byte array of IV values + When creating a GCMParameterSpec using AesUtility.createIVfromValues() + Then the resulting GCMParameterSpec should match the input IV values + """) void createIVfromValues_ShouldReturnGCMParameterSpecFromGivenIV() { // GIVEN: a deterministic IV of length 12 byte[] ivBytes = new byte[AesUtility.IV_LENGTH]; @@ -49,7 +57,11 @@ void createIVfromValues_ShouldReturnGCMParameterSpecFromGivenIV() { } @Test - @DisplayName("createCipher() -> should return Cipher for AES/GCM/NoPadding") + @DisplayName(""" + Given no input + When creating a Cipher instance using AesUtility.createCipher() + Then the resulting Cipher should be of type AES/GCM/NoPadding + """) void createCipher_ShouldReturnAesGcmNoPadding() throws NoSuchPaddingException, NoSuchAlgorithmException { // WHEN @@ -61,7 +73,11 @@ void createCipher_ShouldReturnAesGcmNoPadding() } @Test - @DisplayName("getAESEngine() -> should return instance of AESEngine") + @DisplayName(""" + Given no input + When retrieving the AES engine using AesUtility.getAESEngine() + Then the resulting engine should be an instance of AESEngine + """) void getAESEngine_ShouldReturnAesEngine() { // WHEN MultiBlockCipher engine = AesUtility.getAESEngine(); diff --git a/src/test/java/nl/ictu/utils/Base64WrapperTest.java b/src/test/java/nl/ictu/utils/Base64WrapperTest.java index f4accbe..cdf5c9d 100644 --- a/src/test/java/nl/ictu/utils/Base64WrapperTest.java +++ b/src/test/java/nl/ictu/utils/Base64WrapperTest.java @@ -14,12 +14,15 @@ class Base64WrapperTest { @BeforeEach void setUp() { - base64Wrapper = new Base64Wrapper(); } @Test - @DisplayName("encode() -> Should encode bytes to Base64 bytes") + @DisplayName(""" + Given a byte array of the string "Hello" + When encoding the byte array using Base64Wrapper.encode() + Then the result should be the Base64-encoded string "SGVsbG8=" + """) void encode_ShouldEncodeBytesToBase64Bytes() { // GIVEN byte[] input = "Hello".getBytes(StandardCharsets.UTF_8); @@ -32,7 +35,11 @@ void encode_ShouldEncodeBytesToBase64Bytes() { } @Test - @DisplayName("encodeToString() -> Should encode bytes to Base64 string") + @DisplayName(""" + Given a byte array of the string "Hello" + When encoding the byte array using Base64Wrapper.encodeToString() + Then the result should be the Base64-encoded string "SGVsbG8=" + """) void encodeToString_ShouldEncodeBytesToBase64String() { // GIVEN byte[] input = "Hello".getBytes(StandardCharsets.UTF_8); @@ -44,7 +51,11 @@ void encodeToString_ShouldEncodeBytesToBase64String() { } @Test - @DisplayName("decode() -> Should decode Base64 string to bytes") + @DisplayName(""" + Given a Base64-encoded string "SGVsbG8=" + When decoding the string using Base64Wrapper.decode() + Then the result should be the decoded byte array representing "Hello" + """) void decode_ShouldDecodeBase64StringToBytes() { // GIVEN String base64String = "SGVsbG8="; @@ -57,12 +68,15 @@ void decode_ShouldDecodeBase64StringToBytes() { } @Test - @DisplayName("decode() -> Should throw IllegalArgumentException on invalid Base64") + @DisplayName(""" + Given an invalid Base64 string "Not valid base64!!!" + When attempting to decode using Base64Wrapper.decode() + Then an IllegalArgumentException should be thrown + """) void decode_ShouldThrowException_WhenInvalidBase64String() { // GIVEN String invalidBase64 = "Not valid base64!!!"; // WHEN & THEN - // Base64.getDecoder().decode(...) throws IllegalArgumentException on invalid input assertThrows(IllegalArgumentException.class, () -> base64Wrapper.decode(invalidBase64), "Expected decode() to throw IllegalArgumentException for invalid Base64 string"); diff --git a/src/test/java/nl/ictu/utils/ByteArrayUtilsTest.java b/src/test/java/nl/ictu/utils/ByteArrayUtilsTest.java index ffee23a..ddec53f 100644 --- a/src/test/java/nl/ictu/utils/ByteArrayUtilsTest.java +++ b/src/test/java/nl/ictu/utils/ByteArrayUtilsTest.java @@ -8,7 +8,11 @@ class ByteArrayUtilsTest { @Test - @DisplayName("concat() -> should concatenate two non-empty arrays") + @DisplayName(""" + Given two non-empty byte arrays [1, 2, 3] and [4, 5, 6] + When concatenating the arrays using ByteArrayUtil.concat() + Then the result should be a single byte array [1, 2, 3, 4, 5, 6] + """) void concat_ShouldConcatenateTwoArrays() { // GIVEN byte[] a = {1, 2, 3}; @@ -21,7 +25,11 @@ void concat_ShouldConcatenateTwoArrays() { } @Test - @DisplayName("concat() -> should return empty array if both inputs are empty") + @DisplayName(""" + Given two empty byte arrays + When concatenating the arrays using ByteArrayUtil.concat() + Then the result should be an empty byte array + """) void concat_ShouldHandleTwoEmptyArrays() { // GIVEN byte[] a = {}; @@ -34,7 +42,11 @@ void concat_ShouldHandleTwoEmptyArrays() { } @Test - @DisplayName("concat() -> should handle empty array on either side") + @DisplayName(""" + Given one empty byte array and one non-empty byte array + When concatenating the arrays using ByteArrayUtil.concat() + Then the result should be the non-empty array + """) void concat_ShouldHandleOneEmptyArray() { // GIVEN byte[] a = {1, 2, 3}; diff --git a/src/test/java/nl/ictu/utils/MessageDigestWrapperTest.java b/src/test/java/nl/ictu/utils/MessageDigestWrapperTest.java index 0f5517c..69130b1 100644 --- a/src/test/java/nl/ictu/utils/MessageDigestWrapperTest.java +++ b/src/test/java/nl/ictu/utils/MessageDigestWrapperTest.java @@ -5,6 +5,7 @@ import java.security.MessageDigest; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; class MessageDigestWrapperTest { @@ -13,11 +14,15 @@ class MessageDigestWrapperTest { @BeforeEach void setUp() { - messageDigestWrapper = new MessageDigestWrapper(); } @Test + @DisplayName(""" + Given a MessageDigestWrapper instance + When calling getMessageDigestInstance() + Then the resulting MessageDigest should be SHA-256 + """) void getMessageDigestSha256_ShouldReturnSha256Digest() { // WHEN MessageDigest digest = messageDigestWrapper.getMessageDigestInstance(); From 1c73c8c2ea7ec0f9b6408506e2baf374fcc99b50 Mon Sep 17 00:00:00 2001 From: sayf Date: Fri, 3 Jan 2025 20:46:59 +0100 Subject: [PATCH 28/30] Uniformity --- .../ictu/crypto/AesGcmSivCryptographer.java | 4 +- .../PseudoniemenServicePropertiesTest.java | 6 +- .../v1/ExchangeIdentifierControllerTest.java | 18 +++-- .../v1/ExchangeTokenControllerTest.java | 27 ++++---- .../ExchangeIdentifierServiceTest.java | 65 ++++++++++++------- .../service/ExchangeTokenServiceTest.java | 37 +++++++---- .../nl/ictu/service/GetTokenServiceTest.java | 21 +++--- .../ictu/service/TestAesGcmCryptographer.java | 10 +-- .../service/TestAesGcmSivCryptographer.java | 4 +- .../ictu/service/map/BsnPseudoMapperTest.java | 16 ++--- .../ictu/service/map/BsnTokenMapperTest.java | 4 +- .../service/map/EncryptedBsnMapperTest.java | 6 +- .../OrganisationPseudoTokenMapperTest.java | 16 ++--- .../ictu/service/map/PseudoBsnMapperTest.java | 6 +- .../map/WsGetTokenResponseMapperTest.java | 10 +-- .../map/WsIdentifierOinBsnMapperTest.java | 14 ++-- .../service/validate/OINValidatorTest.java | 23 ++++--- .../java/nl/ictu/utils/AESHelperTest.java | 12 ++-- .../java/nl/ictu/utils/AesUtilityTest.java | 10 +-- .../java/nl/ictu/utils/Base64WrapperTest.java | 18 ++--- .../ictu/utils/MessageDigestWrapperTest.java | 2 +- 21 files changed, 183 insertions(+), 146 deletions(-) diff --git a/src/main/java/nl/ictu/crypto/AesGcmSivCryptographer.java b/src/main/java/nl/ictu/crypto/AesGcmSivCryptographer.java index 2b7e194..bd4b4ae 100644 --- a/src/main/java/nl/ictu/crypto/AesGcmSivCryptographer.java +++ b/src/main/java/nl/ictu/crypto/AesGcmSivCryptographer.java @@ -44,8 +44,8 @@ private AEADParameters createSecretKey(final String salt) { final var nonce16 = messageDigestWrapper.getMessageDigestInstance() .digest(salt.getBytes(StandardCharsets.UTF_8)); final var nonce12 = Arrays.copyOf(nonce16, NONCE_LENTH); - final String identifierPrivateKey = pseudoniemenServiceProperties.getIdentifierPrivateKey(); - final KeyParameter keyParameter = new KeyParameter( + final var identifierPrivateKey = pseudoniemenServiceProperties.getIdentifierPrivateKey(); + final var keyParameter = new KeyParameter( base64Wrapper.decode(identifierPrivateKey)); return new AEADParameters(keyParameter, MAC_SIZE, nonce12); } diff --git a/src/test/java/nl/ictu/configuration/PseudoniemenServicePropertiesTest.java b/src/test/java/nl/ictu/configuration/PseudoniemenServicePropertiesTest.java index acd6080..19a73ae 100644 --- a/src/test/java/nl/ictu/configuration/PseudoniemenServicePropertiesTest.java +++ b/src/test/java/nl/ictu/configuration/PseudoniemenServicePropertiesTest.java @@ -18,7 +18,7 @@ class PseudoniemenServicePropertiesTest { """) void validate_WhenTokenPrivateKeyIsEmpty_ThrowsTokenPrivateKeyException() { // GIVEN - PseudoniemenServiceProperties props = new PseudoniemenServiceProperties() + final var props = new PseudoniemenServiceProperties() .setTokenPrivateKey("") .setIdentifierPrivateKey("someIdentifierKey"); // WHEN & THEN @@ -32,7 +32,7 @@ void validate_WhenTokenPrivateKeyIsEmpty_ThrowsTokenPrivateKeyException() { """) void validate_WhenIdentifierPrivateKeyIsEmpty_ThrowsIdentifierPrivateKeyException() { // GIVEN - PseudoniemenServiceProperties props = new PseudoniemenServiceProperties() + final var props = new PseudoniemenServiceProperties() .setTokenPrivateKey("someTokenKey") .setIdentifierPrivateKey(""); // WHEN & THEN @@ -46,7 +46,7 @@ void validate_WhenIdentifierPrivateKeyIsEmpty_ThrowsIdentifierPrivateKeyExceptio """) void validate_WhenBothKeysAreSet_NoExceptionIsThrown() { // GIVEN - PseudoniemenServiceProperties props = new PseudoniemenServiceProperties() + final var props = new PseudoniemenServiceProperties() .setTokenPrivateKey("someTokenKey") .setIdentifierPrivateKey("someIdentifierKey"); // WHEN & THEN diff --git a/src/test/java/nl/ictu/controller/v1/ExchangeIdentifierControllerTest.java b/src/test/java/nl/ictu/controller/v1/ExchangeIdentifierControllerTest.java index efa139a..53770ee 100644 --- a/src/test/java/nl/ictu/controller/v1/ExchangeIdentifierControllerTest.java +++ b/src/test/java/nl/ictu/controller/v1/ExchangeIdentifierControllerTest.java @@ -32,13 +32,12 @@ When calling exchangeIdentifier() """) void testExchangeIdentifier_Success() { // GIVEN - String callerOIN = "123456789"; - WsExchangeIdentifierRequest request = new WsExchangeIdentifierRequest(); - WsExchangeIdentifierResponse expectedResponse = new WsExchangeIdentifierResponse(); + final var callerOIN = "123456789"; + final var request = new WsExchangeIdentifierRequest(); + final var expectedResponse = new WsExchangeIdentifierResponse(); when(service.exchangeIdentifier(request)).thenReturn(expectedResponse); // WHEN - ResponseEntity response = - controller.exchangeIdentifier(callerOIN, request); + final var response = controller.exchangeIdentifier(callerOIN, request); // THEN assertEquals(ResponseEntity.ok(expectedResponse), response); verify(service).exchangeIdentifier(request); // Ensure service method is called @@ -52,13 +51,12 @@ When calling exchangeIdentifier() """) void testExchangeIdentifier_ServiceThrowsException() { // GIVEN - String callerOIN = "123456789"; - WsExchangeIdentifierRequest request = new WsExchangeIdentifierRequest(); - RuntimeException exception = new RuntimeException("Service error"); + final var callerOIN = "123456789"; + final var request = new WsExchangeIdentifierRequest(); + final var exception = new RuntimeException("Service error"); when(service.exchangeIdentifier(request)).thenThrow(exception); // WHEN & THEN - RuntimeException thrownException = - assertThrows(RuntimeException.class, + final var thrownException = assertThrows(RuntimeException.class, () -> controller.exchangeIdentifier(callerOIN, request)); assertEquals("Service error", thrownException.getMessage()); verify(service).exchangeIdentifier(request); // Ensure service method is called diff --git a/src/test/java/nl/ictu/controller/v1/ExchangeTokenControllerTest.java b/src/test/java/nl/ictu/controller/v1/ExchangeTokenControllerTest.java index 685ec2c..be523e8 100644 --- a/src/test/java/nl/ictu/controller/v1/ExchangeTokenControllerTest.java +++ b/src/test/java/nl/ictu/controller/v1/ExchangeTokenControllerTest.java @@ -42,11 +42,12 @@ When calling exchangeToken() """) void exchangeToken_ShouldReturnOk() throws Exception { // GIVEN: a request payload - WsExchangeTokenRequest requestPayload = new WsExchangeTokenRequest(); - requestPayload.setToken("testToken"); - requestPayload.setIdentifierType(WsIdentifierTypes.BSN); + final var requestPayload = WsExchangeTokenRequest.builder() + .token("testToken") + .identifierType(WsIdentifierTypes.BSN) + .build(); // AND: a mock service response - WsExchangeTokenResponse responsePayload = new WsExchangeTokenResponse(); + final var responsePayload = new WsExchangeTokenResponse(); responsePayload.setIdentifier(WsIdentifier.builder() .type(WsIdentifierTypes.BSN) .value("convertedIdentifier") @@ -55,12 +56,10 @@ void exchangeToken_ShouldReturnOk() throws Exception { when(exchangeTokenService.exchangeToken(eq("TEST_OIN"), any(WsExchangeTokenRequest.class))) .thenReturn(responsePayload); // THEN: perform the POST request - mockMvc.perform( - post("/v1/exchangeToken") + mockMvc.perform(post("/v1/exchangeToken") .header("callerOIN", "TEST_OIN") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(requestPayload)) - ) + .content(objectMapper.writeValueAsString(requestPayload))) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.identifier.value").value("convertedIdentifier")) @@ -75,20 +74,18 @@ When calling exchangeToken() """) void exchangeToken_ShouldReturnUnprocessableEntity() throws Exception { // GIVEN: a request payload - WsExchangeTokenRequest requestPayload = new WsExchangeTokenRequest(); - requestPayload.setToken("testToken"); - requestPayload.setIdentifierType(WsIdentifierTypes.ORGANISATION_PSEUDO); + final var requestPayload = WsExchangeTokenRequest.builder() + .token("testToken").identifierType(WsIdentifierTypes.ORGANISATION_PSEUDO) + .build(); // WHEN: the service throws an exception doThrow(new InvalidOINException("Service error")) .when(exchangeTokenService) .exchangeToken(eq("FAIL_OIN"), any(WsExchangeTokenRequest.class)); // THEN: perform the POST request - mockMvc.perform( - post("/v1/exchangeToken") + mockMvc.perform(post("/v1/exchangeToken") .header("callerOIN", "FAIL_OIN") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(requestPayload)) - ) + .content(objectMapper.writeValueAsString(requestPayload))) .andExpect(status().isUnprocessableEntity()); } } diff --git a/src/test/java/nl/ictu/service/ExchangeIdentifierServiceTest.java b/src/test/java/nl/ictu/service/ExchangeIdentifierServiceTest.java index 227e1f7..8c1e5ef 100644 --- a/src/test/java/nl/ictu/service/ExchangeIdentifierServiceTest.java +++ b/src/test/java/nl/ictu/service/ExchangeIdentifierServiceTest.java @@ -2,6 +2,8 @@ import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.when; @@ -39,24 +41,31 @@ When exchangeIdentifier() is called """) void testExchangeIdentifier_BsnToOrgPseudo() throws Exception { // GIVEN - var request = new WsExchangeIdentifierRequest() - .identifier(new WsIdentifier().type(BSN).value("123456789")) + var request = WsExchangeIdentifierRequest.builder() + .identifier(WsIdentifier.builder() + .type(BSN) + .value("123456789") + .build()) .recipientOIN("TEST_OIN") - .recipientIdentifierType(ORGANISATION_PSEUDO); + .recipientIdentifierType(ORGANISATION_PSEUDO) + .build(); // We mock BsnPseudoMapper to return a WsExchangeIdentifierResponse - var mockedResponse = new WsExchangeIdentifierResponse(); - mockedResponse.setIdentifier( - new WsIdentifier().type(ORGANISATION_PSEUDO).value("encryptedValue")); + final var mockedResponse = WsExchangeIdentifierResponse.builder() + .identifier(WsIdentifier.builder() + .type(ORGANISATION_PSEUDO) + .value("encryptedValue") + .build()) + .build(); when(bsnPseudoMapper.map("123456789", "TEST_OIN")).thenReturn(mockedResponse); // WHEN WsExchangeIdentifierResponse actualResponse = exchangeIdentifierService.exchangeIdentifier(request); // THEN - org.junit.jupiter.api.Assertions.assertNotNull(actualResponse); - org.junit.jupiter.api.Assertions.assertNotNull(actualResponse.getIdentifier()); - org.junit.jupiter.api.Assertions.assertEquals(ORGANISATION_PSEUDO, + assertNotNull(actualResponse); + assertNotNull(actualResponse.getIdentifier()); + assertEquals(ORGANISATION_PSEUDO, actualResponse.getIdentifier().getType()); - org.junit.jupiter.api.Assertions.assertEquals("encryptedValue", + assertEquals("encryptedValue", actualResponse.getIdentifier().getValue()); } @@ -68,23 +77,31 @@ When exchangeIdentifier() is called """) void testExchangeIdentifier_OrgPseudoToBsn() throws Exception { // GIVEN - var request = new WsExchangeIdentifierRequest() - .identifier(new WsIdentifier().type(ORGANISATION_PSEUDO).value("somePseudo")) + final var request = WsExchangeIdentifierRequest.builder() + .identifier(WsIdentifier.builder() + .type(ORGANISATION_PSEUDO) + .value("somePseudo") + .build()) .recipientOIN("TEST_OIN") - .recipientIdentifierType(BSN); + .recipientIdentifierType(BSN) + .build(); // We mock PseudoBsnMapper to return a WsExchangeIdentifierResponse - var mockedResponse = new WsExchangeIdentifierResponse(); - mockedResponse.setIdentifier(new WsIdentifier().type(BSN).value("decryptedBsn")); + final var mockedResponse = WsExchangeIdentifierResponse.builder() + .identifier(WsIdentifier.builder() + .type(BSN) + .value("decryptedBsn") + .build()) + .build(); when(pseudoBsnMapper.map("somePseudo", "TEST_OIN")).thenReturn(mockedResponse); // WHEN WsExchangeIdentifierResponse actualResponse = exchangeIdentifierService.exchangeIdentifier(request); // THEN - org.junit.jupiter.api.Assertions.assertNotNull(actualResponse); - org.junit.jupiter.api.Assertions.assertNotNull(actualResponse.getIdentifier()); - org.junit.jupiter.api.Assertions.assertEquals(BSN, + assertNotNull(actualResponse); + assertNotNull(actualResponse.getIdentifier()); + assertEquals(BSN, actualResponse.getIdentifier().getType()); - org.junit.jupiter.api.Assertions.assertEquals("decryptedBsn", + assertEquals("decryptedBsn", actualResponse.getIdentifier().getValue()); } @@ -96,11 +113,15 @@ When exchangeIdentifier() is called """) void testExchangeIdentifier_UnsupportedMapping_ThrowsException() { // GIVEN - var request = new WsExchangeIdentifierRequest() + final var request = WsExchangeIdentifierRequest.builder() // Let's say we do something like BSN -> BSN or ORG_PSEUDO -> ORG_PSEUDO - .identifier(new WsIdentifier().type(BSN).value("12345")) + .identifier(WsIdentifier.builder() + .type(BSN) + .value("12345") + .build()) .recipientOIN("TEST_OIN") - .recipientIdentifierType(BSN); + .recipientIdentifierType(BSN) + .build(); // WHEN & THEN assertThrows( InvalidWsIdentifierRequestTypeException.class, diff --git a/src/test/java/nl/ictu/service/ExchangeTokenServiceTest.java b/src/test/java/nl/ictu/service/ExchangeTokenServiceTest.java index 82c8b20..7b564a9 100644 --- a/src/test/java/nl/ictu/service/ExchangeTokenServiceTest.java +++ b/src/test/java/nl/ictu/service/ExchangeTokenServiceTest.java @@ -2,7 +2,9 @@ import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.BSN; import static nl.ictu.pseudoniemenservice.generated.server.model.WsIdentifierTypes.ORGANISATION_PSEUDO; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @@ -48,9 +50,11 @@ class ExchangeTokenServiceTest { @BeforeEach void setUp() { - mockToken = Token.builder().build(); - mockToken.setRecipientOIN(callerOIN); - mockToken.setBsn("987654321"); + + mockToken = Token.builder() + .recipientOIN(callerOIN) + .bsn("987654321") + .build(); } @Test @@ -61,21 +65,24 @@ When exchangeToken() is called """) void testExchangeToken_BsnIdentifier() throws Exception { // GIVEN - var request = new WsExchangeTokenRequest().token(encryptedToken).identifierType(BSN); + final var request = WsExchangeTokenRequest.builder() + .token(encryptedToken) + .identifierType(BSN) + .build(); // Stubbing dependencies when(aesGcmCryptographer.decrypt(encryptedToken, callerOIN)).thenReturn(decodedToken); when(tokenCoder.decode(decodedToken)).thenReturn(mockToken); when(oinValidator.isValid(callerOIN, mockToken)).thenReturn(true); - var expectedResponse = new WsExchangeTokenResponse(); + var expectedResponse = mock(WsExchangeTokenResponse.class); when(bsnTokenMapper.map(mockToken)).thenReturn(expectedResponse); // WHEN - WsExchangeTokenResponse actualResponse = exchangeTokenService.exchangeToken(callerOIN, request); + final var actualResponse = exchangeTokenService.exchangeToken(callerOIN, request); // THEN verify(aesGcmCryptographer).decrypt(encryptedToken, callerOIN); verify(tokenCoder).decode(decodedToken); verify(oinValidator).isValid(callerOIN, mockToken); verify(bsnTokenMapper).map(mockToken); - org.junit.jupiter.api.Assertions.assertEquals(expectedResponse, actualResponse); + assertEquals(expectedResponse, actualResponse); } @Test @@ -86,21 +93,24 @@ When exchangeToken() is called """) void testExchangeToken_OrganisationPseudoIdentifier() throws Exception { // GIVEN - var request = new WsExchangeTokenRequest().token(encryptedToken).identifierType(ORGANISATION_PSEUDO); + final var request = WsExchangeTokenRequest.builder() + .token(encryptedToken) + .identifierType(ORGANISATION_PSEUDO) + .build(); // Stubbing dependencies when(aesGcmCryptographer.decrypt(encryptedToken, callerOIN)).thenReturn(decodedToken); when(tokenCoder.decode(decodedToken)).thenReturn(mockToken); when(oinValidator.isValid(callerOIN, mockToken)).thenReturn(true); - var expectedResponse = new WsExchangeTokenResponse(); + final var expectedResponse = mock(WsExchangeTokenResponse.class); when(organisationPseudoTokenMapper.map(callerOIN, mockToken)).thenReturn(expectedResponse); // WHEN - WsExchangeTokenResponse actualResponse = exchangeTokenService.exchangeToken(callerOIN, request); + final var actualResponse = exchangeTokenService.exchangeToken(callerOIN, request); // THEN verify(aesGcmCryptographer).decrypt(encryptedToken, callerOIN); verify(tokenCoder).decode(decodedToken); verify(oinValidator).isValid(callerOIN, mockToken); verify(organisationPseudoTokenMapper).map(callerOIN, mockToken); - org.junit.jupiter.api.Assertions.assertEquals(expectedResponse, actualResponse); + assertEquals(expectedResponse, actualResponse); } @Test @@ -111,7 +121,10 @@ When exchangeToken() is called """) void testExchangeToken_InvalidOIN() throws Exception { // GIVEN - var request = new WsExchangeTokenRequest().token(encryptedToken).identifierType(BSN); + final var request = WsExchangeTokenRequest.builder() + .token(encryptedToken) + .identifierType(BSN) + .build(); // Stubbing dependencies when(aesGcmCryptographer.decrypt(encryptedToken, callerOIN)).thenReturn(decodedToken); when(tokenCoder.decode(decodedToken)).thenReturn(mockToken); diff --git a/src/test/java/nl/ictu/service/GetTokenServiceTest.java b/src/test/java/nl/ictu/service/GetTokenServiceTest.java index 0ebd617..87ef602 100644 --- a/src/test/java/nl/ictu/service/GetTokenServiceTest.java +++ b/src/test/java/nl/ictu/service/GetTokenServiceTest.java @@ -1,8 +1,10 @@ package nl.ictu.service; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @@ -49,23 +51,23 @@ When getWsGetTokenResponse() is called """) void testGetWsGetTokenResponse_ValidInput() throws Exception { // GIVEN - var identifier = WsIdentifier.builder() + final var identifier = WsIdentifier.builder() .type(WsIdentifierTypes.BSN) .value(bsn) .build(); - var expectedResponse = new WsGetTokenResponse(); + final var expectedResponse = mock(WsGetTokenResponse.class); // Stubbing dependencies when(wsIdentifierOinBsnMapper.map(identifier, recipientOIN)).thenReturn(bsn); when(wsGetTokenResponseMapper.map(eq(bsn), anyLong(), eq(recipientOIN))).thenReturn(expectedResponse); // WHEN - WsGetTokenResponse actualResponse = getTokenService.getWsGetTokenResponse(recipientOIN, identifier); + final var actualResponse = getTokenService.getWsGetTokenResponse(recipientOIN, identifier); // THEN verify(wsIdentifierOinBsnMapper).map(identifier, recipientOIN); verify(wsGetTokenResponseMapper).map(eq(bsn), anyLong(), eq(recipientOIN)); - org.junit.jupiter.api.Assertions.assertEquals(expectedResponse, actualResponse); + assertEquals(expectedResponse, actualResponse); } @Test @@ -76,21 +78,22 @@ When getWsGetTokenResponse() is called """) void testGetWsGetTokenResponse_UnexpectedError() { // GIVEN - var identifier = WsIdentifier.builder() + final var identifier = WsIdentifier.builder() .type(WsIdentifierTypes.BSN) .value(bsn) .build(); - var exceptionMessage = "Unexpected processing error"; + final var exceptionMessage = "Unexpected processing error"; // Stubbing dependencies - when(wsIdentifierOinBsnMapper.map(identifier, recipientOIN)).thenThrow(new RuntimeException(exceptionMessage)); + when(wsIdentifierOinBsnMapper.map(identifier, recipientOIN)) + .thenThrow(new RuntimeException(exceptionMessage)); // WHEN & THEN - Exception exception = assertThrows(WsGetTokenProcessingException.class, + final var exception = assertThrows(WsGetTokenProcessingException.class, () -> getTokenService.getWsGetTokenResponse(recipientOIN, identifier)); // Assert exception message - org.junit.jupiter.api.Assertions.assertEquals(exceptionMessage, exception.getMessage()); + assertEquals(exceptionMessage, exception.getMessage()); verify(wsIdentifierOinBsnMapper).map(identifier, recipientOIN); verifyNoInteractions(wsGetTokenResponseMapper); } diff --git a/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java b/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java index 4c370c9..2bc0fcf 100644 --- a/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java +++ b/src/test/java/nl/ictu/service/TestAesGcmCryptographer.java @@ -42,9 +42,9 @@ void testEncyptDecryptForDifferentStringLengths() { testStrings.forEach(plain -> { try { // GIVEN - final String crypted = aesGcmCryptographer.encrypt(plain, "helloHowAreyo12345678"); + final var crypted = aesGcmCryptographer.encrypt(plain, "helloHowAreyo12345678"); // WHEN - final String actual = aesGcmCryptographer.decrypt(crypted, "helloHowAreyo12345678"); + final var actual = aesGcmCryptographer.decrypt(crypted, "helloHowAreyo12345678"); // THEN assertThat(actual).isEqualTo(plain); } catch (final Exception e) { @@ -61,10 +61,10 @@ void testEncyptDecryptForDifferentStringLengths() { """) void testCiphertextIsDifferentForSamePlaintext() throws Exception { // GIVEN - String plaintext = "This is a test message to ensure ciphertext is different!"; + final var plaintext = "This is a test message to ensure ciphertext is different!"; // WHEN - String encryptedMessage1 = aesGcmCryptographer.encrypt(plaintext, "aniceSaltGorYu"); - String encryptedMessage2 = aesGcmCryptographer.encrypt(plaintext, "aniceSaltGorYu"); + final var encryptedMessage1 = aesGcmCryptographer.encrypt(plaintext, "aniceSaltGorYu"); + final var encryptedMessage2 = aesGcmCryptographer.encrypt(plaintext, "aniceSaltGorYu"); // THEN // Assert that the two ciphertexts are different assertThat(encryptedMessage1).isNotEqualTo(encryptedMessage2); diff --git a/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java b/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java index 4c94bbc..81dfc78 100644 --- a/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java +++ b/src/test/java/nl/ictu/service/TestAesGcmSivCryptographer.java @@ -46,12 +46,12 @@ void testEncyptDecryptForDifferentStringLengths() { testStrings.forEach(plain -> { try { // GIVEN - final String crypted = aesGcmSivCryptographer.encrypt(Identifier.builder() + final var crypted = aesGcmSivCryptographer.encrypt(Identifier.builder() .bsn(plain) .build(), "helloHowAreyo12345678"); // WHEN - final Identifier actual = aesGcmSivCryptographer.decrypt(crypted, + final var actual = aesGcmSivCryptographer.decrypt(crypted, "helloHowAreyo12345678"); // THEN assertThat(actual.getBsn()).isEqualTo(plain); diff --git a/src/test/java/nl/ictu/service/map/BsnPseudoMapperTest.java b/src/test/java/nl/ictu/service/map/BsnPseudoMapperTest.java index 84974e5..de699f9 100644 --- a/src/test/java/nl/ictu/service/map/BsnPseudoMapperTest.java +++ b/src/test/java/nl/ictu/service/map/BsnPseudoMapperTest.java @@ -36,13 +36,13 @@ class BsnPseudoMapperTest { """) void map_WhenEncryptionSucceeds_ShouldReturnWsExchangeIdentifierResponse() throws Exception { // GIVEN - String bsn = "123456789"; - String oin = "OIN_X"; - String encryptedValue = "encryptedBsn123"; + final var bsn = "123456789"; + final var oin = "OIN_X"; + final var encryptedValue = "encryptedBsn123"; when(aesGcmSivCryptographer.encrypt(any(Identifier.class), eq(oin))) .thenReturn(encryptedValue); // WHEN - WsExchangeIdentifierResponse response = bsnPseudoMapper.map(bsn, oin); + final var response = bsnPseudoMapper.map(bsn, oin); // THEN assertNotNull(response); assertNotNull(response.getIdentifier()); @@ -58,8 +58,8 @@ void map_WhenEncryptionSucceeds_ShouldReturnWsExchangeIdentifierResponse() throw """) void map_WhenEncryptThrowsIOException_ShouldThrowIOException() throws Exception { // GIVEN - String bsn = "987654321"; - String oin = "OIN_IO"; + final var bsn = "987654321"; + final var oin = "OIN_IO"; when(aesGcmSivCryptographer.encrypt(any(Identifier.class), eq(oin))) .thenThrow(new IOException("Simulated I/O error")); // WHEN & THEN @@ -75,8 +75,8 @@ void map_WhenEncryptThrowsIOException_ShouldThrowIOException() throws Exception void map_WhenEncryptThrowsInvalidCipherTextException_ShouldThrowInvalidCipherTextException() throws Exception { // GIVEN - String bsn = "111222333"; - String oin = "OIN_CIPHER"; + final var bsn = "111222333"; + final var oin = "OIN_CIPHER"; when(aesGcmSivCryptographer.encrypt(any(Identifier.class), eq(oin))) .thenThrow(new InvalidCipherTextException("Simulated cipher error")); // WHEN & THEN diff --git a/src/test/java/nl/ictu/service/map/BsnTokenMapperTest.java b/src/test/java/nl/ictu/service/map/BsnTokenMapperTest.java index 7adcc81..9dc28f1 100644 --- a/src/test/java/nl/ictu/service/map/BsnTokenMapperTest.java +++ b/src/test/java/nl/ictu/service/map/BsnTokenMapperTest.java @@ -33,7 +33,7 @@ void map_WhenTokenHasValidBsn_ShouldReturnResponseWithBsnIdentifier() { .bsn("123456789") .build(); // WHEN - WsExchangeTokenResponse response = bsnTokenMapper.map(token); + final var response = bsnTokenMapper.map(token); // THEN assertNotNull(response, "Response should not be null"); assertNotNull(response.getIdentifier(), "Identifier should not be null"); @@ -52,7 +52,7 @@ void map_WhenTokenHasNoBsn_ShouldHandleNullBsnGracefully() { // GIVEN final var token = Token.builder().build(); // No BSN set // WHEN - WsExchangeTokenResponse response = bsnTokenMapper.map(token); + final var response = bsnTokenMapper.map(token); // THEN assertNotNull(response, "Response should not be null even if BSN is null"); assertNotNull(response.getIdentifier(), "Identifier should not be null"); diff --git a/src/test/java/nl/ictu/service/map/EncryptedBsnMapperTest.java b/src/test/java/nl/ictu/service/map/EncryptedBsnMapperTest.java index 1f1b66e..24bf33e 100644 --- a/src/test/java/nl/ictu/service/map/EncryptedBsnMapperTest.java +++ b/src/test/java/nl/ictu/service/map/EncryptedBsnMapperTest.java @@ -28,9 +28,9 @@ class EncryptedBsnMapperTest { """) void map_WhenDecryptSucceeds_ShouldReturnDecryptedBsn() { // GIVEN - final String encryptedBsn = "someEncryptedValue"; - final String recipientOin = "testOIN"; - final String expectedBsn = "123456789"; + final var encryptedBsn = "someEncryptedValue"; + final var recipientOin = "testOIN"; + final var expectedBsn = "123456789"; final var decryptedIdentifier = Identifier.builder() .bsn(expectedBsn) .build(); diff --git a/src/test/java/nl/ictu/service/map/OrganisationPseudoTokenMapperTest.java b/src/test/java/nl/ictu/service/map/OrganisationPseudoTokenMapperTest.java index 6f0bcde..cb15274 100644 --- a/src/test/java/nl/ictu/service/map/OrganisationPseudoTokenMapperTest.java +++ b/src/test/java/nl/ictu/service/map/OrganisationPseudoTokenMapperTest.java @@ -39,13 +39,13 @@ class OrganisationPseudoTokenMapperTest { """) void map_WhenEncryptionSucceeds_ShouldReturnEncryptedTokenResponse() throws Exception { // GIVEN - final String callerOIN = "TEST_OIN"; - final Token token = Token.builder().bsn("123456789").build(); - final String encryptedValue = "encryptedBSN"; + final var callerOIN = "TEST_OIN"; + final var token = Token.builder().bsn("123456789").build(); + final var encryptedValue = "encryptedBSN"; when(aesGcmSivCryptographer.encrypt(any(Identifier.class), eq(callerOIN))) .thenReturn(encryptedValue); // WHEN - WsExchangeTokenResponse response = organisationPseudoTokenMapper.map(callerOIN, token); + final var response = organisationPseudoTokenMapper.map(callerOIN, token); // THEN assertEquals(ORGANISATION_PSEUDO, response.getIdentifier().getType(), "The identifier type should be ORGANISATION_PSEUDO"); @@ -61,8 +61,8 @@ void map_WhenEncryptionSucceeds_ShouldReturnEncryptedTokenResponse() throws Exce """) void map_WhenEncryptionFails_ShouldThrowInvalidCipherTextException() throws Exception { // GIVEN - final String callerOIN = "FAILING_OIN"; - final Token token = Token.builder().bsn("987654321").build(); + final var callerOIN = "FAILING_OIN"; + final var token = Token.builder().bsn("987654321").build(); when(aesGcmSivCryptographer.encrypt(any(Identifier.class), eq(callerOIN))) .thenThrow(new InvalidCipherTextException("Simulated cipher error")); // WHEN & THEN @@ -79,8 +79,8 @@ void map_WhenEncryptionFails_ShouldThrowInvalidCipherTextException() throws Exce """) void map_WhenEncryptionThrowsIOException_ShouldThrowIOException() throws Exception { // GIVEN - final String callerOIN = "IO_EXCEPTION_OIN"; - final Token token = Token.builder().bsn("555555555").build(); + final var callerOIN = "IO_EXCEPTION_OIN"; + final var token = Token.builder().bsn("555555555").build(); when(aesGcmSivCryptographer.encrypt(any(Identifier.class), eq(callerOIN))) .thenThrow(new IOException("Simulated I/O error")); // WHEN & THEN diff --git a/src/test/java/nl/ictu/service/map/PseudoBsnMapperTest.java b/src/test/java/nl/ictu/service/map/PseudoBsnMapperTest.java index c479f9f..ccccc83 100644 --- a/src/test/java/nl/ictu/service/map/PseudoBsnMapperTest.java +++ b/src/test/java/nl/ictu/service/map/PseudoBsnMapperTest.java @@ -34,10 +34,10 @@ class PseudoBsnMapperTest { """) void map_WhenDecryptionSucceeds_ShouldReturnDecryptedBsn() throws Exception { // GIVEN - String pseudo = "someEncryptedString"; - String oin = "TEST_OIN"; + var pseudo = "someEncryptedString"; + var oin = "TEST_OIN"; // Suppose the decrypted Identifier has BSN "123456789" - Identifier decryptedIdentifier = Identifier.builder() + final var decryptedIdentifier = Identifier.builder() .bsn("123456789") .build(); when(aesGcmSivCryptographer.decrypt(pseudo, oin)) diff --git a/src/test/java/nl/ictu/service/map/WsGetTokenResponseMapperTest.java b/src/test/java/nl/ictu/service/map/WsGetTokenResponseMapperTest.java index dd25638..496b41f 100644 --- a/src/test/java/nl/ictu/service/map/WsGetTokenResponseMapperTest.java +++ b/src/test/java/nl/ictu/service/map/WsGetTokenResponseMapperTest.java @@ -44,7 +44,7 @@ class WsGetTokenResponseMapperTest { void testMap_Success() throws Exception { final var encryptedToken = "encrypted-token"; // GIVEN - Token token = Token.builder() + final var token = Token.builder() .version(WsGetTokenResponseMapper.V_1) .bsn(bsn) .creationDate(creationDate) @@ -53,7 +53,7 @@ void testMap_Success() throws Exception { when(tokenCoder.encode(token)).thenReturn(encodedToken); when(aesGcmCryptographer.encrypt(encodedToken, recipientOIN)).thenReturn(encryptedToken); // WHEN - WsGetTokenResponse response = wsGetTokenResponseMapper.map(bsn, creationDate, recipientOIN); + final var response = wsGetTokenResponseMapper.map(bsn, creationDate, recipientOIN); // THEN verify(tokenCoder).encode(token); verify(aesGcmCryptographer).encrypt(encodedToken, recipientOIN); @@ -68,7 +68,7 @@ void testMap_Success() throws Exception { """) void testMap_EncodingIOException() throws Exception { // GIVEN - Token token = Token.builder() + final var token = Token.builder() .version(WsGetTokenResponseMapper.V_1) .bsn(bsn) .creationDate(creationDate) @@ -90,7 +90,7 @@ void testMap_EncodingIOException() throws Exception { """) void testMap_EncryptionError() throws Exception { // GIVEN - Token token = Token.builder() + final var token = Token.builder() .version(WsGetTokenResponseMapper.V_1) .bsn(bsn) .creationDate(creationDate) @@ -114,7 +114,7 @@ void testMap_EncryptionError() throws Exception { """) void testMap_UnexpectedError() throws Exception { // GIVEN - Token token = Token.builder() + final var token = Token.builder() .version(WsGetTokenResponseMapper.V_1) .bsn(bsn) .creationDate(creationDate) diff --git a/src/test/java/nl/ictu/service/map/WsIdentifierOinBsnMapperTest.java b/src/test/java/nl/ictu/service/map/WsIdentifierOinBsnMapperTest.java index e7f125e..a54c112 100644 --- a/src/test/java/nl/ictu/service/map/WsIdentifierOinBsnMapperTest.java +++ b/src/test/java/nl/ictu/service/map/WsIdentifierOinBsnMapperTest.java @@ -32,10 +32,10 @@ When the map() method is called """) void testMap_BsnType() { // GIVEN - String bsnValue = "987654321"; - WsIdentifier identifier = new WsIdentifier().type(BSN).value(bsnValue); + final var bsnValue = "987654321"; + final var identifier = new WsIdentifier().type(BSN).value(bsnValue); // WHEN - String result = wsIdentifierOinBsnMapper.map(identifier, "123456789"); + final var result = wsIdentifierOinBsnMapper.map(identifier, "123456789"); // THEN assertEquals(bsnValue, result); verifyNoInteractions(encryptedBsnMapper); // Ensure EncryptedBsnMapper is not called @@ -49,10 +49,10 @@ When the map() method is called """) void testMap_OrganisationPseudoType() { // GIVEN - String bsnValue = "987654321"; - String recipientOIN = "123456789"; - String encryptedValue = "encrypted-value"; - WsIdentifier identifier = new WsIdentifier() + final var bsnValue = "987654321"; + final var recipientOIN = "123456789"; + final var encryptedValue = "encrypted-value"; + final var identifier = new WsIdentifier() .type(ORGANISATION_PSEUDO) .value(bsnValue); when(encryptedBsnMapper.map(bsnValue, recipientOIN)).thenReturn(encryptedValue); diff --git a/src/test/java/nl/ictu/service/validate/OINValidatorTest.java b/src/test/java/nl/ictu/service/validate/OINValidatorTest.java index d96dc7a..4ad487c 100644 --- a/src/test/java/nl/ictu/service/validate/OINValidatorTest.java +++ b/src/test/java/nl/ictu/service/validate/OINValidatorTest.java @@ -14,6 +14,7 @@ class OINValidatorTest { @BeforeEach void setUp() { + oinValidator = new OINValidator(); } @@ -25,10 +26,12 @@ When isValid() is called """) void isValid_ReturnsTrue_WhenOINsMatch() { // GIVEN - String callerOIN = "TEST_OIN_123"; - Token token = Token.builder().recipientOIN("TEST_OIN_123").build(); + final var callerOIN = "TEST_OIN_123"; + final var token = Token.builder() + .recipientOIN("TEST_OIN_123") + .build(); // WHEN - boolean result = oinValidator.isValid(callerOIN, token); + final var result = oinValidator.isValid(callerOIN, token); // THEN assertTrue(result, "Expected isValid() to return true for matching OINs"); } @@ -41,10 +44,12 @@ When isValid() is called """) void isValid_ReturnsFalse_WhenOINsDoNotMatch() { // GIVEN - String callerOIN = "TEST_OIN_ABC"; - Token token = Token.builder().recipientOIN("TEST_OIN_XYZ").build(); + final var callerOIN = "TEST_OIN_ABC"; + final var token = Token.builder() + .recipientOIN("TEST_OIN_XYZ") + .build(); // WHEN - boolean result = oinValidator.isValid(callerOIN, token); + final var result = oinValidator.isValid(callerOIN, token); // THEN assertFalse(result, "Expected isValid() to return false for non-matching OINs"); } @@ -57,10 +62,10 @@ When isValid() is called """) void isValid_Behavior_WhenTokenRecipientOINIsNull() { // GIVEN - String callerOIN = "NON_NULL_OIN"; - Token token = Token.builder().build(); + final var callerOIN = "NON_NULL_OIN"; + final var token = Token.builder().build(); // WHEN - boolean result = oinValidator.isValid(callerOIN, token); + final var result = oinValidator.isValid(callerOIN, token); // THEN assertFalse(result, "Expected isValid() to return false if token's recipientOIN is null"); } diff --git a/src/test/java/nl/ictu/utils/AESHelperTest.java b/src/test/java/nl/ictu/utils/AESHelperTest.java index 8d146ae..b0fdd0b 100644 --- a/src/test/java/nl/ictu/utils/AESHelperTest.java +++ b/src/test/java/nl/ictu/utils/AESHelperTest.java @@ -24,13 +24,13 @@ class AESHelperTest { """) void generateIV_ShouldReturnGCMParameterSpec_WithNonNullIV() { // WHEN - GCMParameterSpec gcmParameterSpec = AesUtility.generateIV(); + final var gcmParameterSpec = AesUtility.generateIV(); // THEN assertNotNull(gcmParameterSpec, "GCMParameterSpec should not be null"); assertEquals(AesUtility.TAG_LENGTH, gcmParameterSpec.getTLen(), "Tag length should be 128 (bits)"); // The IV array is extracted from gcmParameterSpec - byte[] iv = gcmParameterSpec.getIV(); + final byte[] iv = gcmParameterSpec.getIV(); assertNotNull(iv, "IV should not be null"); assertEquals(AesUtility.IV_LENGTH, iv.length, "IV length should be " + AesUtility.IV_LENGTH); @@ -44,13 +44,13 @@ void generateIV_ShouldReturnGCMParameterSpec_WithNonNullIV() { """) void createIVfromValues_ShouldReturnGCMParameterSpec_FromGivenIV() { // GIVEN - byte[] ivSource = new byte[AesUtility.IV_LENGTH]; + final byte[] ivSource = new byte[AesUtility.IV_LENGTH]; // Fill the array with deterministic data for test for (int i = 0; i < ivSource.length; i++) { ivSource[i] = (byte) i; } // WHEN - GCMParameterSpec spec = AesUtility.createIVfromValues(ivSource); + final var spec = AesUtility.createIVfromValues(ivSource); // THEN assertNotNull(spec, "GCMParameterSpec should not be null"); assertEquals(AesUtility.TAG_LENGTH, spec.getTLen(), @@ -68,7 +68,7 @@ void createIVfromValues_ShouldReturnGCMParameterSpec_FromGivenIV() { void createCipher_ShouldReturnAesGcmNoPaddingCipher() throws NoSuchPaddingException, NoSuchAlgorithmException { // WHEN - Cipher cipher = AesUtility.createCipher(); + final var cipher = AesUtility.createCipher(); // THEN assertNotNull(cipher, "Cipher should not be null"); // Depending on the JVM/provider, the algorithm name can be uppercase or some variation, @@ -85,7 +85,7 @@ void createCipher_ShouldReturnAesGcmNoPaddingCipher() """) void getAESEngine_ShouldReturnNonNullAESEngineInstance() { // WHEN - MultiBlockCipher engine = AesUtility.getAESEngine(); + final var engine = AesUtility.getAESEngine(); // THEN assertNotNull(engine, "Engine should not be null"); assertInstanceOf(AESEngine.class, engine, "Engine should be an instance of AESEngine"); diff --git a/src/test/java/nl/ictu/utils/AesUtilityTest.java b/src/test/java/nl/ictu/utils/AesUtilityTest.java index ab461ce..a33dbbc 100644 --- a/src/test/java/nl/ictu/utils/AesUtilityTest.java +++ b/src/test/java/nl/ictu/utils/AesUtilityTest.java @@ -24,7 +24,7 @@ class AesUtilityTest { """) void generateIV_ShouldReturnGCMParameterSpec() { // WHEN - GCMParameterSpec spec = AesUtility.generateIV(); + final var spec = AesUtility.generateIV(); // THEN assertNotNull(spec, "GCMParameterSpec should not be null"); assertEquals(AesUtility.TAG_LENGTH, spec.getTLen(), @@ -42,12 +42,12 @@ void generateIV_ShouldReturnGCMParameterSpec() { """) void createIVfromValues_ShouldReturnGCMParameterSpecFromGivenIV() { // GIVEN: a deterministic IV of length 12 - byte[] ivBytes = new byte[AesUtility.IV_LENGTH]; + final var ivBytes = new byte[AesUtility.IV_LENGTH]; for (int i = 0; i < ivBytes.length; i++) { ivBytes[i] = (byte) i; } // WHEN - GCMParameterSpec spec = AesUtility.createIVfromValues(ivBytes); + final var spec = AesUtility.createIVfromValues(ivBytes); // THEN assertNotNull(spec, "GCMParameterSpec should not be null"); assertEquals(AesUtility.TAG_LENGTH, spec.getTLen(), @@ -65,7 +65,7 @@ void createIVfromValues_ShouldReturnGCMParameterSpecFromGivenIV() { void createCipher_ShouldReturnAesGcmNoPadding() throws NoSuchPaddingException, NoSuchAlgorithmException { // WHEN - Cipher cipher = AesUtility.createCipher(); + final var cipher = AesUtility.createCipher(); // THEN assertNotNull(cipher, "Cipher should not be null"); assertEquals("AES/GCM/NoPadding", cipher.getAlgorithm(), @@ -80,7 +80,7 @@ void createCipher_ShouldReturnAesGcmNoPadding() """) void getAESEngine_ShouldReturnAesEngine() { // WHEN - MultiBlockCipher engine = AesUtility.getAESEngine(); + final var engine = AesUtility.getAESEngine(); // THEN assertNotNull(engine, "Engine should not be null"); assertInstanceOf(AESEngine.class, engine, "Engine should be an instance of AESEngine"); diff --git a/src/test/java/nl/ictu/utils/Base64WrapperTest.java b/src/test/java/nl/ictu/utils/Base64WrapperTest.java index cdf5c9d..995bccb 100644 --- a/src/test/java/nl/ictu/utils/Base64WrapperTest.java +++ b/src/test/java/nl/ictu/utils/Base64WrapperTest.java @@ -25,11 +25,11 @@ void setUp() { """) void encode_ShouldEncodeBytesToBase64Bytes() { // GIVEN - byte[] input = "Hello".getBytes(StandardCharsets.UTF_8); + final var input = "Hello".getBytes(StandardCharsets.UTF_8); // WHEN - byte[] result = base64Wrapper.encode(input); + final var result = base64Wrapper.encode(input); // THEN - String resultAsString = new String(result, StandardCharsets.UTF_8); + final var resultAsString = new String(result, StandardCharsets.UTF_8); assertEquals("SGVsbG8=", resultAsString, "Expected Base64 encoding of 'Hello' to be 'SGVsbG8='"); } @@ -42,9 +42,9 @@ void encode_ShouldEncodeBytesToBase64Bytes() { """) void encodeToString_ShouldEncodeBytesToBase64String() { // GIVEN - byte[] input = "Hello".getBytes(StandardCharsets.UTF_8); + final var input = "Hello".getBytes(StandardCharsets.UTF_8); // WHEN - String base64String = base64Wrapper.encodeToString(input); + final var base64String = base64Wrapper.encodeToString(input); // THEN assertEquals("SGVsbG8=", base64String, "Expected Base64 encoding of 'Hello' to be 'SGVsbG8='"); @@ -58,11 +58,11 @@ void encodeToString_ShouldEncodeBytesToBase64String() { """) void decode_ShouldDecodeBase64StringToBytes() { // GIVEN - String base64String = "SGVsbG8="; + final var base64String = "SGVsbG8="; // WHEN - byte[] decoded = base64Wrapper.decode(base64String); + final var decoded = base64Wrapper.decode(base64String); // THEN - String decodedAsString = new String(decoded, StandardCharsets.UTF_8); + final var decodedAsString = new String(decoded, StandardCharsets.UTF_8); assertEquals("Hello", decodedAsString, "Expected Base64 decoding of 'SGVsbG8=' to be 'Hello'"); } @@ -75,7 +75,7 @@ void decode_ShouldDecodeBase64StringToBytes() { """) void decode_ShouldThrowException_WhenInvalidBase64String() { // GIVEN - String invalidBase64 = "Not valid base64!!!"; + final var invalidBase64 = "Not valid base64!!!"; // WHEN & THEN assertThrows(IllegalArgumentException.class, () -> base64Wrapper.decode(invalidBase64), diff --git a/src/test/java/nl/ictu/utils/MessageDigestWrapperTest.java b/src/test/java/nl/ictu/utils/MessageDigestWrapperTest.java index 69130b1..1e80a61 100644 --- a/src/test/java/nl/ictu/utils/MessageDigestWrapperTest.java +++ b/src/test/java/nl/ictu/utils/MessageDigestWrapperTest.java @@ -25,7 +25,7 @@ When calling getMessageDigestInstance() """) void getMessageDigestSha256_ShouldReturnSha256Digest() { // WHEN - MessageDigest digest = messageDigestWrapper.getMessageDigestInstance(); + final var digest = messageDigestWrapper.getMessageDigestInstance(); // THEN assertNotNull(digest, "MessageDigest should not be null"); assertEquals("SHA-256", digest.getAlgorithm(), From bec6e3a50b7a557bfc72a40073761cf1a5d1c0b1 Mon Sep 17 00:00:00 2001 From: sayf Date: Fri, 3 Jan 2025 21:40:10 +0100 Subject: [PATCH 29/30] Een aantal punten die opgepakt zijn ter verbetering --- Verbeteringen.md | 114 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 Verbeteringen.md diff --git a/Verbeteringen.md b/Verbeteringen.md new file mode 100644 index 0000000..71488a2 --- /dev/null +++ b/Verbeteringen.md @@ -0,0 +1,114 @@ +Plus punten: +Java 21 +Heel lief simpel klein applicatie en nog prima in orde te maken omdat het nog niet te laat is! +- weinig complexiteit + +------------------------------- +------------------------------- +Op basis van de ISO-25010 maintainability kwaliteiten: +* Modulariteit +* Herbruikbaarheid +* Analyseerbaarheid +* Aanpasbaarheid +* Testbaarheid + +Het volgende: + +* Niet Modulair: + - een enkel module + +* Moeilijk analyseerbaar + - teveel verschillende verantwoordelijkheden verzameld in een plek + - code duplicatie + - weinig tot geen documentatie (ook m.b.t. OpenAPI contract) + - geen duidelijke benamingen van classes op basis van verantwoordelijkheid + +* Moeilijk herbruikbaar: + - een enkele module met alle verantwoordelijkheden + - geen documentatie + - geen garantie op functionaliteit door ontbreken test coverage + +* Moeilijk aanpasbaar: + - elke aanpassing heeft grote gevolgen door grote stukken code + - weinig segmentatie + +* Moeilijk testbaar: + - erg weinig segementatie in verantwoordelijkheden + - teveel op een plek +------------------------------- +------------------------------- + +Te verbeteren +README.md mag veel meer informatie bevatten +* wat is het +* waar dient het voor +* hoe werkt het opzetten +* wie is ervoor verantwoordelijk +* etc + +Maven project +- versienummers kunnen beter als properties verntraal worden beheerd +- een mutatie test kan iets meer inzicht geven in hoe goed het getest wordt + +Documentatie: +* veel complexe cryptography vergt wel het een en ander aan uitleg + +Checkstyle & psot-bugs: +- veel warnings die suppressed worden kunnen net zo goed eruit of gefixed worden + +Testing +- 3 testen voor een hele applicatie is geen garantie dat de applicatie doet wat het moet doen +- test coverage is erg laag +- MUTATIE coverage is nog lager +- geen duidelijke beschrijving van tests en wat ze moeten doen +- enkel integratie testing en geen unit tests +- geen mocks +- niet makkelijk te testen vanwege + - onwenselijke instantiaties van on-mockbare objecten + - onduidelijkheid en verwevenheid in verantwoordelijkheden (teveel in een plek) +- integratie testen betekent simpelweg dat je bij elke aanpassing alle testen moet herzien + +Controllers: +- weinig validatie op input!!! +- doen teveel! +- @SneakyThrows probleem +- benaming +- moeilijk testbaar +- moeilijk leesbaar +- niet gedocumenteerd + +Services: +- doen teveel! houd het bij controlleren van input +- @SneakyThrows probleem +- benaming +- moeilijk testbaar +- moeilijk leesbaar +- niet gedocumenteerd +- duplicate code + +Utils: +- combineren van Byte[] wordt om meerdere plekken gedaan maar niet consistent gebruikmakend van deze util + +Configuratie: +- @Component vervangen met @Configuratioe +- zorg dat hier de validatie gedaan wordt i.p.v. constructors van classes die hier gebruik van maken +- test de configuratie + +Models: +- te veel suppress warnings +- geen eigen package +- hard coded values +- maak het netter met LOMBOK + +OpenAPI: +- documentatie ontbreekt +- maak het netter met LOMBOK + + +application-properties: +- waarom is PRD logging altijd op DEBUG? + + + + + From 7be8eaa6fb134cef51102adff888e5556565397a Mon Sep 17 00:00:00 2001 From: sayf Date: Fri, 3 Jan 2025 21:42:08 +0100 Subject: [PATCH 30/30] Een aantal punten die opgepakt zijn ter verbetering --- Verbeteringen.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Verbeteringen.md b/Verbeteringen.md index 71488a2..9541eac 100644 --- a/Verbeteringen.md +++ b/Verbeteringen.md @@ -5,7 +5,7 @@ Heel lief simpel klein applicatie en nog prima in orde te maken omdat het nog ni ------------------------------- ------------------------------- -Op basis van de ISO-25010 maintainability kwaliteiten: +Beoordeeld op basis van de ISO-25010 maintainability kwaliteiten: * Modulariteit * Herbruikbaarheid * Analyseerbaarheid @@ -15,7 +15,7 @@ Op basis van de ISO-25010 maintainability kwaliteiten: Het volgende: * Niet Modulair: - - een enkel module + - een enkele module * Moeilijk analyseerbaar - teveel verschillende verantwoordelijkheden verzameld in een plek @@ -104,7 +104,6 @@ OpenAPI: - documentatie ontbreekt - maak het netter met LOMBOK - application-properties: - waarom is PRD logging altijd op DEBUG?