識別子の選び方: UUID、短い ID、そして情報が漏れるもの
内部で使うものには UUID v4 を、識別子が大きなテーブルの主キーになる場合は v7 を、人が読み上げる必要がある場合は文字を絞った短いコードを使ってください。連番の整数は、公開されるものでは避けるべき選択肢です。
押さえておくべき UUID のバージョン
UUID は 128 ビットです。そのうち 4 ビットがバージョン、2 ビットがバリアントを表すため、128 ビットすべてを自由に使えるバージョンは存在しません。
| バージョン | 中身 | 使いどころ |
|---|---|---|
| v1 | 60 ビットのタイムスタンプ、14 ビットのクロックシーケンス、48 ビットのノード ID (MAC アドレス) | 既存の古いデータのみ |
| v3 / v5 | 名前空間と名前の MD5 または SHA-1 ハッシュ | 決定的な ID が必要なとき |
| v4 | 122 ビットの乱数のみ | 既定。何も推測させたくないとき |
| v7 | 48 ビットのミリ秒タイムスタンプ、続いて 74 ビットの乱数 | 挿入順が意味を持つキー |
注意が必要なのは v1 です。生成時刻と、ほとんどの実装では生成したマシンの MAC アドレスが埋め込まれます。1999 年に Melissa ウイルスの作者をたどる手がかりになったのは、まさにこの種の識別子でした。2024 年に RFC 9562 で標準化された v7 は、生成時刻を意図的に公開します。これは長所であり、同時に代償でもあります。
衝突の問題
v4 の UUID は 122 ビットの乱数を持つため、その総数は 2^122、およそ 5.3 x 10^36 です。重要なのは空間の大きさではなく誕生日限界です。n 個の値のうちどれか 2 つが一致する確率は、おおよそ n の 2 乗を 2^123 で割った値になります。1 兆個の UUID を生成しても重複が起きる確率は約 10 兆分の 1 で、確率が五分五分になるにはおよそ 2.7 x 10^18 個が必要です。毎秒 10 億個生成しても 85 年ほどかかります。
「実務上の心配ではない」ことは「不可能」ということではありませんし、その差が生まれるのは常に計算以外の部分です。重複は生成器から生まれます。固定されたシード、エントロピープールごと複製された仮想マシン、PRNG の状態を保存したまま配布されたコンテナイメージなどです。一意性はフォーマットではなく乱数源に属するものなので、カラムには一意制約を残しておいてください。
順序、インデックスの局所性、情報の漏洩
ランダムな識別子は散らばります。B-tree インデックスでは挿入のたびにランダムなリーフページに着地するため、ワーキングセットはインデックスの右端ではなくインデックス全体になり、キャッシュヒット率が下がってページ分割が起きます。時間とともに増加するキーなら 1 か所に追記されるので、同じ少数のページだけがホットなまま保たれます。v7 や ULID が存在するのはこのためです。ULID は 48 ビットのミリ秒タイムスタンプと 80 ビットの乱数から成り、Crockford base32 の 26 文字としてソート可能な形で書かれます。
代償は予測可能性です。時刻順の ID はレコードの作成時刻を公開してしまうため、いくつか手に入れば登録のペースや利用の少ない時間帯が分かります。それが問題になるなら v4 を使い、インデックスのコストを払ってください。
連番の整数はさらに多くを漏らします。/invoices/1041 は請求書をおよそ 1000 通発行済みであることを教えますし、1 週間離れた 2 つの ID があれば成長率が分かります。さらに悪いことに、攻撃者は 1040 を要求できます。レコードが存在し、呼び出し元がログイン済みだというだけで、所有者を確認せずにサーバーがそれを返すなら、それはアクセス制御の不備であり、安全でない直接オブジェクト参照として分類されるものです。推測できない ID は認可チェックの代わりにはなりませんが、テーブルを順にたどられることは防げます。
スラグも識別子の一種であり、正しく作れば件数について何も漏らしません。アクセント記号を畳み、小文字にそろえ、句読点をハイフンにまとめる、Slug Generator が行っているとおりです。公開後は変更せずに保ってください。変更すると外部からのリンクが壊れます。
短い ID、文字集合、長さ
ランダムなコードの 1 文字が持つ情報量は、文字集合の大きさの log2 です。base62 なら 5.95 ビット、base32 なら 5 ビット、16 進数なら 4 ビットです。想定件数に対する衝突確率は、長さと文字集合で決まります。
| 長さ (base62) | 異なる値の数 | ビット数 | 10 億個の ID 内で衝突する確率 |
|---|---|---|---|
| 8 | 2.2 x 10^14 | 43.7 | ほぼ確実 |
| 10 | 8.4 x 10^17 | 59.5 | 約 45% |
| 12 | 3.2 x 10^21 | 71.5 | 約 6,500 分の 1 |
| 16 | 4.8 x 10^28 | 95.3 | 約 1000 億分の 1 |
これは各文字が独立にランダムであることを前提とした値です。inv_ のような接頭辞はビットを 1 つも増やしません。
人が読むものには base62 は不向きです。0 と O、1 と l と I は書き写し間違いの温床ですし、大文字と小文字を区別するコードは電話越しには伝わりません。Crockford base32 は I、L、O、U を除外し、入力ではどちらの大小文字も受け付けます。1 文字増えるだけの価値はあります。
乱数ビットの出どころ
Math.random() は暗号用の乱数源ではありません。V8 はこれを xorshift128+ で実装しており、その内部状態は短い出力列から復元できます。復元されてしまえば、以降の値はすべて予測可能です。デモ用のシャッフルやプレースホルダーなら問題ありません。セッション識別子、API キー、リセットリンク、トークンには不適切です。
// Browsers, Node 19+, Deno and Bun
const id = crypto.randomUUID();
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
その他の環境では、Node の crypto.randomBytes、Python の secrets.token_urlsafe、Go の crypto/rand、Java の SecureRandom、PHP の random_bytes を使います。UUID Generator は暗号論的にランダムな v4 の値を生成し、Password Generator も同じブラウザー API を使います。Random Numbers はサンプリング用であり、秘密情報用ではありません。
自前で実装するときの落とし穴が 1 つあります。ランダムなバイトを % 62 で 62 文字の集合に対応させると、先頭の 8 文字に偏ります。棄却サンプリングを使ってください。
プレースホルダーとサンプルデータ
Lorem ipsum は崩したキケロの文章であり、読めないからこそ生き延びてきました。言語のように見えて言語ではないテキストであれば、レイアウトを文意ではなく形と行長で判断できます。Lorem Ipsum ジェネレーターが生成するのはそういうテキストです。最終確認の前には現実的な文言に差し替えてください。プレースホルダーのラテン語は、実際の見出しが引き起こすはみ出しを隠してしまいます。
作り物のデータに必要な規則は 1 つです。本物と取り違えられてはならない、ということです。
- カード番号は決済事業者が公開しているテスト用の範囲から取るか、Luhn チェックにはっきり落ちるものにします。Luhn を通るランダムな 16 桁の番号は、誰かのものです。
- 国民識別番号は予約された範囲を使います。米国の社会保障番号のうち 000、666、900 ~ 999 で始まるものは発行されません。
- ドメインは RFC 2606 で予約されている
example.comとその仲間を、IP アドレスはドキュメント用の 192.0.2.0/24 を使ってください。
テストデータは思っている以上の頻度で本番に届きます。誤ったデータベースを指したシードスクリプト、リスト全員に送られたメールテンプレートに残っていたプレースホルダーといった具合です。偽のレコードは見るからに偽物であるべきです。そうすればサポートが動き出す前に誰かが気づけます。
QR コードの要点
QR コードはモジュールの格子であり、バージョン 1 から 40 まで、21x21 から 177x177 までのサイズがあります。バージョンを決めるのはペイロードの長さです。印刷サイズが固定なら、バージョンが上がるほどモジュールは小さくなり、より良い印刷と近づけたスキャンが必要になります。QR Code Generator に渡す前に URL を短くしてください。また、英数字モードは大文字のみなので、HTTPS://EXAMPLE.COM は小文字の形より小さくエンコードされます。
誤り訂正には 4 段階あり、破損したコードのおよそ 7% (L)、15% (M)、25% (Q)、30% (H) を復元できます。冗長性は容量を消費するので、レベルを上げると同じペイロードでもより高密度なバージョンに押し上げられます。通常の既定値は M です。小さな印刷、曲面、中央にロゴを重ねる場合は Q か H を選びます。ロゴを重ねられるのは、この冗長性が吸収してくれるからです。
読み取れるかどうかを決める物理的な条件が 2 つあります。1 つはクワイエットゾーンで、四辺すべてに 4 モジュール分の余白が必要です。デコーダーはこれを使って境界を見つけるため、枠線や写真にぴったり接したコードはよく失敗します。もう 1 つはコントラストで、本当に明るい地の上に本当に暗い色でなければなりません。白地に淡い青はよくある失敗例ですし、多くのデコーダーは白黒が反転したコードを受け付けません。