Monday 27 May 2013
Facebook StumbleUpon Twitter Google+ Pin It

PHP encryption for the common man


Consider how today's world differs from the world of just 20 years ago. Long ago, in the 1980s, encryption was spy stuff -- something you read about in a techno-thriller by Tom Clancy. If somebody wanted to keep a bit of information private, he encrypted the data with a password, a pass phrase, or another basic method.
Fast-forward to today and encryption is everywhere. Passwords are stored encrypted in databases. Encrypted tunnels through cyberspace are possible via SSL, SSH, and other technologies -- not to mention virtual private networks. Everyday people can and do use Pretty Good Privacy (PGP) to armor their sensitive files and e-mail.
As a PHP developer, you should be aware that strong security practices aren't just for exotic applications -- they're for the project you're working on now. This awareness runs from the pedestrian (such as not showing plaintext in a password field on a login page) to dizzying heights of cryptographic methods (such as DES, MD5, SHA1, Blowfish).
There's not enough time or room to discuss every aspect of encryption here, but you'll learn the essentials that will cover most situations you'll find yourself in. We begin with an overview of what it means to encrypt and decrypt information, followed by some practical examples involving passwords and other data, using PHP's built-in functionality. Throughout, encryption will be discussed within the larger context of security. Finally, other PHP extensions and plug-ins will be covered.
Cryptography is the art of "secret writing," as the word's Greek roots attest. One of the first, and simplest, forms of encryption is the Caesar cipher, which takes a plaintext message, shifts the letters n places, and results in a ciphertext. For example:
  • Plaintext: Veni Vidi Vici
  • Ciphertext: Xgpk Xkfk Xkek
By examining the ciphertext, you can use a few heuristic tricks to figure out the plaintext has been shifted two characters. The Caesar cipher is easy to break. For example: Examine this message and see that X is repeated a lot, and so is k. It becomes a guessing game to determine how many four-letter words have that many vowels. Once you have the vowels, you know which way to shift the rest of the letters. It also helps if you know the plaintext is in Latin, but you get the picture.
Modern encryption technologies are a lot more powerful, using algorithms that go beyond uniform shifting of letters and symbols. This article doesn't go into the blow-by-blow details of these algorithms, but just about any PHP installation will include all you need to keep your data moderately (or extremely) secure.
There is no 100-percent uncrackable, full-proof cryptographic method. It seems that every month, some hacker kid and his friends tie together 1,000 machines and crack the newest encryption method in a few days of raw, brute-force computational battering. However, you can make your systems and data impervious enough that hackers won't even bother trying to break in. They'll look for easier pickings elsewhere.
With that in mind, let's jump into a sample PHP application secured by a simple login form.
Let's say you're a new Web application developer, and you haven't had much chance to play around with security features. You've created your first application, one that stores usernames and passwords in a dedicated user table, but you haven't encrypted those passwords. They're sitting there in plain sight for anyone who accesses your database. You build a login page like that shown below.




<form action="verify.php" method="post">
<p><label for='username'>Username</label>
<input type='text' name='user' id='username'/>
</p>

<p><label for='password'>Password</label>
<input type='text' name='password' id='password'/>
</p>
<p><input type="submit" name="submit" value="log in"/>
</p>
</form>




What's wrong with this HTML markup? The input type selected for the password field is text, which means that anything the user types into that field appears as plaintext on the screen.
You can easily change the type to password and replace user input in that field to a series of asterisks. Basic? Absolutely. However, this step is overlooked in more applications than you would imagine, and it's the little things that make people uncomfortable when it comes to security. Would you put your money in a bank with broken windows in its lobby? Maybe. But you expect a bank to be well cared for. The same goes for your application.
Let's move on to the verify.php file that processes the form submission.




<?php
 $user = $_POST['user'];
 $pw = $_POST['password'];
 
 $sql = "select user,password from users 
  where user='$user' 
  and password='$pw' 
  limit 1';
 $result = mysql_query($sql);
 
 if (mysql_num_rows($result)){
  //we have a match!
 }else{
  //no match
 }

?>




The savvy among you are grinning as you read this. Those of you who are waiting for the encryption part of this article may be getting impatient, but encryption is just one part of the security puzzle. You also have to be smart about how you handle incoming user data. The developerWorks tutorial "Locking down your PHP applications" (see Resources) talks about SQL injections: the art of sending malformed data to a database to cause harm or unwarranted access. No matter how much encryption you use, leaving that vulnerability open won't help a bit.
You should follow the old security principles of "never trust user-supplied data" and "defense in-depth." Clean up the incoming data and protect the database by escaping the incoming string (see Listing 3). Defense in-depth is all about having redundant security measures in place -- not only encryption but also smart handling of user-supplied data.




<?php
 $user = strip_tags(substr($_POST['user'],0,32));
 $pw = strip_tags(substr($_POST['password'],0,32));
 
 $sql = "select user,password from users 
  where user='". mysql_real_escape_string($user)."' 
  and password='". mysql_real_escape_string($pw)."' 
  limit 1';
 $result = mysql_query($sql);
 
 if (mysql_num_rows($result)){
  //we have a match!
 }else{
  //no match
 }
?>




With judicious use of strip_tags()substr(), and mysql_real_escape_string(), you've stripped out any potentially harmful commands, cut the strings down to 32 characters, and escaped any and all special characters the database might interpret as part of a nonintended command string.
At the end of this process, you still have a plaintext password in the database. You can't let that stand. The easiest fix is to use PHP's built-in crypt() function.
PHP's built-in crypt() function implements one-way encryption or one-way hashing. It's one-way because once you encrypt something, you can never get it back into plaintext. At first blush, this idea seems ridiculous. The point is to protect information, then be able to use that information, which usually means being able to decrypt it.
Don't despair. One-way encryption schemes, and crypt() in particular, are extremely popular, secure ways of protecting information. If your list of user passwords falls into the wrong hands, there's literally no way it can be decrypted to plaintext.
Let's go back to the password example. The notational PHP application probably includes a module that lets system administrators create, edit, and delete users. Before storing a user's record into the users table, for example, it's likely that a PHP script uses crypt() to encrypt the password.




<?php
 $user = strip_tags(substr($_POST['user'],0,32));
 $pw = strip_tags(substr($_POST['password'],0,32));
 
 $cleanpw = crypt($pw);
 
 $sql = "insert into users (username,password) 
 values('".mysql_real_escape_string($user)."',
 '".mysql_real_escape_string($cleanpw)."')";
 //.....etc....
?>




crypt() takes as its first argument a string of plaintext, applies a salt to it to influence the randomness of the encryption algorithm, and generates a one-way ciphertext of the input plaintext. If you don't supply a salt, PHP generally defaults to its system salt, which can be one of the following values and lengths:
AlgorithmSalt
CRYPT_STD_DESTwo-character (default)
CRYPT_EXT_DESNine-character
CRYPT_MD512-character, starting with $1$
CRYPT_BLOWFISH16-character, starting with $2$



Many modern PHP installations use MD5 or higher salts, which use a strong 12-character salt, but don't take anything for granted. The system that you are on could be using anything at all, so it's better to know. You can check your server's setting with the following snippet of PHP code:


<?php echo "System salt size: ". CRYPT_SALT_LENGTH; ?>

The answer will come back 2, 9, 12, or 16, which tells you what you're using. To use MD5 or higher, you can explicitly call thecrypt() function along with the md5() function both in the plaintext and salt arguments to get a random ciphertext (see Listing 5). The md5() function hashes whatever string is fed to it and turns it into a 32-character fixed-length string. You may prefer another method, depending on your security requirements and personal preferences.




<?php
 $user = strip_tags(substr($_POST['user'],0,32));
 $pw = strip_tags(substr($_POST['password'],0,32));
 
 $cleanpw = crypt(md5($pw),md5($user));
 
 $sql = "insert into users (username,password) 
 values('".mysql_real_escape_string($user)."',
 '".mysql_real_escape_string($cleanpw)."')";
 //.....etc....
?>




You now have an encrypted password in a database, but no way to decrypt it. How is that useful? Easy: Use the identical method of encryption on any incoming user-supplied password and compare the result to your stored password.




<?php
 $user = strip_tags(substr($_POST['user'],0,32));
 $pw = strip_tags(substr($_POST['password'],0,32));
 $cleanpw = crypt(md5($pw),md5($user));
 
 $sql = "select user,password from users 
  where user='". mysql_real_escape_string($user)."' 
  and password='". mysql_real_escape_string($cleanpw)."' 
  limit 1';
 $result = mysql_query($sql);
 
 if (mysql_num_rows($result)){
  //we have a match!
 }else{
  //no match
 }
?>




For example, if the stored encrypted password is i83Uw28jKzBrZF, encrypt the incoming password and compare it to the stored one. The only way an attacker can break your encryption is to compare a very long list of strings to your encrypted password, one at a time, until a match is made. This is known as a dictionary attack, and it's one of the many good reasons why your password shouldn't be password or the name of a Star Trek character or even your dog's name. Just because you encrypt Fido and it becomes gibberish doesn't mean your password is safe from this kind of attack. Making sure your password is of a certain length (eight or more characters) and contains uppercase letters, numbers, and special characters -- like ! and $ -- will go a long way toward making your data harder to guess. Even f1D0! is a better password, in the short term, than a longer password likeGandalftheGray, which is longer, uses lowercase letters, and is the name of a character from "Lord of the Rings."
There's another way to use crypt() that isn't so good: using as the salt the first n characters of the plaintext.




<?php
 $user = strip_tags(substr($_POST['user'],0,32));
 $pw = strip_tags(substr($_POST['password'],0,32));
 $cleanpw =crypt($pw, substr($user,0,2));
 
 $sql = "select user,password from users 
  where user='". mysql_real_escape_string($user)."' 
  and password='". mysql_real_escape_string($cleanpw)."' 
  limit 1';
 $result = mysql_query($sql);
 
 if (mysql_num_rows($result)){
  //we have a match!
 }else{
  //no match
 }
?>




If your username is tmyer, the salt is prepended by tm, which makes it easier for someone to figure out what your salt is. Not a good idea.
Most of this article has discussed one-way encryption using crypt(). But what if you want to send a message to someone and provide a way to decrypt the message? Use public-key cryptography, which PHP supports.
Users of public-key encryption have a private key and a public key, and they share their public keys with other users. If you want to send a private note to your friend John Doe, you encrypt that message with John Doe's public key (which you've stored on your own keyring). Once John Doe receives the message, only he can decrypt the message using his private key. The public key and private key for any given user aren't mathematically related. With PGP and other public-key encryption methods, there's no way to deduce someone's private key from the public key.
An added feature of PGP is that the password for the private key isn't really a password; it's a passphrase. And it can be an entire saying, including punctuation, spaces, and all manner of characters.
One way to use PGP-based public-key encryption is to use GNU Privacy Guard (GPG). Any messages encrypted using GPG can be decrypted with GPG, PGP, or any number of e-mail client plug-ins that support either program. In the example, an online form accepts user input, including a message; encrypts that message for a particular recipient using GPG; then sends it on.




<?php
 //set up users
 $from = "webforms@example.com";
 $to = "you@example.com";
 
 //cut the message down to size, remove HTML tags
 $messagebody = strip_tags(substr($_POST['msg'],0,5000));
 $message_body = escapeshellarg($messagebody);
 
 $gpg_path = '/usr/local/bin/gpg';
 $home_dir = '/htdocs/www';
 $user_env = 'web';

 $cmd = "echo $message_body | HOME=$home_dir USER=$user_env $gpg_path" .
  "--quiet --no-secmem-warning --encrypt --sign --armor " .
  "--recipient $to --local-user $from";
 
 $message_body = `$cmd`;
 
 mail($to,'Message from Web Form', $message_body,"From:$from\r\n");

?>




In this example, PHP invokes /usr/local/bin/gpg (this location may vary on your server) to encrypt a message using the sender's private key and the recipient's public key. In effect, only the recipient can decrypt the message and know that the message came from the sender. Furthermore, setting the HOME and USER environment variables tells GPG where to look for the keyrings on which these keys are stored. The other flags do the following:
  • --quiet and --no-secmem-warning suppress warnings from GPG.
  • --encrypt performs the encryption.
  • --sign adds a signature to verify the sender's identity.
  • --armor produces ASCII output instead of binary so it can be easily sent via e-mail.
Normally, and as mentioned, secret keys are protected by a passphrase. This particular instance doesn't use a passphrase because it would need to be manually entered on each form submission. You have other options, of course, such as providing the passphrase in a separate file or protecting the form from public use with its own authentication scheme (for example, if it's a form that can only be accessed by sales reps for your company).
Also note that unless you're using SSL on the form that allows the user to enter the e-mail message, whatever is typed is in cleartext. In other words: It's visible to anyone between the client machine and the server. That, however, is another subject.
I've explained enough about security, encryption techniques, and even public-key cryptography to help make your next PHP project a success. The point of using encryption and other cryptographic methods isn't to create a 100-percent foolproof, uncrackable system. The only positively unhackable system is a computer that's turned off, and even that isn't a guarantee because someone might be able to physically walk up to it, turn it on, and hack it. The point of all this work is to make it so difficult to get at sensitive data that hackers don't even try, or they move on after a few failed attempts.
All security considerations have to exist on the spectrum between convenience and protection. Requiring everything to be one-way-encrypted using a strong algorithm key means that your data is very secure, but not convenient to use. The opposite is just as bad -- using no encryption whatsoever -- because however convenient that may be for you, it's also terribly convenient for someone else to get to it, too. Strike a good balance by encrypting important confidential data (like passwords, credit card numbers, and secret messages) and adding good security measures like defense in-depth, filtering user-supplied data, and plain old common sense.

-By Parthiv Patel

No comments: