【问题标题】:PHP Encrypt Streamed File From JavascriptPHP从Javascript加密流文件
【发布时间】:2018-09-09 01:58:51
【问题描述】:

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

这是我的 Javascript 的样子

function upload(fileInputId, fileIndex)
    {
        var file = document.getElementById(fileInputId).files[fileIndex];
        var blob;
        var reader = new FileReader();
        reader.readAsBinaryString(file); 
        reader.onloadend  = function(evt)
        {
            xhr = new XMLHttpRequest();

            xhr.open("POST", 'upload.php?name=' + file.name, true);

            XMLHttpRequest.prototype.mySendAsBinary = function(text){
                var data = new ArrayBuffer(text.length);
                var ui8a = new Uint8Array(data, 0);
                for (var i = 0; i < text.length; i++){ 
                    ui8a[i] = (text.charCodeAt(i) & 0xff);

                }

                if(typeof window.Blob == "function")
                {
                     blob = new Blob([data]);
                }else{
                     var bb = new (window.MozBlobBuilder || window.WebKitBlobBuilder || window.BlobBuilder)();
                     bb.append(data);
                     blob = bb.getBlob();
                }

                this.send(blob);
            }

            var eventSource = xhr.upload || xhr;
            eventSource.addEventListener("progress", function(e) {
                var position = e.position || e.loaded;
                var total = e.totalSize || e.total;
                var percentage = Math.round((position/total)*100);
            });

            xhr.onreadystatechange = function()
            {
                if(xhr.readyState == 4)
                {
                    if(xhr.status == 200)
                    {
                        console.log("Done");
                    }else{
                        console.log("Fail");
                    }
                }


            };

            xhr.mySendAsBinary(evt.target.result);
        };
    }

这是我的上传.php

$inputHandler = fopen('php://input', "r");
$loc = "uploads/" . $_GET["name"];
$fileHandler = fopen($loc, "w+");

while(true) {
    $buffer = fgets($inputHandler, 4096);



    if (strlen($buffer) == 0) {
        fclose($inputHandler);
        fclose($fileHandler);
        return true;
    }

    fwrite($fileHandler, $buffer);
}

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

【问题讨论】:

  • 你为什么要这样做,我有点认为这就是 https SSL/TLS 的用途。同样取决于它有多大,AES 最终会向你吐槽。也许50-100MB。你可以像我一样做一些事情。取大约 1MB,加密它,添加一个: 再取 1MB 加密它。等等。然后当你解密时,你只需读取文件告诉你得到一个: 解码一个块,等等等等。基本上一次加密一个块。
  • 我的意思是,该文件将存储为加密文件。可能看起来像 image.enc 或类似的东西
  • 它有多大?你迟早会遇到记忆墙,试图加密一个大文件。我忘记了我们有多大,但是我们有 54GB 的内存并且必须逐块加密。
  • 我也会避免 mycrypt - This feature was DEPRECATED in PHP 7.1.0, and REMOVED in PHP 7.2.0. 我只需要重新编码我的 AES,因为几个月后就要迁移到 PHP7。而且加密不向后兼容,这意味着我必须重新加密所有内容。所以我决定把大文件内存不足的问题搁置一旁。
  • 在我们的系统上,我们将最大值设置为 1GB。按 MB 加密有意义

标签: javascript php stream


【解决方案1】:

是这样的。这是来自内存且未经测试,因为我的笔记本电脑上没有 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 之类的内容作为流。

您必须删除=,因为它有时会丢失或其中两个,所以我们不能依赖它们作为分隔符。我通常只是把 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:

然后在我们下载的时候把它拉出来,但是你必须使用单独的函数来获取它,而且不搞乱解码文件可能有点棘手。

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

更新2

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

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;
}

decode get 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);

基本上,我们只是做文件大小标题(或不做),然后倒带并进行下载。它会重新读取文件大小,但这很简单。

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

【讨论】:

  • 感谢您的代码。顺便说一句,AESStreamEncode 应该在while(true) 代码块中还是在外面?输入是$inputHandler(我使用的是php://input)还是$fileHandler(写文件)?
  • 我从来没有像你的问题那样上传过,所以我不知道它是给你整个文件还是一次只给你一些字节。这只是记忆中的……哈哈。我大概是在两周前写的。我也没有测试所以希望没有太多错误。
  • 我认为在您的情况下,我会将读取循环 while(!feof) 替换为您的 while(true) 然后 $input = $inputHandler$output = $fileHandler,您可能还需要循环几次才能获得金额您需要的字节数,正如我所说,我不知道这是否像异步流一样,一次只能获取这么多字节。 fgets 例如只读取 1 行。
  • 可以像读取普通文件一样读取您的流。正如我上面提到的fgets 只读取到下一个\n 新行或字节限制。因此,如果您需要循环多次以获得所需的字节数,则可以检查缓冲区的长度。这是一种平衡,你不希望它太长以至于导致内存问题,但你也不希望它太短以至于你多次调用encrypt
  • 加密文件的大小比原始文件大30-40%。正常吗?
猜你喜欢
  • 1970-01-01
  • 2020-01-27
  • 2015-02-24
  • 2011-08-09
  • 2014-02-26
  • 2011-01-24
  • 2015-11-27
相关资源
最近更新 更多