119

I'm currently a student and I'm studying PHP, I'm trying to make a simple encrypt/decrypt of data in PHP. I made some online research and some of them were quite confusing(at least for me).

Here's what I'm trying to do:

I have a table consisting of these fields (UserID,Fname,Lname,Email,Password)

What I want to have is have the all fields encrypted and then be decrypted(Is it possible to use sha256 for encryption/decryption, if not any encryption algorithm)

Another thing I want to learn is how to create a one way hash(sha256) combined with a good "salt". (Basically I just want to have a simple implementation of encryption/decryption, hash(sha256)+salt) Sir/Ma'am, your answers would be of great help and be very much appreciated. Thank you++

Scott Arciszewski
  • 33,610
  • 16
  • 89
  • 206
Randel Ramirez
  • 3,671
  • 20
  • 49
  • 63
  • 3
    [The horrors of Sha-1 encryption!](http://thenextweb.com/socialmedia/2012/06/06/bad-day-for-linkedin-6-5-million-hashed-passwords-reportedly-leaked-change-yours-now/) – Naftali Jun 06 '12 at 14:30
  • http://php.net/manual/en/faq.passwords.php – Steve Ross Jun 06 '12 at 14:30
  • 9
    SHA is a hash, not encryption. The key point is that a hash cannot be reversed to the original data (not easily, anyway). You probably want [mcrypt](http://php.net/manual/en/book.mcrypt.php) or if it is not available I would recommend [phpseclib](http://phpseclib.sourceforge.net/) - although it's important to note that any pure-PHP implementation of anything that involves a lot of low-level mathematics will be sloooooowww... That's why I like phpseclib, because it uses mcrypt first if it's available and only falls back to PHP implementations as a last resort. – DaveRandom Jun 06 '12 at 14:38
  • 7
    You normally don't want to be able to decrypt a password! – Ja͢ck Jun 06 '12 at 15:20
  • 1
    Basically you should not think of encryption at this level, you should think about access control, confidentiality, integrity and authentication. After that check how you can achieve this, *possibly* using encryption or secure hashing. You might want to read into PBKDF2 and bcrypt/scrypt to understand secure hashing of passwords and the like. – Maarten Bodewes Jun 07 '12 at 21:43
  • You could use PHP's `password_hash` and `password_verify` functions to automatically salt and hash your passwords. You don't want your passwords to be decryptable. Just hash them and use that. Effectively destroys the original password so nobody knows what it is, but still enables checking of passwords by comparing the input password with the stored hash using `password_verify`. Nice. Not sure about encryption... that's why I'm here ! :-D –  Apr 24 '18 at 09:32

6 Answers6

299

Foreword

Starting with your table definition:

- UserID
- Fname
- Lname
- Email
- Password
- IV

Here are the changes:

  1. The fields Fname, Lname and Email will be encrypted using a symmetric cipher, provided by OpenSSL,
  2. The IV field will store the initialisation vector used for encryption. The storage requirements depend on the cipher and mode used; more about this later.
  3. The Password field will be hashed using a one-way password hash,

Encryption

Cipher and mode

Choosing the best encryption cipher and mode is beyond the scope of this answer, but the final choice affects the size of both the encryption key and initialisation vector; for this post we will be using AES-256-CBC which has a fixed block size of 16 bytes and a key size of either 16, 24 or 32 bytes.

Encryption key

A good encryption key is a binary blob that's generated from a reliable random number generator. The following example would be recommended (>= 5.3):

$key_size = 32; // 256 bits
$encryption_key = openssl_random_pseudo_bytes($key_size, $strong);
// $strong will be true if the key is crypto safe

This can be done once or multiple times (if you wish to create a chain of encryption keys). Keep these as private as possible.

IV

The initialisation vector adds randomness to the encryption and required for CBC mode. These values should be ideally be used only once (technically once per encryption key), so an update to any part of a row should regenerate it.

A function is provided to help you generate the IV:

$iv_size = 16; // 128 bits
$iv = openssl_random_pseudo_bytes($iv_size, $strong);

Example

Let's encrypt the name field, using the earlier $encryption_key and $iv; to do this, we have to pad our data to the block size:

function pkcs7_pad($data, $size)
{
    $length = $size - strlen($data) % $size;
    return $data . str_repeat(chr($length), $length);
}

$name = 'Jack';
$enc_name = openssl_encrypt(
    pkcs7_pad($name, 16), // padded data
    'AES-256-CBC',        // cipher and mode
    $encryption_key,      // secret key
    0,                    // options (not used)
    $iv                   // initialisation vector
);

Storage requirements

The encrypted output, like the IV, is binary; storing these values in a database can be accomplished by using designated column types such as BINARY or VARBINARY.

The output value, like the IV, is binary; to store those values in MySQL, consider using BINARY or VARBINARY columns. If this is not an option, you can also convert the binary data into a textual representation using base64_encode() or bin2hex(), doing so requires between 33% to 100% more storage space.

Decryption

Decryption of the stored values is similar:

function pkcs7_unpad($data)
{
    return substr($data, 0, -ord($data[strlen($data) - 1]));
}

$row = $result->fetch(PDO::FETCH_ASSOC); // read from database result
// $enc_name = base64_decode($row['Name']);
// $enc_name = hex2bin($row['Name']);
$enc_name = $row['Name'];
// $iv = base64_decode($row['IV']);
// $iv = hex2bin($row['IV']);
$iv = $row['IV'];

$name = pkcs7_unpad(openssl_decrypt(
    $enc_name,
    'AES-256-CBC',
    $encryption_key,
    0,
    $iv
));

Authenticated encryption

You can further improve the integrity of the generated cipher text by appending a signature that's generated from a secret key (different from the encryption key) and the cipher text. Before the cipher text is decrypted, the signature is first verified (preferably with a constant-time comparison method).

Example

// generate once, keep safe
$auth_key = openssl_random_pseudo_bytes(32, $strong);

// authentication
$auth = hash_hmac('sha256', $enc_name, $auth_key, true);
$auth_enc_name = $auth . $enc_name;

// verification
$auth = substr($auth_enc_name, 0, 32);
$enc_name = substr($auth_enc_name, 32);
$actual_auth = hash_hmac('sha256', $enc_name, $auth_key, true);

if (hash_equals($auth, $actual_auth)) {
    // perform decryption
}

See also: hash_equals()

Hashing

Storing a reversible password in your database must be avoided as much as possible; you only wish to verify the password rather than knowing its contents. If a user loses their password, it's better to allow them to reset it rather than sending them their original one (make sure that password reset can only be done for a limited time).

Applying a hash function is a one-way operation; afterwards it can be safely used for verification without revealing the original data; for passwords, a brute force method is a feasible approach to uncover it due to its relatively short length and poor password choices of many people.

Hashing algorithms such as MD5 or SHA1 were made to verify file contents against a known hash value. They're greatly optimized to make this verification as fast as possible while still being accurate. Given their relatively limited output space it was easy to build a database with known passwords and their respective hash outputs, the rainbow tables.

Adding a salt to the password before hashing it would render a rainbow table useless, but recent hardware advancements made brute force lookups a viable approach. That's why you need a hashing algorithm that's deliberately slow and simply impossible to optimize. It should also be able to increase the load for faster hardware without affecting the ability to verify existing password hashes to make it future proof.

Currently there are two popular choices available:

  1. PBKDF2 (Password Based Key Derivation Function v2)
  2. bcrypt (aka Blowfish)

This answer will use an example with bcrypt.

Generation

A password hash can be generated like this:

$password = 'my password';
$random = openssl_random_pseudo_bytes(18);
$salt = sprintf('$2y$%02d$%s',
    13, // 2^n cost factor
    substr(strtr(base64_encode($random), '+', '.'), 0, 22)
);

$hash = crypt($password, $salt);

The salt is generated with openssl_random_pseudo_bytes() to form a random blob of data which is then run through base64_encode() and strtr() to match the required alphabet of [A-Za-z0-9/.].

The crypt() function performs the hashing based on the algorithm ($2y$ for Blowfish), the cost factor (a factor of 13 takes roughly 0.40s on a 3GHz machine) and the salt of 22 characters.

Validation

Once you have fetched the row containing the user information, you validate the password in this manner:

$given_password = $_POST['password']; // the submitted password
$db_hash = $row['Password']; // field with the password hash

$given_hash = crypt($given_password, $db_hash);

if (isEqual($given_hash, $db_hash)) {
    // user password verified
}

// constant time string compare
function isEqual($str1, $str2)
{
    $n1 = strlen($str1);
    if (strlen($str2) != $n1) {
        return false;
    }
    for ($i = 0, $diff = 0; $i != $n1; ++$i) {
        $diff |= ord($str1[$i]) ^ ord($str2[$i]);
    }
    return !$diff;
}

To verify a password, you call crypt() again but you pass the previously calculated hash as the salt value. The return value yields the same hash if the given password matches the hash. To verify the hash, it's often recommended to use a constant-time comparison function to avoid timing attacks.

Password hashing with PHP 5.5

PHP 5.5 introduced the password hashing functions that you can use to simplify the above method of hashing:

$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 13]);

And verifying:

if (password_verify($given_password, $db_hash)) {
    // password valid
}

See also: password_hash(), password_verify()

Ja͢ck
  • 170,779
  • 38
  • 263
  • 309
  • What length should I use to store name, surname, email etc for safest bet? varbinary(???) – BentCoder Sep 25 '13 at 10:24
  • @MadMax That's entirely up to you; my answer covers the different encoding you could use to store the values, whether it be binary, hex or base64 and how much storage is required per character. – Ja͢ck Sep 25 '13 at 12:01
  • I agree with @MaartenBodewes that your ciphertexts should be authenticated (e.g. HMAC) and verified in constant-time. – Scott Arciszewski May 11 '15 at 10:04
  • @Scott For transport protocols I would use TLS, which is a good standard for transport encryption; for the in-place encryption scenario you could still use HMAC, but a) that's a separate concern (as mentioned earlier) and b) you wouldn't need constant-time verification. – Ja͢ck May 11 '15 at 10:17
  • 2
    Sure, but it depends on how it's being used. If you publish an encryption library, you don't know how developers will implement it. That's why https://github.com/defuse/php-encryption provides authenticated symmetric-key encryption and doesn't let developers weaken it without editing its code. – Scott Arciszewski May 11 '15 at 10:18
  • 2
    @Scott Very well, I've added an example of authenticated encryption; thanks for the push :) – Ja͢ck May 11 '15 at 10:45
  • 1
    +1 for authenticated encryption. There's not enough information in the question to say that AE isn't necessary here. Certainly SQL traffic often goes over a network with unknown security properties, as does traffic from database to storage. Backups and replication too. What's the threat model? The question doesn't say, and it might be dangerous to make assumptions. – Jason Orendorff May 12 '15 at 20:49
  • @JasonOrendorff I'm not exactly sure what point you're trying to make; it should be obvious that AE is supplementary. – Ja͢ck May 13 '15 at 02:44
  • @Ja͢ck it might be prudent to [stress the importance of authenticated encryption](https://news.ycombinator.com/item?id=9865276). – Scott Arciszewski Aug 03 '15 at 23:44
  • How can you select specific rows in the database when an IV is used? Is this possible? – cid Apr 16 '16 at 22:26
  • 1
    Instead of hard-coding`$iv_size = 16;`, I would use: `$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length("AES-256-CBC"))` in order to indicate the link between iv's size to use with the cipher used. You could also expand a bit on the need (or not) of `pkcs7_pad()`/`pkcs7_unpad()`, or just simplify the post by getting rid of them and use "aes-256-ctr". Great post @Ja͢ck – Patrick Allaert Dec 14 '16 at 17:53
  • Hi, I know this is an old post but it is relevant to me at this time. I have tried the solution and I'm VERY impressed! But can someone tell me under what circumstances - "$encryption_key = openssl_random_pseudo_bytes($key_size, $strong); // $strong will be true if the key is crypto safe" Would not be strong and what to do if it was not - thanks – kerry Oct 22 '20 at 12:04
  • @kerry if it’s not, then you may need to run it again or give up – Ja͢ck Oct 22 '20 at 12:30
24

I'm think this has been answered before...but anyway, if you want to encrypt/decrypt data, you can't use SHA256

//Key
$key = 'SuperSecretKey';

//To Encrypt:
$encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, 'I want to encrypt this', MCRYPT_MODE_ECB);

//To Decrypt:
$decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $encrypted, MCRYPT_MODE_ECB);
romo
  • 1,990
  • 11
  • 10
14

Answer Background and Explanation

To understand this question, you must first understand what SHA256 is. SHA256 is a Cryptographic Hash Function. A Cryptographic Hash Function is a one-way function, whose output is cryptographically secure. This means it is easy to compute a hash (equivalent to encrypting data), but hard to get the original input using the hash (equivalent to decrypting the data). Since using a Cryptographic hash function means decrypting is computationally infeasible, so therefore you cannot perform decryption with SHA256.

What you want to use is a two-way function, but more specifically, a Block Cipher. A function that allows for both encryption and decryption of data. The functions mcrypt_encrypt and mcrypt_decrypt by default use the Blowfish algorithm. PHP's use of mcrypt can be found in this manual. A list of cipher definitions to select the cipher mcrypt uses also exists. A wiki on Blowfish can be found at Wikipedia. A block cipher encrypts the input in blocks of known size and position with a known key, so that the data can later be decrypted using the key. This is what SHA256 cannot provide you.

Code

$key = 'ThisIsTheCipherKey';

$ciphertext = mcrypt_encrypt(MCRYPT_BLOWFISH, $key, 'This is plaintext.', MCRYPT_MODE_CFB);

$plaintext = mcrypt_decrypt(MCRYPT_BLOWFISH, $key, $encrypted, MCRYPT_MODE_CFB);
Zoe
  • 27,060
  • 21
  • 118
  • 148
cytinus
  • 5,467
  • 8
  • 36
  • 47
  • You should not use ECB either, for that matter. – Maarten Bodewes Jun 07 '12 at 21:33
  • Keys should be random bytes, or you should use a secure key derivation function. – Maarten Bodewes Jun 07 '12 at 21:36
  • 4
    **Never ever** use the ECB mode. It is insecure and most of the time doesn't really help in actually encrypting the data (rather than just encoding it). See the [excellent Wikipedia article on the subject](http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation#Electronic_codebook_.28ECB.29) for more information. – Holger Just Jun 08 '12 at 06:18
  • 1
    It is best not to use mcrypt, it is abandonware, has not been updated in years and does not support standard PKCS#7 (née PKCS#5) padding, only non-standard null padding that can't even be used with binary data. mcrypt had many outstanding [bugs](https://sourceforge.net/p/mcrypt/bugs/) dating back to 2003.. Instead consider using [defuse](https://github.com/defuse/php-encryption), it is being maintained and is correct. – zaph May 19 '16 at 21:44
10
     function my_simple_crypt( $string, $action = 'e' ) {
        // you may change these values to your own
        $secret_key = 'my_simple_secret_key';
        $secret_iv = 'my_simple_secret_iv';

        $output = false;
        $encrypt_method = "AES-256-CBC";
        $key = hash( 'sha256', $secret_key );
        $iv = substr( hash( 'sha256', $secret_iv ), 0, 16 );

        if( $action == 'e' ) {
            $output = base64_encode( openssl_encrypt( $string, $encrypt_method, $key, 0, $iv ) );
        }
        else if( $action == 'd' ){
            $output = openssl_decrypt( base64_decode( $string ), $encrypt_method, $key, 0, $iv );
        }

        return $output;
    }
Gauravbhai Daxini
  • 2,032
  • 2
  • 22
  • 28
9

Here is an example using openssl_encrypt

//Encryption:
$textToEncrypt = "My Text to Encrypt";
$encryptionMethod = "AES-256-CBC";
$secretHash = "encryptionhash";
$iv = mcrypt_create_iv(16, MCRYPT_RAND);
$encryptedText = openssl_encrypt($textToEncrypt,$encryptionMethod,$secretHash, 0, $iv);

//Decryption:
$decryptedText = openssl_decrypt($encryptedText, $encryptionMethod, $secretHash, 0, $iv);
print "My Decrypted Text: ". $decryptedText;
Vivek
  • 168
  • 2
  • 7
  • 2
    Instead of `mcrypt_create_iv()`, I would use: `openssl_random_pseudo_bytes(openssl_cipher_iv_length($encryptionMethod))`, this way the methodology works for any value of $encryptionMethod and would use openssl extension only. – Patrick Allaert Dec 14 '16 at 17:39
  • The code above returns `false` for `openssl_decrypt()`. See https://stackoverflow.com/q/41952509/1066234 *Since block ciphers such as AES require input data to be an exact multiple of the block size (16-bytes for AES) padding is necessary.* – Avatar Sep 08 '17 at 20:22
0

It took me quite a while to figure out, how to not get a false when using openssl_decrypt() and get encrypt and decrypt working.

    // cryptographic key of a binary string 16 bytes long (because AES-128 has a key size of 16 bytes)
    $encryption_key = '58adf8c78efef9570c447295008e2e6e'; // example
    $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc'));
    $encrypted = openssl_encrypt($plaintext, 'aes-256-cbc', $encryption_key, OPENSSL_RAW_DATA, $iv);
    $encrypted = $encrypted . ':' . base64_encode($iv);

    // decrypt to get again $plaintext
    $parts = explode(':', $encrypted);
    $decrypted = openssl_decrypt($parts[0], 'aes-256-cbc', $encryption_key, OPENSSL_RAW_DATA, base64_decode($parts[1])); 

If you want to pass the encrypted string via a URL, you need to urlencode the string:

    $encrypted = urlencode($encrypted);

To better understand what is going on, read:

To generate 16 bytes long keys you can use:

    $bytes = openssl_random_pseudo_bytes(16);
    $hex = bin2hex($bytes);

To see error messages of openssl you can use: echo openssl_error_string();

Hope that helps.

Avatar
  • 14,622
  • 9
  • 119
  • 198