Skip to content

Commit

Permalink
Add key validation
Browse files Browse the repository at this point in the history
  • Loading branch information
mkay1375 committed Jul 25, 2024
1 parent 04fb063 commit 4282cc9
Show file tree
Hide file tree
Showing 15 changed files with 255 additions and 47 deletions.
9 changes: 8 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,16 @@
<mockito-junit-jupiter.version>5.11.0</mockito-junit-jupiter.version>
<junit-jupiter.version>5.10.2</junit-jupiter.version>
<jetbrains-annotations.version>24.1.0</jetbrains-annotations.version>
<bcrypt.version>0.10.2</bcrypt.version>
<lombok.version>1.18.30</lombok.version>
</properties>

<dependencies>
<dependency>
<groupId>at.favre.lib</groupId>
<artifactId>bcrypt</artifactId>
<version>${bcrypt.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
Expand All @@ -52,7 +59,7 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/io/github/tap30/hiss/Encrypted.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Fields annotated using this will be encrypted.
*/
// todo: add example
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Encrypted {
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/io/github/tap30/hiss/EncryptedInside.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// todo: improve doc
/**
* Used to tell Hiss to scan nested object.
* Fields annotated with this will be scanned
*/
// todo: add example
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface EncryptedInside {
Expand Down
95 changes: 78 additions & 17 deletions src/main/java/io/github/tap30/hiss/Hiss.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.github.tap30.hiss;

import io.github.tap30.hiss.key.KeyHashGenerator;
import io.github.tap30.hiss.properties.HissProperties;
import io.github.tap30.hiss.properties.HissPropertiesValidator;
import io.github.tap30.hiss.utils.EncryptionUtils;
import org.intellij.lang.annotations.Language;
import org.jetbrains.annotations.Nullable;
Expand All @@ -16,58 +16,119 @@ public class Hiss {
private final HissEncryptor hissEncryptor;
private final HissObjectEncryptor hissObjectEncryptor;

Hiss(HissProperties hissProperties) {
new HissPropertiesValidator().validate(hissProperties);
Hiss(HissProperties hissProperties, KeyHashGenerator 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}",
" 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()
});
if (hissProperties.isKeyHashGenerationEnabled()) {
keyHashGenerator.generateAndLogHashes(hissProperties.getKeys().values());
}
}

public String encrypt(String content) {
/**
* Encrypts the provided content with default key and default algorithm.
*
* @param content the content to be encrypted.
* @return encrypted content or null if the content is null.
*/
public String encrypt(@Nullable String content) {
return this.encrypt(content, "");
}

public String encrypt(String content, @Language("regexp") @Nullable String pattern) {
/**
* Encrypts parts of the provided content which match with the provided pattern
* with default key and default algorithm.
*
* @param content the content to be encrypted.
* @param pattern the pattern in regex format; null or empty pattern means to match all.
* @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);
}

public String decrypt(String content) {
/**
* Decrypts the provided content; if the content is encrypted partially (with pattern),
* only the encrypted parts will be decrypted.
* <br>Key ID and algorithm must be among loaded/supported keys and algorithms.
*
* @param content the content to be decrypted.
* @return decrypted content or null if the content is null.
* @throws IllegalArgumentException if key ID or algorithm is not loaded/supported.
*/
public String decrypt(@Nullable String content) {
return this.hissEncryptor.decrypt(content);
}

public String hash(String content) {
/**
* Hashes the provided content with default key and default algorithm.
*
* @param content the content to be hashed.
* @return hashed content or null if the content is null.
*/
public String hash(@Nullable String content) {
return this.hash(content, "");
}

public String hash(String content, @Language("regexp") @Nullable String pattern) {
/**
* Hashes parts of the provided content which match with the provided pattern
* with default key and default algorithm.
*
* @param content the content to be hashed.
* @param pattern the pattern in regex format; null or empty pattern means to match all.
* @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);
}

public boolean isEncrypted(String content) {
/**
* Tells whether the provider content is encrypted.
*
* @param content the content.
* @return <code>true</code> if the content or parts of it is encrypted.
*/
public boolean isEncrypted(@Nullable String content) {
return EncryptionUtils.isHavingEncryptionPattern(content);
}

public boolean isHashed(String content) {
/**
* Tells whether the provider content is hashed.
*
* @param content the content.
* @return <code>true</code> if the content or parts of it is hashed.
*/
public boolean isHashed(@Nullable String content) {
return EncryptionUtils.isHavingEncryptionPattern(content);
}

public void encryptObject(Object object) {
/**
* Encrypts and hashes fields of object annotated with {@link Encrypted} and {@link EncryptedInside}.
*
* @param object the annotated object; no action is taken on null objects.
*/
public void encryptObject(@Nullable Object object) {
this.hissObjectEncryptor.encryptObject(object);
}

public void decryptObject(Object object) {
/**
* Decrypts fields of object annotated with {@link Encrypted} and {@link EncryptedInside}.
*
* @param object the annotated object; no action is taken on null objects.
*/
public void decryptObject(@Nullable Object object) {
this.hissObjectEncryptor.decryptObject(object);
}

Expand Down
22 changes: 21 additions & 1 deletion src/main/java/io/github/tap30/hiss/HissFactory.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
package io.github.tap30.hiss;

import at.favre.lib.crypto.bcrypt.BCrypt;
import io.github.tap30.hiss.key.KeyHashGenerator;
import io.github.tap30.hiss.properties.HissProperties;
import io.github.tap30.hiss.properties.HissPropertiesValidator;

import java.util.logging.Logger;

public class HissFactory {

private static final Logger logger = Logger.getLogger(HissFactory.class.getName());

/**
* Creates a Hiss instance with provided <code>HissProperties</code>.
*
* @param hissProperties the properties by which hiss will be instantiated;
* {@link io.github.tap30.hiss.properties.HissPropertiesFromEnv}
* or any custom implementation of
* {@link io.github.tap30.hiss.properties.HissProperties}
* can be used.
* @return {@link Hiss} instance.
* @throws IllegalArgumentException if the properties are not valid.
*/
public static Hiss createHiss(HissProperties hissProperties) {
return new Hiss(hissProperties);
var keyHashGenerator = new KeyHashGenerator(BCrypt.withDefaults(), BCrypt.verifyer());
new HissPropertiesValidator(keyHashGenerator).validate(hissProperties);
return new Hiss(hissProperties, keyHashGenerator);
}

}
12 changes: 12 additions & 0 deletions src/main/java/io/github/tap30/hiss/key/Key.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.github.tap30.hiss.key;

import lombok.Builder;
import lombok.Value;

@Value
@Builder
public class Key {
String id;
byte[] key;
String keyHash;
}
58 changes: 58 additions & 0 deletions src/main/java/io/github/tap30/hiss/key/KeyHashGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.github.tap30.hiss.key;

import at.favre.lib.crypto.bcrypt.BCrypt;
import io.github.tap30.hiss.utils.StringUtils;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

public class KeyHashGenerator {

private static final Logger logger = Logger.getLogger(KeyHashGenerator.class.getName());
private static final Charset CHARSET = StandardCharsets.UTF_8;

private final BCrypt.Hasher hasher;
private final BCrypt.Verifyer verifyer;

public KeyHashGenerator(BCrypt.Hasher hasher, BCrypt.Verifyer verifyer) {
this.hasher = hasher;
this.verifyer = verifyer;
}

public void generateAndLogHashes(Collection<Key> keys) {
var result = new StringBuilder();
result.append("Keys' Hash:");
generateHashes(keys).forEach((k, v) -> result.append("\n ").append(k).append(": ").append(v));
logger.log(Level.INFO, result.toString());
}

/**
* @param keys
* @return map of key ID to key hash.
*/
public Map<String, String> generateHashes(Collection<Key> keys) {
var hashes = new HashMap<String, String>();
keys.forEach(k -> hashes.put(k.getId(), new String(hasher.hash(12, k.getKey()), CHARSET)));
return hashes;
}

/**
* @param keys
* @return invalid key IDs.
*/
public Set<String> validateKeyHashes(Collection<Key> keys) {
return keys.stream()
.filter(key -> StringUtils.hasText(key.getKeyHash()))
.filter(key -> !verifyer.verify(key.getKey(), key.getKeyHash().getBytes(CHARSET)).verified)
.map(Key::getId)
.collect(Collectors.toSet());
}

}
22 changes: 19 additions & 3 deletions src/main/java/io/github/tap30/hiss/properties/HissProperties.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
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.function.Supplier;
Expand All @@ -8,8 +11,16 @@ public abstract class HissProperties {

private final Map<String, Object> properties = new HashMap<>();

public Map<String, byte[]> getKeys() {
return this.getProperty("Keys", this::loadKeys);
public Map<String, Key> 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;
});
}

public String getDefaultEncryptionKeyId() {
Expand All @@ -28,11 +39,16 @@ public String getDefaultHashingAlgorithm() {
return this.getProperty("DefaultHashingAlgorithm", this::loadDefaultHashingAlgorithm);
}

protected abstract Map<String, byte[]> loadKeys();
public boolean isKeyHashGenerationEnabled() {
return this.getProperty("KeyHashGenerationEnabled", this::loadKeyHashGenerationEnabled);
}

protected abstract Map<String, Key> loadKeys();
protected abstract String loadDefaultEncryptionKeyId();
protected abstract String loadDefaultEncryptionAlgorithm();
protected abstract String loadDefaultHashingKeyId();
protected abstract String loadDefaultHashingAlgorithm();
protected abstract boolean loadKeyHashGenerationEnabled();

@SuppressWarnings("unchecked")
private <T> T getProperty(String key, Supplier<T> valueSupplier) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package io.github.tap30.hiss.properties;

import io.github.tap30.hiss.utils.KeyUtils;
import io.github.tap30.hiss.key.Key;
import lombok.Setter;

import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
Expand All @@ -27,19 +28,25 @@
public class HissPropertiesFromEnv extends HissProperties {

private static final String KEY_ENV_PREFIX = "HISS_KEYS_";
private static final String KEY_HASH_ENV_POSTFIX = "___HASH";

@Setter
@Setter // todo: make it package private
private Supplier<Map<String, String>> envProvider = System::getenv;

@Override
public Map<String, byte[]> loadKeys() {
var keys = new HashMap<String, String>();
public Map<String, Key> loadKeys() {
var keys = new HashMap<String, Key>();
envProvider.get().forEach((k, v) -> {
if (k.startsWith(KEY_ENV_PREFIX)) {
keys.put(k.replace(KEY_ENV_PREFIX, "").toLowerCase(), 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)
.key(Base64.getDecoder().decode(v))
.keyHash(envProvider.get().get(k + KEY_HASH_ENV_POSTFIX))
.build());
}
});
return KeyUtils.convertBase64KeysToByteArrayKeys(keys);
return keys;
}

@Override
Expand All @@ -61,4 +68,9 @@ public String loadDefaultHashingKeyId() {
public String loadDefaultHashingAlgorithm() {
return envProvider.get().get("HISS_DEFAULT_HASHING_ALGORITHM");
}

@Override
protected boolean loadKeyHashGenerationEnabled() {
return Boolean.parseBoolean(envProvider.get().get("HISS_KEY_HASH_GENERATION_ENABLED"));
}
}
Loading

0 comments on commit 4282cc9

Please sign in to comment.