Created
May 16, 2016 21:30
-
-
Save Metevier/dee26ce4c2eb9ba0231f690bf9f5b344 to your computer and use it in GitHub Desktop.
Implementation of Mozilla's node-client-sessions in PHP
This file contains 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 | |
if(!function_exists('hash_equals')) { | |
function hash_equals($str1, $str2) { | |
if(strlen($str1) != strlen($str2)) { | |
return false; | |
} else { | |
$res = $str1 ^ $str2; | |
$ret = 0; | |
for($i = strlen($res) - 1; $i >= 0; $i--) $ret |= ord($res[$i]); | |
return !$ret; | |
} | |
} | |
} | |
class ClientSessions | |
{ | |
private $opts; | |
private $COOKIE_NAME_SEP = '='; | |
private $DEFAULT_SIGNATURE_ALGORITHM = 'sha256'; | |
private $DEFAULT_ENCRYPTION_ALGORITHM = 'RIJNDAEL-128'; | |
private $KDF_ENC = 'cookiesession-encryption'; | |
private $KDF_MAC = 'cookiesession-signature'; | |
function __construct ($opts) { | |
$this->opts = $opts; | |
} | |
private function aes256_cbc_encrypt($key, $data, $iv) { | |
if(32 !== strlen($key)) $key = hash('SHA256', $key, true); | |
if(16 !== strlen($iv)) $iv = hash('MD5', $iv, true); | |
$padding = 16 - (strlen($data) % 16); | |
$data .= str_repeat(chr($padding), $padding); | |
return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, MCRYPT_MODE_CBC, $iv); | |
} | |
function aes256_cbc_decrypt($key, $data, $iv) { | |
if(32 !== strlen($key)) $key = hash('SHA256', $key, true); | |
if(16 !== strlen($iv)) $iv = hash('MD5', $iv, true); | |
$data = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $data, MCRYPT_MODE_CBC, $iv); | |
$padding = ord($data[strlen($data) - 1]); | |
return substr($data, 0, -$padding); | |
} | |
function base64urlencode($arg) { | |
$s = base64_encode($arg); | |
$exploded = explode('=', $s); | |
$s = $exploded[0]; | |
$s = preg_replace('/\+/', '-', $s); | |
$s = preg_replace('/\//', '_', $s); | |
return $s; | |
} | |
function base64urldecode($arg) { | |
$s = preg_replace('/-/', '+', $arg); | |
$s = preg_replace('/_/', '/', $s); | |
switch (strlen($s) % 4) { // Pad with trailing '='s | |
case 0: | |
break; // No pad chars in this case | |
case 2: | |
$s = $s . "=="; | |
break; // Two pad chars | |
case 3: | |
$s = $s . "="; | |
break; // One pad char | |
default: | |
throw new Exception("Illegal base64url string!"); | |
} | |
return base64_decode($s); | |
} | |
private function computeHmac($opts, $iv, $ciphertext, $duration, $createdAt) { | |
$ctx = hash_init($opts->signatureAlgorithm, 1, $opts->signatureKey); | |
hash_update($ctx, $iv); | |
hash_update($ctx, "."); | |
hash_update($ctx, $ciphertext); | |
hash_update($ctx, "."); | |
hash_update($ctx, (string) $createdAt); | |
hash_update($ctx, "."); | |
hash_update($ctx, (string) $duration); | |
return hash_final($ctx, true); | |
} | |
private function constantTimeEquals($a, $b) { | |
return hash_equals($a, $b); | |
} | |
private function deriveKey($master, $type) { | |
return hash_hmac('sha256', $type, $master, true); | |
} | |
private function setupKeys($opts) { | |
$opts->encryptionKey = $this->deriveKey($opts->secret, $this->KDF_ENC); | |
$opts->signatureKey = $this->deriveKey($opts->secret, $this->KDF_MAC); | |
$opts->signatureAlgorithm = $this->DEFAULT_SIGNATURE_ALGORITHM; | |
$opts->encryptionAlgorithm = $this->DEFAULT_ENCRYPTION_ALGORITHM; | |
} | |
public function decode($session) | |
{ | |
// Grabbing all the parts | |
$components = explode('.', $session); | |
// Setting up the authentication/signature encryption-methods/keys | |
$this->setupKeys($this->opts); | |
// Naming the stuff we need right and decoding some things | |
$iv = $this->base64urldecode($components[0]); | |
$ciphertext = $this->base64urldecode($components[1]); | |
$hmac = $components[4]; | |
$createdAt = floatval($components[2]); | |
$duration = floatval($components[3]); | |
// If the IV isn't 16 characters, nothing else will work | |
if (strlen($iv) != 16) { | |
return null; | |
} | |
// Verify the HMAC | |
$expectedHmac = $this->computeHmac($this->opts, $iv, $ciphertext, $duration, $createdAt); | |
$encodedExpectedHmac = $this->base64urlencode($expectedHmac); | |
if (!$this->constantTimeEquals($hmac, $encodedExpectedHmac)) { | |
return null; | |
} | |
$plainBinary = $this->aes256_cbc_decrypt($this->opts->encryptionKey, $ciphertext, $iv); | |
$plainText = utf8_encode($plainBinary); | |
// Returning the results | |
$result = new stdClass(); | |
$sessionJson = substr($plainText, strrpos($plainText, $this->COOKIE_NAME_SEP) + 1); | |
$result->session = json_decode($sessionJson); | |
$result->createdAt = $createdAt; | |
$result->duration = $duration; | |
return $result; | |
} | |
public function encode($session) { | |
// Setting up the authentication/signature encryption-methods/keys | |
$this->setupKeys($this->opts); | |
$duration = round(24*60*60*1000); | |
$createdAt = round(microtime(true)*1000); | |
$iv = mcrypt_create_iv(16, MCRYPT_RAND); | |
$json = json_encode($session); | |
$plainText = $this->opts->cookieName . $this->COOKIE_NAME_SEP . $json; | |
$ciphertext = $this->aes256_cbc_encrypt($this->opts->encryptionKey, $plainText, $iv); | |
$hmac = $this->computeHmac($this->opts, $iv, $ciphertext, $duration, $createdAt); | |
$components = array( | |
$this->base64urlencode($iv), | |
$this->base64urlencode($ciphertext), | |
(string)$createdAt, | |
(string)$duration, | |
$this->base64urlencode($hmac) | |
); | |
$result = implode('.', $components); | |
return $result; | |
} | |
public function destroy() { | |
setcookie($this->opts->cookieName, '', 0, '/', $this->opts->cookieDomain); | |
} | |
public function get() { | |
if (!isset($_COOKIE[$this->opts->cookieName]) || !array_key_exists($this->opts->cookieName, $_COOKIE)) { | |
return null; | |
} | |
$result = $this->decode($_COOKIE[$this->opts->cookieName]); | |
if ($result == null) { | |
return null; | |
} | |
// Checking whether the cookie is expired | |
$expires = $result->createdAt + $result->duration; | |
$now = microtime(true) * 1000; | |
if ($expires <= $now) { | |
return null; | |
} | |
// Returning the results | |
return $result->session; | |
} | |
public function set ($session) { | |
$cookieValue = $this->encode($session); | |
setcookie($this->opts->cookieName, $cookieValue, 0, '/', $this->opts->cookieDomain); | |
} | |
} | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment