diff --git a/pom.xml b/pom.xml index 33ca979..f268f98 100644 --- a/pom.xml +++ b/pom.xml @@ -68,6 +68,12 @@ ${junit-jupiter.version} test + + uk.org.webcompere + system-stubs-jupiter + 2.1.6 + test + org.mockito mockito-junit-jupiter diff --git a/src/main/java/io/github/tap30/hiss/Encrypted.java b/src/main/java/io/github/tap30/hiss/Encrypted.java index 12f662f..4d31418 100644 --- a/src/main/java/io/github/tap30/hiss/Encrypted.java +++ b/src/main/java/io/github/tap30/hiss/Encrypted.java @@ -11,7 +11,7 @@ /** * Fields annotated using this will be encrypted. */ -// todo: add example +// Todo: add example @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Encrypted { diff --git a/src/main/java/io/github/tap30/hiss/EncryptedInside.java b/src/main/java/io/github/tap30/hiss/EncryptedInside.java index d08972f..efb6942 100644 --- a/src/main/java/io/github/tap30/hiss/EncryptedInside.java +++ b/src/main/java/io/github/tap30/hiss/EncryptedInside.java @@ -5,11 +5,11 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -// todo: improve doc +// Todo: improve doc /** * Fields annotated with this will be scanned */ -// todo: add example +// Todo: add example @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface EncryptedInside { diff --git a/src/main/java/io/github/tap30/hiss/Hiss.java b/src/main/java/io/github/tap30/hiss/Hiss.java index 8253ed0..29be267 100644 --- a/src/main/java/io/github/tap30/hiss/Hiss.java +++ b/src/main/java/io/github/tap30/hiss/Hiss.java @@ -6,6 +6,7 @@ import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.Nullable; +import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; @@ -17,21 +18,13 @@ public class Hiss { private final HissObjectEncryptor hissObjectEncryptor; Hiss(HissProperties hissProperties, KeyHashGenerator keyHashGenerator) { + Objects.requireNonNull(hissProperties); + Objects.requireNonNull(keyHashGenerator); + this.hissEncryptor = new HissEncryptor(hissProperties); this.hissObjectEncryptor = new HissObjectEncryptor(this.hissEncryptor); - logger.log(Level.INFO, "Hiss initialized:\n" + - " Loaded Keys: {0}\n" + - " Default Encryption Key ID: {1}\n" + - " Default Encryption Algorithm: {2}\n" + - " Default Hashing Key ID: {3}\n" + - " Default Hashing Algorithm: {4}", - new Object[]{ - hissProperties.getKeys().keySet(), - hissProperties.getDefaultEncryptionKeyId(), - hissProperties.getDefaultEncryptionAlgorithm(), - hissProperties.getDefaultHashingKeyId(), - hissProperties.getDefaultHashingAlgorithm() - }); + + logHissInitialized(hissProperties); if (hissProperties.isKeyHashGenerationEnabled()) { keyHashGenerator.generateAndLogHashes(hissProperties.getKeys().values()); } @@ -44,7 +37,7 @@ public class Hiss { * @return encrypted content or null if the content is null. */ public String encrypt(@Nullable String content) { - return this.encrypt(content, ""); + return encrypt(content, ""); } /** @@ -56,7 +49,7 @@ public String encrypt(@Nullable String content) { * @return encrypted content or null if the content is null. */ public String encrypt(@Nullable String content, @Language("regexp") @Nullable String pattern) { - return this.hissEncryptor.encrypt(content, pattern); + return hissEncryptor.encrypt(content, pattern); } /** @@ -69,7 +62,7 @@ public String encrypt(@Nullable String content, @Language("regexp") @Nullable St * @throws IllegalArgumentException if key ID or algorithm is not loaded/supported. */ public String decrypt(@Nullable String content) { - return this.hissEncryptor.decrypt(content); + return hissEncryptor.decrypt(content); } /** @@ -79,7 +72,7 @@ public String decrypt(@Nullable String content) { * @return hashed content or null if the content is null. */ public String hash(@Nullable String content) { - return this.hash(content, ""); + return hash(content, ""); } /** @@ -91,7 +84,7 @@ public String hash(@Nullable String content) { * @return hashed content or null if the content is null. */ public String hash(@Nullable String content, @Language("regexp") @Nullable String pattern) { - return this.hissEncryptor.hash(content, pattern); + return hissEncryptor.hash(content, pattern); } /** @@ -120,7 +113,7 @@ public boolean isHashed(@Nullable String content) { * @param object the annotated object; no action is taken on null objects. */ public void encryptObject(@Nullable Object object) { - this.hissObjectEncryptor.encryptObject(object); + hissObjectEncryptor.encryptObject(object); } /** @@ -129,7 +122,23 @@ public void encryptObject(@Nullable Object object) { * @param object the annotated object; no action is taken on null objects. */ public void decryptObject(@Nullable Object object) { - this.hissObjectEncryptor.decryptObject(object); + hissObjectEncryptor.decryptObject(object); + } + + private void logHissInitialized(HissProperties hissProperties) { + logger.log(Level.INFO, "Hiss initialized:\n" + + " Loaded Keys: {0}\n" + + " Default Encryption Key ID: {1}\n" + + " Default Encryption Algorithm: {2}\n" + + " Default Hashing Key ID: {3}\n" + + " Default Hashing Algorithm: {4}", + new Object[]{ + hissProperties.getKeys().keySet(), + hissProperties.getDefaultEncryptionKeyId(), + hissProperties.getDefaultEncryptionAlgorithm(), + hissProperties.getDefaultHashingKeyId(), + hissProperties.getDefaultHashingAlgorithm() + }); } } diff --git a/src/main/java/io/github/tap30/hiss/properties/HissProperties.java b/src/main/java/io/github/tap30/hiss/properties/HissProperties.java index 4bb881f..dc27653 100644 --- a/src/main/java/io/github/tap30/hiss/properties/HissProperties.java +++ b/src/main/java/io/github/tap30/hiss/properties/HissProperties.java @@ -1,26 +1,21 @@ package io.github.tap30.hiss.properties; import io.github.tap30.hiss.key.Key; -import io.github.tap30.hiss.utils.StringUtils; import java.util.HashMap; import java.util.Map; +import java.util.Set; import java.util.function.Supplier; +import java.util.stream.Collectors; public abstract class HissProperties { private final Map properties = new HashMap<>(); public Map getKeys() { - return this.getProperty("Keys", () -> { - var keys = loadKeys(); - keys.forEach((k, v) -> { - if (v != null && !StringUtils.hasText(v.getId())) { - throw new RuntimeException("Key ID must not be empty"); - } - }); - return keys; - }); + return this.getProperty("Keys", () -> loadKeys() + .stream() + .collect(Collectors.toMap(Key::getId, k -> k))); } public String getDefaultEncryptionKeyId() { @@ -43,11 +38,16 @@ public boolean isKeyHashGenerationEnabled() { return this.getProperty("KeyHashGenerationEnabled", this::loadKeyHashGenerationEnabled); } - protected abstract Map loadKeys(); + protected abstract Set loadKeys(); + protected abstract String loadDefaultEncryptionKeyId(); + protected abstract String loadDefaultEncryptionAlgorithm(); + protected abstract String loadDefaultHashingKeyId(); + protected abstract String loadDefaultHashingAlgorithm(); + protected abstract boolean loadKeyHashGenerationEnabled(); @SuppressWarnings("unchecked") diff --git a/src/main/java/io/github/tap30/hiss/properties/HissPropertiesFromEnv.java b/src/main/java/io/github/tap30/hiss/properties/HissPropertiesFromEnv.java index 7dd172a..c7e013f 100644 --- a/src/main/java/io/github/tap30/hiss/properties/HissPropertiesFromEnv.java +++ b/src/main/java/io/github/tap30/hiss/properties/HissPropertiesFromEnv.java @@ -1,11 +1,11 @@ package io.github.tap30.hiss.properties; import io.github.tap30.hiss.key.Key; -import lombok.Setter; import java.util.Base64; -import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.function.Supplier; /** @@ -30,19 +30,17 @@ public class HissPropertiesFromEnv extends HissProperties { private static final String KEY_ENV_PREFIX = "HISS_KEYS_"; private static final String KEY_HASH_ENV_POSTFIX = "___HASH"; - @Setter // todo: make it package private - private Supplier> envProvider = System::getenv; + private static final Supplier> ENV_PROVIDER = System::getenv; @Override - public Map loadKeys() { - var keys = new HashMap(); - envProvider.get().forEach((k, v) -> { + public Set loadKeys() { + var keys = new HashSet(); + ENV_PROVIDER.get().forEach((k, v) -> { if (k.startsWith(KEY_ENV_PREFIX) && !k.endsWith(KEY_HASH_ENV_POSTFIX)) { - var id = k.replace(KEY_ENV_PREFIX, "").toLowerCase(); - keys.put(id, Key.builder() - .id(id) + keys.add(Key.builder() + .id(k.replace(KEY_ENV_PREFIX, "").toLowerCase()) .key(Base64.getDecoder().decode(v)) - .keyHash(envProvider.get().get(k + KEY_HASH_ENV_POSTFIX)) + .keyHash(ENV_PROVIDER.get().get(k + KEY_HASH_ENV_POSTFIX)) .build()); } }); @@ -51,26 +49,26 @@ public Map loadKeys() { @Override public String loadDefaultEncryptionKeyId() { - return envProvider.get().get("HISS_DEFAULT_ENCRYPTION_KEY_ID"); + return ENV_PROVIDER.get().get("HISS_DEFAULT_ENCRYPTION_KEY_ID"); } @Override public String loadDefaultEncryptionAlgorithm() { - return envProvider.get().get("HISS_DEFAULT_ENCRYPTION_ALGORITHM"); + return ENV_PROVIDER.get().get("HISS_DEFAULT_ENCRYPTION_ALGORITHM"); } @Override public String loadDefaultHashingKeyId() { - return envProvider.get().get("HISS_DEFAULT_HASHING_KEY_ID"); + return ENV_PROVIDER.get().get("HISS_DEFAULT_HASHING_KEY_ID"); } @Override public String loadDefaultHashingAlgorithm() { - return envProvider.get().get("HISS_DEFAULT_HASHING_ALGORITHM"); + return ENV_PROVIDER.get().get("HISS_DEFAULT_HASHING_ALGORITHM"); } @Override protected boolean loadKeyHashGenerationEnabled() { - return Boolean.parseBoolean(envProvider.get().get("HISS_KEY_HASH_GENERATION_ENABLED")); + return Boolean.parseBoolean(ENV_PROVIDER.get().get("HISS_KEY_HASH_GENERATION_ENABLED")); } } diff --git a/src/main/java/io/github/tap30/hiss/properties/HissPropertiesValidator.java b/src/main/java/io/github/tap30/hiss/properties/HissPropertiesValidator.java index 1a870c8..a428a29 100644 --- a/src/main/java/io/github/tap30/hiss/properties/HissPropertiesValidator.java +++ b/src/main/java/io/github/tap30/hiss/properties/HissPropertiesValidator.java @@ -4,22 +4,39 @@ import io.github.tap30.hiss.utils.StringUtils; import java.util.ArrayList; +import java.util.Objects; +import java.util.Set; public class HissPropertiesValidator { + private static final Set SUPPORTED_ENCRYPTION_ALGORITHMS + = Set.of("aes-128-gcm", "aes-128-cbc"); // Todo: get these from algorithm spec + private static final Set SUPPORTED_HASHING_ALGORITHMS + = Set.of("hmac-sha256"); // Todo: get this from algorithm spec + private final KeyHashGenerator keyHashGenerator; public HissPropertiesValidator(KeyHashGenerator keyHashGenerator) { + Objects.requireNonNull(keyHashGenerator); this.keyHashGenerator = keyHashGenerator; } public void validate(HissProperties hissProperties) { var errors = new ArrayList(); + validateKeys(hissProperties, errors); + validateDefaultEncryptionKeyAndAlgorithm(hissProperties, errors); + validateDefaultHashingKeyAndAlgorithm(hissProperties, errors); + if (!errors.isEmpty()) { + throw new IllegalArgumentException("Hiss properties are not valid: " + String.join("; ", errors)); + } + } + + private void validateKeys(HissProperties hissProperties, ArrayList errors) { if (hissProperties.getKeys() == null || hissProperties.getKeys().isEmpty()) { errors.add("Keys are empty"); } else { hissProperties.getKeys().forEach((k, v) -> { - if (v == null || v.getKey() == null || v.getKey().length == 0) { + if (k == null || v == null || v.getKey() == null || v.getKey().length == 0) { errors.add("Key " + k + " is empty"); } }); @@ -28,20 +45,33 @@ public void validate(HissProperties hissProperties) { errors.add("Key(s) " + mismatches + " did not match with their hashes"); } } + } + + private static void validateDefaultEncryptionKeyAndAlgorithm(HissProperties hissProperties, ArrayList errors) { if (!StringUtils.hasText(hissProperties.getDefaultEncryptionKeyId())) { errors.add("Default encryption key ID is not defined"); } + if (!hissProperties.getKeys().containsKey(hissProperties.getDefaultEncryptionKeyId())) { + errors.add("Default encryption key ID is not among provided keys: " + hissProperties.getKeys().keySet()); + } if (!StringUtils.hasText(hissProperties.getDefaultEncryptionAlgorithm())) { errors.add("Default encryption algorithm is not defined"); + } else if (!SUPPORTED_ENCRYPTION_ALGORITHMS.contains(hissProperties.getDefaultEncryptionAlgorithm())) { + errors.add("Encryption algorithm " + hissProperties.getDefaultEncryptionAlgorithm() + " is not supported"); } + } + + private static void validateDefaultHashingKeyAndAlgorithm(HissProperties hissProperties, ArrayList errors) { if (!StringUtils.hasText(hissProperties.getDefaultHashingKeyId())) { errors.add("Default hashing key ID is not defined"); } + if (!hissProperties.getKeys().containsKey(hissProperties.getDefaultHashingKeyId())) { + errors.add("Default hashing key ID is not among provided keys: " + hissProperties.getKeys().keySet()); + } if (!StringUtils.hasText(hissProperties.getDefaultHashingAlgorithm())) { errors.add("Default hashing algorithm is not defined"); - } - if (!errors.isEmpty()) { - throw new IllegalArgumentException("Hiss properties are not valid: " + String.join("; ", errors)); + } else if (!SUPPORTED_HASHING_ALGORITHMS.contains(hissProperties.getDefaultHashingAlgorithm())) { + errors.add("Hashing algorithm " + hissProperties.getDefaultHashingAlgorithm() + " is not supported"); } } diff --git a/src/test/java/io/github/tap30/hiss/BaseHissTest.java b/src/test/java/io/github/tap30/hiss/BaseHissTest.java index 4b26169..be5886a 100644 --- a/src/test/java/io/github/tap30/hiss/BaseHissTest.java +++ b/src/test/java/io/github/tap30/hiss/BaseHissTest.java @@ -2,24 +2,29 @@ import io.github.tap30.hiss.properties.HissPropertiesFromEnv; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import uk.org.webcompere.systemstubs.environment.EnvironmentVariables; +import uk.org.webcompere.systemstubs.jupiter.SystemStub; +import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; -import java.util.Map; - +@ExtendWith(SystemStubsExtension.class) public class BaseHissTest { + + @SystemStub + private EnvironmentVariables environment = new EnvironmentVariables( + "HISS_DEFAULT_ENCRYPTION_KEY_ID", "default_key", + "HISS_DEFAULT_ENCRYPTION_ALGORITHM", "aes-128-gcm", + "HISS_DEFAULT_HASHING_KEY_ID", "default_key", + "HISS_DEFAULT_HASHING_ALGORITHM", "hmac-sha256", + "HISS_KEYS_DEFAULT_KEY", "AAAAAAAAAAAAAAAAAAAAAA==", + "HISS_KEY_HASH_GENERATION_ENABLED", "false" + ); + protected Hiss hiss; @BeforeEach void setUpHiss() { var hissPropertiesFromEnv = new HissPropertiesFromEnv(); - hissPropertiesFromEnv.setEnvProvider(() -> Map.of( - "HISS_DEFAULT_ENCRYPTION_KEY_ID", "default_key", - "HISS_DEFAULT_ENCRYPTION_ALGORITHM", "aes-128-gcm", - "HISS_DEFAULT_HASHING_KEY_ID", "default_key", - "HISS_DEFAULT_HASHING_ALGORITHM", "hmac-sha256", - "HISS_KEYS_DEFAULT_KEY", "AAAAAAAAAAAAAAAAAAAAAA==", - "HISS_KEYS_DEFAULT_KEY___HASH", "$2a$12$3T0VMnGMgvesehYomommnO02dbFOJuM/3elsmgmsB2/qlGSF3BIbe", // todo: remove from here and add it to separate test - "HISS_KEY_HASH_GENERATION_ENABLED", "true" - )); hiss = HissFactory.createHiss(hissPropertiesFromEnv); } diff --git a/src/test/java/io/github/tap30/hiss/HissFactoryTest.java b/src/test/java/io/github/tap30/hiss/HissFactoryTest.java new file mode 100644 index 0000000..4a7902d --- /dev/null +++ b/src/test/java/io/github/tap30/hiss/HissFactoryTest.java @@ -0,0 +1,101 @@ +package io.github.tap30.hiss; + +import io.github.tap30.hiss.key.Key; +import io.github.tap30.hiss.properties.HissProperties; +import org.junit.jupiter.api.Test; + +import java.util.Base64; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class HissFactoryTest { + + @Test + void testCreateHiss() { + // Given + var properties = new HissProperties() { + + @Override + protected Set loadKeys() { + return Set.of(Key.builder() + .id("default_key") + .key(Base64.getDecoder().decode("AAAAAAAAAAAAAAAAAAAAAA==")) + .keyHash("$2a$12$3T0VMnGMgvesehYomommnO02dbFOJuM/3elsmgmsB2/qlGSF3BIbe") + .build()); + } + + @Override + protected String loadDefaultEncryptionKeyId() { + return "default_key"; + } + + @Override + protected String loadDefaultEncryptionAlgorithm() { + return "aes-128-gcm"; + } + + @Override + protected String loadDefaultHashingKeyId() { + return "default_key"; + } + + @Override + protected String loadDefaultHashingAlgorithm() { + return "hmac-sha256"; + } + + @Override + protected boolean loadKeyHashGenerationEnabled() { + return false; + } + }; + + // When + var hiss = HissFactory.createHiss(properties); + + // Then + assertNotNull(hiss); + assertEquals("#$$#{hmac-sha256:default_key}{izfsg2N2nlwGtgNPzfwTWFUFIb5xJTvV5qEsRRUODmk=}#$$#", + hiss.hash("some encrypted text")); + assertEquals("some encrypted text", + hiss.decrypt("#$$#{aes-128-gcm:default_key}{nYf5c6FQYJCQdc6JcfqTkvaSwqTGg2Oh0fapibp94G4anlMeXZrCAOZLOhMD3QIJymv/}#$$#")); + } + + @Test + void testCreateHiss_shouldPropertiesBeingValidated() { + assertThrows(IllegalArgumentException.class, () -> + HissFactory.createHiss(new HissProperties() { + @Override + protected Set loadKeys() { + return Set.of(); + } + + @Override + protected String loadDefaultEncryptionKeyId() { + return ""; + } + + @Override + protected String loadDefaultEncryptionAlgorithm() { + return ""; + } + + @Override + protected String loadDefaultHashingKeyId() { + return ""; + } + + @Override + protected String loadDefaultHashingAlgorithm() { + return ""; + } + + @Override + protected boolean loadKeyHashGenerationEnabled() { + return false; + } + })); + } + +} \ No newline at end of file diff --git a/src/test/java/io/github/tap30/hiss/HissTest.java b/src/test/java/io/github/tap30/hiss/HissTest.java index 8e35dfa..bd9dd50 100644 --- a/src/test/java/io/github/tap30/hiss/HissTest.java +++ b/src/test/java/io/github/tap30/hiss/HissTest.java @@ -1,49 +1,66 @@ package io.github.tap30.hiss; import io.github.tap30.hiss.key.Key; +import io.github.tap30.hiss.key.KeyHashGenerator; import io.github.tap30.hiss.properties.HissProperties; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; -import java.util.Map; +import java.util.Collection; +import java.util.Set; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; class HissTest extends BaseHissTest { - @Test - void testPropertiesValidationWorks() { - assertThrows(IllegalArgumentException.class, () -> - HissFactory.createHiss(new HissProperties() { - @Override - protected Map loadKeys() { - return Map.of(); - } - - @Override - protected String loadDefaultEncryptionKeyId() { - return ""; - } - @Override - protected String loadDefaultEncryptionAlgorithm() { - return ""; - } - - @Override - protected String loadDefaultHashingKeyId() { - return ""; - } + @Test + void testKeyHashesAreGenerated() { + // Given + var keys = Set.of(Key.builder().build()); + var properties = new HissProperties() { + @Override + protected Set loadKeys() { + return keys; + } + + @Override + protected String loadDefaultEncryptionKeyId() { + return ""; + } + + @Override + protected String loadDefaultEncryptionAlgorithm() { + return ""; + } + + @Override + protected String loadDefaultHashingKeyId() { + return ""; + } + + @Override + protected String loadDefaultHashingAlgorithm() { + return ""; + } + + @Override + protected boolean loadKeyHashGenerationEnabled() { + return true; + } + }; + + var keyHashGenerator = mock(KeyHashGenerator.class); - @Override - protected String loadDefaultHashingAlgorithm() { - return ""; - } + // When + new Hiss(properties, keyHashGenerator); - @Override - protected boolean loadKeyHashGenerationEnabled() { - return false; - } - })); + // Then + var keysCaptor = ArgumentCaptor.forClass(Collection.class); + verify(keyHashGenerator).generateAndLogHashes(keysCaptor.capture()); + assertEquals(keys, Set.copyOf(keysCaptor.getValue())); } @Test @@ -73,12 +90,12 @@ void testEncrypt_whenContentIsAlreadyEncrypted() { } @Test - void testEncrypt_whenValueIsNull() throws Exception { + void testEncrypt_whenValueIsNull() { assertNull(hiss.encrypt(null)); } @Test - void testEncrypt_whenValueIsEmpty() throws Exception { + void testEncrypt_whenValueIsEmpty() { assertEquals("", hiss.encrypt("")); } @@ -107,17 +124,17 @@ void testDecrypt_whenContentNotEncrypted() { } @Test - void testDecrypt_whenValueIsNull() throws Exception { + void testDecrypt_whenValueIsNull() { assertNull(hiss.decrypt(null)); } @Test - void testDecrypt_whenValueIsEmpty() throws Exception { + void testDecrypt_whenValueIsEmpty() { assertEquals("", hiss.decrypt("")); } @Test - void testEncryptAndDecrypt() throws Exception { + void testEncryptAndDecrypt() { // Given final var content = "Hello; user with phone number +989123456789 and national code 1234567890 is verified."; @@ -160,7 +177,7 @@ void testEncryptAndDecrypt_withWeiredValue02() { } @Test - void testEncryptAndDecrypt_whenHavingPattern() throws Exception { + void testEncryptAndDecrypt_whenHavingPattern() { // Given final var content = "Hello; user with phone number +989123456789 and national code 1234567890 is verified."; @@ -183,7 +200,7 @@ void testEncryptAndDecrypt_whenHavingPattern() throws Exception { } @Test - void testHash() throws Exception { + void testHash() { // Given final var content = "Hello; user with phone number +989123456789 and national code 1234567890 is verified."; @@ -210,7 +227,7 @@ void testHash_whenValueIsEmpty() { } @Test - void testHash_producingSameValue() throws Exception { + void testHash_producingSameValue() { // Given final var content = "Hello; user with phone number +989123456789 and national code 1234567890 is verified."; @@ -225,7 +242,7 @@ void testHash_producingSameValue() throws Exception { } @Test - void testHash_whenHavingPattern() throws Exception { + void testHash_whenHavingPattern() { // Given final var content = "Hello; user with phone number +989123456789 and national code 1234567890 is verified."; diff --git a/src/test/java/io/github/tap30/hiss/key/KeyHashGeneratorTest.java b/src/test/java/io/github/tap30/hiss/key/KeyHashGeneratorTest.java new file mode 100644 index 0000000..49dcd0c --- /dev/null +++ b/src/test/java/io/github/tap30/hiss/key/KeyHashGeneratorTest.java @@ -0,0 +1,99 @@ +package io.github.tap30.hiss.key; + +import at.favre.lib.crypto.bcrypt.BCrypt; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +class KeyHashGeneratorTest { + + BCrypt.Hasher hasher = BCrypt.withDefaults(); + BCrypt.Verifyer verifier = BCrypt.verifyer(); + KeyHashGenerator keyHashGenerator = new KeyHashGenerator(hasher, verifier); + + @Test + void testGenerateHashes() { + // Given + var keys = Set.of( + Key.builder().id("key1").key(new byte[]{1, 2, 3}).build(), + Key.builder().id("key2").key(new byte[]{4, 5, 6}).build() + ); + + // When + var hashes = keyHashGenerator.generateHashes(keys); + System.out.println(hashes); + + // Then + assertEquals(2, hashes.size()); + assertTrue(verifier.verify(new byte[]{1, 2, 3}, hashes.get("key1").getBytes(StandardCharsets.UTF_8)).verified); + assertTrue(verifier.verify(new byte[]{4, 5, 6}, hashes.get("key2").getBytes(StandardCharsets.UTF_8)).verified); + } + + @Test + void testValidateKeyHashes() { + // Given + var keys = Set.of( + Key.builder() + .id("key1") + .key(new byte[]{1, 2, 3}) + .keyHash("$2a$12$tvVEa2yZ/RhSbYg16GG5hO5a/2P9HWVWM8c8ISZpgLIRvlF3EzVgm") + .build(), + Key.builder() + .id("key2") + .key(new byte[]{4, 5, 6}) + .keyHash("$2a$12$fwlilo5GtK44245Xcg57HuvNDEhJM7snmQ7VOO2LQfGvtvOk8tbpS") + .build() + ); + + // When + var invalidKeys = keyHashGenerator.validateKeyHashes(keys); + + // Then + assertEquals(0, invalidKeys.size()); + } + + @Test + void testValidateKeyHashes_whenKeyHashesAreInvalid() { + // Given + var keys = Set.of( + Key.builder() + .id("key1") + .key(new byte[]{1, 2, 3}) + .keyHash("chert1") + .build(), + Key.builder() + .id("key2") + .key(new byte[]{4, 5, 6}) + .keyHash("chert2") + .build() + ); + + // When + var invalidKeys = keyHashGenerator.validateKeyHashes(keys); + + // Then + assertEquals(2, invalidKeys.size()); + assertTrue(invalidKeys.contains("key1")); + assertTrue(invalidKeys.contains("key2")); + } + + @Test + void testGenerateAndLogAllHashes() { + // Given + var keys = Set.of(); + var keyHashGenerator = spy(this.keyHashGenerator); + + // When + keyHashGenerator.generateAndLogHashes(keys); + + // Then + verify(keyHashGenerator).generateHashes(keys); + } + +} \ No newline at end of file diff --git a/src/test/java/io/github/tap30/hiss/properties/HissPropertiesFromEnvTest.java b/src/test/java/io/github/tap30/hiss/properties/HissPropertiesFromEnvTest.java new file mode 100644 index 0000000..e373410 --- /dev/null +++ b/src/test/java/io/github/tap30/hiss/properties/HissPropertiesFromEnvTest.java @@ -0,0 +1,49 @@ +package io.github.tap30.hiss.properties; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import uk.org.webcompere.systemstubs.environment.EnvironmentVariables; +import uk.org.webcompere.systemstubs.jupiter.SystemStub; +import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; + +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@ExtendWith(SystemStubsExtension.class) +class HissPropertiesFromEnvTest { + + @SystemStub + EnvironmentVariables environment = new EnvironmentVariables( + "HISS_DEFAULT_ENCRYPTION_KEY_ID", "default_enc_key", + "HISS_DEFAULT_ENCRYPTION_ALGORITHM", "aes-128-gcm", + "HISS_DEFAULT_HASHING_KEY_ID", "default_hash_key", + "HISS_DEFAULT_HASHING_ALGORITHM", "hmac-sha256", + "HISS_KEYS_DEFAULT_KEY", "dGhlIGFjdHVhbCBrZXkK", + "HISS_KEYS_DEFAULT_KEY___HASH", "some hash", + "HISS_KEYS_OTHER_KEY", "dGhlIGFjdHVhbCBvdGhlciBrZXkK", + "HISS_KEYS_OTHER_KEY___HASH", "other key hash", + "HISS_KEY_HASH_GENERATION_ENABLED", "true" + ); + + @Test + void test() { + // Given & When + var hissProperties = new HissPropertiesFromEnv(); + + // Then + assertEquals(2, hissProperties.getKeys().size()); + assertEquals("default_key", hissProperties.getKeys().get("default_key").getId()); + assertArrayEquals("the actual key\n".getBytes(StandardCharsets.US_ASCII), hissProperties.getKeys().get("default_key").getKey()); + assertEquals("some hash", hissProperties.getKeys().get("default_key").getKeyHash()); + assertEquals("other_key", hissProperties.getKeys().get("other_key").getId()); + assertArrayEquals("the actual other key\n".getBytes(StandardCharsets.US_ASCII), hissProperties.getKeys().get("other_key").getKey()); + assertEquals("other key hash", hissProperties.getKeys().get("other_key").getKeyHash()); + assertEquals("default_enc_key", hissProperties.getDefaultEncryptionKeyId()); + assertEquals("aes-128-gcm", hissProperties.getDefaultEncryptionAlgorithm()); + assertEquals("default_hash_key", hissProperties.getDefaultHashingKeyId()); + assertEquals("hmac-sha256", hissProperties.getDefaultHashingAlgorithm()); + } + +} \ No newline at end of file diff --git a/src/test/java/io/github/tap30/hiss/properties/HissPropertiesValidatorTest.java b/src/test/java/io/github/tap30/hiss/properties/HissPropertiesValidatorTest.java new file mode 100644 index 0000000..d3365cd --- /dev/null +++ b/src/test/java/io/github/tap30/hiss/properties/HissPropertiesValidatorTest.java @@ -0,0 +1,201 @@ +package io.github.tap30.hiss.properties; + +import io.github.tap30.hiss.key.Key; +import io.github.tap30.hiss.key.KeyHashGenerator; +import org.junit.jupiter.api.Test; + +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +class HissPropertiesValidatorTest { + + KeyHashGenerator keyHashGenerator = mock(KeyHashGenerator.class); + HissPropertiesValidator hissPropertiesValidator = new HissPropertiesValidator(keyHashGenerator); + + @Test + void testConstructor_whenKeyHashGeneratorIsNull() { + assertThrows(NullPointerException.class, () -> new HissPropertiesValidator(null)); + } + + // Keys Validation + + @Test + void testValidate_whenPropertiesAreValid() { + // Given + var properties = createValidProperties(); + + // When & Then + assertDoesNotThrow(() -> hissPropertiesValidator.validate(properties)); + } + + @Test + void testValidate_whenKeysAreEmpty() { + // Given + var properties = spy(createValidProperties()); + doReturn(Map.of()).when(properties).getKeys(); + + // When & Then + assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); + } + + @Test + void testValidate_whenKeysHaveAKeyWithoutName() { + // Given + var properties = spy(createValidProperties()); + var keys = new HashMap(); + keys.put(null, Key.builder().build()); + doReturn(keys).when(properties).getKeys(); + + // When & Then + assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); + } + + @Test + void testValidate_whenKeyBytesAreEmpty() { + // Given + var properties = spy(createValidProperties()); + doReturn(Map.of("default_key", Key.builder().build())).when(properties).getKeys(); + + // When & Then + assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); + } + + @Test + void testValidate_whenKeyHashIsNotCorrect() { + // Given + var properties = createValidProperties(); + doReturn(Set.of("default_key")).when(keyHashGenerator).validateKeyHashes(any()); + + // When & Then + assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); + } + + // Default Encryption Key and Algorithm Validation + + @Test + void testValidate_whenDefaultEncryptionKeyIdIsMissing() { + // Given + var properties = spy(createValidProperties()); + doReturn(null).when(properties).getDefaultEncryptionKeyId(); + + // When & Then + assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); + } + + @Test + void testValidate_whenDefaultEncryptionKeyIdIsInvalid() { + // Given + var properties = spy(createValidProperties()); + doReturn("some unknown key").when(properties).getDefaultEncryptionKeyId(); + + // When & Then + assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); + } + + @Test + void testValidate_whenDefaultEncryptionAlgorithmIsMissing() { + // Given + var properties = spy(createValidProperties()); + doReturn(null).when(properties).getDefaultEncryptionAlgorithm(); + + // When & Then + assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); + } + + @Test + void testValidate_whenDefaultEncryptionAlgorithmIsInvalid() { + // Given + var properties = spy(createValidProperties()); + doReturn("some unknown algorithm").when(properties).getDefaultEncryptionAlgorithm(); + + // When & Then + assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); + } + + // Default Hashing Key and Algorithm Validation + + @Test + void testValidate_whenDefaultHashingKeyIdIsMissing() { + // Given + var properties = spy(createValidProperties()); + doReturn(null).when(properties).getDefaultHashingKeyId(); + + // When & Then + assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); + } + + @Test + void testValidate_whenDefaultHashingKeyIdIsInvalid() { + // Given + var properties = spy(createValidProperties()); + doReturn("some unknown key").when(properties).getDefaultHashingKeyId(); + + // When & Then + assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); + } + + @Test + void testValidate_whenDefaultHashingAlgorithmIsMissing() { + // Given + var properties = spy(createValidProperties()); + doReturn(null).when(properties).getDefaultHashingAlgorithm(); + + // When & Then + assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); + } + + @Test + void testValidate_whenDefaultHashingAlgorithmIsInvalid() { + // Given + var properties = spy(createValidProperties()); + doReturn("some unknown algorithm").when(properties).getDefaultHashingAlgorithm(); + + // When & Then + assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); + } + + HissProperties createValidProperties() { + return new HissProperties() { + @Override + protected Set loadKeys() { + return Set.of(Key.builder() + .id("default_key") + .key(Base64.getDecoder().decode("AAAAAAAAAAAAAAAAAAAAAA==")) + .keyHash("$2a$12$3T0VMnGMgvesehYomommnO02dbFOJuM/3elsmgmsB2/qlGSF3BIbe") + .build()); + } + + @Override + protected String loadDefaultEncryptionKeyId() { + return "default_key"; + } + + @Override + protected String loadDefaultEncryptionAlgorithm() { + return "aes-128-gcm"; + } + + @Override + protected String loadDefaultHashingKeyId() { + return "default_key"; + } + + @Override + protected String loadDefaultHashingAlgorithm() { + return "hmac-sha256"; + } + + @Override + protected boolean loadKeyHashGenerationEnabled() { + return true; + } + }; + } + +} \ No newline at end of file