├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── src ├── main │ └── java │ │ └── io │ │ └── github │ │ └── tap30 │ │ └── hiss │ │ ├── hasher │ │ ├── impl │ │ │ ├── TapsiHmacSha256Hasher.java │ │ │ └── HmacSha256Hasher.java │ │ └── Hasher.java │ │ ├── encryptor │ │ ├── impl │ │ │ ├── TapsiAesCbcEncryptor.java │ │ │ ├── TapsiAesGcmEncryptor.java │ │ │ ├── AesCbcPkcs5PaddingEncryptor.java │ │ │ ├── AesGcmNoPaddingEncryptor.java │ │ │ └── BaseJavaEncryptor.java │ │ └── Encryptor.java │ │ ├── properties │ │ ├── HissPropertiesProvider.java │ │ ├── HissPropertiesFromEnvProvider.java │ │ ├── HissProperties.java │ │ └── HissPropertiesValidator.java │ │ ├── EncryptedInside.java │ │ ├── key │ │ ├── Key.java │ │ └── KeyHashGenerator.java │ │ ├── utils │ │ ├── StringUtils.java │ │ └── ReflectionUtils.java │ │ ├── Encrypted.java │ │ ├── HissHasher.java │ │ ├── Hiss.java │ │ ├── HissEncryptor.java │ │ ├── HissFactory.java │ │ └── HissObjectEncryptor.java └── test │ └── java │ └── io │ └── github │ └── tap30 │ ├── hissapp │ ├── model │ │ ├── Admin.java │ │ ├── Secret.java │ │ ├── Address.java │ │ └── User.java │ ├── ApplicationWithInvalidKeyHashes.java │ └── Application.java │ └── hiss │ ├── encryptor │ ├── impl │ │ ├── TapsiAesCbcEncryptorTest.java │ │ ├── TapsiAesGcmEncryptorTest.java │ │ ├── AesGcmNoPaddingEncryptorTest.java │ │ └── AesCbcPkcs5PaddingEncryptorTest.java │ └── BaseEncryptorTest.java │ ├── hasher │ ├── impl │ │ ├── HmacSha256HasherTest.java │ │ └── TapsiHmacSha256HasherTest.java │ └── BaseHasherTest.java │ ├── BaseHissTest.java │ ├── utils │ ├── StringUtilsTest.java │ └── ReflectionUtilsTest.java │ ├── HissFactoryTest.java │ ├── properties │ ├── HissPropertiesFromEnvProviderTest.java │ └── HissPropertiesValidatorTest.java │ ├── EncryptedInsideAnnotationOnMapTest.java │ ├── key │ └── KeyHashGeneratorTest.java │ ├── HissHasherTest.java │ ├── EncryptedInsideAnnotationOnSimpleObjectTest.java │ ├── EncryptedInsideAnnotationOnListAndSetTest.java │ ├── EncryptedAnnotationTest.java │ ├── HissEncryptorTest.java │ └── HissTest.java ├── .gitignore ├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── pom.xml ├── mvnw.cmd ├── LICENSE ├── mvnw └── README.md /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tap30/hiss/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 3 | -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/hasher/impl/TapsiHmacSha256Hasher.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.hasher.impl; 2 | 3 | public class TapsiHmacSha256Hasher extends HmacSha256Hasher { 4 | @Override 5 | public String getName() { 6 | return "hmac-sha256"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/encryptor/impl/TapsiAesCbcEncryptor.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.encryptor.impl; 2 | 3 | public class TapsiAesCbcEncryptor extends AesCbcPkcs5PaddingEncryptor { 4 | @Override 5 | public String getName() { 6 | return "aes-128-cbc"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/encryptor/impl/TapsiAesGcmEncryptor.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.encryptor.impl; 2 | 3 | public class TapsiAesGcmEncryptor extends AesGcmNoPaddingEncryptor { 4 | @Override 5 | public String getName() { 6 | return "aes-128-gcm"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hissapp/model/Admin.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hissapp.model; 2 | 3 | import io.github.tap30.hiss.EncryptedInside; 4 | import lombok.Data; 5 | 6 | import java.util.Map; 7 | 8 | @Data 9 | public class Admin extends User { 10 | 11 | @EncryptedInside 12 | private Map secrets; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/properties/HissPropertiesProvider.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.properties; 2 | 3 | import io.github.tap30.hiss.key.Key; 4 | 5 | import java.util.Set; 6 | 7 | public interface HissPropertiesProvider { 8 | Set getKeys(); 9 | String getDefaultEncryptionKeyId(); 10 | String getDefaultEncryptionAlgorithm(); 11 | String getDefaultHashingKeyId(); 12 | String getDefaultHashingAlgorithm(); 13 | boolean isKeyHashGenerationEnabled(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/EncryptedInside.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Fields annotated with this will be scanned 10 | * to find nested fields having {@link Encrypted}. 11 | */ 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Target(ElementType.FIELD) 14 | public @interface EncryptedInside { 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/key/Key.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.key; 2 | 3 | import lombok.Builder; 4 | import lombok.Value; 5 | 6 | @Value 7 | @Builder 8 | public class Key { 9 | /** 10 | * The ID (name) of the key. 11 | */ 12 | String id; 13 | /** 14 | * The key itself. 15 | */ 16 | byte[] key; 17 | /** 18 | * The hash of the key generated by Hiss itself. 19 | * Used to validate the key to prevent accidental key modification. 20 | */ 21 | String keyHash; 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hissapp/model/Secret.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hissapp.model; 2 | 3 | import io.github.tap30.hiss.Encrypted; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.util.Set; 10 | 11 | @Data 12 | @Builder 13 | @AllArgsConstructor 14 | @NoArgsConstructor 15 | public class Secret { 16 | 17 | @Encrypted(hashingEnabled = false) 18 | private String secret; 19 | 20 | private Set applications; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/encryptor/impl/AesCbcPkcs5PaddingEncryptor.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.encryptor.impl; 2 | 3 | import javax.crypto.spec.IvParameterSpec; 4 | 5 | public class AesCbcPkcs5PaddingEncryptor extends BaseJavaEncryptor { 6 | 7 | private final static String ALGORITHM_NAME = "AES/CBC/PKCS5Padding"; 8 | 9 | public AesCbcPkcs5PaddingEncryptor() { 10 | super(ALGORITHM_NAME, "AES", 16, IvParameterSpec::new); 11 | } 12 | 13 | @Override 14 | public String getName() { 15 | return ALGORITHM_NAME; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/encryptor/impl/AesGcmNoPaddingEncryptor.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.encryptor.impl; 2 | 3 | import javax.crypto.spec.GCMParameterSpec; 4 | 5 | public class AesGcmNoPaddingEncryptor extends BaseJavaEncryptor { 6 | 7 | private static final String ALGORITHM_NAME = "AES/GCM/NoPadding"; 8 | 9 | public AesGcmNoPaddingEncryptor() { 10 | super(ALGORITHM_NAME, "AES", 16, iv -> new GCMParameterSpec(128, iv)); 11 | } 12 | 13 | @Override 14 | public String getName() { 15 | return ALGORITHM_NAME; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hiss/encryptor/impl/TapsiAesCbcEncryptorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.encryptor.impl; 2 | 3 | import io.github.tap30.hiss.encryptor.BaseEncryptorTest; 4 | 5 | class TapsiAesCbcEncryptorTest extends BaseEncryptorTest { 6 | 7 | protected TapsiAesCbcEncryptorTest() { 8 | super( 9 | new TapsiAesCbcEncryptor(), 10 | "aes-128-cbc", 11 | new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, 12 | "bzoCDPV5ddz6GEOm1PRt4V9nQJs4Dc6xRFcMea5xB9I=" 13 | ); 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hiss/encryptor/impl/TapsiAesGcmEncryptorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.encryptor.impl; 2 | 3 | import io.github.tap30.hiss.encryptor.BaseEncryptorTest; 4 | 5 | class TapsiAesGcmEncryptorTest extends BaseEncryptorTest { 6 | 7 | protected TapsiAesGcmEncryptorTest() { 8 | super( 9 | new TapsiAesGcmEncryptor(), 10 | "aes-128-gcm", 11 | new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, 12 | "2TtYw+dUzrPOPmvgorLoJAWSgXMDbrmz4BvcFA4+wnX1P6661DlbgrI=" 13 | ); 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/hasher/Hasher.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.hasher; 2 | 3 | public interface Hasher { 4 | /** 5 | * Calculates hash of provided content. 6 | * @return hash of content. 7 | */ 8 | byte[] hash(byte[] key, byte[] content) throws Exception; 9 | 10 | /** 11 | * Returns the hasher name which will be put in final hashed content. 12 | *
13 | * The name must not contain 14 | * '{', '}', ':', and '#$$#'. 15 | * @return hasher name. 16 | */ 17 | String getName(); 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hiss/hasher/impl/HmacSha256HasherTest.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.hasher.impl; 2 | 3 | import io.github.tap30.hiss.hasher.BaseHasherTest; 4 | 5 | class HmacSha256HasherTest extends BaseHasherTest { 6 | 7 | public HmacSha256HasherTest() { 8 | super( 9 | new HmacSha256Hasher(), 10 | "HmacSHA256", 11 | new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}, 12 | "ZjSgZLB+ebSU/dD72P6HULVSl6HoRFIEZNoYP9aqIRU=" 13 | ); 14 | } 15 | } -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hiss/encryptor/impl/AesGcmNoPaddingEncryptorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.encryptor.impl; 2 | 3 | import io.github.tap30.hiss.encryptor.BaseEncryptorTest; 4 | 5 | class AesGcmNoPaddingEncryptorTest extends BaseEncryptorTest { 6 | 7 | protected AesGcmNoPaddingEncryptorTest() { 8 | super( 9 | new AesGcmNoPaddingEncryptor(), 10 | "AES/GCM/NoPadding", 11 | new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, 12 | "2TtYw+dUzrPOPmvgorLoJAWSgXMDbrmz4BvcFA4+wnX1P6661DlbgrI=" 13 | ); 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hiss/encryptor/impl/AesCbcPkcs5PaddingEncryptorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.encryptor.impl; 2 | 3 | import io.github.tap30.hiss.encryptor.BaseEncryptorTest; 4 | 5 | class AesCbcPkcs5PaddingEncryptorTest extends BaseEncryptorTest { 6 | 7 | protected AesCbcPkcs5PaddingEncryptorTest() { 8 | super( 9 | new AesCbcPkcs5PaddingEncryptor(), 10 | "AES/CBC/PKCS5Padding", 11 | new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, 12 | "bzoCDPV5ddz6GEOm1PRt4V9nQJs4Dc6xRFcMea5xB9I=" 13 | ); 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hissapp/model/Address.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hissapp.model; 2 | 3 | import io.github.tap30.hiss.Encrypted; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Data 10 | @Builder 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | public class Address { 14 | 15 | private String name; 16 | 17 | private String city; 18 | 19 | @Encrypted(hashingEnabled = false) 20 | private String street; 21 | 22 | @Encrypted 23 | private String postalCode; 24 | private String hashedPostalCode; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hiss/hasher/impl/TapsiHmacSha256HasherTest.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.hasher.impl; 2 | 3 | import io.github.tap30.hiss.hasher.BaseHasherTest; 4 | 5 | class TapsiHmacSha256HasherTest extends BaseHasherTest { 6 | 7 | public TapsiHmacSha256HasherTest() { 8 | super( 9 | new TapsiHmacSha256Hasher(), 10 | "hmac-sha256", 11 | new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}, 12 | "ZjSgZLB+ebSU/dD72P6HULVSl6HoRFIEZNoYP9aqIRU=" 13 | ); 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hissapp/model/User.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hissapp.model; 2 | 3 | import io.github.tap30.hiss.Encrypted; 4 | import io.github.tap30.hiss.EncryptedInside; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | import java.util.List; 11 | 12 | @Data 13 | @Builder 14 | @AllArgsConstructor 15 | @NoArgsConstructor 16 | public class User { 17 | 18 | private String id; 19 | 20 | private String name; 21 | 22 | @Encrypted(pattern = "\\d{4}$") 23 | private String phoneNumber; 24 | private String hashedPhoneNumber; 25 | 26 | @EncryptedInside 27 | private List
addresses; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/hasher/impl/HmacSha256Hasher.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.hasher.impl; 2 | 3 | import io.github.tap30.hiss.hasher.Hasher; 4 | 5 | import javax.crypto.Mac; 6 | import javax.crypto.spec.SecretKeySpec; 7 | 8 | public class HmacSha256Hasher implements Hasher { 9 | 10 | private static final String HMAC_SHA256 = "HmacSHA256"; 11 | 12 | @Override 13 | public byte[] hash(byte[] key, byte[] content) throws Exception { 14 | var secretKeySpec = new SecretKeySpec(key, HMAC_SHA256); 15 | var mac = Mac.getInstance(HMAC_SHA256); 16 | mac.init(secretKeySpec); 17 | return mac.doFinal(content); 18 | } 19 | 20 | @Override 21 | public String getName() { 22 | return HMAC_SHA256; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/encryptor/Encryptor.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.encryptor; 2 | 3 | public interface Encryptor { 4 | /** 5 | * Encrypts content using key. 6 | * @return encrypted content. 7 | */ 8 | byte[] encrypt(byte[] key, byte[] content) throws Exception; 9 | 10 | /** 11 | * Decrypts content using key 12 | * @return plain content. 13 | */ 14 | byte[] decrypt(byte[] key, byte[] content) throws Exception; 15 | 16 | /** 17 | * Returns the encryptor name which will be put in final encrypted content. 18 | *
19 | * The name must not contain 20 | * '{', '}', ':', and '#$$#'. 21 | * @return encryptor name. 22 | */ 23 | String getName(); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/utils/StringUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.utils; 2 | 3 | public class StringUtils { 4 | 5 | public static boolean hasText(String text) { 6 | return text != null && !text.isBlank(); 7 | } 8 | 9 | public static String requireNonBlank(String text) { 10 | if (!hasText(text)) { 11 | throw new IllegalArgumentException(); 12 | } 13 | return text; 14 | } 15 | 16 | public static String toLowerCase(String text) { 17 | if (!hasText(text)) { 18 | return text; 19 | } 20 | return text.toLowerCase(); 21 | } 22 | 23 | public static String capitalizeFirstLetter(String text) { 24 | if (!hasText(text)) { 25 | return text; 26 | } 27 | return text.substring(0, 1).toUpperCase() + text.substring(1); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hissapp/ApplicationWithInvalidKeyHashes.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hissapp; 2 | 3 | import io.github.tap30.hiss.HissFactory; 4 | import io.github.tap30.hiss.properties.HissProperties; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.extension.ExtendWith; 7 | import uk.org.webcompere.systemstubs.environment.EnvironmentVariables; 8 | import uk.org.webcompere.systemstubs.jupiter.SystemStub; 9 | import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertThrows; 12 | 13 | @ExtendWith(SystemStubsExtension.class) 14 | public class ApplicationWithInvalidKeyHashes { 15 | 16 | @SystemStub 17 | EnvironmentVariables environment = new EnvironmentVariables(Application.environment.getVariables()) 18 | .set("HISS_KEYS_DEFAULT_KEY___HASH", "bad hash") 19 | .set("HISS_KEYS_OLD_KEY___HASH", "bad hash"); 20 | 21 | @Test 22 | void createHiss() { 23 | assertThrows(IllegalArgumentException.class, () -> HissFactory.createHiss(HissProperties.fromEnv())); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hiss/BaseHissTest.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss; 2 | 3 | import io.github.tap30.hiss.properties.HissProperties; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import uk.org.webcompere.systemstubs.environment.EnvironmentVariables; 7 | import uk.org.webcompere.systemstubs.jupiter.SystemStub; 8 | import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; 9 | 10 | @ExtendWith(SystemStubsExtension.class) 11 | public class BaseHissTest { 12 | 13 | @SystemStub 14 | private EnvironmentVariables environment = new EnvironmentVariables( 15 | "HISS_DEFAULT_ENCRYPTION_KEY_ID", "default_key", 16 | "HISS_DEFAULT_ENCRYPTION_ALGORITHM", "aes-128-gcm", 17 | "HISS_DEFAULT_HASHING_KEY_ID", "default_key", 18 | "HISS_DEFAULT_HASHING_ALGORITHM", "hmac-sha256", 19 | "HISS_KEYS_DEFAULT_KEY", "AAAAAAAAAAAAAAAAAAAAAA==", 20 | "HISS_KEY_HASH_GENERATION_ENABLED", "false" 21 | ); 22 | 23 | protected Hiss hiss; 24 | 25 | @BeforeEach 26 | void setUpHiss() { 27 | hiss = HissFactory.createHiss(HissProperties.fromEnv()); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Build 10 | 11 | on: 12 | push: 13 | branches: [ "main" ] 14 | pull_request: 15 | branches: [ "main" ] 16 | 17 | jobs: 18 | build: 19 | 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Set up JDK 11 25 | uses: actions/setup-java@v3 26 | with: 27 | java-version: '11' 28 | distribution: 'temurin' 29 | cache: maven 30 | - name: Build with Maven 31 | run: mvn -B package --file pom.xml 32 | 33 | # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive 34 | - name: Update dependency graph 35 | uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 36 | -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hiss/utils/StringUtilsTest.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.utils; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.*; 6 | 7 | class StringUtilsTest { 8 | 9 | @Test 10 | void hasText_whenTextIsNull() { 11 | assertFalse(StringUtils.hasText(null)); 12 | } 13 | 14 | @Test 15 | void hasText_whenTextIsEmpty() { 16 | assertFalse(StringUtils.hasText("")); 17 | assertFalse(StringUtils.hasText(" ")); 18 | } 19 | 20 | @Test 21 | void requireNonBlank() { 22 | assertEquals("salam", StringUtils.requireNonBlank("salam")); 23 | } 24 | 25 | @Test 26 | void requireNonBlank_whenTextIsBlank() { 27 | assertThrows(IllegalArgumentException.class, () -> StringUtils.requireNonBlank(" ")); 28 | } 29 | 30 | @Test 31 | void toLowerCase() { 32 | assertEquals("salam", StringUtils.toLowerCase("SALAM")); 33 | } 34 | 35 | @Test 36 | void toLowerCase_whenTextIsNull() { 37 | assertNull(StringUtils.toLowerCase(null)); 38 | } 39 | 40 | @Test 41 | void capitalizeFirstLetter() { 42 | assertEquals("Salam", StringUtils.capitalizeFirstLetter("salam")); 43 | } 44 | 45 | @Test 46 | void capitalizeFirstLetter_whenTextIsNull() { 47 | assertNull(StringUtils.capitalizeFirstLetter(null)); 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/Encrypted.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss; 2 | 3 | import org.intellij.lang.annotations.Identifier; 4 | import org.intellij.lang.annotations.Language; 5 | 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | /** 12 | * Fields annotated using this will be encrypted. 13 | */ 14 | @Retention(RetentionPolicy.RUNTIME) 15 | @Target(ElementType.FIELD) 16 | public @interface Encrypted { 17 | /** 18 | * @return the pattern which only matched content will be encrypted; 19 | * empty or null values mean all content should be encrypted. 20 | */ 21 | @Language("regexp") 22 | String pattern() default ""; 23 | 24 | /** 25 | * @return that should we calculate and store hash of content. 26 | * @see #hashFieldName() 27 | */ 28 | boolean hashingEnabled() default true; 29 | 30 | /** 31 | * @return name of the field in which hashed content will be put; 32 | * empty or null values mean the name will be guessed. 33 | *
34 | * The guessing algorithm is by concatenating "hashed" and first-letter-capitalized field name; 35 | * e.g. if the name of the encrypted field is phoneNumber, 36 | * guessed hashed field name is hashedPhoneNumber. 37 | */ 38 | @Identifier 39 | String hashFieldName() default ""; 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a package using Maven and then publish it to GitHub packages when a release is created 2 | # For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-path 3 | 4 | name: Publish package to the Maven Central Repository 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | publish: 12 | 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | packages: write 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Set up JDK 11 21 | uses: actions/setup-java@v3 22 | with: 23 | java-version: '11' 24 | distribution: 'temurin' 25 | server-id: central # Value of the distributionManagement/repository/id field of the pom.xml 26 | server-username: MAVEN_USERNAME 27 | server-password: MAVEN_PASSWORD 28 | settings-path: ${{ github.workspace }} # location for the settings.xml file 29 | gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} 30 | 31 | - name: Build with Maven 32 | run: mvn -B package --file pom.xml 33 | 34 | - name: Publish package 35 | run: mvn javadoc:jar source:jar gpg:sign deploy -s $GITHUB_WORKSPACE/settings.xml 36 | env: 37 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 38 | MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} 39 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 40 | -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/utils/ReflectionUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.utils; 2 | 3 | import java.lang.reflect.Field; 4 | import java.lang.reflect.InvocationTargetException; 5 | import java.lang.reflect.Method; 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | import java.util.Collections; 9 | import java.util.List; 10 | 11 | public class ReflectionUtils { 12 | 13 | public static T invokeSupplierMethod(Object object, Method supplier, Class targetType) { 14 | Object content; 15 | try { 16 | content = supplier.invoke(object); 17 | } catch (IllegalAccessException | InvocationTargetException e) { 18 | throw new RuntimeException(e); 19 | } 20 | if (content == null) { 21 | return null; 22 | } 23 | if (targetType.isAssignableFrom(content.getClass())) { 24 | @SuppressWarnings("unchecked") 25 | var castedContent = (T) content; 26 | return castedContent; 27 | } else { 28 | throw new ClassCastException(String.format( 29 | "Cast error for content of method %s: wanted %s but got %s", 30 | supplier.getName(), targetType.getName(), content.getClass().getName() 31 | )); 32 | } 33 | } 34 | 35 | public static List getAllFields(Class clazz) { 36 | var objectFields = new ArrayList(); 37 | for (var objectClass = clazz; objectClass != null; objectClass = objectClass.getSuperclass()) { 38 | objectFields.addAll(Arrays.asList(objectClass.getDeclaredFields())); 39 | } 40 | return Collections.unmodifiableList(objectFields); 41 | } 42 | 43 | public static Method getMethod(Class clazz, String methodName, Class... parameterTypes) { 44 | try { 45 | return clazz.getDeclaredMethod(methodName, parameterTypes); 46 | } catch (NoSuchMethodException e) { 47 | if (clazz.getSuperclass() != null) { 48 | return getMethod(clazz.getSuperclass(), methodName, parameterTypes); 49 | } 50 | throw new RuntimeException(e); 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hiss/hasher/BaseHasherTest.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.hasher; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.Base64; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | public abstract class BaseHasherTest { 11 | 12 | protected final Hasher hasher; 13 | protected final String hasherName; 14 | protected final byte[] key; 15 | 16 | protected final String plainText = "some text"; 17 | protected final byte[] plainTextBytes = plainText.getBytes(); 18 | 19 | protected final String encodedHashedText; 20 | protected final byte[] hashedTextBytes; 21 | 22 | protected BaseHasherTest(Hasher hasher, String hasherName, byte[] key, String encodedHashedText) { 23 | this.hasher = hasher; 24 | this.hasherName = hasherName; 25 | this.key = key; 26 | this.encodedHashedText = encodedHashedText; 27 | this.hashedTextBytes = base64(encodedHashedText); 28 | } 29 | 30 | protected String base64(byte[] bytes) { 31 | return Base64.getEncoder().encodeToString(bytes); 32 | } 33 | 34 | protected byte[] base64(String base64) { 35 | return Base64.getDecoder().decode(base64); 36 | } 37 | 38 | @Test 39 | void hash() throws Exception { 40 | // When 41 | var hash = hasher.hash(key, plainTextBytes); 42 | 43 | // Then 44 | System.out.printf("Base64 Encoded Hash of Content: %s\n", base64(hash)); 45 | assertArrayEquals(hashedTextBytes, hash); 46 | } 47 | 48 | @Test 49 | void hash_producesSameHashForSameInput() throws Exception { 50 | // When 51 | var hash1 = hasher.hash(key, plainTextBytes); 52 | var hash2 = hasher.hash(key, plainTextBytes); 53 | var hash3 = hasher.hash(key, plainTextBytes); 54 | 55 | // Then 56 | assertArrayEquals(hashedTextBytes, hash1); 57 | assertArrayEquals(hashedTextBytes, hash2); 58 | assertArrayEquals(hashedTextBytes, hash3); 59 | } 60 | 61 | @Test 62 | void getName() { 63 | assertEquals(hasherName, hasher.getName()); 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hiss/HissFactoryTest.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss; 2 | 3 | import io.github.tap30.hiss.key.Key; 4 | import io.github.tap30.hiss.key.KeyHashGenerator; 5 | import io.github.tap30.hiss.properties.HissProperties; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.util.Base64; 9 | import java.util.Set; 10 | 11 | import static org.junit.jupiter.api.Assertions.*; 12 | import static org.mockito.ArgumentMatchers.any; 13 | import static org.mockito.Mockito.mock; 14 | import static org.mockito.Mockito.verify; 15 | 16 | class HissFactoryTest { 17 | 18 | @Test 19 | void createHiss() { 20 | // Given 21 | var properties = HissProperties.builder() 22 | .keys(Set.of(Key.builder() 23 | .id("default_key") 24 | .key(Base64.getDecoder().decode("AAAAAAAAAAAAAAAAAAAAAA==")) 25 | .keyHash("$2a$12$3T0VMnGMgvesehYomommnO02dbFOJuM/3elsmgmsB2/qlGSF3BIbe") 26 | .build())) 27 | .defaultEncryptionKeyId("default_key") 28 | .defaultEncryptionAlgorithm("aes-128-gcm") 29 | .defaultHashingKeyId("default_key") 30 | .defaultHashingAlgorithm("hmac-sha256") 31 | .keyHashGenerationEnabled(true) 32 | .build(); 33 | var keyHashGenerator = mock(KeyHashGenerator.class); 34 | HissFactory.keyHashGeneratorProvider = () -> keyHashGenerator; 35 | 36 | // When 37 | var hiss = HissFactory.createHiss(properties); 38 | 39 | // Then 40 | assertNotNull(hiss); 41 | assertEquals("#$$#{hmac-sha256:default_key}{izfsg2N2nlwGtgNPzfwTWFUFIb5xJTvV5qEsRRUODmk=}#$$#", 42 | hiss.hash("some encrypted text")); 43 | assertEquals("some encrypted text", 44 | hiss.decrypt("#$$#{aes-128-gcm:default_key}{nYf5c6FQYJCQdc6JcfqTkvaSwqTGg2Oh0fapibp94G4anlMeXZrCAOZLOhMD3QIJymv/}#$$#")); 45 | verify(keyHashGenerator).validateKeyHashes(any()); 46 | verify(keyHashGenerator).generateAndLogHashes(any()); 47 | } 48 | 49 | @Test 50 | void createHiss_shouldPropertiesBeingValidated() { 51 | assertThrows(IllegalArgumentException.class, () -> 52 | HissFactory.createHiss(HissProperties.builder().build())); 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hiss/encryptor/BaseEncryptorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.encryptor; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.Base64; 6 | 7 | import static org.junit.jupiter.api.Assertions.*; 8 | 9 | public abstract class BaseEncryptorTest { 10 | 11 | protected final Encryptor encryptor; 12 | protected final String encryptorName; 13 | protected final byte[] key; 14 | 15 | protected final String plainText = "some text"; 16 | protected final byte[] plainTextBytes = plainText.getBytes(); 17 | 18 | protected final String encodedEncryptedText; 19 | protected final byte[] encryptedTextBytes; 20 | 21 | protected BaseEncryptorTest(Encryptor encryptor, 22 | String encryptorName, 23 | byte[] key, 24 | String encodedEncryptedText) { 25 | this.encryptor = encryptor; 26 | this.encryptorName = encryptorName; 27 | this.key = key; 28 | this.encodedEncryptedText = encodedEncryptedText; 29 | this.encryptedTextBytes = base64(encodedEncryptedText); 30 | } 31 | 32 | protected String base64(byte[] bytes) { 33 | return Base64.getEncoder().encodeToString(bytes); 34 | } 35 | 36 | protected byte[] base64(String base64) { 37 | return Base64.getDecoder().decode(base64); 38 | } 39 | 40 | @Test 41 | void encrypt() throws Exception { 42 | // When 43 | var encrypted = encryptor.encrypt(key, plainTextBytes); 44 | 45 | // Then 46 | assertNotEquals(plainTextBytes, encrypted); 47 | System.out.printf("Base64 Encoded Encrypted Content: %s\n", base64(encrypted)); 48 | } 49 | 50 | @Test 51 | void decrypt() throws Exception { 52 | // When 53 | var plain = encryptor.decrypt(key, encryptedTextBytes); 54 | 55 | // Then 56 | assertArrayEquals(plainTextBytes, plain); 57 | } 58 | 59 | @Test 60 | void encryptAndDecrypt() throws Exception { 61 | // When 62 | var encrypted = encryptor.encrypt(key, plainTextBytes); 63 | var plain = encryptor.decrypt(key, encrypted); 64 | 65 | // Then 66 | assertArrayEquals(plainTextBytes, plain); 67 | } 68 | 69 | @Test 70 | void getName() { 71 | assertEquals(encryptorName, encryptor.getName()); 72 | } 73 | 74 | } -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/key/KeyHashGenerator.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.key; 2 | 3 | import at.favre.lib.crypto.bcrypt.BCrypt; 4 | import io.github.tap30.hiss.utils.StringUtils; 5 | 6 | import java.nio.charset.Charset; 7 | import java.nio.charset.StandardCharsets; 8 | import java.util.Collection; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | import java.util.Set; 12 | import java.util.logging.Level; 13 | import java.util.logging.Logger; 14 | import java.util.stream.Collectors; 15 | 16 | public class KeyHashGenerator { 17 | 18 | private static final Logger logger = Logger.getLogger(KeyHashGenerator.class.getName()); 19 | private static final Charset CHARSET = StandardCharsets.UTF_8; 20 | 21 | private final BCrypt.Hasher hasher; 22 | private final BCrypt.Verifyer verifyer; 23 | 24 | public KeyHashGenerator(BCrypt.Hasher hasher, BCrypt.Verifyer verifyer) { 25 | this.hasher = hasher; 26 | this.verifyer = verifyer; 27 | } 28 | 29 | public void generateAndLogHashes(Collection keys) { 30 | var result = new StringBuilder(); 31 | result.append("Keys' Hash:"); 32 | generateHashes(keys).forEach((k, v) -> result.append("\n ").append(k).append(": ").append(v)); 33 | logger.log(Level.INFO, result.toString()); 34 | } 35 | 36 | /** 37 | * @return map of key ID to key hash. 38 | */ 39 | public Map generateHashes(Collection keys) { 40 | var hashes = new HashMap(); 41 | keys.forEach(k -> hashes.put(k.getId(), new String(hasher.hash(12, k.getKey()), CHARSET))); 42 | return hashes; 43 | } 44 | 45 | /** 46 | * @return invalid key IDs. 47 | */ 48 | public Set validateKeyHashes(Collection keys) { 49 | return keys.stream() 50 | .filter(key -> { 51 | if (StringUtils.hasText(key.getKeyHash())) { 52 | return true; 53 | } else { 54 | logger.log(Level.WARNING, 55 | "Key {0} does not have hash; supply it as soon as possible.", 56 | key.getId()); 57 | return false; 58 | } 59 | }) 60 | .filter(key -> !verifyer.verify(key.getKey(), key.getKeyHash().getBytes(CHARSET)).verified) 61 | .map(Key::getId) 62 | .collect(Collectors.toSet()); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hiss/properties/HissPropertiesFromEnvProviderTest.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.properties; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import uk.org.webcompere.systemstubs.environment.EnvironmentVariables; 6 | import uk.org.webcompere.systemstubs.jupiter.SystemStub; 7 | import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; 8 | 9 | import java.nio.charset.StandardCharsets; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | 14 | @ExtendWith(SystemStubsExtension.class) 15 | class HissPropertiesFromEnvProviderTest { 16 | 17 | @SystemStub 18 | EnvironmentVariables environment = new EnvironmentVariables( 19 | "HISS_DEFAULT_ENCRYPTION_KEY_ID", "default_enc_key", 20 | "HISS_DEFAULT_ENCRYPTION_ALGORITHM", "aes-128-gcm", 21 | "HISS_DEFAULT_HASHING_KEY_ID", "default_hash_key", 22 | "HISS_DEFAULT_HASHING_ALGORITHM", "hmac-sha256", 23 | "HISS_KEYS_DEFAULT_KEY", "dGhlIGFjdHVhbCBrZXkK", 24 | "HISS_KEYS_DEFAULT_KEY___HASH", "some hash", 25 | "HISS_KEYS_OTHER_KEY", "dGhlIGFjdHVhbCBvdGhlciBrZXkK", 26 | "HISS_KEYS_OTHER_KEY___HASH", "other key hash", 27 | "HISS_KEY_HASH_GENERATION_ENABLED", "true" 28 | ); 29 | 30 | @Test 31 | void test() { 32 | // Given & When 33 | var hissProperties = HissProperties.fromEnv(); 34 | 35 | // Then 36 | assertEquals(2, hissProperties.getKeys().size()); 37 | assertEquals("default_key", hissProperties.getKeys().get("default_key").getId()); 38 | assertArrayEquals("the actual key\n".getBytes(StandardCharsets.US_ASCII), hissProperties.getKeys().get("default_key").getKey()); 39 | assertEquals("some hash", hissProperties.getKeys().get("default_key").getKeyHash()); 40 | assertEquals("other_key", hissProperties.getKeys().get("other_key").getId()); 41 | assertArrayEquals("the actual other key\n".getBytes(StandardCharsets.US_ASCII), hissProperties.getKeys().get("other_key").getKey()); 42 | assertEquals("other key hash", hissProperties.getKeys().get("other_key").getKeyHash()); 43 | assertEquals("default_enc_key", hissProperties.getDefaultEncryptionKeyId()); 44 | assertEquals("aes-128-gcm", hissProperties.getDefaultEncryptionAlgorithm()); 45 | assertEquals("default_hash_key", hissProperties.getDefaultHashingKeyId()); 46 | assertEquals("hmac-sha256", hissProperties.getDefaultHashingAlgorithm()); 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/encryptor/impl/BaseJavaEncryptor.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.encryptor.impl; 2 | 3 | import io.github.tap30.hiss.encryptor.Encryptor; 4 | 5 | import javax.crypto.Cipher; 6 | import javax.crypto.spec.SecretKeySpec; 7 | import java.security.SecureRandom; 8 | import java.security.spec.AlgorithmParameterSpec; 9 | import java.util.Objects; 10 | import java.util.function.Function; 11 | 12 | public abstract class BaseJavaEncryptor implements Encryptor { 13 | 14 | private static final SecureRandom SECURE_RANDOM = new SecureRandom(); 15 | 16 | private final String algorithmName; 17 | private final String keyAlgorithmName; 18 | private final int ivLength; 19 | private final Function algorithmParameterSpecSupplier; 20 | 21 | protected BaseJavaEncryptor(String algorithmName, 22 | String keyAlgorithmName, 23 | int ivLength, 24 | Function algorithmParameterSpecSupplier) { 25 | this.algorithmName = Objects.requireNonNull(algorithmName); 26 | this.keyAlgorithmName = Objects.requireNonNull(keyAlgorithmName); 27 | this.ivLength = ivLength; 28 | this.algorithmParameterSpecSupplier = Objects.requireNonNull(algorithmParameterSpecSupplier); 29 | } 30 | 31 | 32 | @Override 33 | public byte[] encrypt(byte[] key, byte[] content) throws Exception { 34 | var secretKeySpec = new SecretKeySpec(key, keyAlgorithmName); 35 | var cipher = Cipher.getInstance(algorithmName); 36 | 37 | var iv = new byte[ivLength]; 38 | SECURE_RANDOM.nextBytes(iv); 39 | 40 | cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, algorithmParameterSpecSupplier.apply(iv)); 41 | 42 | var encryptedBytes = cipher.doFinal(content); 43 | var encryptedIvAndContent = new byte[iv.length + encryptedBytes.length]; 44 | System.arraycopy(iv, 0, encryptedIvAndContent, 0, iv.length); 45 | System.arraycopy(encryptedBytes, 0, encryptedIvAndContent, iv.length, encryptedBytes.length); 46 | 47 | return encryptedIvAndContent; 48 | } 49 | 50 | @Override 51 | public byte[] decrypt(byte[] key, byte[] content) throws Exception { 52 | var iv = new byte[ivLength]; 53 | var encryptedBytes = new byte[content.length - iv.length]; 54 | System.arraycopy(content, 0, iv, 0, iv.length); 55 | System.arraycopy(content, iv.length, encryptedBytes, 0, encryptedBytes.length); 56 | 57 | var secretKeySpec = new SecretKeySpec(key, keyAlgorithmName); 58 | Cipher cipher = Cipher.getInstance(algorithmName); 59 | cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, algorithmParameterSpecSupplier.apply(iv)); 60 | 61 | return cipher.doFinal(encryptedBytes); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/HissHasher.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss; 2 | 3 | import io.github.tap30.hiss.hasher.Hasher; 4 | import io.github.tap30.hiss.key.Key; 5 | import io.github.tap30.hiss.utils.StringUtils; 6 | 7 | import java.nio.charset.Charset; 8 | import java.nio.charset.StandardCharsets; 9 | import java.util.Map; 10 | import java.util.Objects; 11 | import java.util.regex.Matcher; 12 | import java.util.regex.Pattern; 13 | 14 | class HissHasher { 15 | 16 | private static final Charset CHARSET = StandardCharsets.UTF_8; 17 | 18 | private final Map hashers; 19 | private final Map keys; 20 | private final String defaultHashingAlgorithm; 21 | private final String defaultHashingKeyId; 22 | 23 | public HissHasher(Map hashers, 24 | Map keys, 25 | String defaultHashingAlgorithm, 26 | String defaultHashingKeyId) { 27 | this.hashers = Objects.requireNonNull(hashers); 28 | this.keys = Objects.requireNonNull(keys); 29 | this.defaultHashingAlgorithm = StringUtils.requireNonBlank(defaultHashingAlgorithm); 30 | this.defaultHashingKeyId = StringUtils.requireNonBlank(defaultHashingKeyId); 31 | } 32 | 33 | public String hash(String content, String pattern) throws Exception { 34 | if (!StringUtils.hasText(content) || HissEncryptor.isHavingEncryptedContentPattern(content)) { 35 | return content; 36 | } 37 | 38 | var hasher = Objects.requireNonNull(hashers.get(defaultHashingAlgorithm), 39 | "Algorithm not supported: " + defaultHashingAlgorithm); 40 | var key = Objects.requireNonNull(keys.get(defaultHashingKeyId), 41 | "Key not found: " + defaultHashingKeyId); 42 | 43 | if (StringUtils.hasText(pattern)) { 44 | StringBuilder result = new StringBuilder(); 45 | Matcher matcher = Pattern.compile(pattern).matcher(content); 46 | 47 | while (matcher.find()) { 48 | var partToBeEncrypted = matcher.group(); 49 | var hashedContent = hash(hasher, key, partToBeEncrypted); 50 | matcher.appendReplacement(result, Matcher.quoteReplacement(hashedContent)); 51 | } 52 | matcher.appendTail(result); 53 | 54 | return result.toString(); 55 | } else { 56 | return hash(hasher, key, content); 57 | } 58 | } 59 | 60 | public boolean isHashed(String content) { 61 | return HissEncryptor.isHavingEncryptedContentPattern(content); 62 | } 63 | 64 | private String hash(Hasher hasher, Key key, String content) throws Exception { 65 | var contentBytes = content.getBytes(CHARSET); 66 | var hash = hasher.hash(key.getKey(), contentBytes); 67 | return HissEncryptor.formatEncryptedBytes(hasher.getName(), key.getId(), hash); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hiss/utils/ReflectionUtilsTest.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.utils; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.*; 6 | 7 | class ReflectionUtilsTest { 8 | 9 | @Test 10 | void invokeSupplierMethod() throws NoSuchMethodException { 11 | // Given 12 | var aClassInstance = new AClass(); 13 | var nameGetterMethod = aClassInstance.getClass().getDeclaredMethod("getName"); 14 | 15 | // When 16 | var content = ReflectionUtils.invokeSupplierMethod(aClassInstance, nameGetterMethod, String.class); 17 | 18 | // Then 19 | assertEquals("Mamad", content); 20 | } 21 | 22 | @Test 23 | void invokeSupplierMethod_whenTypeNotMatches() throws NoSuchMethodException { 24 | // Given 25 | var aClassInstance = new AClass(); 26 | var nameGetterMethod = aClassInstance.getClass().getDeclaredMethod("getName"); 27 | 28 | // When & Then 29 | assertThrows(ClassCastException.class, () -> ReflectionUtils.invokeSupplierMethod(aClassInstance, nameGetterMethod, Integer.class)); 30 | } 31 | 32 | @Test 33 | void getAllFields() { 34 | // Given 35 | var instance = new Level3(); 36 | 37 | // When 38 | var fields = ReflectionUtils.getAllFields(instance.getClass()); 39 | 40 | // Then 41 | assertEquals(3, fields.size()); 42 | assertEquals("level3Field", fields.get(0).getName()); 43 | assertEquals("level2Field", fields.get(1).getName()); 44 | assertEquals("level1Field", fields.get(2).getName()); 45 | } 46 | 47 | @Test 48 | void getMethod() { 49 | // Given 50 | var instance = new Level3(); 51 | 52 | // When 53 | var level1Method = ReflectionUtils.getMethod(instance.getClass(), "level1Method"); 54 | var level2Method = ReflectionUtils.getMethod(instance.getClass(), "level2Method"); 55 | var level3Method = ReflectionUtils.getMethod(instance.getClass(), "level3Method"); 56 | 57 | // Then 58 | assertNotNull(level1Method); 59 | assertNotNull(level2Method); 60 | assertNotNull(level3Method); 61 | assertEquals("level1Method", level1Method.getName()); 62 | assertEquals("level2Method", level2Method.getName()); 63 | assertEquals("level3Method", level3Method.getName()); 64 | } 65 | 66 | public static class AClass { 67 | public String getName() { 68 | return "Mamad"; 69 | } 70 | } 71 | 72 | public static class Level1 { 73 | private String level1Field; 74 | public void level1Method() { 75 | } 76 | } 77 | 78 | public static class Level2 extends Level1 { 79 | private String level2Field; 80 | public void level2Method() { 81 | } 82 | } 83 | 84 | public static class Level3 extends Level2 { 85 | private String level3Field; 86 | public void level3Method() { 87 | } 88 | } 89 | 90 | } -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hiss/EncryptedInsideAnnotationOnMapTest.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.util.Map; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.junit.jupiter.api.Assertions.assertNotEquals; 12 | 13 | public class EncryptedInsideAnnotationOnMapTest extends BaseHissTest { 14 | 15 | @Test 16 | void encryptAndDecryptDomainObject() { 17 | // Given 18 | 19 | var user = User.builder() 20 | .id("5") 21 | .phoneNumber("123456789") 22 | .addressMap(Map.of( 23 | "address1", 24 | Address.builder() 25 | .city("list city 1") 26 | .postalCode("LC1") 27 | .build(), 28 | "address2", 29 | Address.builder() 30 | .city("list city 2") 31 | .postalCode("LC2") 32 | .build() 33 | )) 34 | .build(); 35 | 36 | // When 37 | this.hiss.encryptObject(user); 38 | 39 | // Then 40 | { 41 | assertEquals("5", user.getId()); 42 | assertNotEquals("123456789", user.getPhoneNumber()); 43 | 44 | assertEquals(2, user.getAddressMap().size()); 45 | var listAddress1 = user.getAddressMap().get("address1"); 46 | assertEquals("list city 1", listAddress1.getCity()); 47 | assertNotEquals("LC1", listAddress1.getPostalCode()); 48 | var listAddress2 = user.getAddressMap().get("address2"); 49 | assertEquals("list city 2", listAddress2.getCity()); 50 | assertNotEquals("LC2", listAddress2.getPostalCode()); 51 | } 52 | 53 | 54 | // When 55 | this.hiss.decryptObject(user); 56 | 57 | // Then 58 | { 59 | assertEquals("5", user.getId()); 60 | assertEquals("123456789", user.getPhoneNumber()); 61 | 62 | assertEquals(2, user.getAddressMap().size()); 63 | var listAddress1 = user.getAddressMap().get("address1"); 64 | assertEquals("list city 1", listAddress1.getCity()); 65 | assertEquals("LC1", listAddress1.getPostalCode()); 66 | var listAddress2 = user.getAddressMap().get("address2"); 67 | assertEquals("list city 2", listAddress2.getCity()); 68 | assertEquals("LC2", listAddress2.getPostalCode()); 69 | } 70 | } 71 | 72 | @Getter 73 | @Setter 74 | @Builder 75 | public static class User { 76 | private String id; 77 | @Encrypted(hashingEnabled = false) 78 | private String phoneNumber; 79 | @EncryptedInside 80 | private Map addressMap; 81 | } 82 | 83 | @Getter 84 | @Setter 85 | @Builder 86 | public static class Address { 87 | private String city; 88 | @Encrypted(hashingEnabled = false) 89 | private String postalCode; 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/properties/HissPropertiesFromEnvProvider.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.properties; 2 | 3 | import io.github.tap30.hiss.key.Key; 4 | 5 | import java.util.Base64; 6 | import java.util.HashSet; 7 | import java.util.Map; 8 | import java.util.Set; 9 | import java.util.function.Supplier; 10 | 11 | /** 12 | * Provides {@link HissProperties} from environment variables. 13 | *
14 | * Here's the mapping of the HissProperties field to environment variables: 15 | *
16 | *
    17 | *
  • 18 | * keys: 19 | *
      20 | *
    • HISS_KEYS_{Key ID}: the base64 encoded representation of the key.
    • 21 | *
    • HISS_KEYS_{Key ID}__HASH: the hash of the key.
    • 22 | *
    23 | *
  • 24 | *
  • 25 | * defaultEncryptionKeyId: HISS_DEFAULT_ENCRYPTION_ALGORITHM 26 | *
  • 27 | *
  • 28 | * defaultEncryptionAlgorithm: HISS_DEFAULT_ENCRYPTION_ALGORITHM 29 | *
  • 30 | *
  • 31 | * defaultHashingKeyId: HISS_DEFAULT_HASHING_KEY_ID 32 | *
  • 33 | *
  • 34 | * defaultHashingAlgorithm: HISS_DEFAULT_HASHING_ALGORITHM 35 | *
  • 36 | *
  • 37 | * keyHashGenerationEnabled: HISS_KEY_HASH_GENERATION_ENABLED 38 | *
  • 39 | *
40 | * 41 | * @see HissProperties 42 | */ 43 | public class HissPropertiesFromEnvProvider implements HissPropertiesProvider { 44 | 45 | private static final String KEY_ENV_PREFIX = "HISS_KEYS_"; 46 | private static final String KEY_HASH_ENV_POSTFIX = "___HASH"; 47 | 48 | private static final Supplier> ENV_PROVIDER = System::getenv; 49 | 50 | @Override 51 | public Set getKeys() { 52 | var keys = new HashSet(); 53 | ENV_PROVIDER.get().forEach((k, v) -> { 54 | if (k.startsWith(KEY_ENV_PREFIX) && !k.endsWith(KEY_HASH_ENV_POSTFIX)) { 55 | keys.add(Key.builder() 56 | .id(k.replace(KEY_ENV_PREFIX, "").toLowerCase()) 57 | .key(Base64.getDecoder().decode(v)) 58 | .keyHash(ENV_PROVIDER.get().get(k + KEY_HASH_ENV_POSTFIX)) 59 | .build()); 60 | } 61 | }); 62 | return keys; 63 | } 64 | 65 | @Override 66 | public String getDefaultEncryptionKeyId() { 67 | return ENV_PROVIDER.get().get("HISS_DEFAULT_ENCRYPTION_KEY_ID"); 68 | } 69 | 70 | @Override 71 | public String getDefaultEncryptionAlgorithm() { 72 | return ENV_PROVIDER.get().get("HISS_DEFAULT_ENCRYPTION_ALGORITHM"); 73 | } 74 | 75 | @Override 76 | public String getDefaultHashingKeyId() { 77 | return ENV_PROVIDER.get().get("HISS_DEFAULT_HASHING_KEY_ID"); 78 | } 79 | 80 | @Override 81 | public String getDefaultHashingAlgorithm() { 82 | return ENV_PROVIDER.get().get("HISS_DEFAULT_HASHING_ALGORITHM"); 83 | } 84 | 85 | @Override 86 | public boolean isKeyHashGenerationEnabled() { 87 | return Boolean.parseBoolean(ENV_PROVIDER.get().get("HISS_KEY_HASH_GENERATION_ENABLED")); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hiss/key/KeyHashGeneratorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.key; 2 | 3 | import at.favre.lib.crypto.bcrypt.BCrypt; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.nio.charset.StandardCharsets; 7 | import java.util.Set; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertTrue; 11 | import static org.mockito.Mockito.spy; 12 | import static org.mockito.Mockito.verify; 13 | 14 | class KeyHashGeneratorTest { 15 | 16 | BCrypt.Hasher hasher = BCrypt.withDefaults(); 17 | BCrypt.Verifyer verifier = BCrypt.verifyer(); 18 | KeyHashGenerator keyHashGenerator = new KeyHashGenerator(hasher, verifier); 19 | 20 | @Test 21 | void generateHashes() { 22 | // Given 23 | var keys = Set.of( 24 | Key.builder().id("key1").key(new byte[]{1, 2, 3}).build(), 25 | Key.builder().id("key2").key(new byte[]{4, 5, 6}).build() 26 | ); 27 | 28 | // When 29 | var hashes = keyHashGenerator.generateHashes(keys); 30 | System.out.println(hashes); 31 | 32 | // Then 33 | assertEquals(2, hashes.size()); 34 | assertTrue(verifier.verify(new byte[]{1, 2, 3}, hashes.get("key1").getBytes(StandardCharsets.UTF_8)).verified); 35 | assertTrue(verifier.verify(new byte[]{4, 5, 6}, hashes.get("key2").getBytes(StandardCharsets.UTF_8)).verified); 36 | } 37 | 38 | @Test 39 | void validateKeyHashes() { 40 | // Given 41 | var keys = Set.of( 42 | Key.builder() 43 | .id("key1") 44 | .key(new byte[]{1, 2, 3}) 45 | .keyHash("$2a$12$tvVEa2yZ/RhSbYg16GG5hO5a/2P9HWVWM8c8ISZpgLIRvlF3EzVgm") 46 | .build(), 47 | Key.builder() 48 | .id("key2") 49 | .key(new byte[]{4, 5, 6}) 50 | .keyHash("$2a$12$fwlilo5GtK44245Xcg57HuvNDEhJM7snmQ7VOO2LQfGvtvOk8tbpS") 51 | .build() 52 | ); 53 | 54 | // When 55 | var invalidKeys = keyHashGenerator.validateKeyHashes(keys); 56 | 57 | // Then 58 | assertEquals(0, invalidKeys.size()); 59 | } 60 | 61 | @Test 62 | void validateKeyHashes_whenKeyHashesAreInvalid() { 63 | // Given 64 | var keys = Set.of( 65 | Key.builder() 66 | .id("key1") 67 | .key(new byte[]{1, 2, 3}) 68 | .keyHash("chert1") 69 | .build(), 70 | Key.builder() 71 | .id("key2") 72 | .key(new byte[]{4, 5, 6}) 73 | .keyHash("chert2") 74 | .build() 75 | ); 76 | 77 | // When 78 | var invalidKeys = keyHashGenerator.validateKeyHashes(keys); 79 | 80 | // Then 81 | assertEquals(2, invalidKeys.size()); 82 | assertTrue(invalidKeys.contains("key1")); 83 | assertTrue(invalidKeys.contains("key2")); 84 | } 85 | 86 | @Test 87 | void generateAndLogAllHashes() { 88 | // Given 89 | var keys = Set.of(); 90 | var keyHashGenerator = spy(this.keyHashGenerator); 91 | 92 | // When 93 | keyHashGenerator.generateAndLogHashes(keys); 94 | 95 | // Then 96 | verify(keyHashGenerator).generateHashes(keys); 97 | } 98 | 99 | } -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hiss/HissHasherTest.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss; 2 | 3 | import io.github.tap30.hiss.hasher.Hasher; 4 | import io.github.tap30.hiss.key.Key; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.util.Map; 9 | 10 | import static org.junit.jupiter.api.Assertions.*; 11 | import static org.mockito.ArgumentMatchers.any; 12 | import static org.mockito.ArgumentMatchers.eq; 13 | import static org.mockito.Mockito.*; 14 | 15 | class HissHasherTest { 16 | 17 | final String defaultAlgorithm = "default-alg"; 18 | final String defaultKeyId = "default-key"; 19 | final Key key = Key.builder().id(defaultKeyId).build(); 20 | Hasher hasher; 21 | HissHasher hissHasher; 22 | 23 | @BeforeEach 24 | void setUpHissHasher() { 25 | hasher = spy(new Hasher() { 26 | @Override 27 | public byte[] hash(byte[] key, byte[] content) { 28 | return content; 29 | } 30 | 31 | @Override 32 | public String getName() { 33 | return defaultAlgorithm; 34 | } 35 | }); 36 | 37 | hissHasher = new HissHasher( 38 | Map.of(defaultAlgorithm, hasher), 39 | Map.of(defaultKeyId, key), 40 | defaultAlgorithm, 41 | defaultKeyId 42 | ); 43 | } 44 | 45 | @Test 46 | void hash() throws Exception { 47 | // Given 48 | var text = "plain text"; 49 | 50 | // When 51 | var hashedText = hissHasher.hash(text, null); 52 | 53 | // Then 54 | assertEquals("#$$#{default-alg:default-key}{cGxhaW4gdGV4dA==}#$$#", hashedText); 55 | verify(hasher).hash(eq(key.getKey()), any()); 56 | } 57 | 58 | @Test 59 | void hash_whenAlreadyHashed() throws Exception { 60 | // Given 61 | final var hashedText = "#$$#{default-alg:default-key}{cGxhaW4gdGV4dA==}#$$#"; 62 | 63 | // When 64 | var hashedAgainText = hissHasher.hash(hashedText, null); 65 | 66 | // Then 67 | assertEquals(hashedText, hashedAgainText); 68 | verify(hasher, never()).hash(any(), any()); 69 | } 70 | 71 | @Test 72 | void hash_whenContentIsNull() throws Exception { 73 | assertNull(hissHasher.hash(null, null)); 74 | } 75 | 76 | @Test 77 | void hash_withPattern() throws Exception { 78 | // Given 79 | var text = "your secure code is 1234567890; keep it safe."; 80 | 81 | // When 82 | var hashedText = hissHasher.hash(text, "\\d+"); 83 | 84 | // Then 85 | assertEquals("your secure code is #$$#{default-alg:default-key}{MTIzNDU2Nzg5MA==}#$$#; keep it safe.", 86 | hashedText); 87 | } 88 | 89 | 90 | @Test 91 | void isHashed() throws Exception { 92 | // Given 93 | var text = "plain text"; 94 | var encryptedText = hissHasher.hash(text, null); 95 | 96 | // When & Then 97 | assertFalse(hissHasher.isHashed(text)); 98 | assertTrue(hissHasher.isHashed(encryptedText)); 99 | } 100 | 101 | @Test 102 | void isHashed_whenHavingPattern() throws Exception { 103 | // Given 104 | var text = "your secure code is 1234567890; keep it safe."; 105 | var encryptedText = hissHasher.hash(text, "\\d+"); 106 | 107 | // When & Then 108 | assertFalse(hissHasher.isHashed(text)); 109 | assertTrue(hissHasher.isHashed(encryptedText)); 110 | } 111 | 112 | } -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/properties/HissProperties.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.properties; 2 | 3 | import io.github.tap30.hiss.key.Key; 4 | import io.github.tap30.hiss.utils.StringUtils; 5 | import lombok.AccessLevel; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Builder; 8 | import lombok.Value; 9 | 10 | import java.util.Map; 11 | import java.util.Set; 12 | import java.util.function.Function; 13 | import java.util.stream.Collectors; 14 | 15 | /** 16 | * Properties by which an Hiss instance can be created. 17 | */ 18 | @Builder 19 | @Value 20 | @AllArgsConstructor(access = AccessLevel.PACKAGE) 21 | public class HissProperties { 22 | 23 | /** 24 | * Pairs of key ID (name) to key. 25 | */ 26 | Map keys; 27 | /** 28 | * The key ID of the key by which encryption will be done. It must exist in `keys` map. 29 | */ 30 | String defaultEncryptionKeyId; 31 | /** 32 | * The algorithm name by which encryption will be done. 33 | * It must exist among default or custom encryption algorithms. 34 | */ 35 | String defaultEncryptionAlgorithm; 36 | /** 37 | * The key ID of the key by which hashing will be done. It must exist in `keys` map. 38 | */ 39 | String defaultHashingKeyId; 40 | /** 41 | * The algorithm name by which hashing will be done. 42 | * It must exist among default or custom hashing algorithms. 43 | */ 44 | String defaultHashingAlgorithm; 45 | /** 46 | * Whether to generate keys' hashes on Hiss instantiation. 47 | */ 48 | boolean keyHashGenerationEnabled; 49 | 50 | /** 51 | * See {@link HissPropertiesFromEnvProvider}. 52 | */ 53 | public static HissProperties fromEnv() { 54 | return withProvider(new HissPropertiesFromEnvProvider()); 55 | } 56 | 57 | public static HissProperties withProvider(HissPropertiesProvider provider) { 58 | return builder() 59 | .keys(provider.getKeys()) 60 | .defaultEncryptionKeyId(provider.getDefaultEncryptionKeyId()) 61 | .defaultEncryptionAlgorithm(provider.getDefaultEncryptionAlgorithm()) 62 | .defaultHashingKeyId(provider.getDefaultHashingKeyId()) 63 | .defaultHashingAlgorithm(provider.getDefaultHashingAlgorithm()) 64 | .keyHashGenerationEnabled(provider.isKeyHashGenerationEnabled()) 65 | .build(); 66 | } 67 | 68 | public static class HissPropertiesBuilder { 69 | public HissPropertiesBuilder keys(Set keys) { 70 | this.keys = keys.stream() 71 | .collect(Collectors.toMap(k -> StringUtils.toLowerCase(k.getId()), Function.identity())); 72 | return this; 73 | } 74 | 75 | public HissPropertiesBuilder defaultEncryptionKeyId(String defaultEncryptionKeyId) { 76 | this.defaultEncryptionKeyId = StringUtils.toLowerCase(defaultEncryptionKeyId); 77 | return this; 78 | } 79 | 80 | public HissPropertiesBuilder defaultEncryptionAlgorithm(String defaultEncryptionAlgorithm) { 81 | this.defaultEncryptionAlgorithm = StringUtils.toLowerCase(defaultEncryptionAlgorithm); 82 | return this; 83 | } 84 | 85 | public HissPropertiesBuilder defaultHashingKeyId(String defaultHashingKeyId) { 86 | this.defaultHashingKeyId = StringUtils.toLowerCase(defaultHashingKeyId); 87 | return this; 88 | } 89 | 90 | public HissPropertiesBuilder defaultHashingAlgorithm(String defaultHashingAlgorithm) { 91 | this.defaultHashingAlgorithm = StringUtils.toLowerCase(defaultHashingAlgorithm); 92 | return this; 93 | } 94 | } 95 | 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hiss/EncryptedInsideAnnotationOnSimpleObjectTest.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.junit.jupiter.api.Assertions.*; 8 | 9 | public class EncryptedInsideAnnotationOnSimpleObjectTest extends BaseHissTest { 10 | 11 | @Test 12 | void encryptAndDecryptDomainObject() { 13 | // Given 14 | var latlng = new LatLng(); 15 | latlng.setLat("31.3"); 16 | latlng.setLng("82.5"); 17 | latlng.setUpdatedAt("Yesterday"); 18 | 19 | var address = new Address(); 20 | address.setCity("tehran"); 21 | address.setStreet("enghelab"); 22 | address.setPlate("70"); 23 | address.setUnit("3"); 24 | address.setLatLng(latlng); 25 | 26 | var user = new User(); 27 | user.setId("5"); 28 | user.setPhoneNumber("123456789"); 29 | user.setAddress(address); 30 | 31 | 32 | // When 33 | this.hiss.encryptObject(user); 34 | 35 | // Then 36 | assertEquals("5", user.getId()); 37 | assertNotEquals("123456789", user.getPhoneNumber()); 38 | assertNotNull(user.getHashedPhoneNumber()); 39 | assertEquals("tehran", user.getAddress().getCity()); 40 | assertEquals("enghelab", user.getAddress().getStreet()); 41 | assertNotEquals("70", user.getAddress().getPlate()); 42 | assertNotNull(user.getAddress().getHashedPlate()); 43 | assertNotEquals("3", user.getAddress().getUnit()); 44 | assertNotNull(user.getAddress().getHashedUnit()); 45 | assertNotEquals("31.3", user.getAddress().getLatLng().getLat()); 46 | assertNotEquals("82.5", user.getAddress().getLatLng().getLng()); 47 | assertEquals("Yesterday", user.getAddress().getLatLng().getUpdatedAt()); 48 | 49 | // When 50 | this.hiss.decryptObject(user); 51 | 52 | // Then 53 | assertEquals("5", user.getId()); 54 | assertEquals("123456789", user.getPhoneNumber()); 55 | assertNotNull(user.getHashedPhoneNumber()); 56 | assertEquals("tehran", user.getAddress().getCity()); 57 | assertEquals("enghelab", user.getAddress().getStreet()); 58 | assertEquals("70", user.getAddress().getPlate()); 59 | assertNotNull(user.getAddress().getHashedPlate()); 60 | assertEquals("3", user.getAddress().getUnit()); 61 | assertNotNull(user.getAddress().getHashedUnit()); 62 | assertEquals("31.3", user.getAddress().getLatLng().getLat()); 63 | assertEquals("82.5", user.getAddress().getLatLng().getLng()); 64 | assertEquals("Yesterday", user.getAddress().getLatLng().getUpdatedAt()); 65 | } 66 | 67 | @Getter 68 | @Setter 69 | public static class User { 70 | private String id; 71 | @Encrypted 72 | private String phoneNumber; 73 | private String hashedPhoneNumber; 74 | @EncryptedInside 75 | private Address address; 76 | } 77 | 78 | @Getter 79 | @Setter 80 | public static class Address { 81 | private String city; 82 | private String street; 83 | @Encrypted 84 | private String plate; 85 | private String hashedPlate; 86 | @Encrypted 87 | private String unit; 88 | private String hashedUnit; 89 | @EncryptedInside 90 | private LatLng latLng; 91 | } 92 | 93 | @Getter 94 | @Setter 95 | public static class LatLng { 96 | @Encrypted(hashingEnabled = false) 97 | private String lat; 98 | @Encrypted(hashingEnabled = false) 99 | private String lng; 100 | private String updatedAt; 101 | } 102 | 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/properties/HissPropertiesValidator.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.properties; 2 | 3 | import io.github.tap30.hiss.encryptor.Encryptor; 4 | import io.github.tap30.hiss.hasher.Hasher; 5 | import io.github.tap30.hiss.key.KeyHashGenerator; 6 | import io.github.tap30.hiss.utils.StringUtils; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Map; 10 | import java.util.Objects; 11 | 12 | public class HissPropertiesValidator { 13 | 14 | private final KeyHashGenerator keyHashGenerator; 15 | private final Map encryptors; 16 | private final Map hashers; 17 | 18 | public HissPropertiesValidator(KeyHashGenerator keyHashGenerator, 19 | Map encryptors, 20 | Map hashers) { 21 | this.keyHashGenerator = Objects.requireNonNull(keyHashGenerator); 22 | this.encryptors = Objects.requireNonNull(encryptors); 23 | this.hashers = Objects.requireNonNull(hashers); 24 | } 25 | 26 | public void validate(HissProperties hissProperties) { 27 | var errors = new ArrayList(); 28 | validateKeys(hissProperties, errors); 29 | validateDefaultEncryptionKeyAndAlgorithm(hissProperties, errors); 30 | validateDefaultHashingKeyAndAlgorithm(hissProperties, errors); 31 | if (!errors.isEmpty()) { 32 | throw new IllegalArgumentException("Hiss properties are not valid: " + String.join("; ", errors)); 33 | } 34 | } 35 | 36 | private void validateKeys(HissProperties hissProperties, ArrayList errors) { 37 | if (hissProperties.getKeys() == null || hissProperties.getKeys().isEmpty()) { 38 | errors.add("Keys are empty"); 39 | } else { 40 | hissProperties.getKeys().forEach((k, v) -> { 41 | if (k == null || v == null || v.getKey() == null || v.getKey().length == 0) { 42 | errors.add("Key " + k + " is empty"); 43 | } 44 | }); 45 | var mismatches = keyHashGenerator.validateKeyHashes(hissProperties.getKeys().values()); 46 | if (!mismatches.isEmpty()) { 47 | errors.add("Key(s) " + mismatches + " did not match with their hashes"); 48 | } 49 | } 50 | } 51 | 52 | private void validateDefaultEncryptionKeyAndAlgorithm(HissProperties hissProperties, ArrayList errors) { 53 | if (!StringUtils.hasText(hissProperties.getDefaultEncryptionKeyId())) { 54 | errors.add("Default encryption key ID is not defined"); 55 | } 56 | if (hissProperties.getKeys() != null 57 | && !hissProperties.getKeys().containsKey(hissProperties.getDefaultEncryptionKeyId())) { 58 | errors.add("Default encryption key ID is not among provided keys: " + hissProperties.getKeys().keySet()); 59 | } 60 | if (!StringUtils.hasText(hissProperties.getDefaultEncryptionAlgorithm())) { 61 | errors.add("Default encryption algorithm is not defined"); 62 | } else if (!encryptors.containsKey(hissProperties.getDefaultEncryptionAlgorithm())) { 63 | errors.add("Encryption algorithm " + hissProperties.getDefaultEncryptionAlgorithm() + " is not supported"); 64 | } 65 | } 66 | 67 | private void validateDefaultHashingKeyAndAlgorithm(HissProperties hissProperties, ArrayList errors) { 68 | if (!StringUtils.hasText(hissProperties.getDefaultHashingKeyId())) { 69 | errors.add("Default hashing key ID is not defined"); 70 | } 71 | if (hissProperties.getKeys() != null 72 | && !hissProperties.getKeys().containsKey(hissProperties.getDefaultHashingKeyId())) { 73 | errors.add("Default hashing key ID is not among provided keys: " + hissProperties.getKeys().keySet()); 74 | } 75 | if (!StringUtils.hasText(hissProperties.getDefaultHashingAlgorithm())) { 76 | errors.add("Default hashing algorithm is not defined"); 77 | } else if (!hashers.containsKey(hissProperties.getDefaultHashingAlgorithm())) { 78 | errors.add("Hashing algorithm " + hissProperties.getDefaultHashingAlgorithm() + " is not supported"); 79 | } 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hiss/EncryptedInsideAnnotationOnListAndSetTest.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.util.List; 9 | import java.util.Set; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | import static org.junit.jupiter.api.Assertions.assertNotEquals; 13 | 14 | public class EncryptedInsideAnnotationOnListAndSetTest extends BaseHissTest { 15 | 16 | @Test 17 | void encryptAndDecryptDomainObject() { 18 | // Given 19 | 20 | var user = User.builder() 21 | .id("5") 22 | .phoneNumber("123456789") 23 | .addressList(List.of( 24 | Address.builder() 25 | .city("list city 1") 26 | .postalCode("LC1") 27 | .build(), 28 | Address.builder() 29 | .city("list city 2") 30 | .postalCode("LC2") 31 | .build() 32 | )) 33 | .addressSet(Set.of( 34 | Address.builder() 35 | .city("set city 1") 36 | .postalCode("SC1") 37 | .build(), 38 | Address.builder() 39 | .city("set city 2") 40 | .postalCode("SC2") 41 | .build() 42 | )) 43 | .build(); 44 | 45 | // When 46 | this.hiss.encryptObject(user); 47 | 48 | // Then 49 | { 50 | assertEquals("5", user.getId()); 51 | assertNotEquals("123456789", user.getPhoneNumber()); 52 | 53 | assertEquals(2, user.getAddressList().size()); 54 | var listAddress1 = user.getAddressList().get(0); 55 | assertEquals("list city 1", listAddress1.getCity()); 56 | assertNotEquals("LC1", listAddress1.getPostalCode()); 57 | var listAddress2 = user.getAddressList().get(1); 58 | assertEquals("list city 2", listAddress2.getCity()); 59 | assertNotEquals("LC2", listAddress2.getPostalCode()); 60 | 61 | assertEquals(2, user.getAddressSet().size()); 62 | var setAddress1 = user.getAddressSet().stream().filter(a -> a.getCity().equals("set city 1")).findFirst().get(); 63 | assertEquals("set city 1", setAddress1.getCity()); 64 | assertNotEquals("SC1", setAddress1.getPostalCode()); 65 | var setAddress2 = user.getAddressSet().stream().filter(a -> a.getCity().equals("set city 2")).findFirst().get(); 66 | assertEquals("set city 2", setAddress2.getCity()); 67 | assertNotEquals("SC2", setAddress2.getPostalCode()); 68 | } 69 | 70 | 71 | // When 72 | this.hiss.decryptObject(user); 73 | 74 | // Then 75 | { 76 | assertEquals("5", user.getId()); 77 | assertEquals("123456789", user.getPhoneNumber()); 78 | 79 | assertEquals(2, user.getAddressList().size()); 80 | var listAddress1 = user.getAddressList().get(0); 81 | assertEquals("list city 1", listAddress1.getCity()); 82 | assertEquals("LC1", listAddress1.getPostalCode()); 83 | var listAddress2 = user.getAddressList().get(1); 84 | assertEquals("list city 2", listAddress2.getCity()); 85 | assertEquals("LC2", listAddress2.getPostalCode()); 86 | 87 | assertEquals(2, user.getAddressSet().size()); 88 | var setAddress1 = user.getAddressSet().stream().filter(a -> a.getCity().equals("set city 1")).findFirst().get(); 89 | assertEquals("set city 1", setAddress1.getCity()); 90 | assertEquals("SC1", setAddress1.getPostalCode()); 91 | var setAddress2 = user.getAddressSet().stream().filter(a -> a.getCity().equals("set city 2")).findFirst().get(); 92 | assertEquals("set city 2", setAddress2.getCity()); 93 | assertEquals("SC2", setAddress2.getPostalCode()); 94 | } 95 | } 96 | 97 | @Getter 98 | @Setter 99 | @Builder 100 | public static class User { 101 | private String id; 102 | @Encrypted(hashingEnabled = false) 103 | private String phoneNumber; 104 | @EncryptedInside 105 | private List
addressList; 106 | @EncryptedInside 107 | private Set
addressSet; 108 | } 109 | 110 | @Getter 111 | @Setter 112 | @Builder 113 | public static class Address { 114 | private String city; 115 | @Encrypted(hashingEnabled = false) 116 | private String postalCode; 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/Hiss.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss; 2 | 3 | import org.intellij.lang.annotations.Language; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.Objects; 7 | 8 | public class Hiss { 9 | 10 | private final HissEncryptor hissEncryptor; 11 | private final HissHasher hissHasher; 12 | private final HissObjectEncryptor hissObjectEncryptor; 13 | 14 | Hiss(HissEncryptor hissEncryptor, 15 | HissHasher hissHasher, 16 | HissObjectEncryptor hissObjectEncryptor) { 17 | this.hissEncryptor = Objects.requireNonNull(hissEncryptor); 18 | this.hissHasher = Objects.requireNonNull(hissHasher); 19 | this.hissObjectEncryptor = Objects.requireNonNull(hissObjectEncryptor); 20 | } 21 | 22 | /** 23 | * Encrypts the provided content with default key and default algorithm. 24 | * 25 | * @param content the content to be encrypted. 26 | * @return encrypted content or null if the content is null. 27 | */ 28 | public String encrypt(@Nullable String content) { 29 | return encrypt(content, ""); 30 | } 31 | 32 | /** 33 | * Encrypts parts of the provided content which match with the provided pattern 34 | * with default key and default algorithm. 35 | * 36 | * @param content the content to be encrypted. 37 | * @param pattern the pattern in regex format; null or empty pattern means to match all. 38 | * @return encrypted content or null if the content is null. 39 | */ 40 | public String encrypt(@Nullable String content, @Language("regexp") @Nullable String pattern) { 41 | try { 42 | return hissEncryptor.encrypt(content, pattern); 43 | } catch (Exception e) { 44 | throw new RuntimeException(e); 45 | } 46 | } 47 | 48 | /** 49 | * Decrypts the provided content; if the content is encrypted partially (with pattern), 50 | * only the encrypted parts will be decrypted. 51 | *
Key ID and algorithm must be among loaded/supported keys and algorithms. 52 | * 53 | * @param content the content to be decrypted. 54 | * @return decrypted content or null if the content is null. 55 | * @throws IllegalArgumentException if key ID or algorithm is not loaded/supported. 56 | */ 57 | public String decrypt(@Nullable String content) { 58 | try { 59 | return hissEncryptor.decrypt(content); 60 | } catch (Exception e) { 61 | throw new RuntimeException(e); 62 | } 63 | } 64 | 65 | /** 66 | * Hashes the provided content with default key and default algorithm. 67 | * 68 | * @param content the content to be hashed. 69 | * @return hashed content or null if the content is null. 70 | */ 71 | public String hash(@Nullable String content) { 72 | return hash(content, ""); 73 | } 74 | 75 | /** 76 | * Hashes parts of the provided content which match with the provided pattern 77 | * with default key and default algorithm. 78 | * 79 | * @param content the content to be hashed. 80 | * @param pattern the pattern in regex format; null or empty pattern means to match all. 81 | * @return hashed content or null if the content is null. 82 | */ 83 | public String hash(@Nullable String content, @Language("regexp") @Nullable String pattern) { 84 | try { 85 | return hissHasher.hash(content, pattern); 86 | } catch (Exception e) { 87 | throw new RuntimeException(e); 88 | } 89 | } 90 | 91 | /** 92 | * Tells whether the provider content is encrypted. 93 | * 94 | * @param content the content. 95 | * @return true if the content or parts of it is encrypted. 96 | */ 97 | public boolean isEncrypted(@Nullable String content) { 98 | return hissEncryptor.isEncrypted(content); 99 | } 100 | 101 | /** 102 | * Tells whether the provider content is hashed. 103 | * 104 | * @param content the content. 105 | * @return true if the content or parts of it is hashed. 106 | */ 107 | public boolean isHashed(@Nullable String content) { 108 | return hissHasher.isHashed(content); 109 | } 110 | 111 | /** 112 | * Encrypts and hashes fields of object annotated with {@link Encrypted} and {@link EncryptedInside}. 113 | * 114 | * @param object the annotated object; no action is taken on null objects. 115 | */ 116 | public void encryptObject(@Nullable Object object) { 117 | hissObjectEncryptor.encryptObject(object); 118 | } 119 | 120 | /** 121 | * Decrypts fields of object annotated with {@link Encrypted} and {@link EncryptedInside}. 122 | * 123 | * @param object the annotated object; no action is taken on null objects. 124 | */ 125 | public void decryptObject(@Nullable Object object) { 126 | hissObjectEncryptor.decryptObject(object); 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hiss/EncryptedAnnotationTest.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.junit.jupiter.api.Assertions.*; 8 | 9 | public class EncryptedAnnotationTest extends BaseHissTest { 10 | 11 | @Test 12 | void encryptAndDecryptDomainObject() throws Exception { 13 | // Given 14 | var aClassWithEncryptedAnnotationObject = new AClassWithEncryptedAnnotation(); 15 | aClassWithEncryptedAnnotationObject.setNormalField("normal field"); 16 | aClassWithEncryptedAnnotationObject.setField("field value"); 17 | aClassWithEncryptedAnnotationObject.setHashedField("hashed field value"); 18 | aClassWithEncryptedAnnotationObject.setCustomField("custom field value"); 19 | aClassWithEncryptedAnnotationObject.setCustomHashedField("custom hashed field value"); 20 | aClassWithEncryptedAnnotationObject.setFieldWithoutHash("field without hash"); 21 | aClassWithEncryptedAnnotationObject.setFieldHavingPattern("Your code: 123456"); 22 | aClassWithEncryptedAnnotationObject.setHashedFieldHavingPattern("hash of Your code: 123456"); 23 | aClassWithEncryptedAnnotationObject.setParentField("parent field value"); 24 | aClassWithEncryptedAnnotationObject.setHashedParentField("hashed parent field value"); 25 | 26 | // When 27 | hiss.encryptObject(aClassWithEncryptedAnnotationObject); 28 | 29 | // Then 30 | assertEquals("normal field", aClassWithEncryptedAnnotationObject.getNormalField()); 31 | 32 | assertNotEquals("field value", aClassWithEncryptedAnnotationObject.getField()); 33 | assertNotEquals("hashed field value", aClassWithEncryptedAnnotationObject.getHashedField()); 34 | assertEquals(hiss.hash("field value", ""), aClassWithEncryptedAnnotationObject.getHashedField()); 35 | 36 | assertNotEquals("custom field value", aClassWithEncryptedAnnotationObject.getCustomField()); 37 | assertNotEquals("custom hashed field value", aClassWithEncryptedAnnotationObject.getCustomHashedField()); 38 | assertEquals(hiss.hash("custom field value", ""), aClassWithEncryptedAnnotationObject.getCustomHashedField()); 39 | 40 | assertNotEquals("field without hash", aClassWithEncryptedAnnotationObject.getFieldWithoutHash()); 41 | 42 | assertNotEquals("Your code: 123456", aClassWithEncryptedAnnotationObject.getFieldHavingPattern()); 43 | assertTrue(aClassWithEncryptedAnnotationObject.getFieldHavingPattern().startsWith("Your code: ")); 44 | assertNotEquals("hash of Your code: 123456", aClassWithEncryptedAnnotationObject.getHashedFieldHavingPattern()); 45 | assertTrue(aClassWithEncryptedAnnotationObject.getHashedFieldHavingPattern().startsWith("Your code: ")); 46 | assertTrue(aClassWithEncryptedAnnotationObject.getHashedFieldHavingPattern().contains(hiss.hash("123456", ""))); 47 | 48 | assertNotEquals("parent field value", aClassWithEncryptedAnnotationObject.getParentField()); 49 | assertNotEquals("hashed parent field value", aClassWithEncryptedAnnotationObject.getHashedParentField()); 50 | 51 | assertNull(aClassWithEncryptedAnnotationObject.getNullField()); 52 | assertNull(aClassWithEncryptedAnnotationObject.getHashedNullField()); 53 | 54 | // When 55 | hiss.decryptObject(aClassWithEncryptedAnnotationObject); 56 | 57 | // Then 58 | assertEquals("normal field", aClassWithEncryptedAnnotationObject.getNormalField()); 59 | assertEquals("field value", aClassWithEncryptedAnnotationObject.getField()); 60 | assertNotEquals("field value", aClassWithEncryptedAnnotationObject.getHashedField()); 61 | assertEquals("custom field value", aClassWithEncryptedAnnotationObject.getCustomField()); 62 | assertNotEquals("field value", aClassWithEncryptedAnnotationObject.getCustomHashedField()); 63 | assertEquals("field without hash", aClassWithEncryptedAnnotationObject.getFieldWithoutHash()); 64 | assertEquals("Your code: 123456", aClassWithEncryptedAnnotationObject.getFieldHavingPattern()); 65 | assertNotEquals("Your code: 123456", aClassWithEncryptedAnnotationObject.getHashedFieldHavingPattern()); 66 | assertEquals("parent field value", aClassWithEncryptedAnnotationObject.getParentField()); 67 | assertNull(aClassWithEncryptedAnnotationObject.getNullField()); 68 | assertNull(aClassWithEncryptedAnnotationObject.getHashedNullField()); 69 | } 70 | 71 | @Getter 72 | @Setter 73 | public static class ParentClassWithEncryptedAnnotation { 74 | @Encrypted 75 | private String parentField; 76 | private String hashedParentField; 77 | } 78 | 79 | @Getter 80 | @Setter 81 | public static class AClassWithEncryptedAnnotation extends ParentClassWithEncryptedAnnotation { 82 | private String normalField; 83 | @Encrypted 84 | private String field; 85 | private String hashedField; 86 | @Encrypted(hashFieldName = "customHashedField") 87 | private String customField; 88 | private String customHashedField; 89 | @Encrypted(hashingEnabled = false) 90 | private String fieldWithoutHash; 91 | @Encrypted(pattern = "\\d+") 92 | private String fieldHavingPattern; 93 | private String hashedFieldHavingPattern; 94 | @Encrypted 95 | private String nullField; 96 | private String hashedNullField; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/HissEncryptor.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss; 2 | 3 | import io.github.tap30.hiss.encryptor.Encryptor; 4 | import io.github.tap30.hiss.key.Key; 5 | import io.github.tap30.hiss.utils.StringUtils; 6 | import lombok.Value; 7 | 8 | import java.nio.charset.Charset; 9 | import java.nio.charset.StandardCharsets; 10 | import java.util.Base64; 11 | import java.util.Map; 12 | import java.util.Objects; 13 | import java.util.regex.Matcher; 14 | import java.util.regex.Pattern; 15 | 16 | class HissEncryptor { 17 | 18 | /** 19 | * Group 1 is algorithm name; group 2 is key ID; group 3 is base64 encoded encrypted content 20 | * Example: #$$#{aes-128-gcm:default_key}{P4KYuz1zmvJC+vDIR4ej9bKX+e2uAapg040b1cLYxtjBx9RShwUbRFpUcQ==}#$$# 21 | */ 22 | private static final Pattern ENCTYPTED_CONTENT_PATTERN = Pattern.compile("#\\$\\$#\\{(.*?):(.*?)}\\{(.+?)}#\\$\\$#"); 23 | private static final Charset CHARSET = StandardCharsets.UTF_8; 24 | 25 | private final Map encryptors; 26 | private final Map keys; 27 | private final String defaultEncryptionAlgorithm; 28 | private final String defaultEncryptionKeyId; 29 | 30 | public HissEncryptor(Map encryptors, 31 | Map keys, 32 | String defaultEncryptionAlgorithm, 33 | String defaultEncryptionKeyId) { 34 | this.encryptors = Objects.requireNonNull(encryptors); 35 | this.keys = Objects.requireNonNull(keys); 36 | this.defaultEncryptionAlgorithm = StringUtils.requireNonBlank(defaultEncryptionAlgorithm); 37 | this.defaultEncryptionKeyId = StringUtils.requireNonBlank(defaultEncryptionKeyId); 38 | } 39 | 40 | public String encrypt(String content, String pattern) throws Exception { 41 | if (!StringUtils.hasText(content) || isEncrypted(content)) { 42 | return content; 43 | } 44 | 45 | var encryptorAndKey = getEncryptorAndKey(defaultEncryptionAlgorithm, defaultEncryptionKeyId); 46 | 47 | if (StringUtils.hasText(pattern)) { 48 | StringBuilder result = new StringBuilder(); 49 | Matcher matcher = Pattern.compile(pattern).matcher(content); 50 | 51 | while (matcher.find()) { 52 | var partToBeEncrypted = matcher.group(); 53 | var encryptedContent = encrypt(encryptorAndKey, partToBeEncrypted); 54 | matcher.appendReplacement(result, Matcher.quoteReplacement(encryptedContent)); 55 | } 56 | matcher.appendTail(result); 57 | 58 | return result.toString(); 59 | } else { 60 | return encrypt(encryptorAndKey, content); 61 | } 62 | } 63 | 64 | public String decrypt(String content) throws Exception { 65 | if (!StringUtils.hasText(content) || !isEncrypted(content)) { 66 | return content; 67 | } 68 | 69 | var result = new StringBuilder(); 70 | 71 | var matcher = ENCTYPTED_CONTENT_PATTERN.matcher(content); 72 | while (matcher.find()) { 73 | var algorithm = matcher.group(1); 74 | var keyId = matcher.group(2); 75 | var encryptedContent = matcher.group(3); 76 | 77 | var decryptedContent = decrypt(getEncryptorAndKey(algorithm, keyId), encryptedContent); 78 | matcher.appendReplacement(result, Matcher.quoteReplacement(decryptedContent)); 79 | } 80 | matcher.appendTail(result); 81 | 82 | return result.toString(); 83 | } 84 | 85 | public boolean isEncrypted(String content) { 86 | return isHavingEncryptedContentPattern(content); 87 | } 88 | 89 | static String formatEncryptedBytes(String algorithmName, String keyId, byte[] bytes) { 90 | var base64Encoded = Base64.getEncoder().encodeToString(bytes); 91 | return "#$$#{" + algorithmName + ":" + keyId + "}{" + base64Encoded + "}#$$#"; 92 | } 93 | 94 | static boolean isHavingEncryptedContentPattern(String content) { 95 | return ENCTYPTED_CONTENT_PATTERN.matcher(content).find(); 96 | } 97 | 98 | private String encrypt(EncryptorAndKey encryptorAndKey, String content) throws Exception { 99 | var encryptor = encryptorAndKey.getEncryptor(); 100 | var key = encryptorAndKey.getKey(); 101 | 102 | var contentBytes = content.getBytes(CHARSET); 103 | var encryptedBytes = encryptor.encrypt(key.getKey(), contentBytes); 104 | return formatEncryptedBytes(encryptor.getName(), key.getId(), encryptedBytes); 105 | } 106 | 107 | private String decrypt(EncryptorAndKey encryptorAndKey, String content) throws Exception { 108 | var encryptor = encryptorAndKey.getEncryptor(); 109 | var key = encryptorAndKey.getKey(); 110 | 111 | var contentBytes = Base64.getDecoder().decode(content); 112 | var decryptedBytes = encryptor.decrypt(key.getKey(), contentBytes); 113 | return new String(decryptedBytes, CHARSET); 114 | } 115 | 116 | private EncryptorAndKey getEncryptorAndKey(String algorithmName, String keyId) { 117 | var encryptor = Objects.requireNonNull(encryptors.get(algorithmName), "Algorithm not supported: " + algorithmName); 118 | var key = Objects.requireNonNull(keys.get(keyId), "Key not found: " + keyId); 119 | return new EncryptorAndKey(encryptor, key); 120 | } 121 | 122 | @Value 123 | private static class EncryptorAndKey { 124 | Encryptor encryptor; 125 | Key key; 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | io.github.tap30 7 | hiss 8 | 0.12.0 9 | Hiss 10 | A simple field-level encryption library 11 | 12 | 13 | mkay1375 14 | Mostafa Kazemi 15 | mkay1375@gmail.com 16 | 17 | 18 | airavanimanesh 19 | Amirhossein Iravanimanesh 20 | airavanimanesh@gmail.com 21 | 22 | 23 | 24 | 25 | Apache-2.0 26 | https://www.apache.org/licenses/LICENSE-2.0.txt 27 | repo 28 | 29 | 30 | https://github.com/Tap30/hiss 31 | 32 | https://github.com/Tap30/hiss.git 33 | HEAD 34 | https://github.com/Tap30/hiss 35 | 36 | 37 | 11 38 | 11 39 | 11 40 | 5.11.0 41 | 5.10.2 42 | 24.1.0 43 | 0.10.2 44 | 1.18.30 45 | 46 | 47 | 48 | 49 | at.favre.lib 50 | bcrypt 51 | ${bcrypt.version} 52 | 53 | 54 | org.jetbrains 55 | annotations 56 | ${jetbrains-annotations.version} 57 | compile 58 | 59 | 60 | org.projectlombok 61 | lombok 62 | ${lombok.version} 63 | provided 64 | 65 | 66 | org.junit.jupiter 67 | junit-jupiter 68 | ${junit-jupiter.version} 69 | test 70 | 71 | 72 | uk.org.webcompere 73 | system-stubs-jupiter 74 | 2.1.6 75 | test 76 | 77 | 78 | org.mockito 79 | mockito-junit-jupiter 80 | ${mockito-junit-jupiter.version} 81 | test 82 | 83 | 84 | 85 | 86 | 87 | 88 | org.sonatype.central 89 | central-publishing-maven-plugin 90 | 0.4.0 91 | true 92 | 93 | central 94 | true 95 | 96 | 97 | 98 | org.apache.maven.plugins 99 | maven-surefire-plugin 100 | 3.2.5 101 | 102 | 103 | maven-source-plugin 104 | 3.2.1 105 | 106 | 107 | attach-sources 108 | 109 | jar-no-fork 110 | 111 | 112 | 113 | 114 | 115 | maven-javadoc-plugin 116 | 3.1.1 117 | 118 | 119 | attach-javadocs 120 | 121 | jar 122 | 123 | 124 | 125 | 126 | none 127 | 128 | 129 | 130 | org.apache.maven.plugins 131 | maven-gpg-plugin 132 | 3.2.4 133 | 134 | 135 | sign-artifacts 136 | verify 137 | 138 | sign 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hiss/HissEncryptorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss; 2 | 3 | import io.github.tap30.hiss.encryptor.Encryptor; 4 | import io.github.tap30.hiss.key.Key; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.util.Map; 9 | 10 | import static org.junit.jupiter.api.Assertions.*; 11 | import static org.mockito.ArgumentMatchers.any; 12 | import static org.mockito.ArgumentMatchers.eq; 13 | import static org.mockito.Mockito.*; 14 | 15 | class HissEncryptorTest { 16 | 17 | final String defaultAlgorithm = "default-alg"; 18 | final String defaultKeyId = "default-key"; 19 | final Key key = Key.builder().id(defaultKeyId).build(); 20 | Encryptor encryptor; 21 | HissEncryptor hissEncryptor; 22 | 23 | @BeforeEach 24 | void setUpHissEncryptor() { 25 | encryptor = spy(new Encryptor() { 26 | @Override 27 | public byte[] encrypt(byte[] key, byte[] content) { 28 | return content; 29 | } 30 | 31 | @Override 32 | public byte[] decrypt(byte[] key, byte[] content) { 33 | return content; 34 | } 35 | 36 | @Override 37 | public String getName() { 38 | return defaultAlgorithm; 39 | } 40 | }); 41 | 42 | hissEncryptor = new HissEncryptor( 43 | Map.of(defaultAlgorithm, encryptor), 44 | Map.of(defaultKeyId, key), 45 | defaultAlgorithm, 46 | defaultKeyId 47 | ); 48 | } 49 | 50 | @Test 51 | void encrypt() throws Exception { 52 | // Given 53 | var text = "plain text"; 54 | 55 | // When 56 | var encryptedText = hissEncryptor.encrypt(text, null); 57 | 58 | // Then 59 | assertEquals("#$$#{default-alg:default-key}{cGxhaW4gdGV4dA==}#$$#", encryptedText); 60 | verify(encryptor).encrypt(eq(key.getKey()), any()); 61 | } 62 | 63 | @Test 64 | void encrypt_whenAlreadyEncrypted() throws Exception { 65 | // Given 66 | final var encryptedText = "#$$#{default-alg:default-key}{cGxhaW4gdGV4dA==}#$$#"; 67 | 68 | // When 69 | var encryptedAgainText = hissEncryptor.encrypt(encryptedText, null); 70 | 71 | // Then 72 | assertEquals(encryptedText, encryptedAgainText); 73 | verify(encryptor, never()).encrypt(any(), any()); 74 | } 75 | 76 | @Test 77 | void encrypt_whenContentIsNull() throws Exception { 78 | assertNull(hissEncryptor.encrypt(null, null)); 79 | } 80 | 81 | @Test 82 | void encrypt_withPattern() throws Exception { 83 | // Given 84 | var text = "your secure code is 1234567890; keep it safe."; 85 | 86 | // When 87 | var encryptedText = hissEncryptor.encrypt(text, "\\d+"); 88 | 89 | // Then 90 | assertEquals("your secure code is #$$#{default-alg:default-key}{MTIzNDU2Nzg5MA==}#$$#; keep it safe.", 91 | encryptedText); 92 | } 93 | 94 | @Test 95 | void decrypt() throws Exception { 96 | // Given 97 | var text = "#$$#{default-alg:default-key}{cGxhaW4gdGV4dA==}#$$#"; 98 | 99 | // When 100 | var decryptedText = hissEncryptor.decrypt(text); 101 | 102 | // Then 103 | assertEquals("plain text", decryptedText); 104 | verify(encryptor).decrypt(eq(key.getKey()), any()); 105 | } 106 | 107 | @Test 108 | void decrypt_whenIsNotEncrypted() throws Exception { 109 | // Given 110 | final var text = "plain text"; 111 | 112 | // When 113 | var decryptedAgainText = hissEncryptor.decrypt(text); 114 | 115 | // Then 116 | assertEquals(text, decryptedAgainText); 117 | verify(encryptor, never()).decrypt(any(), any()); 118 | } 119 | 120 | @Test 121 | void decrypt_whenContentIsNull() throws Exception { 122 | assertNull(hissEncryptor.decrypt(null)); 123 | } 124 | 125 | @Test 126 | void decrypt_withPattern() throws Exception { 127 | // Given 128 | var text = "your secure code is #$$#{default-alg:default-key}{MTIzNDU2Nzg5MA==}#$$#; keep it safe."; 129 | 130 | // When 131 | var decryptedText = hissEncryptor.decrypt(text); 132 | 133 | // Then 134 | assertEquals("your secure code is 1234567890; keep it safe.", 135 | decryptedText); 136 | } 137 | 138 | @Test 139 | void encryptAndDecrypt() throws Exception { 140 | // Given 141 | final var text = "plain text"; 142 | 143 | // When 144 | var encryptedText = hissEncryptor.encrypt(text, null); 145 | var decryptedText = hissEncryptor.decrypt(encryptedText); 146 | 147 | // Then 148 | assertEquals(text, decryptedText); 149 | } 150 | 151 | @Test 152 | void encryptAndDecrypt_withPattern() throws Exception { 153 | // Given 154 | final var text = "your secure code is 1234567890; keep it safe."; 155 | 156 | // When 157 | var encryptedText = hissEncryptor.encrypt(text, "\\d+"); 158 | var decryptedText = hissEncryptor.decrypt(encryptedText); 159 | 160 | // Then 161 | assertEquals(text, decryptedText); 162 | } 163 | 164 | @Test 165 | void isEncrypted() throws Exception { 166 | // Given 167 | var text = "plain text"; 168 | var encryptedText = hissEncryptor.encrypt(text, null); 169 | 170 | // When & Then 171 | assertFalse(hissEncryptor.isEncrypted(text)); 172 | assertTrue(hissEncryptor.isEncrypted(encryptedText)); 173 | } 174 | 175 | @Test 176 | void isEncrypted_whenHavingPattern() throws Exception { 177 | // Given 178 | var text = "your secure code is 1234567890; keep it safe."; 179 | var encryptedText = hissEncryptor.encrypt(text, "\\d+"); 180 | 181 | // When & Then 182 | assertFalse(hissEncryptor.isEncrypted(text)); 183 | assertTrue(hissEncryptor.isEncrypted(encryptedText)); 184 | } 185 | 186 | } -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/HissFactory.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss; 2 | 3 | import at.favre.lib.crypto.bcrypt.BCrypt; 4 | import io.github.tap30.hiss.encryptor.Encryptor; 5 | import io.github.tap30.hiss.encryptor.impl.AesCbcPkcs5PaddingEncryptor; 6 | import io.github.tap30.hiss.encryptor.impl.AesGcmNoPaddingEncryptor; 7 | import io.github.tap30.hiss.encryptor.impl.TapsiAesCbcEncryptor; 8 | import io.github.tap30.hiss.encryptor.impl.TapsiAesGcmEncryptor; 9 | import io.github.tap30.hiss.hasher.Hasher; 10 | import io.github.tap30.hiss.hasher.impl.HmacSha256Hasher; 11 | import io.github.tap30.hiss.hasher.impl.TapsiHmacSha256Hasher; 12 | import io.github.tap30.hiss.key.KeyHashGenerator; 13 | import io.github.tap30.hiss.properties.HissProperties; 14 | import io.github.tap30.hiss.properties.HissPropertiesProvider; 15 | import io.github.tap30.hiss.properties.HissPropertiesValidator; 16 | import org.jetbrains.annotations.NotNull; 17 | 18 | import java.util.*; 19 | import java.util.function.Supplier; 20 | import java.util.logging.Level; 21 | import java.util.logging.Logger; 22 | import java.util.stream.Collectors; 23 | 24 | public class HissFactory { 25 | 26 | private static final Logger logger = Logger.getLogger(HissFactory.class.getName()); 27 | 28 | static Supplier keyHashGeneratorProvider = () -> new KeyHashGenerator(BCrypt.withDefaults(), BCrypt.verifyer()); 29 | 30 | /** 31 | * Creates a Hiss instance with provided HissProperties and default encryptors and hashers. 32 | * 33 | * @param hissProperties the properties by which hiss will be instantiated; 34 | * use {@link HissProperties#fromEnv()} 35 | * or {@link HissProperties#builder()} 36 | * or {@link HissProperties#withProvider(HissPropertiesProvider)} 37 | * @return {@link Hiss} instance. 38 | * @throws IllegalArgumentException if the properties are not valid. 39 | */ 40 | public static Hiss createHiss(@NotNull HissProperties hissProperties) { 41 | return createHiss(hissProperties, Set.of(), Set.of()); 42 | } 43 | 44 | /** 45 | * Creates a Hiss instance with provided HissProperties and encryptors and hashers. 46 | *
47 | * Provided encryptors and hashers will be added alongside default ones. 48 | * 49 | * @param hissProperties the properties by which hiss will be instantiated; 50 | * use {@link HissProperties#fromEnv()} 51 | * or {@link HissProperties#builder()} 52 | * or {@link HissProperties#withProvider(HissPropertiesProvider)} 53 | * @param encryptors custom {@link Encryptor} implementations. Can be empty but not null. 54 | * @param hashers custom {@link Hasher} implementations. Can be empty but not null. 55 | * @return {@link Hiss} instance. 56 | * @throws IllegalArgumentException if the properties are not valid. 57 | */ 58 | public static Hiss createHiss(@NotNull HissProperties hissProperties, 59 | @NotNull Set encryptors, 60 | @NotNull Set hashers) { 61 | Objects.requireNonNull(hissProperties); 62 | Objects.requireNonNull(encryptors); 63 | Objects.requireNonNull(hashers); 64 | 65 | var encryptorsMap = addDefaultEncryptors(encryptors) 66 | .stream().collect(Collectors.toMap(e -> e.getName().toLowerCase(), e -> e)); 67 | var hashersMap = addDefaultHashers(hashers) 68 | .stream().collect(Collectors.toMap(h -> h.getName().toLowerCase(), h -> h)); 69 | 70 | var keyHashGenerator = keyHashGeneratorProvider.get(); 71 | new HissPropertiesValidator(keyHashGenerator, encryptorsMap, hashersMap).validate(hissProperties); 72 | 73 | var hissEncryptor = new HissEncryptor( 74 | encryptorsMap, 75 | hissProperties.getKeys(), 76 | hissProperties.getDefaultEncryptionAlgorithm(), 77 | hissProperties.getDefaultEncryptionKeyId() 78 | ); 79 | var hissHasher = new HissHasher( 80 | hashersMap, 81 | hissProperties.getKeys(), 82 | hissProperties.getDefaultHashingAlgorithm(), 83 | hissProperties.getDefaultHashingKeyId() 84 | ); 85 | var hissObjectEncryptor = new HissObjectEncryptor(hissEncryptor, hissHasher); 86 | 87 | logInitializingHiss(hissProperties, encryptorsMap, hashersMap); 88 | if (hissProperties.isKeyHashGenerationEnabled()) { 89 | keyHashGenerator.generateAndLogHashes(hissProperties.getKeys().values()); 90 | } 91 | 92 | return new Hiss(hissEncryptor, hissHasher, hissObjectEncryptor); 93 | } 94 | 95 | private static @NotNull Set addDefaultEncryptors(Set encryptors) { 96 | encryptors = new HashSet<>(encryptors); 97 | encryptors.add(new AesCbcPkcs5PaddingEncryptor()); 98 | encryptors.add(new AesGcmNoPaddingEncryptor()); 99 | encryptors.add(new TapsiAesCbcEncryptor()); 100 | encryptors.add(new TapsiAesGcmEncryptor()); 101 | encryptors = Collections.unmodifiableSet(encryptors); 102 | return encryptors; 103 | } 104 | 105 | private static @NotNull Set addDefaultHashers(Set hashers) { 106 | hashers = new HashSet<>(hashers); 107 | hashers.add(new HmacSha256Hasher()); 108 | hashers.add(new TapsiHmacSha256Hasher()); 109 | hashers = Collections.unmodifiableSet(hashers); 110 | return hashers; 111 | } 112 | 113 | private static void logInitializingHiss(HissProperties hissProperties, 114 | Map encryptors, 115 | Map hashers) { 116 | logger.log(Level.INFO, "Hiss initialized:\n" + 117 | " Loaded Keys: {0}\n" + 118 | " Default Encryption Key ID: {1}\n" + 119 | " Default Encryption Algorithm: {2}\n" + 120 | " Default Hashing Key ID: {3}\n" + 121 | " Default Hashing Algorithm: {4}\n" + 122 | " Encryptors: {5}\n" + 123 | " Hashers: {6}\n", 124 | new Object[]{ 125 | hissProperties.getKeys().keySet(), 126 | hissProperties.getDefaultEncryptionKeyId(), 127 | hissProperties.getDefaultEncryptionAlgorithm(), 128 | hissProperties.getDefaultHashingKeyId(), 129 | hissProperties.getDefaultHashingAlgorithm(), 130 | encryptors.keySet(), 131 | hashers.keySet() 132 | }); 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hiss/HissTest.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.*; 6 | 7 | class HissTest extends BaseHissTest { 8 | 9 | 10 | @Test 11 | void encrypt() { 12 | // Given 13 | var text = "some text"; 14 | 15 | // When 16 | var encrypted = hiss.encrypt(text); 17 | 18 | // Then 19 | assertNotEquals("some text", encrypted); 20 | } 21 | 22 | @Test 23 | void encrypt_whenContentIsAlreadyEncrypted() { 24 | // Given 25 | var text = "some text"; 26 | 27 | // When 28 | var encrypted = hiss.encrypt(text); 29 | encrypted = hiss.encrypt(encrypted); 30 | encrypted = hiss.encrypt(encrypted); 31 | 32 | // Then 33 | assertEquals("some text", hiss.decrypt(encrypted)); 34 | } 35 | 36 | @Test 37 | void encrypt_whenValueIsNull() { 38 | assertNull(hiss.encrypt(null)); 39 | } 40 | 41 | @Test 42 | void encrypt_whenValueIsEmpty() { 43 | assertEquals("", hiss.encrypt("")); 44 | } 45 | 46 | @Test 47 | void decrypt() { 48 | // Given 49 | var encryptedText = "#$$#{aes-128-gcm:default_key}{5Ki0pm8DwBRQPLXtkbBwNqAceuzgLEkOiZv6ecVNyPaAjqmme6gmVKw=}#$$#"; 50 | 51 | // When 52 | var text = hiss.decrypt(encryptedText); 53 | 54 | // Then 55 | assertEquals("some text", text); 56 | } 57 | 58 | @Test 59 | void decrypt_whenContentNotEncrypted() { 60 | // Given 61 | var text = "some text"; 62 | 63 | // When 64 | var decryptedText = hiss.decrypt(text); 65 | 66 | // Then 67 | assertEquals("some text", decryptedText); 68 | } 69 | 70 | @Test 71 | void decrypt_whenValueIsNull() { 72 | assertNull(hiss.decrypt(null)); 73 | } 74 | 75 | @Test 76 | void decrypt_whenValueIsEmpty() { 77 | assertEquals("", hiss.decrypt("")); 78 | } 79 | 80 | @Test 81 | void encryptAndDecrypt() { 82 | // Given 83 | final var content = "Hello; user with phone number +989123456789 and national code 1234567890 is verified."; 84 | 85 | // When 86 | var encryptedContent = hiss.encrypt(content); 87 | var decryptedContent = hiss.decrypt(encryptedContent); 88 | 89 | // Then 90 | assertFalse(hiss.isEncrypted(content)); 91 | assertTrue(hiss.isEncrypted(encryptedContent)); 92 | assertNotEquals(content, encryptedContent); 93 | assertFalse(encryptedContent.contains("+989123456789")); 94 | assertFalse(encryptedContent.contains("1234567890")); 95 | assertTrue(encryptedContent.matches("#\\$\\$#\\{aes-128-gcm:default_key}\\{.+?}#\\$\\$#")); 96 | assertEquals(content, decryptedContent); 97 | } 98 | 99 | @Test 100 | void encryptAndDecrypt_withWeiredValue01() { 101 | // Given 102 | var content = "$@$@N"; 103 | 104 | // When 105 | var decryptedContent = hiss.decrypt(hiss.encrypt(content)); 106 | 107 | // Then 108 | assertEquals(content, decryptedContent); 109 | } 110 | 111 | @Test 112 | void encryptAndDecrypt_withWeiredValue02() { 113 | // Given 114 | var content = "@MIRI"; 115 | 116 | // When 117 | var decryptedContent = hiss.decrypt(hiss.encrypt(content)); 118 | 119 | // Then 120 | assertEquals(content, decryptedContent); 121 | } 122 | 123 | @Test 124 | void encryptAndDecrypt_whenHavingPattern() { 125 | // Given 126 | final var content = "Hello; user with phone number +989123456789 and national code 1234567890 is verified."; 127 | 128 | // When 129 | var encryptedContent = hiss.encrypt(content, "\\+989\\d+|\\d{10}"); 130 | var decryptedContent = hiss.decrypt(encryptedContent); 131 | 132 | // Then 133 | assertFalse(hiss.isEncrypted(content)); 134 | assertTrue(hiss.isEncrypted(encryptedContent)); 135 | assertNotEquals(content, encryptedContent); 136 | assertFalse(encryptedContent.contains("+989123456789")); 137 | assertFalse(encryptedContent.contains("1234567890")); 138 | assertTrue(encryptedContent.matches("Hello; user with phone number " + 139 | "#\\$\\$#\\{aes-128-gcm:default_key}\\{.+?}#\\$\\$#" + 140 | " and national code " + 141 | "#\\$\\$#\\{aes-128-gcm:default_key}\\{.+?}#\\$\\$#" + 142 | " is verified.")); 143 | assertEquals(content, decryptedContent); 144 | } 145 | 146 | @Test 147 | void hash() { 148 | // Given 149 | final var content = "Hello; user with phone number +989123456789 and national code 1234567890 is verified."; 150 | 151 | // When 152 | var hashedContent = hiss.hash(content); 153 | 154 | // Then 155 | assertFalse(hiss.isHashed(content)); 156 | assertTrue(hiss.isHashed(hashedContent)); 157 | assertNotEquals(content, hashedContent); 158 | assertFalse(hashedContent.contains("+989123456789")); 159 | assertFalse(hashedContent.contains("1234567890")); 160 | assertTrue(hashedContent.matches("#\\$\\$#\\{hmac-sha256:default_key}\\{.+?}#\\$\\$#")); 161 | } 162 | 163 | @Test 164 | void hash_whenValueIsNull() { 165 | assertNull(hiss.hash(null)); 166 | } 167 | 168 | @Test 169 | void hash_whenValueIsEmpty() { 170 | assertEquals("", hiss.hash("")); 171 | } 172 | 173 | @Test 174 | void hash_producingSameValue() { 175 | // Given 176 | final var content = "Hello; user with phone number +989123456789 and national code 1234567890 is verified."; 177 | 178 | // When 179 | var hashedContent1 = hiss.hash(content, ""); 180 | var hashedContent2 = hiss.hash(content, ""); 181 | var hashedContent3 = hiss.hash(content, ""); 182 | 183 | // Then 184 | assertEquals(hashedContent1, hashedContent2); 185 | assertEquals(hashedContent1, hashedContent3); 186 | } 187 | 188 | @Test 189 | void hash_whenHavingPattern() { 190 | // Given 191 | final var content = "Hello; user with phone number +989123456789 and national code 1234567890 is verified."; 192 | 193 | // When 194 | var hashedContent = hiss.hash(content, "\\+989\\d+|\\d{10}"); 195 | 196 | // Then 197 | assertFalse(hiss.isHashed(content)); 198 | assertTrue(hiss.isHashed(hashedContent)); 199 | assertNotEquals(content, hashedContent); 200 | assertFalse(hashedContent.contains("+989123456789")); 201 | assertFalse(hashedContent.contains("1234567890")); 202 | assertTrue(hashedContent.matches("Hello; user with phone number " + 203 | "#\\$\\$#\\{hmac-sha256:default_key}\\{.+?}#\\$\\$#" + 204 | " and national code " + 205 | "#\\$\\$#\\{hmac-sha256:default_key}\\{.+?}#\\$\\$#" + 206 | " is verified.")); 207 | } 208 | 209 | } -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hiss/properties/HissPropertiesValidatorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss.properties; 2 | 3 | import io.github.tap30.hiss.encryptor.Encryptor; 4 | import io.github.tap30.hiss.hasher.Hasher; 5 | import io.github.tap30.hiss.key.Key; 6 | import io.github.tap30.hiss.key.KeyHashGenerator; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.util.Base64; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | import java.util.Set; 13 | 14 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 15 | import static org.junit.jupiter.api.Assertions.assertThrows; 16 | import static org.mockito.Mockito.*; 17 | 18 | class HissPropertiesValidatorTest { 19 | 20 | KeyHashGenerator keyHashGenerator = mock(KeyHashGenerator.class); 21 | HissPropertiesValidator hissPropertiesValidator = new HissPropertiesValidator( 22 | keyHashGenerator, 23 | Map.of("aes-128-gcm", mock(Encryptor.class)), 24 | Map.of("hmac-sha256", mock(Hasher.class)) 25 | ); 26 | 27 | @Test 28 | void constructor_whenConstructionArgumentsAreNull() { 29 | assertThrows(NullPointerException.class, 30 | () -> new HissPropertiesValidator(null, Map.of(), Map.of())); 31 | assertThrows(NullPointerException.class, 32 | () -> new HissPropertiesValidator(keyHashGenerator, null, Map.of())); 33 | assertThrows(NullPointerException.class, 34 | () -> new HissPropertiesValidator(keyHashGenerator, Map.of(), null)); 35 | } 36 | 37 | // Keys Validation 38 | 39 | @Test 40 | void validate_whenPropertiesAreValid() { 41 | // Given 42 | var properties = createValidProperties(); 43 | 44 | // When & Then 45 | assertDoesNotThrow(() -> hissPropertiesValidator.validate(properties)); 46 | } 47 | 48 | @Test 49 | void validate_whenKeysAreEmpty() { 50 | // Given 51 | var properties = spy(createValidProperties()); 52 | doReturn(Map.of()).when(properties).getKeys(); 53 | 54 | // When & Then 55 | assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); 56 | } 57 | 58 | @Test 59 | void validate_whenKeysHaveAKeyWithoutName() { 60 | // Given 61 | var properties = spy(createValidProperties()); 62 | var keys = new HashMap(); 63 | keys.put(null, Key.builder().build()); 64 | doReturn(keys).when(properties).getKeys(); 65 | 66 | // When & Then 67 | assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); 68 | } 69 | 70 | @Test 71 | void validate_whenKeyBytesAreEmpty() { 72 | // Given 73 | var properties = spy(createValidProperties()); 74 | doReturn(Map.of("default_key", Key.builder().build())).when(properties).getKeys(); 75 | 76 | // When & Then 77 | assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); 78 | } 79 | 80 | @Test 81 | void validate_whenKeyHashIsNotCorrect() { 82 | // Given 83 | var properties = createValidProperties(); 84 | doReturn(Set.of("default_key")).when(keyHashGenerator).validateKeyHashes(any()); 85 | 86 | // When & Then 87 | assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); 88 | } 89 | 90 | // Default Encryption Key and Algorithm Validation 91 | 92 | @Test 93 | void validate_whenDefaultEncryptionKeyIdIsMissing() { 94 | // Given 95 | var properties = spy(createValidProperties()); 96 | doReturn(null).when(properties).getDefaultEncryptionKeyId(); 97 | 98 | // When & Then 99 | assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); 100 | } 101 | 102 | @Test 103 | void validate_whenDefaultEncryptionKeyIdIsInvalid() { 104 | // Given 105 | var properties = spy(createValidProperties()); 106 | doReturn("some unknown key").when(properties).getDefaultEncryptionKeyId(); 107 | 108 | // When & Then 109 | assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); 110 | } 111 | 112 | @Test 113 | void validate_whenDefaultEncryptionAlgorithmIsMissing() { 114 | // Given 115 | var properties = spy(createValidProperties()); 116 | doReturn(null).when(properties).getDefaultEncryptionAlgorithm(); 117 | 118 | // When & Then 119 | assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); 120 | } 121 | 122 | @Test 123 | void validate_whenDefaultEncryptionAlgorithmIsInvalid() { 124 | // Given 125 | var properties = spy(createValidProperties()); 126 | doReturn("some unknown algorithm").when(properties).getDefaultEncryptionAlgorithm(); 127 | 128 | // When & Then 129 | assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); 130 | } 131 | 132 | // Default Hashing Key and Algorithm Validation 133 | 134 | @Test 135 | void validate_whenDefaultHashingKeyIdIsMissing() { 136 | // Given 137 | var properties = spy(createValidProperties()); 138 | doReturn(null).when(properties).getDefaultHashingKeyId(); 139 | 140 | // When & Then 141 | assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); 142 | } 143 | 144 | @Test 145 | void validate_whenDefaultHashingKeyIdIsInvalid() { 146 | // Given 147 | var properties = spy(createValidProperties()); 148 | doReturn("some unknown key").when(properties).getDefaultHashingKeyId(); 149 | 150 | // When & Then 151 | assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); 152 | } 153 | 154 | @Test 155 | void validate_whenDefaultHashingAlgorithmIsMissing() { 156 | // Given 157 | var properties = spy(createValidProperties()); 158 | doReturn(null).when(properties).getDefaultHashingAlgorithm(); 159 | 160 | // When & Then 161 | assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); 162 | } 163 | 164 | @Test 165 | void validate_whenDefaultHashingAlgorithmIsInvalid() { 166 | // Given 167 | var properties = spy(createValidProperties()); 168 | doReturn("some unknown algorithm").when(properties).getDefaultHashingAlgorithm(); 169 | 170 | // When & Then 171 | assertThrows(IllegalArgumentException.class, () -> hissPropertiesValidator.validate(properties)); 172 | } 173 | 174 | HissProperties createValidProperties() { 175 | return HissProperties.builder() 176 | .keys(Set.of(Key.builder() 177 | .id("default_key") 178 | .key(Base64.getDecoder().decode("AAAAAAAAAAAAAAAAAAAAAA==")) 179 | .keyHash("$2a$12$3T0VMnGMgvesehYomommnO02dbFOJuM/3elsmgmsB2/qlGSF3BIbe") 180 | .build())) 181 | .defaultEncryptionKeyId("default_key") 182 | .defaultEncryptionAlgorithm("aes-128-gcm") 183 | .defaultHashingKeyId("default_key") 184 | .defaultHashingAlgorithm("hmac-sha256") 185 | .keyHashGenerationEnabled(false) 186 | .build(); 187 | } 188 | 189 | } -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Apache Maven Wrapper startup batch script, version 3.2.0 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 28 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 29 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 30 | @REM e.g. to debug Maven itself, use 31 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 32 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 33 | @REM ---------------------------------------------------------------------------- 34 | 35 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 36 | @echo off 37 | @REM set title of command window 38 | title %0 39 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 40 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 41 | 42 | @REM set %HOME% to equivalent of $HOME 43 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 44 | 45 | @REM Execute a user defined script before this one 46 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 47 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 48 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 49 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 50 | :skipRcPre 51 | 52 | @setlocal 53 | 54 | set ERROR_CODE=0 55 | 56 | @REM To isolate internal variables from possible post scripts, we use another setlocal 57 | @setlocal 58 | 59 | @REM ==== START VALIDATION ==== 60 | if not "%JAVA_HOME%" == "" goto OkJHome 61 | 62 | echo. 63 | echo Error: JAVA_HOME not found in your environment. >&2 64 | echo Please set the JAVA_HOME variable in your environment to match the >&2 65 | echo location of your Java installation. >&2 66 | echo. 67 | goto error 68 | 69 | :OkJHome 70 | if exist "%JAVA_HOME%\bin\java.exe" goto init 71 | 72 | echo. 73 | echo Error: JAVA_HOME is set to an invalid directory. >&2 74 | echo JAVA_HOME = "%JAVA_HOME%" >&2 75 | echo Please set the JAVA_HOME variable in your environment to match the >&2 76 | echo location of your Java installation. >&2 77 | echo. 78 | goto error 79 | 80 | @REM ==== END VALIDATION ==== 81 | 82 | :init 83 | 84 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 85 | @REM Fallback to current working directory if not found. 86 | 87 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 88 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 89 | 90 | set EXEC_DIR=%CD% 91 | set WDIR=%EXEC_DIR% 92 | :findBaseDir 93 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 94 | cd .. 95 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 96 | set WDIR=%CD% 97 | goto findBaseDir 98 | 99 | :baseDirFound 100 | set MAVEN_PROJECTBASEDIR=%WDIR% 101 | cd "%EXEC_DIR%" 102 | goto endDetectBaseDir 103 | 104 | :baseDirNotFound 105 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 106 | cd "%EXEC_DIR%" 107 | 108 | :endDetectBaseDir 109 | 110 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 111 | 112 | @setlocal EnableExtensions EnableDelayedExpansion 113 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 114 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 115 | 116 | :endReadAdditionalConfig 117 | 118 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 123 | 124 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 125 | IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | if "%MVNW_VERBOSE%" == "true" ( 132 | echo Found %WRAPPER_JAR% 133 | ) 134 | ) else ( 135 | if not "%MVNW_REPOURL%" == "" ( 136 | SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 137 | ) 138 | if "%MVNW_VERBOSE%" == "true" ( 139 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 140 | echo Downloading from: %WRAPPER_URL% 141 | ) 142 | 143 | powershell -Command "&{"^ 144 | "$webclient = new-object System.Net.WebClient;"^ 145 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 146 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 147 | "}"^ 148 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ 149 | "}" 150 | if "%MVNW_VERBOSE%" == "true" ( 151 | echo Finished downloading %WRAPPER_JAR% 152 | ) 153 | ) 154 | @REM End of extension 155 | 156 | @REM If specified, validate the SHA-256 sum of the Maven wrapper jar file 157 | SET WRAPPER_SHA_256_SUM="" 158 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 159 | IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B 160 | ) 161 | IF NOT %WRAPPER_SHA_256_SUM%=="" ( 162 | powershell -Command "&{"^ 163 | "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ 164 | "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ 165 | " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ 166 | " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ 167 | " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ 168 | " exit 1;"^ 169 | "}"^ 170 | "}" 171 | if ERRORLEVEL 1 goto error 172 | ) 173 | 174 | @REM Provide a "standardized" way to retrieve the CLI args that will 175 | @REM work with both Windows and non-Windows executions. 176 | set MAVEN_CMD_LINE_ARGS=%* 177 | 178 | %MAVEN_JAVA_EXE% ^ 179 | %JVM_CONFIG_MAVEN_PROPS% ^ 180 | %MAVEN_OPTS% ^ 181 | %MAVEN_DEBUG_OPTS% ^ 182 | -classpath %WRAPPER_JAR% ^ 183 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 184 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 185 | if ERRORLEVEL 1 goto error 186 | goto end 187 | 188 | :error 189 | set ERROR_CODE=1 190 | 191 | :end 192 | @endlocal & set ERROR_CODE=%ERROR_CODE% 193 | 194 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 195 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 196 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 197 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 198 | :skipRcPost 199 | 200 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 201 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 202 | 203 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 204 | 205 | cmd /C exit /B %ERROR_CODE% 206 | -------------------------------------------------------------------------------- /src/main/java/io/github/tap30/hiss/HissObjectEncryptor.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hiss; 2 | 3 | import io.github.tap30.hiss.utils.ReflectionUtils; 4 | import io.github.tap30.hiss.utils.StringUtils; 5 | import lombok.Value; 6 | import org.intellij.lang.annotations.Language; 7 | 8 | import java.lang.reflect.Field; 9 | import java.lang.reflect.InvocationTargetException; 10 | import java.lang.reflect.Method; 11 | import java.util.*; 12 | import java.util.function.BiConsumer; 13 | import java.util.logging.Level; 14 | import java.util.logging.Logger; 15 | 16 | class HissObjectEncryptor { 17 | 18 | private static final Logger logger = Logger.getLogger(HissObjectEncryptor.class.getName()); 19 | private static final Map, ClassDescription> CLASSES_DESCRIPTION_CACHE = new HashMap<>(); 20 | 21 | private final HissEncryptor hissEncryptor; 22 | private final HissHasher hissHasher; 23 | 24 | public HissObjectEncryptor(HissEncryptor hissEncryptor, 25 | HissHasher hissHasher) { 26 | this.hissEncryptor = Objects.requireNonNull(hissEncryptor); 27 | this.hissHasher = Objects.requireNonNull(hissHasher); 28 | } 29 | 30 | public void encryptObject(Object domainObject) { 31 | this.encryptFields(domainObject); 32 | } 33 | 34 | public void decryptObject(Object domainObject) { 35 | this.decryptFields(domainObject); 36 | } 37 | 38 | private void encryptFields(Object object) { 39 | this.processFields(object, this::encryptField); 40 | } 41 | 42 | private void decryptFields(Object object) { 43 | this.processFields(object, this::decryptField); 44 | } 45 | 46 | private void processFields(Object object, 47 | BiConsumer processor) { 48 | if (object == null) return; 49 | 50 | var classDescription = getClassDescription(object.getClass()); 51 | for (var field : classDescription.getFieldsAnnotatedWithEncrypted()) { 52 | processor.accept(object, field); 53 | } 54 | for (var field : classDescription.getFieldsAnnotatedWithEncryptedInside()) { 55 | this.processFieldsAnnotatedWithEncryptedInside(object, field, processor); 56 | } 57 | } 58 | 59 | private void processFieldsAnnotatedWithEncryptedInside(Object object, 60 | FieldAnnotatedWithEncryptedInside fieldAnnotatedWithEncryptedInside, 61 | BiConsumer processor) { 62 | var fieldContent = fieldAnnotatedWithEncryptedInside.getField().getContent(object); 63 | if (fieldContent instanceof Iterable) { 64 | ((Iterable) fieldContent).forEach(item -> this.processFields(item, processor)); 65 | } else if (fieldContent instanceof Map) { 66 | ((Map) fieldContent).forEach((k, v) -> this.processFields(v, processor)); 67 | } else { 68 | this.processFields(fieldContent, processor); 69 | } 70 | } 71 | 72 | private void encryptField(Object object, FieldAnnotatedWithEncrypted fieldAnnotatedWithEncrypted) { 73 | try { 74 | var content = fieldAnnotatedWithEncrypted.getContentField().getContent(object); 75 | if (content == null) { 76 | return; 77 | } 78 | @Language("regexp") 79 | var pattern = fieldAnnotatedWithEncrypted.getEncryptedAnnotation().pattern(); 80 | var encryptedContent = this.hissEncryptor.encrypt(content, pattern); 81 | fieldAnnotatedWithEncrypted.getContentField().setContent(object, encryptedContent); 82 | if (fieldAnnotatedWithEncrypted.getEncryptedAnnotation().hashingEnabled()) { 83 | var hashedContent = this.hissHasher.hash(content, pattern); 84 | fieldAnnotatedWithEncrypted.getHashField().setContent(object, hashedContent); 85 | } 86 | } catch (Exception e) { 87 | throw new RuntimeException(e); 88 | } 89 | } 90 | 91 | private void decryptField(Object object, FieldAnnotatedWithEncrypted fieldAnnotatedWithEncrypted) { 92 | try { 93 | var content = fieldAnnotatedWithEncrypted.getContentField().getContent(object); 94 | var decryptedContent = this.hissEncryptor.decrypt(content); 95 | fieldAnnotatedWithEncrypted.getContentField().setContent(object, decryptedContent); 96 | } catch (Exception e) { 97 | throw new RuntimeException(e); 98 | } 99 | } 100 | 101 | private static ClassDescription getClassDescription(Class clazz) { 102 | if (CLASSES_DESCRIPTION_CACHE.containsKey(clazz)) { 103 | return CLASSES_DESCRIPTION_CACHE.get(clazz); 104 | } 105 | 106 | var fieldsAnnotatedWithEncrypted = new ArrayList(); 107 | var fieldsAnnotatedWithEncryptedInside = new ArrayList(); 108 | for (var field : ReflectionUtils.getAllFields(clazz)) { 109 | getFieldAnnotatedWithEncrypted(clazz, field).ifPresent(fieldsAnnotatedWithEncrypted::add); 110 | getFieldAnnotatedWithEncryptedInside(clazz, field).ifPresent(fieldsAnnotatedWithEncryptedInside::add); 111 | } 112 | 113 | var classDescription = new ClassDescription(fieldsAnnotatedWithEncrypted, fieldsAnnotatedWithEncryptedInside); 114 | CLASSES_DESCRIPTION_CACHE.put(clazz, classDescription); 115 | if (CLASSES_DESCRIPTION_CACHE.size() > 10000) { 116 | logger.log(Level.WARNING, "{0} classes are cached", CLASSES_DESCRIPTION_CACHE.size()); 117 | } 118 | return classDescription; 119 | } 120 | 121 | private static Optional 122 | getFieldAnnotatedWithEncrypted(Class clazz, Field field) { 123 | var encryptedAnnotation = field.getDeclaredAnnotation(Encrypted.class); 124 | if (encryptedAnnotation != null) { 125 | var contentField = new StringField(clazz, field.getName()); 126 | var hashField = getHashField(clazz, field, encryptedAnnotation); 127 | return Optional.of(new FieldAnnotatedWithEncrypted(encryptedAnnotation, contentField, hashField)); 128 | } else { 129 | return Optional.empty(); 130 | } 131 | } 132 | 133 | private static Optional 134 | getFieldAnnotatedWithEncryptedInside(Class clazz, Field field) { 135 | var encryptedInsideAnnotation = field.getDeclaredAnnotation(EncryptedInside.class); 136 | if (encryptedInsideAnnotation != null) { 137 | return Optional.of(new FieldAnnotatedWithEncryptedInside(new ReadOnlyObjectField(clazz, field.getName()))); 138 | } else { 139 | return Optional.empty(); 140 | } 141 | } 142 | 143 | private static StringField getHashField(Class clazz, Field field, Encrypted encryptedAnnotation) { 144 | if (encryptedAnnotation.hashingEnabled()) { 145 | if (StringUtils.hasText(encryptedAnnotation.hashFieldName())) { 146 | return new StringField(clazz, encryptedAnnotation.hashFieldName()); 147 | } else { 148 | return new StringField(clazz, "hashed" + StringUtils.capitalizeFirstLetter(field.getName())); 149 | } 150 | } 151 | return null; 152 | } 153 | 154 | @Value 155 | private static class ClassDescription { 156 | List fieldsAnnotatedWithEncrypted; 157 | List fieldsAnnotatedWithEncryptedInside; 158 | } 159 | 160 | @Value 161 | private static class FieldAnnotatedWithEncryptedInside { 162 | ReadOnlyObjectField field; 163 | } 164 | 165 | @Value 166 | private static class FieldAnnotatedWithEncrypted { 167 | Encrypted encryptedAnnotation; 168 | StringField contentField; 169 | StringField hashField; 170 | } 171 | 172 | private static class StringField { 173 | private final Method getter; 174 | private final Method setter; 175 | 176 | public StringField(Class clazz, String fieldName) { 177 | this.getter = ReflectionUtils.getMethod(clazz, 178 | "get" + StringUtils.capitalizeFirstLetter(fieldName)); 179 | this.setter = ReflectionUtils.getMethod(clazz, 180 | "set" + StringUtils.capitalizeFirstLetter(fieldName), String.class); 181 | } 182 | 183 | public String getContent(Object object) { 184 | return ReflectionUtils.invokeSupplierMethod(object, getter, String.class); 185 | } 186 | 187 | public void setContent(Object object, String content) { 188 | try { 189 | setter.invoke(object, content); 190 | } catch (IllegalAccessException | InvocationTargetException e) { 191 | throw new RuntimeException(e); 192 | } 193 | } 194 | } 195 | 196 | private static class ReadOnlyObjectField { 197 | private final Method getter; 198 | 199 | public ReadOnlyObjectField(Class clazz, String fieldName) { 200 | this.getter = ReflectionUtils.getMethod(clazz, 201 | "get" + StringUtils.capitalizeFirstLetter(fieldName)); 202 | } 203 | 204 | public Object getContent(Object object) { 205 | return ReflectionUtils.invokeSupplierMethod(object, getter, Object.class); 206 | } 207 | 208 | } 209 | 210 | } 211 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.2.0 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | # e.g. to debug Maven itself, use 32 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | # ---------------------------------------------------------------------------- 35 | 36 | if [ -z "$MAVEN_SKIP_RC" ] ; then 37 | 38 | if [ -f /usr/local/etc/mavenrc ] ; then 39 | . /usr/local/etc/mavenrc 40 | fi 41 | 42 | if [ -f /etc/mavenrc ] ; then 43 | . /etc/mavenrc 44 | fi 45 | 46 | if [ -f "$HOME/.mavenrc" ] ; then 47 | . "$HOME/.mavenrc" 48 | fi 49 | 50 | fi 51 | 52 | # OS specific support. $var _must_ be set to either true or false. 53 | cygwin=false; 54 | darwin=false; 55 | mingw=false 56 | case "$(uname)" in 57 | CYGWIN*) cygwin=true ;; 58 | MINGW*) mingw=true;; 59 | Darwin*) darwin=true 60 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 61 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 62 | if [ -z "$JAVA_HOME" ]; then 63 | if [ -x "/usr/libexec/java_home" ]; then 64 | JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME 65 | else 66 | JAVA_HOME="/Library/Java/Home"; export JAVA_HOME 67 | fi 68 | fi 69 | ;; 70 | esac 71 | 72 | if [ -z "$JAVA_HOME" ] ; then 73 | if [ -r /etc/gentoo-release ] ; then 74 | JAVA_HOME=$(java-config --jre-home) 75 | fi 76 | fi 77 | 78 | # For Cygwin, ensure paths are in UNIX format before anything is touched 79 | if $cygwin ; then 80 | [ -n "$JAVA_HOME" ] && 81 | JAVA_HOME=$(cygpath --unix "$JAVA_HOME") 82 | [ -n "$CLASSPATH" ] && 83 | CLASSPATH=$(cygpath --path --unix "$CLASSPATH") 84 | fi 85 | 86 | # For Mingw, ensure paths are in UNIX format before anything is touched 87 | if $mingw ; then 88 | [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && 89 | JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" 90 | fi 91 | 92 | if [ -z "$JAVA_HOME" ]; then 93 | javaExecutable="$(which javac)" 94 | if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then 95 | # readlink(1) is not available as standard on Solaris 10. 96 | readLink=$(which readlink) 97 | if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then 98 | if $darwin ; then 99 | javaHome="$(dirname "\"$javaExecutable\"")" 100 | javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" 101 | else 102 | javaExecutable="$(readlink -f "\"$javaExecutable\"")" 103 | fi 104 | javaHome="$(dirname "\"$javaExecutable\"")" 105 | javaHome=$(expr "$javaHome" : '\(.*\)/bin') 106 | JAVA_HOME="$javaHome" 107 | export JAVA_HOME 108 | fi 109 | fi 110 | fi 111 | 112 | if [ -z "$JAVACMD" ] ; then 113 | if [ -n "$JAVA_HOME" ] ; then 114 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 115 | # IBM's JDK on AIX uses strange locations for the executables 116 | JAVACMD="$JAVA_HOME/jre/sh/java" 117 | else 118 | JAVACMD="$JAVA_HOME/bin/java" 119 | fi 120 | else 121 | JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" 122 | fi 123 | fi 124 | 125 | if [ ! -x "$JAVACMD" ] ; then 126 | echo "Error: JAVA_HOME is not defined correctly." >&2 127 | echo " We cannot execute $JAVACMD" >&2 128 | exit 1 129 | fi 130 | 131 | if [ -z "$JAVA_HOME" ] ; then 132 | echo "Warning: JAVA_HOME environment variable is not set." 133 | fi 134 | 135 | # traverses directory structure from process work directory to filesystem root 136 | # first directory with .mvn subdirectory is considered project base directory 137 | find_maven_basedir() { 138 | if [ -z "$1" ] 139 | then 140 | echo "Path not specified to find_maven_basedir" 141 | return 1 142 | fi 143 | 144 | basedir="$1" 145 | wdir="$1" 146 | while [ "$wdir" != '/' ] ; do 147 | if [ -d "$wdir"/.mvn ] ; then 148 | basedir=$wdir 149 | break 150 | fi 151 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 152 | if [ -d "${wdir}" ]; then 153 | wdir=$(cd "$wdir/.." || exit 1; pwd) 154 | fi 155 | # end of workaround 156 | done 157 | printf '%s' "$(cd "$basedir" || exit 1; pwd)" 158 | } 159 | 160 | # concatenates all lines of a file 161 | concat_lines() { 162 | if [ -f "$1" ]; then 163 | # Remove \r in case we run on Windows within Git Bash 164 | # and check out the repository with auto CRLF management 165 | # enabled. Otherwise, we may read lines that are delimited with 166 | # \r\n and produce $'-Xarg\r' rather than -Xarg due to word 167 | # splitting rules. 168 | tr -s '\r\n' ' ' < "$1" 169 | fi 170 | } 171 | 172 | log() { 173 | if [ "$MVNW_VERBOSE" = true ]; then 174 | printf '%s\n' "$1" 175 | fi 176 | } 177 | 178 | BASE_DIR=$(find_maven_basedir "$(dirname "$0")") 179 | if [ -z "$BASE_DIR" ]; then 180 | exit 1; 181 | fi 182 | 183 | MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR 184 | log "$MAVEN_PROJECTBASEDIR" 185 | 186 | ########################################################################################## 187 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 188 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 189 | ########################################################################################## 190 | wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" 191 | if [ -r "$wrapperJarPath" ]; then 192 | log "Found $wrapperJarPath" 193 | else 194 | log "Couldn't find $wrapperJarPath, downloading it ..." 195 | 196 | if [ -n "$MVNW_REPOURL" ]; then 197 | wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 198 | else 199 | wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 200 | fi 201 | while IFS="=" read -r key value; do 202 | # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) 203 | safeValue=$(echo "$value" | tr -d '\r') 204 | case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; 205 | esac 206 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" 207 | log "Downloading from: $wrapperUrl" 208 | 209 | if $cygwin; then 210 | wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") 211 | fi 212 | 213 | if command -v wget > /dev/null; then 214 | log "Found wget ... using wget" 215 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" 216 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 217 | wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 218 | else 219 | wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 220 | fi 221 | elif command -v curl > /dev/null; then 222 | log "Found curl ... using curl" 223 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" 224 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 225 | curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" 226 | else 227 | curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" 228 | fi 229 | else 230 | log "Falling back to using Java to download" 231 | javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" 232 | javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" 233 | # For Cygwin, switch paths to Windows format before running javac 234 | if $cygwin; then 235 | javaSource=$(cygpath --path --windows "$javaSource") 236 | javaClass=$(cygpath --path --windows "$javaClass") 237 | fi 238 | if [ -e "$javaSource" ]; then 239 | if [ ! -e "$javaClass" ]; then 240 | log " - Compiling MavenWrapperDownloader.java ..." 241 | ("$JAVA_HOME/bin/javac" "$javaSource") 242 | fi 243 | if [ -e "$javaClass" ]; then 244 | log " - Running MavenWrapperDownloader.java ..." 245 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" 246 | fi 247 | fi 248 | fi 249 | fi 250 | ########################################################################################## 251 | # End of extension 252 | ########################################################################################## 253 | 254 | # If specified, validate the SHA-256 sum of the Maven wrapper jar file 255 | wrapperSha256Sum="" 256 | while IFS="=" read -r key value; do 257 | case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; 258 | esac 259 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" 260 | if [ -n "$wrapperSha256Sum" ]; then 261 | wrapperSha256Result=false 262 | if command -v sha256sum > /dev/null; then 263 | if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then 264 | wrapperSha256Result=true 265 | fi 266 | elif command -v shasum > /dev/null; then 267 | if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then 268 | wrapperSha256Result=true 269 | fi 270 | else 271 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." 272 | echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." 273 | exit 1 274 | fi 275 | if [ $wrapperSha256Result = false ]; then 276 | echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 277 | echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 278 | echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 279 | exit 1 280 | fi 281 | fi 282 | 283 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 284 | 285 | # For Cygwin, switch paths to Windows format before running java 286 | if $cygwin; then 287 | [ -n "$JAVA_HOME" ] && 288 | JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") 289 | [ -n "$CLASSPATH" ] && 290 | CLASSPATH=$(cygpath --path --windows "$CLASSPATH") 291 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 292 | MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") 293 | fi 294 | 295 | # Provide a "standardized" way to retrieve the CLI args that will 296 | # work with both Windows and non-Windows executions. 297 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" 298 | export MAVEN_CMD_LINE_ARGS 299 | 300 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 301 | 302 | # shellcheck disable=SC2086 # safe args 303 | exec "$JAVACMD" \ 304 | $MAVEN_OPTS \ 305 | $MAVEN_DEBUG_OPTS \ 306 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 307 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 308 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 309 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hiss 2 | 3 | Hiss is a Java/Kotlin field-level encryption and hashing library 4 | which lets you encrypt and calculate hash of only selected (annotated) fields of an object. 5 | 6 | It is most useful when you want to persist or send an object which has sensitive fields. 7 | 8 | The motivation behind this project was we wanted to 9 | encrypt [personally identifiable information (PII)](https://en.wikipedia.org/wiki/Personal_data) 10 | of our users prior to persisting them in the database that in case of 11 | a data breach or unauthorized access, user identities would be protected. 12 | 13 | ## Contents 14 | 15 | * [Quick Start](#quick-start) 16 | * [Adding Hiss Dependency](#adding-hiss-dependency) 17 | * [Creating Hiss Instance](#creating-hiss-instance) 18 | * [Annotating Fields](#annotating-fields) 19 | * [Encrypting Object](#encrypting-object) 20 | * [How does Hiss work?](#how-does-hiss-work) 21 | * [Overview](#overview) 22 | * [Use of Getters and Setters](#use-of-getters-and-setters) 23 | * [Nested Classes and Usage of `@EncryptedInside`](#nested-classes-and-usage-of-encryptedinside) 24 | * [Hash Calculation](#hash-calculation) 25 | * [Partial Encryption](#partial-encryption) 26 | * [Supported Algorithms](#supported-algorithms) 27 | * [Hiss Instantiation](#hiss-instantiation) 28 | * [Hiss Properties](#hiss-properties) 29 | * [Creating Properties From Environment Variables](#creating-properties-from-environment-variables) 30 | * [Key Integrity Validation](#key-integrity-validation) 31 | 32 | ## Quick Start 33 | 34 | Using Hiss is straight forward; by adding Hiss dependency and annotating your classes you're good to go. 35 | 36 | **Hiss is also integrated with Spring Data 37 | Mongo. [Check this out to find out more](https://github.com/Tap30/hiss-spring-boot-mongo-starter).** 38 | 39 | ### Adding Hiss Dependency 40 | 41 | Apache Maven: 42 | 43 | ```xml 44 | 45 | 46 | io.github.tap30 47 | hiss 48 | 0.12.0 49 | 50 | ``` 51 | 52 | Gradle (Groovy): 53 | 54 | ```groovy 55 | implementation 'io.github.tap30:hiss:0.12.0' 56 | ``` 57 | 58 | Gradle (Kotlin): 59 | 60 | ```kotlin 61 | implementation("io.github.tap30:hiss:0.12.0") 62 | ``` 63 | 64 | ### Creating Hiss Instance 65 | 66 | To create an Hiss instance, keys and default encryption and hashing algorithms must be configured; 67 | below is simple configuration by which an Hiss instance can be created. For more details [see here](#hiss-instantiation). 68 | 69 | ```java 70 | var properties = HissProperties.builder() 71 | .keys(Set.of(Key.builder() 72 | .id("default_key") 73 | .key(Base64.getDecoder().decode("AAAAAAAAAAAAAAAAAAAAAA==")) // 74 | .keyHash("$2a$12$3T0VMnGMgvesehYomommnO02dbFOJuM/3elsmgmsB2/qlGSF3BIbe") 75 | .build())) 76 | .defaultEncryptionKeyId("default_key") 77 | .defaultEncryptionAlgorithm("AES/GCM/NoPadding") 78 | .defaultHashingKeyId("default_key") 79 | .defaultHashingAlgorithm("HmacSHA256") 80 | .keyHashGenerationEnabled(false) 81 | .build(); 82 | 83 | var hiss = HissFactory.createHiss(properties); 84 | ``` 85 | 86 | ### Annotating Fields 87 | 88 | Assume we have a `User` class containing a phone number and a list of `Address`es; 89 | the `Address` class contains postal code alongside other fields. 90 | 91 | We want to make sure phone number is encrypted and its hash is calculated by which 92 | we can search for a user by his/her phone number. 93 | We also want to encrypt his/her postal code but the postal code is not searchable 94 | (at least in our imaginary app 😌). 95 | 96 | Here will be the code in Java: 97 | 98 | ```java 99 | import io.github.tap30.hiss.Encrypted; 100 | import io.github.tap30.hiss.EncryptedInside; 101 | 102 | public class User { 103 | private String name; 104 | @Encrypted 105 | private String phoneNumber; 106 | private String hashedPhoneNumber; // Hiss will automatically fill this field. 107 | @EncryptedInside 108 | private List
addresses; 109 | 110 | // Getters and setters; Hiss will use these! 111 | } 112 | 113 | public class Address { 114 | private String name; 115 | private String street; 116 | private String city; 117 | private String state; 118 | @Encrypted(hashingEnabled = false) 119 | private String postalCode; 120 | 121 | // Getters and setters; Hiss will use these! 122 | } 123 | ``` 124 | 125 | And in Kotlin (`@Encrypted` can be only applied on `var` fields): 126 | 127 | ```kotlin 128 | import io.github.tap30.hiss.Encrypted 129 | import io.github.tap30.hiss.EncryptedInside 130 | 131 | data class User( 132 | val name: String, 133 | @Encrypted 134 | var phoneNumber: String, 135 | var hashedPhoneNumber: String, 136 | @EncryptedInside 137 | val addresses: List
138 | ) 139 | 140 | data class Address( 141 | val name: String, 142 | val street: String, 143 | val city: String, 144 | val state: String, 145 | @Encrypted(hashingEnabled = false) 146 | val postalCode: String 147 | ) 148 | ``` 149 | 150 | ### Encrypting Object 151 | 152 | By simply calling `hiss.encryptObject(user)`, the annotated fields will be encrypted 153 | and their hash string (if enabled) will be calculated. 154 | 155 | Also, by calling `hiss.decryptObject(user)`, the annotated fields will be decrypted. 156 | 157 | All methods in Hiss class are idempotent; meaning calling `encryptObject` twice, 158 | won't result in encrypting fields twice and as for decryption, plain texts will be left untouched. 159 | 160 | There is a [sample application in tests](src/test/java/io/github/tap30/hissapp/Application.java) 161 | which demonstrates more use cases of Hiss. 162 | 163 | ## How does Hiss work? 164 | 165 | ### Overview 166 | 167 | When you call `encryptObject` of Hiss, it'll scan all fields of the object using Java reflection. 168 | String fields annotated with `@Encrypted`, will be read using getters of the fields, 169 | and their value will be encrypted and their hash will be calculated, and the encrypted content and hash value 170 | will be set using the fields setters. 171 | 172 | The story is same for `decryptObject` while it only decrypts those fields. 173 | 174 | The object field scanning will be done once per class and the scan result will be cached. 175 | 176 | ### Use of Getters and Setters 177 | 178 | As stated earlier, Hiss won't change value of fields directly. It relies on getters and setters 179 | of the fields. Thus, it is necessary to implement those in Java classes. 180 | 181 | #### Kotlin Data Classes 182 | 183 | For Kotlin data classes, no getters or setters are needed for `var` fields 184 | as they are automatically generated in their Java representation. 185 | 186 | As `val` fields are immutable, `@Encrypted` can't be used on them. 187 | 188 | ### Nested Classes and Usage of `@EncryptedInside` 189 | 190 | Recall the `addresses` field of the `User` class in [Quick Start: Annotating Fields](#annotating-fields), 191 | we've annotated the `addresses` field with `@EncryptedInside` to tell Hiss to scan fields inside them. 192 | 193 | `@EncryptedInside` can be used on non-primitive fields, 194 | subtypes of `Iterable` (e.g. `Set`, `List`, ...) and, subtypes of `Map`. 195 | 196 | Here is an example of valid `@EncryptedInside` usages: 197 | 198 | ```java 199 | public class ValidEncryptedInsideUsage { 200 | @EncryptedInside 201 | private Address address; 202 | 203 | @EncryptedInside 204 | private List
addressList; 205 | 206 | @EncryptedInside 207 | private Map addressMap; 208 | 209 | // getters ... 210 | } 211 | ``` 212 | 213 | ### Hash Calculation 214 | 215 | Hash values of fields are most useful when you want to search on these fields. 216 | 217 | Unless you disable hash calculation of a field (i.e `@Encrypted(hashingEnabled = false)`), 218 | during encryption, the hash value of the field will also be calculated. 219 | 220 | The hash value will be stored in a different field having the pattern `hashed`; 221 | for example the field for storing hash value of field `phoneNumber`, will be `hashedPhoneNumber`. 222 | You can provide your custom hash field name in `@Encrypted` annotation field `hashFieldName`. 223 | Here's an example in which we want to store hash value of `phoneNumber` in `searchablePhoneNumber`: 224 | 225 | ```java 226 | public class User { 227 | @Encrypted(hashFieldName="searchablePhoneNumber") 228 | private String phoneNumber; 229 | private String searchablePhoneNumber; // The hash value will be stored here. 230 | 231 | // getters and setters ... 232 | } 233 | ``` 234 | 235 | ### Partial Encryption 236 | 237 | By setting `pattern` in `@Encrypted`, only parts matched with the pattern will be encrypted and hashed. 238 | 239 | For example, having: 240 | 241 | ```java 242 | public class Message { 243 | 244 | @Encrypted(pattern = "\\d+") 245 | private String content; 246 | private String hashedContent; 247 | 248 | // getters and setters ... 249 | } 250 | 251 | var message = new Message(); 252 | message.setContent("User 123 called you."); 253 | 254 | hiss.encryptObject(message); 255 | 256 | System.out.println(message.getContent()); 257 | System.out.println(message.getHashedContent()); 258 | ``` 259 | 260 | will result in: 261 | 262 | ``` 263 | User #$$#{aes/gcm/nopadding:default_key}{anibgQ6BsnMbFz5+mtNENjE1ioAaOm5J7T4pyEIhEKTiqeY=}#$$# called you. 264 | User #$$#{hmacsha256:default_key}{wMwN/frvI3Dk1WcRF1/jSd727Uy6JdPHoB/G72VoIg0=}#$$# called you. 265 | ``` 266 | 267 | ### Supported Algorithms 268 | 269 | For encryption, these algorithms are supported: 270 | 271 | - [AES/CBC/PKCS5Padding](src/main/java/io/github/tap30/hiss/encryptor/impl/AesCbcPkcs5PaddingEncryptor.java) 272 | - [AES/GCM/NoPadding](src/main/java/io/github/tap30/hiss/encryptor/impl/AesGcmNoPaddingEncryptor.java) 273 | 274 | For hashing, only [HmacSHA256](src/main/java/io/github/tap30/hiss/hasher/impl/HmacSha256Hasher.java) is supported. 275 | 276 | By implementing [`Encryptor`](src/main/java/io/github/tap30/hiss/encryptor/Encryptor.java) 277 | and [`Hasher`](src/main/java/io/github/tap30/hiss/hasher/Hasher.java) interfaces, you can provide 278 | your own algorithms. We'll talk more about it in [Hiss Instantiation](#hiss-instantiation). 279 | 280 | ## Hiss Instantiation 281 | 282 | Hiss can be instantiated using [`HissFactory`](src/main/java/io/github/tap30/hiss/HissFactory.java)'s `createHiss` methods. 283 | 284 | `createHiss` method requires [`HissProperties`](src/main/java/io/github/tap30/hiss/properties/HissProperties.java) instances. 285 | In the overloaded method, it accepts sets of `Encryptor`s and `Hasher`s by which you can provide your own custom 286 | algorithm implementations. The default algorithms will be available. 287 | 288 | ### Hiss Properties 289 | 290 | [`HissProperties`](src/main/java/io/github/tap30/hiss/properties/HissProperties.java) 291 | can be created using its builder, using environment variables, or by implementing 292 | [`HissPropertiesProvider`](src/main/java/io/github/tap30/hiss/properties/HissPropertiesProvider.java) 293 | and passing it to `HissProperties.withProvider`. 294 | 295 | Here are the fields in `HissProperties`: 296 | 297 | ```java 298 | /** 299 | * Pairs of key ID (name) to key. 300 | */ 301 | Map keys; 302 | /** 303 | * The key ID of the key by which encryption will be done. It must exist in `keys` map. 304 | */ 305 | String defaultEncryptionKeyId; 306 | /** 307 | * The algorithm name by which encryption will be done. 308 | * It must exist among default or custom encryption algorithms. 309 | */ 310 | String defaultEncryptionAlgorithm; 311 | /** 312 | * The key ID of the key by which hashing will be done. It must exist in `keys` map. 313 | */ 314 | String defaultHashingKeyId; 315 | /** 316 | * The algorithm name by which hashing will be done. 317 | * It must exist among default or custom hashing algorithms. 318 | */ 319 | String defaultHashingAlgorithm; 320 | /** 321 | * Whether to generate keys' hashes on Hiss instantiation. 322 | */ 323 | boolean keyHashGenerationEnabled; 324 | ``` 325 | 326 | Creating `HissProperties` using its builder is straight-forward and explained in [Quick Start](#create-hiss-instance). 327 | In the following, we'll describe creating key from environment variables. 328 | 329 | #### Creating Properties From Environment Variables 330 | 331 | By calling `fromEnv` function of `HissProperties`, `HissProperties` will be created. 332 | 333 | Here are the mapping of the properties fields to environment variables: 334 | 335 | - `keys`: 336 | - `HISS_KEYS_{Key ID}`: the base64 encoded representation of the key. 337 | - `HISS_KEYS_{Key ID}__HASH`: the hash of the key. 338 | - `defaultEncryptionKeyId`: `HISS_DEFAULT_ENCRYPTION_KEY_ID` 339 | - `defaultEncryptionAlgorithm`: `HISS_DEFAULT_ENCRYPTION_ALGORITHM` 340 | - `defaultHashingKeyId`: `HISS_DEFAULT_HASHING_KEY_ID` 341 | - `defaultHashingAlgorithm`: `HISS_DEFAULT_HASHING_ALGORITHM` 342 | - `keyHashGenerationEnabled`: `HISS_KEY_HASH_GENERATION_ENABLED` 343 | 344 | Below is a full working set of envs having two keys IDed `default_key` and `old_key`: 345 | 346 | ```bash 347 | HISS_KEYS_DEFAULT_KEY='AAAAAAAAAAAAAAAAAAAAAA==' 348 | HISS_KEYS_DEFAULT_KEY___HASH='$2a$12$3T0VMnGMgvesehYomommnO02dbFOJuM/3elsmgmsB2/qlGSF3BIbe' 349 | 350 | HISS_KEYS_OLD_KEY='AQIDBAUGBwgJCgsMDQ4PEA==' 351 | HISS_KEYS_OLD_KEY___HASH='$2a$12$THkoYZHlqD/HvrSkKUDs9eyHwY7W2FmyJm6SMp4xeGfP2g7F6Ro/i' 352 | 353 | HISS_DEFAULT_ENCRYPTION_KEY_ID='default_key' 354 | HISS_DEFAULT_ENCRYPTION_ALGORITHM='aes-128-gcm' 355 | 356 | HISS_DEFAULT_HASHING_KEY_ID='default_key' 357 | HISS_DEFAULT_HASHING_ALGORITHM='hmac-sha256' 358 | 359 | HISS_KEY_HASH_GENERATION_ENABLED='true' 360 | ``` 361 | 362 | ### Key Integrity Validation 363 | 364 | Above we've seen "key hash". By setting `keyHashGenerationEnabled` to `true`, Hiss, upon instantiation, will 365 | print hashes of the keys on the console. 366 | 367 | Later by providing these hashes, Hiss will make sure integrity of keys will be left untouched; 368 | this should hopefully prevent accidental key change or manipulation 🤞. 369 | -------------------------------------------------------------------------------- /src/test/java/io/github/tap30/hissapp/Application.java: -------------------------------------------------------------------------------- 1 | package io.github.tap30.hissapp; 2 | 3 | import io.github.tap30.hiss.Encrypted; 4 | import io.github.tap30.hiss.Hiss; 5 | import io.github.tap30.hiss.HissFactory; 6 | import io.github.tap30.hiss.properties.HissProperties; 7 | import io.github.tap30.hissapp.model.Address; 8 | import io.github.tap30.hissapp.model.Admin; 9 | import io.github.tap30.hissapp.model.Secret; 10 | import io.github.tap30.hissapp.model.User; 11 | import lombok.Getter; 12 | import lombok.Setter; 13 | import org.junit.jupiter.api.BeforeAll; 14 | import org.junit.jupiter.api.Test; 15 | import org.junit.jupiter.api.extension.ExtendWith; 16 | import uk.org.webcompere.systemstubs.environment.EnvironmentVariables; 17 | import uk.org.webcompere.systemstubs.jupiter.SystemStub; 18 | import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; 19 | 20 | import java.util.List; 21 | import java.util.Map; 22 | import java.util.Set; 23 | 24 | import static org.junit.jupiter.api.Assertions.*; 25 | 26 | @ExtendWith(SystemStubsExtension.class) 27 | public class Application { 28 | 29 | @SystemStub 30 | static EnvironmentVariables environment = new EnvironmentVariables( 31 | "HISS_DEFAULT_ENCRYPTION_KEY_ID", "default_key", 32 | "HISS_DEFAULT_ENCRYPTION_ALGORITHM", "aes-128-gcm", 33 | "HISS_DEFAULT_HASHING_KEY_ID", "default_key", 34 | "HISS_DEFAULT_HASHING_ALGORITHM", "hmac-sha256", 35 | "HISS_KEYS_DEFAULT_KEY", "AAAAAAAAAAAAAAAAAAAAAA==", 36 | "HISS_KEYS_DEFAULT_KEY___HASH", "$2a$12$3T0VMnGMgvesehYomommnO02dbFOJuM/3elsmgmsB2/qlGSF3BIbe", 37 | "HISS_KEYS_OLD_KEY", "AQIDBAUGBwgJCgsMDQ4PEA==", 38 | "HISS_KEYS_OLD_KEY___HASH", "$2a$12$THkoYZHlqD/HvrSkKUDs9eyHwY7W2FmyJm6SMp4xeGfP2g7F6Ro/i", 39 | "HISS_KEY_HASH_GENERATION_ENABLED", "true" 40 | ); 41 | 42 | private static Hiss hiss; 43 | 44 | @BeforeAll 45 | static void setUpHiss() { 46 | hiss = HissFactory.createHiss(HissProperties.fromEnv()); 47 | } 48 | 49 | @Test 50 | void encryptUser() { 51 | // Given 52 | var user = User.builder() 53 | .id("user-01") 54 | .name("Mostafa") 55 | .phoneNumber("+989123456789") 56 | .addresses(List.of( 57 | Address.builder() 58 | .name("home") 59 | .city("Tehran") 60 | .street("Enghelab") 61 | .postalCode("1234567890") 62 | .build(), 63 | Address.builder() 64 | .name("work") 65 | .city("Mashhad") 66 | .street("Azadi") 67 | .postalCode("1234567891") 68 | .build() 69 | )) 70 | .build(); 71 | 72 | // When 73 | hiss.encryptObject(user); 74 | 75 | // Then 76 | assertEquals("user-01", user.getId()); 77 | assertEquals("Mostafa", user.getName()); 78 | assertTrue(user.getPhoneNumber().startsWith("+98912345")); 79 | assertFalse(user.getPhoneNumber().contains("6789")); 80 | assertEquals(2, user.getAddresses().size()); 81 | { 82 | var address = user.getAddresses().get(0); 83 | assertEquals("home", address.getName()); 84 | assertEquals("Tehran", address.getCity()); 85 | assertFalse(address.getStreet().contains("Enghelab")); 86 | assertFalse(address.getPostalCode().contains("1234567890")); 87 | } 88 | { 89 | var address = user.getAddresses().get(1); 90 | assertEquals("work", address.getName()); 91 | assertEquals("Mashhad", address.getCity()); 92 | assertFalse(address.getStreet().contains("Azadi")); 93 | assertFalse(address.getPostalCode().contains("1234567891")); 94 | } 95 | 96 | // When 97 | hiss.decryptObject(user); 98 | 99 | 100 | // Then 101 | assertEquals("user-01", user.getId()); 102 | assertEquals("Mostafa", user.getName()); 103 | assertEquals("+989123456789", user.getPhoneNumber()); 104 | assertEquals(2, user.getAddresses().size()); 105 | { 106 | var address = user.getAddresses().get(0); 107 | assertEquals("home", address.getName()); 108 | assertEquals("Tehran", address.getCity()); 109 | assertEquals("Enghelab", address.getStreet()); 110 | assertEquals("1234567890", address.getPostalCode()); 111 | } 112 | { 113 | var address = user.getAddresses().get(1); 114 | assertEquals("work", address.getName()); 115 | assertEquals("Mashhad", address.getCity()); 116 | assertEquals("Azadi", address.getStreet()); 117 | assertEquals("1234567891", address.getPostalCode()); 118 | } 119 | } 120 | 121 | @Test 122 | void rotateUserAlgorithmAndKeyId() { 123 | // Given 124 | var user = User.builder() 125 | .id("user-01") 126 | .name("Mostafa") 127 | .phoneNumber("+98912345#$$#{aes-128-cbc:old_key}{/UjgtjsLutCrcYTJ/APxVTu0CLjWlElScSgBw7IGcwY=}#$$#") 128 | .addresses(List.of( 129 | Address.builder() 130 | .name("home") 131 | .city("Tehran") 132 | .street("#$$#{aes-128-cbc:old_key}{qhcgSbNLHfWzJS1vIUC23xdZWDdc6/L8RSf9nArTGN8=}#$$#") 133 | .postalCode("#$$#{aes-128-cbc:old_key}{PjjcFEuslRRhP39s8oOYNGIxL8ta2748wpVu4SZkiBo=}#$$#") 134 | .build(), 135 | Address.builder() 136 | .name("work") 137 | .city("Mashhad") 138 | .street("#$$#{aes-128-cbc:old_key}{anBz1DfneYckkZ2/Esh9MybXh0xWJIT0SCEBI9RtWoc=}#$$#") 139 | .postalCode("#$$#{aes-128-cbc:old_key}{UtTWbxJTqRDcHRebijQpVROSN0NAgwR3qETjSSVKDyA=}#$$#") 140 | .build() 141 | )) 142 | .build(); 143 | 144 | // When 145 | hiss.decryptObject(user); 146 | 147 | 148 | // Then 149 | assertEquals("user-01", user.getId()); 150 | assertEquals("Mostafa", user.getName()); 151 | assertEquals("+989123456789", user.getPhoneNumber()); 152 | assertEquals(2, user.getAddresses().size()); 153 | { 154 | var address = user.getAddresses().get(0); 155 | assertEquals("home", address.getName()); 156 | assertEquals("Tehran", address.getCity()); 157 | assertEquals("Enghelab", address.getStreet()); 158 | assertEquals("1234567890", address.getPostalCode()); 159 | } 160 | { 161 | var address = user.getAddresses().get(1); 162 | assertEquals("work", address.getName()); 163 | assertEquals("Mashhad", address.getCity()); 164 | assertEquals("Azadi", address.getStreet()); 165 | assertEquals("1234567891", address.getPostalCode()); 166 | } 167 | } 168 | 169 | @Test 170 | void encryptAdmin() { 171 | // Given 172 | var admin = new Admin(); 173 | admin.setId("admin-01"); 174 | admin.setName("Mostafa"); 175 | admin.setPhoneNumber("+989123456789"); 176 | admin.setAddresses(List.of( 177 | Address.builder() 178 | .name("home") 179 | .city("Tehran") 180 | .street("Enghelab") 181 | .postalCode("1234567890") 182 | .build(), 183 | Address.builder() 184 | .name("work") 185 | .city("Mashhad") 186 | .street("Azadi") 187 | .postalCode("1234567891") 188 | .build() 189 | )); 190 | admin.setSecrets(Map.of( 191 | "secret-01", Secret.builder() 192 | .secret("secret number one") 193 | .applications(Set.of("app1", "app2")) 194 | .build(), 195 | "secret-02", Secret.builder() 196 | .secret("secret number two") 197 | .applications(Set.of("app3", "app4")) 198 | .build() 199 | )); 200 | 201 | // When 202 | hiss.encryptObject(admin); 203 | 204 | // Then 205 | assertEquals("admin-01", admin.getId()); 206 | assertEquals("Mostafa", admin.getName()); 207 | assertTrue(admin.getPhoneNumber().startsWith("+98912345")); 208 | assertFalse(admin.getPhoneNumber().contains("6789")); 209 | assertEquals(2, admin.getAddresses().size()); 210 | { 211 | var address = admin.getAddresses().get(0); 212 | assertEquals("home", address.getName()); 213 | assertEquals("Tehran", address.getCity()); 214 | assertFalse(address.getStreet().contains("Enghelab")); 215 | assertFalse(address.getPostalCode().contains("1234567890")); 216 | } 217 | { 218 | var address = admin.getAddresses().get(1); 219 | assertEquals("work", address.getName()); 220 | assertEquals("Mashhad", address.getCity()); 221 | assertFalse(address.getStreet().contains("Azadi")); 222 | assertFalse(address.getPostalCode().contains("1234567891")); 223 | } 224 | assertEquals(2, admin.getSecrets().size()); 225 | { 226 | var secret = admin.getSecrets().get("secret-01"); 227 | assertFalse(secret.getSecret().contains("secret number one")); 228 | assertEquals(Set.of("app1", "app2"), secret.getApplications()); 229 | } 230 | { 231 | var secret = admin.getSecrets().get("secret-02"); 232 | assertFalse(secret.getSecret().contains("secret number two")); 233 | assertEquals(Set.of("app3", "app4"), secret.getApplications()); 234 | } 235 | 236 | // When 237 | hiss.decryptObject(admin); 238 | 239 | 240 | // Then 241 | assertEquals("admin-01", admin.getId()); 242 | assertEquals("Mostafa", admin.getName()); 243 | assertEquals("+989123456789", admin.getPhoneNumber()); 244 | 245 | assertEquals(2, admin.getAddresses().size()); 246 | { 247 | var address = admin.getAddresses().get(0); 248 | assertEquals("home", address.getName()); 249 | assertEquals("Tehran", address.getCity()); 250 | assertEquals("Enghelab", address.getStreet()); 251 | assertEquals("1234567890", address.getPostalCode()); 252 | } 253 | { 254 | var address = admin.getAddresses().get(1); 255 | assertEquals("work", address.getName()); 256 | assertEquals("Mashhad", address.getCity()); 257 | assertEquals("Azadi", address.getStreet()); 258 | assertEquals("1234567891", address.getPostalCode()); 259 | } 260 | assertEquals(2, admin.getSecrets().size()); 261 | { 262 | var secret = admin.getSecrets().get("secret-01"); 263 | assertEquals("secret number one", secret.getSecret()); 264 | assertEquals(Set.of("app1", "app2"), secret.getApplications()); 265 | } 266 | { 267 | var secret = admin.getSecrets().get("secret-02"); 268 | assertEquals("secret number two", secret.getSecret()); 269 | assertEquals(Set.of("app3", "app4"), secret.getApplications()); 270 | } 271 | } 272 | 273 | @Test 274 | void encryptString() { 275 | // Given 276 | final var text = "plain text"; 277 | 278 | // When 279 | var encryptedText = hiss.encrypt(text); 280 | encryptedText = hiss.encrypt(encryptedText); 281 | encryptedText = hiss.encrypt(encryptedText); 282 | 283 | // Then 284 | assertFalse(encryptedText.contains(text)); 285 | 286 | // When 287 | var decryptedText = hiss.decrypt(encryptedText); 288 | decryptedText = hiss.decrypt(decryptedText); 289 | decryptedText = hiss.decrypt(decryptedText); 290 | 291 | // Then 292 | assertEquals(text, decryptedText); 293 | } 294 | 295 | @Test 296 | void encryptString_withPattern() { 297 | // Given 298 | final var text = "Your code is 12345. Keep it safe."; 299 | 300 | // When 301 | var encryptedText = hiss.encrypt(text, "\\d+"); 302 | encryptedText = hiss.encrypt(encryptedText, "\\d+"); 303 | encryptedText = hiss.encrypt(encryptedText, "\\d+"); 304 | 305 | // Then 306 | assertFalse(encryptedText.contains("12345")); 307 | assertTrue(encryptedText.startsWith("Your code is ")); 308 | assertTrue(encryptedText.endsWith(". Keep it safe.")); 309 | 310 | // When 311 | var decryptedText = hiss.decrypt(encryptedText); 312 | decryptedText = hiss.decrypt(decryptedText); 313 | decryptedText = hiss.decrypt(decryptedText); 314 | 315 | // Then 316 | assertEquals(text, decryptedText); 317 | } 318 | 319 | @Test 320 | void decryptString() { 321 | // Given 322 | var encryptedText = "#$$#{aes-128-gcm:default_key}{dZdE50gZRAtgzQ9ar2hemaWg0flEL9/SO8CoaZ+K12u6mDirOSaIeA==}#$$#"; 323 | 324 | // When 325 | var decryptedText = hiss.decrypt(encryptedText); 326 | 327 | // Then 328 | assertEquals("Enghelab", decryptedText); 329 | } 330 | 331 | @Test 332 | void decryptString_withPattern() { 333 | // Given 334 | var encryptedText = "+98912345#$$#{aes-128-gcm:default_key}{ha8e/UDZmsLuAqGW9zGo7eaq5e8cM79OO7Mp2ZUThcup2+8O}#$$#"; 335 | 336 | // When 337 | var decryptedText = hiss.decrypt(encryptedText); 338 | 339 | // Then 340 | assertEquals("+989123456789", decryptedText); 341 | } 342 | 343 | @Test 344 | void decryptString_withKeyIdAndAlgorithmDifferentFromDefaults() { 345 | // Given 346 | var encryptedText = "#$$#{aes-128-cbc:old_key}{UtTWbxJTqRDcHRebijQpVROSN0NAgwR3qETjSSVKDyA=}#$$#"; 347 | 348 | // When 349 | var decryptedText = hiss.decrypt(encryptedText); 350 | 351 | // Then 352 | assertEquals("1234567891", decryptedText); 353 | } 354 | 355 | @Test 356 | void hashString() { 357 | // Given 358 | final var text = "plain text"; 359 | 360 | // When 361 | var hashedText1 = hiss.hash(text); 362 | var hashedText2 = hiss.hash(hashedText1); 363 | var hashedText3 = hiss.hash(hashedText1); 364 | 365 | // Then 366 | assertEquals(hashedText1, hashedText2); 367 | assertEquals(hashedText2, hashedText3); 368 | assertFalse(hashedText1.contains(text)); 369 | } 370 | 371 | @Test 372 | void hashString_withPattern() { 373 | // Given 374 | final var text = "Your code is 12345. Keep it safe."; 375 | 376 | // When 377 | var hashedText1 = hiss.hash(text, "\\d+"); 378 | var hashedText2 = hiss.hash(hashedText1, "\\d+"); 379 | var hashedText3 = hiss.hash(hashedText2, "\\d+"); 380 | 381 | // Then 382 | assertEquals(hashedText1, hashedText2); 383 | assertEquals(hashedText2, hashedText3); 384 | assertFalse(hashedText1.contains("12345")); 385 | assertTrue(hashedText1.startsWith("Your code is ")); 386 | assertTrue(hashedText1.endsWith(". Keep it safe.")); 387 | } 388 | 389 | @Test 390 | void test() { 391 | var message = new Message(); 392 | message.setContent("User 123 called you."); 393 | 394 | hiss.encryptObject(message); 395 | 396 | System.out.println(message.getContent()); 397 | System.out.println(message.getHashedContent()); 398 | } 399 | 400 | @Getter 401 | @Setter 402 | public static class Message { 403 | 404 | @Encrypted(pattern = "\\d+") 405 | private String content; 406 | private String hashedContent; 407 | 408 | // getters and setters ... 409 | } 410 | 411 | } 412 | --------------------------------------------------------------------------------