2 pontos por GN⁺ 2023-06-29 | 1 comentários | Compartilhar no WhatsApp
  • TypeID é um identificador seguro por tipo que estende o UUIDv7, um formato de identificador global único inspirado na forma como a API da Stripe usa prefixos
  • A string canônica é em minúsculas e composta por três partes: um prefixo de tipo ASCII em snake_case minúsculo de até 63 caracteres, um separador _ e um sufixo UUIDv7 de 26 caracteres codificado em base32 modificada
  • O prefixo de tipo evita usar por engano um ID de user onde é necessário um ID de post, e também permite identificar imediatamente, durante a depuração, a que tipo de entidade o identificador se refere
  • TypeID é um superconjunto de UUID; ao remover a informação de tipo e decodificar, obtém-se um UUIDv7 válido
  • É ordenável em K, podendo ser usado como chave primária de banco de dados, com o objetivo de reduzir o problema de piora de locality no banco em comparação com UUIDv4, que é um ID global totalmente aleatório
  • A codificação em base32 oferece segurança para URL, insensibilidade a maiúsculas e minúsculas, evita caracteres ambíguos, permite seleção com duplo clique e tem comprimento menor que a codificação hexadecimal tradicional do UUID
  • Implementações oficiais são fornecidas em Go, SQL e TypeScript, e a versão mais recente da especificação é a v0.3.0
  • Com a ferramenta de linha de comando, é possível gerar TypeIDs, decodificar o UUID de um TypeID existente e codificar um UUID em TypeID
    • typeid new prefix para gerar um novo TypeID
    • typeid decode prefix_01h2xcejqtf2nbrexx3vqjhp41 para exibir o tipo e o uuid
    • typeid encode prefix 0188bac7-4afa-78aa-bc3b-bd1eef28d881 para gerar um TypeID
  • No TypeID Converter da Jetify, é possível converter uma string TypeID em UUID ou converter para TypeID no formato prefix:UUID

1 comentários

 
GN⁺ 2023-06-29
Comentários no Hacker News
  • Algumas sugestões: seria bom fixar e documentar a string de prefixo antes que fique tarde demais
    Pela implementação em Go, parece ok usar ASCII minúsculo, mas isso fica ambíguo para tipos compostos como article-comment
    Em projetos complexos ou com ORM, é difícil evitar separadores, então valeria considerar permitir um único separador
    A implementação em Go não tem testes, mas como o código é fácil de testar em nível unitário, isso deveria ser adicionado sem falta
    Em Go, o mais correto seria seguir a implementação de UUID do Google, com uma função de parsing adequada e um array interno de bytes; string deveria ser usada só para renderização e prefixo
    O parsing atual é permissivo demais e parece entrar em modo de geração quando o suffix está vazio; além disso, fazer indexação depois de SplitN pode causar panic se não houver sublinhado
    Tentei encontrar falhas no design, mas no fim parece que acertaram bem o sweet spot dos trade-offs

    • As partes mais complexas, como a codificação base32, têm testes: https://github.com/jetpack-io/typeid-go/blob/main/base32/bas...
      Ainda assim, a crítica é válida, e conforme surgirem implementações em mais linguagens, vamos adicionar um conjunto de testes mais rigoroso para verificar a compatibilidade entre elas
      Sobre o prefixo, fiquei curioso se a preocupação é o fato de o conjunto de caracteres permitidos não estar definido na especificação
    • Como o README pede outras implementações, um conjunto de testes também seria útil para código de terceiros
    • Depois implementamos testes bem completos: https://github.com/jetpack-io/typeid-go/blob/main/typeid_tes...
      E também deixamos o prefixo mais claro na especificação
    • Vou tentar escrever testes para isso
    • Acho que houve um mal-entendido de que o prefixo seria algo fixo
      O prefixo é definido conforme o domínio de uso
  • Uso uma abordagem assim há alguns anos, mas com duas diferenças
    Primeiro, não considero que as pessoas realmente digitem esses valores à mão, então não me preocupo muito com a confusão entre l e 1
    Em vez disso, uso base32 sem as vogais eiou para reduzir a chance de formar palavras por acaso, especialmente palavrões
    Segundo, adiciono dois caracteres base32 com salt como checksum
    Assim, quando o valor está estranho por erro ou má-fé, não preciso nem consultar o armazenamento de dados; não sei por que outras implementações não fazem isso

    • Há alguns anos, uma senha gerada automaticamente saiu como analrita e recebi reclamações por isso
      Talvez valha considerar incluir a também na lista de exclusão
    • Concordo com adicionar checksum, mas fiquei curioso sobre a parte de “erro ou má-fé”
      Se a premissa é que humanos não digitam isso manualmente, fico em dúvida sobre qual seria o caminho para um valor incorreto entrar por engano
      Pode haver erro de copiar e colar ou uma página renderizar tudo em maiúsculas, mas com base32 dá para normalizar maiúsculas e minúsculas
      Também fico curioso sobre como um checksum de 2 bytes, não criptograficamente seguro, ajudaria em caso de má-fé
    • A ideia do checksum é interessante
      Estamos avaliando se faz sentido incluí-la na especificação do TypeID
    • Implementei a segunda abordagem alguns meses atrás como parte do esquema de codificação
      Não sei o quanto isso reduziu consultas ao banco de dados, mas é bom ver que não acabamos em erros mais ambíguos durante a decodificação
  • Separadamente do texto principal, há um link para o “Crockford's alphabet”: https://www.crockford.com/base32.html
    Esse esquema de base32 exclui I, que se confunde com 1, L, que também se confunde com 1, O, que se confunde com 0, e U
    Na página, diz-se que o motivo para excluir o U é “obscenidade acidental”, mas eu sinceramente não entendo o que isso quer dizer

    • Foi algo que o Crockford escreveu em tom de brincadeira
      Para criar um bom alfabeto base32 com caracteres alfanuméricos que não causem confusão, basta remover O, I e L
      Ainda assim sobram 33 caracteres, então é preciso tirar mais um, e como tanto faz qual seja, ele só inventou uma justificativa arbitrária para o último caractere removido
      Se for um ID visível ao usuário, não é um motivo totalmente absurdo, mas obviamente está longe de ser uma prevenção perfeita
    • Se I e O já foram removidos e você ainda tira o U, dá para eliminar bastante combinações de três ou quatro letras que podem parecer grosseiras
      Claro, ainda existem combinações possíveis por causa do A, mas a chance fica bem baixa
    • Para um verdadeiro purista do latim, U nem existia na época de Cícero, então é uma letra vulgar a ponto de beirar a obscenidade
    • Há mais coisas desse tipo: base58 no estilo do Satoshi/Bitcoin https://en.wikipedia.org/wiki/Binary-to-text_encoding#Base58, “base62” do saltpack do Keybase https://github.com/keybase/saltpack, o famoso “Adobe 85” https://en.wikipedia.org/wiki/Ascii85, e basE91 https://base91.sourceforge.net
      Na empresa, definimos várias novas “bases” para QR codes, e pessoalmente acho que essa é uma área pouco explorada da ciência da computação
    • Uns 4 anos antes do Crockford, eu e um colega tivemos a ideia de um conjunto de caracteres quase igual
      Estávamos resolvendo um problema de URL slug, e como o comprimento ficava relativamente grande, achamos que 5 bits por byte ajudariam a reduzir o incômodo de transcrição
      No fim, ainda havia margem para remover mais algumas letras, e depois que todo mundo foi embora, nós dois ficamos sentados classificando palavrões pelo grau de ofensa para decidir quais caracteres tirar
      Depois me convenceram de que termos pejorativos eram um problema maior, então passamos a focar em remover n em vez de u
      tggr é só meio fofo, mas n**r significaria ter várias conversas constrangedoras com equipes de RH
      O conjunto final de caracteres já está meio nebuloso na minha memória, mas em geral lidávamos com [a-z][0-9] e havia muitos símbolos impróprios para URL ou difíceis de ditar
      Pelo que lembro, removemos 0, l e 1, e acho que partíamos do princípio de que a transcrição seria toda em maiúsculas ou toda em minúsculas
      0o não era um problema, e 1L também não
  • Não gosto muito da codificação Crockford
    Na prática, ao dar suporte técnico ou analisar valores codificados dessa forma, achei que isso foi claramente um erro
    Esse alfabeto foi projetado para um objetivo que na prática é raro, como ler identificadores por telefone, e introduz ambiguidade
    Em situações em que você precisa dar grep em logs ou fazer referência cruzada usando a própria string codificada, isso é um desastre, e ainda permite hífens, o que vira uma grande fonte de erros de copiar/colar e de quebra de linha
    É raro digitar identificadores de objeto manualmente, mas é comum copiar e colar entre aplicativos, chats e fóruns, encaminhar por email e procurar em arquivos de log
    Nessas condições, ser pronunciável não tem valor, não diferenciar maiúsculas de minúsculas atrapalha, e o que se precisa é consistência e resistência a colagem e quebra de linha
    base58 é uma codificação bijetiva mais adequada para essas necessidades e ainda por cima mais curta
    Inspirados pela Stripe, há alguns anos usamos como identificadores de objeto UUIDs codificados em base58 com prefixo de tipo, como user_1BzGURpnHGn6oNru84B3Ri
    Ainda assim, é preciso levar em conta que a codificação base32 do Douglas Crockford foi projetada há 20 anos, em uma época em que o ambiente de uso era bem diferente

    • Também considerei base58 e base64url, e gostei das codificações mais curtas
      Mas eu não queria depender de distinção entre maiúsculas e minúsculas, então no fim acabei pendendo para base32
      Por exemplo, você pode querer usar o ID como nome de arquivo, e pode estar preso a um ambiente com sistema de arquivos case-insensitive
      O TypeID sempre usa o alfabeto do Crockford em minúsculas; ele não usa o conjunto completo de regras da codificação Crockford
      No TypeID, hífens não são permitidos, e também não é permitido codificar o mesmo ID de várias formas trocando caracteres ambíguos
    • Concordo que ler identificadores por telefone é raro
      Mas às vezes você digita manualmente um identificador que apareceu em uma captura de tela ou compartilhamento de tela, ou em outro dispositivo do qual não é fácil copiar o identificador
    • base62 e o KSUID usado em base58 têm muitas vantagens
      A base32 do Crockford também funciona, mas não me agrada tanto assim
      Pessoalmente, minha primeira opção seria usar KSUID com prefixo de tipo
      Você obtém um ID de 160 bits ordenável por K com codificação base62, e isso funciona bem a menos que você realmente precise de IDs de 128 bits por questão de compatibilidade
    • Não sei onde entram os hífens na Crockford Base32: https://en.wikipedia.org/wiki/Base32#Crockford's_Base32
      Minha codificação base32 favorita é a z-base-32, que me parece mais agradável aos olhos: https://philzimmermann.com/docs/human-oriented-base-32-encod...
      O maior problema da base58 é que ela funciona bem para inteiros, mas é menos adequada para dados binários arbitrários, como chaves criptográficas, e strings com distinção entre maiúsculas e minúsculas não ficam bonitas
  • Elegante
    Gostei do prefixo “type-safe”
    No nosso ORM, chamávamos de tagged ids uma abordagem parecida que anexava automaticamente tags por entidade aos IDs inteiros dentro do DB: https://joist-orm.io/docs/advanced/tagged-ids
    Usávamos : como separador, mas me arrependo um pouco de não ter usado _ pela facilidade de copiar/colar com clique duplo
    Em teoria, também seria bem fácil fazer o Joist converter uma “coluna UUID do DB” em um “TypeID do modelo de domínio”, mas atualmente isso provavelmente não seria possível só com configuração na camada do usuário
    Ainda assim, é uma boa ideia

    • O Reddit também usa algo parecido, mas otimizado para comprimento de string
      Os IDs dos elementos têm a forma t3_15bfi0, e t3_ é o prefixo de tipo
      t3 é post, t1 é comentário, t5 é subreddit, e o restante é a codificação em base36 da chave primária auto-incremental
    • O sistema interno de IDs tipados da nossa empresa usava inicialmente dois-pontos como separador, mas logo mudamos para ponto (.)
      Os dois-pontos eram codificados como % em URLs, aumentando o tamanho do ID, e as URLs ficavam feias e longas, o que era muito irritante
  • UUIDv7 é popular no HN há anos, e fico me perguntando quando isso vai virar um padrão de verdade e quando bibliotecas, bancos de dados e o restante do ecossistema vão passar a oferecer suporte nativo

    • Não sei que tipo de suporte você espera
      A maioria dos softwares não se importa com bits específicos dentro do UUID, então já dá para usar hoje
      Se houver software que se importe com bits específicos, basta fazê-lo parecer um UUIDv4, e esses bits também podem ser gerados aleatoriamente
      Se for necessário um procedimento de geração, você mesmo pode implementá-lo, e não é difícil
    • Parece estar na etapa final de padronização no IETF: https://datatracker.ietf.org/doc/draft-ietf-uuidrev-rfc4122b...
    • Já passou por rascunhos e melhorias, está muito perto da padronização, e muitas bibliotecas já oferecem suporte ou estão adicionando novos recursos
      Por exemplo, eu mantenho a biblioteca UUID de Dart, e a versão beta major mais recente inclui v6, v7 e v8 customizado
      Também existe uma lista de implementações em algum lugar, e eu apareço naquela página como mantenedor de biblioteca, então recebo avisos para os autores sempre que sai um novo rascunho
  • Gostei da parte de “dá para copiar/colar com clique duplo”
    Detalhes importam

  • Tenho algumas reclamações sobre UUID
    Não entendo por que precisamos passar por todo esse ritual de versionamento de UUID em vez de simplesmente combinar tempo + aleatoriedade
    Se localidade não importa, basta usar 128 bits aleatórios
    Pela minha experiência, a maioria das pessoas parece achar que UUID precisa ser armazenado na representação hexadecimal legível por humanos, inclusive com hífens
    Isso desperdiça espaço demais em banco de dados, rede e memória

    • Muita gente pensou a mesma coisa
      Por exemplo, o ULID https://github.com/ulid/spec é mais curto, armazena o tempo e é ordenável lexicograficamente
    • No mundo UUID, há uma tendência de falar como se um único caso de uso representasse todos os outros e, portanto, existisse um único melhor formato de UUID
      Na realidade, como em outros problemas de engenharia, basta listar os requisitos e ver o que resolve o problema
      Ler sobre as vantagens de vários formatos ajuda, porque permite aproveitar o que os outros aprenderam com dificuldade
      Ordenabilidade e localidade baseada em tempo podem não ser coisas que vêm naturalmente à cabeça, mas, se forem necessárias, é muito melhor saber disso antes do que descobrir quatro anos depois do projeto que você descartou esses dados
      Alguns formatos de UUID também introduziram pequenos problemas de segurança por conta própria; por exemplo, vale a pena evitar coisas como o vazamento de endereço MAC no UUID v1
      Se uma solução existente servir para o caso de uso certo, é só usá-la
      Se não servir, também não é algo com que você precise se preocupar demais
      Pessoalmente, no lugar onde mais uso UUID, a especificação foi “mande apenas uma string única; se isso te deixar mais confortável, use a biblioteca de UUID que quiser”
      Esse sistema parece ter um formato diferente para cada origem de dados, mas tudo bem
      A escala é de dezenas de milhares por dia, então não há problema de escala, e também não há necessidade de ordenar por UUID
      Em vez de ser um identificador de fato, isso funciona mais como um token único criado pelo emissor para cada mensagem, usado para detectar chegadas duplicadas a jusante em um sistema heterogêneo com várias filas
      Também não há necessidade de unicidade global; basta ser único dentro de um shard pequeno e, em princípio, até poderia se repetir depois de algum tempo
      Mas, por simplicidade, mantemos o sistema de forma que seja único ao longo de todo o período
      Quando gero isso eu mesmo, leio dados de /dev/urandom, codifico em base64 e escolhi um tamanho que não termine com ==
      Isso também não foi por um problema real, mas por estética
    • Combinar tempo e aleatoriedade não garante unicidade global
    • Pessoalmente, acho que o único padrão UUID realmente valioso é o v4, por ser pura aleatoriedade
      Mesmo assim, ainda não entendo por que os hífens são necessários, e acho que as outras versões não servem para muita coisa
    • O pior dos hífens é que eles impedem copiar/colar tudo facilmente com clique duplo
  • Se você não precisa necessariamente de UUIDv7 ou UUID, https://github.com/segmentio/ksuid oferece um espaço de chaves muito maior
    Se precisar de namespace, basta adicionar um prefixo de string, e a probabilidade de colisão do KSUID é muito menor do que a de qualquer versão de UUID
    Entre os geradores de ID de uso geral com timestamp ordenável, o ksuid foi o melhor que encontrei, e há bibliotecas para a maioria das linguagens
    UUID v1~v7 desperdiçam muito

    • Publiquei um pacote JavaScript enxuto que gera KSUIDs com prefixo de tipo
      Se você puder arcar com os bits extras, e a maioria pode, o KSUID é um ótimo formato
      [1] https://github.com/sophiabits/resource-id
    • O motivo para mudar para ULID foi que ele é sempre minúsculo, então pode ser tratado sem diferenciar maiúsculas de minúsculas
    • Mantenho algumas bibliotecas populares de ksuid e realmente uso ksuid, então gosto dele
      Mas o grande problema do ksuid é que ele tem 160 bits, então não se encaixa no tipo UUID nativo dos bancos de dados, por exemplo no PostgreSQL, o que gera custo de performance
      1: https://github.com/svix/rust-ksuid
      2: https://github.com/svix/python-ksuid
  • K-ordenável é um conceito excelente, e chaves fracamente ordenadas resolvem vários casos de uso
    Também gosto da representação em string compacta com tipo
    Mas me preocupo se muitos problemas de segurança vão surgir como efeito colateral não intencional do UUID v7
    As pessoas não deveriam usar UUID como token, nem como chave primária do banco de dados, mas na prática fazem isso
    UUID v4 é, na prática, um número aleatório criptográfico, então acho que até agora muitas vulnerabilidades de segurança acabaram passando ilesas por sorte
    No UUID v7, eu tinha entendido que os dados realmente aleatórios caíam para 32 bits, e achava que seria preciso educar bem os desenvolvedores sobre o fato de UUIDs poderem ser adivinháveis
    Correção: parece que eu li errado sobre a previsibilidade do UUID v7
    O rascunho recomenda CSPRNG para os bits aleatórios, diz que a entropia é de no mínimo 74 bits e que ele foi projetado para ser “imprevisível”
    Para usos relacionados à segurança ele manda usar “UUID v4”, mas isso provavelmente pode ter a ver com o significado ligado ao timestamp

    • Há uma questão de usabilidade interessante
      Se os nomes são UUIDv4 e UUIDv7, isso não vai gerar uma confusão sem fim, em que é preciso ficar lembrando qual é melhor para banco de dados e qual é melhor para token de uso único?
      Também não vejo uma boa solução com compatibilidade retroativa
      Em Elixir, usa-se a função UUID.uuid4() para gerar UUID v4
      Em teoria daria para varrer o código procurando esses usos, mas tudo isso só aumenta a chance de erro
    • Consigo ver alguns casos em que pode valer a pena, mas no passado, sempre que encontrei UUIDs parcialmente sequenciais como v1 ou v5, eles estavam sendo usados de forma indevida
      O mesmo acontecia com os baseados em hash, como o v3
      v4 é simples e menos propenso a uso indevido