Last active
September 29, 2015 00:08
-
-
Save jligerofleitas/5d9521ac5dcd86cc4834 to your computer and use it in GitHub Desktop.
Inspired by Enrizo Zimmel' secure sessions, I just added extra code to allow filesystem and DB session storage (via PDO), configured in the constructor. get(), set() and flash() methods added for daily usage.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* ------------------------------------------------ | |
* Encrypt PHP session data using files | |
* ------------------------------------------------ | |
* The encryption is built using mcrypt extension | |
* and the randomness is managed by openssl | |
* The default encryption algorithm is AES (Rijndael-128) | |
* and we use CBC+HMAC (Encrypt-then-mac) with SHA-256 | |
* | |
* @author Enrico Zimuel ([email protected]) | |
* @copyright GNU General Public License | |
* @GitHub https://github.com/ezimuel/PHP-Secure-Session | |
* | |
* Some modifications added to allow filesystem AND database session using PDO. | |
* Convinience set(), get() and flash() methods. | |
* | |
* Filesystem - default behavior | |
* $session = new SecureSession(); | |
* | |
* Database: | |
* $session = new SecureSession('db', array( | |
* 'dsn' => 'mysql:host=localhost;dbname=testdb', | |
* 'user' => 'root', | |
* 'password' => '123', | |
* )); | |
* | |
* Usage: | |
* - Just create a new SessionSecure instance: $session = new SecureSession(); | |
* - $session->get('session_key') to retrieve stored session data | |
* - $session->set('session_key', 'session_value') to set session data; | |
* - ALL DATA IS SERIALIZED BEFORE IT IS SAVED IN $_SESSION, so you can save almost whatever you want. | |
* - Besides that, when stored in DB, it is also base64 encoded to ensure binary correctness. | |
* - flash messages, $session->flash('flash_key', 'flash_value') to set a flash message, and $session->flash('flash_key') to retrieve. | |
* | |
* | |
* Table definition to store sessions (MySQL): | |
CREATE TABLE IF NOT EXISTS `session` ( | |
`id` int(11) NOT NULL, | |
`sessionId` varchar(255) NOT NULL, | |
`data` blob NOT NULL, | |
`updated` datetime NOT NULL | |
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; | |
* | |
* ATENTION: | |
* This is not fully tested, just a work in progress; use on your own risk ;) | |
*/ | |
class SecureSession | |
{ | |
/** | |
* Encryption algorithm | |
* | |
* @var string | |
*/ | |
protected $_algo= MCRYPT_RIJNDAEL_128; | |
/** | |
* Key for encryption/decryption | |
* | |
* @var string | |
*/ | |
protected $_key; | |
/** | |
* Key for HMAC authentication | |
* | |
* @var string | |
*/ | |
protected $_auth; | |
/** | |
* Path of the session file | |
* | |
* @var string | |
*/ | |
protected $_path; | |
/** | |
* Session name (optional) | |
* | |
* @var string | |
*/ | |
protected $_name; | |
/** | |
* Size of the IV vector for encryption | |
* | |
* @var integer | |
*/ | |
protected $_ivSize; | |
/** | |
* Cookie variable name of the encryption + auth key | |
* | |
* @var string | |
*/ | |
protected $_keyName; | |
/** | |
* Database PDO conn | |
*/ | |
private $conn; | |
/** | |
* Generate a random key using openssl | |
* fallback to mcrypt_create_iv | |
* | |
* @param integer $length | |
* @return string | |
*/ | |
protected function _randomKey($length=32) { | |
if(function_exists('openssl_random_pseudo_bytes')) { | |
$rnd = openssl_random_pseudo_bytes($length, $strong); | |
if ($strong === true) { | |
return $rnd; | |
} | |
} | |
return mcrypt_create_iv($length, MCRYPT_DEV_URANDOM); | |
} | |
/** | |
* Constructor | |
*/ | |
public function __construct($type = 'file', $conn = array()) | |
{ | |
if ( ! extension_loaded('mcrypt')) { | |
throw new Exception("The SecureSession class needs the Mcrypt PHP " | |
. "extension, please install it."); | |
} | |
if ($type === 'db'): | |
session_set_save_handler( | |
array($this, "open"), | |
array($this, "close"), | |
array($this, "db_read"), | |
array($this, "db_write"), | |
array($this, "db_destroy"), | |
array($this, "db_gc") | |
); | |
try { | |
$this->conn = | |
new \PDO($conn['dsn'], | |
$conn['user'], | |
$conn['password']); | |
$this->conn->setAttribute(\PDO::ATTR_ERRMODE, | |
\PDO::ERRMODE_EXCEPTION); | |
$this->conn->setAttribute(\PDO::ATTR_EMULATE_PREPARES, | |
false); | |
} catch(\PDOException $e) { | |
throw new \PDOException("SecureSession could not connect " | |
. "to database with provided parameters " | |
. "({$e->getMessage()})"); | |
} | |
else: | |
session_set_save_handler( | |
array($this, "open"), | |
array($this, "close"), | |
array($this, "read"), | |
array($this, "write"), | |
array($this, "destroy"), | |
array($this, "gc") | |
); | |
endif; | |
// TODO: kind of "autostart" | |
session_start(); | |
} | |
/** | |
* FILE HANDLER | |
*/ | |
/** | |
* Open the session | |
* | |
* @param string $save_path | |
* @param string $session_name | |
* @return bool | |
*/ | |
public function open($save_path, $session_name) | |
{ | |
$this->_path = $save_path.'/'; | |
$this->_name = $session_name; | |
$this->_keyName = "KEY_$session_name"; | |
$this->_ivSize = mcrypt_get_iv_size($this->_algo, MCRYPT_MODE_CBC); | |
$_keyName = (isset($_COOKIE[$this->_keyName]))? | |
$_COOKIE[$this->_keyName]:''; | |
if (empty($_keyName) || strpos($_keyName,':')===false) { | |
$keyLength = mcrypt_get_key_size($this->_algo, MCRYPT_MODE_CBC); | |
$this->_key = self::_randomKey($keyLength); | |
$this->_auth = self::_randomKey(32); | |
$cookie_param = session_get_cookie_params(); | |
setcookie( | |
$this->_keyName, | |
base64_encode($this->_key) . ':' . base64_encode($this->_auth), | |
// if session cookie lifetime > 0 then add to current time; otherwise leave it as zero, honoring zero's special meaning: expire at browser close. | |
($cookie_param['lifetime'] > 0) ? time() + $cookie_param['lifetime'] : 0, | |
$cookie_param['path'], | |
$cookie_param['domain'], | |
$cookie_param['secure'], | |
$cookie_param['httponly'] | |
); | |
} else { | |
list ($this->_key, $this->_auth) = explode (':',$_keyName); | |
$this->_key = base64_decode($this->_key); | |
$this->_auth = base64_decode($this->_auth); | |
} | |
return true; | |
} | |
/** | |
* Close the session | |
* | |
* @return bool | |
*/ | |
public function close() | |
{ | |
\session_write_close(); | |
return true; | |
} | |
/** | |
* Read and decrypt the session | |
* | |
* @param integer $id | |
* @return string | |
*/ | |
public function read($id) | |
{ | |
$sess_file = $this->_path.$this->_name."_$id"; | |
if ( ! file_exists($sess_file)) { | |
return false; | |
} | |
$data = file_get_contents($sess_file); | |
list($hmac, $iv, $encrypted)= explode(':',$data); | |
$iv = base64_decode($iv); | |
$encrypted = base64_decode($encrypted); | |
$newHmac = hash_hmac('sha256', $iv . $this->_algo . $encrypted, $this->_auth); | |
if ($hmac !== $newHmac) { | |
return false; | |
} | |
$decrypt = mcrypt_decrypt( | |
$this->_algo, | |
$this->_key, | |
$encrypted, | |
MCRYPT_MODE_CBC, | |
$iv | |
); | |
return rtrim($decrypt, "\0"); | |
} | |
/** | |
* Encrypt and write the session | |
* | |
* @param integer $id | |
* @param string $data | |
* @return bool | |
*/ | |
public function write($id, $data) | |
{ | |
$sess_file = $this->_path . $this->_name . "_$id"; | |
$iv = mcrypt_create_iv($this->_ivSize, MCRYPT_DEV_URANDOM); | |
$encrypted = mcrypt_encrypt( | |
$this->_algo, | |
$this->_key, | |
$data, | |
MCRYPT_MODE_CBC, | |
$iv | |
); | |
$hmac = hash_hmac('sha256', $iv . $this->_algo . $encrypted, $this->_auth); | |
$bytes = file_put_contents($sess_file, $hmac . ':' . base64_encode($iv) . ':' . base64_encode($encrypted)); | |
return ($bytes !== false); | |
} | |
/** | |
* Destoroy the session | |
* | |
* @param int $id | |
* @return bool | |
*/ | |
public function destroy($id) | |
{ | |
\session_write_close(); | |
$sess_file = $this->_path . $this->_name . "_$id"; | |
setcookie ($this->_keyName, '', time() - 3600); | |
return(@unlink($sess_file)); | |
} | |
/** | |
* Garbage Collector | |
* | |
* @param int $max | |
* @return bool | |
*/ | |
public function gc($max) | |
{ | |
foreach (glob($this->_path . $this->_name . '_*') as $filename) { | |
if (filemtime($filename) + $max < time()) { | |
@unlink($filename); | |
} | |
} | |
return true; | |
} | |
/** | |
* Just checks if session has been started. | |
*/ | |
public function session_start() | |
{ | |
if (\function_exists('session_status')): | |
if (\session_status() === PHP_SESSION_NONE): | |
\session_start(); | |
endif; | |
elseif (\session_id() == ''): | |
\session_start(); | |
endif; | |
} | |
public function get($key = '') | |
{ | |
$this->session_start(); | |
if (isset($_SESSION[$key])) { | |
return \unserialize($_SESSION[$key]); | |
} else { | |
return false; | |
} | |
} | |
public function set($key, $value = '') | |
{ | |
$this->session_start(); | |
$_SESSION[$key] = \serialize($value); | |
\session_write_close(); | |
} | |
public function delete($key) | |
{ | |
$this->session_start(); | |
unset($_SESSION[$key]); | |
\session_write_close(); | |
} | |
/** | |
* Flash messages | |
* | |
* http://www.phpdevtips.com/2013/05/simple-session-based-flash-messages/ | |
* | |
* @param type $key | |
* @param type $value | |
*/ | |
public function flash($key = '', $value = '') | |
{ | |
$this->session_start(); | |
if (empty($key)) { | |
return array(); | |
} | |
// Flash message does not exists: create it | |
if ( ! empty($value) && empty($_SESSION[$key])) { | |
if ( ! empty($_SESSION[$key])) { | |
unset($_SESSION[$key]); | |
} | |
$_SESSION[$key] = serialize($value); | |
\session_write_close(); | |
return true; | |
} | |
// Flash message exists: return it | |
elseif ( ! empty($_SESSION[$key]) && empty($value)) { | |
$value = $_SESSION[$key]; | |
$flash = unserialize($value); | |
unset($_SESSION[$key]); | |
\session_write_close(); | |
return $flash; | |
} | |
// Just in case... | |
else { | |
return array(); | |
} | |
} | |
/*************************************************************************** | |
* DATABASE HANDLER | |
*/ | |
/** | |
* Read and decrypt the session | |
* | |
* @param integer $id | |
* @return string | |
*/ | |
public function db_read($id) | |
{ | |
// Does session exist? | |
$session = $this->conn->prepare("SELECT data | |
FROM session | |
WHERE sessionId=:session_id"); | |
$session->execute(array(':session_id' => $id)); | |
$row = $session->fetch(); | |
// Row with session ID already exists | |
if ((bool)$row): | |
$data = (\base64_decode($row['data'])); | |
list($hmac, $iv, $encrypted)= explode(':',$data); | |
$iv = base64_decode($iv); | |
$encrypted = base64_decode($encrypted); | |
$newHmac = hash_hmac('sha256', $iv . $this->_algo . $encrypted, $this->_auth); | |
if ($hmac !== $newHmac) { | |
return false; | |
} | |
$decrypt = mcrypt_decrypt( | |
$this->_algo, | |
$this->_key, | |
$encrypted, | |
MCRYPT_MODE_CBC, | |
$iv | |
); | |
return rtrim($decrypt, "\0"); | |
// No session in DB: create row with session ID | |
else: | |
$now = \date("Y-m-d H:i:s"); | |
$query = "INSERT INTO session (sessionId, updated) VALUES ('$id', '$now');"; | |
$session = $this->conn->query($query); | |
return (bool)$session->rowCount(); | |
endif; | |
} | |
/** | |
* Encrypt and write the session | |
* | |
* @param integer $id | |
* @param string $data | |
* @return bool | |
*/ | |
public function db_write($id, $data) | |
{ | |
$iv = mcrypt_create_iv($this->_ivSize, MCRYPT_DEV_URANDOM); | |
$encrypted = mcrypt_encrypt( | |
$this->_algo, | |
$this->_key, | |
$data, | |
MCRYPT_MODE_CBC, | |
$iv | |
); | |
$hmac = hash_hmac('sha256', $iv . $this->_algo . $encrypted, $this->_auth); | |
$session = $this->conn->prepare("UPDATE session SET data=:data, updated=:updated | |
WHERE sessionId=:session_id"); | |
$row_data = | |
\base64_encode(($hmac.':'.\base64_encode($iv).':'.\base64_encode($encrypted))); | |
$session->execute(array(':data' => $row_data, | |
':updated' => date("Y-m-d H:i:s"), | |
':session_id' => $id, | |
)); | |
return (bool)$session->rowCount(); | |
} | |
/** | |
* Destroy the session | |
* | |
* @param int $id | |
* @return bool | |
*/ | |
public function db_destroy($id) | |
{ | |
\session_write_close(); | |
$session = $this->conn->prepare("DELETE FROM session WHERE sessionId=:session_id"); | |
$session->execute(array(':session_id' => $id)); | |
return (bool)$session->rowCount(); | |
} | |
/** | |
* Garbage Collector. | |
* Delete rows from database. | |
* | |
* @param int $max Seconds of inactive session | |
* @return bool | |
*/ | |
public function db_gc($max) | |
{ | |
$session = $this->conn->prepare("DELETE FROM session WHERE updated < :date"); | |
$date = date("Y-m-d H:i:s", time() - $max); | |
$session->execute(array(':date' => $date)); | |
return true; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Just some code to enable database stored encrypted sessions as described by Enrico Zimmel, besides default filesystem storage. Not sure if this is correct, but...Just some test done but not fully tested.