Google Tink - How to encrypt your data
23 November 2022

Whenever we store data that contains sensitive information about our users, we need to make sure that we handle this in a secure way, because you know, its the responsible adult thing to do. After you have thought carefully and decided that you do in fact need to store the data (after all, an attacker can’t steal what is not there, points to temple) , encryption is a way to add an aditional security layer and make things harder for an attacker. In your typical Spring application there exist multiple ways of achieving this. Let’s dive into it.
Introduction
Whenever we store data that contains sensitive information about our users, we need to make sure that we handle this in a secure way, because you know, its the responsible adult thing to do.
After you have thought carefully and decided that you do in fact need to store the data (after all, an attacker can’t steal what is not there. points to temple) , encryption is a way to add an aditional security layer and make things harder for an attacker. In your typical Spring application there exist multiple ways of achieving this. Let’s dive into it.
Terminology & Context
The following terms are important to understand when first dealing with cryptography. If you are already familiar or just want to see the code, feel free to skip.
Plaintext: The original message before encryption.
Ciphertext: The message after encipherment.
Cipher: Any general system for hiding the meaning of a message by replacing each letter in the orignal message with
another letter.
Encryption Algorithm: Any general encryption proceess which can be specified exactly by choosing a key. (e.g AES )
Key: The element that turns the general encryption algorithm into a specific method for encryption.
Key length: Computer encryption involves keys which are numbers. The key length refers to the number of digits or
bits in the key, and thus indicates the biggest number that can be used as a key, thereby definging the number of
possible keys. The longer the key length, the longer it will take to bruteforce your way through all possibly
combinations.
By using a key with an encryption algorithm you turn the plaintext into a ciphertext. Great!
Now lets look at a simple example from ancient times using the Caesar cipher which shifts the regular alphabet 3
letters to the right:
Plain alphabet: abcdefghijklmnopqrstuvwxyz
Cipher alphabet: DEFGHIJKLMNOPQRSTUVWXYZABC
Plaintext: veni, vidi, vici
Ciphertext: YHQL, YLGL, YLFL
Voilà! There is your encrypted message. Of course, over the course of history cryptography grew way more complex than that. If you are interested in that kind of stuff, I can recommend The Code Book by Simon Singh, where I took that example from.
Implementation
Generally speaking, if you don’t want your company’s name in the headlines, you should not write your own encryption algorithm. As of writing this article, there exist a couple of proven libraries to help you out with this task, just to name a few:
- Javax.Crypto
- Google Tink
- Jasypt
- Bouncy Castle
While you should probably able to handle all your use cases with the javax.crypto library, the focus of this post will be Tink, an open-source cryptography library written by cryptographers and security engineers at Google, which by its own accord has simple APIs that reduce common pitfalls through user-centered design, careful implementation, code reviews, and extensive testing.
The implementation of a CryptoService using Tink is pretty straightforward, as you can see in the following example.
public class AESCryptoService implements CryptoService {
Aead aead;
Base64.Encoder encoder = Base64.getEncoder();
Base64.Decoder decoder = Base64.getDecoder();
public AESCryptoService() throws GeneralSecurityException {
AeadConfig.register();
KeysetHandle keysetHandle = KeysetHandle.generateNew(KeyTemplates.get("AES256_GCM"));
this.aead = keysetHandle.getPrimitive(Aead.class);
}
public String encrypt(String plainText, String associatedData) {
byte[] ciphertext;
try {
ciphertext = aead.encrypt(plainText.getBytes(), associatedData.getBytes());
} catch (GeneralSecurityException e) {
throw new IllegalStateException(e);
}
return this.encoder.encodeToString(ciphertext);
}
public String decrypt(String cipherText, String associatedData) {
byte[] decodedCipherText = this.decoder.decode(cipherText);
byte[] plainText;
try {
plainText = aead.decrypt(decodedCipherText, associatedData.getBytes(StandardCharsets.UTF_8));
} catch (GeneralSecurityException e) {
throw new IllegalStateException(e);
}
return new String(plainText);
}
Here we are using an AES256 encryption algorithm together with a simple Base64 encoding to encrypt & decrypt our data.
A couple of important things to note here though. It is not recommended to create the encryption key in the code. As we have seen before, the encryption key must be kept secret at all cost. In an enterprise environment, the keysetHandle should be provided from outside the application and should be rotated regularly. Hashicorp Vault has excellent support for this usecase and describes it in their documentation.
Another important concept is the associatedData we provide together with the plainText, the Tink documentation describes it as follows:
AEAD can also be used to tie ciphertext to specific associated data. For example, suppose you have a database with a field,
user-id, and a field,encrypted-medical-history. In this case,user-idshould be used as associated data when encrypting the medical history. This ensures that an attacker cannot move medical history from one user to another.
To showcase how this all ties together, you can have a look at the following Unit tests that illustrate these concepts.
class AESCryptoServiceTest {
CryptoService cryptoService;
@BeforeEach
void setUp() throws GeneralSecurityException {
this.cryptoService = new AESCryptoService();
}
@Test
void testEncryptingAndDecryptingResultsInSamePlainText() {
// Arrange
String associatedData = UUID.randomUUID().toString();
String plainText = "plainText";
// Act
String encryptedString = cryptoService.encrypt(plainText, associatedData);
String decryptedString = cryptoService.decrypt(encryptedString, associatedData);
// Assert
Assertions.assertAll("Assert encryption and decryption",
() -> Assertions.assertNotEquals(plainText, encryptedString),
() -> Assertions.assertEquals(plainText, decryptedString));
}
@Test
void testEncryptingAndDecryptingWithDifferentAssociatedDataFails() {
// Arrange
String associatedData = UUID.randomUUID().toString();
String fakeAssociatedData = UUID.randomUUID().toString();
String plainText = "plainText";
// Act
String encryptedString = cryptoService.encrypt(plainText, associatedData);
Exception exception = assertThrows(IllegalStateException.class, () -> {
cryptoService.decrypt(encryptedString, fakeAssociatedData);
});
// Assert
Assertions.assertAll("Assert decrypting with different associated Data fails",
() -> assertEquals("java.security.GeneralSecurityException: decryption failed", exception.getMessage())
);
}
So now, what is the best way to integrate this Service into our codebase to make sure we always encrypt and decrypt our sensitive Fields?
Integrating Cryptography in a Java/Spring application
Using Java and Spring, you have a couple of possibilities. Since it can be argued that cryptography in an application is a cross-cutting concern, it makes sense that the usage should be centralized, easy to use and as explicit as possible. Keeping this in mind, we can go through the list. At first glance, it makes sense to use JPA for this, for example an AttributeConverter or EntityListener. While I first started out with this approach, I noticed a couple of drawbacks.
Unfortunately, AttributeConverters are not yet spring managed, so you would need to statically access the spring context, which feels a bit hacky to me. Also, this makes accessing SpringProperties, which might be used to access the encryption key, harder. Also, you don’t have easy access to EntityInformation when using @Convert on a specific attribute, lets say a simple String, as per interface definition. If you want to use context data of the same entity as associated data, this is a problem.
JPA Entity Listeners seem to solve both of these problems, they are spring managed since JPA 2.1 and in the JPA Lifecycle hooks you get access to the whole entity. Although this might work depending on the complexity of your data model and your understanding of the intrinsics of JPA lifecycles, this can add hard to debug complexity to your project and I personally didn’t feel 100% comfortable using this approach.
So while exploring these options and hitting a dead end, the next place to handle this issue seemed to be directly on the entity. One could argue that the business/application layer might be the correct place for encryption, but then again – encryption isn’t really a fundamental aspect of most systems and more on the level as Security, Logging etc. So lets finally get to the “final” solution:
public class User {
private UUID id;
private String associatedData;
private String name;
private String email;
private SensitiveField sensitiveInformation = new SensitiveField();
public String getSensitiveInformation(CryptoService cryptoService) {
return this.sensitiveInformation.getPlainText(cryptoService, this.associatedData);
}
public void setSensitiveInformation(CryptoService cryptoService, String sensitiveInformation) {
this.sensitiveInformation.setPlainText(cryptoService, sensitiveInformation, this.associatedData);
}
public SensitiveField getSensitiveInformation() {
throw new UnsupportedOperationException();
}
}
Here you can see the new type that was introduced, the SensitiveField that contains the information that needs to be encrypted.
public class SensitiveField {
private transient String plainText;
private String cipherText;
public String getPlainText(CryptoService cryptoService, String associatedData) {
if (!StringUtils.hasText(this.plainText) && StringUtils.hasText(this.cipherText)) {
this.plainText = cryptoService.decrypt(this.cipherText, associatedData);
}
return this.plainText;
}
public void setPlainText(CryptoService cryptoService, String plainText, String associatedData) {
this.cipherText = cryptoService.encrypt(plainText, associatedData);
this.plainText = plainText;
}
}
And inside the type, the magic happens. As you can see, the plaintext only exists as a transient attribute to prevent it from serialisation. As we can see, our cryptoService is passed down from the calling Service, which isn’t entirely pleasing but at least makes everything quite explicit and self-explanatory.
We can run a final test to showcase that everything works as aspected:
@Test
void testEncryptionDecryptionWithGetterSetterApproach(){
// Arrange
var user=new User();
user.setAssociatedData("associatedData");
user.setSensitiveInformation(cryptoService,"sensitive");
// Act
String sensitiveInformation=user.getSensitiveInformation(cryptoService);
// Assert
Assertions.assertAll("Assert Encryption & Decryption with Getter/Setter Approach",
() -> assertEquals(sensitiveInformation,"sensitive"));
}
(Although to be fair, to be 100% sure we would need a proper integration test that fetches the encrypted value directly from the database).
Conclusion
And we are good to go! Hope you enjoyed this little example showcasing the Google Tink Library.
If you want to try it out yourself, feel free to head over to github for the full demo.