リピストX PHPer チャレンジ 2023 問題トークン

PHPer チャレンジ 2023 のリピストからの問題トークンです。 この問題には 2〜5 番目のトークンが隠されています。問題に正解すると合計 4 つのトークンが得られます。

尚、1 番目のトークンはこのページには存在しません。 1 番目のトークンをお探しの方は https://rpst.jp/phperkaigi2023/ をご覧ください。

問題

後述のソースコードおよびヒントをよく読んで PHPer トークンを探し出してください。

ルール

ヒント

<?php /** * PHPer チャレンジ 2023 リピスト 問題トークン */ declare(strict_types=1); namespace Rpst\PHPerKaigi2023\PHPerChallenge; /** * トークン抽象クラス */ abstract class TokenBase { protected ?string $name; protected ?string $check_hash; protected ?string $hint; protected ?string $answer; public function getName() { return $this->name; } public function getHint(): string { return $this->hint; } public function getNextKey(): string { return ''; } public function getToken(): string { return isset($this->answer) ? \sprintf('#%s', $this->answer) : ''; } public function setAnswer(string $value): void { try { $this->check($value); $this->answer = $value; } catch (\Throwable $e) { throw $e; } } public function check(string $value) { if (\hash('sha256', $value) !== $this->check_hash) { throw new \ValueError('入力値と正答のHASHが一致しません。入力値が異なるようです。'); } return true; } } /** * 2番目のトークンクラス */ final class SecondToken extends TokenBase { protected ?string $name = '2番目のトークン'; protected ?string $check_hash = '31ca9aff6991c76ccf4751aaa16de4930e9708a77924c2a34e1829df9635527b'; protected ?string $hint = '弊社サービスの短縮名。アルファベット小文字4文字です。サービスドメイン名にも使われています。'; public function check(string $value) { if (\preg_match('/^[a-z]{4}$/', $value) !== 1) { throw new \ValueError('正答はアルファベット小文字4文字です。'); } return parent::check($value); } public function getNextKey(): string { return $this->answer; } } /** * 3番目のトークンクラス */ final class ThirdToken extends TokenBase { protected ?string $name = '3番目のトークン'; protected ?string $check_hash = '51580fd10f5d91fa612c4cef41eefae0d5a56139489e242a5a137b6d2c716d56'; protected ?string $hint = '関数名。この関数名を見るたびに、いつも弊社サービスを思い出してしまいます。(ソースコードを読んで判断お願いします。)'; public function __construct( private TokenBase $prev_token, ) { } public function check($value) { $key = $this->prev_token->getNextKey(); if ($value !== \preg_replace('/([a-z])([a-z])([a-z]{2})/', '${3}${1}_${1}e${2}eat', $key)) { throw new \ValueError('パターンが一致しません。'); } if (!\function_exists($value)) { throw new \ValueError('関数名ではありません。'); } return parent::check($value); } public function getNextKey(): string { if (!isset($this->answer)) { throw new \ErrorException('未回答です。'); } $func = $this->answer; return \sprintf('%s.%s.jp', \hash('md5', $func('PHPerKaigi', 2023)), $this->prev_token->getNextKey()); } } /** * 4番目のトークンクラス */ final class FourthToken extends TokenBase { protected ?string $name = '4番目のトークン'; protected ?string $check_hash = 'cc58479a82f8a28424199ba44ede3e60af82c3817bf6bc1fd32aed8eac0a18f9'; protected ?string $hint = '関数名。3番目のトークンが正解すると鍵のありかの書いた宝の地図(FQDN)が得られます。鍵を得るにはこの関数が必要です。(こちらもソースコードを読んで判断お願いします。)'; public function __construct( private TokenBase $prev_token, ) { } public function check(string $value) { if (!\function_exists($value)) { throw new \ValueError('関数名ではありません。'); } return parent::check($value); } public function getNextKey(): string { if (!isset($this->answer)) { throw new \ErrorException('未回答です。'); } $func = $this->answer; $record = $func($this->prev_token->getNextKey(), \DNS_TXT); $key = $record[0]['txt'] ?? false; if (!$key) { throw new \ErrorException('鍵の取得に失敗しました。DNSによる名前解決できる環境でお試しください。'); } return $key; } } /** * 5番目のトークンクラス */ final class FifthToken extends TokenBase { public const ENCRYPTED_BOOKNAME = 'LfdFSlwY2CRPaxq+Zd0Gkg2HzpIsixn67MUAT5lc2S7peplTwWwVeoroKvaJNKu4W13qWAH7O+471BXDb3Bs4w=='; protected ?string $name = '5番目のトークン'; protected ?string $check_hash = '7d17de8f7c8be29a03985d384c91998e98d80efc823926764e809b2b0bb12f0b'; protected ?string $hint = 'とある本の名前です。'; public function __construct( private TokenBase $prev_token, ) { } public function getToken(): string { if (!isset($this->answer)) { $this->answer = \openssl_decrypt( self::ENCRYPTED_BOOKNAME, 'AES-256-CBC', $this->prev_token->getNextKey(), ); } return \sprintf('#%s', $this->answer); } public function check(string $value) { return true; } } // 初期化 $second_token = new SecondToken(); $third_token = new ThirdToken($second_token); $fourth_token = new FourthToken($third_token); $fifth_token = new FifthToken($fourth_token); $tokens = [ $second_token, $third_token, $fourth_token, $fifth_token, ]; // メインループ echo "2番目のトークンから始まります。1番目のトークンをお探しの方は https://rpst.jp/phperkaigi2023/ をご覧ください。\n\n"; foreach ($tokens as $key => $token) { echo \sprintf("%s\n", $token->getName()); echo \sprintf("トークンのヒント: %s\n", $token->getHint()); $token_value = $token->getToken(); if ('' === $token_value) { echo '入力してください: '; for (;;) { $input_value = \trim(\fgets(\STDIN)); try { $token->check($input_value); echo '正解!'; $token->setAnswer($input_value); $token_value = $token->getToken(); break; } catch (\Throwable $e) { echo $e->getMessage(); } echo '再度入力してください:'; } } echo \sprintf("%s は %s です。\n\n", $token->getName(), $token_value); }