Почему я получаю разные результаты шифрования между Node.js и PHP при чтении больших файлов по частям?

Несогласованный вывод шифра между Node.js и PHP при чтении больших файлов по частям

описание проблемы

Я столкнулся с проблемой обеспечения согласованного вывода шифрования между Node.js и PHP при чтении больших файлов по частям. Вывод отличается, хотя считываются и обрабатываются одни и те же значения. Ниже приведен код для реализаций PHP и Node.js.

PHP-код

<?php
require 'vendor/autoload.php';

define('PLAINTEXT_DATA_KEY', 'poSENHhkGVG/4fEHvhRO6j9W3goETWZAg+ZgTWxhw34=');
define('IV', "X1bIRjgIoDn/BDFhHIbg7g= = ");
define('ALGORITHM', 'aes-256-cbc');
define('CHUNK_SIZE', 16 * 1024);

class Cipher
{
    private function pkcs7_pad(string $data, int $blockSize)
    {
        $padLength = $blockSize - (strlen($data) % $blockSize);
        return $data . str_repeat(chr($padLength), $padLength);
    }
    public function encrypt($source, $destination)
    {
        $inputFile = fopen($source, 'rb');
        $outputFile = fopen($destination, 'wb');
        try {
            fwrite($outputFile, base64_decode(IV));

            while (!feof($inputFile)) {
                $buffer = fread($inputFile, CHUNK_SIZE);
                // Pad the last chunk if it is not the block size
                if (feof($inputFile)) {
                    $buffer = $this->pkcs7_pad($buffer, 16);
                }
                $cipherText = openssl_encrypt($buffer, ALGORITHM, PLAINTEXT_DATA_KEY, OPENSSL_NO_PADDING, base64_decode(IV));
                fwrite($outputFile, $cipherText);
            }
        } catch (Exception $e) {
            throw $e;
        } finally {
            fclose($inputFile);
            fclose($outputFile);
        }
    }
}
?>

const PADDING_BLOCK_SIZE = 16;
const ALGORITHM = "aes-256-cbc";
const PLAINTEXT_DATA_KEY = "poSENHhkGVG/4fEHvhRO6j9W3goETWZAg+ZgTWxhw34 = ";
const IV = "X1bIRjgIoDn/BDFhHIbg7g= = "; // randombytes(16) converted to base64
const CHUNK_SIZE = 16 * 1024;

class Cipher {
    private pkcs7Pad(buffer: Buffer, blockSize: number = PADDING_BLOCK_SIZE): Buffer {
      const padding = blockSize - (buffer.length % blockSize);
      const padBuffer = Buffer.alloc(padding, padding);
      return Buffer.concat([buffer, padBuffer]);
    }
  
    async encrypt(source: string, dest: string) {
      return new Promise(async (res, rej) => {
        const iv = base64ToBuffer(IV);
  
        const cipher = createCipheriv(ALGORITHM, base64ToUint8Array(PLAINTEXT_DATA_KEY), iv);
        cipher.setAutoPadding(false);
  
        const readStream = createReadStream(source, { highWaterMark: CHUNK_SIZE });
        const writeStream = createWriteStream(dest, { highWaterMark: CHUNK_SIZE });
  
        writeStream.write(iv);
  
        let tempChunkStorage = Buffer.alloc(0); // Buffer to store remaining data
        readStream.on(DATA_EVENT, (chunk) => {
          if (typeof chunk === "string") {
            chunk = Buffer.from(chunk);
          }
  
          // Append the new chunk to the temp storage
          tempChunkStorage = Buffer.concat([tempChunkStorage, chunk]);
  
          while (tempChunkStorage.length >= CHUNK_SIZE) {
            const block = tempChunkStorage.subarray(0, CHUNK_SIZE);
            const encryptedBuffer = cipher.update(block);
            writeStream.write(encryptedBuffer);
            tempChunkStorage = tempChunkStorage.subarray(CHUNK_SIZE);
          }
        });
        readStream.on("end", () => {
          if (tempChunkStorage.length > 0) {
            const encryptedBuffer = cipher.update(this.pkcs7Pad(tempChunkStorage)); // Add padding
            writeStream.write(encryptedBuffer);
            cipher.final();
          }
          writeStream.end();
          res(true);
        });
        readStream.on("error", (err) => {
          writeStream.close();
          rej(err);
        });
      });
    }
  }
First 50 characters of the cipher (base64) in PHP:  0tCb9xtx5KpG+56ukYvcQDoNKCdoPtAFUrFDRc4TiqQrQocQRK

First 50 characters of the cipher (base64) in Node: sUUI4nXHwhKNdRs+Brqc5neKuKb3fx4qqBohlDSn/7FVrYo46/

Я бы рекомендовал сократить коды. Поскольку вас интересуют различные зашифрованные тексты, вы можете исключить всю часть MD5, а также множество журналов. Это только отвлекает от сути проблемы. Публикуйте только минимальный код, необходимый для воспроизведения, см. MCVE.

Topaco 21.06.2024 11:53

Я сократил код. Кроме того, полный код шифрования и дешифрования можно найти здесь. Ссылка: drive.google.com/drive/folders/…

Bikash 22.06.2024 22:49
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Symfony Station Communiqué - 7 июля 2023 г
Symfony Station Communiqué - 7 июля 2023 г
Это коммюнике первоначально появилось на Symfony Station .
Оживление вашего приложения Laravel: Понимание режима обслуживания
Оживление вашего приложения Laravel: Понимание режима обслуживания
Здравствуйте, разработчики! В сегодняшней статье мы рассмотрим важный аспект управления приложениями, который часто упускается из виду в суете...
Установка и настройка Nginx и PHP на Ubuntu-сервере
Установка и настройка Nginx и PHP на Ubuntu-сервере
В этот раз я сделаю руководство по установке и настройке nginx и php на Ubuntu OS.
Коллекции в Laravel более простым способом
Коллекции в Laravel более простым способом
Привет, читатели, сегодня мы узнаем о коллекциях. В Laravel коллекции - это способ манипулировать массивами и играть с массивами данных. Благодаря...
Как установить PHP на Mac
Как установить PHP на Mac
PHP - это популярный язык программирования, который используется для разработки веб-приложений. Если вы используете Mac и хотите разрабатывать...
0
2
112
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В обоих кодексах есть несколько проблем.

  1. Декодирование ключа Base64 отсутствует в коде PHP.

  2. Вам также следует изменить шифрование в коде PHP, чтобы создавался тот же зашифрованный текст, что и при шифровании, которое шифрует весь открытый текст одновременно. Благодаря этому изменению можно использовать любой размер фрагмента (при условии, что он кратен размеру блока).

    Это не только приводит к разделению шифрования и дешифрования, но и упрощает реализацию NodeJS, как будет объяснено позже.

    Для этого необходимо внести следующие изменения в заполнение CBC/PKCS#7:

    • Заполнение PKCS#7 по умолчанию должно быть отключено для всех фрагментов, кроме последнего.
    • Последний блок зашифрованного текста n-го фрагмента зашифрованного текста должен использоваться как IV n+1-го фрагмента зашифрованного текста.
  3. Кроме того, следует устранить несоответствия и неэффективность (что также облегчает реализацию вышеуказанных изменений):

В целом описанные изменения в PHP-коде можно реализовать следующим образом:

...
$key = base64_decode(PLAINTEXT_DATA_KEY); // Base64 decode key
$iv = base64_decode(IV);

$inputFile = fopen($source, 'rb');
$outputFile = fopen($dest, 'wb');

fwrite($outputFile, $iv); // write initial IV
$options = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING; // disable Base64 encoding and padding
while (!feof($inputFile)) {
    $buffer = fread($inputFile, CHUNK_SIZE); // CHUNK_SIZE must be an integer multiple of blocksize (16 bytes for AES)
    if (feof($inputFile)) {
        $options = OPENSSL_RAW_DATA; // enable padding for the last chunk
    }
    $cipherText = openssl_encrypt($buffer, ALGORITHM, $key, $options, $iv); 
    $iv = substr($cipherText, -16); // determine IV for the next chunk
    fwrite($outputFile, $cipherText); // write ciphertext chunk
}

fclose($inputFile);
fclose($outputFile);
...

Как уже упоминалось выше, одним из преимуществ внесенных изменений является независимость зашифрованного текста от размера фрагмента.
Это позволяет обрабатывать размер фрагмента внутри NodeJS, что значительно сокращает код шифрования:

...
var key = Buffer.from(PLAINTEXT_DATA_KEY, 'base64');
var iv = Buffer.from(IV, 'base64');

var readStream = fs.createReadStream(pathPlaintextFile);
var writeStream = fs.createWriteStream(pathCiphertextFile);

writeStream.write(iv); // write IV
var cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
readStream.pipe(cipher).pipe(writeStream); // write ciphertext chunk
...

Благодаря этим изменениям обе стороны создают идентичные зашифрованные тексты (для идентичных входных данных).


Безопасность:
В случае, если статический IV используется не только в целях тестирования, следует отметить, что повторное использование пар ключ/IV является уязвимостью.
Следовательно, для фиксированного ключа не следует использовать статический IV, а вместо этого для каждого шифрования следует генерировать случайный IV.

Я также подозреваю, что while (!feof($inputFile)) в PHP так же сломан, как и в C - например, feof() не возвращает true до тех пор, пока чтение не завершится неудачей, поэтому он склонен обрабатывать последний прочитанный фрагмент дважды.

Andrew Henle 23.06.2024 20:32

@AndrewHenle - В случае, когда feof() не возвращает true, но открытый текст уже прочитан полностью (обычно, когда размер открытого текста кратен размеру фрагмента), шифрование выполняется в этом цикле с отключенным дополнением. Затем в следующем цикле генерируется заполнение (путем шифрования пустого открытого текста с включенным заполнением). Так что это не должно быть проблемой.

Topaco 24.06.2024 00:24

Другие вопросы по теме