PHP 加密来自 Javascript 的流文件 [英] PHP Encrypt Streamed File From Javascript

查看:34
本文介绍了PHP 加密来自 Javascript 的流文件的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

这是我的 Javascript 代码

函数上传(fileInputId, fileIndex){var file = document.getElementById(fileInputId).files[fileIndex];var blob;var reader = new FileReader();reader.readAsBinaryString(file);reader.onloadend = 函数(evt){xhr = 新的 XMLHttpRequest();xhr.open("POST", 'upload.php?name=' + file.name, true);XMLHttpRequest.prototype.mySendAsBinary = 函数(文本){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 == "函数"){blob = new Blob([数据]);}别的{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. 加载;var total = e.totalSize ||e.总计;var 百分比 = Math.round((position/total)*100);});xhr.onreadystatechange = function(){如果(xhr.readyState == 4){如果(xhr.status == 200){console.log("完成");}别的{console.log("失败");}}};xhr.mySendAsBinary(evt.target.result);};}

这是我的upload.php

$inputHandler = fopen('php://input', "r");$loc = "上传/" .$_GET["名称"];$fileHandler = fopen($loc, "w+");而(真){$buffer = fgets($inputHandler, 4096);如果 (strlen($buffer) == 0) {fclose($inputHandler);fclose($fileHandler);返回真;}fwrite($fileHandler, $buffer);}

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

解决方案

事情是这样的.这是来自记忆并且未经测试,因为我的笔记本电脑上没有 PHPSecLib 库,而且我懒得设置所有...

需要 __DIR__ .'/vendor/autoload.php';使用 phpseclib\Crypt\AES;使用 phpseclib\Crypt\Random;AESStreamEncode($input, $output, $key){$cipher = 新 AES(AES::MODE_CBC);$cipher->setKey($key);$iv = Random::string($cipher->getBlockLength()>>3);$cipher->setIV($iv);$base64_iv = rtrim(base64_encode($iv), '=');//22个字符fwrite($output, $base64_iv);//存储IV这就像盐while(!feof($input)) {$contents = fread($input, 1000000);//要加密的字节数$encrypted = $cipher->encrypt($contents);//修剪=或==,并替换为:,写入输出流.fwrite($output, rtrim(base64_encode($encrypted), '=').':');}}AESStreamDecode($input, $output, $key){$cipher = 新 AES(AES::MODE_CBC);$cipher->setKey($key);$buffer = '';$iv = 假;while(!feof($input)) {$char = fgetc($input);//获取单个字符if($char ==':'){如果(!$iv){$iv = base64_decode(substr($buffer, 0, 22).'=');//iv 是第一个块的前 22 个.$cipher->setIV($iv);$buffer = substr($buffer, 22);//删除iv}$buffer = base64_decode($buffer.'=');//将base64解码为bin$decrypted = $cipher->decrypt($buffer);fwrite($output, $decrypted);$buffer = '';//清除缓冲区.}别的{$buffer .= $char;}}}

其中 $input$output 是有效的资源流句柄,例如来自 fopen

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

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

您必须删除 = 因为它有时会丢失或其中 2 个,因此我们不能依赖它们作为分隔符.我通常只是把 1 放回去,它总是能正确解码.我认为无论如何这只是一些填充.

参考资料

GitHub 上的 PHPSecLib

PHPSecLib 示例

加密后的文件应该是这样的:

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 上可用.

更新

对于下载,您可以只发出标头,然后为解码功能提供输出流,类似这样

 $input = fopen('encrypted_file.txt', 'r');$output = fopen('php://output', 'w');header('Content-Type: "text/plain"');header('Content-Disposition: attachment; filename="decoded.txt"');header('过期时间:0');header('Cache-Control: must-revalidate, post-check=0, pre-check=0, max-age=0');标头(内容传输编码:二进制");header('Pragma: public');//header('Content-Length: '.$fileSize);//未知AESStreamDecode($input, $output, $key);

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

但是因为我们在加密之前就知道大小,所以我们可以像这样将它嵌入到文件数据本身中:

 3555543|xUg8L3AatsbvsGUaHLg6uYUDIpqv0xnZsimumv7j:zBzWUn3xqBt+k1XP0KmWoU8lyfFh1ege:nJzxnYF51VeMRZEeQDRl8:

然后在我们下载时将其拉出,但是您必须将其用作单独的函数来获取它,并且不搞乱解码文件可能有点棘手.

老实说,我认为这更麻烦而不值得.

更新 2

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

AESStreamEncode($input, $output, $key, $filesize = false){$cipher = 新 AES(AES::MODE_CBC);$cipher->setKey($key);$iv = Random::string($cipher->getBlockLength()>>3);$cipher->setIV($iv);$base64_iv = rtrim(base64_encode($iv), '=');//22个字符//Option1 - 可选文件大小如果(假!== $文件大小){//如果在参数中给出,则添加文件大小fwrite($output, $filesize.'|');}/*//选项2:使用fstat,从参数中删除'$filesize = false'$stat = fstat($input);fwrite($output, $stat['size'].'|');*/fwrite($output, $base64_iv);//存储IV这就像盐while(!feof($input)) {$contents = fread($input, 1000000);//要加密的字节数$encrypted = $cipher->encrypt($contents);//修剪=或==,并替换为:,写入输出流.fwrite($output, rtrim(base64_encode($encrypted), '=').':');}}

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

AESStreamDecode($input, $output, $key){$cipher = 新 AES(AES::MODE_CBC);$cipher->setKey($key);$buffer = '';$iv = 假;$文件大小=空;while(!feof($input)) {$char = fgetc($input);//获取单个字符if($char =='|'){/*从文件中获取文件大小,这是一个后备方法,所以它不会影响文件,如果我们不使用其他函数将其拉出(见下文)*/$文件大小= $缓冲区;$buffer = '';}elseif($char ==':'){如果(!$iv){$iv = base64_decode(substr($buffer, 0, 22).'=');//iv 是第一个块的前 22 个.$cipher->setIV($iv);$buffer = substr($buffer, 22);//删除iv}$buffer = base64_decode($buffer.'=');//将base64解码为bin$decrypted = $cipher->decrypt($buffer);fwrite($output, $decrypted);$buffer = '';//清除缓冲区.}别的{$buffer .= $char;}}//当我们进行下载时,我们不想等待这个返回 $ 文件大小;}

解码获取文件大小部分充当后备,或者如果您不需要它,那么您不必担心它在解码时弄乱文件.下载的时候我们可以使用下面的函数,这样我们就不用等文件读完才知道大小了(这个和我们上面做的基本一样).

//我们必须使用一个单独的函数,因为//我们迫不及待地告诉阅读已完成//返回文件大小,它违背了目的AESStreamGetSize($input){$buffer = '';//PHP_INT_MAX(最大允许整数)为 19 个字符长//因此通过将限制设置为 20,我们可以缩短阅读时间//如果我们找不到文件大小$limit = 20;$i;//简单的计数器.while(!feof($input)) {$char = fgetc($input);//获取单个字符if($char =='|'){返回 $buffer;}elseif($i >= $limit){休息;}$buffer .= $char;++$i;//增加我们读取的字符数}返回假;}

然后在下载时您只需要进行一些更改即可.

$input = fopen('encrypted_file.txt', 'r');//输出流将其直接转储到输出,让我们处理更大的文件$output = fopen('php://output', 'w');//其他标题在这里if(false !== ($filesize = AESStreamGetSize($input))){header('内容长度:'.$fileSize);//未知//因为它是一个文件指针,我们可以利用它//解码函数将从 getSize 停止的地方开始.//或者你可以因为我们有后备而倒带它.AESStreamDecode($input, $output, $key);}别的{//如果我们找不到文件大小,那么我们可以在没有它的情况下回退下载//在这种情况下,我们需要倒带文件倒带($输入);AESStreamDecode($input, $output, $key);}

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

 if(false !== ($filesize = AESStreamGetSize($input))) header('Content-Length: '.$fileSize);倒带($输入);AESStreamDecode($input, $output, $key);

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

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

I am developing a File Uploader for big file. Upload from HTML script and send by byte from Javascript using ArrayBuffer and Unit8Array to PHP. The PHP script will stream the file and save it into folder.

Here's my Javascript looks like

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

This is my upload.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);
}

My question is, how do I encrypt those upload file using AES or mcrypt while the file is in streaming mode?

解决方案

It was something like this. This is from memory and untested, because I don't have the PHPSecLib library on my Laptop, and I am too lazy to set that all up...

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

Where $input and $output are valid resource stream handles like from fopen etc.

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

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

This lets you use things like php://output as the stream if downloading the decrypted file.

You have to remove the = because it is sometimes missing or 2 of them, So we cant rely on them as a separator. I usually just put 1 back on and it always decodes it correctly. I think it's just some padding anyway.

References

PHPSecLib on GitHub

PHPSecLib Examples

The encrypted file should look something like this:

xUg8L3AatsbvsGUaHLg6uYUDIpqv0xnZsimumv7j:zBzWUn3xqBt+k1XP0KmWoU8lyfFh1ege:nJzxnYF51VeMRZEeQDRl8:

But with longer chunks. The IV is like a salt and it's pretty common practice to just add it to the front or back of the encrypted string. So for example

[xUg8L3AatsbvsGU]aHLg6uYUDIpqv0xnZsimumv7j:

The part in the [] is the IV, (its 22 chars long after base64_encode) I counted it many times and it always comes out that long. We only need to record the IV and set it one time. I suppose you could do a different IV for each chunk, but whatever.

If you do use PHPSecLib, it also has some nice sFTP stuff in it. Just make sure to get the 2.0 version. Basically it has some fallbacks and native PHP implementations for different encryption algos. So like it would try open_ssl then if you were missing it, it would use their native implementation. I use it for sFTP, so I already had it available. sFTP requires an extension ssh2_sftp and If I recall it was only available on Linux at the time we set things up.

UPDATE

For downloading you can just issue the headers then give the decode function the output stream, something like this

 $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);

These are pretty standard headers. The only real catch is because the filesize is different when it's encryped you can't just simply get the size of the file and use that as it will be quite a bit bigger. Not passing the filesize won't prevent the download, it just wont have an estimated time etc.

But because we know the size before encrypting it, we could embed it in the file data itself like this:

 3555543|xUg8L3AatsbvsGUaHLg6uYUDIpqv0xnZsimumv7j:zBzWUn3xqBt+k1XP0KmWoU8lyfFh1ege:nJzxnYF51VeMRZEeQDRl8:

And then pull it out when we do the download, but you would have to use as separate function to get it and it might be a bit tricky to not mess up decoding the file.

Honestly I think it's more hassle then it's worth.

UPDATE2

Anyway, I worked up these changes for embedding the file size, it's an option, but it could also mess up the decryption of the file if not done carefully. (I haven't tested this)

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), '=').':'); 
    }
}

So now we should have the filesize 3045345|asdaeASE:AEREA etc. Then we can pull it back out when decrypting.

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

The decode get filesize part acts as a fallback, or if you don't need it then you don't have to worry about it messing the file up when decoding it. When downloading we can use the following function, that way we don't have to wait for the file to be completely read to get the size (this is basically the same as what we did above).

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

Then when downloading you just need to make a few changes.

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

If you want to shorten this you can just do it this way too, it's only about 19 chars at most so it's not to big a performance issue.

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

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

Basically above, we just do the filesize header (or not) and then rewind and do the download. It will re-read the filesize, but that's pretty trivial.

For reference fstat(), Hopefully that makes sense.

这篇关于PHP 加密来自 Javascript 的流文件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆