如何使用 Tink 轻松加密和解密字符串?

How to easily encrypt and decrypt a String using Tink?

直到现在,我一直在使用 jasypt 加密字符串,然后在关闭应用程序时将其存储在磁盘上,然后在打开应用程序时从磁盘检索字符串后解密字符串。

使用 jasypt 非常简单,这是代码:

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);
}

它工作得很好,但现在,jasypt 已被弃用,我正在尝试迁移到 Google Tink 库,问题是 Google Tink 似乎比使用 jasypt 加密和解密字符串要复杂得多。

我在 Tink repo readme 中找不到加密和解密字符串的简单方法,只能找到更复杂的操作,实际上我无法理解,因为我对加密的知识完全是空的。因此,我使用了一个非常简单的库,比如 jasypt。

这是 Tink 存储库:https://github.com/Google/tink

有没有类似于我的 jasypt 代码的简单方法,用 Tink 加密和解密字符串?

注:post指的是Tink version 1.2.2。 posted 代码与更高版本部分不兼容。

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?。因此,在 Tink 中不可能通过密码进行加密,并且目前在 jasypt 代码中使用的概念无法实现。如果在任何情况下都使用基于密码的加密,这会破坏 Tink.

另一种方法是直接使用密钥。 TinkAesGcmJce-class which uses AES-GCM 用于加密。这里密钥的长度必须为 128 位或 256 位:

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());

使用简单,而且使用的密码(AES-GCM)是安全的。但是,Tink-开发者自己不推荐这种方法,因为AesGcmJce-class属于com.google.crypto.tink.subtle-包which may change at any time without further notice, (see also here,部分 重要警告 )。因此,这种方法也不是最优的。

嗯,Tink 通常如何使用对称加密?这显示在以下片段 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());

generateNew-方法生成一个新的密钥。然而,创建不是基于密码或字节序列,因此,为加密生成的密钥不能轻易重建用于解密。因此,用于加密的密钥必须保存到存储系统中,例如文件系统,以便稍后用于解密。 Tink 允许存储明文密钥(当然不推荐)。一种更安全的方法是使用存储在远程密钥管理系统中的主密钥对密钥进行加密(这在 TinkJAVA-HOWTO 部分 存储密钥集[=61 中有更详细的解释=] 和 加载现有键集).

Tink的密钥管理理念(旨在避免敏感密钥意外泄露material)也让它变得有些麻烦(这可能会在以后的版本中改变)。这就是为什么我在评论中说我不确定 Tink 是否符合您关于简单性的想法。

免责声明:我是 Tink 的首席开发人员。

如果您正在开发 Android 应用程序,您可以查看 AndroidKeysetManager. There's a hello world example,您可以从中进行复制。

一般来说,每当您想加密某些内容时,您应该问自己的第一个问题是您要将密钥存储在哪里。将密钥存储在存储加密数据的同一位置(并使用相同的 ACL)没有多大意义。密钥应存储在不同的位置(或使用不同的 ACL)。

Tink 的密钥管理 API 有点复杂,因为我们想要引导用户 to storing keys in the right location

我一直在寻找一种使用基于密码的加密 (PBE) 来加密短文本消息的简单方法,并使用 Google Tink 作为加密部分,发现 Tink 本身不提供 PBE。为了解决这个问题,我制作了一个简单的 class 来完成 PBE、键盘处理和 encryption/decryption.

的所有工作

在程序中的使用非常简单,只需要4行代码就可以使用:

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

在我的 Github 上,您可以找到两个示例程序来展示如何实现 class(有和没有 GUI):https://github.com/java-crypto/H-Google-Tink/tree/master/H%20Tink%20Textencryption%20PBE

这里是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;
    }
}

请记住,使用纯文本-String 意味着您的纯文本在您的堆中是不可变的且不可删除的,直到垃圾收集器销毁它们。

我的网站上有更详细的描述:http://javacrypto.bplaced.net/h-tink-string-encryption-using-pbe-and-gui/