リピストX PHPer チャレンジ 2023 問題トークン
PHPer チャレンジ 2023 のリピストからの問題トークンです。 この問題には 2〜5 番目のトークンが隠されています。問題に正解すると合計 4 つのトークンが得られます。
尚、1 番目のトークンはこのページには存在しません。 1 番目のトークンをお探しの方は https://rpst.jp/phperkaigi2023/ をご覧ください。
問題
後述のソースコードおよびヒントをよく読んで PHPer トークンを探し出してください。
ルール
- ソースコードを保存の上、PHP 8.0 以降で実行ください。
- プログラムを実行すると、回答の入力を求められますので入力してください。正答の場合はトークンが表示されます。
- 2〜4 番目までのトークンは、答えの先頭に '#' をつけたものがトークンです。
- 3、4 番目のトークンは関数名です。ソースコードを読んだ上で関数名を当ててください。
- 5 番目のトークンは暗号化されていますが、2〜4 に正解すると答えを知ることができます。
ヒント
-
2 番目のトークン
- 弊社サービスの短縮名。アルファベット小文字 4 文字です。サービスドメイン名にも使われています。
-
3 番目のトークン
-
この関数名を見るたびに、いつも弊社サービスを思い出してしまいます。
(ソースコードを読んで判断お願いします。)
-
この関数名を見るたびに、いつも弊社サービスを思い出してしまいます。
-
4 番目のトークン
-
3番目のトークンが正解すると鍵のありかの書いた宝の地図(FQDN)が得られます。鍵を得るにはこの関数が必要です。
(こちらもソースコードを読んで判断お願いします。)
-
3番目のトークンが正解すると鍵のありかの書いた宝の地図(FQDN)が得られます。鍵を得るにはこの関数が必要です。
-
5 番目のトークン
- とある本の名前です。
<?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);
}