2 points par GN⁺ 2023-06-29 | 1 commentaires | Partager sur WhatsApp
  • 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 user là où un ID post est 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 prefix pour générer un nouveau TypeID
    • typeid decode prefix_01h2xcejqtf2nbrexx3vqjhp41 pour afficher le type et l’UUID
    • typeid encode prefix 0188bac7-4afa-78aa-bc3b-bd1eef28d881 pour générer un TypeID
  • Le TypeID Converter de Jetify permet de convertir une chaîne TypeID en UUID, ou de convertir un prefix:UUID au format TypeID

1 commentaires

 
GN⁺ 2023-06-29
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-comment
    Dans 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 soulignement
    J’essayais de trouver des défauts au design, mais au final il semble avoir trouvé le bon sweet spot des compromis

    • Il y a bien des tests sur la partie la plus complexe, comme l’encodage base32 : https://github.com/jetpack-io/typeid-go/blob/main/base32/bas...
      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
    • Comme le README demande d’autres implémentations, avoir une suite de tests serait aussi utile pour le code tiers
    • Nous avons ensuite implémenté une batterie de tests assez complète : https://github.com/jetpack-io/typeid-go/blob/main/typeid_tes...
      Et nous avons aussi clarifié le préfixe dans la spécification
    • Je vais essayer d’écrire des tests pour ça
    • Je pense qu’il y a eu un malentendu : le préfixe n’est pas quelque chose de figé
      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 l et 1 ne m’inquiète pas beaucoup
    À la place, j’utilise du base32 sans les voyelles eiou pour réduire le risque de former accidentellement des mots, en particulier des grossièretés
    Ensuite, 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

    • Il y a quelques années, un mot de passe auto-généré avait donné analrita, et j’ai reçu une plainte à ce sujet
      On pourrait envisager d’ajouter aussi a aux caractères exclus
    • Je suis d’accord pour ajouter un checksum, mais la partie « par erreur ou de façon malveillante » m’interpelle
      Si 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
    • L’idée du checksum est intéressante
      Nous réfléchissons à son éventuelle inclusion dans la spécification TypeID
    • Il y a quelques mois, j’ai implémenté la seconde approche dans le cadre du schéma d’encodage
      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 I et L, qui se confondent avec 1, O, qui se confond avec 0, ainsi que U
    La page explique que U a été retiré pour éviter une « obscénité accidentelle », mais je ne vois pas bien ce que cela veut dire

    • C’est une plaisanterie de Crockford
      Pour fabriquer un alphabet base32 correct avec des caractères alphanumériques non ambigus, il suffit d’enlever O, I et L
      Il 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
    • Si on exclut déjà I et O puis qu’on retire aussi U, on élimine pas mal de combinaisons de trois ou quatre lettres qui peuvent paraître grossières
      Bien sûr, comme A reste présent, certaines combinaisons restent possibles, mais la probabilité est très faible
    • Pour un véritable latiniste, U n’existait pas à l’époque de Cicéron, donc c’est une lettre vulgaire au point de frôler l’obscénité
    • Il y a aussi ce genre de choses : le base58 façon Satoshi/Bitcoin https://en.wikipedia.org/wiki/Binary-to-text_encoding#Base58, le « base62 » de saltpack chez Keybase https://github.com/keybase/saltpack, le célèbre « Adobe 85 » https://en.wikipedia.org/wiki/Ascii85, et basE91 https://base91.sourceforge.net
      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
    • J’avais imaginé avec un collègue un ensemble de caractères presque identique environ quatre ans avant Crockford
      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 n plutôt que de u
      tggr était juste mignon, mais n**r signifiait devoir avoir des conversations gênantes avec plusieurs équipes RH
      L’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 à dicter
      Si je me souviens bien, nous avions supprimé 0, l et 1, et nous partions du principe que la recopie se ferait soit tout en majuscules, soit tout en minuscules
      0o n’était donc pas un problème, pas plus que 1L
  • Je 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 grep des 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 ligne
    On 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_1BzGURpnHGn6oNru84B3Ri
    Cela 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

    • J’ai aussi réfléchi au base58 et au base64url, et j’aimais bien l’idée d’un encodage plus court
      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
    • Je suis d’accord pour dire qu’on lit rarement des identifiants au téléphone
      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 base58 ou le base62 utilisé par KSUID présentent beaucoup d’avantages
      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
    • Je ne vois pas où sont les tirets dans le Base32 de Crockford : https://en.wikipedia.org/wiki/Base32#Crockford's_Base32
      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-clic
    En 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

    • Reddit fait quelque chose de similaire, mais optimisé pour la longueur des chaînes
      Les ID des éléments ressemblent à t3_15bfi0, où t3_ est le préfixe de type
      t3 correspond à une publication, t1 à un commentaire, t5 à un subreddit, et le reste est un encodage en base36 de la clé primaire auto-incrémentée
    • Dans notre entreprise, notre système interne d’ID typés utilisait au départ les deux-points comme séparateur, mais nous sommes vite passés au point (.)
      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çant
  • UUIDv7 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

    • Je ne vois pas trop quel type de support on peut attendre
      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é
    • Cela semble être dans la phase finale de standardisation à l’IETF : https://datatracker.ietf.org/doc/draft-ietf-uuidrev-rfc4122b...
    • Il y a déjà eu plusieurs brouillons et améliorations, c’est très proche de la standardisation, et beaucoup de bibliothèques le prennent déjà en charge ou ajoutent cette fonctionnalité
      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

    • Beaucoup de gens se sont dit la même chose
      Par exemple, ULID https://github.com/ulid/spec est plus court, stocke le temps et se trie lexicographiquement
    • Du côté des UUID, on a tendance à parler comme si un seul cas d’usage représentait tout, et qu’il existait donc un format UUID idéal et unique
      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
    • Combiner le temps et l’aléatoire ne garantit pas l’unicité globale
    • À titre personnel, le seul standard UUID que je trouve vraiment utile est le v4, qui n’est qu’un aléatoire simple
      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
    • Le pire avec les tirets, c’est qu’ils empêchent de sélectionner facilement l’ensemble avec un double-clic
  • 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

    • J’ai publié un petit package JavaScript léger qui génère des KSUID avec préfixe de type
      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
    • La raison du passage à ULID est qu’il est toujours en minuscules, donc on peut le traiter sans sensibilité à la casse
    • Je maintiens plusieurs bibliothèques ksuid populaires et je les utilise en production, donc j’aime ksuid
      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

    • Il y a un problème d’utilisabilité intéressant
      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 v4
      En théorie, on pourrait analyser le code pour repérer ces usages, mais tout cela augmente les risques d’erreur
    • Je vois quelques cas où cela pourrait être utile, mais par le passé, chaque fois que j’ai croisé des UUID partiellement séquentiels comme v1 ou v5, ils étaient mal utilisés
      Même chose pour les versions basées sur un hachage comme v3
      v4 est simple et se prête moins aux erreurs