235 lines
7.1 KiB
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;
|
|
}
|
|
}
|