はてなインターン応募クイズ

こんにちは!2012年にはてなインターンに参加し、その後社員として入社、現在はチーフエンジニアの id:cockscomb です。

はてなのインターンシップでは、この数年、応募時に課題を課しています。この課題を「応募クイズ」と呼んでいます。

応募クイズは、技術に関するちょっとしたクイズです。まずは実際のクイズを紹介します。

応募クイズの歴史

2020年 Docker

2020年の応募クイズは、募集サイトに示された docker run を実行するものでした。実行すると、対話型のCUIプログラムが起動します。質問に答えていくと、最後にトークンが発行され、これを応募時に提出します。トークンの正体はJSON Web Token (JWT)で、質問に対する回答が記録されています。

Dockerが使えると解けるという、単純な課題でした。

2021年 Webインスペクタ

2021年は募集サイトに「Webインスペクタを表示する」キーボードショートカットが表示(OSによって表示を変えています)されていて、Webインスペクタを開くと示される指示に従う、という課題でした。Webインスペクタに馴染みがあれば特に難しくありません。

この課題ではSelf-XSS攻撃を助長しないように、赤い文字で警告を表示しています。こういった警告はFacebookなどでもみられますが、特にこの課題ではWebインスペクタを開くように促しているため、気を遣っています。

2022年 GraphiQL

昨年は、募集サイトにGraphiQLというGraphQL IDEを埋め込みました。GraphiQLを介して適切にGraphQLのクエリを書き換えて実行すると、応募URLが取得できます。GraphiQLに内蔵されたドキュメントビューワーを見れば難しくないはずです。

前年のWebインスペクタに続いて、ブラウザ上で完結するようにしています。ちなみに実装上も、GraphQL APIをブラウザ上で動作させています。

応募クイズの作問

ところでこの応募クイズの作問は、毎年とても頭を悩ませる仕事です。

難しさの一つに、難易度の設定があります。昨年までの3年間の応募クイズを見てわかるように、どれも難しくはありません。実際、ちょっとしたプログラミングの経験があれば容易に解けるくらいの難易度を目指しています。難易度が高すぎて応募が減ってしまうと本末転倒ということで、易しめに設定しています。

一方で、難易度が低すぎるとおもしろくないんじゃないかとも思います。毎年インターン生に感想を尋ねていますが、ちょっと易しすぎるものの、つまらないというほどでもない、というような返答を得ることが多いです。バランスが難しいところです。

また、あまり面倒じゃないように工夫しています。面倒であるがために応募が減ってしまうことを懸念して、何分間か手を動かしたら解けるくらいに収めようとしています。ここ2年のブラウザ上で完結させるのは、そういう意識が働いた結果と言えるでしょう。

実際のボツ案

作問をしていてボツにしたアイデアもたくさんあります。ボツ案からアイデアが浮かぶこともあります。例えば昨年のGraphiQLは、ボツ案が元になっています。

当初考えていたのは、gRPCを題材にしたクイズです。

gRPCにはリフレクションという機能があり、リフレクションを有効にしているとクライアントからgRPCサーバーのサービス定義を取得できます。GraphQLのintrospectionに近いものです。これを利用して、リフレクションを有効にしたgRPCサーバーを公開しておいて、回答者はリフレクションを駆使してgRPCのメソッドを呼び出す、という課題を考えました。

典型的な回答例は、evansのようなCLIツールで --reflection オプションを使ってリクエストするようなイメージです。

おもしろくていいアイデアのように思いましたが、ちょっと難しく、面倒そうです。応募クイズとしてはあまり適切とは言えないと思い直しました。それで、gRPCをGraphQLに置き換えたのが2022年の応募クイズでした。

2023年の応募クイズ

さて、では2023年の応募クイズを見てみます。

今年は、パスコードをモチーフにして、特定の4桁の数字を引数に unlock() 関数を呼び出す、という課題です。募集ページにテキストエディタが設置してあり、表示されているJavaScriptを書き換えて「実行」ボタンを押すと、実際にそのコードが実行されます。

4桁の数字については全く手掛かりがありませんが、たかだか10,000通りなので……、という問題になっています。一応ヒントとして、for文の書き方などがガイドされています。

今年も、ブラウザ上で完結させるために、iframeを使ったサンドボックス環境でJavaScriptを動作させています。サンドボックスの内部には暗号化されたURLと unlock() 関数が定義してあり、正しいパスコードを引数にすると復号化されるようになっています。

const encrypted = 's9Vo5kz2HD5y/H/I6ioBKweghEcM2zyoI9Hf74b4jxwALSgj9hk5V6dLe2LJZ/dVRXlO+FW6Kmv895UAhzN4WcDM40gYPN+PaT9hEBTwNyV2fxbe'
const salt = new TextEncoder().encode(atob('MTY2LDYxLDIxOCwyMTMsMjQ5LDEwNiwyNDEsMjQzLDE3Myw2LDAsMTY0LDYxLDEyMiwyMzYsNzg='))

async function _convertPasscodeToKey(passcode) {
  const digest = await crypto.subtle.digest('SHA-256', (new TextEncoder()).encode(String(passcode).padStart(4, '0')))
  const keyMaterial = await crypto.subtle.importKey('raw', digest, 'PBKDF2', false, ['deriveKey'])
  return await crypto.subtle.deriveKey(
    { name: 'PBKDF2', salt, iterations: 1, hash: 'SHA-256' },
    keyMaterial,
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt', 'decrypt']
  )
}

async function _decrypt(key, data) {
  const buffer = new Uint8Array(atob(data).split('').map((c) => c.charCodeAt(0)))
  const iv = buffer.slice(0, 12)
  const encrypted = buffer.slice(12)
  const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, encrypted)
  return (new TextDecoder()).decode(decrypted)
}

async function unlock(passcode) {
  const key = await _convertPasscodeToKey(passcode)
  try {
    return await _decrypt(key, encrypted)
  } catch (e) {
    throw new Error('Invalid passcode')
  }
}

{ name: 'PBKDF2', salt, iterations: 1, hash: 'SHA-256' }iterations を小さな値にするのがポイントで、これであえて強度を下げています。もし大きな値にすると問題として成立しにくくなります。(実際のユースケースでは必ず大きな値にしてください。)

皆さんはこの問題が解けますか?

ぜひご応募ください

ということで、はてなインターンの応募クイズについてご紹介しました。毎年作問にとても悩んでいますが、その分少しでも楽しんでいただければ幸いです。

はてなリモートインターンシップ2023の応募締め切りは6月5日 月曜日の12時(日本標準時)です!今年はまだ枠に余裕があるので、応募するなら今!です。