Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

timingSafeEqual を使う

timingSafeEqual で値を安全に比較し、タイミング攻撃から守ります。

最終更新 Markdown で表示Agent セットアップ

すぐに始める場合は、下のボタンをクリックします。

Cloudflare にデプロイ

GitHub アカウントにリポジトリが作られ、アプリケーションが Cloudflare Workers にデプロイされます。

crypto.subtle.timingSafeEqual は、定数時間アルゴリズムで 2 つの値を比較します。所要時間は値の内容に依存しません。

等価演算子(== または ===)で文字列を比較すると、最初の不一致文字で比較が終わります。timingSafeEqual を使うと、攻撃者はタイミングから 2 つの文字列のどこが違うかを特定できません。

timingSafeEqual は、比較する 2 つの ArrayBuffer または TypedArray を受け取ります。バッファーの長さは同じである必要があり、違うと例外が投げられます。 この関数はパラメータの長さに対しては定数時間ではなく、周囲のコードの定数時間も保証しません。 シークレットの扱いは、タイミング副作用を持ち込まないよう注意してください。

2 つの文字列を比較するには、TextEncoder API を使います。

interface Environment {
	MY_SECRET_VALUE?: string;
}

export default {
	async fetch(req: Request, env: Environment) {
		if (!env.MY_SECRET_VALUE) {
			return new Response("Missing secret binding", { status: 500 });
		}

		const authToken = req.headers.get("Authorization") || "";

		const encoder = new TextEncoder();

		const userValue = encoder.encode(authToken);
		const secretValue = encoder.encode(env.MY_SECRET_VALUE);

		// Do not return early when lengths differ — that leaks the secret's
		// length through timing.  Instead, always perform a constant-time
		// comparison: when the lengths match compare directly; otherwise
		// compare the user input against itself (always true) and negate.
		const lengthsMatch = userValue.byteLength === secretValue.byteLength;
		const isEqual = lengthsMatch
			? crypto.subtle.timingSafeEqual(userValue, secretValue)
			: !crypto.subtle.timingSafeEqual(userValue, userValue);

		if (!isEqual) {
			return new Response("Unauthorized", { status: 401 });
		}

		return new Response("Welcome!");
	},
};
from workers import WorkerEntrypoint, Response
from js import TextEncoder, crypto

class Default(WorkerEntrypoint):
    async def fetch(self, request):
        auth_token = request.headers["Authorization"] or ""
        secret = self.env.MY_SECRET_VALUE

        if secret is None:
            return Response("Missing secret binding", status=500)

        encoder = TextEncoder.new()
        user_value = encoder.encode(auth_token)
        secret_value = encoder.encode(secret)

        # Do not return early when lengths differ — that leaks the secret's
        # length through timing.  Always perform a constant-time comparison.
        if user_value.byteLength == secret_value.byteLength:
            is_equal = crypto.subtle.timingSafeEqual(user_value, secret_value)
        else:
            is_equal = not crypto.subtle.timingSafeEqual(user_value, user_value)

        if not is_equal:
            return Response("Unauthorized", status=401)

        return Response("Welcome!")
import { Hono } from 'hono';

interface Environment {
  Bindings: {
    MY_SECRET_VALUE?: string;
  }
}

const app = new Hono<Environment>();

// Middleware to handle authentication with timing-safe comparison
app.use('*', async (c, next) => {
  const secret = c.env.MY_SECRET_VALUE;

  if (!secret) {
    return c.text("Missing secret binding", 500);
  }

  const authToken = c.req.header("Authorization") || "";

  const encoder = new TextEncoder();

  const userValue = encoder.encode(authToken);
  const secretValue = encoder.encode(secret);

  // Do not return early when lengths differ — that leaks the secret's
  // length through timing.  Instead, always perform a constant-time
  // comparison: when the lengths match compare directly; otherwise
  // compare the user input against itself (always true) and negate.
  const lengthsMatch = userValue.byteLength === secretValue.byteLength;
  const isEqual = lengthsMatch
    ? crypto.subtle.timingSafeEqual(userValue, secretValue)
    : !crypto.subtle.timingSafeEqual(userValue, userValue);

  if (!isEqual) {
    return c.text("Unauthorized", 401);
  }

  // If we got here, the auth token is valid
  await next();
});

// Protected route
app.get('*', (c) => {
  return c.text("Welcome!");
});

export default app;

役に立ちましたか?