Skip to content

Instantly share code, notes, and snippets.

@Metevier
Created May 16, 2016 21:30
Show Gist options
  • Save Metevier/dee26ce4c2eb9ba0231f690bf9f5b344 to your computer and use it in GitHub Desktop.
Save Metevier/dee26ce4c2eb9ba0231f690bf9f5b344 to your computer and use it in GitHub Desktop.
Implementation of Mozilla's node-client-sessions in PHP
<?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