the browser-facing portion of osu!
at master 3.3 kB view raw
1<?php 2 3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 4// See the LICENCE file in the repository root for full licence text. 5 6declare(strict_types=1); 7 8namespace App\Libraries\SessionVerification; 9 10use App\Exceptions\UserVerificationException; 11use App\Interfaces\SessionVerificationInterface; 12use App\Libraries\SignedRandomString; 13use Carbon\CarbonImmutable; 14 15class State 16{ 17 private const KEY_VALID_DURATION = 600; 18 19 public readonly CarbonImmutable $expiresAt; 20 public readonly string $key; 21 public readonly string $linkKey; 22 public int $tries = 0; 23 24 private function __construct( 25 private readonly string $sessionClass, 26 private readonly string $sessionId, 27 ) { 28 // 1 byte = 8 bits = 2^8 = 16^2 = 2 hex characters 29 $this->key = bin2hex(random_bytes($GLOBALS['cfg']['osu']['user']['verification_key_length_hex'] / 2)); 30 $this->linkKey = SignedRandomString::create(32); 31 $this->expiresAt = CarbonImmutable::now()->addSeconds(static::KEY_VALID_DURATION); 32 } 33 34 public static function create(SessionVerificationInterface $session): static 35 { 36 $state = new static($session::class, $session->getKey()); 37 $state->save(true); 38 39 return $state; 40 } 41 42 public static function fromSession(SessionVerificationInterface $session): ?static 43 { 44 return \Cache::get(static::cacheKey($session::class, $session->getKey())); 45 } 46 47 public static function fromVerifyLink(string $linkKey): ?static 48 { 49 if (!SignedRandomString::isValid($linkKey)) { 50 return null; 51 } 52 53 $cacheKey = \Cache::get(static::cacheLinkKey($linkKey)); 54 55 return $cacheKey === null ? null : \Cache::get($cacheKey); 56 } 57 58 private static function cacheKey(string $class, string $id): string 59 { 60 return "session_verification:{$class}:{$id}"; 61 } 62 63 private static function cacheLinkKey(string $linkKey): string 64 { 65 return "session_verification_link:{$linkKey}"; 66 } 67 68 public function delete(): void 69 { 70 \Cache::delete(static::cacheKey($this->sessionClass, $this->sessionId)); 71 \Cache::delete(static::cacheLinkKey($this->linkKey)); 72 } 73 74 public function findSession(): ?SessionVerificationInterface 75 { 76 return $this->sessionClass::findForVerification($this->sessionId); 77 } 78 79 public function verify(string $inputKey): void 80 { 81 $this->tries++; 82 83 if ($this->expiresAt->isPast()) { 84 throw new UserVerificationException('expired', true); 85 } 86 87 if (!hash_equals($this->key, $inputKey)) { 88 if ($this->tries >= $GLOBALS['cfg']['osu']['user']['verification_key_tries_limit']) { 89 throw new UserVerificationException('retries_exceeded', true); 90 } else { 91 $this->save(false); 92 throw new UserVerificationException('incorrect_key', false); 93 } 94 } 95 } 96 97 private function save(bool $saveLinkKey): void 98 { 99 $cacheKey = static::cacheKey($this->sessionClass, $this->sessionId); 100 \Cache::put($cacheKey, $this, $this->expiresAt); 101 if ($saveLinkKey) { 102 \Cache::put(static::cacheLinkKey($this->linkKey), $cacheKey, $this->expiresAt); 103 } 104 } 105}