PageRenderTime 433ms CodeModel.GetById 81ms app.highlight 199ms RepoModel.GetById 52ms app.codeStats 1ms

/users/CookieStorage.php

http://showslow.googlecode.com/
PHP | 249 lines | 179 code | 20 blank | 50 comment | 19 complexity | 3ac50e0c9be4b0979120d021c495850b MD5 | raw file
  1<?php
  2/**
  3 * Store tamper-proof strings in an HTTP cookie
  4 *
  5 * Source: http://code.google.com/p/mrclay/source/browse/trunk/php/MrClay/CookieStorage.php
  6 *
  7 * <code>
  8 * $storage = new MrClay_CookieStorage(array(
  9 *     'secret' => '67676kmcuiekihbfyhbtfitfytrdo=op-p-=[hH8'
 10 * ));
 11 * if ($storage->store('user', 'id:62572,email:bob@yahoo.com,name:Bob')) {
 12 *    // cookie OK length and no complaints from setcookie()
 13 * } else {
 14 *    // check $storage->errors
 15 * }
 16 * 
 17 * // later request
 18 * $user = $storage->fetch('user');
 19 * if (is_string($user)) {
 20 *    // valid cookie
 21 *    $age = time() - $storage->getTimestamp('user');
 22 * } else {
 23 *     if (false === $user) {
 24 *         // data was altered!
 25 *     } else {
 26 *         // cookie not present
 27 *     }
 28 * }
 29 * 
 30 * // encrypt cookie contents
 31 * $storage = new MrClay_CookieStorage(array(
 32 *     'secret' => '67676kmcuiekihbfyhbtfitfytrdo=op-p-=[hH8'
 33 *     ,'mode' => MrClay_CookieStorage::MODE_ENCRYPT
 34 * ));
 35 * </code>
 36 */
 37class MrClay_CookieStorage {
 38
 39    // conservative storage limit considering variable-length Set-Cookie header
 40    const LENGTH_LIMIT = 3896;
 41    const MODE_VISIBLE = 0;
 42    const MODE_ENCRYPT = 1;
 43    
 44    /**
 45     * @var array errors that occured
 46     */
 47    public $errors = array();
 48
 49
 50    public function __construct($options = array())
 51    {
 52        $this->_o = array_merge(self::getDefaults(), $options);
 53    }
 54    
 55    public static function hash($input)
 56    {
 57        return str_replace('=', '', base64_encode(hash('ripemd160', $input, true)));
 58    }
 59    
 60    public static function encrypt($key, $str)
 61    {
 62        $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB);  
 63        $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);  
 64        $data = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $str, MCRYPT_MODE_ECB, $iv);
 65        return base64_encode($data);
 66    }
 67    
 68    public static function decrypt($key, $data)
 69    {
 70        if (false === ($data = base64_decode($data))) {
 71            return false;
 72        }
 73        $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB);  
 74        $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);  
 75        return mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $data, MCRYPT_MODE_ECB, $iv);
 76    }
 77
 78    public function getDefaults()
 79    {
 80        return array(
 81            'secret' => ''
 82            ,'domain' => ''
 83            ,'secure' => false
 84            ,'path' => '/'
 85            ,'expire' => '2147368447' // Sun, 17-Jan-2038 19:14:07 GMT (Google)
 86            ,'hashFunc' => array('MrClay_CookieStorage', 'hash')
 87            ,'encryptFunc' => array('MrClay_CookieStorage', 'encrypt')
 88            ,'decryptFunc' => array('MrClay_CookieStorage', 'decrypt')
 89            ,'mode' => self::MODE_VISIBLE
 90	    ,'httponly' => false
 91        );
 92    }
 93
 94    public function setOption($name, $value)
 95    {
 96        $this->_o[$name] = $value;
 97    }
 98
 99    /**
100     * @return bool success
101     */
102    public function store($name, $str)
103    {
104        if (empty($this->_o['secret'])) {
105            $this->errors[] = 'Must first set the option: secret.';
106            return false;
107        }
108        return ($this->_o['mode'] === self::MODE_ENCRYPT)
109            ? $this->_storeEncrypted($name, $str)
110            : $this->_store($name, $str);
111    }
112    
113    private function _store($name, $str)
114    {
115        if (! is_callable($this->_o['hashFunc'])) {
116            $this->errors[] = 'Hash function not callable';
117            return false;
118        }
119        $time = base_convert($_SERVER['REQUEST_TIME'], 10, 36); // pack time
120        // tie sig to this cookie name
121        $hashInput = $this->_o['secret'] . $name . $time . $str;
122        $sig = call_user_func($this->_o['hashFunc'], $hashInput);
123        $raw = $sig . '|' . $time . '|' . $str;
124        if (strlen($name . $raw) > self::LENGTH_LIMIT) {
125            $this->errors[] = 'Cookie is likely too large to store.';
126            return false;
127        }
128        $res = setcookie($name, $raw, $this->_o['expire'], $this->_o['path'], 
129                         $this->_o['domain'], $this->_o['secure'], $this->_o['httponly']);
130        if ($res) {
131            return true;
132        } else {
133            $this->errors[] = 'Setcookie() returned false. Headers may have been sent.';
134            return false;
135        }
136    }
137    
138    private function _storeEncrypted($name, $str)
139    {
140        if (! is_callable($this->_o['encryptFunc'])) {
141            $this->errors[] = 'Encrypt function not callable';
142            return false;
143        }
144        $time = base_convert($_SERVER['REQUEST_TIME'], 10, 36); // pack time
145        $key = self::hash($this->_o['secret']);
146        $raw = call_user_func($this->_o['encryptFunc'], $key, $key . $time . '|' . $str);
147        if (strlen($name . $raw) > self::LENGTH_LIMIT) {
148            $this->errors[] = 'Cookie is likely too large to store.';
149            return false;
150        }
151        $res = setcookie($name, $raw, $this->_o['expire'], $this->_o['path'], 
152                         $this->_o['domain'], $this->_o['secure'], $this->_o['httponly']);
153        if ($res) {
154            return true;
155        } else {
156            $this->errors[] = 'Setcookie() returned false. Headers may have been sent.';
157            return false;
158        }
159    }
160
161    /**
162     * @return string null if cookie not set, false if tampering occured
163     */
164    public function fetch($name)
165    {
166        if (!isset($_COOKIE[$name])) {
167            return null;
168        }
169        return ($this->_o['mode'] === self::MODE_ENCRYPT)
170            ? $this->_fetchEncrypted($name)
171            : $this->_fetch($name);
172    }
173    
174    private function _fetch($name)
175    {
176        if (isset($this->_returns[self::MODE_VISIBLE][$name])) {
177            return $this->_returns[self::MODE_VISIBLE][$name][0];
178        }
179        $cookie = get_magic_quotes_gpc()
180            ? stripslashes($_COOKIE[$name])
181            : $_COOKIE[$name];
182        $parts = explode('|', $cookie, 3);
183        if (3 !== count($parts)) {
184            $this->errors[] = 'Cookie was tampered with.';
185            return false;
186        }
187        list($sig, $time, $str) = $parts;
188        $hashInput = $this->_o['secret'] . $name . $time . $str;
189        if ($sig !== call_user_func($this->_o['hashFunc'], $hashInput)) {
190            $this->errors[] = 'Cookie was tampered with.';
191            return false;
192        }
193        $time = base_convert($time, 36, 10); // unpack time
194        $this->_returns[self::MODE_VISIBLE][$name] = array($str, $time);
195        return $str;
196    }
197    
198    private function _fetchEncrypted($name)
199    {
200        if (isset($this->_returns[self::MODE_ENCRYPT][$name])) {
201            return $this->_returns[self::MODE_ENCRYPT][$name][0];
202        }
203        if (! is_callable($this->_o['decryptFunc'])) {
204            $this->errors[] = 'Decrypt function not callable';
205            return false;
206        }
207        $cookie = get_magic_quotes_gpc()
208            ? stripslashes($_COOKIE[$name])
209            : $_COOKIE[$name];
210        $key = self::hash($this->_o['secret']);
211        $timeStr = call_user_func($this->_o['decryptFunc'], $key, $cookie);
212        if (! $timeStr) {
213            $this->errors[] = 'Cookie was tampered with.';
214            return false;
215        }
216        $timeStr = rtrim($timeStr, "\x00");
217        // verify decryption
218        if (0 !== strpos($timeStr, $key)) {
219            $this->errors[] = 'Cookie was tampered with.';
220            return false;
221        }
222        $timeStr = substr($timeStr, strlen($key));
223        list($time, $str) = explode('|', $timeStr, 2);
224        $time = base_convert($time, 36, 10); // unpack time
225        $this->_returns[self::MODE_ENCRYPT][$name] = array($str, $time);
226        return $str;
227    }
228
229    public function getTimestamp($name)
230    {
231        if (is_string($this->fetch($name))) {
232            return $this->_returns[$this->_o['mode']][$name][1];
233        }
234        return false;
235    }
236
237    public function delete($name)
238    {
239        setcookie($name, '', time() - 3600, $this->_o['path'], $this->_o['domain'], $this->_o['secure'], $this->_o['httponly']);
240    }
241    
242    /**
243     * @var array options
244     */
245    private $_o;
246
247    private $_returns = array();
248}
249