PHP Cookbook/Encryption and Security

From WikiContent

< PHP Cookbook
Revision as of 22:30, 6 March 2008 by Evanlenz (Talk | contribs)
Jump to: navigation, search
PHP Cookbook


Contents

Introduction

In a perfect world, encryption wouldn't be necessary. Nosy people would keep their eyes on their own data, and a credit card number floating around the Internet would attract no special attention. In so many ways, however, our world isn't perfect, so we need encryption.

Encryption scrambles data. Some data scrambling can't be unscrambled without unreasonable amounts of processing. This is called one-way encryption . Other encryption methods work in two directions: data is encrypted; then it's decrypted.

PHP supplies tools to encrypt and secure your data. Some tools, such as the crypt( ) and md5( ) functions, are part of PHP's base set of functions, and some are extensions that need to be explicitly included when PHP is compiled (e.g., mcrypt, mhash, and cURL).

The crypt( ) function does one-way DES encryption using the first eight characters of plaintext to calculate the ciphertext. You pass it the plaintext to encrypt (and a salt, which strengthens the encryption), and it returns the encrypted ciphertext. PHP generates a random salt if you don't supply one:

print crypt('shrimp','34');
34b/4qaoXmcoY
         

If the constant CRYPT_MD5 is set to 1, crypt( ) can do MD5 encryption. To tell PHP to use MD5 encryption, start the salt with $1$:

print crypt('shrimp','$1$seasalt!');
$1$seasalt!$C8bRD475BC3T4EvjjmR9I.

Recipe 14.5 discusses crypt( ). It is most widely used for encrypting passwords.

mcrypt is a more full-featured encryption library that offers different algorithms and encryption modes. Because it supports different kinds of encryption, mcrypt is especially helpful when you need to exchange encrypted data with other systems or with programs not written in PHP. mcrypt is discussed in detail in Recipe 14.8.

PHP gives you the tools to protect your data with robust encryption, but encryption is just part of the large and often complex security picture. Your encrypted data can be unlocked with a key, so protecting that key is very important. If your encryption keys are accessible to unauthorized users (because they're stored in a file accessible via your web server or because they're stored in a file accessible by other users in a shared hosting environment, for example), your data is at risk, no matter how airtight your chosen encryption algorithm is.

You need to determine how secure you want your data to be. Encrypting it is more secure but more complex. Simpler encoding hides your data from elementary prying eyes but offers less security. No encryption or security is absolute. Picking an appropriate security method means finding a place on the spectrum between convenience and protection. The more convenient (or computationally inexpensive) types of security generally provide less protection. Sometimes your goal isn't to protect data from prying eyes but to avoid the appearance of impropriety. Seeing a plaintext field in a form (or URL) named "Password" could be more disturbing to your users than the same data wrapped in Base64 encoding. Recipe 14.3 shows how to obscure data with Base64.

Sensitive data needs to be protected not just on the server but also when it's traveling on the network between your server and your users. Data sent over regular HTTP is visible to anybody with access to the network at any point between your server and a user. Recipe 14.11 discusses how to layer HTTP over SSL to prevent network snoopers from peeping at data as it passes by.

There are plenty of nontechnical prerequisites to tight security. Assigning passwords that are a random jumble of letters, numbers, and punctuation does no good if those passwords are so hard to remember that users write them on sticky notes attached to their monitors. As we have already said, security is not an absolute, but a tradeoff between convenience and protection. As you use the recipes in this chapter to protect your data, decide what is an acceptable risk for your data versus the corresponding appropriate level of inconvenience that security introduces.[1]

Keeping Passwords Out of Your Site Files

Problem

You need to use a password to connect to a database, for example. You don't want to put the password in the PHP files you use on your site in case those files are compromised.

Solution

Store the password in an environment variable in a file that the web server loads when starting up; then, just reference the environment variable in your script:

mysql_connect('localhost',$_SERVER['MYSQL_USER'],$_SERVER['MYSQL_PASSWORD']);

Discussion

While this technique removes passwords from the source code of your pages, it does make them available in other places that need to be protected. Most importantly, make sure that there are no publicly viewable pages that call phpinfo( ) . Because phpinfo( ) displays environment variables available to scripts, it displays the passwords put into environment variables.

Next, especially if you are in a shared hosting setup, make sure that the environment variables are set in such a way that they are available only to your virtual host, not to all shared hosting users. With Apache, you can do this by setting the variables in a separate file from the main configuration file:

SetEnv  MYSQL_USER     "susannah"
SetEnv  MYSQL_PASSWORD "y23a!t@ce8"

Inside the <VirtualHost> directive for the site in the main configuration file, include this separate file as follows:

Include "/usr/local/apache/database-passwords"

Make sure that the separate file that contains the passwords (e.g., /usr/local/apache/database-passwords) is not readable by any users other than the one that controls the appropriate virtual host. When Apache starts up and is reading in configuration files, it's usually running as root, so it is able to read the included file.

See Also

Documentation on Apache's Include directive at http://httpd.apache.org/docs/mod/core.html#include.

Obscuring Data with Encoding

Problem

You want to prevent data being viewable as plaintext. For example, you don't want hidden form data to be revealed simply by someone viewing the source code of a web page.

Solution

Encode the data with base64_encode( ) :

$personal_data = array('code' => 5123, 'blood_type' => 'O');
$info = base64_encode(serialize($personal_data));
print '<input type="hidden" name="info" value="'.$info.'">';
<input type="hidden" name="info"
               value="YToyOntzOjQ6ImNvZGUiO2k6NTEyMztzOjEwOiJibG9vZF90eXBlIjtzOjE6Ik8iO30=">
            

Decode the data with base64_decode( ) :

$personal_data = unserialize(base64_decode($_REQUEST['info']));
get_transfusion($personal_data['blood_type']);

Discussion

The Base64 algorithm encodes data as a string of letters, numbers, and punctuation marks. This makes it ideal for transforming binary data into a plaintext form and also for obfuscating data.

See Also

Documentation on base64_encode( ) at http://www.php.net/base64-encode and base64_decode( ) at http://www.php.net/base64-decode; the Base64 algorithm is defined in RFC 2045, available at http://www.faqs.org/rfcs/rfc2045.html.

Verifying Data with Hashes

Problem

You want to make sure users don't alter data you've sent them in a cookie or form element.

Solution

Along with the data, send an MD5 hash of the data with a secret word. When you receive the data back, compute the hash of the received value with the same secret word. If they don't match, the user has altered the data.

Here's how to print a hash in a hidden form field:

$secret_word = 'flyingturtle';
$id = 2836;
$hash = md5($secret_word . $id);

print<<<_HTML_
<input type="hidden" name="id" value="$id">
<input type="hidden" name="idhash" value="$hash">
_HTML_;

Here's how to verify the hidden form field data when it's submitted:

$secret_word = 'flyingturtle';

if (md5($secret_word . $_REQUEST['id']) == $_REQUEST['idhash']) {
    $id = $_REQUEST['id'];
} else {
    die("Invalid data in $_REQUEST[id]");
}

Discussion

When processing the submitted form data, compute the hash of the submitted value of $_REQUEST['id'] and the secret word. If it matches the submitted hash, the value of $_REQUEST['id'] has not been altered by the user. If the hashes don't match, you know that the value of $_REQUEST['id'] you received is not the same as the one you sent.

To use a verification hash with a cookie, add the hash to the cookie value with join( ) :

$secret_word = 'flyingturtle';
$cookie_value = 'Ellen';
$hash = md5($secret_word . $cooki_value);

setcookie('name',join('|',array($cookie_value,$hash)));

Parse the hash from the cookie value with explode( ) :

$secret_word = 'flyingturtle';
list($cookie_value,$cookie_hash) = explode('|',$_COOKIE['name'],2);
if (md5($secret_word . $cookie_value) == $cookie_hash) {
    $name = $cookie_value;
} else {
    die('Invalid data in $_COOKIE[name]');
}

Using a data-verification hash in a form or cookie obviously depends on the secret word used in hash computation. If a malicious user discovers your secret word, the hash offers no protection. Aside from guarding the secret word zealously, changing it frequently is a good idea. For an additional layer of protection, use different secret words, choosing the specific word to use in the hash based on some property of the $id value (10 different words selected by $id%10, for example). That way, damage is controlled if one of the words is compromised.

If you have the mhash module installed, you're not limited to MD5 hashes. mhash supports a number of different hash algorithms. For more information about mhash, see the mhash material in the online PHP manual or the mhash home page at http://mhash.sourceforge.net/.

See Also

Recipe 8.11 uses a verification hash for cookie-based authentication; Recipe 9.4 for an example of using hashes with hidden form variables; documentation on md5( ) at http://www.php.net/md5 and the mhash extension at http://www.php.net/mhash.

Storing Passwords

Problem

You need to keep track of users' passwords so they can log in to your web site.

Solution

When a user signs up, encrypt her chosen password with crypt( ) and store the encrypted password in your database of users:

// encrypt the password
$encrypted_password = crypt($_REQUEST['password']);

// store $encrypted_password in the user database 
$dbh->query('INSERT INTO users (username,password) VALUES (?,?)',
            array($_REQUEST['username'],$encrypted_password));

Then, when that user attempts to log in to your web site, encrypt the password she supplies with crypt( ) and compare it to the stored encrypted password. If the two encrypted values match, she has supplied the correct password:

$encrypted_password = 
    $dbh->getOne('SELECT password FROM users WHERE username = ?',
                 array($_REQUEST['username']));

if (crypt($_REQUEST['password'],$encrypted_password) == $encrypted_password) {
  // successful login
} else {
  // unsuccessful login
}

Discussion

Storing encrypted passwords prevents users' accounts from becoming compromised if an unauthorized person gets a peek at your username and password database. (Although such unauthorized peeks may foreshadow other security problems.)

When the password is initially encrypted, crypt( ) supplies two randomly generated characters of salt that get prepended to the encrypted password. Passing $encrypted_password to crypt( ) when testing a user-supplied password tells crypt( ) to use the same salt characters again. The salt reduces your vulnerability to dictionary attacks, in which someone compares encrypted passwords with encrypted versions of common words. Still, it's a good idea to prevent users from choosing passwords that are simple words or other easier-to-crack combinations. Recipe 14.6 provides a function to filter out easily guessable passwords.

The crypt( ) function uses a one-way algorithm. This means it's currently impossible (or at least prohibitively computationally expensive) to turn a crypt( )-generated ciphertext back into plain text. This makes your stored passwords somewhat more secure, but it also means that you can't get at the plaintext of users' passwords even if you need to. So, for example, if a user forgets his password, you won't be able to tell him what it is. The best you can do is to reset the password to a new value and then tell the user the new password. A method for dealing with lost passwords is covered in Recipe 14.7.

See Also

Recipe 14.9 for information on storing encrypted data; documentation on crypt( ) at http://www.php.net/crypt.

Checking Password Strength

Problem

You want to make sure users pick passwords that are hard to guess.

Solution

Test a user's password choice with the pc_passwordcheck( ) function, shown later in Example 14-1. For example:

if ($err = pc_passwordcheck($_REQUEST['username'],$_REQUEST['password'])) {
    print "Bad password: $err";
    // Make the user pick another password
}

Discussion

The pc_passwordcheck( ) function, shown in Example 14-1, performs some tests on user-entered passwords to make sure they are harder to crack. It returns a string describing the problem if the password doesn't meet its criteria. The password must be at least six characters long and must have a mix of uppercase letters, lowercase letters, numerals, and special characters. The password can't contain the username either in regular order or reverse order. Additionally, the password can't contain a dictionary word. The filename for the word list used for dictionary checking is stored in $word_file.

The checks for the username or dictionary words in the password are also applied to a version of the password with letters substituted for lookalike numbers. For example, if the supplied password is w0rd$%, the function also checks the string word$% for the username and dictionary words. The "0" character is turned into an "o." Also, "5" is turned into "s," "3" into "e," and both "1" and "!" into "l" (el).

Example 14-1. pc_passwordcheck( )

function pc_passwordcheck($user,$pass) {
    $word_file = '/usr/share/dict/words';
    
    $lc_pass = strtolower($pass);
    // also check password with numbers or punctuation subbed for letters
    $denum_pass = strtr($lc_pass,'5301!','seoll');
    $lc_user = strtolower($user);

    // the password must be at least six characters
    if (strlen($pass) < 6) {
        return 'The password is too short.';
    }

    // the password can't be the username (or reversed username) 
    if (($lc_pass == $lc_user) || ($lc_pass == strrev($lc_user)) ||
        ($denum_pass == $lc_user) || ($denum_pass == strrev($lc_user))) {
        return 'The password is based on the username.';
    }

    // count how many lowercase, uppercase, and digits are in the password 
    $uc = 0; $lc = 0; $num = 0; $other = 0;
    for ($i = 0, $j = strlen($pass); $i < $j; $i++) {
        $c = substr($pass,$i,1);
        if (preg_match('/^[[:upper:]]$/',$c)) {
            $uc++;
        } elseif (preg_match('/^[[:lower:]]$/',$c)) {
            $lc++;
        } elseif (preg_match('/^[[:digit:]]$/',$c)) {
            $num++;
        } else {
            $other++;
        }
    }

    // the password must have more than two characters of at least 
    // two different kinds 
    $max = $j - 2;
    if ($uc > $max) {
        return "The password has too many upper case characters.";
    }
    if ($lc > $max) {
        return "The password has too many lower case characters.";
    }
    if ($num > $max) {
        return "The password has too many numeral characters.";
    }
    if ($other > $max) {
        return "The password has too many special characters.";
    }

    // the password must not contain a dictionary word 
    if (is_readable($word_file)) {
        if ($fh = fopen($word_file,'r')) {
            $found = false;
            while (! ($found || feof($fh))) {
                $word = preg_quote(trim(strtolower(fgets($fh,1024))),'/');
                if (preg_match("/$word/",$lc_pass) ||
                    preg_match("/$word/",$denum_pass)) {
                    $found = true;
                }
            }
            fclose($fh);
            if ($found) {
                return 'The password is based on a dictionary word.';
            }
        }
    }

    return false;
}

See Also

Helpful password choosing guidelines are available at http://tns.sdsu.edu/security/passwd.html .

Dealing with Lost Passwords

Problem

You want to issue a password to a user who claims he's lost his password.

Solution

Generate a new password and send it to the user's email address (which you should have on file):

// generate new password
$new_password = '';
$i = 8;
while ($i--) { $new_password .= chr(mt_rand(33,126)); }

// encrypt new password
$encrypted_password = crypt($new_password);

// save new encrypted password to the database
$dbh->query('UPDATE users SET password = ? WHERE username = ?',
            array($encrypted_password,$username));

// email new plaintext password to user
mail($email,"New Password","Your new password is $new_password");

Discussion

If a user forgets his password, and you store encrypted passwords as recommended in Recipe 14.5, you can't provide the forgotten password. The one-way nature of crypt( ) prevents you from retrieving the unencrypted password.

Instead, generate a new password and send that to his preexisting contact address. If you send the new password to an address you don't already have on file for that user, you don't have a way to verify that the new address really belongs to the user. It may be an attacker attempting to impersonate the real user.

Because the email containing the new password isn't encrypted, the code in the Solution doesn't include the username in the email message to reduce the chances that an attacker that eavesdrops on the email message can steal the password. To avoid disclosing a new password by email at all, let a user authenticate himself without a password by answering one or more personal questions (the answers to which you have on file). These questions can be "What was the name of your first pet?" or "What's your mother's maiden name?" — anything a malicious attacker is unlikely to know. If the user provides the correct answers to your questions, you can let him choose a new password.

One way to compromise between security and readability is to generate a password for a user out of actual words interrupted by some numbers.

$words = 
array('dished','mother','basset','detain','sudden','fellow','logged','sonora',
      'earths','remove','dustin','snails','direct','serves','daring','cretan',
      'chirps','reward','snakes','mchugh','uphold','wiring','gaston','nurses',
      'regent','ornate','dogmas','singed','mended','hinges','latent','verbal',
      'grimes','ritual','drying','hobbes','chests','newark','sourer','rumple');

mt_srand((double) microtime() * 1000000);
$word_count = count($words);

$password = sprintf('%s%02d%s',
                    $words[mt_rand(0,$word_count - 1)],
                    mt_rand(0,99),
                    $words[mt_rand(0,$word_count - 1)]);

print $password;

This code produces passwords that are two six-letter words with two numbers between them, like mother43hinges or verbal08chirps. The passwords are long, but remembering them is made easier by the words in them.

See Also

Recipe 14.5 for information about storing encrypted passwords and Recipe 14.6 for details on checking password strength.

Encrypting and Decrypting Data

Problem

You want to encrypt and decrypt data using one of a variety of popular algorithms.

Solution

Use PHP's mcrypt extension:

$key  = 'That golden key that opes the palace of eternity.';
$data = 'The chicken escapes at dawn. Send help with Mr. Blue.';
$alg  = MCRYPT_BLOWFISH;
$mode = MCRYPT_MODE_CBC;

$iv = mcrypt_create_iv(mcrypt_get_iv_size($alg,$mode),MCRYPT_DEV_URANDOM);
$encrypted_data = mcrypt_encrypt($alg, $key, $data, $mode, $iv);
$plain_text = base64_encode($encrypted_data);

print $plain_text."\n";
$decoded = mcrypt_decrypt($alg,$key,base64_decode($plain_text),$mode,$iv);
print $decoded."\n";
NNB9WnuCYjyd3Y7vUh7XDfWFCWnQY0BsMehHNmBHbGOdJ3cM+yghABb/XyrJ+w3xz9tms74/a70=
               The chicken escapes at dawn. Send help with Mr. Blue.

Discussion

The mcrypt extension is an interface with mcrypt, a library that implements many different encryption algorithms. The data is encrypted and decrypted by mcrypt_encrypt( ) and mcrypt_decrypt( ), respectively. They each take five arguments. The first is the algorithm to use. To find which algorithms mcrypt supports on your system, call mcrypt_list_algorithms( ) . The full list of mcrypt algorithms is shown in Table 14-1. The second argument is the encryption key; the third argument is the data to encrypt or decrypt. The fourth argument is the mode for the encryption or decryption (a list of supported modes is returned by mcrypt_list_modes( ) ). The fifth argument is an initialization vector (IV), used by some modes as part of the encryption or decryption process.

Table 14-1 lists all the possible mcrypt algorithms, including the constant value used to indicate the algorithm, the key and block sizes in bits, and whether the algorithm is supported by libmcrypt 2.2.x and 2.4.x.

Table 14-1. mcrypt algorithm constants

Algorithm constant Description Key size Block size 2.2.x 2.4.x
MCRYPT_3DES Triple DES 168 (112 effective) 64 Yes Yes
MCRYPT_TRIPLEDES Triple DES 168 (112 effective) 64 No Yes
MCRYPT_3WAY 3way (Joan Daemen) 96 96 Yes No
MCRYPT_THREEWAY 3way 96 96 Yes Yes
MCRYPT_BLOWFISH Blowfish (Bruce Schneier) Up to 448 64 No Yes
MCRYPT_BLOWFISH_COMPAT Blowfish with compatibility to other implementations Up to 448 64 No Yes
MCRYPT_BLOWFISH_128 Blowfish 128 64 Yes No
MCRYPT_BLOWFISH_192 Blowfish 192 64 Yes
MCRYPT_BLOWFISH_256 Blowfish 256 64 Yes No
MCRYPT_BLOWFISH_448 Blowfish 448 64 Yes No
MCRYPT_CAST_128 CAST (Carlisle Adams and Stafford Tavares) 128 64 Yes Yes
MCRYPT_CAST_256 CAST 256 128 Yes Yes
MCRYPT_CRYPT One-rotor Unix crypt 104 8 Yes
MCRYPT_ENIGNA One-rotor Unix crypt 104 8 No Yes
MCRYPT_DES U.S. Data Encryption Standard 56 64 Yes Yes
MCRYPT_GOST Soviet Gosudarstvennyi Standard ("Government Standard") 256 64 Yes Yes
MCRYPT_IDEA International Data Encryption Algorithm 128 64 Yes Yes
MCRYPT_LOKI97 LOKI97 (Lawrie Brown, Josef Pieprzyk) 128, 192, or 256 64 Yes Yes
MCRYPT_MARS MARS (IBM) 128-448 128 No Yes
MCRYPT_PANAMA PANAMA (Joan Daemen, Craig Clapp) - Stream No Yes
MCRYPT_RC2 Rivest Cipher 2 8-1024 64 No Yes
MCRYPT_RC2_1024 Rivest Cipher 2 1024 64 Yes No
MCRYPT_RC2_128 Rivest Cipher 2 128 64 Yes No
MCRYPT_RC2_256 Rivest Cipher 2 256 64 Yes No
MCRYPT_RC4 Rivest Cipher 4 Up to 2048 Stream Yes No
MCRYPT_ARCFOUR Non-trademarked RC4 compatible Up to 2048 Stream No Yes
MCRYPT_ARCFOUR_IV Arcfour with Initialization Vector Up to 2048 Stream No Yes
MCRYPT_RC6 Rivest Cipher 6 128, 192, or 256 128 No Yes
MCRYPT_RC6_128 Rivest Cipher 6 128 128 Yes No
MCRYPT_RC6_192 Rivest Cipher 6 192 128 Yes No
MCRYPT_RC6_256 Rivest Cipher 6 256 128 Yes No
MCRYPT_RIJNDAEL_128 Rijndael (Joan Daemen, Vincent Rijmen) 128 128 Yes Yes
MCRYPT_RIJNDAEL_192 Rijndael 192 192 Yes Yes
MCRYPT_RIJNDAEL_256 Rijndael 256 256 Yes Yes
MCRYPT_SAFERPLUS SAFER+ (based on SAFER) 128, 192, or 256 128 Yes Yes
MCRYPT_SAFER_128 Secure And Fast Encryption Routine with strengthened key schedule 128 64 Yes Yes
MCRYPT_SAFER_64 Secure And Fast Encryption Routine with strengthened key 64 64 Yes Yes
MCRYPT_SERPENT Serpent (Ross Anderson, Eli Biham, Lars Knudsen) 128, 192, or 256 128 No Yes
MCRYPT_SERPENT_128 Serpent 128 128 Yes No
MCRYPT_SERPENT_192 Serpent 192 128 Yes No
MCRYPT_SERPENT_256 Serpent 256 128 Yes No
MCRYPT_SKIPJACK U.S. NSA Clipper Escrowed Encryption Standard 80 64 No Yes
MCRYPT_TWOFISH Twofish (Counterpane Systems) 128, 192, or 256 128 No Yes
MCRYPT_TWOFISH_128 Twofish 128 128 Yes No
MCRYPT_TWOFISH_192 Twofish 192 128 Yes No
MCRYPT_TWOFISH_256 Twofish 256 128 Yes No
MCRYPT_WAKE Word Auto Key Encryption (David Wheeler) 256 32 No Yes
MCRYPT_XTEA Extended Tiny Encryption Algorithm (David Wheeler, Roger Needham) 128 64 Yes Yes


Except for the data to encrypt or decrypt, all the other arguments must be the same when encrypting and decrypting. If you're using a mode that requires an initialization vector, it's okay to pass the initialization vector in the clear with the encrypted text.

The different modes are appropriate in different circumstances. Cipher Block Chaining (CBC) mode encrypts the data in blocks, and uses the encrypted value of each block (as well as the key) to compute the encrypted value of the next block. The initialization vector affects the encrypted value of the first block. Cipher Feedback (CFB) and Output Feedback (OFB) also use an initialization vector, but they encrypt data in units smaller than the block size. Note that OFB mode has security problems if you encrypt data in smaller units than its block size. Electronic Code Book (ECB) mode encrypts data in discreet blocks that don't depend on each other. ECB mode doesn't use an initialization vector. It is also less secure than other modes for repeated use, because the same plaintext with a given key always produces the same ciphertext. Constants to set each mode are listed in Table 14-2.

Table 14-2. mcrypt mode constants

Mode constant Description
MCRYPT_MODE_ECB Electronic Code Book mode
MCRYPT_MODE_CBC Cipher Block Chaining mode
MCRYPT_MODE_CFB Cipher Feedback mode
MCRYPT_MODE_OFB Output Feedback mode with 8 bits of feedback
MCRYPT_MODE_NOFB Output Feedback mode with n bits of feedback, where n is the block size of the algorithm used (libmcrypt 2.4 and higher only)
MCRYPT_MODE_STREAM Stream Cipher mode, for algorithms such as RC4 and WAKE (libmcrypt 2.4 and higher only)


Different algorithms have different block sizes. You can retrieve the block size for a particular algorithm with mcrypt_get_block_size( ) . Similarly, the initialization vector size is determined by the algorithm and the mode. mcrypt_create_iv( ) and mcrypt_get_iv_size( ) make it easy to create an appropriate random initialization vector:

$iv = mcrypt_create_iv(mcrypt_get_iv_size($alg,$mode),MCRYPT_DEV_URANDOM);

The first argument to mcrypt_create_iv( ) is the size of the vector, and the second is a source of randomness. You have three choices for the source of randomness. MCRYPT_DEV_RANDOM reads from the pseudodevice /dev/random, MCRYPT_DEV_URANDOM reads from the pseudo-device /dev/urandom, and MCRYPT_RAND uses an internal random number generator. Not all operating systems support random-generating pseudo-devices. Make sure to call srand( ) before using MCRYPT_RAND in order to get a nonrepeating random number stream.

The code and examples in this recipe are compatible with mcrypt 2.4. PHP's mcrypt interface supports both mcrypt 2.2 and mcrypt 2.4, but there are differences between the two. With mcrypt 2.2, PHP supports only the following mcrypt functions: mcrypt_ecb( ), mcrypt_cbc( ), mcrypt_cfb( ), mcrypt_ofb( ), mcrypt_get_key_size( ), mcrypt_get_block_size( ), mcrypt_get_cipher_name( ), and mcrypt_create_iv( ). To encrypt or decrypt data with mcrypt 2.2, call the appropriate mcrypt_ MODE ( ) function, based on what mode you want to use, and pass it an argument that instructs it to encrypt or decrypt. The following code is the mcrypt 2.2-compatible version of the code in the Solution:

$key  = 'That golden key that opes the palace of eternity.';
$data = 'The chicken escapes at dawn. Send help with Mr. Blue.';
$alg = MCRYPT_BLOWFISH;

$iv = mcrypt_create_iv(mcrypt_get_block_size($alg),MCRYPT_DEV_URANDOM);
$encrypted_data = mcrypt_cbc($alg,$key,$data,MCRYPT_ENCRYPT);
$plain_text = base64_encode($encrypted_data);

print $plain_text."\n";

$decoded = mcrypt_cbc($alg,$key,base64_decode($plain_text),MCRYPT_DECRYPT);

print $decoded."\n";

See Also

Documentation on the mcrypt extension at http://www.php.net/mcrypt; the mcrypt library is available at http://mcrypt.hellug.gr/; choosing an appropriate algorithm and using it securely requires care and planning: for more information about mcrypt and the cipher algorithms it uses, see the online PHP manual section on mcrypt, the mcrypt home page, and the manpages for /dev/random and /dev/urandom; good books about cryptography include Applied Cryptography, by Bruce Schneier (Wiley) and Cryptography: Theory and Practice, by Douglas R. Stinson (Chapman & Hall).

Storing Encrypted Data in a File or Database

Problem

You want to store encrypted data that needs to be retrieved and decrypted later by your web server.

Solution

Store the additional information required to decrypt the data (such as algorithm, cipher mode, and initialization vector) along with the encrypted information, but not the key:

// encrypt data
$alg  = MCRYPT_BLOWFISH;
$mode = MCRYPT_MODE_CBC;
$iv = mcrypt_create_iv(mcrypt_get_iv_size($alg,$mode),MCRYPT_DEV_URANDOM);
$ciphertext = mcrypt_encrypt($alg,$_REQUEST['key'],$_REQUEST['data'],$mode,$iv);

// save encrypted data
$dbh->query('INSERT INTO noc_list (algorithm,mode,iv,data) values (?,?,?,?)',
            array($alg,$mode,$iv,$ciphertext));

To decrypt, retrieve a key from the user and use it with the saved data:

$row = $dbh->getRow('SELECT * FROM noc_list WHERE id = 27');
$plaintext = mcrypt_decrypt($row->algorithm,$_REQUEST['key'],$row->data,
                            $row->mode,$row->iv);

Discussion

The save-crypt.php program shown in Example 14-2 stores encrypted data to a file.

Example 14-2. save-crypt.php

function show_form() {
    print<<<_FORM_
<form method="post" action="$_SERVER[PHP_SELF]">
<textarea name="data" rows="10" cols="40">
Enter data to be encrypted here.
</textarea>
<br>
Encryption Key: <input type="text" name="key">
<input name="submit" type="submit" value="save">
</form>
_FORM_;
}

function save_form() {
    $alg  = MCRYPT_BLOWFISH;
    $mode = MCRYPT_MODE_CBC;

    // encrypt data
    $iv = mcrypt_create_iv(mcrypt_get_iv_size($alg,$mode),MCRYPT_DEV_URANDOM);
    $ciphertext = mcrypt_encrypt($alg, $_REQUEST['key'], 
                                 $_REQUEST['data'], $mode, $iv);
    
    // save encrypted data
    $filename = tempnam('/tmp','enc') or die($php_errormsg);
    $fh = fopen($filename,'w')        or die($php_errormsg);
    if (false === fwrite($fh,$iv.$ciphertext)) {
        fclose($fh);
        die($php_errormsg);
    }
    fclose($fh)                       or die($php_errormsg);

    return $filename;
}

if ($_REQUEST['submit']) {
    $file = save_form();
    print "Encrypted data saved to file: $file";
} else { 
    show_form();
}

Example 14-3 shows the corresponding program, get-crypt.php , that accepts a filename and key and produces the decrypted data.

Example 14-3. get-crypt.php

function show_form() {
    print<<<_FORM_
<form method="post" action="$_SERVER[PHP_SELF]">
Encrypted File: <input type="text" name="file">
<br>
Encryption Key: <input type="text" name="key">
<input name="submit" type="submit" value="display">
</form>
_FORM_;
}

function display() {
    $alg  = MCRYPT_BLOWFISH;
    $mode = MCRYPT_MODE_CBC;

    $fh = fopen($_REQUEST['file'],'r') or die($php_errormsg);
    $iv = fread($fh,mcrypt_get_iv_size($alg,$mode));
    $ciphertext = fread($fh,filesize($_REQUEST['file']));
    fclose($fh);

    $plaintext = mcrypt_decrypt($alg,$_REQUEST['key'],$ciphertext,$mode,$iv);
    print "<pre>$plaintext</pre>";
}

if ($_REQUEST['submit']) {
    display();
} else { 
    show_form();
}

These two programs have their encryption algorithm and mode hardcoded in them, so there's no need to store this information in the file. The file consists of the initialization vector immediately followed by the encrypted data. There's no need for a delimiter after the initialization vector (IV), because mcrypt_get_iv_size( ) returns exactly how many bytes the decryption program needs to read to get the whole IV. Everything after that in the file is encrypted data.

Encrypting files using the method in this recipe offers protection if an attacker gains access to the server on which the files are stored. Without the appropriate key or tremendous amounts of computing power, the attacker won't be able to read the files. However, the security that these encrypted file provides is undercut if the data to be encrypted and the encryption keys travel between your server and your users' web browsers in the clear. Someone who can intercept or monitor network traffic can see data before it even gets encrypted. To prevent this kind of eavesdropping, use SSL.

An additional risk when your web server encrypts data as in this recipe comes from how the data is visible before it's encrypted and written to a file. Someone with root or administrator access to the server can look in the memory the web server process is using and snoop on the unencrypted data and the key. If the operating system swaps the memory image of the web server process to disk, the unencrypted data might also be accessible in this swap file. This kind of attack can be difficult to pull off but can be devastating. Once the encrypted data is in a file, it's unreadable even to an attacker with root access to the web server, but if the attacker can peek at the unencrypted data before it's in that file, the encryption offers little protection.

See Also

Recipe 14.11 discusses SSL and protecting data as it moves over the network; documentation on mcrypt_encrypt( ) at http://www.php.net/mcrypt-encrypt, mcrypt_decrypt( ) at http://www.php.net/mcrypt-decrypt, mcrypt_create_iv( ) at http://www.php.net/mcrypt-create-iv, and mcrypt_get_iv_size( ) at http://www.php.net/mcrypt-get-iv-size.

Sharing Encrypted Data with Another Web Site

Problem

You want to securely exchange data with another web site.

Solution

If the other web site is pulling the data from your site, put the data up on a password-protected page. You can also make the data available in encrypted form, with or without a password. If you need to push the data to another web site, submit the potentially encrypted data via POST to a password-protected URL.

Discussion

The following page requires a username and password and then encrypts and displays the contents of a file containing yesterday's account activity:

$user = 'bank';
$password = 'fas8uj3';

if (! (($_SERVER['PHP_AUTH_USER'] == $user) && 
       ($_SERVER['PHP_AUTH_PW'] == $password))) {
    header('WWW-Authenticate: Basic realm="Secure Transfer"');
    header('HTTP/1.0 401 Unauthorized');
    echo "You must supply a valid username and password for access.";
    exit;
}

header('Content-type: text/plain');
$filename = strftime('/usr/local/account-activity.%Y-%m-%d',time() - 86400);
$data = join('',file($filename));

$alg  = MCRYPT_BLOWFISH;
$mode = MCRYPT_MODE_CBC;
$key  = "There are many ways to butter your toast.";
 
// encrypt data
$iv = $iv = mcrypt_create_iv(mcrypt_get_iv_size($alg,$mode),
                             MCRYPT_DEV_URANDOM);
$ciphertext = mcrypt_encrypt($alg, $key, $data, $mode, $iv);

print base64_encode($iv.$ciphertext);

Here's the corresponding code to retrieve the encrypted page and decrypt the information:

$user = 'bank';
$password = 'fas8uj3';
$alg  = MCRYPT_BLOWFISH;
$mode = MCRYPT_MODE_CBC;
$key  = "There are many ways to butter your toast.";

$fh = fopen("http://$user:$password@bank.example.com/accounts.php",'r') 
      or die($php_errormsg);
$data = '';
while (! feof($fh)) { $data .= fgets($fh,1048576); }
fclose($fh) or die($php_errormsg);
$binary_data = base64_decode($data);
$iv_size = mcrypt_get_iv_size($alg,$mode);
$iv = substr($binary_data,0,$iv_size);
$ciphertext = substr($binary_data,$iv_size,strlen($binary_data));

print mcrypt_decrypt($alg,$key,$ciphertext,$mode,$iv);

The retrieval program does all the steps of the encryption program but in reverse. It retrieves the Base64 encoded encrypted data, supplying a username and password. Then, it decodes the data with Base64 and separates out the initialization vector. Last, it decrypts the data and prints it out.

In the previous examples, the username and password are still sent over the network in clear text, unless the connections happen over SSL. However, if you're using SSL, it's probably not necessary to encrypt the file's contents. We included both password-prompting and file encryption in these examples to show how it can be done.

There's one circumstance, however, in which both password protection and file encryption is helpful: if the file isn't automatically decrypted when it's retrieved. An automated program can retrieve the encrypted file and put it, still encrypted, in a place that can be accessed later. The decryption key thus doesn't need to be stored in the retrieval program.

See Also

Recipe 8.10 for information on using HTTP Basic authentication; Recipe 14.11 discusses SSL and protecting data as it moves over the network; documentation on mcrypt_encrypt( ) at http://www.php.net/mcrypt-encrypt and mcrypt_decrypt( ) at http://www.php.net/mcrypt-decrypt.

Detecting SSL

Problem

You want to know if a request arrived over SSL.

Solution

Test the value of $_SERVER['HTTPS']:

if ('on' == $_SERVER['HTTPS']) {
  print "The secret ingredient in Coca-Cola is Soylent Green.";
} else {
  print "Coca-Cola contains many delicious natural and artificial flavors.";
}

Discussion

SSL operates on a lower level than HTTP. The web server and a browser negotiate an appropriately secure connection, based on their capabilities, and the HTTP messages can pass over that secure connection. To an attacker intercepting the traffic, it's just a stream of nonsense bytes that can't be read.

Different web servers have different requirements to use SSL, so check your server's documentation for specific details. No changes have to be made to PHP to work over SSL.

In addition to altering code based on $_SERVER['HTTPS'], you can also set cookies to be exchanged only over SSL connections. If the last argument to setcookie( ) is 1, the browser sends the cookie back to the server only over a secure connection:

/* set an SSL-only cookie named "sslonly" with value "yes" that expires
 * at the end of the current browser session */
setcookie('sslonly','yes','','/','sklar.com',1);

Although the browser sends these cookies back to the server only over an SSL connection, the server sends them to the browser (when you call setcookie( ) in your page) whether or not the request for the page that sets the cookie is over SSL. If you're putting sensitive data in the cookie, make sure that you set the cookie only in an SSL request as well. Keep in mind as well that the cookie data is unencrypted on the user's computer.

See Also

Recipe 8.2 discusses setting cookies; documentation on setcookie( ) at http://www.php.net/setcookie.

Encrypting Email with GPG

Problem

You want to send encrypted email messages. For example, you take orders on your web site and need to send an email to your factory with order details for processing. By encrypting the email message, you prevent sensitive data such as credit card numbers from passing over the network in the clear.

Solution

Encrypt the body of the email message with GNU Privacy Guard (GPG) before sending it:

$message_body = escapeshellarg($message_body);
$gpg_path     = '/usr/local/bin/gpg';
$sender       = 'web@example.com';
$recipient    = 'ordertaker@example.com';
$home_dir     = '/home/web';
$user_env     = 'web';

$cmd = "echo $message_body | HOME=$home_dir USER=$user_env $gpg_path " .
       '--quiet --no-secmem-warning --encrypt --sign --armor ' .
       "--recipient $recipient --local-user $sender";

$message_body = `$cmd`;

mail($recipient,'Web Site Order',$message_body);

The email message can be decrypted by GPG, Pretty Good Privacy (PGP) or an email client plug-in that supports either program.

Discussion

PGP is a popular public key encryption program; GPG is an open-source program based on PGP. Because PGP in encumbered by a variety of patent and control issues, it's often easier to use GPG.

The code in the Solution invokes /usr/local/bin/gpg to encrypt the message in $message_body. It uses the private key belonging to $sender and the public key belonging to $recipient. This means that only $recipient can decrypt the email message and when she does, she knows the message came from $sender.

Setting the HOME and USER environment variables tells GPG where to look for its keyring: $HOME/.gnupg/secring.gpg. The --quiet and --no-secmem-warning options suppress warnings GPG would otherwise generate. The --encrypt and --sign options tell GPG to both encrypt and sign the message. Encrypting the message obscures it to anyone other than the recipient. Signing it adds information so that the recipient knows who generated the message and when it was generated. The --armor option produces plaintext output instead of binary, so the encrypted message is suitable for emailing.

Normally, private keys are protected with a passphrase. If a private key protected by a passphrase is copied by an attacker, the attacker can't encrypt messages with the private key unless he also knows the passphrase. GPG prompts for the passphrase when encrypting a message. In this recipe, however, we don't want the private key of $sender to have a passphrase. If it did, the web site couldn't send new-order email messages without a human typing in the passphrase each time. Storing the passphrase in a file and providing it to GPG each time you encrypt offers no additional security over not having a passphrase in the first place.

The downside of using a key without a passphrase for encryption is that an attacker who obtains the secret key can send fake order emails to your order processor. This is a manageable risk. Since orders can be submitted via a web site in the first place, there is already a place where false information can be injected into the order process. Any procedures for catching bad orders can also be triggered by these potential fake emails. Also, once the key theft is discovered, and the problem that enabled the theft is fixed, the attacker is easily disabled by switching to a new private key.

See Also

The GNU Privacy Guard home page at http://www.gnupg.org/ and the MIT PGP distribution site at http://web.mit.edu/network/pgp.html.

Notes

  1. Practical Unix and Internet Security, by Simson Garfinkel and Gene Spafford (O'Reilly) offers some helpful and (not surprisingly) practical advice on how to think about the balancing act of risk management.
Personal tools