PHP从Javascript加密流式文件

时间:2018-03-30 01:55:52

标签: javascript php stream

我正在为大文件开发文件上传器。从HTML脚本上传并使用ArrayBuffer和Unit8Array从Javascript逐字节发送到PHP。 PHP脚本将流式传输文件并将其保存到文件夹中。

这里的Javascript看起来像是

mpl.style.use('classic')

这是我的upload.php

   $myArray = array(

        0 => array(
            account => 251234567890,
            price => 83,
            ),

        1 => array(
            account => 251234567890,
            price => 27,
            ),
        2 => array(
            account => 251234564526,
            price => 180,
            ),
        3 => array(
            account => 251234567890,
            price => 40,
            ),
   );
$keyed = [];
foreach ($myArray as $item) {
  if (!isset($keyed[$item['account']])) {
    $keyed[$item['account']] = 0;
  }
  $keyed[$item['account']] += $item['price'];
}

$merged = [];
foreach ($keyed as $account => $price) {
  $merged[] = compact('account', 'price');
}

print_r($merged);

我的问题是,当文件处于流式传输模式时,如何使用AES或mcrypt加密这些上传文件?

1 个答案:

答案 0 :(得分:2)

就是这样的。这是来自内存和未经测试的,因为我的笔记本电脑上没有PHPSecLib库,而且我懒得把它全部设置好......

require __DIR__ . '/vendor/autoload.php';

use phpseclib\Crypt\AES;
use phpseclib\Crypt\Random;

AESStreamEncode($input, $output, $key)
{
    $cipher = new AES(AES::MODE_CBC);
    $cipher->setKey($key);

    $iv = Random::string($cipher->getBlockLength() >> 3);
    $cipher->setIV($iv);

    $base64_iv = rtrim(base64_encode($iv), '='); //22 chars

    fwrite($output, $base64_iv); //store the IV this is like a salt

    while(!feof($input)) {
        $contents = fread($input, 1000000); //number of bytes to encrypt 
        $encrypted = $cipher->encrypt($contents);
        //trim the = or ==, and replace with :, write to output stream.
        fwrite($output, rtrim(base64_encode($encrypted), '=').':'); 
    }
}

AESStreamDecode($input, $output, $key)
{
    $cipher = new AES(AES::MODE_CBC);
    $cipher->setKey($key);

    $buffer = '';
    $iv = false;

    while(!feof($input)) {
        $char = fgetc($input); //get a single char
        if($char ==':'){
            if(!$iv){
                $iv = base64_decode(substr($buffer, 0, 22).'=');  //iv is the first 22 of the first chunk.
                $cipher->setIV($iv);
                $buffer = substr($buffer, 22); //remove the iv
            }
            $buffer = base64_decode($buffer.'='); //decode base64 to bin
            $decrypted = $cipher->decrypt($buffer);
            fwrite($output, $decrypted);

            $buffer = ''; //clear buffer.
        }else{
            $buffer .= $char;
        }
    }
}

其中$input$output是来自fopen等的有效资源流处理。

 $input = fopen($filepath, 'r');
 $output = fopen($ohter_filepath, 'w');

 AESStreamEncode($input, $output, $key);

这使您可以在下载解密文件时使用php://output之类的内容作为流。

您必须删除=,因为它有时会丢失或其中2个,所以我们不能依赖它们作为分隔符。我通常只重新打开1,它总是正确地解码它。我认为这只是一些填充。

<强>参考

PHPSecLib on GitHub

PHPSecLib Examples

加密文件应如下所示:

xUg8L3AatsbvsGUaHLg6uYUDIpqv0xnZsimumv7j:zBzWUn3xqBt+k1XP0KmWoU8lyfFh1ege:nJzxnYF51VeMRZEeQDRl8:

但是有更长的块。 IV就像一个盐,将它添加到加密字符串的前面或后面是很常见的做法。例如,

[xUg8L3AatsbvsGU]aHLg6uYUDIpqv0xnZsimumv7j:

[]中的部分是IV,(它在base64_encode之后很长的22个字符)我计算了很多次,它总是很长。我们只需要记录IV并将其设置一次。我想你可以为每个块做一个不同的IV,但无论如何。

如果您使用PHPSecLib,它还有一些不错的sFTP内容。只需确保获得2.0版本。基本上它有一些后备和本机PHP实现不同的加密算法。所以就像尝试open_ssl一样,如果你错过它,它会使用它们的原生实现。我将它用于sFTP,所以我已经有了它。 sFTP需要扩展名ssh2_sftp,如果我记得它只在我们设置的时候在Linux上可用。

更新

下载时你可以发出标题,然后给解码函数output stream,就像这样

 $input = fopen('encrypted_file.txt', 'r');
 $output = fopen('php://output', 'w');

 header('Content-Type: "text/plain"');
 header('Content-Disposition: attachment; filename="decoded.txt"');

 header('Expires: 0');
 header('Cache-Control: must-revalidate, post-check=0, pre-check=0, max-age=0');
 header("Content-Transfer-Encoding: binary");
 header('Pragma: public');

 //header('Content-Length: '.$fileSize);  //unknown

 AESStreamDecode($input, $output, $key);

这些是非常标准的标题。唯一真正的问题是因为文件大小在加密时是不同的,你不能简单地获取文件的大小并使用它,因为它会更大一些。没有传递文件大小不会阻止下载,它只是没有预计的时间等。

但是因为我们在加密之前知道了它的大小,我们可以将它嵌入文件数据中,如下所示:

 3555543|xUg8L3AatsbvsGUaHLg6uYUDIpqv0xnZsimumv7j:zBzWUn3xqBt+k1XP0KmWoU8lyfFh1ege:nJzxnYF51VeMRZEeQDRl8:

然后在我们进行下载时将其拉出来,但是您必须使用单独的函数来获取它,并且可能有点难以解决文件的解码。

老实说,我认为这样做更麻烦。

UPDATE2

无论如何,我为嵌入文件大小进行了这些更改,这是一个选项,但是如果不仔细的话,它也可能会破坏文件的解密。 (我没有测试过这个)

AESStreamEncode($input, $output, $key, $filesize = false)
{
    $cipher = new AES(AES::MODE_CBC);
    $cipher->setKey($key);

    $iv = Random::string($cipher->getBlockLength() >> 3);
    $cipher->setIV($iv);

    $base64_iv = rtrim(base64_encode($iv), '='); //22 chars

    //Option1 - optional filesize
    if(false !== $filesize){
        //add filesize if given in the arguments
        fwrite($output, $filesize.'|');
    }

    /*
        //Option2: using fstat, remove '$filesize = false' from the arguments
        $stat = fstat($input);
        fwrite($output, $stat['size'].'|');
    */

    fwrite($output, $base64_iv); //store the IV this is like a salt

    while(!feof($input)) {
        $contents = fread($input, 1000000); //number of bytes to encrypt 
        $encrypted = $cipher->encrypt($contents);
        //trim the = or ==, and replace with :, write to output stream.
        fwrite($output, rtrim(base64_encode($encrypted), '=').':'); 
    }
}

所以现在我们应该有文件大小3045345|asdaeASE:AEREA等等。然后我们可以在解密时把它拉出来。

AESStreamDecode($input, $output, $key)
{
    $cipher = new AES(AES::MODE_CBC);
    $cipher->setKey($key);

    $buffer = '';
    $iv = false;
    $filesize = null;

    while(!feof($input)) {
        $char = fgetc($input); //get a single char
        if($char =='|'){
            /*
              get the filesize from the file,
              this is a fallback method, so it wont affect the file if
              we don't pull it out with the other function (see below)
            */
            $filesize = $buffer;
            $buffer = '';
        }elseif($char ==':'){
            if(!$iv){
                $iv = base64_decode(substr($buffer, 0, 22).'=');  //iv is the first 22 of the first chunk.
                $cipher->setIV($iv);
                $buffer = substr($buffer, 22); //remove the iv
            }
            $buffer = base64_decode($buffer.'='); //decode base64 to bin
            $decrypted = $cipher->decrypt($buffer);
            fwrite($output, $decrypted);

            $buffer = ''; //clear buffer.
        }else{
            $buffer .= $char;
        }
    }
    //when we do a download we don't want to wait for this
    return $filesize;
}

解码获取文件大小部分充当回退,或者如果您不需要它,那么您不必担心在解码时弄乱文件。下载时我们可以使用以下函数,这样我们就不必等待文件被完全读取以获得大小(这与我们上面做的基本相同)。

//We have to use a separate function because
//we can't wait tell reading is complete to 
//return the filesize, it defeats the purpose
AESStreamGetSize($input){
    $buffer = '';
    //PHP_INT_MAX (maximum allowed integer) is 19 chars long
    //so by putting a limit of 20 in we can short cut reading
    //if we can't find the filesize
    $limit = 20;
    $i; //simple counter.
    while(!feof($input)) {
        $char = fgetc($input); //get a single char
        if($char =='|'){
            return $buffer;
        }elseif($i >= $limit){
            break;
        }
        $buffer .= $char;
        ++$i; //increment how many chars we have read
    }
    return false;
}

然后在下载时,您只需进行一些更改。

$input = fopen('encrypted_file.txt', 'r');
//output streams dumps it directly to output, lets us handle larger files
$output = fopen('php://output', 'w');
//other headers go here

if(false !== ($filesize = AESStreamGetSize($input))){
    header('Content-Length: '.$fileSize);  //unknown
    //because it's a file pointer we can take advantage of that
    //and the decode function will start where the getSize left off.
    // or you could rewind it because of the fallback we have.
    AESStreamDecode($input, $output, $key);
}else{
    //if we can't find the filesize, then we can fallback to download without it
    //in this case we need to rewind the file
    rewind($input);
    AESStreamDecode($input, $output, $key);
}

如果你想缩短它,你也可以这样做,它最多只有19个字符,所以这不是一个很大的性能问题。

 if(false !== ($filesize = AESStreamGetSize($input))) header('Content-Length: '.$fileSize);

 rewind($input);
 AESStreamDecode($input, $output, $key);

基本上,我们只是执行filesize标头(或不),然后倒回并进行下载。它将重新读取文件大小,但这非常简单。

参考fstat(),希望这是有道理的。