CryptoAPI C ++ взаимодействует с Java с использованием AES - PullRequest
1 голос
/ 06 января 2012

Я пытаюсь зашифровать в C ++ с помощью CryptoAPI и расшифровать Java с помощью SunJCE. Я получил ключ RSA для работы - и проверен на тестовой строке. Однако мой ключ AES не работает - я получаю javax.crypto.BadPaddingException: Given final block not properly padded.

C ++ Шифрование:

// init and gen key
HCRYPTPROV provider;
CryptAcquireContext(&provider, NULL, MS_ENH_RSA_AES_PROV, PROV_RSA_AES, CRYPT_VERIFYCONTEXT);

// Use symmetric key encryption
HCRYPTKEY sessionKey;
DWORD exportKeyLen;
CryptGenKey(provider, CALG_AES_128, CRYPT_EXPORTABLE, &sessionKey);

// Export key
BYTE exportKey[1024];
CryptExportKey(sessionKey, NULL, PLAINTEXTKEYBLOB, 0, exportKey, &exportKeyLen);

// skip PLAINTEXTKEYBLOB header
//      { uint8_t bType, uint8_t version, uint16_t reserved, uint32_t aiKey, uint32_t keySize }
DWORD keySize =  *((DWORD*)(exportKey + 8));
BYTE * rawKey = exportKey + 12;

// reverse bytes for java
for (unsigned i=0; i<keySize/2; i++) {
    BYTE temp = rawKey[i];
    rawKey[i] = rawKey[keySize-i-1];
    rawKey[keySize-i-1] = temp;
}

// Encrypt message
BYTE encryptedMessage[1024];
const char * message = "Decryption Works";
BYTE messageLen = (BYTE)strlen(message);
memcpy(encryptedMessage, message, messageLen);
DWORD encryptedMessageLen = messageLen;
CryptEncrypt(sessionKey, NULL, TRUE, 0, encryptedMessage, &encryptedMessageLen, sizeof(encryptedMessage));

// reverse bytes for java
for (unsigned i=0; i<encryptedMessageLen/2; i++) {
    BYTE temp = encryptedMessage[i];
    encryptedMessage[i] = encryptedMessage[encryptedMessageLen - i - 1];
    encryptedMessage[encryptedMessageLen - i - 1] = temp;
}

BYTE byteEncryptedMessageLen = (BYTE)encryptedMessageLen;
FILE * f = fopen("test.aes", "wb");
fwrite(rawKey, 1, keySize, f);
fwrite(&byteEncryptedMessageLen, 1, sizeof(byteEncryptedMessageLen), f);
fwrite(encryptedMessage, 1, encryptedMessageLen, f);
fclose(f);

// destroy session key
CryptDestroyKey(sessionKey);
CryptReleaseContext(provider, 0);

Расшифровка Java:

try
{
    FileInputStream in = new FileInputStream("test.aes");
    DataInputStream dataIn = new DataInputStream(in);

    // stream key and message
    byte[] rawKey = new byte[16];
    dataIn.read(rawKey);
    byte encryptedMessageLen = dataIn.readByte();
    byte[] encryptedMessage = new byte[encryptedMessageLen];
    dataIn.read(encryptedMessage);

    // use CBC/PKCS5PADDING, with 0 IV -- default for Microsoft Base Cryptographic Provider
    SecretKeySpec sessionKey = new SecretKeySpec(rawKey, "AES");
    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
    cipher.init(Cipher.DECRYPT_MODE, sessionKey, new IvParameterSpec(new byte[16]));

    cipher.doFinal(encryptedMessage);
}
catch (Exception e) {
  e.printStackTrace();
}

В похожем примере я пробовал перестановки не обращать байты ключа и не обращать байты в сообщении. Если я зашифровал и расшифровал с помощью импортированного ключа в Java, я получу действительные результаты. Я также могу зашифровать и расшифровать исключительно на C ++.

Вопросы:

  1. Должен ли я использовать CBC / PKCS5PADDING? Это значение по умолчанию для MS_ENH_RSA_AES_PROV?
  2. Является ли нулевой IV действительно значением по умолчанию для MS_ENH_RSA_AES_PROV?
  3. Есть ли способы диагностировать особенности поведения ключа?
  4. Я бы хотел придерживаться стандартных пакетов Java вместо установки BouncyCastle, но есть ли какие-то отличия, которые позволили бы улучшить работу сторонних пакетов?

Ответы [ 3 ]

2 голосов
/ 09 января 2012

Мне пришлось сделать несколько вещей, чтобы правильно получить сообщение:

  1. Явно установить KP_MODE на CRYPT_MODE_CBC и KP_IV на 0
  2. Использовать NoPadding в Java для расшифровки
  3. Не инвертировать байты для ключа или сообщения

С точки зрения диагностики проблемы самый полезный советдолжен был установить NoPadding в Java, который предотвращает BadPaddingException.Это позволило мне увидеть результаты - даже если они ошибочны.

Как ни странно, для решения взаимодействия RSA Java / CryptoAPI требуется, чтобы сообщение было полностью перебрано, чтобы работать с Java, но AES не ожидает ключили сообщение, которое должно быть обращено к байту.

CryptSetKeyParam не позволил бы мне использовать ZERO_PADDING, но при просмотре расшифрованных байтов становится ясно, что CryptoAPI заполняется количеством неиспользуемых байтов.Например, при размере блока 16, если последний блок использует только 9 байтов, то оставшиеся 5 байтов получают значение 0x05.Представляет ли это потенциальную утечку безопасности?Должен ли я заполнять все другие байты случайными байтами и использовать только последний байт, чтобы указать, сколько используется заполнение?

Рабочий код (с использованием соглашения CryptoAPI о том, что последний байт является счетчиком заполнения) приведен ниже (проверка возврата)значения из Crypt были удалены для простоты):

// init and gen key
HCRYPTPROV provider;
CryptAcquireContext(&provider, NULL, MS_ENH_RSA_AES_PROV, PROV_RSA_AES, CRYPT_VERIFYCONTEXT);

// Use symmetric key encryption
HCRYPTKEY sessionKey;
DWORD exportKeyLen;
BYTE iv[32];
memset(iv, 0, sizeof(iv));
DWORD padding = PKCS5_PADDING;
DWORD mode = CRYPT_MODE_CBC;
CryptGenKey(provider, CALG_AES_128, CRYPT_EXPORTABLE, &sessionKey);
CryptSetKeyParam(sessionKey, KP_IV, iv, 0);
CryptSetKeyParam(sessionKey, KP_PADDING, (BYTE*)&padding, 0);
CryptSetKeyParam(sessionKey, KP_MODE, (BYTE*)&mode, 0);

// Export key
BYTE exportKey[1024];
CryptExportKey(sessionKey, NULL, PLAINTEXTKEYBLOB, 0, exportKey, &exportKeyLen);

// skip PLAINTEXTKEYBLOB header
//      { uint8_t bType, uint8_t version, uint16_t reserved, uint32_t aiKey, uint32_t keySize }
DWORD keySize =  *((DWORD*)(exportKey + 8));
BYTE * rawKey = exportKey + 12;

// Encrypt message
BYTE encryptedMessage[1024];
const char * message = "Decryption Works -- using multiple blocks";
BYTE messageLen = (BYTE)strlen(message);
memcpy(encryptedMessage, message, messageLen);
DWORD encryptedMessageLen = messageLen;
CryptEncrypt(sessionKey, NULL, TRUE, 0, encryptedMessage, &encryptedMessageLen, sizeof(encryptedMessage));

BYTE byteEncryptedMessageLen = (BYTE)encryptedMessageLen;
FILE * f = fopen("test.aes", "wb");
fwrite(rawKey, 1, keySize, f);
fwrite(&byteEncryptedMessageLen, 1, sizeof(byteEncryptedMessageLen), f);
fwrite(encryptedMessage, 1, encryptedMessageLen, f);
fclose(f);

// destroy session key
CryptDestroyKey(sessionKey);
CryptReleaseContext(provider, 0);

Расшифровка Java:

try
{
    FileInputStream in = new FileInputStream("test.aes");
    DataInputStream dataIn = new DataInputStream(in);

    // stream key and message
    byte[] rawKey = new byte[16];
    dataIn.read(rawKey);
    byte encryptedMessageLen = dataIn.readByte();
    byte[] encryptedMessage = new byte[encryptedMessageLen];
    dataIn.read(encryptedMessage);

    // use CBC/NoPadding, with 0 IV -- (each message is creating it's own session key, so zero IV is ok)
    SecretKeySpec sessionKey = new SecretKeySpec(rawKey, "AES");
    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
    cipher.init(Cipher.DECRYPT_MODE, sessionKey, new IvParameterSpec(new byte[16]));

    byte[] decryptedBlocks = cipher.doFinal(encryptedMessage);

    // check versus expected message
    byte[] expectedBytes = "Decryption Works -- using multiple blocks".getBytes();
    Assert.assertTrue("Incorrect Message" + new String(message), Arrays.equals(message, expectedBytes));
}
catch (Exception e) {
  e.printStackTrace();
}
1 голос
/ 07 января 2012

Вы выполняете слишком много вращений для клавиши AES под Windows.Установите его на известное значение с помощью CryptImportKey - см., Например, WinAES: класс CES AES .

Вы должны установить режим CBC в Windows, используя CryptSetKeyParam, KP_MODECRYPT_MODE_CBC.В противном случае вы используете режим ECB (если я правильно помню). Снова см. WinAES: Класс C ++ AES .

По умолчанию для симметричных шифров используется заполнение PKCS5.Я даже не помню, как это изменить (если это возможно).Я подозреваю, что у вас есть только другой выбор - «без заполнения».

По умолчанию Microsoft использует строку 0 для IV.Вам нужно будет установить IV через CryptSetKeyParam и KP_IV.

0 голосов
/ 07 января 2012

Q1 & Q2: просто не полагайтесь на значения по умолчанию. Для удобства обслуживания вы можете выбрать три варианта: позволить всем узнать, какие значения по умолчанию (я думаю, не самый лучший вариант), использовать комментарии или просто установить все возможные параметры. Лично я бы всегда выбрал третий вариант - другие варианты слишком хрупкие.

Q3 Нет, если биты ключа неверны или находятся в неправильном порядке (см. Ниже), вы получите либо исключение плохого заполнения , либо вывод мусора. Что вы можете сделать, это использовать «/ NoPadding» в Java во время дешифрования (или аналогичного в C ++). Таким образом, вы можете увидеть, есть ли у вас проблема с заполнением, посмотрев на результат. Если у вас есть простой текст, то, скорее всего, у вас проблема с отступом. Если только первый блок неправильный, у вас возникли проблемы с IV.

Q4 Нет, не совсем. Java JCE работает довольно хорошо, если вы хотите остаться в Java. Надувной замок (кстати) обладает большей функциональностью и может иметь разные характеристики производительности. Вы можете использовать других провайдеров для использования разных хранилищ ключей (например, в зависимости от ОС или смарт-карты), для реализации с улучшенной производительностью (собственной) и т. Д.

Возможно, , что вам понадобится обратная сторона ключа, потому что Java использует big endian, а C ++ может использовать little endian. Хотя я не могу предположить, что C ++ полностью изменил бы байты ввода / вывода. Обычно ни один из них не представляет число, поэтому порядок должен быть одинаковым для обеих платформ.

Удалить сторнирование байтов, указать все параметры и сообщить?

...