- UUIDv47 permet de stocker en base de données un UUIDv7 triable, tout en exposant via les API externes une valeur qui ressemble à un UUIDv4
- Seul le champ d’horodatage est masqué par XOR afin de protéger l’information temporelle d’UUIDv7, tandis que les autres champs aléatoires restent inchangés
- Le masquage s’appuie sur SipHash-2-4 avec une clé de 128 bits, ce qui permet une protection sûre des informations sans risque d’exposition de la clé
- L’encode/decode est déterministe et réversible, la part aléatoire est conservée, et le risque de collision reste faible
- Les benchmarks montrent des performances très élevées et une intégration simple, avec une connexion facile à des bases comme PostgreSQL
Présentation du projet et intérêt
- UUIDv47 est une bibliothèque open source en C qui stocke en interne dans la base de données des UUIDv7 avantageux pour le tri et l’indexation, tout en exposant aux API et systèmes externes des valeurs qui ressemblent à des UUIDv4, afin de concilier protection de la vie privée et traitement hautes performances
- Par rapport à d’autres algorithmes de conversion d’UUID, elle se distingue par des atouts spécifiques : mapping réversible, compatibilité RFC, sécurité avec impossibilité de retrouver la clé, zero-deps et structure simple reposant sur un unique fichier d’en-tête
Principales caractéristiques
- Header-only C (C89), intégration simple sans dépendance externe
- Seul le champ d’horodatage d’UUIDv7 est masqué par XOR afin d’empêcher l’exposition de l’information temporelle, tandis que les autres champs aléatoires ne sont pas modifiés
- Le masquage via SipHash-2-4 avec clé permet de protéger les informations de façon sûre grâce à une clé de 128 bits
- Le processus d’encode/decode est déterministe et entièrement réversible (restauration exacte de l’original)
- Prise en charge d’un mapping rapide entre des UUID destinés au stockage en base (v7) et des UUID destinés à l’exposition externe (v4)
- Fournit de nombreux exemples, notamment du code de test et des outils de benchmark
Cas d’usage et avantages
- Permet d’utiliser des UUIDv7 triables pour maximiser la localité des index et l’efficacité de la pagination dans la base
- À l’extérieur, seul un motif ressemblant à un UUIDv4 est exposé, ce qui évite la fuite de l’horodatage et le pistage
- L’usage de SipHash empêche la récupération de la clé et garantit la sécurité de la clé secrète
- Gestion des bits de version/variant compatible RFC
- L’exécution est rapide, ce qui le rend efficace dans les environnements de traitement en temps réel et de génération à grande échelle
Structure principale et fonctionnement interne
UUIDv7 Layout
- ts_ms_be : horodatage big-endian sur 48 bits
- ver : nibble de poids fort du 6e octet (0x7=DB, 0x4=externe)
- rand_a : valeur aléatoire sur 12 bits
- var : variant RFC (0b10)
- rand_b : valeur aléatoire sur 62 bits
Logique de masquage et de mapping (Façade mapping)
- Encodage : ts48 XOR mask48(R), version=4
- Décodage : encTS XOR mask48(R), version=7
- Les champs aléatoires ne sont pas modifiés
- Le champ aléatoire de 10 octets est utilisé comme entrée de SipHash
- Le masquage XOR est immédiatement inversable si l’on connaît la clé
Modèle de sécurité
- Objectif : éviter toute exposition de la clé même si l’attaquant peut choisir les entrées
- Implémentation : utilisation de SipHash-2-4, une fonction pseudo-aléatoire à clé (PRF)
- Utilisation d’une clé de 128 bits, avec recommandation de dériver les clés via HKDF, etc.
- En cas de rotation de clé, il est recommandé de ne pas stocker la clé dans l’UUID lui-même, mais de conserver séparément un petit identifiant de clé
API publique (C)
- uuidv47_encode_v4facade : conversion v7→v4
- uuidv47_decode_v4facade : restauration v4→v7
- Autres fonctions fournies pour le réglage de version, le parsing et le formatage
Performances et benchmarks
- Pour l’opération de masquage SipHash (10B), le coût est inférieur à 14ns/op, et un round trip complet encode+decode atteint environ 33ns/op (sur Apple M1)
- Garantit un traitement rapide même lors de la génération et du mapping de grands volumes d’UUID
- Performances optimales avec les options
-O3 -march=native
Intégration et extension
- Il est recommandé d’effectuer l’encode/decode à la frontière de l’API
- En cas d’intégration avec PostgreSQL, écrire une extension C
- En sharding, il est possible de hasher la façade v4 avec xxh3, SipHash, etc.
Divers
- Des ports dans d’autres langages sont aussi proposés, comme Go (
n2p5/uuid47)
- Hachage recommandé : xxHash n’étant pas une PRF, il présente un risque de fuite d’information ; l’usage de SipHash est recommandé
Licence
- Licence MIT (Stateless Limited, 2025)
1 commentaires
Commentaire Hacker News
Bonjour, je suis l’auteur de uuidv47. L’idée de base est d’utiliser UUIDv7 en interne pour bénéficier de l’indexation et de l’ordonnancement en base de données, tout en exposant en externe une valeur qui ressemble à un UUIDv4 afin de ne pas révéler aux clients des motifs temporels.
Le fonctionnement consiste à masquer par XOR le timestamp 48 bits avec un flux SipHash-2-4 dérivé du champ aléatoire de l’UUID.
Les bits aléatoires sont conservés tels quels, la version passe de 7 en interne à 4 en externe, et la valeur de variante RFC est également préservée.
Le mapping est injectif : structure (ts, rand) → (encTS, rand).
Le décodage se fait via encTS ⊕ mask, ce qui permet une transformation aller-retour parfaite.
Du point de vue sécurité, comme SipHash est une PRF, voir la valeur encapsulée depuis l’extérieur ne révèle pas la clé.
Si la clé est erronée, le timestamp obtenu sera lui aussi complètement différent.
La rotation de clé peut aussi être prise en charge via une gestion externe des key IDs.
Côté performances, on parle d’un SipHash pour 10 octets, de quelques load/store 48 bits, donc d’un surcoût de l’ordre de la nanoseconde ; c’est du C11 header-only, sans dépendance externe, et sans allocation.
Les tests couvrent les vecteurs de référence SipHash, l’encodage/décodage aller-retour, ainsi que l’invariance de version et de variante.
Je suis curieux d’avoir vos retours.
J’aime bien cette idée.
Les UUID sont souvent générés côté client, et ça ne semble pas possible avec cette approche.
Si vous acceptez des UUID générés par le client puis renvoyez une version masquée, est-ce qu’il n’y aurait pas une vulnérabilité du fait que quelqu’un pourrait fournir deux UUID avec des
tsdifférents mais le mêmerand?En somme, je me demande si cette approche n’est adaptée qu’au cas où l’on génère soi-même les UUIDv7.
J’ai deux remarques.
Je ne sais pas si ce surcroît de complexité vaut vraiment le coup.
Ma principale inquiétude concerne la qualité d’entropie des bits aléatoires.
UUIDv7 vise davantage l’évitement des collisions que l’imprévisibilité.
Ainsi, la RFC n’impose pas strictement le caractère aléatoire (
must) mais le recommande seulement (should) pour la non-prédictibilité, et certaines implémentations utilisent un PRNG faible ou un compteur, voire ajoutent des données d’horloge supplémentaires à la place des bits aléatoires (voir : RFC9562 s6.2 & s6.9).Du coup, utiliser directement
rand_aetrand_bde v7 comme seed de PRF peut être plus risqué qu’il n’y paraît si les données viennent d’en dehors de la frontière de confiance.Même le nouveau
uuidv7()de PostgreSQL 18 remplit entièrementrand_aà partir d’un timestamp haute précision, ce qui reste conforme à la RFC.Si l’on regarde des UUID générés lors d’un import massif, ce schéma v7-to-v4 risque donc lui aussi de rester groupable, avec fuite d’information à la clé.
Pour de la télémétrie de pièces moteur, ce n’est sans doute pas gênant, mais pour des identifiants liés directement à des personnes, il faut être prudent.
En pratique, tant qu’on ne garantit pas soi-même une entropie fiable, ce schéma peut aussi laisser fuiter des informations de timing, de sérialisation ou de corrélation ; il faut donc impérativement auditer le code source de l’implémentation v7 utilisée.
Je pense que ce n’est pas une bonne idée.
Dans PostgreSQL 18, le paramètre optionnel
shiftdécale le timestamp de l’intervalle fourni.https://www.postgresql.org/docs/18/functions-uuid.html
Il y a quelques années, j’avais conçu mon propre schéma : en base, j’utilisais des identifiants numériques séquentiels, et en externe j’exposais de courtes chaînes aléatoires de 4 à 20 caractères.
Pour cela, j’avais utilisé une instance personnalisée de la famille de chiffrements Speck, que je trouvais robuste et assez élégante.
J’ai terminé le travail, mais comme j’ai repoussé le projet où je comptais l’utiliser, je ne l’ai jamais publié.
Je prévois de rendre cela public officiellement cette année ou l’an prochain.
J’ai aussi des notes bien structurées sur l’implémentation, ses avantages et ses inconvénients, si ça intéresse quelqu’un.
https://temp.chrismorgan.info/2025-09-17-tesid/
J’avais moi aussi essayé il y a quelque temps d’obfusquer un
bigserialPKID avec Speck, mais le manque d’implémentations cross-platform, et surtout le faible support danspgcrypto, m’ont fait choisirbase58(AES_K1(id{8} || HMAC_K2(id{8})[0..7])).Le résultat est plus long, en général autour de 22 caractères, mais ça reste implémentable presque partout et les performances sont tout à fait satisfaisantes.
C’est une bonne idée.
Dans un registre proche,
sqids(anciennementhashids) peut aussi valoir le détour.https://sqids.org/
J’ai déjà rencontré une situation similaire : on utilisait deux colonnes, un UUID public et un
bigintPK non exposé via l’API (c’était bien avant UUIDv7).C’était un peu moins pratique qu’un UUID unique, mais si l’on extrayait correctement les PK, on pouvait fusionner facilement des dumps provenant de différentes bases, ce qui était un vrai avantage.
Même en faisant les recherches via un hash, j’ai l’impression qu’il faudrait quand même deux colonnes, mais il est possible que je comprenne mal le fonctionnement du hash.
On peut convertir la valeur UUIDv4 reçue dans la requête vers le UUIDv7 de la base.
L’idée est intéressante en soi, mais j’aimerais que la base de données prenne directement ce genre de traitement en charge.
Autrement dit, qu’elle puisse convertir UUIDv7 en “UUIDv4” et inversement, et qu’on puisse distinguer explicitement les deux formats dans les requêtes.
Projet vraiment très cool.
J’ai réalisé une implémentation Go en m’appuyant sur la bibliothèque siphash de dchest.
https://github.com/n2p5/uuid47
Référence : https://github.com/dchest/siphash
Le projet est intéressant, mais j’aimerais voir un exemple concret du risque de fuite lié à l’exposition de la partie temporelle dans UUID v7.
Cela peut exposer des situations où il est problématique de révéler des habitudes ou des séquences de comportement utilisateur.
imageID(uniques au moment de création) tombent toujours vers 3 h du matin, non ? »Pour des messages individuels ou des transactions temps réel, ce n’est peut-être pas important, mais pour la création de comptes utilisateurs ou des données de long terme, cela peut être exploité pour recouper l’identité de quelqu’un.
J’ai déjà brute-force une partie d’un UUID pour en faire une clé AES dans un CTF.
Comme la clé dérivait en partie d’une source temporelle, il suffisait de retrouver l’heure système au moment de sa génération pour rendre l’attaque possible.
Un autre exemple simple serait un service de partage de fichiers qui n’expose que des URL du type
siteweb.com/GUID, sans publier séparément l’heure d’upload.Avec UUIDv7, on peut quand même estimer l’heure d’envoi du fichier à partir de l’identifiant lui-même.
Ce n’est pas forcément une menace de sécurité majeure, mais c’est bien une fuite d’information non intentionnelle.
Imaginez par exemple un système stockant des données médicales.
Même si l’on supprime les informations personnelles après avoir uploadé les résultats juste après une IRM pour les besoins d’analyse,
le timestamp de UUIDv7 permettrait une analyse de corrélation externe : « une seule personne a passé une IRM à cette date, donc on peut savoir de qui il s’agit ».
Le point le plus gênant avec UUIDv7, c’est qu’il est très difficile à comparer visuellement (
diff) dans des listes.S’il existait dans
psqlune couche de visualisation qui mettrait les bits aléatoires au début tout en conservant le tri réel sur le timestamp, l’UX s’en trouverait énormément améliorée.Moi, j’ai simplement pris l’habitude de ne regarder que la fin de l’UUID.
Il suffit d’écrire une fonction dédiée et de l’utiliser dans les requêtes.
Par exemple, après représentation hexadécimale, on peut inverser la chaîne, ou encore l’afficher en base64 inversé, ce qui la rendrait plus courte et plus facile à distinguer.
Cette approche me paraît plutôt solide.
En revanche, je trouve qu’on dramatise un peu trop l’exposition du timestamp, et l’idée selon laquelle exposer des identifiants séquentiels entraînerait automatiquement une surface d’attaque ou une fuite d’informations métier me semble relever davantage d’une inquiétude exagérée que d’un véritable problème de sécurité.
On pourrait simplement ajouter périodiquement une grande valeur aléatoire à un entier, ce qui préserverait son caractère monotone tout en rendant les motifs plus difficiles à percevoir depuis l’extérieur.
À mes yeux, il y a aussi une tendance à surjouer le risque sous couvert d’éviter des fuites d’informations sensibles.
Les données que le système laisse échapper peuvent sembler sans importance prises isolément, mais observées à grande échelle ou dans le temps, elles permettent d’inférer d’autres informations.
Un bon exemple est la conférence SpiegelMining de David Kriesel : rien qu’en récupérant les dates de publication des articles et leurs auteurs, on peut déduire les périodes de congés de chacun.
En comparant plusieurs auteurs, on peut même découvrir des relations internes, comme des romances au bureau.
Pourquoi ne pas utiliser une clé de chiffrement différente par session et n’exposer à l’extérieur que l’identifiant chiffré ?
Dans ce cas, la base pourrait simplement utiliser un identifiant séquentiel classique, non ?
Si l’on change régulièrement de clé, la gestion des clés devient très complexe, et il faut encore résoudre le problème de savoir comment retrouver la bonne à chaque fois.
Pourquoi ne pas avoir utilisé la version 8 plutôt que la version 4 ?
v4 signifie que les bits sont aléatoires, alors qu’en réalité ils ne le sont pas tant que ça.
v8, au contraire, n’impose aucune contrainte sur la signification des bits.
Comme l’objectif de cette approche est précisément d’avoir quelque chose qui semble aléatoire de l’extérieur, j’imagine que v8 aurait peut-être davantage attiré l’attention.