Шифрование и дешифрование с помощью PBKDF2 и AES256 - необходим практический пример - как получить производный ключ

Я пытаюсь понять, как получается производный ключ с помощью PBKDF2 с SHA256.

Я запуталась, и мне нужен ясный, простой для понимания пример.

Что у меня есть на данный момент:

  1. Я нашел https://en.wikipedia.org/wiki/PBKDF2 с пример, но с SHA1, со следующими значениями:

    ПАРОЛЬ plnlrtfpijpuhqylxbgqiiyipieyxvfsavzgxbbcfusqkozwpngsyejqlmjsytrmd UTF8

    СОЛЬ A009C1A485912C6AE630D3E744240B04 HEX

    Функция хеширования SHA1

    Размер ключа 128

    Итерации 1000

  2. Я использовал https://gchq.github.io/CyberChef и могу получить результат 17EB4014C8C461C300E9B61518B9A18B, который соответствует производным байтам ключа в примере из Википедии.


Я работал с https://mkyong.com/java/java-aes-encryption-and-decryption/, в котором есть метод getAESKeyFromPassword, который находится здесь:

// Password derived AES 256 bits secret key
public static SecretKey getAESKeyFromPassword(char[] password, byte[] salt)
        throws NoSuchAlgorithmException, InvalidKeySpecException {

    SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
    // iterationCount = 65536
    // keyLength = 256
    KeySpec spec = new PBEKeySpec(password, salt, 65536, 256);
    SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
    return secret;

}

Я хочу провести такое же расследование, как я проделал со страницей Википедии, SHA1 и CyberChef, но используя SHA256 (заменяя значения в коде Java, чтобы они соответствовали соли, паролю, итерациям). , из примера).

Вот где начинается мое замешательство:

Если бы я использовал CyberChef для работы с теми же значениями, что и выше, но заменил бы на SHA256:

ПАРОЛЬ plnlrtfpijpuhqylxbgqiiyipieyxvfsavzgxbbcfusqkozwpngsyejqlmjsytrmd UTF8

СОЛЬ A009C1A485912C6AE630D3E744240B04 HEX

Функция хеширования SHA256

Размер ключа 128

Итерации 1000

Я ожидаю, что производный ключ будет таким же в CyberChef, что и https://mkyong.com/java/java-aes-encryption-and-decryption/.

Это не так.

Я не могу не думать, что в моем понимании есть изъян.

Может ли кто-нибудь предоставить простой (проработанный) пример PBKDF2 с SHA256, чтобы я мог понять, что происходит. Если производный ключ не должен быть таким же (как в примере с SHA1, объясните, почему).

Секретный ключ Java:

SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");

То же, что и производный ключ?

Кажется, не хватает простых для понимания примеров для подражания.

Спасибо

Майлз.


person chocksaway    schedule 10.10.2020    source источник
comment
Вы уже заметили, что mkyong-код использует 65536 итераций и длину ключа 256? Мне неясно, что вы сравниваете - использование хэша SHA-1 определенно даст другой результат, использующий SHA-256. В-третьих: factory.generateSecret (spec) .getEncoded () дает вам ключ, который используется с new SecretKeySpec ... AES для генерации AES-ключа. Кстати: соль должна быть случайной в программах с реальным словом, чтобы получать разные хеши даже при использовании одной и той же строки пароля.   -  person Michael Fehr    schedule 10.10.2020
comment
В Java PBKDF2 работает немного иначе. Создаваемый вами ключ - это просто контейнер для вводимых значений, таких как пароль. Фактический вывод ключа выполняется самим шифром. См. Все ответы на этот вопрос: stackoverflow.com/q/39954211/150978   -  person Robert    schedule 10.10.2020
comment
Я не могу воспроизвести это: с вашими значениями: пароль: pln..., соль: _2 _..., дайджест: SHA256, размер ключа: 128 и итераций: 1000 метод getAESKeyFromPassword() возвращает шестнадцатеричное кодирование 28869B5F31AE29236F164C5CB33E2E3B, что равно CyberChef результат. Возможно, вам стоит опубликовать полный getAESKeyFromPassword()-звонок.   -  person user 9014097    schedule 10.10.2020
comment
В этот код включено много текста, но фактические значения, которые вы генерируете или требуемые, нет. Убедитесь, что вы разместили MCVE и фактические значения для сравнения; указывать для них на внешние ресурсы крайне не рекомендуется.   -  person Maarten Bodewes    schedule 10.10.2020


Ответы (1)


Спасибо всем за ваш вклад, особенно Topaco :)

Я собираюсь ответить на свой вопрос, так как я некоторое время работал над MCVE, и мне удалось получить тот же SecretKey, что и cyberChef.

Значение секретного ключа: 28869b5f31ae29236f164c5cb33e2e3bb46f483867a15f8e7208e1836070f64a.

Вот результат работы cyberChef:

производный ключ cyberChef PBKDF2

Вот код Java и результат его запуска:

package crypto;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Base64;

public class EncryptDecryptAesGcmPassword {

    private static final String ENCRYPT_ALGO = "AES/GCM/NoPadding";

    private static final int TAG_LENGTH_BIT = 128; // must be one of {128, 120,     112, 104, 96}
    private static final int IV_LENGTH_BYTE = 12;
    private static final int SALT_LENGTH_BYTE = 16;
    public static final int ITERATION_COUNT = 1000;
    public static final int KEY_LENGTH = 256;

    private static final Charset UTF_8 = StandardCharsets.UTF_8;

    // return a base64 encoded AES encrypted text
    public static String encrypt(byte[] salt, byte[] pText, String password) throws Exception {
        // GCM recommended 12 bytes iv?
        byte[] iv = getRandomNonce(IV_LENGTH_BYTE);

        // secret key from password
        SecretKey aesKeyFromPassword = getAESKeyFromPassword(password.toCharArray(), salt);

        Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO);

        // ASE-GCM needs GCMParameterSpec
        cipher.init(Cipher.ENCRYPT_MODE, aesKeyFromPassword, new GCMParameterSpec(TAG_LENGTH_BIT, iv));

        byte[] cipherText = cipher.doFinal(pText);

        // prefix IV and Salt to cipher text
        byte[] cipherTextWithIvSalt = ByteBuffer.allocate(iv.length + salt.length + cipherText.length)
            .put(iv)
            .put(salt)
            .put(cipherText)
            .array();

        // string representation, base64, send this string to other for decryption.
        return Base64.getEncoder().encodeToString(cipherTextWithIvSalt);

    }

    // we need the same password, salt and iv to decrypt it
    private static String decrypt(String cText, String password) throws Exception {
        byte[] decode = Base64.getDecoder().decode(cText.getBytes(UTF_8));

        // get back the iv and salt from the cipher text
        ByteBuffer bb = ByteBuffer.wrap(decode);

        byte[] iv = new byte[IV_LENGTH_BYTE];
        bb.get(iv);

        byte[] salt = new byte[SALT_LENGTH_BYTE];
        bb.get(salt);

        byte[] cipherText = new byte[bb.remaining()];
        bb.get(cipherText);

        // get back the aes key from the same password and salt
        SecretKey aesKeyFromPassword = getAESKeyFromPassword(password.toCharArray(), salt);

        Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO);

        cipher.init(Cipher.DECRYPT_MODE, aesKeyFromPassword, new GCMParameterSpec(TAG_LENGTH_BIT, iv));

        byte[] plainText = cipher.doFinal(cipherText);

        return new String(plainText, UTF_8);

    }


    public static byte hexToByte(String hexString) {
        int firstDigit = toDigit(hexString.charAt(0));
        int secondDigit = toDigit(hexString.charAt(1));
        return (byte) ((firstDigit << 4) + secondDigit);
    }

    public static byte[] decodeHexString(String hexString) {
        if (hexString.length() % 2 == 1) {
            throw new IllegalArgumentException(
                "Invalid hexadecimal String supplied.");
        }

        byte[] bytes = new byte[hexString.length() / 2];
        for (int i = 0; i < hexString.length(); i += 2) {
            bytes[i / 2] = hexToByte(hexString.substring(i, i + 2));
        }  
        return bytes;
    }

    private static int toDigit(char hexChar) {
        int digit = Character.digit(hexChar, 16);
        if (digit == -1) {
            throw new IllegalArgumentException(
                "Invalid Hexadecimal Character: "+ hexChar);
        }
        return digit;
    }

    // Random byte[] with length numBytes
    public static byte[] getRandomNonce(int numBytes) {
        byte[] nonce = new byte[numBytes];
        new SecureRandom().nextBytes(nonce);
        return nonce;
    }

    // Password derived AES 256 bits secret key
    public static SecretKey getAESKeyFromPassword(char[] password, byte[] salt)
        throws NoSuchAlgorithmException, InvalidKeySpecException {

        SecretKeyFactory factory =    SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        // iterationCount = 1000
        // keyLength = 256
        KeySpec spec = new PBEKeySpec(password, salt, ITERATION_COUNT,
            KEY_LENGTH);
        SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");

        String encodedKey = hex(secret.getEncoded());

        // print SecretKey as hex
        System.out.println("SecretKey: " + encodedKey);

        return secret;

    }

    // hex representation
    public static String hex(byte[] bytes) {
        StringBuilder result = new StringBuilder();
        for (byte b : bytes) {
            result.append(String.format("%02x", b));
        }
        return result.toString();
    }



    public static void main(String[] args) throws Exception {
        String OUTPUT_FORMAT = "%-30s:%s";
        String PASSWORD = "plnlrtfpijpuhqylxbgqiiyipieyxvfsavzgxbbcfusqkozwpngsyejqlmjsytrmd";

        // plain text
        String pText = "AES-GSM Password-Bases encryption!";

        // convert hex string to byte[]
        byte[] salt = decodeHexString("A009C1A485912C6AE630D3E744240B04");


        String encryptedTextBase64 = EncryptDecryptAesGcmPassword.encrypt(salt, pText.getBytes(UTF_8), PASSWORD);

        System.out.println("\n------ AES GCM Password-based Encryption ------");
        System.out.println(String.format(OUTPUT_FORMAT, "Input (plain text)", pText));
        System.out.println(String.format(OUTPUT_FORMAT, "Encrypted (base64) ", encryptedTextBase64));

        System.out.println("\n------ AES GCM Password-based Decryption ------");
        System.out.println(String.format(OUTPUT_FORMAT, "Input (base64)", encryptedTextBase64));

        String decryptedText = EncryptDecryptAesGcmPassword.decrypt(encryptedTextBase64, PASSWORD);
        System.out.println(String.format(OUTPUT_FORMAT, "Decrypted (plain text)", decryptedText));
    }
}

Запуск этого кода приводит к следующему:

SecretKey: 28869b5f31ae29236f164c5cb33e2e3bb46f483867a15f8e7208e1836070f64a

------ AES GCM Password-based Encryption ------
Input (plain text)            :AES-GSM Password-Bases encryption!
Encrypted (base64)            :/PuTLBTKVWgJB2iMoAnBpIWRLGrmMNPnRCQLBABOkwNeY8BrrdtoRNVFqZ+xmUjvF2PET6Ne2+PAp34QLCUFjQodTMdmzaNAfzcLWOf4

------ AES GCM Password-based Decryption ------
Input (base64)               :/PuTLBTKVWgJB2iMoAnBpIWRLGrmMNPnRCQLBABOkwNeY8BrrdtoRNVFqZ+xmUjvF2PET6Ne2+PAp34QLCUFjQodTMdmzaNAfzcLWOf4
SecretKey: 28869b5f31ae29236f164c5cb33e2e3bb46f483867a15f8e7208e1836070f64a
Decrypted (plain text)        :AES-GSM Password-Bases encryption!

Спасибо

Майлз.

person chocksaway    schedule 11.10.2020
comment
Я знаю, что вы закодировали свой прекрасный пример на равенство с CyberChef, но для большей безопасности вам следует увеличить количество итераций до (минимум) 10.000, а лучше больше. Во-вторых (по той же причине) соль должна быть случайной, чтобы вы не получили один и тот же ключ с одинаковым паролем. - person Michael Fehr; 11.10.2020
comment
Спасибо, что уделили время, чтобы отметить эти моменты. Очень признателен :) - person chocksaway; 12.10.2020