21 Punkte von GN⁺ 2025-09-19 | 1 Kommentare | Auf WhatsApp teilen
  • UUIDv47 speichert in der Datenbank eine sortierbare UUIDv7 und liefert über externe APIs einen Wert, der wie UUIDv4 aussieht
  • Nur das Timestamp-Feld wird per XOR maskiert, um die Zeitinformationen von UUIDv7 zu schützen; die übrigen Zufallsfelder bleiben unverändert
  • Die Maskierung mit einem 128-Bit-Schlüssel auf Basis von SipHash-2-4 ermöglicht sicheren Informationsschutz, ohne das Risiko einer Schlüsseloffenlegung
  • Encode/Decode sind deterministisch und reversibel, die Zufälligkeit bleibt erhalten, wodurch das Kollisionsrisiko gering ist
  • Benchmark-Ergebnisse zeigen sehr hohe Geschwindigkeit und eine einfache Integrationsmethode; eine leichte Anbindung an Datenbanken wie PostgreSQL ist möglich

Projektüberblick und Bedeutung

  • UUIDv47 ist eine Open-Source-C-Bibliothek, die intern in der Datenbank UUIDv7, vorteilhaft für Sortierung und Indizierung, speichert und nach außen über APIs und Systeme Werte ausgibt, die wie UUIDv4 aussehen, um Datenschutz und hohe Performance gleichzeitig zu erreichen
  • Im Vergleich zu anderen UUID-Transformationsalgorithmen hebt sie sich durch reversible Abbildung, RFC-Kompatibilität, Sicherheit ohne Schlüsselrückgewinnung, zero-deps und eine Struktur mit nur einer einzubindenden Header-Datei als besondere Stärke hervor

Hauptmerkmale

  • Header-only C (C89), einfache Integration ohne externe Abhängigkeiten
  • Nur das Timestamp-Feld von UUIDv7 wird per XOR maskiert, um das Offenlegen von Zeitinformationen zu verhindern; die übrigen Zufallsfelder werden nicht verändert
  • Die Maskierung mit keyed SipHash-2-4 ermöglicht mit einem 128-Bit-Schlüssel einen sicheren Informationsschutz
  • Der Encode-/Decode-Prozess ist deterministisch und vollständig reversibel (das Original kann exakt wiederhergestellt werden)
  • Unterstützt schnelles Mapping zwischen UUIDs für die Datenbankspeicherung (v7) und für die externe Bereitstellung (v4)
  • Bietet zahlreiche Beispiele wie Testcode und Benchmark-Tools

Einsatzzweck und Vorteile

  • Nutzt sortierbare UUIDv7, um Index Locality und Paging-Effizienz in der DB zu maximieren
  • Nach außen wird nur ein Muster offengelegt, das wie UUIDv4 aussieht, wodurch Timestamp-Leaks und Nachverfolgung verhindert werden
  • Durch SipHash ist eine Schlüsselrückgewinnung nicht möglich, die Sicherheit des Geheimschlüssels bleibt gewährleistet
  • RFC-kompatible Verarbeitung von Versions-/Varianten-Bits
  • Die Ausführung ist schnell und damit auch in Echtzeitverarbeitung und Umgebungen mit großen Generierungsmengen effizient

Wichtige Struktur und internes Funktionsprinzip

UUIDv7 Layout

  • ts_ms_be: 48-Bit-Big-Endian-Timestamp
  • ver: oberes Nibble des 6. Bytes (0x7=DB, 0x4=extern)
  • rand_a: 12-Bit-Zufallswert
  • var: RFC-Variante (0b10)
  • rand_b: 62-Bit-Zufallswert

Maskierungs- und Mapping-Logik (Façade mapping)

  • Encoding: ts48 XOR mask48(R), version=4 setzen
  • Decoding: encTS XOR mask48(R), version=7 setzen
  • Keine Änderung an den Zufallsfeldern
  • Als SipHash-Input wird ein 10-Byte-Zufallsfeld verwendet
  • Die XOR-Maskierung kann bei bekanntem Schlüssel sofort rückgängig gemacht werden

Sicherheitsmodell

  • Ziel: Der Schlüssel darf nicht offengelegt werden, selbst wenn Eingaben gezielt gewählt werden können
  • Umsetzung: Verwendung von SipHash-2-4 als keyed pseudorandom function (PRF)
  • Nutzung eines 128-Bit-Schlüssels; empfohlen wird die Schlüsselableitung etwa über HKDF
  • Bei Schlüsselrotation wird empfohlen, den Schlüssel nicht in der UUID zu speichern, sondern eine kleine separate Key-ID zu verwalten

Öffentliche API (C)

  • uuidv47_encode_v4facade : v7→v4-Umwandlung
  • uuidv47_decode_v4facade : v4→v7-Wiederherstellung
  • Weitere Funktionen für Versionssetzung, Parsing und Formatierung verfügbar

Performance und Benchmarks

  • Bei SipHash-Maskierung (10B) unter 14 ns/op, vollständiger Roundtrip aus Encode+Decode bei etwa 33 ns/op (Apple M1)
  • Schnelle Verarbeitung auch bei massenhafter UUID-Erzeugung und -Zuordnung
  • Beste Performance mit den Optionen -O3 -march=native

Integration und Erweiterung

  • Encode/Decode an der API-Grenze wird empfohlen
  • Für die PostgreSQL-Anbindung sollte eine C-Erweiterung geschrieben werden
  • Beim Sharding kann die v4-Façade mit xxh3, SipHash usw. gehasht werden

Sonstiges

  • Es gibt Ports in anderen Sprachen, z. B. Go (n2p5/uuid47)
  • Empfohlener Hash: xxHash ist kein PRF und birgt daher das Risiko von Informationslecks; SipHash wird empfohlen

Lizenz

  • MIT-Lizenz (Stateless Limited, 2025)

1 Kommentare

 
GN⁺ 2025-09-19
Hacker-News-Kommentare
  • Hallo, ich bin der Autor von uuidv47. Die Grundidee ist, intern UUIDv7 zu verwenden, um Datenbank-Indexierung und Sortierbarkeit zu erhalten, nach außen aber einen Wert zu zeigen, der wie UUIDv4 aussieht, damit Timing-Muster nicht an Clients offengelegt werden.
    Die Funktionsweise besteht darin, den 48-Bit-Zeitstempel per XOR mit einem aus dem SipHash-2-4-Stream abgeleiteten Maskenwert aus dem Zufallsfeld der UUID zu maskieren.
    Die Zufallsbits bleiben unverändert, die Version wird intern als 7 und extern als 4 gesetzt, und auch der RFC-Variantenwert bleibt erhalten.
    Das Mapping ist injektiv: Es hat die Struktur (ts, rand) → (encTS, rand).
    Das Dekodieren erfolgt per encTS ⊕ mask, daher ist eine perfekte Roundtrip-Transformation möglich.
    Sicherheitstechnisch ist SipHash ein PRF, daher wird der Schlüssel nicht offengelegt, selbst wenn man die extern ausgegebenen Werte sieht.
    Ist der Schlüssel falsch, ergibt sich auch ein vollständig anderer Zeitstempel.
    Mit externer Verwaltung einer Key-ID ist auch Key-Rotation möglich.
    Leistungsmäßig fällt ungefähr ein SipHash pro 10 Byte sowie einige 48-Bit-Load-/Store-Operationen an, also Overhead im Nanosekundenbereich; C11, Header-only, keine externen Abhängigkeiten und keine Allokationen nötig.
    Getestet wurden SipHash-Referenzvektoren, Roundtrip-Encode/Decode sowie Tests zur Invarianz von Version und Variante.
    Mich würde Feedback interessieren.

    • Mir gefällt die Idee.
      UUIDs werden oft clientseitig erzeugt, und das scheint bei diesem Ansatz nicht möglich zu sein.
      Wenn man vom Client erzeugte UUIDs annimmt und dann die maskierte Version zurückgibt: Entsteht dann nicht eine Schwachstelle, weil jemand zwei UUIDs mit unterschiedlichem ts, aber gleichem rand liefern könnte?
      Ich frage mich also, ob dieser Ansatz letztlich nur geeignet ist, wenn man UUIDv7 selbst direkt erzeugt.

    • Ich habe zwei Anmerkungen.

      1. Dieser Ansatz nimmt anderen die Möglichkeit, den Wert von UUIDv7 weiterzuverwenden, was aus Sicht von API-Nutzern schade ist.
      2. Wenn externe API und interne Speicherung unterschiedlich sind, muss man immer diesen Umwandlungsschritt durchlaufen, was den Betrieb etwas komplizierter macht.
        Ich bin nicht sicher, ob dieser zusätzliche Aufwand den Nutzen wert ist.
    • Meine größte Sorge gilt der Qualität der Entropie in den Zufallsbits.
      UUIDv7 legt mehr Wert auf Kollisionsvermeidung, daher liegt der Fokus eher auf Kollisionswahrscheinlichkeit als auf Unvorhersehbarkeit.
      Entsprechend schreibt der RFC für Nicht-Zufälligkeit nur ein „should“ statt eines „must“ vor, und es gibt Implementierungen mit schwachen PRNGs oder Zählern oder sogar solchen, die zusätzliche Zeitdaten in die Zufallsbits schreiben (siehe RFC9562 s6.2 & s6.9).
      Deshalb kann es riskanter sein als gedacht, rand_a und rand_b von v7 direkt als Seed für ein PRF zu verwenden, wenn die Daten von außerhalb der Vertrauensgrenze kommen.
      Selbst das neue uuidv7() in PostgreSQL 18 füllt rand_a vollständig mit dem hochpräzisen Zeitstempel, und auch das ist nach RFC zulässig.
      Betrachtet man UUIDs, die bei Massenimporten erzeugt wurden, kann man am Ende auch bei diesem v7-zu-v4-Verfahren Gruppierungen erkennen, wodurch Informationen offengelegt werden könnten.
      Für Dinge wie das Sammeln von Motordaten mag das unproblematisch sein, aber bei Identifikationsdaten, die direkt mit Personen verknüpft sind, ist Vorsicht geboten.
      Unterm Strich gilt: Solange man verlässliche Entropie nicht selbst garantiert, kann auch dieses Schema Timing-, Serien- oder Korrelationsinformationen preisgeben, daher sollte man die Quelle der v7-Implementierung unbedingt selbst prüfen.

    • Ich halte das für keine gute Idee.
      In PostgreSQL 18 verschiebt der optionale Parameter shift den Zeitstempel um das angegebene Intervall.
      https://www.postgresql.org/docs/18/functions-uuid.html

  • Vor ein paar Jahren habe ich mein eigenes Schema gebaut, bei dem ich in der DB fortlaufende numerische IDs verwendet und nach außen kurze zufällige Strings von 4 bis 20 Zeichen Länge ausgegeben habe.
    Damals habe ich eine benutzerdefinierte Instanz der Speck-Chiffrenfamilie verwendet, was ich für robust und ziemlich überzeugend hielt.
    Fertig war es zwar, aber ich habe das Projekt, in dem ich es einsetzen wollte, verschoben und es daher nicht veröffentlicht.
    Dieses oder nächstes Jahr will ich das Material offiziell veröffentlichen.
    Ich habe auch Notizen, in denen Implementierung, Vor- und Nachteile gut zusammengefasst sind; wer neugierig ist, kann sie sich ansehen.
    https://temp.chrismorgan.info/2025-09-17-tesid/

    • Ich hatte früher ebenfalls versucht, bigserial-PKIDs mit Speck zu verschleiern, aber die plattformübergreifenden Implementierungen waren dürftig und insbesondere in pgcrypto war die Unterstützung schwach.
      Deshalb habe ich mich für base58(AES_K1(id{8} || HMAC_K2(id{8})[0..7])) entschieden.
      Das Ergebnis ist mit meist rund 22 Zeichen zwar länger, lässt sich aber in fast jeder Umgebung implementieren, und die Performance ist für mich völlig ausreichend.

    • Gute Idee.
      In einem ähnlichen Kontext ist auch sqids (früher: hashids) einen Blick wert.
      https://sqids.org/

  • Ich hatte einmal etwas Ähnliches: zwei Spalten, eine öffentliche UUID und ein bigint-PK, das nicht über die API offengelegt wurde. Das war lange vor UUIDv7.
    Bei der Bequemlichkeit von UUIDs hat man dabei zwar etwas verloren, aber wenn man nur den PK sauber herausgezogen hat, konnte man verschiedene DB-Dumps leicht zusammenführen, was ein Vorteil war.
    Selbst wenn man per Hash nachschlägt, scheint man am Ende doch zwei Spalten zu brauchen, aber vielleicht verstehe ich auch die Funktionsweise des Hashings falsch.

    • Die Umwandlung ist mit einem geheimen kryptografischen Schlüssel rückgängig zu machen.
      Man kann also im Request den UUIDv4-Wert wieder in die UUIDv7 der DB zurückübersetzen.
  • Die Idee an sich ist interessant, aber ich fände es besser, wenn die Datenbank so etwas direkt unterstützen würde.
    Also dass sich UUIDv7 und „UUIDv4“ gegenseitig umwandeln lassen und man in Abfragen beide Formate explizit unterscheiden könnte.

  • Wirklich ein cooles Projekt.
    Ich habe mit dchests SipHash-Bibliothek eine Go-Implementierung gebaut.
    https://github.com/n2p5/uuid47
    Referenz: https://github.com/dchest/siphash

  • Das Projekt ist interessant, aber ich würde gern ein konkretes Beispiel sehen, wie sich die Freilegung des Zeitanteils bei UUID v7 tatsächlich auswirken kann.

    • Es kann Situationen geben, in denen die Offenlegung von Verhaltensmustern oder Sequenzen von Nutzern problematisch ist.

      • „Ex-Mann: An deiner User-ID auf der Dating-Seite sehe ich, dass du dein Konto definitiv auf Toms Party angelegt hast, oder?“
      • „Du behauptest, deine TZ sei XYZ, aber die imageID-Logs mit ihrem eindeutigen Erstellungszeitpunkt scheinen immer auf 3 Uhr morgens zu fallen, oder?“
        Für einzelne Nachrichten oder Echtzeit-Transaktionen ist das vielleicht egal, aber bei der Erstellung von Nutzerkonten oder langfristigen Daten kann jemand das zur Identitätsverfolgung missbrauchen.
    • Ich habe in einem CTF schon einmal einen Teil einer UUID per Brute Force als AES-Schlüssel angegriffen.
      Weil der Schlüssel teilweise aus einer Zeitquelle abgeleitet war, reichte es, die system time zum Zeitpunkt der Schlüsselerzeugung zu kennen.
      Ein weiteres einfaches Beispiel wäre ein File-Sharing-Dienst, der nur etwas wie webseite.com/GUID veröffentlicht und keine separaten Upload-Zeitinformationen ausgibt.
      Verwendet er UUIDv7, lässt sich daraus trotzdem die Upload-Zeit der Datei abschätzen.
      Das ist vielleicht nicht zwingend eine große Sicherheitsbedrohung, aber es ist eine unbeabsichtigte Informationsoffenlegung.

    • Man stelle sich zum Beispiel ein System vor, das medizinische Daten speichert.
      Selbst wenn nach einer MRT zur Analyse unmittelbar Ergebnisse hochgeladen und anschließend personenbezogene Daten entfernt würden,
      könnte man über den Zeitstempel von UUIDv7 externe Korrelationsanalysen durchführen und sagen: „An diesem Datum hat nur eine Person ein MRT bekommen, also kann ich herausfinden, wessen MRT das ist.“

  • Das Unangenehmste an UUIDv7 ist für mich, dass sie sich in Listen per bloßem Auge so schwer vergleichen lassen.
    Es wäre ein enormer UX-Gewinn, wenn es in psql eine Visualisierungsebene gäbe, bei der die Zufallsbits nach vorn geholt werden, während die tatsächliche Sortierung weiterhin nach dem Zeitstempel erfolgt.

    • Ich habe mir einfach angewöhnt, nur den letzten Teil der UUID anzuschauen.

    • Man kann sich einfach selbst eine Funktion schreiben und sie in Queries verwenden.
      Zum Beispiel als Hex-Darstellung mit anschließend umgekehrtem String oder als invertiertes Base64; das wäre kürzer und leichter zu unterscheiden.

  • Das wirkt insgesamt ziemlich vernünftig.
    Aber die Aufregung darüber, dass ein Zeitstempel sichtbar wird, und die Behauptung, die Offenlegung sequenzieller IDs bedeute automatisch Angriffsfläche oder Preisgabe von Geschäftsinformationen, wirken auf mich eher wie übertriebene Sorge als wie ein echtes Sicherheitsproblem.
    Man könnte einfach periodisch zu int-Werten eine große Zufallszahl addieren; dann bliebe die monotone Eigenschaft erhalten, während Außenstehende Muster schwerer erkennen könnten.
    Insgesamt scheint mir da auch etwas unnötige Dramatisierung im Spiel zu sein, als würde man eine große Informationsoffenlegung herbeireden.

    • Was hier offengelegt wird, sind nicht Geschäftsinformationen, sondern Client-Informationen.
      Die Informationen, die das System selbst preisgibt, mögen für sich genommen wenig bedeuten, aber bei Beobachtung in großer Menge oder als Zeitreihe lassen sich zusätzliche Daten erschließen.
      Ein Beispiel ist David Kriesels Vortrag SpiegelMining: Schon wenn man nur Datum und Autor von Zeitungsartikeln sammelt, kann man Muster erkennen, etwa wer wann im Urlaub ist.
      Vergleicht man Daten mehrerer Autoren, kann am Ende sogar eine interne Beziehung offenkundig werden.
  • Warum verwendet man nicht pro Sitzung einen anderen kryptografischen Schlüssel und gibt nach außen nur verschlüsselte IDs aus?
    Dann könnte die DB doch einfach schlichte sequentielle IDs verwenden.

    • Um die im Token verborgenen Zeitstempelbits zu entschlüsseln, muss man wissen, welcher Schlüssel verwendet wurde.
      Wenn man die Schlüssel periodisch wechselt, wird das Schlüsselmanagement enorm kompliziert, und man hat zusätzlich das Problem, jedes Mal den richtigen Schlüssel zu finden.
  • Warum wurde statt Version 4 nicht Version 8 verwendet?
    v4 bedeutet Zufallsbits, aber tatsächlich ist es hier gar nicht so zufällig.
    v8 hat keine Einschränkungen hinsichtlich der Bitbedeutung.

    • Ich kenne die richtige Antwort auch nicht, aber bei hoher Entropie könnte man es vielleicht ähnlich wie einen Seed-basierten PRNG betrachten.
      Der Zweck dieses Ansatzes ist ja gerade, nach außen zufällig zu wirken, daher wäre v8 womöglich sogar auffälliger gewesen.