includes/rijndael.php
author Dan
Sun, 04 May 2008 21:57:48 -0400
changeset 541 acb7e23b6ffa
parent 518 2b826f2640e9
child 595 b051eb79b158
permissions -rw-r--r--
Massive commit with various changes. Added user ranks system (no admin interface yet) and ability for users to have custom user titles. Made cron framework accept fractions of hours through floating-point intervals. Modifed ACL editor to use miniPrompt framework for close confirmation box. Made avatar system use a special page as opposed to fetching the files directly for caching reasons.

<?php

/**
 * Phijndael - an implementation of the AES encryption standard in PHP
 * Originally written by Fritz Schneider <fritz AT cd DOT ucsd DOT edu>
 * Ported to PHP by Dan Fuhry <dan AT enano DOT homelinux DOT org>
 * @package phijndael
 * @author Fritz Schneider
 * @author Dan Fuhry
 * @license BSD-style license
 */

define ('ENC_HEX', 201);
define ('ENC_BASE64', 202);
define ('ENC_BINARY', 203);

$_aes_objcache = array();

class AESCrypt {
  
  var $debug = false;
  var $mcrypt = false;
  var $decrypt_cache = array();

  // Rijndael parameters --  Valid values are 128, 192, or 256
  
  var $keySizeInBits = 128;
  var $blockSizeInBits = 128;
  
  ///////  You shouldn't have to modify anything below this line except for
  ///////  the function getRandomBytes().
  //
  // Note: in the following code the two dimensional arrays are indexed as
  //       you would probably expect, as array[row][column]. The state arrays
  //       are 2d arrays of the form state[4][Nb].
  
  
  // The number of rounds for the cipher, indexed by [Nk][Nb]
  var $roundsArray = Array(0,0,0,0,Array(0,0,0,0,10,0, 12,0, 14),0, 
                               Array(0,0,0,0,12,0, 12,0, 14),0, 
                               Array(0,0,0,0,14,0, 14,0, 14) );
  
  // The number of bytes to shift by in shiftRow, indexed by [Nb][row]
  var $shiftOffsets = Array(0,0,0,0,Array(0,1, 2, 3),0,Array(0,1, 2, 3),0,Array(0,1, 3, 4) );
  
  // The round constants used in subkey expansion
  var $Rcon = Array( 
  0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 
  0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 
  0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 
  0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 
  0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 );
  
  // Precomputed lookup table for the SBox
  var $SBox = Array(
   99, 124, 119, 123, 242, 107, 111, 197,  48,   1, 103,  43, 254, 215, 171, 
  118, 202, 130, 201, 125, 250,  89,  71, 240, 173, 212, 162, 175, 156, 164, 
  114, 192, 183, 253, 147,  38,  54,  63, 247, 204,  52, 165, 229, 241, 113, 
  216,  49,  21,   4, 199,  35, 195,  24, 150,   5, 154,   7,  18, 128, 226, 
  235,  39, 178, 117,   9, 131,  44,  26,  27, 110,  90, 160,  82,  59, 214, 
  179,  41, 227,  47, 132,  83, 209,   0, 237,  32, 252, 177,  91, 106, 203, 
  190,  57,  74,  76,  88, 207, 208, 239, 170, 251,  67,  77,  51, 133,  69, 
  249,   2, 127,  80,  60, 159, 168,  81, 163,  64, 143, 146, 157,  56, 245, 
  188, 182, 218,  33,  16, 255, 243, 210, 205,  12,  19, 236,  95, 151,  68,  
  23,  196, 167, 126,  61, 100,  93,  25, 115,  96, 129,  79, 220,  34,  42, 
  144, 136,  70, 238, 184,  20, 222,  94,  11, 219, 224,  50,  58,  10,  73,
    6,  36,  92, 194, 211, 172,  98, 145, 149, 228, 121, 231, 200,  55, 109, 
  141, 213,  78, 169, 108,  86, 244, 234, 101, 122, 174,   8, 186, 120,  37,  
   46,  28, 166, 180, 198, 232, 221, 116,  31,  75, 189, 139, 138, 112,  62, 
  181, 102,  72,   3, 246,  14,  97,  53,  87, 185, 134, 193,  29, 158, 225,
  248, 152,  17, 105, 217, 142, 148, 155,  30, 135, 233, 206,  85,  40, 223,
  140, 161, 137,  13, 191, 230,  66, 104,  65, 153,  45,  15, 176,  84, 187,  
   22 );
  
  // Precomputed lookup table for the inverse SBox
  var $SBoxInverse = Array(
   82,   9, 106, 213,  48,  54, 165,  56, 191,  64, 163, 158, 129, 243, 215, 
  251, 124, 227,  57, 130, 155,  47, 255, 135,  52, 142,  67,  68, 196, 222, 
  233, 203,  84, 123, 148,  50, 166, 194,  35,  61, 238,  76, 149,  11,  66, 
  250, 195,  78,   8,  46, 161, 102,  40, 217,  36, 178, 118,  91, 162,  73, 
  109, 139, 209,  37, 114, 248, 246, 100, 134, 104, 152,  22, 212, 164,  92, 
  204,  93, 101, 182, 146, 108, 112,  72,  80, 253, 237, 185, 218,  94,  21,  
   70,  87, 167, 141, 157, 132, 144, 216, 171,   0, 140, 188, 211,  10, 247, 
  228,  88,   5, 184, 179,  69,   6, 208,  44,  30, 143, 202,  63,  15,   2, 
  193, 175, 189,   3,   1,  19, 138, 107,  58, 145,  17,  65,  79, 103, 220, 
  234, 151, 242, 207, 206, 240, 180, 230, 115, 150, 172, 116,  34, 231, 173,
   53, 133, 226, 249,  55, 232,  28, 117, 223, 110,  71, 241,  26, 113,  29, 
   41, 197, 137, 111, 183,  98,  14, 170,  24, 190,  27, 252,  86,  62,  75, 
  198, 210, 121,  32, 154, 219, 192, 254, 120, 205,  90, 244,  31, 221, 168,
   51, 136,   7, 199,  49, 177,  18,  16,  89,  39, 128, 236,  95,  96,  81,
  127, 169,  25, 181,  74,  13,  45, 229, 122, 159, 147, 201, 156, 239, 160,
  224,  59,  77, 174,  42, 245, 176, 200, 235, 187,  60, 131,  83, 153,  97, 
   23,  43,   4, 126, 186, 119, 214,  38, 225, 105,  20,  99,  85,  33,  12,
  125 );
  
  function __construct($ks = 128, $bs = 128, $debug = false)
  {
    $this->keySizeInBits = $ks;
    $this->blockSizeInBits = $bs;
    
    // Use the Mcrypt library? This speeds things up dramatically.
    if(defined('MCRYPT_RIJNDAEL_' . $ks) && defined('MCRYPT_ACCEL'))
    {
      eval('$mcb = MCRYPT_RIJNDAEL_' . $ks.';');
      $bks = mcrypt_module_get_algo_block_size($mcb);
      $bks = $bks * 8;
      if ( $bks != $bs )
      {
        $mcb = false;
        echo (string)$bks;
      }
    }
    else
    {
      $mcb = false;
    }
      
    $this->mcrypt = $mcb;
    
    // Cipher parameters ... do not change these
    $this->Nk = $this->keySizeInBits / 32;
    $this->Nb = $this->blockSizeInBits / 32;
    $this->Nr = $this->roundsArray[$this->Nk][$this->Nb];
    $this->debug = $debug;
  }
  
  public static function singleton($key_size, $block_size)
  {
    static $_aes_objcache;
    if ( isset($_aes_objcache["$key_size,$block_size"]) )
    {
      return $_aes_objcache["$key_size,$block_size"];
    }
    
    $_aes_objcache["$key_size,$block_size"] = new AESCrypt($key_size, $block_size);
    return $_aes_objcache["$key_size,$block_size"];
  }
  
  // Error handler
  
  function trigger_error($text, $level = E_USER_NOTICE)
  {
    $bt = debug_backtrace();
    $lastfunc =& $bt[1];
    switch($level)
    {
      case E_USER_NOTICE:
      default:
        $desc = 'Notice';
        break;
      case E_USER_WARNING:
        $desc = 'Warning';
        break;
      case E_USER_ERROR:
        $desc = 'Fatal';
        break;
    }
    ob_start();
    if($this->debug || $level == E_USER_ERROR) echo "AES encryption: <b>{$desc}:</b> $text in {$lastfunc['file']} on line {$lastfunc['line']} in function {$lastfunc['function']}<br />";
    if($this->debug)
    {
      //echo '<pre>'.enano_debug_print_backtrace(true).'</pre>';
    }
    ob_end_flush();
    if($level == E_USER_ERROR)
    {
      echo '<p><b>This can sometimes happen if you are upgrading Enano to a new version and did not log out first.</b> <a href="'.$_SERVER['PHP_SELF'].'?do=diag&amp;sub=cookie_destroy">Click here</a> to force cookies to clear and try again. You will be logged out.</p>';
      exit;
    }
  }
  
  function array_slice_js_compat($array, $start, $finish = 0)
  {
    $len = $finish - $start;
    if($len < 0) $len = 0 - $len;
    //if($this->debug) echo (string)$len . ' ';
    //if(count($array) < $start + $len)
    //  $this->trigger_error('Index out of range', E_USER_WARNING);
    return array_slice($array, $start, $len);
  }
  
  function concat($s1, $s2)
  {
    if(is_array($s1) && is_array($s2))
      return array_merge($s1, $s2);
    elseif( ( is_array($s1) && !is_array($s2) ) || ( !is_array($s1) && is_array($s2) ) )
    {
      $this->trigger_error('incompatible types - you can\'t combine a non-array with an array', E_USER_WARNING);
      return false;
    }
    else
      return $s1 . $s2;
  }
  
  // This method circularly shifts the array left by the number of elements
  // given in its parameter. It returns the resulting array and is used for 
  // the ShiftRow step. Note that shift() and push() could be used for a more 
  // elegant solution, but they require IE5.5+, so I chose to do it manually. 
  
  function cyclicShiftLeft($theArray, $positions) {
    if(!is_int($positions))
    {
      $this->trigger_error('$positions is not an integer! Backtrace:<br /><pre>'.print_r(debug_backtrace(), true).'</pre>', E_USER_WARNING);
      return false;
    }
    $second = array_slice($theArray, 0, $positions);
    $first = array_slice($theArray, $positions);
    $theArray = array_merge($first, $second);
    return $theArray;
  }
  
  // Multiplies the element "poly" of GF(2^8) by x. See the Rijndael spec.
  
  function xtime($poly) {
    $poly <<= 1;
    return (($poly & 0x100) ? ($poly ^ 0x11B) : ($poly));
  }
  
  // Multiplies the two elements of GF(2^8) together and returns the result.
  // See the Rijndael spec, but should be straightforward: for each power of
  // the indeterminant that has a 1 coefficient in x, add y times that power
  // to the result. x and y should be bytes representing elements of GF(2^8)
  
  function mult_GF256($x, $y) {
    $result = 0;
    
    for ($bit = 1; $bit < 256; $bit *= 2, $y = $this->xtime($y)) {
      if ($x & $bit) 
        $result ^= $y;
    }
    return $result;
  }
  
  // Performs the substitution step of the cipher. State is the 2d array of
  // state information (see spec) and direction is string indicating whether
  // we are performing the forward substitution ("encrypt") or inverse 
  // substitution (anything else)
  
  function byteSub(&$state, $direction) {
    //global $this->SBox, $this->SBoxInverse, $this->Nb;
    if ($direction == "encrypt")           // Point S to the SBox we're using
      $S =& $this->SBox;
    else
      $S =& $this->SBoxInverse;
    for ($i = 0; $i < 4; $i++)           // Substitute for every byte in state
      for ($j = 0; $j < $this->Nb; $j++)
         $state[$i][$j] = $S[$state[$i][$j]];
  }
  
  // Performs the row shifting step of the cipher.
  
  function shiftRow(&$state, $direction) {
    //global $this->Nb, $this->shiftOffsets;
    for ($i=1; $i<4; $i++)               // Row 0 never shifts
      if ($direction == "encrypt")
         $state[$i] = $this->cyclicShiftLeft($state[$i], $this->shiftOffsets[$this->Nb][$i]);
      else
         $state[$i] = $this->cyclicShiftLeft($state[$i], $this->Nb - $this->shiftOffsets[$this->Nb][$i]);
  
  }
  
  // Performs the column mixing step of the cipher. Most of these steps can
  // be combined into table lookups on 32bit values (at least for encryption)
  // to greatly increase the speed. 
  
  function mixColumn(&$state, $direction) {
    //global $this->Nb;
    $b = Array();                                  // Result of matrix multiplications
    for ($j = 0; $j < $this->Nb; $j++) {                 // Go through each column...
      for ($i = 0; $i < 4; $i++) {                 // and for each row in the column...
        if ($direction == "encrypt")
          $b[$i] = $this->mult_GF256($state[$i][$j], 2) ^ // perform mixing
                   $this->mult_GF256($state[($i+1)%4][$j], 3) ^ 
                   $state[($i+2)%4][$j] ^ 
                   $state[($i+3)%4][$j];
        else 
          $b[$i] = $this->mult_GF256($state[$i][$j], 0xE) ^ 
                   $this->mult_GF256($state[($i+1)%4][$j], 0xB) ^
                   $this->mult_GF256($state[($i+2)%4][$j], 0xD) ^
                   $this->mult_GF256($state[($i+3)%4][$j], 9);
      }
      for ($i = 0; $i < 4; $i++)          // Place result back into column
        $state[$i][$j] = $b[$i];
    }
  }
  
  // Adds the current round key to the state information. Straightforward.
  
  function addRoundKey(&$state, $roundKey) {
    //global $this->Nb;
    for ($j = 0; $j < $this->Nb; $j++) {                      // Step through columns...
      $state[0][$j] ^= ( $roundKey[$j] & 0xFF);         // and XOR
      $state[1][$j] ^= (($roundKey[$j]>>8) & 0xFF);
      $state[2][$j] ^= (($roundKey[$j]>>16) & 0xFF);
      $state[3][$j] ^= (($roundKey[$j]>>24) & 0xFF);
    }
  }
  
  // This function creates the expanded key from the input (128/192/256-bit)
  // key. The parameter key is an array of bytes holding the value of the key.
  // The returned value is an array whose elements are the 32-bit words that 
  // make up the expanded key.
  
  function keyExpansion($key) {
    //global $this->keySizeInBits, $this->blockSizeInBits, $this->roundsArray, $this->Nk, $this->Nb, $this->Nr, $this->Nk, $this->SBox, $this->Rcon;
    $expandedKey = Array();
  
    // in case the key size or parameters were changed...
    $this->Nk = $this->keySizeInBits / 32;                   
    $this->Nb = $this->blockSizeInBits / 32;
    $this->Nr = $this->roundsArray[$this->Nk][$this->Nb];
  
    for ($j=0; $j < $this->Nk; $j++)     // Fill in input key first
      $expandedKey[$j] = 
        ($key[4*$j]) | ($key[4*$j+1]<<8) | ($key[4*$j+2]<<16) | ($key[4*$j+3]<<24);
  
    // Now walk down the rest of the array filling in expanded key bytes as
    // per Rijndael's spec
    for ($j = $this->Nk; $j < $this->Nb * ($this->Nr + 1); $j++) {    // For each word of expanded key
      $temp = $expandedKey[$j - 1];
      if ($j % $this->Nk == 0) 
        $temp = ( ($this->SBox[($temp>>8) & 0xFF]) |
                  ($this->SBox[($temp>>16) & 0xFF]<<8) |
                  ($this->SBox[($temp>>24) & 0xFF]<<16) |
                  ($this->SBox[$temp & 0xFF]<<24) ) ^ $this->Rcon[floor($j / $this->Nk) - 1];
      elseif  ($this->Nk > 6 && $j % $this->Nk == 4)
        $temp = ($this->SBox[($temp>>24) & 0xFF]<<24) |
               ($this->SBox[($temp>>16) & 0xFF]<<16) |
               ($this->SBox[($temp>>8) & 0xFF]<<8) |
               ($this->SBox[ $temp & 0xFF]);
      $expandedKey[$j] = $expandedKey[$j-$this->Nk] ^ $temp;
    }
    return $expandedKey;
  }
  
  // Rijndael's round functions... 
  
  function RijndaelRound(&$state, $roundKey) {
    $this->byteSub($state, "encrypt");
    $this->shiftRow($state, "encrypt");
    $this->mixColumn($state, "encrypt");
    $this->addRoundKey($state, $roundKey);
  }
  
  function InverseRijndaelRound(&$state, $roundKey) {
    $this->addRoundKey($state, $roundKey);
    $this->mixColumn($state, "decrypt");
    $this->shiftRow($state, "decrypt");
    $this->byteSub($state, "decrypt");
  }
  
  function FinalRijndaelRound(&$state, $roundKey) {
    $this->byteSub($state, "encrypt");
    $this->shiftRow($state, "encrypt");
    $this->addRoundKey($state, $roundKey);
  }
  
  function InverseFinalRijndaelRound(&$state, $roundKey){
    $this->addRoundKey($state, $roundKey);
    $this->shiftRow($state, "decrypt");
    $this->byteSub($state, "decrypt");  
  }
  
  // encrypt is the basic encryption function. It takes parameters
  // block, an array of bytes representing a plaintext block, and expandedKey,
  // an array of words representing the expanded key previously returned by
  // keyExpansion(). The ciphertext block is returned as an array of bytes.
  
  function cryptBlock($block, $expandedKey) {
    //global $this->blockSizeInBits, $this->Nb, $this->Nr;
    $t=count($block)*8;
    if (!is_array($block) || count($block)*8 != $this->blockSizeInBits)
    {
      $this->trigger_error('block is bad or block size is wrong<pre>'.print_r($block, true).'</pre><p>Aiming for size '.$this->blockSizeInBits.', got '.$t.'.', E_USER_WARNING); 
      return false;
    }
    if (!$expandedKey)
      return;
  
    $block = $this->packBytes($block);
    $this->addRoundKey($block, $expandedKey);
    for ($i=1; $i<$this->Nr; $i++) 
      $this->RijndaelRound($block, $this->array_slice_js_compat($expandedKey, $this->Nb*$i, $this->Nb*($i+1)));
    $this->FinalRijndaelRound($block, $this->array_slice_js_compat($expandedKey, $this->Nb*$this->Nr));
    $ret = $this->unpackBytes($block);
    return $ret;
  }
  
  // decrypt is the basic decryption function. It takes parameters
  // block, an array of bytes representing a ciphertext block, and expandedKey,
  // an array of words representing the expanded key previously returned by
  // keyExpansion(). The decrypted block is returned as an array of bytes.
  
  function unCryptBlock($block, $expandedKey) {
    $t = count($block)*8;
    if (!is_array($block) || count($block)*8 != $this->blockSizeInBits)
    {
      $this->trigger_error('$block is not a valid rijndael-block array: '.$this->byteArrayToHex($block).'<pre>'.print_r($block, true).'</pre><p>Block size is '.$t.', should be '.$this->blockSizeInBits.'</p>', E_USER_WARNING);
      return false;
    }
    if (!$expandedKey)
    {
      $this->trigger_error('$expandedKey is invalid', E_USER_WARNING);
      return false;
    }
  
    $block = $this->packBytes($block);
    $this->InverseFinalRijndaelRound($block, $this->array_slice_js_compat($expandedKey, $this->Nb*$this->Nr)); 
    for ($i = $this->Nr - 1; $i>0; $i--) 
    {
      $this->InverseRijndaelRound($block, $this->array_slice_js_compat($expandedKey, $this->Nb*$i, $this->Nb*($i+1)));
    }
    $this->addRoundKey($block, $expandedKey);
    $ret = $this->unpackBytes($block);
    if(!is_array($ret))
    {
      $this->trigger_error('$ret is not an array', E_USER_WARNING);
    }
    return $ret;
  }
  
  // This method takes a byte array (byteArray) and converts it to a string by
  // applying String.fromCharCode() to each value and concatenating the result.
  // The resulting string is returned. Note that this function SKIPS zero bytes
  // under the assumption that they are padding added in formatPlaintext().
  // Obviously, do not invoke this method on raw data that can contain zero
  // bytes. It is really only appropriate for printable ASCII/Latin-1 
  // values. Roll your own function for more robust functionality :)
  
  function byteArrayToString($byteArray) {
    $result = "";
    for($i=0; $i<count($byteArray); $i++)
      if ($byteArray[$i] != 0) 
        $result .= chr($byteArray[$i]);
    return $result;
  }
  
  // This function takes an array of bytes (byteArray) and converts them
  // to a hexadecimal string. Array element 0 is found at the beginning of 
  // the resulting string, high nibble first. Consecutive elements follow
  // similarly, for example [16, 255] --> "10ff". The function returns a 
  // string.
  
  /*
  function byteArrayToHex($byteArray) {
    $result = "";
    if (!$byteArray)
      return;
    for ($i=0; $i<count($byteArray); $i++)
      $result .= (($byteArray[$i]<16) ? "0" : "") + toString($byteArray[$i]); // magic number here is 16, not sure how to handle this...
  
    return $result;
  }
  */
  function byteArrayToHex($arr)
  {
    $ret = '';
    foreach($arr as $a)
    {
      $nibble = (string)dechex(intval($a));
      if(strlen($nibble) == 1) $nibble = '0' . $nibble;
      $ret .= $nibble;
    }
    return $ret;
  }
  
  // PHP equivalent of Javascript's toString()
  function toString($bool)
  {
    if(is_bool($bool))
      return ($bool) ? 'true' : 'false';
    elseif(is_array($bool))
      return implode(',', $bool);
    else
      return (string)$bool;
  }
  
  // This function converts a string containing hexadecimal digits to an 
  // array of bytes. The resulting byte array is filled in the order the
  // values occur in the string, for example "10FF" --> [16, 255]. This
  // function returns an array. 
  
  /*
  function hexToByteArray($hexString) {
    $byteArray = Array();
    if (strlen($hexString) % 2)             // must have even length
      return;
    if (strstr($hexString, "0x") == $hexString || strstr($hexString, "0X") == $hexString)
      $hexString = substr($hexString, 2);
    for ($i = 0; $i<strlen($hexString); $i++,$i++) 
      $byteArray[floor($i/2)] = intval(substr($hexString, $i, 2)); // again, that strange magic number: 16
    return $byteArray;
  }
  */
  function hexToByteArray($str)
  {
    if(substr($str, 0, 2) == '0x' || substr($str, 0, 2) == '0X')
      $str = substr($str, 2);
    $arr = Array();
    $str = $this->enano_str_split($str, 2);
    foreach($str as $s)
    {
      $arr[] = intval(hexdec($s));
    }
    return $arr;
  }
  
  // This function packs an array of bytes into the four row form defined by
  // Rijndael. It assumes the length of the array of bytes is divisible by
  // four. Bytes are filled in according to the Rijndael spec (starting with
  // column 0, row 0 to 3). This function returns a 2d array.
  
  function packBytes($octets) {
    $state = Array();
    if (!$octets || count($octets) % 4)
      return;
  
    $state[0] = Array(); $state[1] = Array(); 
    $state[2] = Array(); $state[3] = Array();
    for ($j=0; $j<count($octets); $j = $j+4) {
       $state[0][$j/4] = $octets[$j];
       $state[1][$j/4] = $octets[$j+1];
       $state[2][$j/4] = $octets[$j+2];
       $state[3][$j/4] = $octets[$j+3];
    }
    return $state;
  }
  
  // This function unpacks an array of bytes from the four row format preferred
  // by Rijndael into a single 1d array of bytes. It assumes the input "packed"
  // is a packed array. Bytes are filled in according to the Rijndael spec. 
  // This function returns a 1d array of bytes.
  
  function unpackBytes($packed) {
    $result = Array();
    for ($j=0; $j<count($packed[0]); $j++) {
      $result[] = $packed[0][$j];
      $result[] = $packed[1][$j];
      $result[] = $packed[2][$j];
      $result[] = $packed[3][$j];
    }
    return $result;
  }
  
  function charCodeAt($str, $i)
  {
    return ord(substr($str, $i, 1));
  }
  
  function fromCharCode($str)
  {
    return chr($str);
  }
  
  // This function takes a prospective plaintext (string or array of bytes)
  // and pads it with zero bytes if its length is not a multiple of the block 
  // size. If plaintext is a string, it is converted to an array of bytes
  // in the process. The type checking can be made much nicer using the 
  // instanceof operator, but this operator is not available until IE5.0 so I 
  // chose to use the heuristic below. 
  
  function formatPlaintext($plaintext) {
    //global $this->blockSizeInBits;
    $bpb = $this->blockSizeInBits / 8;               // bytes per block
  
    // if primitive string or String instance
    if (is_string($plaintext)) {
      $plaintext = $this->enano_str_split($plaintext);
      // Unicode issues here (ignoring high byte)
      for ($i=0; $i<sizeof($plaintext); $i++)
        $plaintext[$i] = $this->charCodeAt($plaintext[$i], 0) & 0xFF;
    } 
  
    for ($i = $bpb - (sizeof($plaintext) % $bpb); $i > 0 && $i < $bpb; $i--) 
      $plaintext[] = 0;
    
    return $plaintext;
  }
  
  // Returns an array containing "howMany" random bytes. YOU SHOULD CHANGE THIS
  // TO RETURN HIGHER QUALITY RANDOM BYTES IF YOU ARE USING THIS FOR A "REAL"
  // APPLICATION. (edit: done, mt_rand() is relatively secure)
  
  function getRandomBytes($howMany) {
    $bytes = Array();
    for ($i=0; $i<$howMany; $i++)
      $bytes[$i] = mt_rand(0, 255);
    return $bytes;
  }
  
  // rijndaelEncrypt(plaintext, key, mode)
  // Encrypts the plaintext using the given key and in the given mode. 
  // The parameter "plaintext" can either be a string or an array of bytes. 
  // The parameter "key" must be an array of key bytes. If you have a hex 
  // string representing the key, invoke hexToByteArray() on it to convert it 
  // to an array of bytes. The third parameter "mode" is a string indicating
  // the encryption mode to use, either "ECB" or "CBC". If the parameter is
  // omitted, ECB is assumed.
  // 
  // An array of bytes representing the cihpertext is returned. To convert 
  // this array to hex, invoke byteArrayToHex() on it. If you are using this 
  // "for real" it is a good idea to change the function getRandomBytes() to 
  // something that returns truly random bits.
  
  function rijndaelEncrypt($plaintext, $key, $mode = 'ECB') {
    //global $this->blockSizeInBits, $this->keySizeInBits;
    $bpb = $this->blockSizeInBits / 8;          // bytes per block
    // var ct;                                 // ciphertext
  
    if($mode == 'CBC')
    {
      if (!is_string($plaintext) || !is_array($key))
      {
        $this->trigger_error('In CBC mode the first and second parameters should be strings', E_USER_WARNING);
        return false;
      }
    } else {
      if (!is_array($plaintext) || !is_array($key))
      {
        $this->trigger_error('In ECB mode the first and second parameters should be byte arrays', E_USER_WARNING);
        return false;
      }
    }
    if (sizeof($key)*8 != $this->keySizeInBits)
    {
      $this->trigger_error('The key needs to be '. ( $this->keySizeInBits / 8 ) .' bytes in length', E_USER_WARNING);
      return false;
    }
    if ($mode == "CBC")
      $ct = $this->getRandomBytes($bpb);             // get IV
    else {
      $mode = "ECB";
      $ct = Array();
    }
    
    // convert plaintext to byte array and pad with zeros if necessary. 
    $plaintext = $this->formatPlaintext($plaintext);
    
    $expandedKey = $this->keyExpansion($key);
    
    for ($block=0; $block<sizeof($plaintext) / $bpb; $block++) {
      $aBlock = $this->array_slice_js_compat($plaintext, $block*$bpb, ($block+1)*$bpb);
      if ($mode == "CBC")
      {
        for ($i=0; $i<$bpb; $i++)
        {
          $aBlock[$i] ^= $ct[$block*$bpb + $i];
        }
      }
      $cp = $this->cryptBlock($aBlock, $expandedKey);
      $ct = $this->concat($ct, $cp);
    }
  
    return $ct;
  }
  
  // rijndaelDecrypt(ciphertext, key, mode)
  // Decrypts the using the given key and mode. The parameter "ciphertext" 
  // must be an array of bytes. The parameter "key" must be an array of key 
  // bytes. If you have a hex string representing the ciphertext or key, 
  // invoke hexToByteArray() on it to convert it to an array of bytes. The
  // parameter "mode" is a string, either "CBC" or "ECB".
  // 
  // An array of bytes representing the plaintext is returned. To convert 
  // this array to a hex string, invoke byteArrayToHex() on it. To convert it 
  // to a string of characters, you can use byteArrayToString().
  
  function rijndaelDecrypt($ciphertext, $key, $mode = 'ECB') {
    //global $this->blockSizeInBits, $this->keySizeInBits;
    $bpb = $this->blockSizeInBits / 8;          // bytes per block
    $pt = Array();                   // plaintext array
    // $aBlock;                             // a decrypted block
    // $block;                              // current block number
  
    if (!$ciphertext)
    {
      $this->trigger_error('$ciphertext should be a byte array', E_USER_WARNING);
      return false;
    }
    if(  !is_array($key) )
    {
      $this->trigger_error('$key should be a byte array', E_USER_WARNING);
      return false;
    }
    if( is_string($ciphertext) )
    {
      $this->trigger_error('$ciphertext should be a byte array', E_USER_WARNING);
      return false;
    }
    if (sizeof($key)*8 != $this->keySizeInBits)
    {
      $this->trigger_error('Encryption key is the wrong length', E_USER_WARNING);
      return false;
    }
    if (!$mode)
      $mode = "ECB";                         // assume ECB if mode omitted
  
    $expandedKey = $this->keyExpansion($key);
   
    // work backwards to accomodate CBC mode 
    for ($block=(sizeof($ciphertext) / $bpb)-1; $block>0; $block--)
    {
      if( ( $block*$bpb ) + ( ($block+1)*$bpb ) > count($ciphertext) )
      {
        //$this->trigger_error('$ciphertext index out of bounds', E_USER_ERROR);
      }
      $current_block = $this->array_slice_js_compat($ciphertext, $block*$bpb, ($block+1)*$bpb);
      if(count($current_block) * 8 != $this->blockSizeInBits)
      {
        // $c=count($current_block)*8;
        // $this->trigger_error('We got a '.$c.'-bit block, instead of '.$this->blockSizeInBits.'', E_USER_ERROR);
      }
      $aBlock = $this->uncryptBlock($current_block, $expandedKey);
      if(!$aBlock)
      {
        $this->trigger_error('Shared block decryption routine returned false', E_USER_WARNING);
        return false;
      }
      if ($mode == "CBC")
        for ($i=0; $i<$bpb; $i++) 
          $pt[($block-1)*$bpb + $i] = $aBlock[$i] ^ $ciphertext[($block-1)*$bpb + $i];
      else
        $pt = $this->concat($aBlock, $pt);
    }
  
    // do last block if ECB (skips the IV in CBC)
    if ($mode == "ECB")
    {
      $x = $this->uncryptBlock($this->array_slice_js_compat($ciphertext, 0, $bpb), $expandedKey);
      if(!$x)
      {
        $this->trigger_error('ECB block decryption routine returned false', E_USER_WARNING);
        return false;
      }
      $pt = $this->concat($x, $pt);
      if(!$pt)
      {
        $this->trigger_error('ECB concatenation routine returned false', E_USER_WARNING);
        return false;
      }
    }
  
    return $pt;
  }
  
  /**
   * Wrapper for encryption.
   * @param string $text the text to encrypt
   * @param string $key the raw binary key to encrypt with
   * @param int $return_encoding optional - can be ENC_BINARY, ENC_HEX or ENC_BASE64
   */
   
  function encrypt($text, $key, $return_encoding = ENC_HEX)
  {
    if ( $text == '' )
      return '';
    if ( $this->mcrypt && $this->blockSizeInBits == mcrypt_module_get_algo_block_size(eval('return MCRYPT_RIJNDAEL_'.$this->keySizeInBits.';')) )
    {
      $iv_size = mcrypt_get_iv_size($this->mcrypt, MCRYPT_MODE_ECB);
      $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
      $cryptext = mcrypt_encrypt($this->mcrypt, $key, $text, MCRYPT_MODE_ECB, $iv);
      switch($return_encoding)
      {
        case ENC_HEX:
        default:
          $cryptext = $this->strtohex($cryptext);
          break;
        case ENC_BINARY:
          $cryptext = $cryptext;
          break;
        case ENC_BASE64:
          $cryptext = base64_encode($cryptext);
          break;
      }
    }
    else
    {
      $key = $this->prepare_string($key);
      $text = $this->prepare_string($text);
      profiler_log('AES: Started encryption of a string');
      $cryptext = $this->rijndaelEncrypt($text, $key, 'ECB');
      profiler_log('AES: Finished encryption of a string');
      if(!is_array($cryptext))
      {
        echo 'Warning: encryption failed for string: '.print_r($text,true).'<br />';
        return false;
      }
      switch($return_encoding)
      {
        case ENC_HEX:
        default:
          $cryptext = $this->byteArrayToHex($cryptext);
          break;
        case ENC_BINARY:
          $cryptext = $this->byteArrayToString($cryptext);
          break;
        case ENC_BASE64:
          $cryptext = base64_encode($this->byteArrayToString($cryptext));
          break;
      }
    }
    return $cryptext;
  }
  
  /**
   * Wrapper for decryption.
   * @param string $text the encrypted text
   * @param string $key the raw binary key used to encrypt the text
   * @param int $input_encoding the encoding used for the encrypted string. Can be ENC_BINARY, ENC_HEX, or ENC_BASE64.
   * @param bool $no_cache If true, will not cache the decrypted string on disk.
   * @return string
   */
   
  function decrypt($text, $key, $input_encoding = ENC_HEX, $no_cache = false)
  {
    if ( $text == '' )
      return '';
    
    switch($input_encoding)
    {
      case ENC_BINARY:
      default:
        break;
      case ENC_HEX:
        $text = $this->hextostring($text);
        break;
      case ENC_BASE64:
        $text = base64_decode($text);
        break;
    }
    
    // Run memory-cache check
    if ( isset($this->decrypt_cache[$key]) && is_array($this->decrypt_cache[$key]) )
    {
      if ( isset($this->decrypt_cache[$key][$text]) )
      {
        return $this->decrypt_cache[$key][$text];
      }
    }
    
    // Run disk-cache check
    $hash = sha1($text . '::' . $key);
    if ( $dypt = aes_decrypt_cache_fetch($hash) )
      return $dypt;
    
    $text_bin = $text;
    $key_bin = $key;
    
    if ( $this->mcrypt )
    {
      $iv_size = mcrypt_get_iv_size($this->mcrypt, MCRYPT_MODE_ECB);
      $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
      $dypt = mcrypt_decrypt($this->mcrypt, $key, $text, MCRYPT_MODE_ECB, $iv);
    }
    else
    {
      $etext = $this->prepare_string($text);
      $ekey  = $this->prepare_string($key);
      $mod = count($etext) % $this->blockSizeInBits;
      profiler_log('AES: Started decryption of a string');
      $dypt = $this->rijndaelDecrypt($etext, $ekey, 'ECB');
      profiler_log('AES: Finished decryption of a string');
      if(!$dypt)
      {
        echo '<pre>'.print_r($dypt, true).'</pre>';
        $this->trigger_error('Rijndael main decryption routine failed', E_USER_ERROR);
      }
      $dypt = $this->byteArrayToString($dypt);
    }
    if ( !isset($this->decrypt_cache[$key_bin]) )
      $this->decrypt_cache[$key_bin] = array();
    
    $this->decrypt_cache[$key_bin][$text_bin] = $dypt;
    
    if ( !$no_cache )
      aes_decrypt_cache_store($text_bin, $dypt, $key_bin);
    
    return $dypt;
  }
  
  /**
   * Enano-ese equivalent of str_split() which is only found in PHP5
   * @param $text string the text to split
   * @param $inc int size of each block
   * @return array
   */
   
  function enano_str_split($text, $inc = 1)
  {
    if($inc < 1) return false;
    if($inc >= strlen($text)) return Array($text);
    $len = ceil(strlen($text) / $inc);
    $ret = Array();
    for($i=0;$i<strlen($text);$i=$i+$inc)
    {
      $ret[] = substr($text, $i, $inc);
    }
    return $ret;
  }
  
  /**
   * Generates a random key suitable for encryption
   * @param int $len the length of the key, in bytes
   * @return string a BINARY key
   */
  
  function randkey($len = 32)
  {
    $key = '';
    for($i=0;$i<$len;$i++)
    {
      $key .= chr(mt_rand(0, 255));
    }
    if ( @file_exists('/dev/urandom') && @is_readable('/dev/urandom') )
    {
      // Let's use something a little more secure
      $ur = @fopen('/dev/urandom', 'r');
      if ( !$ur )
        return $key;
      $ukey = @fread($ur, $len);
      fclose($ur);
      if ( strlen($ukey) != $len )
        return $key;
      return $ukey;
    }
    return $key;
  }
  
  /*
  function byteArrayToString($arr)
  {
    if(!is_array($arr))
    {
      $this->trigger_error('First parameter should be an array', E_USER_WARNING);
      return false;
    }
    $ret = '';
    foreach($arr as $a)
    {
      if($a != 0) $ret .= chr($a);
    }
    return $ret;
  }
  */
  
  function strtohex($str)
  {
    $str = $this->enano_str_split($str);
    $ret = '';
    foreach($str as $s)
    {
      $chr = dechex(ord($s));
      if(strlen($chr) < 2) $chr = '0' . $chr;
      $ret .= $chr;
    }
    return $ret;
  }
  
  function gen_readymade_key()
  {
    $key = $this->strtohex($this->randkey($this->keySizeInBits / 8));
    return $key;
  }
  
  function prepare_string($text)
  {
    $ret = $this->hexToByteArray($this->strtohex($text));
    if(count($ret) != strlen($text))
    {
      die('Could not convert string "' . $text . '" to hex byte array for encryption');
    }
    return $ret;
  }
  
  /**
   * Decodes a hex string.
   * @param string $hex The hex code to decode
   * @return string
   */
  
  function hextostring($hex)
  {
    $hex = $this->enano_str_split($hex, 2);
    $bin_key = '';
    foreach($hex as $nibble)
    {
      $byte = chr(hexdec($nibble));
      $bin_key .= $byte;
    }
    return $bin_key;
  }
}

function aes_decrypt_cache_store($encrypted, $decrypted, $key)
{
  $cache_file = ENANO_ROOT . '/cache/aes_decrypt.php';
  // only cache if $decrypted is long enough to actually warrant caching
  if ( strlen($decrypted) < 32 )
  {
    profiler_log("AES: Skipped caching a string (probably a password, we dunno) because it's too short");
    return false;
  }
  if ( file_exists($cache_file) )
  {
    require_once($cache_file);
    global $aes_decrypt_cache;
    $cachekey = sha1($encrypted . '::' . $key);
    $aes_decrypt_cache[$cachekey] = $decrypted;
    
    if ( count($aes_decrypt_cache) > 5000 )
    {
      // we've got a lot of strings in the cache, clear out a few
      $keys = array_keys($aes_decrypt_cache);
      for ( $i = 0; $i < 2500; $i++ )
      {
        unset($aes_decrypt_cache[$keys[$i]]);
        unset($aes_decrypt_cache[$keys[$i]]);
      }
    }
  }
  else
  {
    $aes_decrypt_cache = array(
      sha1($encrypted . '::' . $key) => $decrypted
    );
  }
  // call var_export and collect contents
  ob_start();
  var_export($aes_decrypt_cache);
  $dec_cache_string = ob_get_contents();
  ob_end_clean();
  $f = @fopen($cache_file, 'w');
  if ( !$f )
    return false;
  fwrite($f, "<?php
\$GLOBALS['aes_decrypt_cache'] = $dec_cache_string;
");
  fclose($f);
  return true;
}

function aes_decrypt_cache_fetch($hash)
{
  $cache_file = ENANO_ROOT . '/cache/aes_decrypt.php';
  if ( !file_exists($cache_file) )
    return false;
  
  require_once($cache_file);
  global $aes_decrypt_cache;
  if ( isset($aes_decrypt_cache[$hash]) )
  {
    profiler_log("AES: Loaded cached decrypted string, hash is $hash");
    return $aes_decrypt_cache[$hash];
  }
  
  return false;
}

function aes_decrypt_cache_destroy($hash)
{
  $cache_file = ENANO_ROOT . '/cache/aes_decrypt.php';
  if ( !file_exists($cache_file) )
    return false;
  
  require_once($cache_file);
  global $aes_decrypt_cache;
  
  if ( isset($aes_decrypt_cache[$hash]) )
    unset($aes_decrypt_cache[$hash]);
  
  // call var_export and collect contents
  ob_start();
  var_export($aes_decrypt_cache);
  $dec_cache_string = ob_get_contents();
  ob_end_clean();
  $f = @fopen($cache_file, 'w');
  if ( !$f )
    return false;
  fwrite($f, "<?php
\$GLOBALS['aes_decrypt_cache'] = $dec_cache_string;
");
  fclose($f);
  return true;
}

?>