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 into spring-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.