Create an encrypted zip archive with PHP - php

I am searching for a way to encrypt a .txt file into a zip, but in a secure password protected way. My goal is to email this file to me, without anyone being able to read the content of the attachment.
Does anybody know an easy, and above all, secure way to accomplish this ? I can create zip archives, but I do not know how to encrypt them, or, how secure this is.

As of php 7.2 (which was released a hours ago), the right way to do this is to use additional functionality included in ZipArchive native php code. (thanks to abraham-tugalov for pointing out that this change was coming)
Now the simple answer looks something like this:
<?php
$zip = new ZipArchive();
if ($zip->open('test.zip', ZipArchive::CREATE) === TRUE) {
$zip->setPassword('secret_used_as_default_for_all_files'); //set default password
$zip->addFile('thing1.txt'); //add file
$zip->setEncryptionName('thing1.txt', ZipArchive::EM_AES_256); //encrypt it
$zip->addFile('thing2.txt'); //add file
$zip->setEncryptionName('thing2.txt', ZipArchive::EM_AES_256); //encrypt it
$zip->close();
echo "Added thing1 and thing2 with the same password\n";
} else {
echo "KO\n";
}
?>
But you can also set the encryption method by index and not name, and you can set each password on a per-file basis... as well as specify weaker encryption options, using the newly supported encryption options.
This example exercises these more complex options.
<?php
$zip = new ZipArchive();
if ($zip->open('test.zip', ZipArchive::CREATE) === TRUE) {
//being here means that we were able to create the file..
//setting this means that we do not need to pass in a password to every file, this will be the default
$zip->addFile('thing3.txt');
//$zip->setEncryptionName('thing3.txt', ZipArchive::EM_AES_128);
//$zip->setEncryptionName('thing3.txt', ZipArchive::EM_AES_192);
//you should just use ZipArchive::EM_AES_256 unless you have super-good reason why not.
$zip->setEncryptionName('thing3.txt', ZipArchive::EM_AES_256, 'password_for_thing3');
$zip->addFile('thing4.txt');
//or you can also use the index (starting at 0) of the file...
//which means the following line should do the same thing...
//but just referencing the text.txt by index instead of name..
//$zip->setEncryptionIndex(1, ZipArchive::EM_AES_256, 'password_for_thing_4'); //encrypt thing4, using its index instead of its name...
$zip->close();
echo "Added thing3 and thing4 with two different passwords\n";
} else {
echo "KO\n";
}
?>
The underlying support for zip encryption is enabled because libzip 1.2.0 introduced support for encryption. So you will need to have php 7.2 and libzip 7.2 in order to run this code... Hopefully this note will be cruft on this answer "real soon"

Note: this answer recommends a cryptographic method that is known
insecure, even with good password. Please see link from comments
and the Winzip QA on AES. Support for in-php AES zip encryption
arrives with php 7.2 (and libzip 1.2.0), which means this
answer will soon be outdated too. Until then see this answer for how
to call out to 7z instead of the zip command, which supports winzip's
AES encryption.
You can use this:
<?php echo system('zip -P pass file.zip file.txt'); ?>
Where pass is the password, and file.txt will be zipped into file.zip. This should work on Windows and Linux, you just need to get a free version of zip for Windows ( http://www.info-zip.org/Zip.html#Win32 )
This kind of security can be broken by brute force attacks, dictionary attacks and etc. But it's not that easy, specially if you chose a long and hard to guess password.

Although PHP is a mature language, there is no adequate method (excluding custom extension or something like that) to achieve such a simple task with pure PHP.
What you also can do, is to wait until PHP 7.2 will be available for production (because ZipArchive::setEncryptionName is implemented (thanks to Pierre and Remi)).
But, until then you also can try to port php_zip >= 1.14.0 to PHP < 7.2, but there is currently no compiled binaries available, so you have to compile it yourself and try if it is possible at all (I believe it is).
PS I would try it, but have no VS2015+ on my PC right now.

More and more tools are supporting AES-encrypted ZIP files. It works, it's secure.
EDIT2: You can use DotNetZip from PHP to dynamically generate AES-encrypted zip archives from PHP. DotNetZip is a .NET library that is designed for .NET languages (C#, VB, etc). It runs only on Windows :(. But DotNetZip does AES, and it's free, and it works from PHP.
This is the code I used. (PHP v5.2.9 on Win32)
<?php
try
{
$fname = "zip-generated-from-php-" . date('Y-m-d-His') . ".zip";
$zipOutput = "c:\\temp\\" . $fname;
$zipfact = new COM("Ionic.Zip.ZipFile");
$zip->Name = $zipOutput;
$dirToZip= "c:\\temp\\psh";
# Encryption: 3 => 256-bit AES.
# 2 => 128-bit AES.
# 1 => PKZIP (Weak).
# 0 => None
$zip->Encryption = 3;
$zip->Password = "AES-Encryption-Is-Secure";
$zip->AddDirectory($dirToZip);
$zip->Save();
$zip->Dispose();
if (file_exists($zipOutput))
{
header('Cache-Control: no-cache, must-revalidate');
header('Content-Type: application/x-zip');
header('Content-Disposition: attachment; filename=' . $fname);
header('Content-Length: ' . filesize($zipOutput));
readfile($zipOutput);
unlink($zipOutput);
}
else
{
echo '<html>';
echo ' <head>';
echo ' <title>Calling DotNetZip from PHP through COM</title>';
echo ' <link rel="stylesheet" href="basic.css"/>';
echo ' </head>';
echo '<body>';
echo '<h2>Whoops!</h2>' . "<br/>\n";
echo '<p>The file was not successfully generated.</p>';
echo '</body>';
echo '</html>';
}
}
catch (Exception $e)
{
echo '<html>';
echo ' <head>';
echo ' <title>Calling DotNetZip from PHP through COM</title>';
echo ' <link rel="stylesheet" href="basic.css"/>';
echo ' </head>';
echo '<body>';
echo '<h2>Whoops!</h2>' . "<br/>\n";
echo '<p>The file was not successfully generated.</p>';
echo '<p>Caught exception: ', $e->getMessage(), '</p>', "\n";
echo '<pre>';
echo $e->getTraceAsString(), "\n";
echo '</pre>';
echo '</body>';
echo '</html>';
}
?>
I had to modify DotNetZip to make it work with PHP: I had to make the Name property read/write, and I had to make it COM-callable. This change is first available in the v1.8.2.3 release.

This is how I did it.
It's with an excel, but it's the same thing.
Let php create a random codename.
Save the codename in db or in a file to be included by the retrieve.php.
Mail yourself the codename.
Access via web the retrieve.php?codename=[codename]
Let php check if codename is correct. (maybe even it's age).
Create the excel/textfile in memory from the data that needs to be send to you.
Create an encryption code by adding a local password (that only you know) with the codename.( I say add but can also be mixed or xor-ed or substr... )
Encrypt on the fly with the created encryption code.
Remove the codename from db or config file.
Return this encrypted document in a mail (zipped or not for size).
Maybe add two first characters from codename in the mail to know what local full codename to use.
Use a local decryption script to decode the downloaded file. Use same codename/password algorithm to create a decryption key.
I use gibberish-aes-php. ( https://github.com/ivantcholakov/gibberish-aes-php )
Because then I can use https://github.com/mdp/gibberish-aes as javascript on a client decoder (for things I want to take a quick peek at in a browser).

Related

Consume dll from php wamp

I have been given 2 dll's.
The first encrypts a string.
The second, decrypts an encrypted string.
I do not know how it works this dll.
Also I have been given a code example in .net (Don't know .net)
dll_decrypt_.Decrypt myuser = new dll_decrypt.Decrypt();
user = myuser.DecryptString(userEncrypted);
I have searched among the web, but still cant' find the answer.
Also I have added the 2 dll's to the ext folder in wamp:
C:\wamp64\bin\php\php7.0.32\ext
And added the "extension=dll_decrypt.dll" into the php.ini file both in apache and php folders.
This would be my php code:
try
{
$dll = new COM('dll_decrypt.Decrypt');
$dll->Function();
}
catch(Exception $e)
{
echo 'error: ' . $e->getMessage(), "\n";
}
I expect to get the decrypted string so I can see the user.

Detect broken symlink in PHP on Windows 7 + Apache environment

I have a Windows 7 dev environment and I am using symlinks (not junctions) with php's symlink() function. Everything works fine until a target of a symlink gets deleted. When that happens all the PHP's file functions (file_exists(), is_file(), is_dir(), is_link(),...) return false although the symlink is still there.
This gets even more troubling if the broken symlink was originally targetting a directory. I can do file_put_contents() on the symlink path, which creates a file on the original target directory path. That's quite unfortunate and unpredictable.
So to my question: is there a way to detect a broken symlink in PHP on Windows 7 environment? I need whatever decent solution possible (an exec() or something like that). The production server is running standard LAMP configuration which works fine as expected.
I am using XAMPP with PHP 5.5.3.
A sample script:
$dir_path = __DIR__ . '/temporary_directory';
$link_path = __DIR__ . '/broken_symlink';
mkdir($dir_path);
symlink($dir_path, $link_path);
rmdir($dir_path);
echo 'file_exists(): ';
var_dump(file_exists($link_path));// false
echo "<br>\n";
echo 'is_file(): ';
var_dump(is_file($link_path));// false
echo "<br>\n";
echo 'is_dir(): ';
var_dump(is_dir($link_path));// false
echo "<br>\n";
echo 'is_link(): ';
var_dump(is_link($link_path));// false
echo "<br>\n";
echo 'readlink(): ';
var_dump(readlink($link_path));// false
echo "<br>\n";
// Now it is possible to create file:
file_put_contents($link_path, '');
// which creates a new file on the $dir_path. That makes the symlink somewhat hybrid as Windows Explorer thinks it is a directory symlink but it points to a file (so it's unable to resolve it).
I have come up with a solution. I am not completely happy about it but it should work.
function is_link_on_windows($path) {
$parent_dir_path = dirname($path);
if (false === file_exists($parent_dir_path)) return false;
$filename_regex = preg_quote(substr($path, strlen($parent_dir_path) + 1), '#');
// DOS' dir command is unlike PHP sensitive about the path format.
$parent_dir_path = realpath($parent_dir_path);
exec('dir ' . $parent_dir_path, $dir_output);
foreach ($dir_output as $line) {
// Symlink's entry should look like this:
// broken_symlink [C:\xampp\htdocs\symlink_test\temporary_directory]
// where the part within square brackets is the symlink's target.
if (preg_match('#\\s' . $filename_regex . '\\s\\[[^\\]]+\\]#', $line)) {
return true;
}
}
return false;
}

clamdscan can't read from tmp directory

I was wondering what's wrong with my code, if I use clamscan, it works fine both reading from /tmp, or manually specified the path. but if I use clamdscan, any path from /tmp will result in error (the int result is 2). This is the code.
$command = 'clamdscan ' . escapeshellarg($_FILES['file']['tmp_name']);
$out = '';
$int = -1;
exec($command, $out, $int);
echo "\n" . $command;
echo "\n" . $out;
echo "\n This is int = " . $int;
if ($int == 0) {
// all good, code goes here uploads file as normal IE move to
//echo "File path : ".$file."Return code : ".cl_pretcode($retcode);
//echo "\n";
move_uploaded_file($_FILES["file"]["tmp_name"], "filesave/" . $_FILES["file"]["name"]);
echo "Stored in: " . "filesave/" . $_FILES["file"]["name"];
} else {
echo "\n FAILED";
}
based on above code, it will failed because $int = 2. But, if I change the command to
//some file that is saved already in the directory
$command = 'clamdscan ' . '/abc/abc.txt';
It works perfectly fine.
It only failed if the command is clamdscan. if I use clamscan, temp directory is fine
any idea?
You should really just use one of the many clamd clients out there instead of relying on exec'ing a command and parsing its output, that's super fragile and will bring you nothing but headache in the future. For example:
http://torquecp.sourceforge.net/phpclamcli.html
If you are the do-it-yourself type, the clamd wire protocol is super simple (http://linux.die.net/man/8/clamd) and you could potentially write up a simple client in a couple of hours. Again, the benefit here is that it's a well defined protocol and has some nice features like a streaming call that allows you to operate the clamd service and your webapp with completely difference security credentials (heck they can even run on different boxes).
I hope this helps.
Just a quick remark on using http://torquecp.sourceforge.net/phpclamcli.html as a supposedly better alternative to a DIY cli exec. The aforementioned php script does entirely rely on the syntax of the clamav response text as well.
Old question but I've just had the same issue. clamdscan runs as user clamav which doesn't have permission to files in /tmp. There is an additional parameter --fdpass which runs the command as the user running the script.
Using $command = 'clamdscan --fdpass' . escapeshellarg($_FILES['file']['tmp_name']); should run the command as the www user which will have access to the temporary file.

Using Office Word to read doc files with PHP

I am trying to use PHP with word.application to read a file. It simply will not open the file. It's echoing the right version.
$w = new COM("word.application") or die("Is office installed?");
echo 'Loaded Word, version ' . $w->Version . '<br>';
$w->Visible = false;
$w->Documents->Open(realpath('test.docx'));
$content = (string) $w->ActiveDocument->Content;
echo $content;
$w->Quit();
$w->Release();
$w = null;
I get the error:
Uncaught exception 'com_exception' with message 'Source: Microsoft Word
Description: This command is not available because no document is open.'
It feels like it's some kind of permission problem. I tried to put the path of the test.docx besides using realpath and that did not help. Also tried to put it in the root of my C drive. I am using Windows 7 Professional and Microsoft Office 2007.
Documents->Open returns a document if all is ok. Probably, the document does not exists (path incorrect), or you haven't got the rights to open it from PHP. Store the result in $var, check if it has an appropriate value (probably not isset, null or false if not), and use $var->Content to read the content.
Try doing a file_exists on said
file/path.
If that works, try
file_get_contents and see if you can
read it.
If that all works - then it's not a problem with permissions/etc.

PHP: Equivalent of include using eval

If the code is the same, there appears to be a difference between:
include 'external.php';
and
eval('?>' . file_get_contents('external.php') . '<?php');
What is the difference? Does anybody know?
I know the two are different because the include works fine and the eval gives an error. When I originally asked the question, I wasn't sure whether it gave an error on all code or just on mine (and because the code was evaled, it was very hard to find out what the error meant). However, after having researched the answer, it turns out that whether or not you get the error does not depend on the code in the external.php, but does depend on your php settings (short_open_tag to be precise).
After some more research I found out what was wrong myself. The problem is in the fact that <?php is a "short opening tag" and so will only work if short_open_tag is set to 1 (in php.ini or something to the same effect). The correct full tag is <?php, which has a space after the second p.
As such the proper equivalent of the include is:
eval('?>' . file_get_contents('external.php') . '<?php ');
Alternatively, you can leave the opening tag out all together (as noted in the comments below):
eval('?>' . file_get_contents('external.php'));
My original solution was to add a semicolon, which also works, but looks a lot less clean if you ask me:
eval('?>' . file_get_contents('external.php') . '<?php;');
AFAIK you can't take advantage of php accelerators if you use eval().
If you are using a webserver on which you have installed an opcode cache, like APC, eval will not be the "best solution" : eval'd code is not store in the opcode cache, if I remember correctly (and another answer said the same thing, btw).
A solution you could use, at least if the code is not often changed, is get a mix of code stored in database and included code :
when necessary, fetch the code from DB, and store it in a file on disk
include that file
as the code is now in a file, on disk, opcode cache will be able to cache it -- which is better for performances
and you will not need to make a request to the DB each time you have to execute the code.
I've worked with software that uses this solution (the on-disk file being no more than a cache of the code stored in DB), and I worked not too bad -- way better that doing loads of DB requests of each page, anyway...
Some not so good things, as a consequence :
you have to fetch the code from the DB to put it in the file "when necessary"
this could mean re-generating the temporary file once every hour, or deleting it when the entry in DB is modified ? Do you have a way to identify when this happens ?
you also have to change your code, to use the temporary file, or re-generate it if necessary
if you have several places to modifiy, this could mean some work
BTW : would I dare saying something like "eval is evil" ?
This lets you include a file assuming file wrappers for includes is on in PHP:
function stringToTempFileName($str)
{
if (version_compare(PHP_VERSION, '5.1.0', '>=') && strlen($str < (1024 * 512))) {
$file = 'data://text/plain;base64,' . base64_encode($str);
} else {
$file = Utils::tempFileName();
file_put_contents($file, $str);
}
return $file;
}
... Then include that 'file.' Yes, this will also disable opcode caches, but it makes this 'eval' the same as an include with respect to behavior.
As noted by #bwoebi in this answer to my question, the eval substitution does not respect the file path context of the included file. As a test case:
Baz.php:
<?php return __FILE__;
Foo.php:
<?php
echo eval('?>' . file_get_contents('Baz.php', FILE_USE_INCLUDE_PATH)) . "\n";
echo (include 'Baz.php') . "\n";
Result of executing php Foo.php:
$ php Foo.php
/path/to/file/Foo.php(2) : eval()'d code
/path/to/file/Baz.php
I don't know of any way to change the __FILE__ constant and friends at runtime, so I do not think there is any general way to define include in terms of eval.
Only eval('?>' . file_get_contents('external.php')); variant is correct replacement for include.
See tests:
<?php
$includes = array(
'some text',
'<?php print "some text"; ?>',
'<?php print "some text";',
'some text<?php',
'some text<?php ',
'some text<?php;',
'some text<?php ?>',
'<?php ?>some text',
);
$tempFile = tempnam('/tmp', 'test_');
print "\r\n" . "Include:" . "\r\n";
foreach ($includes as $include)
{
file_put_contents($tempFile, $include);
var_dump(include $tempFile);
}
unlink($tempFile);
print "\r\n" . "Eval 1:" . "\r\n";
foreach ($includes as $include)
var_dump(eval('?>' . $include . '<?php '));
print "\r\n" . "Eval 2:" . "\r\n";
foreach ($includes as $include)
var_dump(eval('?>' . $include));
print "\r\n" . "Eval 3:" . "\r\n";
foreach ($includes as $include)
var_dump(eval('?>' . $include . '<?php;'));
Output:
Include:
some textint(1)
some textint(1)
some textint(1)
some text<?phpint(1)
some textint(1)
some text<?php;int(1)
some textint(1)
some textint(1)
Eval 1:
some textNULL
some textNULL
bool(false)
some text<?phpNULL
bool(false)
some text<?php;NULL
some textNULL
some textNULL
Eval 2:
some textNULL
some textNULL
some textNULL
some text<?phpNULL
some textNULL
some text<?php;NULL
some textNULL
some textNULL
Eval 3:
some text<?php;NULL
some text<?php;NULL
bool(false)
some text<?php<?php;NULL
bool(false)
some text<?php;<?php;NULL
some text<?php;NULL
some text<?php;NULL
Some thoughts about the solutions above:
Temporary file
Don't. It's very bad for performance, just don't do it. Not only does it drive your opcode cache totally crazy (cache hit never happens + it tries to cache it again every time) but also gives you the headache of filesystem locking under high (even moderate) loads, as you have to write the file and Apache/PHP has to read it.
Simple eval()
Acceptable in rare cases; don't do it too often. Indeed it's not cached (poor opcode cache just doesn't know it's the same string as before); at the same time, if your code is changing each time, eval is A LOT BETTER than include(), mostly because include() fills up the opcode cache on each call. Just like the tempfile case. It's horrible (~4x slower).
In-memory eval()
Actually, eval is very fast when your script is already in the string; most of the time it's the disk operation that pulls it back, now surely this depends on what you do in the script but in my very-small-script case, it was ~400 times faster. (Do you have memcached? Just thinking loud) So what include() can't do is evaluate the same thing twice without file operation, and this is very important. If you use it for ever-changing, small, memory-generated strings, obviously it's eval to choose - it's many-many times faster to load once + eval again and again than an iterated include().
TL;DR
Same code, once per request: include
Same code, several calls per request: eval
Varying code: eval
here is my approach.
it creates temporary php file and includes it.
but this way if code you want to run on this function has errors program exits before removing temporary file
so i make an autoclean procedure in function. this way it cleans old temporary files by an timeout everytime function runs. you can set timeout or disable it from options at start of function
i also added ignore error option for solving non removed temporary files. if errors ignored, program will continue and remove temporary file.
also some projects have to disable autoclean because it scans whole directory everytime it runs. it could hurt disk performance.
function eval2($c) {
$auto_clean_old_temporary_files=false; //checks old temporary eval2 files for this spesific temporary file names generated by settings below
$ignore_all_errors=true; //if you ignore errors you can remove temporary files even there is an error
$tempfiledirectory=''; //temporary file directory
$tempfileheader='eval2_'; // temporary file header
$tempfiletimeseperator='__'; // temporary file seperator for time
$tempfileremovetimeout=200; // temp file cleaning time in seconds
if ($auto_clean_old_temporary_files===true) {
$sd=scandir('.'); //scaning for old temporary files
foreach ($sd as $sf) {
if (strlen($sf)>(32+strlen($tempfileheader)+strlen($tempfiletimeseperator)+3)) { // if filename long enough
$t1=substr($sf,(32+strlen($tempfileheader)),strlen($tempfiletimeseperator)); //searching time seperator
$t2=substr($sf,0,strlen($tempfileheader)); //searching file header
if ($t1==$tempfiletimeseperator && $t2==$tempfileheader) { //checking for timeseperator and file name header
$ef=explode('.',$sf);
unset($ef[count($ef)]);//removing file extension
$nsf=implode('.',$ef);//joining file name without extension
$ef=explode($tempfiletimeseperator,$nsf);
$tm=(int)end($ef); //getting time from filename
$tmf=time()-$tm;
if ($tmf>$tempfileremovetimeout && $tmf<123456 && $tmf>0) { // if time passed more then timeout and difference with real time is logical
unlink($sf); // finally removing temporary file
}
}
}
}
}
$n=$tempfiledirectory.$tempfileheader . md5(microtime().rand(0,5000)). $tempfiletimeseperator . time() .'.php'; //creating spesific temporary file name
$c='<?php' . PHP_EOL . $c . PHP_EOL; //generating php content
file_put_contents($n,$c); //creating temporary file
if ($ignore_all_errors===true) { // including temporary file by your choise
$s=#include($n);
}else{
$s=include($n);
}
return $s;
}

Categories