A reversible password encryption routine for PHP

By Tony Marston

14th June 2003
Amended 16th June 2007

Intended Audience
Prerequisites
Encryption Methods
- Rotating each character a fixed number of positions
- Swapping between two different character strings
- Manipulating the index number between the two strings
- Customising the encryption algorithm
Test Harness
The Encryption Class
- Class Variables
- Class Constructor
- The Encrypt function
- The ConvertKey function
- The ApplyFudgeFactor function
- The CheckRange function
- The Decrypt function
Using the Encryption Class
Summary
Amendment History

Intended Audience

This tutorial is intended for those PHP developers who want a password encryption routine that is reversible - i.e. the encrypted password can be decrypted back into plain text. The reason for wanting such a routine could be that your web hosting service does not include an acceptable encryption module in his PHP build, or perhaps you want a routine that allows a certain amount of customisation that makes it totally different from the encryption routines used by others.

Prerequisites

A working PHP development environment. Expert knowledge of PHP is not required, although an understanding of classes and OOP (Object Oriented Programming) would be useful.

Encryption Methods

Many different methods of encryption have been used over the years, some are simple and can be broken easily while others are more complex and therefore more difficult to break.

Rotating each character a fixed number of positions

In this method each character is converted into a number, another number is added to it, then it is converted back into a different character from the same character set. This is how the ROT13 system works as it rotates each character 13 positions. This system is easy to break.

Swapping between two different character strings

A better method is to use two different character sets - one for obtaining the index number of the source character (STRING1), and a second for converting this index number into a different target character (STRING2). The advantage is that there is no fixed rotation between one character and the next, which would make it difficult for any potential hacker. The disadvantage is that each source character will always be converted to the same target character, so once a source and target pair have been found they will always remain the same. This is not good.

Manipulating the index number between the two strings

This involves an additional step in between converting a character into an index number using STRING1 and converting that index number into a character from STRING2. This additional step adjusts the index number at runtime according to some fixed rules. The rules must be fixed so that the same source string will always result in the same target string, otherwise it will not be possible to verify a password with one that is held on file. This method means that if the same character appears in the input string more than once that it will not generate the same corresponding character in the output string, thus making the hacker's job that much more difficult.

A common method is to use a separate character string as a 'key'. This key is reduced to an array of numbers, and it is this array of numbers that is used to adjust the index number between STRING1 and STRING2. The method I use is to take the first number from the array, add it to the index number, then move this number from the beginning to the end of the array in a rotation movement.

The beauty of this method is that even if you know the exact encryption algorithm being used it is totally useless without the key.

Customising the encryption algorithm

By this I mean taking a routine that is freely available, such as this one, and customising it so that it produces totally different results from any other implementation. This customisation may be done at two levels:

The first method is something that should be left to someone who knows what he's doing as making a mistake can result in a broken algorithm. The second method is as simple as supplying the routine with the values that you want it to use.

Test Harness

In order to demonstrate that this routine does in fact encrypt a text string and decrypt it back into the original text I have created a test harness which looks like the following:

encryption (6K)

The fields labelled 'key' and 'password' should be self-explanatory. The 'password length' field is there to ensure that a shorter password is padded out with spaces so that the length of the encrypted string does not give away the length of the original string. Notice how each of the trailing spaces in the above example has been converted into a totally different character.

'Adjustment' is a value that is added to the value obtained from the 'key' array in each processing cycle. By allowing decimal places it means that the resulting number may be rounded either up or down, which causes the number to be incremented again on occasions.

'Modulus' is another value that is used to adjust the index number between STRING1 and STRING2. If the value can be divided by 'modulus' with no remainder then it is multiplied by -1 which reverses the sign. This effectively means that when picking out characters from the string you may sometimes start from the beginning and search forwards, while at other times you start from the end and search backwards.

You can run the test harness by clicking here. You can view the source for the test harness by clicking here.

The Encryption Class

This routine is provided in the form of a class with properties (variables) and methods (functions). Below are commented highlights, but the entire class file can be viewed here.

Class Variables

This first section of code defines the class name and identifies the variables that are available throughout the class.

class encryption_class 
{ 
   var $scramble1;      // 1st string of ASCII characters 
   var $scramble2;      // 2nd string of ASCII characters 
   var $errors;         // array of error messages 
   var $adj;            // 1st adjustment value (optional) 
   var $mod;            // 2nd adjustment value (optional) 

Class Constructor

This function is automatically called whenever an object is created from this class. I use this to set up the contents of the two strings which I call scramble1 and scramble2. Each of these strings contains exactly the same characters, but in a different order. In this case I am using all 95 printable characters from the ASCII character set EXCEPT single quote, double and backslash as these have special meaning in PHP which can sometimes lead to problems.

I also use the class constructor to define default values for adj and mod.

   function encryption_class () 
   { 
      // Each of these two strings must contain the same characters, but in a different order. 
      // Use only printable characters from the ASCII table. 
      // Do not use single quote, double quote or backslash as these have special meanings in PHP.
      // Each character can only appear once in each string. 
      $this->scramble1 = '! #$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~'; 
      $this->scramble2 = 'f^jAE]okIOzU[2&q1{3`h5w_794p@6s8?BgP>dFV=m D<TcS%Ze|r:lGK/uCy.Jx)HiQ!#$~(;Lt-R}Ma,NvW+Ynb*0X'; 
       
      if (strlen($this->scramble1) <> strlen($this->scramble2)) { 
         trigger_error('** SCRAMBLE1 is not same length as SCRAMBLE2 **', E_USER_ERROR); 
      } // if 
       
      $this->adj = 1.75;    // this value is added to the rolling fudgefactors 
      $this->mod = 3;       // if divisible by this the adjustment is made negative 
       
   } // constructor 

The Encrypt function

This function will take a plain text string and encrypt it using the supplied key. Notice that the string length defaults to zero.

   function encrypt ($key, $source, $sourcelen=0) 
   {

The first step is to convert the key into an array of numbers which I call $fudgefactor. This array is used to 'fudge' or 'adjust' the index number obtained from the first character string before it is used on the second character string. The contents of the _convertkey function can be viewed here.

      $this->errors = array();
      
      $fudgefactor = $this->_convertKey($key); 
      if ($this->errors) return; 

This next piece of code checks that a source string has actually been supplied.

      if (empty($source)) { 
         $this->errors[] = 'No value has been supplied for encryption'; 
         return; 
      } // if 

Next we pad the input string with spaces up the specified length.

      while (strlen($source) < $sourcelen) { 
         $source .= ' '; 
      } // while 

Here we are setting up a for loop to process each character from $source.

      $target  = NULL; 
      $factor2 = 0;
      for ($i = 0; $i < strlen($source); $i++) {

Here we extract the next character from $source and identify its position in $scramble1. Note that we cannot continue processing if the character cannot be found.

         $char1 = substr($source, $i, 1); 
         $num1 = strpos($this->scramble1, $char1); 
         if ($num1 === false) { 
            $this->errors[] = "Source string contains an invalid character ($char1)"; 
            return; 
         } // if 

Next we obtain the adjustment value from the $fudgefactor array and accumulate it in $factor1 along with the previous contents of $factor2. The contents of the _applyFudgeFactor function can be viewed here.

         $adj = $this->_applyFudgeFactor($fudgefactor);
         $factor1 = $factor2 + $adj;                  // accumulate in $factor1

Here we add $factor1 to the offset from the $scramble1 string ($num1) to give us the offset into the $scramble2 string ($num2). Note that factor may contain decimal digits, so it has to be rounded in order to supply a whole number.

         $num2    = round($factor1) + $num1;        // generate offset for $scramble2

The value at this point may be a negative number or even a large positive number, so we have to check that it can actually point to a character in the $scramble2 string. The contents of the _checkRange function can be viewed here.

         $num2    = $this->_checkRange($num2);      // check range

As an added complication to confuse potential hackers we are also accumulating the value of $num2 and $factor1 in $factor2.

         $factor2 = $factor1 + $num2;               // accumulate in $factor 

Here we extract a character from $scramble2 using the value in $num2 and append it to the output string.

         $char2 = substr($this->scramble2, $num2, 1); 
         $target .= $char2;

Finally we close the for loop and return the encrypted string.

      } // for 
       
      return $target; 
       
   } // encrypt 

The ConvertKey function

This function converts the $key string into an array of numbers. The first check is to ensure that a value has actually been supplied.

   function _convertKey ($key) 
   { 
      if (empty($key)) { 
         $this->errors[] = 'No value has been supplied for the encryption key'; 
         return; 
      } // if 

The first entry in the array is the length of the $key string.

      $array[] = strlen($key);

Next we set up a for loop to examine every character in the $key string.

      $tot = 0;  
      for ($i = 0; $i < strlen($key); $i++) {

Here we extract the next character from $key and identify its position in $scramble1. Note that we cannot continue processing if the character cannot be found.

         $char = substr($key, $i, 1); 
         $num = strpos($this->scramble1, $char); 
         if ($num === false) { 
            $this->errors[] = "Key contains an invalid character ($char)"; 
            return; 
         } // if 

He we append the number to the output array and accumulate the total for later.

         $array[] = $num; 
         $tot = $tot + $num;

At the end of the for loop we add the accumulated total to the end of the array and return the array to the calling process.

      } // for 
       
      $array[] = $tot; 
       
      return $array; 
    
   } // _convertKey 

The ApplyFudgeFactor function

This function will return an adjustment value using the contents of the $fudgefactor array. Note that $fudgefactor is passed by reference so that it can be modified

   function _applyFudgeFactor (&$fudgefactor) 
   {

Here we extract the first entry in the array and remove it from the array.

      $fudge = array_shift($fudgefactor);

Next we add in the optional $adj value and put the result back into the end of the array.

      $fudge = $fudge + $this->adj;
      $fudgefactor[] = $fudge;

If a $modulus value has been supplied we use it and possibly reverse the sign on the output value.

      if (!empty($this->mod)) {           // if modifier has been supplied 
         if ($fudge % $this->mod == 0) {  // if it is divisible by modifier 
            $fudge = $fudge * -1;         // reverse then sign 
         } // if 
      } // if 

There is no more 'fudging' left to do, so we can return the value to the calling process.

      return $fudge; 
       
   } // _applyFudgeFactor 

The CheckRange function

This function checks that the value in $num can actually be used as a pointer to an entry in $scramble1.

   function _checkRange ($num) 
   {

First we must round up to the nearest whole number.

      $num = round($num);

We use the length of the scramble string as the limit.

      $limit = strlen($this->scramble1); 

If the value is too high we must reduce it.

      while ($num >= $limit) { 
         $num = $num - $limit; 
      } // while 

If the value is too low we must increase it.

      while ($num < 0) { 
         $num = $num + $limit; 
      } // while 

We can now return a valid pointer back to the calling process.

      return $num; 
       
   } // _checkRange 

The Decrypt function

This function will take an encrypted string and turn it into plain text using the supplied key. Note that this must be exactly the same key that was used to encrypt the string in the first place.

function decrypt ($key, $source) 
   { 

The first step is to convert the key into an array of numbers which I call $fudgefactor. The contents of the _convertkey function can be viewed here.

      $this->errors = array();
       
      $fudgefactor = $this->_convertKey($key); 
      if ($this->errors) return; 

This next piece of code checks that a source string has actually been supplied.

      if (empty($source)) { 
         $this->errors[] = 'No value has been supplied for decryption'; 
         return; 
      } // if 

Here we are setting up a for loop to process each character from $source.

      $target  = NULL; 
      $factor2 = 0; 
      for ($i = 0; $i < strlen($source); $i++) {

Here we extract the next character from $source and identify its position in $scramble2. Note that we cannot continue processing if the character cannot be found.

         $char2 = substr($source, $i, 1); 
         $num2  = strpos($this->scramble2, $char2); 
         if ($num2 === false) { 
            $this->errors[] = "Source string contains an invalid character ($char2)"; 
            return; 
         } // if

Next we obtain the adjustment value from the $fudgefactor array and accumulate it in $factor1 along with the previous contents of $factor2. The contents of the _applyFudgeFactor function can be viewed here.

         $adj = $this->_applyFudgeFactor($fudgefactor); 
         $factor1 = $factor2 + $adj;

Here we add $factor1 to the offset from the $scramble2 string ($num2) to give us the offset into the $scramble1 string ($num1). Note that factor may contain decimal digits, so it has to be rounded in order to supply a whole number.

         $num1    = $num2 - round($factor1);        // generate offset for $scramble1

The value at this point may be a negative number or even a large positive number, so we have to check that it can actually point to a character in the $scramble1 string. The contents of the _checkRange function can be viewed here.

         $num1    = $this->_checkRange($num1);      // check range

As an added complication to confuse potential hackers we are also accumulating the value of $num2 and $factor1 in $factor2.

         $factor2 = $factor1 + $num2;               // accumulate in $factor2 

Here we extract a character from $scramble1 using the value in $num1 and append it to the output string.

         $char1 = substr($this->scramble1, $num1, 1); 
         $target .= $char1;

Finally we close the for loop, return the decrypted string, and close the class.

      } // for 
       
      return rtrim($target); 
       
   } // decrypt 
   
} // end encryption_class

Using the Encryption Class

In order to use this class you must first include/require the file containing the class definition and then create an object or instance of the class.

require 'std.encryption.class.inc';
$crypt = new encryption_class;

To encrypt a string you must supply the string and a key. The string length is optional. Note that before you write this to your database you should use the addslashes() command to deal with any special characters.

$encrypt_result = $crypt->encrypt($key, $password, $pswdlen);
$errors = $crypt->errors;

To decrypt a string you must supply the string and the key that was used to encrypt it.

$decrypt_result = $crypt->decrypt($key, $password);
$errors = $crypt->errors;

Summary

As you can see it is not impossible to create a reasonable encryption function of your own using standard PHP functions. This way you can have your own customisable routine which does not rely on any 3rd party modules which your web hosting service may not feel inclined to provide.


© Tony Marston
14th June 2003

http://www.tonymarston.net
http://www.radicore.org

Amendment history:

16 June 2007 Removed the single quote, double quote and backslash characters from the two scramble strings as these have special meaning in PHP, and can sometimes cause problems.
Removed the need to have the first character of each scramble string duplicated at the end. My thanks to Miroslav Kolar for supplying a fix.
27 August 2004 Amended the decrypt() function so that if $num2 (the offset into $scramble2) points to the first character it is switched to point to the last character, which is a duplicate. For some reason the value zero has a peculiar effect.
14 July 2004 Amended the strings in $scramble1 and $scramble2 so that the first character in each string is also duplicated at the end of that string. This gets round a bijou problemette when the first character of the password is also the first character in $scramble1.

counter