misuzu/src/AuthToken.php

235 lines
7.1 KiB
PHP

<?php
namespace Misuzu;
use Index\IO\MemoryStream;
use Index\Serialisation\UriBase64;
use Misuzu\Auth\SessionInfo;
use Misuzu\Users\User;
/* Map of props
* u - User ID
* s - Plaintext token string
* t - Old hex token string, fallback for s
* i - Impersonation User ID
*/
class AuthToken {
private const EPOCH = 1682985600;
private int $timestamp = 0;
private int $cookieExpires = 0;
private array $props = [];
private static string $secretKey = '';
public static function setSecretKey(string $secretKey): void {
self::$secretKey = $secretKey;
}
public function getTimestamp(): int {
return $this->timestamp;
}
public function updateTimestamp(): void {
$this->timestamp = self::timestamp();
}
public function hasProperty(string $name): bool {
return isset($this->props[$name]);
}
public function getProperty(string $name): string {
return $this->props[$name] ?? '';
}
public function setProperty(string $name, string $value): void {
$this->props[$name] = $value;
$this->updateTimestamp();
}
public function removeProperty(string $name): void {
unset($this->props[$name]);
$this->updateTimestamp();
}
public function isValid(): bool {
if($this->getUserId() < 1 || empty($this->getSessionToken()))
return false;
return true;
}
public function getUserId(): int {
$value = (int)$this->getProperty('u');
return $value < 1 ? -1 : $value;
}
public function setUserId(int $userId): self {
$this->setProperty('u', (string)$userId);
return $this;
}
public function getSessionToken(): string {
if($this->hasProperty('s'))
return $this->getProperty('s');
if($this->hasProperty('t'))
return bin2hex($this->getProperty('t'));
return '';
}
public function setSessionToken(string $token): self {
$this->setProperty('s', $token);
return $this;
}
public function hasImpersonatedUserId(): bool {
return $this->hasProperty('i');
}
public function getImpersonatedUserId(): int {
$value = (int)$this->getProperty('i');
return $value < 1 ? -1 : $value;
}
public function setImpersonatedUserId(int $userId): void {
$this->setProperty('i', (string)$userId);
}
public function removeImpersonatedUserId(): void {
$this->removeProperty('i');
}
public function pack(bool $base64 = true): string {
$data = '';
foreach($this->props as $name => $value) {
// very smart solution for this issue, you definitely won't be confused by this later
// down the line when a variable suddenly despawns from the token
$nameLength = strlen($name);
$valueLength = strlen($value);
if($nameLength > 255 || $valueLength > 255)
continue;
$data .= chr($nameLength) . $name . chr($valueLength) . $value;
}
$prefix = pack('CN', 2, $this->getTimestamp());
$data = $prefix . hash_hmac('sha3-256', $prefix . $data, self::$secretKey, true) . $data;
if($base64)
$data = UriBase64::encode($data);
return $data;
}
public static function unpack(string $data, bool $base64 = true): self {
$obj = new AuthToken;
if(empty($data))
return $obj;
if($base64)
$data = UriBase64::decode($data);
if(empty($data))
return $obj;
$version = ord($data[0]);
$data = substr($data, 1);
if($version === 1) {
$data = str_pad($data, 36, "\x00");
$data = unpack('Nuser/H*token', $data);
$obj->props['u'] = (string)$data['user'];
$obj->props['s'] = $data['token'];
$obj->updateTimestamp();
} elseif($version === 2) {
$timestamp = substr($data, 0, 4);
$userHash = substr($data, 4, 32);
$data = substr($data, 36);
$realHash = hash_hmac('sha3-256', chr($version) . $timestamp . $data, self::$secretKey, true);
if(!hash_equals($realHash, $userHash))
return $obj;
$unpacked = unpack('Nts', $timestamp);
$obj->timestamp = (int)$unpacked['ts'];
$stream = MemoryStream::fromString($data);
$stream->seek(0);
for(;;) {
$length = $stream->readChar();
if($length === null)
break;
$length = ord($length);
if($length < 1)
break;
$name = $stream->read($length);
$value = null;
$length = $stream->readChar();
if($length !== null) {
$length = ord($length);
if($length > 0)
$value = $stream->read($length);
}
$obj->props[$name] = $value;
}
}
return $obj;
}
public static function timestamp(): int {
return time() - self::EPOCH;
}
public static function create(User $userInfo, SessionInfo $sessionInfo): self {
$token = new AuthToken;
$token->setUserId($userInfo->getId());
$token->setSessionToken($sessionInfo->getToken());
return $token;
}
public static function cookieDomain(bool $compatible = true): string {
$url = parse_url($_SERVER['HTTP_HOST'], PHP_URL_HOST);
if(empty($url))
$url = $_SERVER['HTTP_HOST'];
if(!filter_var($url, FILTER_VALIDATE_IP) && $compatible)
$url = '.' . $url;
return $url;
}
public function applyCookie(int $expires = 0): void {
if($expires > 0)
$this->cookieExpires = $expires;
else
$expires = $this->cookieExpires;
setcookie('msz_auth', $this->pack(), $expires, '/', self::cookieDomain(), !empty($_SERVER['HTTPS']), true);
}
public static function nukeCookie(): void {
setcookie('msz_auth', '', -9001, '/', self::cookieDomain(), !empty($_SERVER['HTTPS']), true);
setcookie('msz_auth', '', -9001, '/', '', !empty($_SERVER['HTTPS']), true);
}
public static function nukeCookieLegacy(): void {
setcookie('msz_uid', '', -3600, '/', '', !empty($_SERVER['HTTPS']), true);
setcookie('msz_sid', '', -3600, '/', '', !empty($_SERVER['HTTPS']), true);
}
// please never use the below functions beyond the scope of the sharpchat auth stuff
// a better mechanism for keeping a global instance of this available
// that isn't a $GLOBAL variable or static instance needs to be established, for User as well
private static $localToken = null;
public function setCurrent(): void {
self::$localToken = $this;
}
public static function unsetCurrent(): void {
self::$localToken = null;
}
public static function getCurrent(): ?self {
return self::$localToken;
}
public static function hasCurrent(): bool {
return self::$localToken !== null;
}
}