24

Trying to write a couple of functions that will encrypt or decrypt a file and am using the class found here to try and accomplish this:

http://www.itnewb.com/v/PHP-Encryption-Decryption-Using-the-MCrypt-Library-libmcrypt

The encryption function below seems to work, in that it appears to encrypt the file and place it in the intended directory. I'm trying to decrypt the file now, and it just dies with the message "Failed to complete decryption" (which is coded in there...) There's nothing in the php error logs, so I'm not sure why it's failing, but as mcrypt is entirely new to me, I'm more than inclined to believe I'm doing something wrong here...

Here are the functions:

//ENCRYPT FILE
    function encryptFile() {
        global $cryptastic;
        $pass = PGPPASS;
        $salt = PGPSALT;
        $key = $cryptastic->pbkdf2($pass, $salt, 1000, 32) or die("Failed to generate secret key.");

        if ($handle = opendir(PATH.'/ftpd')) {
            while (false !== ($file = readdir($handle))) {
                if ($file != "." && $file != "..") {
                    $newfile = PATH.'/encrypted/'.$file.'.txt';
                    $msg = file_get_contents(PATH.'/ftpd/'.$file);
                    $encrypted = $cryptastic->encrypt($msg, $key) or die("Failed to complete encryption.");
                    $nfile = fopen($newfile, 'w');
                    fwrite($nfile, $encrypted);
                    fclose($nfile);
                    unlink(PATH.'/ftpd/'.$file);

                }
            }
            closedir($handle);
        }       


//DECRYPT FILE
    function inFTP() {
        global $cryptastic;
        $pass = PGPPASS;
        $salt = PGPSALT;
        $key = $cryptastic->pbkdf2($pass, $salt, 1000, 32) or die("Failed to generate secret key.");

        if ($handle = opendir(PATH.'/encrypted')) {
            while (false !== ($file = readdir($handle))) {
                if ($file != "." && $file != "..") {
                    $newfile = PATH.'/decrypted/'.$file;
                    $msg = PATH.'/encrypted/'.$file;
                    $decrypted = $cryptastic->decrypt($msg, $key) or die("Failed to complete decryption.");
                    $nfile = fopen($newfile, 'w');
                    fwrite($nfile, $decrypted);
                    fclose($nfile);
                    //unlink(PATH.'/encrypted/'.$file);

                }
            }
            closedir($handle);
        }       
        //$crypt->decrypt($file);
    }
jww
  • 97,681
  • 90
  • 411
  • 885
whitman6732
  • 465
  • 3
  • 8
  • 12

4 Answers4

57

Since mcrypt is abandonware and no longer recommended to be used, here's an example using openssl.

class AES256Encryption
{
    public const BLOCK_SIZE = 8;
    public const IV_LENGTH = 16;
    public const CIPHER = 'AES256';

    public static function generateIv(bool $allowLessSecure = false): string
    {
        $success = false;
        $random = openssl_random_pseudo_bytes(openssl_cipher_iv_length(static::CIPHER));
        if (!$success) {
            if (function_exists('sodium_randombytes_random16')) {
                $random = sodium_randombytes_random16();
            } else {
                try {
                    $random = random_bytes(static::IV_LENGTH);
                }
                catch (Exception $e) {
                    if ($allowLessSecure) {
                        $permitted_chars = implode(
                            '',
                            array_merge(
                                range('A', 'z'),
                                range(0, 9),
                                str_split('~!@#$%&*()-=+{};:"<>,.?/\'')
                            )
                        );
                        $random = '';
                        for ($i = 0; $i < static::IV_LENGTH; $i++) {
                            $random .= $permitted_chars[mt_rand(0, (static::IV_LENGTH) - 1)];
                        }
                    }
                    else {
                        throw new RuntimeException('Unable to generate initialization vector (IV)');
                    }
                }
            }
        }
        return $random;
    }

    protected static function getPaddedText(string $plainText): string
    {
        $stringLength = strlen($plainText);
        if ($stringLength % static::BLOCK_SIZE) {
            $plainText = str_pad($plainText, $stringLength + static::BLOCK_SIZE - $stringLength % static::BLOCK_SIZE, "\0");
        }
        return $plainText;
    }

    public static function encrypt(string $plainText, string $key, string $iv): string
    {
        $plainText = static::getPaddedText($plainText);
        return base64_encode(openssl_encrypt($plainText, static::CIPHER, $key, OPENSSL_RAW_DATA, $iv));
    }

    public static function decrypt(string $encryptedText, string $key, string $iv): string
    {
        return openssl_decrypt(base64_decode($encryptedText), static::CIPHER, $key, OPENSSL_RAW_DATA, $iv);
    }
}

$text = '8SViI0Gz4r-p7A15YxkwjOBFuW*@NTtbm{U]D&E=~6yLM+adX'P;h3$,KJ%/eo>}<Rs:2#gZ.9fqn"Cv_^[(H\c!)?`Ql';
$key = 'secretkey';
$iv = AES256Encryption::generateIv();
$encryptedText = AES256Encryption::encrypt($text, $key, $iv);
$decryptedText = AES256Encryption::decrypt($encryptedText, $key, $iv);

printf('Original Text: %s%s', $text, PHP_EOL);
printf('Encrypted: %s%s', $encryptedText, PHP_EOL);
printf('Decrypted: %s%s', $decryptedText, PHP_EOL);

Output:

// Long string with lots of different characters
Original Text: 8SViI0Gz4r-p7A15YxkwjOBFuW*@NTtbm{U]D&E=~6yLM+adX'P;h3$,KJ%/eo>}<Rs:2#gZ.9fqn"Cv_^[(H\c!)?`Ql
Encrypted    : rsiF4PMCMyvAp+CTuJrxJYGoV4BSy8Fy+q+FL8m64+Mt5V3o0HS0elRkWXsy+//hPjzNhjmVktxVvMY55Negt4DyLcf2QpH05wUX+adJDe634J/9fWd+nlEFoDutXuhY+/Kep9zUZFDmLmszJaBHWQ==
Decrypted    : 8SViI0Gz4r-p7A15YxkwjOBFuW*@NTtbm{U]D&E=~6yLM+adX'P;h3$,KJ%/eo>}<Rs:2#gZ.9fqn"Cv_^[(H\c!)?`Ql 

Old Answer

Try this PHP5 class for encryption using mcrypt. In this case it's using AES encryption. You'll want to change the key for each site you use it on. If you don't use it at least it may guide you on writing your own version of it.

<?php

class Encryption
{
    const CIPHER = MCRYPT_RIJNDAEL_128; // Rijndael-128 is AES
    const MODE   = MCRYPT_MODE_CBC;

    /* Cryptographic key of length 16, 24 or 32. NOT a password! */
    private $key;
    public function __construct($key) {
        $this->key = $key;
    }

    public function encrypt($plaintext) {
        $ivSize = mcrypt_get_iv_size(self::CIPHER, self::MODE);
        $iv = mcrypt_create_iv($ivSize, MCRYPT_DEV_URANDOM);
        $ciphertext = mcrypt_encrypt(self::CIPHER, $this->key, $plaintext, self::MODE, $iv);
        return base64_encode($iv.$ciphertext);
    }

    public function decrypt($ciphertext) {
        $ciphertext = base64_decode($ciphertext);
        $ivSize = mcrypt_get_iv_size(self::CIPHER, self::MODE);
        if (strlen($ciphertext) < $ivSize) {
            throw new Exception('Missing initialization vector');
        }

        $iv = substr($ciphertext, 0, $ivSize);
        $ciphertext = substr($ciphertext, $ivSize);
        $plaintext = mcrypt_decrypt(self::CIPHER, $this->key, $ciphertext, self::MODE, $iv);
        return rtrim($plaintext, "\0");
    }
}

Usage:

$key = /* CRYPTOGRAPHIC!!! key */;
$crypt = new Encryption($key);
$encrypted_string = $crypt->encrypt('this is a test');
$decrypted_string = $crypt->decrypt($encrypted_string); // this is a test

Notes:

  • This class is not safe for use with binary data (which may end in NUL bytes)
  • This class does not provide authenticated encryption.
John Conde
  • 217,595
  • 99
  • 455
  • 496
  • 3
    For binary data I think that you have to base64_encode it before encription – DaGhostman Dimitrov Dec 23 '12 at 10:30
  • 4
    That's correct. I ran this class on text files, and it worked great. For binary files it is necessary to encode the information before encrypting it. If the files are larger than 100MB or so, base64_encode will cause performance issues, so you may want to consider splitting the files into chunks for encrypting. From a security standpoint this isn't an ideal solution because it provides more opportunity for recovering partial plaintext. But, it works. – John Poulin Feb 21 '13 at 19:46
  • 1
    Why did you change to AES instead of Blowfish? – Lars Gyrup Brink Nielsen Sep 06 '13 at 12:04
  • Using `MCRYPT_DEV_RANDOM` would be faster correct (avail on unix systems)? – rynop Sep 20 '13 at 20:31
  • Just to clarify my recent edit of @John's great answer: 1.) According to the Usage example the methods should be declared 'static' 2.) Class constants in PHP are always public and PHP doesn't support the concept of private constants. So private static variables are a better way. – jaltek Feb 24 '14 at 16:19
  • I took the freedom of making this code actually use AES (no, Rijndael 256 is not AES 256), use a suitable source of randomness for the IV, properly trim the plaintext after decryption and not use the horrendous mcrypt_generic APIs. Also added notes that this is not binary safe (no PKCS#7 padding) and not authenticated (no HMAC). – NikiC Mar 24 '14 at 16:50
  • Newbie question about this sort of thing: Can I safely reuse $crypt, or should I regenerate it each time I want to call $crypt->encrypt? I'm assuming it's reusable, but I want to avoid doing something stupid. – Jim Miller Mar 29 '14 at 18:51
  • 2
    Note to @rynop: I think you meant to say MCRYPT_DEV_URANDOM(? or are we out of sync with edits?). I started using this class with MCRYPT_DEV_RANDOM and encountered a lot of performance issues; switching to MCRYPT_DEV_URANDOM is working much better for me (at some cost in randomness quality, but I think I can live with it. – Jim Miller Apr 04 '14 at 01:36
  • 6
    Can we add a strong disclaimer? Authenticated encryption is **absolutely essential** to defend against active attackers. There's really no way to negotiate it away unless you cripple your threat model below what the average script kiddie can pull off, which helps approximately no one. – Scott Arciszewski Aug 11 '15 at 23:27
  • Just want to echo what Jim Miller said so others see it-- Use MCRYPT_DEV_URANDOM, **NOT** DEV_RANDOM as is stated in the answer. DEV_RANDOM will block execution until entropy accumulates. Sometimes your script will take 1 ms to run, sometimes 100 seconds! Use URANDOM instead. – Richard Mar 09 '16 at 22:10
  • @ScottArciszewski Authenticated encryption is not necessary. It is a precaution to prevent attackers from having any possibility of gathering information about the encryption algorithm, but there are many strong encryption algorithms that are unbreakable and will be for as long as we live, but AE probably is a necessary for a cryptoholic. – Motomotes Dec 09 '16 at 17:32
  • 4
    "Authenticated encryption is not necessary." [Wrong](http://robertheaton.com/2013/07/29/padding-oracle-attack/). [Super wrong](https://tonyarcieri.com/all-the-crypto-code-youve-ever-written-is-probably-broken). Authenticated encryption is no longer negotiable. – Scott Arciszewski Dec 09 '16 at 17:34
  • 3
    @Motes Authenticated encryption should always be a requirement. Without it, your implementation falls victim to chosen ciphertext attacks. – Aaron Toponce Dec 09 '16 at 17:44
  • 3
    @LayZee It's 2016. No one should be using 64-bit block cipher modes to encrypt data. See https://sweet32.info/ for a great explanation as to why. – Aaron Toponce Dec 09 '16 at 17:46
  • @ScottArciszewski lol, the oracle padding attack requires knowing the key. You literally have to have code that knows the key and will respond to you with error information. Allowing code that knows the key to be used by unauthorized users is the issue there. – Motomotes Dec 09 '16 at 18:06
  • 1
    @Motes "lol, the oracle padding attack requires knowing the key." No, it doesn't. If you had the key, you wouldn't need an attack. – Scott Arciszewski Dec 09 '16 at 19:39
  • 1
    @Aaron Toponce there's more to it than block size. It depends on the amounts of data that are encrypted and when and how much they are used. The birthday bound described in your link refers to SSL over HTTP where you would *only* need 32 GB of data to exploit data encrypted with 64 bit blocks. See http://crypto.stackexchange.com/a/5771 – Lars Gyrup Brink Nielsen Dec 09 '16 at 20:32
  • @LayZee Sweet32 is a proof-of-concept on attacking 64-bit block ciphers on the wire, you are correct. However, birthday attacks are applied to any blind brute force scenario, not just SSL over HTTP. There are 2^64 possibilities to arrange bits in Blowfish. The birthday attack guarantees I can find a 64-bit block correctly with a 50% probability by only searching 2^32 combinations. Distributed.net successfully found a 64-bit key to decrypt an RC5 ciphertext in ~4.7 years using distributed clients at a rate of about 100 billion keys per second. The birthday bound on 64-bits is clearly practical. – Aaron Toponce Dec 09 '16 at 22:39
2

While Johns answer is good, using base64 encoding just to fix the binary safety issue is overkill and will make your encrypted files 33% larger than the original. Here is my PHP Implementation of the AES Crypt file format which solves all the above issues transparently.

https://github.com/philios33/PHP-AES-File-Encryption

It is binary safe and includes authenticated encryption. Since it uses the open source aes crypt file format (.aes) it is fully compatible with other .aes software.

https://www.aescrypt.com/

The interface is pretty simple whether you are encrypting or decrypting. You just give it a source file and password.

Phil
  • 1,996
  • 1
  • 19
  • 26
2

You should not be using Mcrypt to encrypt/decrypt data. As shown in your question, and in the accepted answer, the data is not authenticated, which means it will fall victim to chosen ciphertext attacks.

Further, a great deal of effort has been done to make sure that developers put together cryptographic primitives correctly. As such, instead of Mcrypt, you should be using libsodium for your PHP projects. libsodium is a fork of NaCl. NaCl/libsodium is written to remove a lot of the cryptographic pitfalls that developers find themselves in, such as timing attacks with verification of MAC tags.

Mcrypt is deprecated in PHP 7.1, and libsodim is the preferred way to handle cryptography in PHP.

Using libsodium in your PHP project is easy, and secure. Scott Arciszewski has written an extensive ebook on using libsodium with PHP at https://paragonie.com/book/pecl-libsodium. It's worth the read for anyone doing PHP cryptography.

Aaron Toponce
  • 547
  • 5
  • 9
1

CakePHP has a pretty good implementation of rijndael. I'm not posting code directly here because not sure the legal ramifications.

Here are the api docs for the Security::rijndael() method.

If encoding a file, you will want to base64_encode() before calling this method with 'encrypt', and base64_decode() after calling this method with 'decrypt'

rynop
  • 50,086
  • 26
  • 101
  • 112