loadCookie(); } protected string $selector = ''; protected string $token = ''; public function isIn(?string $selector = null, ?string $sentToken = null): bool { try { $login = $this->repository->fetchActiveBySelector($selector ?? $this->selector); if (!$this->validToken($login, $sentToken)) { return false; } $now = new DateTimeImmutable(); if ($login->dateTime->add(new DateInterval("PT{$this->max_login_time}H")) > $now) { return true; } } catch (PDOException|EmptyResult) {} return false; } public function getUser(): Model\User { $login = $this->repository->fetchActiveBySelector($this->selector); if (!$this->validToken($login)) { throw new Exception('User not found'); } return $login->user; } public function getToken(): string { return implode($this->cookie_separator, [$this->selector, $this->token]); } public function getSeparator(): string { return $this->cookie_separator; } public function addUser(array $data): Model\User { try { return $this->userRepository->fetchByName($data['name']); } catch (EmptyResult) { list($passphrase, $encrypted) = $this->splitPassword($data['password']); $password = $this->cryptoJs_aes_decrypt($encrypted, $passphrase); $password = password_hash($password, PASSWORD_DEFAULT); $user = $this->userRepository->create([ 'name' => $data['name'], 'password' => $password, 'enabled' => $data['enabled'] ?? 1 ]); return $this->userRepository->save($user); } } public function validateUser(Model\User $user, string $encryptedPassword): bool { list($passphrase, $encrypted) = $this->splitPassword($encryptedPassword); try { $password = $this->cryptoJs_aes_decrypt($encrypted, $passphrase); } catch (Exception) { return false; } return $user->validate($password); } public function login(Model\User $user): bool { try { $login = $this->repository->fetchActiveByUser($user->id); $this->logout($login->user); } catch (PDOException|EmptyResult) { } try { $now = new DateTimeImmutable(); $login = $this->repository->create([ 'user_id' => $user->id, 'time' => $now->format('Y-m-d H:i:s'), 'status' => 1 ]); list('selector' => $selector, 'token' => $token) = $this->generateToken($login); $login->selector = $selector; $login->token = password_hash($token, PASSWORD_DEFAULT); $this->repository->save($login); $this->saveCookie($selector, $token, $login->dateTime->add(new DateInterval("PT{$this->max_login_time}H"))); return true; } catch (PDOException|Exception) { return false; } } public function logout(Model\User $user): bool { $this->removeCookie(); try { $logins = $this->repository->fetchByUser($user->id); } catch (PDOException) { return true; } try { foreach ($logins as $login) { $this->repository->edit($login, ['status' => 0]); } return true; } catch (PDOException) { return false; } } public function parseToken(Model\Login $login): string { return implode($this->cookie_separator, [$login->selector, $login->token]); } protected function loadCookie(): void { if (!isset($_COOKIE[$this->cookie_name])) { return; } $cookie = $_COOKIE[$this->cookie_name]; if (!str_contains($cookie, $this->cookie_separator)) { $this->removeCookie(); return; } list($this->selector, $this->token) = explode($this->cookie_separator, $cookie); } protected function saveCookie(string $selector, string $token, DateTimeInterface $expires): void { setcookie( $this->cookie_name, implode($this->cookie_separator, [$selector, $token]), [ 'expires' => $expires->getTimestamp(), 'path' => $this->path, 'domain' => $this->domain, 'samesite' => 'Strict' ] ); $this->selector = $selector; $this->token = $token; } protected function removeCookie(): void { setcookie( $this->cookie_name, '', (new DateTimeImmutable())->getTimestamp(), $this->path, $this->domain ); } protected function validToken(Model\Login $login, ?string $sentToken = null): bool { if ($sentToken !== null) { return password_verify($sentToken, $login->token); } return password_verify($this->token, $login->token); } protected function generateToken(Model\Login $login): array { $selector = bin2hex(random_bytes(12)); $token = bin2hex(random_bytes(20)); return ['selector' => $selector, 'token' => $token]; } protected function splitPassword(string $input): array { $ini = strpos($input, 'U'); $passphrase = substr($input, 0, $ini); $message = substr($input, $ini); return [$passphrase, $message]; } protected function cryptoJs_aes_decrypt($data, $key): string { $data = base64_decode($data); if (!str_starts_with($data, "Salted__")) { return false; } $salt = substr($data, 8, 8); $keyAndIV = $this->aes_evpKDF($key, $salt); $decrypted = openssl_decrypt( substr($data, 16), "aes-256-cbc", $keyAndIV["key"], OPENSSL_RAW_DATA, // base64 was already decoded $keyAndIV["iv"] ); if ($decrypted === false) { throw new Exception(); } return $decrypted; } protected function aes_evpKDF($password, $salt, $keySize = 8, $ivSize = 4, $iterations = 1, $hashAlgorithm = "md5"): array { $targetKeySize = $keySize + $ivSize; $derivedBytes = ""; $numberOfDerivedWords = 0; $block = NULL; $hasher = hash_init($hashAlgorithm); while ($numberOfDerivedWords < $targetKeySize) { if ($block != NULL) { hash_update($hasher, $block); } hash_update($hasher, $password); hash_update($hasher, $salt); $block = hash_final($hasher, TRUE); $hasher = hash_init($hashAlgorithm); // Iterations for ($i = 1; $i < $iterations; $i++) { hash_update($hasher, $block); $block = hash_final($hasher, TRUE); $hasher = hash_init($hashAlgorithm); } $derivedBytes .= substr($block, 0, min(strlen($block), ($targetKeySize - $numberOfDerivedWords) * 4)); $numberOfDerivedWords += strlen($block) / 4; } return array( "key" => substr($derivedBytes, 0, $keySize * 4), "iv" => substr($derivedBytes, $keySize * 4, $ivSize * 4) ); } }