Getting Started Spring Content with Encryption
What you'll build
We'll build on the previous guide Getting Started with Spring Content REST API.
What you'll need
-
About 30 minutes
-
A favorite text editor or IDE
-
JDK 1.8 or later
-
Maven 3.0+
-
Docker Desktop (for the vault test container)
How to complete this guide
Before we begin let's set up our development environment:
-
Download and unzip the source repository for this guide, or clone it using Git:
git clone https://github.com/paulcwarren/spring-content-gettingstarted.git
-
We are going to start where Getting Started with Spring Content REST API leaves off so
cd
intospring-content-gettingstarted/spring-content-rest/complete
When you're finished, you can check your results against the code in
spring-content-gettingstarted/spring-content-with-encryption/complete
.
Update dependencies
Add the com.github.paulcwarren:spring-content-encryption
dependency. This provides us the implementation of a EncryptingContentStore.
Also add org.testcontainers:vault:1.17.6
. We are going to use vault to provide a keyring for encrypting the content encryption key which in turn is used to encrypt the content.
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-content-with-encryption</artifactId>
<parent>
<groupId>com.github.paulcwarren</groupId>
<artifactId>gettingstarted-spring-content</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>com.github.paulcwarren</groupId>
<artifactId>spring-content-fs-boot-starter</artifactId>
<version>${spring.content.version}</version>
</dependency>
<dependency>
<groupId>com.github.paulcwarren</groupId>
<artifactId>spring-content-rest-boot-starter</artifactId>
<version>${spring.content.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.github.paulcwarren</groupId>
<artifactId>spring-content-encryption</artifactId>
<version>${spring.content.version}</version>
</dependency>
Vault TestContainer
First we add a simple class that creates and starts a vault test container upon demand. The vault is configured with a vault token root-token
(referenced later) and enables the transit module that provides cryptographic functions to the encrypting content store.
src/main/java/gettingstarted/VaultContainerSupport.java
package gettingstarted;
import org.testcontainers.vault.VaultContainer;
public class VaultContainerSupport {
private static VaultContainer vaultContainer = null;
public static VaultContainer getVaultContainer() {
if (vaultContainer == null) {
vaultContainer = new VaultContainer<>()
.withVaultToken("root-token")
.withVaultPort(8200)
.withInitCommand("secrets enable transit");
vaultContainer.start();
}
return vaultContainer;
}
}
Configuration
Next we need to introduce a small Configuration
class to provide a vault endpoint to our application as well as configuring our encrypting content store. We add the following @Configuration
to our SpringContentApplication
.
src/main/java/gettingstarted/SpringContentApplication.java
package gettingstarted;
import internal.org.springframework.content.fragments.EncryptingContentStoreConfiguration;
import internal.org.springframework.content.fragments.EncryptingContentStoreConfigurer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.content.encryption.EnvelopeEncryptionService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.vault.authentication.ClientAuthentication;
import org.springframework.vault.authentication.TokenAuthentication;
import org.springframework.vault.client.VaultEndpoint;
import org.springframework.vault.config.AbstractVaultConfiguration;
import org.springframework.vault.core.VaultOperations;
@SpringBootApplication
public class SpringContentApplication {
public static void main(String[] args) {
SpringApplication.run(SpringContentApplication.class, args);
}
@Configuration
public static class Config extends AbstractVaultConfiguration {
@Override
public VaultEndpoint vaultEndpoint() {
String host = VaultContainerSupport.getVaultContainer().getHost();
int port = VaultContainerSupport.getVaultContainer().getMappedPort(8200);
VaultEndpoint vault = VaultEndpoint.create(host, port);
vault.setScheme("http");
return vault;
}
@Override
public ClientAuthentication clientAuthentication() {
return new TokenAuthentication("root-token");
}
@Bean
public EnvelopeEncryptionService encrypter(VaultOperations vaultOperations) {
return new EnvelopeEncryptionService(vaultOperations);
}
@Bean
public EncryptingContentStoreConfigurer config() {
return new EncryptingContentStoreConfigurer<FileContentStore>() {
@Override
public void configure(EncryptingContentStoreConfiguration config) {
config.keyring("fsfile").encryptionKeyContentProperty("key");
}
};
}
}
}
Points to note:
- the clientAuthentication
references the root-token
- the encrypter
bean creates an instance an EnvelopeEncryptionService
. This is used by the EncyptingContentStore
to provide envelope encryption on the content.
- the config
bean configures the EnryptingContentStore
specifying the vault keyring to use to encrypt the content encryption key and the content property attribute to use to store that encrypted key for later use when decrypting content.
Update File
Update File to add the new content property attribute called key
that will store the encrypted content encryption key.
src/main/java/gettingstarted/File.java
package gettingstarted;
import java.util.Date;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.content.commons.annotations.ContentId;
import org.springframework.content.commons.annotations.ContentLength;
import org.springframework.content.commons.annotations.MimeType;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@Setter
@NoArgsConstructor
public class File {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private Date created = new Date();
private String summary;
@ContentId private String contentId;
@ContentLength private long contentLength;
@MimeType private String contentMimeType = "text/plain";
@JsonIgnore
private byte[] contentKey;
}
Update FileContentStore
Decorate the FileContentStore
as an EncryptingContentStore
to enable encryption/decryption on the content.
src/main/java/gettingstarted/FileContentStore.java
package gettingstarted;
import org.springframework.content.commons.repository.ContentStore;
import org.springframework.content.encryption.EncryptingContentStore;
import org.springframework.content.rest.StoreRestResource;
@StoreRestResource
public interface FileContentStore extends ContentStore<File, String>, EncryptingContentStore<File, String> {
}
Build an executable JAR
If you are using Maven, you can run the application using mvn spring-boot:run
.
Or you can build the JAR file with mvn clean package
and run the JAR
by typing:
java -jar target/gettingstarted-spring-content-with-encryption-0.0.1.jar
As the application starts up look for the root of the filesystem storage. We'll look in here shortly to check content is encrypted. Look for the
log entry that starts Default filesystem storage to ...
and copy the file path. e.g.
2022-12-06 21:45:14.180 INFO 89223 --- [ main] o.s.c.fs.io.FileSystemResourceLoader : Defaulting filesystem root to /var/folders/65/d8zxcwh13sjfrkry92vgs5x80000gr/T/13946690314057106966
Test Encryption
Create an entity:
$ curl -X POST -H 'Content-Type:application/hal+json' -d '{}' http://localhost:8080/files/
Associate content with that entity:
$ curl -X PUT -H 'Content-Type:text/plain' -d 'Hello Spring Content World!' http://localhost:8080/files/1/content
Get the content id:
$ curl -X GET -H 'Accept:application/hal+json' http://localhost:8080/files/1
{
"name" : null,
"created" : "2022-12-07T05:47:42.356+00:00",
"summary" : null,
"contentId" : "2d654dfc-57dc-44b7-aad9-f9ad0701c5d1",
"contentLength" : 27,
"contentMimeType" : "text/plain",
"_links" : {
"self" : {
"href" : "http://localhost:8080/files/1"
},
"file" : {
"href" : "http://localhost:8080/files/1"
},
"content" : {
"href" : "http://localhost:8080/files/1/content"
}
}
}
Copy the contentId
Check the content is encrypted:
$ cat /var/folders/65/d8zxcwh13sjfrkry92vgs5x80000gr/T/13946690314057106966/<contnetId>
i.e.
$ cat /var/folders/65/d8zxcwh13sjfrkry92vgs5x80000gr/T/13946690314057106966/2d654dfc-57dc-44b7-aad9-f9ad0701c5d1
��H�}��l������إ�.��^
Fetch the content:
$ curl -H 'Accept:text/plain' http://localhost:8080/files/1/content
Hello Spring Content World!
Summary
Congratulations! You've just written a simple application that uses Spring Content and Spring Content Encryption to be store content encrypted.
Spring Content Encryption is also capable of serving bytes ranges. The default implementation uses AES CTR encryption and when used with Spring Content S3 will decrypt and serve just the byte ranges. With any other storage the content will be fully decrypted before serving the byte range.