Wybór identyfikatora: UUID, krótkie kody i te, które zdradzają informacje

Której wersji UUID użyć, dlaczego kolizje v4 nie są praktycznym zmartwieniem, co zdradzają kolejne liczby całkowite, jakie kompromisy niosą alfabety krótkich identyfikatorów i skąd brać losowe bity.

Do wszystkiego, co wewnętrzne, używaj UUID v4, v7 wtedy, gdy identyfikator jest kluczem głównym dużej tabeli, a krótkiego kodu z ograniczonego alfabetu wtedy, gdy człowiek musi go odczytać na głos. Kolejne liczby całkowite to opcja, której należy unikać we wszystkim, co publiczne.

Wersje UUID, które mają znaczenie

UUID ma 128 bitów. Cztery niosą wersję, a dwa wariant, więc żadna wersja nie ma do dyspozycji pełnych 128.

WersjaCo zawieraKiedy jej używać
v160 bitowy znacznik czasu, 14 bitowa sekwencja zegara, 48 bitowy identyfikator węzła (adres MAC)Tylko dane odziedziczone
v3 / v5Skrót MD5 albo SHA-1 z przestrzeni nazw i nazwyGdy potrzebne są identyfikatory deterministyczne
v4122 losowe bity i nic więcejDomyślnie, gdy niczego nie powinno dać się wywnioskować
v748 bitowy znacznik czasu w milisekundach, potem 74 losowe bityKlucze, w których liczy się kolejność wstawiania

v1 jest tą ostrzegawczą. Osadza czas utworzenia, a w większości implementacji także adres MAC maszyny, która go wytworzyła, i właśnie dzięki temu takie identyfikatory pomogły w 1999 roku namierzyć autora wirusa Melissa. v7, ustandaryzowany w RFC 9562 w 2024 roku, publikuje czas utworzenia celowo: to jednocześnie jego zaleta i jego koszt.

Kwestia kolizji

UUID v4 ma 122 losowe bity, więc takich wartości jest 2^122, mniej więcej 5,3 x 10^36. Znaczenie ma granica urodzinowa, a nie rozmiar przestrzeni: szansa, że dowolne dwie z n wartości okażą się takie same, wynosi mniej więcej n do kwadratu przez 2^123. Bilion wygenerowanych UUID niesie z sobą mniej więcej jedną na dziesięć bilionów szansę na duplikat, a szansa rzutu monetą wymaga około 2,7 x 10^18 wartości, czyli jakichś 85 lat przy miliardzie na sekundę.

"Nie jest to praktyczny problem" wciąż nie znaczy "niemożliwe", a luka nigdy nie leży w arytmetyce. Duplikaty biorą się z generatora: ze stałego ziarna, z maszyny wirtualnej sklonowanej razem z pulą entropii, z obrazu kontenera, który wozi z sobą zapisany stan generatora pseudolosowego. Unikalność należy do źródła losowości, a nie do formatu, więc zostaw na kolumnie ograniczenie unikalności.

Kolejność, lokalność indeksu i wyciek informacji

Identyfikatory losowe rozpraszają się. W indeksie B-drzewa każde wstawienie ląduje na losowej stronie liścia, więc zbiorem roboczym jest cały indeks, a nie jego prawa krawędź, spada trafność pamięci podręcznej, a strony się rozszczepiają. Klucz rosnący w czasie dokleja się w jednym miejscu i utrzymuje w cieple wciąż te same kilka stron. Właśnie po to istnieją v7 i ULID; ULID to 48 bitów znacznika czasu w milisekundach plus 80 bitów losowych, zapisane jako 26 sortowalnych znaków w base32 Crockforda.

Kosztem jest przewidywalność: identyfikator uporządkowany w czasie ogłasza, kiedy powstał jego rekord, więc garść takich identyfikatorów odsłania tempo rejestracji i godziny martwe. Jeśli to ma znaczenie, użyj v4 i zapłać cenę po stronie indeksu.

Kolejne liczby całkowite idą jeszcze dalej. /invoices/1041 mówi, że wystawiłeś mniej więcej tysiąc faktur, a dwa identyfikatory oddalone o tydzień zdradzają tempo wzrostu. Gorzej, atakujący może poprosić o 1040. Jeżeli serwer zwróci ten rekord dlatego, że istnieje, a wywołujący jest zalogowany, bez sprawdzenia, do kogo rekord należy, mamy do czynienia z naruszeniem kontroli dostępu, skatalogowanym jako niezabezpieczone bezpośrednie odwołanie do obiektu. Nieodgadywalny identyfikator nie jest kontrolą uprawnień, ale skutecznie powstrzymuje spacer po całej tabeli.

Slugi też są identyfikatorami i przy poprawnej budowie nie zdradzają niczego o skali: znaki diakrytyczne złożone, wielkość liter obniżona, interpunkcja sprowadzona do łączników, dokładnie tak, jak robi to generator slugów. Po publikacji utrzymuj slug niezmieniony; zmiana psuje odnośniki przychodzące.

Krótkie identyfikatory, alfabety i długość

Każdy znak losowego kodu jest wart log2 z rozmiaru alfabetu: 5,95 bita dla base62, 5 dla base32, 4 dla systemu szesnastkowego. Długość i alfabet ustalają prawdopodobieństwo kolizji przy zadanej liczbie identyfikatorów.

Długość (base62)Liczba wartościBitySzansa kolizji w miliardzie identyfikatorów
82,2 x 10^1443,7Pewna
108,4 x 10^1759,5Około 45%
123,2 x 10^2171,5Około 1 na 6500
164,8 x 10^2895,3Około 1 na 100 miliardów

To przy założeniu, że każdy znak jest niezależnie losowy; przedrostek taki jak inv_ dodaje zero bitów.

Do wszystkiego, co czyta człowiek, base62 jest złym alfabetem. 0 i O, 1 oraz l i I to gotowe błędy przy przepisywaniu, a kod rozróżniający wielkość liter nie przeżyje rozmowy telefonicznej. Base32 Crockforda usuwa I, L, O i U oraz przyjmuje na wejściu dowolną wielkość liter, co jest warte jednego dodatkowego znaku.

Skąd biorą się losowe bity

Math.random() nie jest źródłem kryptograficznym. V8 implementuje je za pomocą xorshift128+, którego stan wewnętrzny da się odtworzyć z krótkiej serii wyników, po czym każda przyszła wartość jest przewidywalna. Nadaje się do tasowania w wersji demonstracyjnej albo do wypełniacza. Nie nadaje się do identyfikatora sesji, klucza API, odnośnika resetującego ani tokenu.

// Browsers, Node 19+, Deno and Bun
const id = crypto.randomUUID();

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

Gdzie indziej: crypto.randomBytes w Node, secrets.token_urlsafe w Pythonie, crypto/rand w Go, SecureRandom w Javie, random_bytes w PHP. Generator UUID wytwarza kryptograficznie losowe wartości v4, a generator haseł korzysta z tego samego API przeglądarki; generator liczb losowych służy do próbkowania, a nie do sekretów.

Jedna pułapka przy pisaniu własnego rozwiązania: odwzorowanie losowego bajtu na alfabet 62 znakowy za pomocą % 62 przechyla wynik w stronę pierwszych ośmiu znaków; użyj próbkowania z odrzucaniem.

Dane zastępcze i przykładowe

Lorem ipsum to poszatkowany Cyceron i przetrwał dlatego, że jest nieczytelny: tekst, który wygląda jak język, nie będąc językiem, pozwala ocenić układ po kształcie i długości wiersza, a nie po zdaniach, i właśnie to wytwarza generator Lorem Ipsum. Przed zatwierdzeniem podmień go na realistyczną treść, bo zastępcza łacina ukrywa przepełnienie, które wywoła prawdziwy nagłówek.

Zmyślone dane wymagają jednej zasady: nigdy nie wolno ich pomylić z prawdziwymi.

  • Numery kart pochodzą z opublikowanego zakresu testowego dostawcy płatności albo wprost nie przechodzą kontroli Luhna. Losowy 16 cyfrowy numer, który przechodzi Luhna, należy do kogoś.
  • Identyfikatory krajowe korzystają z zakresów zarezerwowanych. Amerykańskie numery Social Security zaczynające się od 000, 666 albo od 900 do 999 nigdy nie są wydawane.
  • Używaj example.com i domen pokrewnych, zarezerwowanych przez RFC 2606, oraz bloku dokumentacyjnego 192.0.2.0/24.

Dane testowe trafiają na produkcję częściej, niż się spodziewamy: skrypt zasilający wycelowany w niewłaściwą bazę, wypełniacz zostawiony w szablonie wiadomości wysłanej do całej listy. Fałszywe rekordy powinny wyglądać ewidentnie fałszywie, żeby człowiek wyłapał taki rekord, zanim wsparcie zacznie na nim działać.

Kody QR w skrócie

Kod QR to siatka modułów, wersje od 1 do 40, od 21x21 aż po 177x177. Długość ładunku wyznacza wersję, a przy stałym rozmiarze wydruku wyższa wersja oznacza mniejsze moduły, co wymaga lepszego druku i skanowania z bliższej odległości. Skróć adres URL, zanim podasz go generatorowi kodów QR, i pamiętaj, że tryb alfanumeryczny obsługuje wyłącznie wielkie litery, więc HTTPS://EXAMPLE.COM koduje się do mniejszego kodu niż postać zapisana małymi literami.

Korekcja błędów ma cztery poziomy, odzyskujące mniej więcej 7% (L), 15% (M), 25% (Q) i 30% (H) uszkodzonego kodu. Nadmiarowość kosztuje pojemność, więc wyższy poziom wpycha ten sam ładunek w gęstszą wersję. M jest zwykłą wartością domyślną; wybierz Q albo H przy małym druku, powierzchniach zakrzywionych albo logo na środku, które działa właśnie dlatego, że nadmiarowość je pochłania.

O tym, czy kod da się zeskanować, decydują dwa wymagania fizyczne. Strefa ciszy to czysty margines szerokości czterech modułów z każdej strony, po którym dekoder rozpoznaje granicę, więc kod dostawiony wprost do ramki albo zdjęcia często zawodzi. Kontrast musi być naprawdę ciemny na naprawdę jasnym: blady błękit na bieli to częsta przyczyna porażki, a wiele dekoderów odmawia odczytu kodów odwróconych.