Data Seal is a JVM-based framework that allows for annotation based signing and encrypting of data structures fields intended for application level signing and encryption of data.
When data is secured by the application before being persisted, it ensures that only the intended business processes (e.g. the application) can read / modify the data, so that no administrator can modify data in a database without detection or read data if it is encrypted.
In particular the framework can be used for column level encryption and signing of data in SQL databases.
We recommend only using this framework is you have a (business/compliance) requirement to secure data "at rest" for instance data in a database, session data in memory, files on disk etc.
The framework provides 3 types of security mechanisms
- Signed fields, the data remains in plain text, but cannot be modified without detection
- Encrypted fields, the data is encrypted using AES-GCM, the data can optionally be gzipped
- Searchable fields, the data is encrypted using AES-CBC, which allows for matching the encrypted values if the exact plain text is know. This is useful for data that needs to be secured, but still searchable, however the level of security is reduced.
Both encrypted and searchable fields are also signed.
The following data types are supported for signing
- All primitives (boolean, byte, short, int, long, float, double, char) and their corresponding arrays
- All boxed primitives (Bolean, Byte, Short, Integer, Long, Float, Double, Character) and their corresponding arrays
- java.math.BigInteger
- java.math.BigDecimal
- java.lang.String, and arrays of java.lang.String
- java.util.UUID
- java.time.Instant
- All enums
The framework allows for extension of the supported types via custom plugins.
The following data types are supported for encryption and searchable encryption:
- java.lang.String
- byte[]
Below is an example of a simple bean that utilizes all 3 security mechanisms.
It implements the Sealable
interface which is required and provides the Sealable.seal()
which creates the HMAC signature and encrypts the specified fields and the corresponding Sealable.unseal()
method which validates the HMAC signature and decrypts the encrypted fields.
The @Metadata
data annotated field is required as this is where the bean field layout information, bean encryption key and signature is kept.
public class Bean implements Sealable {
@Signed
String signed;
@Searchable
String searchable;
@Encrypted
String encrypted;
@Metadata
String metadata;
}
To secure the bean simply do the following
Bean bean = new Bean();
bean.signed = "signed";
bean.searchable = "searchable";
bean.encrypted = "encrypted";
bean.seal();
The bean is now secured and modifications will be detected when .unseal() is called.
bean.unseal();
If any of the fields are modifed before unseal is called a MacMismatchException
will be thrown for instance,
Bean bean = new Bean();
bean.signed = "signed";
bean.searchable = "searchable";
bean.encrypted = "encrypted";
bean.seal();
// Modify data while object is sealed
bean.signed = "different";
// Will now throw MacMismatchException
bean.unseal();
To allow for search for matching values of fields protected with @Searchable
the search values can be generated by using the Seal.createSearchValues()
Bean bean = new Bean();
bean.signed = "signed";
bean.searchable = "searchable";
bean.encrypted = "encrypted";
bean.seal();
// Generates a list of search values to support rotation of encryption keys
List<String> searchValues = Seal.createSearchValues("searchable", bean.getClass(), "signed");
boolean match = false;
for (var searchValue : searchValues) {
if (searchValues.equals(bean.searchable)) {
match = true;
break;
}
}
Data Seal relies on 2 layers of encryption keys to secure the data that is sealed.
The KeyService
provides functionality for managing these keys, such as loading keys and generating new ones, and the KeyService
must be initialized before any invocations of Sealable.seal()
and Sealable.unseal()
In the example below the KeyService
is initialized with a SoftwareSecurityModule
, which loads its keys from the provided input stream - here loaded from the classpath - as well as a single key set also loaded from the classpath.
// Prepare the SecurityModule
var keyAlias = "example-key";
var jksInputStream = SealableTest.class.getClassLoader().getResourceAsStream("keystore.jks");
var softwareSecurityModule = new SoftwareSecurityModule("jks", jksInputStream, "secret".toCharArray(), keyAlias, "secret".toCharArray(), "RSA");
KeyService.INSTANCE.setSecurityModule(softwareSecurityModule)
KeyService.INSTANCE.setSecurityModuleKeyAlias(keyAlias);
// Load encrypted AES keys - will be decrypted by the security module
ObjectMapper mapper = new ObjectMapper();
var keySet = mapper.readValue(getClass().getClassLoader().getResourceAsStream("keyset-0.json"), EncryptedKeySet.class);
KeyService.INSTANCE.loadEncryptedKeySet(keySet);
KeyService.INSTANCE.setCurrentKeySetId(0);
The keyset being loaded looks like this
{
"id": 0,
"signatureKey": "VF9FRJx9Y2GOrAommEfkPK/cJ+FIzEa5ijxv3cc+yf31s33hUZG5zyC3v+K8EylzDxS/626GDO1aECZR7L+KPn4GZoMxbgT+XP3FeRrFWkgLqFtD9OQCekVmN1fNc2G6pwXxwer/M1HSKwkrD5faZf/N+T791lzz0whO2zSYpVJ9gW42bTwRc2PfRTShnjUHGErANYQ/IfbX536C8egVJUN0UNP5AXeapmVFGAntUq6JaRshMKFfnHUaRVjdJ306P5lczs5mNAQK6piuGOeq/Ts3+3K5e2s8/+nYs28pYFE3C3YDXq16C/ZgZEt2FF7uaP4waan/ug3/BdOEIZFL/Q==",
"encryptionKey": "EeSEqO/3q3RmKX26NgsbhbM6wsh+wssEdUwlXbiWOeJSkWXjvGzEy33/qZJ9sWqUXLfyTQTQCnfw71iLyoOoHzJmmUbWcEKmw6Mgp1a0LJd6K59+UFnU6IYzoLYlOOngNpXVj4Jcfhnz449z55zbJKKMIjVTxH9nxtAy58Pv2IAD7e6wAR56hQ6TOHr+p/oj2WQ7OsYGJEYJ3rtWjQ1LLcbvC5Z06OpJqIXBdR1+eGSNNYp7hGtZPxrnD9xSzRlpxIp2r0qXssSFv2mcLa0ecVRJJgOkGXSV+y5g1WNuKhD5vTsno95yfho6+y+iU715a3SdxR9ZkaZu6XvLcJkDEw==",
"searchableEncryptionKey": "Y+KaMPeS7bulN3rjZb0vfqyg+Wgf6QpH2KNwRUddSh41wIp3RSi19ts5Jbi4QOajlNhDt6iYK+FpneDmtFoXampzQL0uap4ljrcwsDs+PeFmLUdVv5mvUCAfrq8jeQQVfNPdetuLmBOOWxjTnLmVq4MC4QlIcALIYsRbnl3P/pAIRsshIiHjiC9qhRDahcK9dfa/mJb43WQpSmhBt9mjsP3FvkK8Bj116fy7v9ieAzs4Evv/9yYyj5Pjcd5ifNAxSVRCYniN5RJyjicAE7IlBjQ5VoAUPXOaBJnjheBYe9hodm1BaIHAZUbOggsbhzjVMXf7eGgNu8tEFiNiMoZ2GA=="
}
To support testing, both JUnit 4 and JUnit 5 plugins are provided that automatically wires up the necessary keys for Data Seal.
For JUnit 4 for a @ClassRule
DataSealRule
is provided via the data-seal-junit
module.
This can be used as follows
public class ExampleTest {
@ClassRule
public static DataSealRule beforeAll = new DataSealRule();
@Test
public void test() {
var bean = new Bean();
bean.setSigned("signed");
bean.seal();
bean.unseal();
}
}
For JUnit 5 for an extension DataSealExtension
is provided via the data-seal-junit-jupiter
module.
This can be used as follows
@ExtendWith(DataSealExtension.class)
public class ExampleTest {
@Test
public void test() {
var bean = new Bean();
bean.setSigned("signed");
bean.seal();
bean.unseal();
}
}
The seal framework supports Hibernate (v6.x.x) providing transparent signing and encryption of Hibernate entities as they are loaded and persisted, making it possible for developers to focus on what data needs to be secured, rather than keeping track of object states.
To use with hibernate entities, simply annotate the entity wit the @Signed
, @Encrypted
and @Searchable
annotations as required.
@Data
@Entity
public class BeanEntity implements Sealable {
@Signed
@Id
private int id;
@Signed
@Column
private String signed;
@Searchable
@Column
private String searchable;
@Encrypted
@Column
private String encrypted;
// Relations can also be signed
@Signed
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "child_entity_id")
private ChildEntity child;
@Metadata
@Column
private String metadata;
}
Data Seal comes with support for the Spring Boot (v3.x.x) via the data-seal-spring-booter-starter
module and for use with spring-boot-starter-data-jpa
the data-seal-hibernate-spring-boot-starter
provides for plug and play integration.
To configure Data Seal with Spring Boot a DataSealConfigurer
must be provided to the Spring Context, which implements methods for loading the encrypted key sets, a security module that supports encrypting and decrypting the encrypted key sets and a key alias to allow for rotation of security module keys.
Below is an example of how to implement the DataSealConfigurer
, that leverages Lombok and Jackson.
@Configuration
public class AppConfig implements DataSealConfigurer {
ObjectMapper mapper = new ObjectMapper();
@Override
@SneakyThrows
public List<EncryptedKeySet> encryptedKeySets() {
// Read encrypted key set serialized as json and located on the classpath
var keySet = mapper.readValue(getClass().getClassLoader().getResourceAsStream("keyset-0.json"), EncryptedKeySet.class);
return List.of(keySet);
}
@Override
public SecurityModule loadSecurityModule() {
// Read JKS keystore located on the classpath
var jksInputStream = AppConfig.class.getClassLoader().getResourceAsStream("keystore.jks");
return new SoftwareSecurityModule("jks", jksInputStream, "secret".toCharArray(), "key-alias", "secret".toCharArray(), "RSA");
}
@Override
public String securityModuleKeyAlias() {
return "key-alias";
}
}
The Spring Framework is released under version 2.0 of the Apache License.