9

Until now, I was using jasypt to encrypt a string before storing it on disk on app closing, and later when opening the app for decrypt the string after retrieving it from disk.

It was super easy with jasypt, this was the code:

private static final String JASYPT_PWD = "mypassword";

public static String encryptString(String string) {
    StrongTextEncryptor textEncryptor = new StrongTextEncryptor();
    textEncryptor.setPassword(JASYPT_PWD);
    return textEncryptor.encrypt(string);
}

public static String decryptString(String string) {
    StrongTextEncryptor textEncryptor = new StrongTextEncryptor();
    textEncryptor.setPassword(JASYPT_PWD);
    return textEncryptor.decrypt(string);
}

It worked perfectly, but now, jasypt is deprecated and I'm trying to migrate to the Google Tink library, the problem is that Google Tink seems to be much more complex for just encrypt and decrypt a string as easily as with jasypt.

I can't find in the Tink repo readme the simple way to encrypt and decrypt a string, just can find more complex operations which in fact I can't understand because my knowledge in encryption is totally empty. Because of that I was using a very easy library like jasypt.

This is the Tink repo: https://github.com/Google/tink

Is there an easy way, similar to my jasypt code, to encrypt and decrypt a string with Tink?

Joachim Sauer
  • 302,674
  • 57
  • 556
  • 614
NullPointerException
  • 36,107
  • 79
  • 222
  • 382
  • Because of Tink's key management concept key generation is relatively restrictive, e.g. it's not so easy to generate a key from a byte-sequence. And also password-based encryption is not supported (as at 08/2018). This is discussed in detail [here](https://stackoverflow.com/questions/52171198/how-to-create-symmetric-encryption-key-with-google-tink). Thus, I'm not sure if Tink fits your ideas concerning simplicity. Apart from this, encr./decr. with Tink is straightforward, see e.g. [here](https://github.com/google/tink/blob/master/docs/JAVA-HOWTO.md), chapter "Symmetric Key Encryption". – Topaco Mar 09 '19 at 16:04
  • @Topaco it's still too much complex, maybe can you post an answer with a sample about how to achieve it? You seems to be an experte in Tink. Thank you – NullPointerException Mar 09 '19 at 16:16
  • I'm really no Tink-expert and I tested it rather superficially. That's why I certainly don't have a complete overview of the features. In my answer, I have summarized some aspects that may be relevant to your question. – Topaco Mar 10 '19 at 08:56

3 Answers3

16

Note: The post refers to Tink version 1.2.2. The posted code is partially incompatible with later versions.

The StrongTextEncryptor-class in your jasypt-example-code uses the PBEWithMD5AndTripleDES-algorithm. This algorithm uses the symmetric-key block cipher Triple DES and derives the key from the password using the MD5 hash function. The latter is called password-based encryption and this isn't supported in Tink (at least as at 08/2018), see How to create symmetric encryption key with Google Tink?. Thus, it's impossible in Tink to encrypt by means of a password and the concept used so far in the jasypt-code couldn't be implemented. If password-based encryption is to be used in any case that is a deal-breaker for Tink.

Another approach is to directly use a key. Tink has the AesGcmJce-class which uses AES-GCM for encryption. Here the key must have a length of either 128 Bit or 256 bit:

String plainText = "This is a plain text which needs to be encrypted!";
String aad = "These are additional authenticated data (optional)";
String key = "ThisIsThe32ByteKeyForEncryption!"; // 256 bit
    
// Encryption
AesGcmJce agjEncryption = new AesGcmJce(key.getBytes());
byte[] encrypted = agjEncryption.encrypt(plainText.getBytes(), aad.getBytes());

// Decryption
AesGcmJce agjDecryption = new AesGcmJce(key.getBytes());
byte[] decrypted = agjDecryption.decrypt(encrypted, aad.getBytes());

The use is simple and furthermore the used cipher (AES-GCM) is secure. However, the Tink-developers themselves don't recommend this approach because the AesGcmJce-class belongs to the com.google.crypto.tink.subtle-package which may change at any time without further notice, (see also here, section Important Warnings). Therefore, also this approach isn't optimal.

Well, how does Tink typically use symmetric encryption? This is shown in the following snippet from:

String plainText = "This is a plain text which needs to be encrypted!";
String aad = "These are additional authenticated data (optional)";

AeadConfig.register();
    
KeysetHandle keysetHandle = KeysetHandle.generateNew(AeadKeyTemplates.AES256_GCM);
Aead aead = AeadFactory.getPrimitive(keysetHandle);
    
// Encryption
byte[] ciphertext = aead.encrypt(plainText.getBytes(), aad.getBytes());

// Decryption
byte[] decrypted = aead.decrypt(ciphertext, aad.getBytes());

The generateNew-method generates a new key. However, the creation isn't based on a password or a byte-sequence and because of this, a key generated for the encryption can't be easily reconstructed for decryption. Therefore, the key used for encryption has to be persisted to a storage system, e.g. the file system, so it can be used later for decryption. Tink allows the storing of cleartext keys (which is of course not recommended). A more secure approach is the encryption of keys with master keys stored in a remote key management system (this is in more detail explained Tink's JAVA-HOWTO, sections Storing Keysets and Loading Existing Keysets).

Tink's key management concept (with the aim of avoiding accidental leakage of sensitive key material) makes it also somehow cumbersome (this may change in later versions). That's why I said in my comment that I'm not sure if Tink fits your ideas concerning simplicity.

Topaco
  • 40,594
  • 4
  • 35
  • 62
  • 1
    Worth mentioning that you cannot generate a new `KeysetHandle` that way. The new HOWTO from google Tink github shows this way: `KeyTemplate keysetTemplate = AesGcmKeyManager.aes128GcmTemplate(); KeysetHandle keysetHandle = KeysetHandle.generateNew(keysetTemplate);` – Renis1235 Apr 12 '21 at 06:38
  • 2
    @Renis1235 - The post is from March 2019. The posted code is compatible with the Tink version at that time (1.2.2). Today's (April 2021) current Tink version is 1.5.0, to which the posted code is no longer compatible. I've moved the links from the master version (currently version 1.5) to version 1.2.2 so that the 1.2.2 documentation is referenced (as before) and the links are consistent with the text again. – Topaco Apr 12 '21 at 20:36
10

Disclaimer: I'm Tink's lead developer.

If you're working on an Android app, you can check out AndroidKeysetManager. There's a hello world example that you can copy from.

In general whenever you want to encrypt something, the first question you should ask yourself is where you're going to store keys. It doesn't make a lot of senses to store keys in the same place (and with the same ACL) where you store your encrypted data. Keys should be stored in a different location (or with different ACL).

Tink's Key Management API is a bit more complex because we want to steer users to storing keys in the right location.

HexAndBugs
  • 5,549
  • 2
  • 27
  • 36
Thai Duong
  • 199
  • 8
  • 2
    please whould you add a simple way to encrypt and decrypt strings for simple uses like jasypt has? Sometimes, the encryption is needed for not too much sensible data, just for fit a small and not complex requisite, and in such cases, your library is too much complex – NullPointerException Mar 20 '19 at 08:17
  • @Thai- The question I had was about the maximum size of encrypted string if we use AES-GCM 128 bit/256-bit keysetTemplate. Would the size of the encrypted string max out at a particular value because if I want to persist the data in the database, what should be the size that I should initiate it with. – Siddharth.Singh Jan 04 '21 at 01:52
3

I was looking for a simple way to encrypt a short text message with a Password Based Encryption (PBE) and use Google Tink for the cryptographic part and found that Tink doesn't provide PBE natively. To solve this problem I made a simple class that does all the work with PBE, keyhandling and encryption/decryption.

The usage in a program is very simple and you need only 4 lines of code to use it:

AeadConfig.register(); // tink initialisation
TinkPbe tpbe = new TinkPbe(); // tink pbe initialisation
String ciphertextString = tpbe.encrypt(passwordChar, plaintextString); // encryption
String decryptedtextString = tpbe.decrypt(passwordChar, ciphertextString); // decryption

On my Github you find two sample programms to show how to implement the class (with and without GUI): https://github.com/java-crypto/H-Google-Tink/tree/master/H%20Tink%20Textencryption%20PBE

Here is the sourcecode of the TinkPbe.java-class:

package tinkPbe;

/*
*  
* Diese Klasse gehört zu diesen beiden Hauptklassen
* This class belongs to these main classes:
* TinkPbeConsole.java | TinkPbeGui.java 
* 
* Herkunft/Origin: http://javacrypto.bplaced.net/
* Programmierer/Programmer: Michael Fehr
* Copyright/Copyright: frei verwendbares Programm (Public Domain)
* Copyright: This is free and unencumbered software released into the public domain.
* Lizenttext/Licence: <http://unlicense.org>
* getestet mit/tested with: Java Runtime Environment 8 Update 191 x64
* getestet mit/tested with: Java Runtime Environment 11.0.1 x64
* Datum/Date (dd.mm.jjjj): 20.11.2019
* Funktion: verschlüsselt und entschlüsselt einen Text mittels Google Tink
*           im Modus AES GCM 256 Bit. Der Schlüssel wird mittels PBE
*           (Password based encryption) erzeugt.
* Function: encrypts and decrypts a text message with Google Tink.
*           Used Mode is AES GCM 256 Bit. The key is generated with PBE
*           (Password based encryption).
*
* Sicherheitshinweis/Security notice
* Die Programmroutinen dienen nur der Darstellung und haben keinen Anspruch auf eine korrekte Funktion, 
* insbesondere mit Blick auf die Sicherheit ! 
* Prüfen Sie die Sicherheit bevor das Programm in der echten Welt eingesetzt wird.
* The program routines just show the function but please be aware of the security part - 
* check yourself before using in the real world !
* 
* Das Programm benötigt die nachfolgenden Bibliotheken (siehe Github Archiv):
* The programm uses these external libraries (see Github Archive):
* jar-Datei/-File: tink-1.2.2.jar
* https://mvnrepository.com/artifact/com.google.crypto.tink/tink/1.2.2
* jar-Datei/-File: protobuf-java-3.10.0.jar
* https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java/3.10.0
* jar-Datei/-File: json-20190722.jar
* https://mvnrepository.com/artifact/org.json/json/20190722
*  
*/

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import com.google.crypto.tink.Aead;
import com.google.crypto.tink.CleartextKeysetHandle;
import com.google.crypto.tink.JsonKeysetReader;
import com.google.crypto.tink.KeysetHandle;
import com.google.crypto.tink.aead.AeadFactory;

public class TinkPbe {

    public static String encrypt(char[] passwordChar, String plaintextString)
            throws GeneralSecurityException, IOException {
        byte[] keyByte = pbkdf2(passwordChar);
        String valueString = buildValue(keyByte);
        String jsonKeyString = writeJson(valueString);
        KeysetHandle keysetHandleOwn = CleartextKeysetHandle.read(JsonKeysetReader.withString(jsonKeyString));
        // initialisierung
        Aead aead = AeadFactory.getPrimitive(keysetHandleOwn);
        // verschlüsselung
        byte[] ciphertextByte = aead.encrypt(plaintextString.getBytes("utf-8"), null); // no aad-data
        return Base64.getEncoder().encodeToString(ciphertextByte);
    }

    public static String decrypt(char[] passwordChar, String ciphertextString)
            throws GeneralSecurityException, IOException {
        byte[] keyByte = pbkdf2(passwordChar);
        String valueString = buildValue(keyByte);
        String jsonKeyString = writeJson(valueString);
        KeysetHandle keysetHandleOwn = CleartextKeysetHandle.read(JsonKeysetReader.withString(jsonKeyString));
        // initialisierung
        Aead aead = AeadFactory.getPrimitive(keysetHandleOwn);
        // verschlüsselung
        byte[] plaintextByte = aead.decrypt(Base64.getDecoder().decode(ciphertextString), null); // no aad-data
        return new String(plaintextByte, StandardCharsets.UTF_8);
    }

    private static byte[] pbkdf2(char[] passwordChar)
            throws NoSuchAlgorithmException, InvalidKeySpecException, UnsupportedEncodingException {
        final byte[] passwordSaltByte = "11223344556677881122334455667788".getBytes("UTF-8");
        final int PBKDF2_ITERATIONS = 10000; // anzahl der iterationen, höher = besser = langsamer
        final int SALT_SIZE_BYTE = 256; // grösse des salts, sollte so groß wie der hash sein
        final int HASH_SIZE_BYTE = 256; // größe das hashes bzw. gehashten passwortes, 128 byte = 512 bit
        byte[] passwordHashByte = new byte[HASH_SIZE_BYTE]; // das array nimmt das gehashte passwort auf
        PBEKeySpec spec = new PBEKeySpec(passwordChar, passwordSaltByte, PBKDF2_ITERATIONS, HASH_SIZE_BYTE);
        SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512");
        passwordHashByte = skf.generateSecret(spec).getEncoded();
        return passwordHashByte;
    }

    private static String buildValue(byte[] gcmKeyByte) {
        // test for correct key length
        if ((gcmKeyByte.length != 16) && (gcmKeyByte.length != 32)) {
            throw new NumberFormatException("key is not 16 or 32 bytes long");
        }
        // header byte depends on keylength
        byte[] headerByte = new byte[2]; // {26, 16 }; // 1A 10 for 128 bit, 1A 20 for 256 Bit
        if (gcmKeyByte.length == 16) {
            headerByte = new byte[] { 26, 16 };
        } else {
            headerByte = new byte[] { 26, 32 };
        }
        byte[] keyByte = new byte[headerByte.length + gcmKeyByte.length];
        System.arraycopy(headerByte, 0, keyByte, 0, headerByte.length);
        System.arraycopy(gcmKeyByte, 0, keyByte, headerByte.length, gcmKeyByte.length);
        String keyBase64 = Base64.getEncoder().encodeToString(keyByte);
        return keyBase64;
    }

    private static String writeJson(String value) {
        int keyId = 1234567; // fix
        String str = "{\n";
        str = str + "    \"primaryKeyId\": " + keyId + ",\n";
        str = str + "    \"key\": [{\n";
        str = str + "        \"keyData\": {\n";
        str = str + "            \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesGcmKey\",\n";
        str = str + "            \"keyMaterialType\": \"SYMMETRIC\",\n";
        str = str + "            \"value\": \"" + value + "\"\n";
        str = str + "        },\n";
        str = str + "        \"outputPrefixType\": \"TINK\",\n";
        str = str + "        \"keyId\": " + keyId + ",\n";
        str = str + "        \"status\": \"ENABLED\"\n";
        str = str + "    }]\n";
        str = str + "}";
        return str;
    }
}

Please keep in mind that using a plaintext-String means that your plaintext is inmutable and undeletable in your heap until the Garbage Collector destroys them.

A more detailed description is vailable on my website: http://javacrypto.bplaced.net/h-tink-string-encryption-using-pbe-and-gui/

Michael Fehr
  • 5,827
  • 2
  • 19
  • 40