/**
* 共有トークンの検証(P1 の簡易トークン認証)。
*
* ★比較は必ず定数時間で行う★
* `a === b` は先頭から比較して違えば即 false を返すため、応答時間の差から
* 1文字ずつ推測される(タイミング攻撃)。`node:crypto` の `timingSafeEqual` を使い、
* ★長さが違う場合も同じ時間で false を返す★。
* `proxy` やAPIルートの中で `===` を書かず、必ずこの関数を通すこと。
*/
import { timingSafeEqual } from "node:crypto";
/**
* 入力トークンが期待値と一致するかを定数時間で判定する。
*
* ★`expected` が未設定・空なら常に false★
* `AUTH_TOKEN` の設定漏れが「認証を無効化して全部通す」になってはいけない。
* ★未設定なら誰も入れない★が正しい挙動(設定漏れをそのまま全公開にしない)。
*
* @param input 利用者が入力した値、または cookie に入っていた値
* @param expected `AUTH_TOKEN`(`readAuthToken()` の戻り値)
*/
export function verifyToken(
input: string | null | undefined,
expected: string | null | undefined
): boolean {
if (!input || !expected) return false;
const inputBuffer = Buffer.from(input, "utf8");
const expectedBuffer = Buffer.from(expected, "utf8");
// timingSafeEqual は長さが違うと throw するので、先に長さを揃えた比較を行う。
// 長さが違う時点で結果は false と決まっているが、★早期 return せずに★
// 同じだけの比較処理を通してから false を返し、応答時間の差を作らない。
if (inputBuffer.length !== expectedBuffer.length) {
// 自分自身と比較して同じ計算量を消費する(結果は捨てる)
timingSafeEqual(expectedBuffer, expectedBuffer);
return false;
}
return timingSafeEqual(inputBuffer, expectedBuffer);
}
/**
* `AUTH_TOKEN` を読む薄い関数(`lib/logger/index.ts` の `resolveLevel()` と同じ形)。
* ★戻り値をログに出さないこと★
*/
export function readAuthToken(): string | undefined {
return process.env.AUTH_TOKEN;
}