misuzu/src/AuthToken.php
flash 383e2ed0e0 Rewrote the user information class.
This one took multiple days and it pretty invasive into the core of Misuzu so issue might (will) arise, there's also some features that have gone temporarily missing in the mean time and some inefficiencies introduced that will be fixed again at a later time.
The old class isn't gone entirely because I still have to figure out what I'm gonna do about validation, but for the most part this knocks out one of the "layers of backwards compatibility", as I've been referring to it, and is moving us closer to a future where Flashii actually gets real updates.
If you run into anything that's broken and you're inhibited from reporting it through the forum, do it through chat or mail me at flashii-issues@flash.moe.
2023-08-02 22:12:47 +00:00

216 lines
6.4 KiB
PHP

<?php
namespace Misuzu;
use Index\IO\MemoryStream;
use Index\Serialisation\UriBase64;
use Misuzu\Auth\SessionInfo;
use Misuzu\Users\UserInfo;
/* 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(): string {
return $this->getProperty('u');
}
public function setUserId(string $userId): self {
$this->setProperty('u', $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(UserInfo $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);
}
}