TypeID - identifiants globaux uniques, sûrs par type et triables par K, inspirés de Stripe ID
(github.com/jetpack-io)- TypeID est un format d’identifiant globalement unique, sûr par type, qui étend UUIDv7 et s’inspire de l’usage des préfixes dans l’API Stripe
- La forme canonique en chaîne est en minuscules et se compose de trois parties : un préfixe de type ASCII en snake_case minuscules de 63 caractères maximum, un séparateur
_, et un suffixe UUIDv7 de 26 caractères encodé en base32 modifiée - Le préfixe de type évite d’utiliser par erreur un ID
userlà où un IDpostest attendu, et permet d’identifier immédiatement à quel type d’entité renvoie l’identifiant lors du débogage - TypeID est un sur-ensemble de UUID : en supprimant l’information de type puis en le décodant, on obtient un UUIDv7 valide
- Il est triable par K, ce qui permet de l’utiliser comme clé primaire de base de données, avec pour objectif de réduire les problèmes de locality en base par rapport à UUIDv4, qui est un ID global entièrement aléatoire
- L’encodage base32 offre des propriétés de sûreté pour les URL, d’insensibilité à la casse, d’évitement des caractères ambigus, de sélection facile au double-clic, ainsi qu’une longueur plus courte que l’encodage hexadécimal traditionnel des UUID
- Des implémentations officielles sont disponibles en Go, SQL et TypeScript, et la version la plus récente de la spec est v0.3.0
- L’outil en ligne de commande permet de générer des TypeID, de décoder l’UUID d’un TypeID existant et d’encoder un UUID en TypeID
typeid new prefixpour générer un nouveau TypeIDtypeid decode prefix_01h2xcejqtf2nbrexx3vqjhp41pour afficher le type et l’UUIDtypeid encode prefix 0188bac7-4afa-78aa-bc3b-bd1eef28d881pour générer un TypeID
- Le TypeID Converter de Jetify permet de convertir une chaîne TypeID en UUID, ou de convertir un
prefix:UUIDau format TypeID
1 commentaires
Commentaires sur Hacker News
Quelques suggestions : il vaudrait mieux figer et documenter la chaîne de préfixe avant qu’il ne soit trop tard
En voyant l’implémentation Go, les ASCII minuscules semblent convenir, mais cela devient ambigu pour des types composés comme
article-commentDans les projets complexes ou avec un ORM, il est difficile d’éviter les séparateurs, donc autoriser un seul séparateur pourrait valoir le coup d’être envisagé
L’implémentation Go n’a pas de tests, alors que le code est facile à tester unitairement, donc il faut absolument en ajouter
En Go, il serait plus approprié d’utiliser une vraie fonction de parsing et un tableau d’octets interne, comme dans l’implémentation UUID de Google, et de réserver les chaînes au rendu et au préfixe
Le parsing actuel est trop permissif et semble passer en mode génération si le suffixe est vide ; avec l’indexation après
SplitN, il pourrait aussi paniquer s’il n’y a pas de soulignementJ’essayais de trouver des défauts au design, mais au final il semble avoir trouvé le bon sweet spot des compromis
Cela dit, la remarque est juste, et à mesure que les implémentations dans différents langages se multiplient, il faudra vérifier leur interopérabilité, donc nous prévoyons d’ajouter une suite de tests plus stricte
Concernant le préfixe, je me demande si l’inquiétude vient du fait que l’ensemble de caractères autorisés n’est pas défini dans la spécification
Et nous avons aussi clarifié le préfixe dans la spécification
Le préfixe est défini selon le domaine d’usage
J’utilise une approche de ce genre depuis plusieurs années, avec deux différences
D’abord, je pars du principe que les gens ne saisissent pas réellement ces valeurs à la main, donc la confusion entre
let1ne m’inquiète pas beaucoupÀ la place, j’utilise du base32 sans les voyelles
eioupour réduire le risque de former accidentellement des mots, en particulier des grossièretésEnsuite, j’ajoute deux caractères base32 salés comme checksum
Cela évite d’interroger le datastore quand une valeur est invalide par erreur ou de façon malveillante, et je ne comprends pas pourquoi les autres implémentations ne font pas ça
analrita, et j’ai reçu une plainte à ce sujetOn pourrait envisager d’ajouter aussi
aaux caractères exclusSi l’on considère que les humains ne les saisissent pas à la main, je me demande par quel chemin une mauvaise valeur pourrait être fournie par erreur
Il peut y avoir des erreurs de copier-coller ou une page qui affiche tout en majuscules, mais avec du base32 on peut normaliser la casse
Et je me demande aussi en quoi un checksum de 2 octets, non sûr cryptographiquement, aide en cas d’usage malveillant
Nous réfléchissons à son éventuelle inclusion dans la spécification TypeID
Je ne sais pas à quel point cela réduit les accès à la base de données, mais c’est appréciable de ne pas tomber sur des erreurs plus ambiguës pendant le décodage
Indépendamment du corps du texte, il y a aussi un lien vers « l’alphabet de Crockford » : https://www.crockford.com/base32.html
Ce système de base32 exclut
IetL, qui se confondent avec1,O, qui se confond avec0, ainsi queULa page explique que
Ua été retiré pour éviter une « obscénité accidentelle », mais je ne vois pas bien ce que cela veut direPour fabriquer un alphabet base32 correct avec des caractères alphanumériques non ambigus, il suffit d’enlever
O,IetLIl en reste quand même 33, donc il faut en retirer un de plus, et comme peu importe lequel, il a simplement attribué une justification arbitraire au dernier caractère supprimé
Pour un identifiant visible par l’utilisateur, ce n’est pas une raison totalement absurde, mais bien sûr cela ne constitue en rien une prévention parfaite
IetOpuis qu’on retire aussiU, on élimine pas mal de combinaisons de trois ou quatre lettres qui peuvent paraître grossièresBien sûr, comme
Areste présent, certaines combinaisons restent possibles, mais la probabilité est très faibleUn’existait pas à l’époque de Cicéron, donc c’est une lettre vulgaire au point de frôler l’obscénitéDans mon entreprise, nous avons défini plusieurs nouvelles « bases » pour les QR codes, et à titre personnel je trouve que c’est un domaine plutôt sous-exploité en informatique
Nous étions en train de résoudre un problème de slug d’URL, et comme la longueur était assez importante, nous pensions que 5 bits par octet permettraient de réduire l’inconfort de recopie manuelle
Nous avions finalement la marge pour retirer quelques caractères de plus, et après le départ de tout le monde nous nous sommes retrouvés tous les deux à classer les grossièretés par degré d’offense afin de choisir quoi supprimer
Ensuite, on m’a convaincu que les insultes à caractère discriminatoire posaient un problème plus sérieux, donc nous nous sommes concentrés sur la suppression de
nplutôt que deutggrétait juste mignon, maisn**rsignifiait devoir avoir des conversations gênantes avec plusieurs équipes RHL’ensemble final est un peu flou dans ma mémoire maintenant, mais il manipulait en gros
[a-z][0-9], avec beaucoup de symboles inutilisables dans une URL ou difficiles à dicterSi je me souviens bien, nous avions supprimé
0,let1, et nous partions du principe que la recopie se ferait soit tout en majuscules, soit tout en minuscules0on’était donc pas un problème, pas plus que1LJe n’aime pas beaucoup l’encodage Crockford
En pratique, quand il a fallu faire du support technique ou de l’analyse sur des valeurs encodées ainsi, j’ai eu l’impression que c’était clairement une erreur
Cet alphabet a été conçu pour un objectif en réalité rare, comme lire un identifiant au téléphone, et il introduit de l’ambiguïté
Dès qu’on veut
grepdes logs ou faire des recoupements à partir de la chaîne encodée elle-même, c’est catastrophique, et le fait d’autoriser les tirets devient une source majeure d’erreurs de copier-coller et de retour à la ligneOn tape rarement des identifiants d’objet à la main, mais on les copie/colle souvent entre applications, chats et forums, on les transmet par e-mail et on les recherche dans des fichiers de logs
Dans ces conditions, la prononçabilité ne sert à rien, l’insensibilité à la casse devient une gêne, et ce qu’il faut, c’est de la cohérence ainsi qu’une bonne résistance au collage et aux retours à la ligne
Le base58 est un encodage bijectif mieux adapté à ces exigences, et en plus il est plus court
Inspirés par Stripe, nous utilisons depuis plusieurs années des UUID encodés en base58 avec un préfixe de type comme identifiants d’objet, par exemple
user_1BzGURpnHGn6oNru84B3RiCela dit, il faut garder à l’esprit que l’encodage base32 de Douglas Crockford a été conçu il y a vingt ans, à une époque où le contexte d’usage était assez différent
Mais je ne voulais pas partir du principe que la casse serait distinguée, donc j’ai fini par pencher vers le base32
Par exemple, on peut vouloir utiliser ces ID comme noms de fichiers, ou être dans un environnement contraint par un système de fichiers insensible à la casse
TypeID utilise simplement l’alphabet de Crockford toujours en minuscules, pas l’ensemble complet des règles d’encodage Crockford
Les tirets ne sont pas autorisés dans TypeID, et il n’est pas permis non plus d’encoder le même ID de plusieurs façons en remplaçant des caractères ambigus par d’autres
Mais il arrive parfois qu’on doive les saisir à la main à partir d’une capture d’écran, d’un partage d’écran ou d’un autre appareil depuis lequel il n’est pas facile de récupérer l’identifiant
Le base32 de Crockford fonctionne aussi, mais sans vraiment me convaincre
Personnellement, mon premier choix serait d’utiliser un KSUID avec préfixe de type
On obtient alors un ID K-triable de 160 bits encodé en base62, ce qui convient bien tant qu’on n’a pas absolument besoin, pour des raisons de compatibilité, d’un ID sur 128 bits
Mon encodage base32 préféré est z-base-32, que je trouve plus agréable à l’œil : https://philzimmermann.com/docs/human-oriented-base-32-encod...
Le plus gros problème du base58, c’est qu’il convient bien aux entiers mais moins bien aux données binaires arbitraires comme les clés cryptographiques, et que les chaînes sensibles à la casse ne sont pas très esthétiques à regarder
C’est propre
J’aime bien le préfixe « type-safe »
Dans notre ORM, on avait quelque chose de similaire où l’on ajoutait automatiquement des tags par entité aux ID entiers dans la base de données ; on appelait ça les tagged ids : https://joist-orm.io/docs/advanced/tagged-ids
On utilisait
:comme séparateur, mais je regrette un peu de ne pas avoir choisi_pour faciliter le copier-coller au double-clicEn théorie, il serait aussi très facile de faire en sorte que Joist transforme une « colonne UUID de base de données » en « TypeID du modèle de domaine », mais cela ne semble pas faisable aujourd’hui par simple configuration côté utilisateur
Cela reste quand même une bonne idée
Les ID des éléments ressemblent à
t3_15bfi0, oùt3_est le préfixe de typet3correspond à une publication,t1à un commentaire,t5à un subreddit, et le reste est un encodage en base36 de la clé primaire auto-incrémentée.)Les deux-points étaient encodés en
%dans les URL, ce qui allongeait les ID et rendait les URL laides et interminables, ce qui était extrêmement agaçantUUIDv7 est populaire sur HN depuis des années, et je me demande quand cela deviendra un vrai standard, et quand les bibliothèques, les bases de données et le reste de l’écosystème le prendront en charge nativement
La plupart des logiciels ne se soucient pas de bits particuliers à l’intérieur d’un UUID, donc on peut déjà l’utiliser aujourd’hui
S’il existe des logiciels qui dépendent de certains bits, il suffit d’imiter un UUIDv4, et ces bits peuvent eux aussi être générés aléatoirement
Si une procédure de génération est nécessaire, on peut l’écrire soi-même sans difficulté
Par exemple, je maintiens la bibliothèque UUID pour Dart, et la dernière bêta de la version majeure inclut v6, v7 et un v8 personnalisé
Il existe aussi quelque part une liste des implémentations, et je figure sur cette page dans la liste des mainteneurs de bibliothèques, ce qui fait que je reçois une notification des auteurs à chaque nouveau brouillon
J’aime bien la partie sur le fait de « pouvoir copier-coller avec un double-clic »
Les détails comptent
J’ai quelques griefs avec les UUID
Je ne comprends pas pourquoi il faut passer par tout le rituel du versionnage des UUID au lieu de simplement combiner temps + aléatoire
Si la localité n’a pas d’importance, on peut simplement utiliser 128 bits aléatoires
D’après mon expérience, la plupart des gens semblent penser qu’un UUID doit être stocké dans une représentation hexadécimale lisible par l’humain, parfois même avec les tirets
C’est un énorme gaspillage d’espace dans les bases de données, sur le réseau et en mémoire
Par exemple, ULID https://github.com/ulid/spec est plus court, stocke le temps et se trie lexicographiquement
En réalité, comme pour les autres problèmes d’ingénierie, il suffit d’écrire les exigences et de voir ce qui y répond
Lire les avantages de plusieurs formats est utile, car cela permet de profiter de choses que d’autres ont apprises à la dure
La triabilité et la localité basée sur le temps ne sont pas forcément des propriétés auxquelles on pense spontanément, mais si vous en avez besoin, mieux vaut le savoir à l’avance que découvrir quatre ans plus tard que vous avez saboté vos données
Certains formats UUID ont aussi introduit eux-mêmes de petits problèmes de sécurité ; il vaut mieux éviter par exemple la fuite d’adresse MAC avec UUID v1
Si une solution existante correspond à votre cas d’usage, utilisez-la simplement
Sinon, inutile de trop s’inquiéter
Personnellement, là où j’utilise le plus des UUID, j’ai simplement défini : « envoyez juste une chaîne unique ; si ça vous rassure, utilisez la bibliothèque UUID que vous voulez »
Dans ce système, il semble y avoir un format différent selon la source des données, mais ce n’est pas grave
Le volume est de quelques dizaines de milliers par jour, donc il n’y a pas de problème d’échelle, et il n’est pas nécessaire de trier par UUID
Ce n’est pas vraiment un identifiant au sens strict, mais plutôt un jeton unique créé par l’émetteur pour chaque message, utilisé pour détecter les doublons en aval dans un système hétérogène avec plusieurs files
Il n’y a même pas besoin d’une unicité globale ; il suffit qu’il soit unique à l’intérieur d’un petit shard, et en principe ce ne serait même pas grave s’il se répétait avec le temps
Mais par souci de simplicité, nous maintenons quand même le système de façon à garantir l’unicité sur toute sa durée de vie
Quand je les génère moi-même, je lis des données depuis
/dev/urandom, puis je les encode en base64, en choisissant une taille qui ne se termine pas par==Là encore, ce n’est pas à cause d’un vrai problème, mais pour des raisons esthétiques
Cela dit, je ne comprends toujours pas pourquoi les tirets sont nécessaires, et je trouve que les autres versions n’apportent pas grand-chose
Si vous n’avez pas absolument besoin de UUIDv7 ou même d’un UUID, https://github.com/segmentio/ksuid offre un espace de clés bien plus grand
Si vous avez besoin d’un namespace, il suffit d’ajouter un préfixe de chaîne, et la probabilité de collision de KSUID est bien plus faible que celle de n’importe quelle version d’UUID
Parmi les générateurs d’ID génériques avec horodatage triable, ksuid était selon moi le meilleur, et il existe des bibliothèques dans la plupart des langages
Les UUID v1 à v7 sont très gaspilleurs
Si vous pouvez vous permettre les bits supplémentaires, ce qui est sans doute le cas dans la plupart des situations, KSUID est un excellent format
[1] https://github.com/sophiabits/resource-id
Cela dit, le gros problème de ksuid, c’est qu’avec 160 bits il ne rentre pas dans le type UUID natif des bases de données, par exemple dans PostgreSQL, ce qui a un coût en performances
1: https://github.com/svix/rust-ksuid
2: https://github.com/svix/python-ksuid
K-sortable est un excellent concept, et des clés faiblement ordonnées résolvent de nombreux cas d’usage
J’aime aussi la représentation sous forme de chaîne compacte avec typage
En revanche, je m’inquiète des nombreux problèmes de sécurité que pourrait entraîner comme effet de bord involontaire UUID v7
Il ne faut pas utiliser les UUID comme jetons, ni comme clés primaires de base de données, mais en pratique les gens le font
UUID v4 est en pratique un nombre aléatoire cryptographique, donc jusqu’ici beaucoup de failles de sécurité ont sans doute été évitées par hasard
Pour UUID v7, je croyais que la quantité réelle de données aléatoires retombait à 32 bits, et je pensais qu’il fallait vraiment mieux former les développeurs au fait qu’un UUID peut être devinable
Correction : il semble que j’aie mal compris la question de la prédictibilité des UUID v7
Le brouillon recommande un CSPRNG pour les bits aléatoires, indique une entropie minimale de 74 bits et précise qu’ils sont conçus pour être « imprévisibles »
Il recommande bien d’utiliser « UUID v4 » pour les usages liés à la sécurité, mais c’est peut-être en raison de la sémantique liée à l’horodatage
Si les noms sont UUIDv4 et UUIDv7, cela ne va-t-il pas créer une confusion permanente, où il faut toujours se rappeler lequel est bon pour la base de données et lequel est bon pour un jeton à usage unique ?
Je ne vois pas non plus vraiment de solution rétrocompatible
En Elixir, on utilise la fonction
UUID.uuid4()pour créer un UUID v4En théorie, on pourrait analyser le code pour repérer ces usages, mais tout cela augmente les risques d’erreur
Même chose pour les versions basées sur un hachage comme v3
v4 est simple et se prête moins aux erreurs