Я столкнулся с проблемой обеспечения согласованного вывода шифрования между Node.js и PHP при чтении больших файлов по частям. Вывод отличается, хотя считываются и обрабатываются одни и те же значения. Ниже приведен код для реализаций PHP и Node.js.
<?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/
Я сократил код. Кроме того, полный код шифрования и дешифрования можно найти здесь. Ссылка: drive.google.com/drive/folders/…
В обоих кодексах есть несколько проблем.
Декодирование ключа Base64 отсутствует в коде PHP.
Вам также следует изменить шифрование в коде PHP, чтобы создавался тот же зашифрованный текст, что и при шифровании, которое шифрует весь открытый текст одновременно. Благодаря этому изменению можно использовать любой размер фрагмента (при условии, что он кратен размеру блока).
Это не только приводит к разделению шифрования и дешифрования, но и упрощает реализацию NodeJS, как будет объяснено позже.
Для этого необходимо внести следующие изменения в заполнение CBC/PKCS#7:
n
-го фрагмента зашифрованного текста должен использоваться как IV n+1
-го фрагмента зашифрованного текста.Кроме того, следует устранить несоответствия и неэффективность (что также облегчает реализацию вышеуказанных изменений):
В целом описанные изменения в 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
до тех пор, пока чтение не завершится неудачей, поэтому он склонен обрабатывать последний прочитанный фрагмент дважды.
@AndrewHenle - В случае, когда feof()
не возвращает true
, но открытый текст уже прочитан полностью (обычно, когда размер открытого текста кратен размеру фрагмента), шифрование выполняется в этом цикле с отключенным дополнением. Затем в следующем цикле генерируется заполнение (путем шифрования пустого открытого текста с включенным заполнением). Так что это не должно быть проблемой.
Я бы рекомендовал сократить коды. Поскольку вас интересуют различные зашифрованные тексты, вы можете исключить всю часть MD5, а также множество журналов. Это только отвлекает от сути проблемы. Публикуйте только минимальный код, необходимый для воспроизведения, см. MCVE.