Hiss is a Java/Kotlin field-level encryption and hashing library which lets you encrypt and calculate hash of only selected (annotated) fields of an object.
It is most useful when you want to persist or send an object which has sensitive fields.
The motivation behind this project was we wanted to encrypt personally identifiable information (PII) of our users prior to persisting them in the database that in case of a data breach or unauthorized access, user identities would be protected.
Using Hiss is straight forward; by adding Hiss dependency and annotating your classes you're good to go.
Hiss is also integrated with Spring Data Mongo. Check this out to find out more.
Apache Maven:
<dependency>
<groupId>io.github.tap30</groupId>
<artifactId>hiss</artifactId>
<version>0.12.0</version>
</dependency>
Gradle (Groovy):
implementation 'io.github.tap30:hiss:0.12.0'
Gradle (Kotlin):
implementation("io.github.tap30:hiss:0.12.0")
To create an Hiss instance, keys and default encryption and hashing algorithms must be configured; below is simple configuration by which an Hiss instance can be created. For more details see here.
var properties = HissProperties.builder()
.keys(Set.of(Key.builder()
.id("default_key")
.key(Base64.getDecoder().decode("AAAAAAAAAAAAAAAAAAAAAA==")) //
.keyHash("$2a$12$3T0VMnGMgvesehYomommnO02dbFOJuM/3elsmgmsB2/qlGSF3BIbe")
.build()))
.defaultEncryptionKeyId("default_key")
.defaultEncryptionAlgorithm("AES/GCM/NoPadding")
.defaultHashingKeyId("default_key")
.defaultHashingAlgorithm("HmacSHA256")
.keyHashGenerationEnabled(false)
.build();
var hiss = HissFactory.createHiss(properties);
Assume we have a User
class containing a phone number and a list of Address
es;
the Address
class contains postal code alongside other fields.
We want to make sure phone number is encrypted and its hash is calculated by which we can search for a user by his/her phone number. We also want to encrypt his/her postal code but the postal code is not searchable (at least in our imaginary app ๐).
Here will be the code in Java:
import io.github.tap30.hiss.Encrypted;
import io.github.tap30.hiss.EncryptedInside;
public class User {
private String name;
@Encrypted
private String phoneNumber;
private String hashedPhoneNumber; // Hiss will automatically fill this field.
@EncryptedInside
private List<Address> addresses;
// Getters and setters; Hiss will use these!
}
public class Address {
private String name;
private String street;
private String city;
private String state;
@Encrypted(hashingEnabled = false)
private String postalCode;
// Getters and setters; Hiss will use these!
}
And in Kotlin (@Encrypted
can be only applied on var
fields):
import io.github.tap30.hiss.Encrypted
import io.github.tap30.hiss.EncryptedInside
data class User(
val name: String,
@Encrypted
var phoneNumber: String,
var hashedPhoneNumber: String,
@EncryptedInside
val addresses: List<Address>
)
data class Address(
val name: String,
val street: String,
val city: String,
val state: String,
@Encrypted(hashingEnabled = false)
val postalCode: String
)
By simply calling hiss.encryptObject(user)
, the annotated fields will be encrypted
and their hash string (if enabled) will be calculated.
Also, by calling hiss.decryptObject(user)
, the annotated fields will be decrypted.
All methods in Hiss class are idempotent; meaning calling encryptObject
twice,
won't result in encrypting fields twice and as for decryption, plain texts will be left untouched.
There is a sample application in tests which demonstrates more use cases of Hiss.
When you call encryptObject
of Hiss, it'll scan all fields of the object using Java reflection.
String fields annotated with @Encrypted
, will be read using getters of the fields,
and their value will be encrypted and their hash will be calculated, and the encrypted content and hash value
will be set using the fields setters.
The story is same for decryptObject
while it only decrypts those fields.
The object field scanning will be done once per class and the scan result will be cached.
As stated earlier, Hiss won't change value of fields directly. It relies on getters and setters of the fields. Thus, it is necessary to implement those in Java classes.
For Kotlin data classes, no getters or setters are needed for var
fields
as they are automatically generated in their Java representation.
As val
fields are immutable, @Encrypted
can't be used on them.
Recall the addresses
field of the User
class in Quick Start: Annotating Fields,
we've annotated the addresses
field with @EncryptedInside
to tell Hiss to scan fields inside them.
@EncryptedInside
can be used on non-primitive fields,
subtypes of Iterable<T>
(e.g. Set<T>
, List<T>
, ...) and, subtypes of Map<?, T>
.
Here is an example of valid @EncryptedInside
usages:
public class ValidEncryptedInsideUsage {
@EncryptedInside
private Address address;
@EncryptedInside
private List<Address> addressList;
@EncryptedInside
private Map<String, Address> addressMap;
// getters ...
}
Hash values of fields are most useful when you want to search on these fields.
Unless you disable hash calculation of a field (i.e @Encrypted(hashingEnabled = false)
),
during encryption, the hash value of the field will also be calculated.
The hash value will be stored in a different field having the pattern hashed<FIELD NAME>
;
for example the field for storing hash value of field phoneNumber
, will be hashedPhoneNumber
.
You can provide your custom hash field name in @Encrypted
annotation field hashFieldName
.
Here's an example in which we want to store hash value of phoneNumber
in searchablePhoneNumber
:
public class User {
@Encrypted(hashFieldName="searchablePhoneNumber")
private String phoneNumber;
private String searchablePhoneNumber; // The hash value will be stored here.
// getters and setters ...
}
By setting pattern
in @Encrypted
, only parts matched with the pattern will be encrypted and hashed.
For example, having:
public class Message {
@Encrypted(pattern = "\\d+")
private String content;
private String hashedContent;
// getters and setters ...
}
var message = new Message();
message.setContent("User 123 called you.");
hiss.encryptObject(message);
System.out.println(message.getContent());
System.out.println(message.getHashedContent());
will result in:
User #$$#{aes/gcm/nopadding:default_key}{anibgQ6BsnMbFz5+mtNENjE1ioAaOm5J7T4pyEIhEKTiqeY=}#$$# called you.
User #$$#{hmacsha256:default_key}{wMwN/frvI3Dk1WcRF1/jSd727Uy6JdPHoB/G72VoIg0=}#$$# called you.
For encryption, these algorithms are supported:
For hashing, only HmacSHA256 is supported.
By implementing Encryptor
and Hasher
interfaces, you can provide
your own algorithms. We'll talk more about it in Hiss Instantiation.
Hiss can be instantiated using HissFactory
's createHiss
methods.
createHiss
method requires HissProperties
instances.
In the overloaded method, it accepts sets of Encryptor
s and Hasher
s by which you can provide your own custom
algorithm implementations. The default algorithms will be available.
HissProperties
can be created using its builder, using environment variables, or by implementing
HissPropertiesProvider
and passing it to HissProperties.withProvider
.
Here are the fields in HissProperties
:
/**
* Pairs of key ID (name) to key.
*/
Map<String, Key> keys;
/**
* The key ID of the key by which encryption will be done. It must exist in `keys` map.
*/
String defaultEncryptionKeyId;
/**
* The algorithm name by which encryption will be done.
* It must exist among default or custom encryption algorithms.
*/
String defaultEncryptionAlgorithm;
/**
* The key ID of the key by which hashing will be done. It must exist in `keys` map.
*/
String defaultHashingKeyId;
/**
* The algorithm name by which hashing will be done.
* It must exist among default or custom hashing algorithms.
*/
String defaultHashingAlgorithm;
/**
* Whether to generate keys' hashes on Hiss instantiation.
*/
boolean keyHashGenerationEnabled;
Creating HissProperties
using its builder is straight-forward and explained in Quick Start.
In the following, we'll describe creating key from environment variables.
By calling fromEnv
function of HissProperties
, HissProperties
will be created.
Here are the mapping of the properties fields to environment variables:
keys
:HISS_KEYS_{Key ID}
: the base64 encoded representation of the key.HISS_KEYS_{Key ID}__HASH
: the hash of the key.
defaultEncryptionKeyId
:HISS_DEFAULT_ENCRYPTION_KEY_ID
defaultEncryptionAlgorithm
:HISS_DEFAULT_ENCRYPTION_ALGORITHM
defaultHashingKeyId
:HISS_DEFAULT_HASHING_KEY_ID
defaultHashingAlgorithm
:HISS_DEFAULT_HASHING_ALGORITHM
keyHashGenerationEnabled
:HISS_KEY_HASH_GENERATION_ENABLED
Below is a full working set of envs having two keys IDed default_key
and old_key
:
HISS_KEYS_DEFAULT_KEY='AAAAAAAAAAAAAAAAAAAAAA=='
HISS_KEYS_DEFAULT_KEY___HASH='$2a$12$3T0VMnGMgvesehYomommnO02dbFOJuM/3elsmgmsB2/qlGSF3BIbe'
HISS_KEYS_OLD_KEY='AQIDBAUGBwgJCgsMDQ4PEA=='
HISS_KEYS_OLD_KEY___HASH='$2a$12$THkoYZHlqD/HvrSkKUDs9eyHwY7W2FmyJm6SMp4xeGfP2g7F6Ro/i'
HISS_DEFAULT_ENCRYPTION_KEY_ID='default_key'
HISS_DEFAULT_ENCRYPTION_ALGORITHM='aes-128-gcm'
HISS_DEFAULT_HASHING_KEY_ID='default_key'
HISS_DEFAULT_HASHING_ALGORITHM='hmac-sha256'
HISS_KEY_HASH_GENERATION_ENABLED='true'
Above we've seen "key hash". By setting keyHashGenerationEnabled
to true
, Hiss, upon instantiation, will
print hashes of the keys on the console.
Later by providing these hashes, Hiss will make sure integrity of keys will be left untouched; this should hopefully prevent accidental key change or manipulation ๐ค.