← ./articles-ja

crypto.randomUUIDがLAN HTTPで失敗する時はgetRandomValues fallbackを使う

crypto.randomUUID() はlocal developmentでは動くのに、phoneから http://192.168.x.x で開くと落ちることがあります。

これは偶然ではありません。crypto.randomUUID() はsecure contextに制限されています。https:// はsecureです。http://localhost も開発用にsecure扱いされます。しかしplain HTTPのLAN IPはsecureではありません。

同じnetwork上の別deviceからclient-side appを開く可能性があるなら、crypto.randomUUID() を直接呼ばずfallbackを用意します。

誤解しやすいlocal test

これは動きます。

http://localhost:5173

これは失敗することがあります。

http://192.168.1.20:5173

codeは同じです。

const id = crypto.randomUUID();

originが変わったことで、browser security ruleも変わります。

fallback UUID生成にgetRandomValuesを使う

crypto.getRandomValues() はより広いcontextで使え、UUID v4風の値を生成できます。

export function createBrowserId(): string {
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
    try {
      return crypto.randomUUID();
    } catch {
      // LAN HTTPなどのnon-secure contextではfallbackする
    }
  }

  const bytes = new Uint8Array(16);
  crypto.getRandomValues(bytes);

  bytes[6] = (bytes[6] & 0x0f) | 0x40;
  bytes[8] = (bytes[8] & 0x3f) | 0x80;

  const hex = [...bytes]
    .map((byte) => byte.toString(16).padStart(2, "0"))
    .join("");

  return [
    hex.slice(0, 8),
    hex.slice(8, 12),
    hex.slice(12, 16),
    hex.slice(16, 20),
    hex.slice(20),
  ].join("-");
}

browserでIDを生成する箇所は、このhelperに寄せます。

使うべき場面

fallbackを入れる場面です。

  • dev serverをphone/tabletから開く
  • LAN access用のQR codeを表示する
  • self-hosted appをhome-network HTTPで使う
  • React componentやbrowser storeでIDを作る
  • localhostでは動くが別deviceで失敗する

Node.js server-side codeや、常にHTTPSで配信されるbrowser appでは不要なこともあります。

security-sensitive IDには使わない

このfallbackは、temporary UI ID、local record、optimistic row、tab ID、draft IDのような通常のclient-generated identifier向けです。

token、authentication、password reset、license key、payment referenceのようなsecurity-sensitive IDは、server-sideまたはtrusted backendで生成します。client-side UUIDをsecurity boundaryにしてはいけません。

fallback pathのtestを入れる

randomUUID がthrowする形でtestできます。

import { expect, test, vi } from "vitest";
import { createBrowserId } from "./createBrowserId";

test("falls back when randomUUID throws", () => {
  const originalCrypto = globalThis.crypto;

  vi.stubGlobal("crypto", {
    randomUUID: () => {
      throw new Error("secure context required");
    },
    getRandomValues: (array: Uint8Array) => {
      array.fill(1);
      return array;
    },
  });

  expect(createBrowserId()).toMatch(
    /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
  );

  vi.stubGlobal("crypto", originalCrypto);
});

重要なのはrandom値そのものではなく、UUID shapeとversion bitです。

デバッグチェック

phoneやLAN URLだけで落ちる場合:

  1. crypto.randomUUID() を検索する
  2. 失敗originがplain HTTPかつlocalhostではないか確認する
  3. direct callをhelperへ置き換える
  4. security-sensitive IDはserver側に置く
  5. localhostだけでなくLAN URLで再テストする

参考