TypeID – typsichere, K-sortierbare, global eindeutige Identifikatoren, inspiriert von Stripe-IDs
(github.com/jetpack-io)- TypeID ist ein typsicherer Identifikator auf Basis einer Erweiterung von UUIDv7, ein Format für global eindeutige Identifikatoren, inspiriert von der Nutzung von Präfixen in der Stripe API
- Die kanonische Zeichenkette besteht aus drei Teilen: einem kleingeschriebenen type prefix in snake_case-ASCII mit maximal 63 Zeichen, einem Trennzeichen
_und einem 26 Zeichen langen UUIDv7-Suffix, das mit modifiziertem base32 kodiert ist - Mit dem type prefix wird verhindert, dass eine
user-ID versehentlich dort verwendet wird, wo einepost-ID erforderlich ist; außerdem lässt sich beim Debugging sofort erkennen, auf welchen Entitätstyp sich ein Identifikator bezieht - TypeID ist eine Obermenge von UUID; entfernt man die Typinformation und dekodiert den Wert, erhält man eine gültige UUIDv7
- Durch die K-Sortierbarkeit kann TypeID als Primärschlüssel in Datenbanken verwendet werden und soll im Vergleich zu vollständig zufälligen globalen IDs wie UUIDv4 Probleme mit schlechter Datenbank-Locality verringern
- Die base32-Kodierung bietet URL-Sicherheit, Unabhängigkeit von Groß-/Kleinschreibung, vermeidet mehrdeutige Zeichen, ermöglicht Auswahl per Doppelklick und ist kürzer als die traditionelle hexadezimale Kodierung von UUIDs
- Offizielle Implementierungen werden für Go, SQL und TypeScript bereitgestellt; die aktuelle Spec-Version ist v0.3.0
- Mit dem Kommandozeilen-Tool lassen sich TypeIDs erzeugen, UUIDs aus bestehenden TypeIDs dekodieren und UUIDs als TypeID kodieren
typeid new prefixerzeugt eine neue TypeIDtypeid decode prefix_01h2xcejqtf2nbrexx3vqjhp41gibt type und uuid austypeid encode prefix 0188bac7-4afa-78aa-bc3b-bd1eef28d881erzeugt eine TypeID
- Im TypeID Converter von Jetify lassen sich TypeID-Strings in UUIDs umwandeln oder UUIDs im Format
prefix:UUIDin TypeIDs konvertieren
1 Kommentare
Hacker-News-Kommentare
Es gibt ein paar Vorschläge: Den Prefix-String sollte man besser festlegen und dokumentieren, bevor es zu spät ist.
In der Go-Implementierung sieht es mit kleingeschriebenem ASCII gut aus, aber bei zusammengesetzten Typen wie
article-commentist es etwas unklar.Komplexe Projekte oder ORMs können Trennzeichen kaum vermeiden, daher wäre es sinnvoll zu prüfen, ob ein einzelnes Trennzeichen erlaubt werden sollte.
In der Go-Implementierung gibt es keine Tests; da sich der Code leicht per Unit-Tests prüfen lässt, sollte man sie unbedingt hinzufügen.
In Go wäre es besser, wie bei Googles UUID-Implementierung eine saubere Parsing-Funktion und ein internes Byte-Array zu verwenden, und Strings nur für Rendering und Prefixe zu nutzen.
Das aktuelle Parsing ist zu großzügig und scheint bei leerem Suffix in einen Erzeugungsmodus zu wechseln; außerdem dürfte die Indexierung nach
SplitNpanicen, wenn kein Unterstrich vorhanden ist.Am Design selbst wollte ich etwas aussetzen, aber am Ende scheint es den Sweet Spot der Trade-offs ziemlich gut zu treffen.
Trotzdem ist der Hinweis berechtigt, und je mehr Implementierungen in verschiedenen Sprachen dazukommen, desto mehr müssen wir sicherstellen, dass sie miteinander kompatibel sind; deshalb planen wir, strengere Testsuiten hinzuzufügen.
Was den Prefix betrifft, würde mich interessieren, ob die Sorge darin liegt, dass die erlaubte Zeichmenge in der Spezifikation nicht definiert ist.
Außerdem wurde der Prefix in der Spezifikation klargestellt.
Der Prefix wird je nach Einsatzdomäne festgelegt.
Ich verwende so einen Ansatz seit einigen Jahren, mit zwei Unterschieden.
Erstens gehe ich nicht davon aus, dass Menschen die Werte tatsächlich von Hand eingeben, daher kümmere ich mich nicht besonders um die Verwechslung von
lund1.Stattdessen verwende ich base32 ohne die Vokale
eiou, um die Wahrscheinlichkeit zu verringern, dass zufällig Wörter entstehen, insbesondere Schimpfwörter.Zweitens füge ich zwei base32-Zeichen mit Salt als Prüfsumme hinzu.
So muss man bei fehlerhaften oder böswilligen Werten nicht erst den Datenspeicher abfragen; ich verstehe nicht, warum andere Implementierungen das nicht machen.
analritaals automatisch generiertes Passwort ausgegeben, woraufhin sich jemand beschwert hat.Man könnte erwägen, auch
azu den ausgeschlossenen Zeichen hinzuzufügen.Wenn man nicht davon ausgeht, dass Menschen die Werte von Hand eingeben, frage ich mich, über welchen Weg versehentlich falsche Werte überhaupt hineingeraten.
Kopier-/Einfügefehler oder Fälle, in denen eine Seite alles in Großbuchstaben rendert, kann es natürlich geben, aber bei base32 könnte man Groß-/Kleinschreibung normalisieren.
Außerdem würde mich interessieren, wie eine 2-Byte-Prüfsumme, die kryptografisch nicht sicher ist, im Fall von böswilliger Eingabe helfen soll.
Wir prüfen gerade, ob sie in die TypeID-Spezifikation aufgenommen werden sollte.
Ich weiß nicht, wie viele Datenbankabfragen dadurch eingespart wurden, aber es ist angenehm zu sehen, dass man beim Dekodieren nicht in undeutlichere Fehlerzustände läuft.
Unabhängig vom Haupttext gibt es noch einen Link zum „Crockford's alphabet“: https://www.crockford.com/base32.html
Dieses Base32-Schema schließt aus den alphanumerischen Zeichen
IundLaus, die mit1verwechselt werden,O, das mit0verwechselt wird, sowieU.Auf der Seite heißt es, der Grund für das Weglassen von
Usei „versehentliche Obszönität“, aber ich verstehe ehrlich gesagt nicht, was damit gemeint ist.Um aus nicht leicht verwechselbaren alphanumerischen Zeichen ein brauchbares Base32-Alphabet zu bauen, müsste man nur
O,IundLweglassen.Dann bleiben aber noch 33 Zeichen übrig, also muss noch eines weg, und da es egal ist, welches, hat er dem letzten ausgeschlossenen Zeichen einfach einen willkürlichen Grund gegeben.
Für IDs, die Nutzer zu Gesicht bekommen, ist das kein völlig abwegiger Grund, aber natürlich verhindert es überhaupt nichts perfekt.
IundObereits ausgeschlossen hat und dann auch nochUentfernt, kann man ziemlich viele grob wirkende Drei- oder Vierbuchstabenkombinationen vermeiden.Natürlich gibt es mit
Aweiterhin mögliche Kombinationen, aber die Wahrscheinlichkeit ist sehr gering.Uzu Ciceros Zeiten noch nicht und ist daher ein vulgärer Buchstabe, fast schon obszön.Im Unternehmen haben wir mehrere neue „Basen“ für QR-Codes definiert; ich persönlich halte das für einen in der Informatik eher untergenutzten Bereich.
Wir arbeiteten an einem Problem mit URL-Slugs, und weil die ziemlich lang waren, hielten wir 5 Bit pro Byte für sinnvoll, um den Aufwand beim Abschreiben zu verringern.
Am Ende hatten wir noch Spielraum, weitere Zeichen wegzulassen, und als alle anderen schon Feierabend hatten, saßen wir zu zweit da und ordneten Flüche nach ihrem Beleidigungsgrad, um zu entscheiden, welche Zeichen entfernt werden sollten.
Später habe ich dann überzeugt, dass abwertende Bezeichnungen das größere Problem sind, und wir haben uns darauf konzentriert, statt
uliebernzu entfernen.tggrist einfach nur niedlich, abern**rhätte zu unangenehmen Gesprächen mit mehreren HR-Teams geführt.Der endgültige Zeichensatz ist inzwischen etwas verschwommen in meiner Erinnerung, aber normalerweise arbeiteten wir mit
[a-z][0-9], und es gab viele Zeichen, die man nicht in URLs verwenden konnte oder die sich schlecht vorlesen ließen.Soweit ich mich erinnere, haben wir
0,lund1alle entfernt, und wir gingen davon aus, dass alles entweder komplett in Groß- oder komplett in Kleinbuchstaben abgeschrieben würde.0owar kein Problem, und1Lebenso wenig.Crockford-Encoding gefällt mir nicht besonders.
Wenn ich tatsächlich kodierte Werte dieser Art supporten oder analysieren musste, fühlte es sich wie ein offensichtlicher Fehler an.
Dieses Alphabet wurde auf ein in der Praxis eher seltenes Ziel hin entworfen, nämlich Identifikatoren am Telefon vorzulesen, und führt dadurch Mehrdeutigkeit ein.
Wenn man anhand des kodierten Strings selbst Logs
grepen oder Querverweise herstellen will, ist das katastrophal, und die erlaubten Bindestriche sind zudem eine Hauptquelle für Copy-and-paste- und Zeilenumbruchfehler.Objektkennungen tippt man zwar nur selten direkt ein, aber man kopiert und fügt sie ständig zwischen Anwendungen, Chats und Foren ein, verschickt sie per E-Mail und sucht in Logdateien danach.
Unter solchen Bedingungen ist Aussprechbarkeit bedeutungslos, Groß-/Kleinschreibungsinsensitivität eher hinderlich, und was man braucht, sind Konsistenz sowie Robustheit gegenüber Einfügen und Zeilenumbrüchen.
base58 ist dafür eine deutlich besser passende bijektive Kodierung und sogar noch kürzer.
Von Stripe inspiriert verwende ich seit Jahren Base58-kodierte UUIDs mit Typ-Präfixen wie
user_1BzGURpnHGn6oNru84B3Rials Objektkennungen.Man sollte allerdings berücksichtigen, dass Douglas Crockfords Base32-Kodierung vor 20 Jahren entworfen wurde, in einer Zeit mit recht anderen Einsatzbedingungen.
Ich wollte mich aber letztlich nicht auf Groß-/Kleinschreibung verlassen, daher tendierte ich am Ende eher zu base32.
Es kann zum Beispiel sein, dass man IDs als Dateinamen verwenden möchte und dabei an eine Umgebung mit case-insensitivem Dateisystem gebunden ist.
TypeID verwendet nur das Crockford-Alphabet immer in Kleinbuchstaben, nicht das vollständige Crockford-Encoding-Regelwerk.
In TypeID sind Bindestriche nicht erlaubt, und es ist auch nicht erlaubt, dieselbe ID auf mehrere Arten zu kodieren, indem man mehrdeutige Zeichen anders schreibt.
Aber es kommt gelegentlich vor, dass man Kennungen aus Screenshots oder Screen-Sharing eingeben muss oder von einem anderen Gerät, auf dem sich die Kennung nicht leicht übernehmen lässt.
Crockford-Base32 funktioniert zwar, überzeugt mich aber auch nicht völlig.
Persönlich wäre meine erste Wahl ein KSUID mit Typ-Präfix.
Damit bekommt man eine 160-Bit-K-sortierbare ID in Base62-Kodierung, und solange man nicht aus Kompatibilitätsgründen unbedingt eine 128-Bit-ID braucht, passt das sehr gut.
Meine bevorzugte base32-Kodierung ist z-base-32, die ich augenfreundlicher finde: https://philzimmermann.com/docs/human-oriented-base-32-encod...
Das größte Problem von base58 ist, dass es sich gut für Ganzzahlen eignet, aber weniger für beliebige Binärdaten wie kryptografische Schlüssel, und dass Zeichenketten mit Groß-/Kleinschreibung nicht besonders gut aussehen.
Sauber
Mir gefällt das Prefix für „Typensicherheit“.
In unserem ORM nannten wir eine ähnliche Methode, bei der automatisch entity-spezifische Tags an Integer-IDs in der DB angehängt werden, tagged ids: https://joist-orm.io/docs/advanced/tagged-ids
Wir haben
:als Trennzeichen verwendet, aber im Hinblick auf Copy-and-Paste per Doppelklick bereue ich ein wenig, nicht_genommen zu haben.Theoretisch wäre es für Joist wohl auch sehr einfach, eine „UUID-Spalte in der DB“ in eine „TypeID im Domain-Modell“ umzuwandeln, aber aktuell scheint das nicht allein über Konfiguration im User-Bereich möglich zu sein.
Trotzdem eine gute Idee.
Element-IDs sehen etwa so aus:
t3_15bfi0, wobeit3_das Typ-Prefix ist.t3steht für Beiträge,t1für Kommentare,t5für Subreddits, und der Rest ist eine Base36-Codierung des autoinkrementierenden Primärschlüssels..).Doppelpunkte wurden in der URL-Codierung zu
%-Escapes, wodurch die IDs länger wurden und die URLs hässlich und lang aussahen, was extrem nervig war.UUIDv7 ist auf HN seit Jahren beliebt, und ich frage mich, wann es zu einem echten Standard wird und wann Libraries, Datenbanken und der Rest des Ökosystems es nativ unterstützen werden.
Die meiste Software kümmert sich nicht um bestimmte Bits innerhalb einer UUID, also kann man sie auch heute schon verwenden.
Falls es Software gibt, die sich für bestimmte Bits interessiert, kann man sie einfach wie UUIDv4 aussehen lassen, und auch solche Bits können zufällig erzeugt werden.
Wenn ein Erzeugungsverfahren nötig ist, kann man es selbst schreiben; das ist nicht schwer.
Ich betreue zum Beispiel die Dart-UUID-Library, und im neuesten Beta-Major-Release sind v6, v7 und benutzerdefinierte v8 enthalten.
Irgendwo gibt es auch eine Implementierungsliste, und ich stehe auf dieser Seite als Library-Maintainer, sodass ich benachrichtigt werde, wenn ein neuer Draft erscheint.
Mir gefällt der Teil mit „per Doppelklick kopieren/einfügen“.
Details sind wichtig.
Ich habe ein paar Beschwerden über UUIDs.
Ich verstehe nicht, warum man nicht einfach Zeit + Zufall kombinieren kann, statt dieses ganze Ritual um UUID-Versionierung zu betreiben.
Wenn Lokalität nicht wichtig ist, kann man einfach direkt 128 Bit Zufallsdaten verwenden.
Meiner Erfahrung nach glauben die meisten Leute, UUIDs müssten in ihrer menschenlesbaren hexadezimalen Darstellung gespeichert werden, sogar mit Bindestrichen.
Das verschwendet in Datenbanken, Netzwerken und im Speicher viel zu viel Platz.
Zum Beispiel ist ULID https://github.com/ulid/spec kürzer, speichert Zeit und ist lexikografisch sortierbar.
In Wirklichkeit ist es wie bei anderen Engineering-Problemen auch: Man schreibt die Anforderungen auf und schaut, was sie erfüllt.
Es ist hilfreich, über die Vorteile verschiedener Formate zu lesen, weil man dadurch von dem profitiert, was andere mühsam gelernt haben.
Sortierbarkeit und zeitbasierte Lokalität kommen einem nicht unbedingt von selbst in den Sinn, aber wenn man sie braucht, ist es viel besser, das vorher zu wissen, als vier Jahre später im Projekt festzustellen, dass man diese Daten weggeworfen hat.
Manche UUID-Formate haben sich auch kleine Sicherheitsprobleme selbst eingebaut; Dinge wie das Leaken der MAC-Adresse bei UUID v1 sollte man besser vermeiden.
Wenn eine bestehende Lösung für den passenden Use Case richtig ist, dann nutze sie einfach.
Wenn nicht, muss man sich auch keine allzu großen Sorgen machen.
Persönlich habe ich dort, wo ich UUIDs am häufigsten benutze, einfach festgelegt: „Schickt einfach irgendeinen eindeutigen String; wenn es euch ein gutes Gefühl gibt, benutzt die UUID-Library eurer Wahl.“
In diesem System scheint es je nach Datenquelle unterschiedliche eindeutige Formate zu geben, aber das ist in Ordnung.
Wir liegen nur bei einigen Zehntausend Vorgängen pro Tag, also gibt es auch kein Skalierungsproblem, und niemand muss nach UUID sortieren.
Es ist weniger eine echte Kennung als ein eindeutiges Token, das der Sender pro Nachricht erzeugt, um in einem heterogenen System mit mehreren Queues doppelt eintreffende Nachrichten nachgelagert zu erkennen.
Es muss nicht einmal global eindeutig sein, sondern nur innerhalb eines kleinen Shards, und prinzipiell wäre es auch in Ordnung, wenn es sich im Lauf der Zeit wiederholt.
Der Einfachheit halber halten wir das System aber über die gesamte Laufzeit hinweg eindeutig.
Wenn wir sie selbst erzeugen, lesen wir Daten aus
/dev/urandomund kodieren sie als Base64; dabei haben wir eine Größe gewählt, die nicht auf==endet.Auch das war kein echtes Problem, sondern nur eine Frage der Ästhetik.
Trotzdem verstehe ich immer noch nicht, warum die Bindestriche nötig sind, und die anderen Versionen halte ich für wenig sinnvoll.
Wenn UUIDv7 oder UUID nicht zwingend erforderlich sind, bietet https://github.com/segmentio/ksuid einen deutlich größeren Schlüsselraum
Wenn ein Namespace benötigt wird, kann man einfach einen String-Präfix anhängen, und die Wahrscheinlichkeit einer KSUID-Kollision ist viel geringer als bei jeder UUID-Version
Unter den universellen ID-Generatoren mit sortierbarem Zeitstempel war ksuid für mich am besten, und es gibt Bibliotheken für die meisten Sprachen
UUID v1~v7 sind verschwenderisch
Wenn man sich die zusätzlichen Bits leisten kann, was meistens der Fall sein dürfte, ist KSUID ein hervorragendes Format
[1] https://github.com/sophiabits/resource-id
Das große Problem von ksuid ist allerdings, dass es 160 Bit hat und deshalb nicht zu nativen UUID-Typen in Datenbanken wie PostgreSQL passt, was Performance-Kosten verursacht
1: https://github.com/svix/rust-ksuid
2: https://github.com/svix/python-ksuid
K-sortierbar ist ein großartiges Konzept, und schwach sortierte Schlüssel lösen viele Anwendungsfälle
Auch die typisierte kompakte String-Darstellung gefällt mir
Ich frage mich allerdings, ob der unbeabsichtigte Nebeneffekt von UUID v7 viele Sicherheitsprobleme verursachen wird
Man sollte UUIDs weder als Token noch als Primärschlüssel der DB verwenden, aber in der Praxis wird genau das getan
UUID v4 ist praktisch kryptografischer Zufall, daher sind viele Sicherheitslücken bisher wohl eher zufällig glimpflich ausgegangen
Ich dachte bei UUID v7, dass die tatsächlichen Zufallsdaten auf 32 Bit hinauslaufen, und meinte, man müsse Entwickler stärker darauf hinweisen, dass UUIDs vorhersagbar sein können
Korrektur: Offenbar habe ich mich bei der Vorhersagbarkeit von v7-UUIDs geirrt
Im Entwurf wird für die Zufallsbits ein CSPRNG empfohlen, die Entropie beträgt mindestens 74 Bit und sie sind so ausgelegt, dass sie „nicht vorhersagbar“ sind
Für sicherheitsrelevante Zwecke wird zwar empfohlen, „UUID v4“ zu verwenden, aber das könnte sich vermutlich auf die Bedeutung des Zeitstempels beziehen
Wenn die Namen UUIDv4 und UUIDv7 lauten, führt das dann nicht zu endloser Verwirrung darüber, welches für Datenbanken gut ist und welches für Einmal-Token?
Mir ist auch keine gute rückwärtskompatible Lösung bekannt
In Elixir verwendet man für das Erzeugen einer v4-UUID die Funktion
UUID.uuid4()Theoretisch könnte man den Code nach Verwendungen durchsuchen, aber all das erhöht die Fehleranfälligkeit
Dasselbe galt für hashbasierte Varianten wie v3
v4 ist einfach und weniger anfällig für Missbrauch