C Логика шифрования не совпадает с java

Я ищу эквивалентный код C для кода Java ниже. Я пытаюсь написать два приложения, одно на java, а другое - на C. Java-приложение шифрует / дешифрует строку с помощью ниже логики, и оно работает при использовании ниже java-метода.

public class AEScrypt {

    public static String encryptString(String strToEncrypt, String secret, String salt, byte[] iv) {
        try {

            IvParameterSpec ivspec = new IvParameterSpec(iv);

            SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
            KeySpec keySpec = new PBEKeySpec(secret.toCharArray(), salt.getBytes(), 65536, 256);
            SecretKey secretKeySpec = keyFactory.generateSecret(keySpec);
            SecretKeySpec secretKey = new SecretKeySpec(secretKeySpec.getEncoded(), "AES");

            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivspec);
            int length = 0;
            if (strToEncrypt.length() <= 16) {
                length = 16;
            } else if (strToEncrypt.length() > 16 && strToEncrypt.length() <= 32) {
                length = 16;
            }
            strToEncrypt = fixedLengthString(strToEncrypt, length);
            return Base64.getEncoder().encodeToString(cipher.doFinal(strToEncrypt.getBytes(StandardCharsets.UTF_8)));
        } catch (Exception exception) {
            System.out.println("Error while encrypting value : "+exception.getMessage());
        }
        return null;
    }

    private static String fixedLengthString(String string, int length) {
        return String.format("%" + length + "s", string);
    }

    public static String decryptString(String strToDecrypt, String secret, String salt, byte[] iv) {
        try {
            IvParameterSpec ivspec = new IvParameterSpec(iv);

            SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
            KeySpec keySpec = new PBEKeySpec(secret.toCharArray(), salt.getBytes(), 65536, 256);
            SecretKey secretKeySpec = keyFactory.generateSecret(keySpec);
            SecretKeySpec secretKey = new SecretKeySpec(secretKeySpec.getEncoded(), "AES");

            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            cipher.init(Cipher.DECRYPT_MODE, secretKey, ivspec);
            return new String(cipher.doFinal(Base64.getDecoder().decode(strToDecrypt))).trim();
        } catch (Exception e) {
            e.getMessage();
        }
        return null;
    }
}

Что я понимаю из приведенного выше кода JAVA:

ДЛЯ ШИФРОВАНИЯ:

  1. Мы используем HMAC-sha256 для генерации ключа, который принимает соль, пароль.
  2. Заполнение входных данных.
  3. Мы используем AES-CBC-256 для шифрования вводимых данных, используя сгенерированный выше ключ и iv.
  4. Мы кодируем данные шифрования с помощью base64.

ДЛЯ РАСШИФРОВКИ:

  1. Мы используем HMAC-sha256 для генерации ключа, который принимает соль, пароль.
  2. Мы декодируем с помощью base64 и получаем зашифрованные данные.
  3. Мы используем AES-CBC-256 для дешифрования зашифрованных данных, используя сгенерированный выше ключ и iv.
  4. Обрежьте расшифрованные данные.

Для репликации того же самого в C я использовал метод шифрования / дешифрования из ссылки ниже; Симметричное шифрование и дешифрование EVP

Для генерации ключа я использовал PKCS5_PBKDF2_HMAC и EVP_MD как EVP_sha256 ().

int PKCS5_PBKDF2_HMAC(const char *pass, int passlen,
                       const unsigned char *salt, int saltlen, int iter,
                       const EVP_MD *digest,
                       int keylen, unsigned char *out);

Для кодирования / декодирования base64: кодирование / декодирование base64

Также я позаботился о логике заполнения и обрезки. Но я получаю разные зашифрованные данные из кода Java и C. Я что-то здесь упускаю?

Если у вас есть пример функции на C, это будет очень полезно.


person Aby    schedule 08.12.2020    source источник
comment
Во-первых, изолируйте проблему до одного шага. Например, протестируйте кодировку base64 как на Java, так и на C. Если результаты различаются, отладьте кодировку base64. Если результат будет таким же, проверьте шифрование AES. Если вы изолировали проблему до одного шага, если вы не можете решить ее, создайте минимальный воспроизводимый пример и опубликуйте вопрос, содержащий его, включая полный код Java и C для воспроизведения проблемы и образец ввода.   -  person Eric Postpischil    schedule 08.12.2020
comment
Криптография рассчитана на провал даже при малейшем несоответствии. Например, строки C имеют завершающий байт 0, а строки Java - нет. Точно так же вам может потребоваться явно указать кодировки символов, настройки EoL и т. Д. Как для C, так и для Java. Различные настройки по умолчанию для разных систем являются источником ошибок.   -  person rossum    schedule 08.12.2020


Ответы (1)


ДЛЯ ШИФРОВАНИЯ:

  1. Мы используем HMAC-sha256 для генерации ключа, который принимает соль, пароль.

Нет, вы используете PBKDF2 С HMAC-SHA256. Это совсем не то же самое, что простой HMAC-SHA256. Однако функция OpenSSL, которую вы определили, ДЕЙСТВИТЕЛЬНО соответствует этому, если вы зададите ей правильные параметры. Это также относится к шагу расшифровки 1.

  1. Заполнение входных данных.

Вроде, как бы, что-то вроде. Это заполнение правильно работает только для входных данных длиной до 16 символов, все из которых являются ASCII (потому что вы кодируете его как UTF-8, а любой символ, отличный от ASCII, создает более одного байта, что делает закодированное значение недопустимой длиной) . Большинство длинных значений потерпят неудачу, хотя некоторым удастся добиться успеха по невезению. И даже для «успешных» значений некоторые из них будут изменены; это считается плохой практикой, и практически все грамотно разработанные криптографические схемы примерно с 1980 года предназначены для сохранения всех данных. В частности, очень распространенное стандартное заполнение PKCS5 (иногда называемое PKCS7 или PKCS5 / PKCS7 по техническим причинам) сохраняет все данные правильно и уже реализовано как в Java, так и в OpenSSL, а также почти во всех других достойных криптографических библиотеках и устройствах, и было бы лучший выбор, а также проще.

При фиксированном заполнении сторона Java могла обрабатывать данные, отличные от ASCII, но только если вы оба соответствующим образом кодируете открытый текст, который должен быть зашифрован , и декодируете открытый текст после дешифрования. У вас есть .getBytes(StandardCharsets.UTF_8) при шифровании, но вам нужно сопоставить его с new String(cipher.doFinal(...), StandardCharsets.UTF_8) при расшифровке, иначе он может работать или не работать в зависимости от платформы и среды, которые вы используете для его запуска.

Сторона C может быть сложнее. OpenSSL основан на коде старой школы C, начатом до того, как версии C 1995 и 1999 годов начали обрабатывать неанглийские символы, и он понимает только байты, которые могут быть однобайтовыми или «узкими» символами. Либо вы должны обернуть его вызывающим кодом, который обрабатывает `` широкие '' символы в многобайтовой кодировке, такой как UTF-8 (и вызывает части OpenSSL с использованием байтов), либо вы должны делать это вне программы, контролируя среду (например, как терминал или эмулятор) или файлы. В вашем вопросе нет даже намека ни на что из этого, поэтому дать какие-либо рекомендации невозможно.

Поскольку вы обрабатываете «секрет» (пароль), соль и IV как String, к ним применяются те же соображения, за исключением того, что они, вероятно, поступают из другого источника (-ов), чем данные. IV и соль разработаны как байтовые последовательности, и ограничение IV, в частности, кодировками ASCII или даже UTF-8, вероятно, несколько снижает безопасность, но поскольку тема SO - это программирование, а не безопасность, я не буду преследовать это. На самом деле PBKDF2 в пароле PKCS5 также является октетами (байтами Java), но он «рекомендует» кодировать текст (символы) как ASCII или UTF-8, а Java принимает char[] в PBEKeySpec и кодирует как UTF-8, поэтому для не -ASCII вызывающий OpenSSL или окружение должны соответствовать этому.

С учетом этих ограничений: все значения - только ASCII, данные не более 16 символов = байтов, а IV - ровно 16, следующий код C соответствует и может взаимодействовать с вашей Java. Обработка ошибок минимальна, и я выполняю как шифрование, так и дешифрование в одной функции; вы хотели бы иметь возможность разделить их. (исправленный)

/* SO65195128.c 20dec09,11 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/evp.h>
#include <openssl/err.h>
#include <openssl/ssl.h>

void err (int n, const char * s){ printf("%s:%d\n", s, n); ERR_print_errors_fp(stdout); exit(1); }

int main (int argc, char **argv){
    if( argc != 5 || strlen(argv[3]) != 16 || strlen(argv[4]) > 16 ){ printf("bad args\n"); exit(1); }
    const char * pw = argv[1], * salt = argv[2], * iv = argv[3], * org = argv[4];
    unsigned char key [32], pad [16], enc [16], b64 [25], unb [16], dec [16];
    int rc, len, temp, i, j;
    SSL_library_init();

    /* for both */
    rc = PKCS5_PBKDF2_HMAC (pw, strlen(pw), (unsigned char*)salt, strlen(salt), 65536, EVP_sha256(), 32, key);
    if( rc != 1 ) err(rc,"PBKDF2");
    EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();

    /* for encrypt */
    len = strlen(org); memset(pad, ' ', 16-len); memcpy (pad+16-len, org, len);
    rc = EVP_EncryptInit (ctx, EVP_aes_256_cbc(), key, (unsigned char*)iv);
    if( rc != 1 ) err(rc,"EncryptInit");
    rc = EVP_CIPHER_CTX_set_padding(ctx,0);
    if( rc != 1 ) err(rc,"set_padding");
    rc = EVP_EncryptUpdate (ctx, enc, &len, pad, 16);
    if( rc != 1 || len != 16 ) err(rc,"EncryptUpdate");
    rc = EVP_EncryptFinal (ctx, enc+len, &temp); 
    if( rc != 1 || temp != 0 ) err(rc,"EncryptFinal");
    rc = EVP_EncodeBlock(b64, enc, 16);
    if( rc <= 0 ) err(rc,"EncodeBlock");

    printf ("%.*s\n", rc, b64);

    /* for decrypt */
    rc = EVP_DecodeBlock(unb, b64, /*len*/rc)-(b64[rc-1]=='=')-(b64[rc-2]=='=');
    /* this is a hack, should go for DecodeInit,Update,Final */
    if( rc != 16 ) err(rc,"DecodeBlock");
    rc = EVP_DecryptInit (ctx, EVP_aes_256_cbc(), key, (unsigned char*)iv);
    if( rc != 1 ) err(rc,"DecryptInit");
    rc = EVP_CIPHER_CTX_set_padding(ctx,0);
    if( rc != 1 ) err(rc,"set_padding");
    rc = EVP_DecryptUpdate (ctx, dec, &len, unb, 16);
    if( rc != 1 || len != 16 ) err(rc,"DecryptUpdate");
    rc = EVP_DecryptFinal (ctx, dec+len, &temp); 
    if( rc != 1 || temp != 0 ) err(rc,"DecryptFinal");
    i=0; while(i<16&&dec[i]<=' ') i++; j=16; while(j>0&&dec[j-1]<=' ') j--;

    printf ("%.*s\n", j-i, dec+i);
    /* note this is NOT a C string -- needs to be copied and NUL added for that */

    return 0;
}

(добавлено) с паролем / секретом SEKRIT salt NOSAILOR (шутка) и IV STARTINGSTARTING
для данных SOMEDATA Я получаю MOOn6FicaVcnVLokSANQsw==
для данных, НО МНОГО МНОГО Я получаю 5NsJUO4z1Bbap0U85ZClMg==
, оба из которых соответствуют тому, что я получаю от вашего Java, и расшифровать правильно. Посмотри, что получишь.

person dave_thompson_085    schedule 08.12.2020
comment
Спасибо, что поделились подробной информацией и кодом C, поэтому разница, которую я вижу в вашем коде C и моем, составляет всего 2 места. 1) rc = EVP_EncryptFinal (ctx, enc+len, &temp); я использовал это 2) мне не хватало EVP_CIPHER_CTX_set_padding(ctx,0); из-за того, что мой зашифрованный текст был 32 байта, а не 16 байтов. Но все же я вижу, что код Java и код C генерируют другой 16-байтовый текст шифрования, и я использую тот же пароль на основе символов ASCII, соль и IV. есть ли что-нибудь, что я могу сделать на стороне Java, чтобы соответствовать этому. - person Aby; 10.12.2020
comment
(1) Да, EncryptFinal «должен» использовать enc, хотя это не имеет значения, потому что с заполнением в приложении только полные блоки в EVP, EncryptFinal, а также DecryptFinal никогда не сохраняются - см. Мои temp!=0 тесты ошибок. Я также обнаружил, что не копировал последнюю версию с обходным путем для DecodeBlock. Отредактировал оба. (2) (с этими изменениями) мне подходит; Я добавил несколько тестовых примеров. - person dave_thompson_085; 12.12.2020