Как легко зашифровать и расшифровать строку с помощью 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.

Я не могу найти в файле readme репозитория Tink простой способ зашифровать и расшифровать строку, но могу найти более сложные операции, которые на самом деле я не могу понять, потому что мои знания в области шифрования совершенно пусты. Из-за этого я использовал очень простую библиотеку, такую ​​как jasypt.

Это репозиторий Tink: https://github.com/Google/tink.

Есть ли простой способ, похожий на мой код jasypt, для шифрования и расшифровки строки с помощью Tink?


person NullPointerException    schedule 08.03.2019    source источник
comment
Из-за концепции управления ключами Tink генерация ключей является относительно ограниченной, например. не так просто сгенерировать ключ из последовательности байтов. А также не поддерживается шифрование на основе пароля (по состоянию на 08/2018). Это подробно обсуждается здесь. Таким образом, я не уверен, что Tink соответствует вашим представлениям о простоте. Кроме того, инкр./декр. с Tink просто, см., например. здесь, глава Шифрование с симметричным ключом.   -  person user 9014097    schedule 09.03.2019
comment
@Topaco, это все еще слишком сложно, может быть, вы можете опубликовать ответ с образцом о том, как этого добиться? Вы, кажется, эксперт в Tink. Спасибо   -  person NullPointerException    schedule 09.03.2019
comment
Я действительно не эксперт по Tink, и я тестировал его довольно поверхностно. Вот почему у меня, конечно, нет полного обзора функций. В своем ответе я обобщил некоторые аспекты, которые могут иметь отношение к вашему вопросу.   -  person user 9014097    schedule 10.03.2019


Ответы (3)


Примечание. Сообщение относится к Tink версии 1.2. .2. Опубликованный код частично несовместим с более поздними версиями.

Класс StrongTextEncryptor в вашем jasypt-примере кода используется PBEWithMD5AndTripleDES-алгоритм. Этот алгоритм использует блочный шифр с симметричным ключом Triple DES и извлекает ключ из пароля с помощью хеш-функции MD5. Последнее называется шифрование на основе пароля, и оно не поддерживается в Tink (по крайней мере, по состоянию на 08/2018), см. Как создать симметричный ключ шифрования с помощью Google Tink?. Таким образом, в Tink невозможно зашифровать с помощью пароля, и концепция, использовавшаяся до сих пор в jasypt-коде, не может быть реализована. Если в любом случае будет использоваться шифрование на основе пароля, это нарушит условия сделки для Tink.

Другой подход заключается в непосредственном использовании ключа. Tink имеет AesGcmJce — класс, который использует 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-класс принадлежит com.google.crypto.tink.subtle-пакету который может быть изменен в любое время без предварительного уведомления (см. также здесь, раздел Важные предупреждения). Следовательно, и этот подход не является оптимальным.

А как Tink обычно использует симметричное шифрование? Это показано в следующем фрагменте от:

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 позволяет хранить ключи в открытом виде (что, конечно, не рекомендуется). Более безопасным подходом является шифрование ключей мастер-ключами, хранящимися в удаленной системе управления ключами (более подробно это описано в JAVA-HOWTO, разделы Сохранение наборов ключей и Загрузка существующих наборов ключей).

Концепция управления ключами Tink (с целью предотвращения случайной утечки конфиденциального ключевого материала) также делает его несколько громоздким (это может измениться в более поздних версиях). Вот почему я сказал в своем комментарии, что не уверен, соответствует ли Tink вашим представлениям о простоте.

person user 9014097    schedule 10.03.2019
comment
Стоит отметить, что вы не можете сгенерировать новый KeysetHandle таким образом. Новый HOWTO от google Tink github показывает так: KeyTemplate keysetTemplate = AesGcmKeyManager.aes128GcmTemplate(); KeysetHandle keysetHandle = KeysetHandle.generateNew(keysetTemplate); - person Renis1235; 12.04.2021
comment
@ Renis1235 Renis1235 - Сообщение от марта 2019 года. Опубликованный код совместим с версией Tink на тот момент (1.2.2). Сегодняшняя (апрель 2021 г.) текущая версия Tink — 1.5.0, с которой опубликованный код больше не совместим. Я переместил ссылки из основной версии (в настоящее время версия 1.5) в версию 1.2.2, чтобы ссылаться на документацию 1.2.2 (как и раньше), а ссылки снова соответствовали тексту. - person user 9014097; 12.04.2021

Отказ от ответственности: я ведущий разработчик Tink.

Если вы работаете над приложением для Android, вы можете проверить AndroidKeysetManager. Есть пример привет, мир, который можно скопировать.

В общем, всякий раз, когда вы хотите что-то зашифровать, первый вопрос, который вы должны себе задать, это где вы собираетесь хранить ключи. Нет особого смысла хранить ключи в том же месте (и с тем же ACL), где вы храните свои зашифрованные данные. Ключи должны храниться в другом месте (или с другим ACL).

API управления ключами Tink немного сложнее, потому что мы хотим направлять пользователей для хранения ключей в нужном месте.

person Thai Duong    schedule 19.03.2019
comment
пожалуйста, не могли бы вы добавить простой способ шифрования и дешифрования строк для простых целей, как у jasypt? Иногда шифрование нужно для не слишком большого количества толковых данных, просто для подгонки небольшого и не сложного реквизита, и в таких случаях ваша библиотека слишком сложная - person NullPointerException; 20.03.2019
comment
@Thai- У меня был вопрос о максимальном размере зашифрованной строки, если мы используем AES-GCM 128-битный/256-битный keysetTemplate. Будет ли размер зашифрованной строки максимальным при определенном значении, потому что, если я хочу сохранить данные в базе данных, каким должен быть размер, с которым я должен его инициировать. - person Siddharth.Singh; 04.01.2021

Я искал простой способ зашифровать короткое текстовое сообщение с помощью шифрования на основе пароля (PBE) и использовать Google Tink для криптографической части и обнаружил, что Tink изначально не предоставляет PBE. Чтобы решить эту проблему, я сделал простой класс, который выполняет всю работу с PBE, обработкой ключей и шифрованием/дешифрованием.

Использование в программе очень простое, и для его использования вам нужно всего 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 вы найдете два примера программ, показывающих, как реализовать класс (с графическим интерфейсом и без него): https://github.com/java-crypto/H-Google-Tink/tree/master/H.%20Tink%20Textencryption%20PBE

Вот исходный код класса TinkPbe.java:

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/

person Michael Fehr    schedule 21.11.2019