21 pontos por GN⁺ 2025-09-19 | 1 comentários | Compartilhar no WhatsApp
  • UUIDv47 permite armazenar no banco de dados um UUIDv7 ordenável e, ao mesmo tempo, fornecer em APIs externas um valor com aparência de UUIDv4
  • Faz mascaramento XOR apenas no campo de timestamp, protegendo as informações temporais do UUIDv7, enquanto mantém intactos os demais campos aleatórios
  • Usa SipHash-2-4 com chave de 128 bits para mascaramento, permitindo proteger informações com segurança sem risco de exposição da chave
  • O processo de encode/decode é determinístico e reversível, e a aleatoriedade é preservada, mantendo baixo risco de colisão
  • Os benchmarks mostram desempenho muito rápido e um método de integração simples, com fácil conexão a bancos de dados como PostgreSQL

Visão geral e importância do projeto

  • UUIDv47 é uma biblioteca open source em C que armazena internamente no banco de dados um UUIDv7 vantajoso para ordenação e indexação, enquanto expõe a APIs e sistemas externos um valor com aparência de UUIDv4, alcançando ao mesmo tempo proteção de privacidade e alto desempenho
  • Em comparação com outros algoritmos de conversão de UUID, destaca-se por mapeamento reversível, compatibilidade com RFC, segurança com impossibilidade de recuperar a chave, zero-deps e uma estrutura simples baseada apenas na inclusão de um arquivo de cabeçalho

Principais características

  • Header-only C (C89), permitindo integração simples sem dependências externas
  • Faz mascaramento XOR apenas no campo de timestamp do UUIDv7 para evitar a exposição de informações temporais, sem alterar os demais campos aleatórios
  • Usa SipHash-2-4 com chave para mascaramento, permitindo proteger informações com segurança por meio de uma chave de 128 bits
  • O processo de encode/decode é determinístico e completamente reversível (restauração exata do valor original)
  • Suporta mapeamento rápido entre UUIDs para armazenamento em banco (v7) e para exposição externa (v4)
  • Fornece exemplos abundantes, incluindo código de teste e ferramentas de benchmark

Objetivos de uso e benefícios

  • Permite usar UUIDv7 ordenável para maximizar a localidade de índice e a eficiência de paginação no banco de dados
  • Externamente, expõe apenas um padrão com aparência de UUIDv4, evitando vazamento de timestamp e rastreamento
  • Usa SipHash, tornando inviável recuperar a chave e garantindo a segurança da chave secreta
  • Tratamento compatível com RFC para bits de versão/variante
  • O funcionamento é rápido, sendo eficiente também em processamento em tempo real e em ambientes de geração em massa

Estrutura principal e funcionamento interno

Layout do UUIDv7

  • ts_ms_be: timestamp big-endian de 48 bits
  • ver: nibble alto do 6º byte (0x7=BD, 0x4=externo)
  • rand_a: valor aleatório de 12 bits
  • var: variante RFC (0b10)
  • rand_b: valor aleatório de 62 bits

Lógica de mascaramento e mapeamento (Façade mapping)

  • Codificação: ts48 XOR mask48(R), version=4
  • Decodificação: encTS XOR mask48(R), version=7
  • Os campos aleatórios não são alterados
  • O SipHash usa como entrada o campo aleatório de 10 bytes
  • O mascaramento XOR pode ser revertido imediatamente se a chave for conhecida

Modelo de segurança

  • Objetivo: impedir exposição mesmo se a chave receber entradas escolhidas seletivamente
  • Implementação: uso do SipHash-2-4, uma função pseudoaleatória com chave (PRF)
  • Uso de chave de 128 bits, com recomendação de derivação de chave via HKDF etc.
  • Ao fazer rotação de chave, recomenda-se não armazená-la dentro do UUID e manter separadamente apenas um pequeno ID de chave

API pública (C)

  • uuidv47_encode_v4facade : conversão v7→v4
  • uuidv47_decode_v4facade : restauração v4→v7
  • Também oferece funções relacionadas a definição de versão, parsing e formatação

Desempenho e benchmarks

  • Na operação de mascaramento SipHash (10B), fica abaixo de 14ns/op, e o round trip completo de encode+decode atinge cerca de 33ns/op (base Apple M1)
  • Garante processamento rápido mesmo em geração e mapeamento de UUIDs em grande volume
  • Melhor desempenho com as opções -O3 -march=native

Integração e expansão

  • Recomenda-se fazer encode/decode na fronteira da API
  • Para integração com PostgreSQL, deve-se escrever uma extensão em C
  • Em cenários de sharding, é possível aplicar hash à façade v4 com xxh3, SipHash etc.

Outros

  • Há ports para outras linguagens, como Go (n2p5/uuid47)
  • Hash recomendado: como xxHash não é uma PRF, há risco de vazamento de informação; recomenda-se usar SipHash

Licença

  • Licença MIT (Stateless Limited, 2025)

1 comentários

 
GN⁺ 2025-09-19
Comentários do Hacker News
  • Olá, sou o autor do uuidv47. A ideia básica é usar UUIDv7 internamente para garantir indexação e ordenação no banco de dados, mas expor externamente um valor que pareça um UUIDv4 para não revelar padrões temporais ao cliente
    Ele funciona mascarando com XOR o timestamp de 48 bits usando um fluxo SipHash-2-4 derivado do campo aleatório do UUID
    Os bits aleatórios são preservados, a versão muda de 7 internamente para 4 externamente, e o valor de variante RFC também é mantido
    O mapeamento é injetivo: segue a estrutura (ts, rand) → (encTS, rand)
    A decodificação é feita como encTS ⊕ mask, então a transformação de ida e volta é perfeita
    Do ponto de vista de segurança, como o SipHash é um PRF, mesmo vendo o valor empacotado externamente não se expõe a chave
    Se a chave estiver errada, o timestamp também sai completamente diferente
    Também é possível dar suporte a rotação de chaves com gerenciamento externo de key-ID
    Em termos de desempenho, é um SipHash a cada 10 bytes e algumas operações de load/store de 48 bits, então o overhead fica na casa de nanossegundos; é header-only em C11, sem dependências externas e sem necessidade de alocação
    Os testes cobrem vetores de referência do SipHash, round-trip de encode/decode e invariância de versão/variante
    Queria saber o que vocês acham

    • Gostei da ideia
      UUID muitas vezes é gerado no lado do cliente, e nesse esquema isso parece impossível
      Será que, mesmo recebendo UUIDs gerados pelo cliente e devolvendo uma versão mascarada, não surgiria uma vulnerabilidade porque alguém poderia fornecer dois UUIDs com ts diferente e rand igual?
      No fim, queria entender se isso só serve mesmo para os casos em que você gera diretamente o UUIDv7

    • Tenho duas observações

      1. Esse esquema elimina a possibilidade de outras pessoas aproveitarem mais o valor do UUIDv7, o que é uma pena do ponto de vista de quem usa a API
      2. Se a API externa e o formato interno de armazenamento forem diferentes, vai ser preciso passar sempre por esse processo de conversão, o que complica um pouco a manutenção
        Não sei se esse incômodo vale tanto a pena assim
    • Minha maior preocupação é a qualidade da entropia dos bits aleatórios
      O UUIDv7 está mais focado em evitar colisões do que em imprevisibilidade
      Por isso, o RFC fala de não aleatoriedade como uma recomendação (should), não uma exigência (must), e há implementações que usam PRNG fraco ou contador, ou até colocam dados adicionais de relógio no lugar dos bits aleatórios (referência: RFC9562 s6.2 & s6.9)
      Então, usar diretamente rand_a e rand_b do v7 como seed do PRF pode ser mais arriscado do que parece se esses dados vierem de fora da fronteira de confiança
      Até o novo uuidv7() do PostgreSQL 18 preenche rand_a inteiro com timestamp de alta precisão, o que continua estando em conformidade com o RFC
      Se você olhar UUIDs gerados em importações em massa, esse esquema de v7-para-v4 também acaba permitindo agrupamento e, portanto, vazamento de informação
      Para coleta de dados de peças de motores talvez não haja problema, mas se forem identificadores ligados diretamente a pessoas, vale ter cuidado
      No fim, a menos que você garanta diretamente uma entropia confiável, esse esquema também pode vazar timing, série ou correlação, então é indispensável inspecionar a implementação de v7 usada como origem

    • Acho uma má ideia
      No PostgreSQL 18, o parâmetro opcional shift desloca o timestamp pelo intervalo fornecido
      https://www.postgresql.org/docs/18/functions-uuid.html

  • Há alguns anos criei meu próprio esquema: usar IDs numéricos sequenciais no banco e expor externamente strings aleatórias curtas de 4 a 20 caracteres
    Na época usei uma instância customizada da família de cifras Speck, e acho que ficou sólido e bem convincente
    Cheguei a concluir o trabalho, mas como adiei os projetos em que ele seria usado, nunca publiquei
    Pretendo divulgar esse material oficialmente este ano ou no próximo
    Se tiver curiosidade, há notas bem organizadas sobre a implementação, vantagens e desvantagens
    https://temp.chrismorgan.info/2025-09-17-tesid/

    • Eu também já tentei usar Speck para ofuscar bigserial PKID, mas faltava implementação multiplataforma e, especialmente no pgcrypto, o suporte era fraco
      Então acabei escolhendo base58(AES_K1(id{8} || HMAC_K2(id{8})[0..7]))
      O resultado fica mais comprido, normalmente algo em torno de 22 caracteres, mas é implementável em praticamente qualquer ambiente e o desempenho é mais do que suficiente

    • Boa ideia
      Em um conceito parecido, também vale dar uma olhada em sqids (nome antigo: hashids)
      https://sqids.org/

  • Já tive uma experiência semelhante: usávamos duas colunas, um uuid público e um bigint PK que não era exposto pela API (isso foi bem antes de existir uuidv7)
    Fica um pouco menos conveniente em termos de uuid, mas se você remover direito só o PK, a vantagem é que dá para mesclar dumps de bancos diferentes com facilidade
    Mesmo fazendo busca por hash, me parece que no fim ainda seria preciso manter duas colunas, embora eu possa estar entendendo errado como esse hash funciona

    • A conversão é reversível com uma chave criptográfica secreta
      A partir do valor uuidv4 da requisição, você pode voltar para o uuidv7 no banco
  • A ideia em si é interessante, mas eu gostaria que o próprio banco de dados desse suporte direto a isso
    Ou seja, que fosse possível converter UUIDv7 em “UUIDv4” e vice-versa, e usar os dois formatos explicitamente nas consultas

  • Projeto muito legal
    Fiz uma implementação em Go usando a biblioteca siphash do dchest
    https://github.com/n2p5/uuid47
    Referência: https://github.com/dchest/siphash

  • O projeto é interessante, mas queria ver um exemplo real mostrando o risco de expor a parte temporal do uuid v7

    • Isso pode expor padrões de comportamento ou sequências de ações de usuários em contextos delicados

      • “Ex-marido: vendo seu userID no site de namoro, dá para ter certeza de que você criou a conta na festa do Tom?”
      • “Você diz que seu TZ é XYZ, mas o log do imageID (que revela a hora de criação) parece sempre marcar 3 da manhã, não?”
        Para mensagens individuais ou transações em tempo real isso talvez não importe, mas em criação de contas ou dados de longo prazo alguém pode usar isso para rastrear identidades
    • Já brute forcei parte de um UUID em um CTF para obter uma chave AES
      Como a chave era derivada em parte de uma fonte temporal, bastava descobrir o system time no momento da geração para viabilizar o ataque
      Outro exemplo simples é um serviço de compartilhamento de arquivos que só expõe algo como website.com/GUID; mesmo sem divulgar separadamente o horário do upload
      se usar UUIDv7, o próprio identificador já permite estimar quando o arquivo foi enviado
      Talvez isso não seja uma grande ameaça de segurança, mas ainda assim é vazamento de informação não intencional

    • Imagine, por exemplo, um sistema que armazena dados médicos
      Mesmo que ele remova dados pessoais depois que um resultado de MRI é enviado logo após o exame para fins de análise
      a correlação externa com o timestamp do uuidv7 ainda pode permitir inferências do tipo: “nesse dia só uma pessoa fez MRI, então dá para descobrir de quem era esse MRI”

  • O ponto mais incômodo do uuidv7 é que, numa lista, é muito difícil para humanos comparar visualmente (diff) os valores
    Se houvesse no psql uma camada de visualização em que os bits aleatórios viessem na frente e a ordenação real continuasse baseada no timestamp, isso seria uma enorme melhora de UX

    • Eu simplesmente me acostumei a olhar só para a parte final do UUID

    • Dá para criar uma função por conta própria e usar na query
      Por exemplo, gerar a representação hexadecimal e inverter a string, ou exibir em base64 invertido; ficaria mais curto e mais fácil de diferenciar

  • Esse esquema parece muito bom
    Mas esse alarmismo em torno de timestamp exposto, e a ideia de que expor IDs sequenciais automaticamente implica superfície de ataque ou vazamento de informação de negócio, me parecem mais preocupação excessiva do que problema real de segurança
    Bastaria somar periodicamente um valor aleatório grande a um int, mantendo ainda assim a característica monotônica, e já ficaria bem mais difícil para um observador externo entender o padrão
    No fim das contas, às vezes parece que há certo exagero em fingir preocupação com vazamento de informação crítica

    • O que está sendo exposto aqui não é informação do negócio, e sim informação do cliente
      A informação vazada pelo sistema em si pode parecer irrelevante, mas quando observada em volume ou ao longo do tempo permite inferir dados adicionais
      Um exemplo é a palestra SpiegelMining, de David Kriesel: só coletando datas e autores de artigos de jornal já dá para extrair padrões de quando cada pessoa tira férias
      Comparando dados de vários autores, até relacionamentos internos podem acabar ficando óbvios
  • Por que não usar uma chave criptográfica diferente por sessão e expor externamente apenas IDs criptografados?
    Assim o banco poderia continuar usando apenas IDs sequenciais simples, não?

    • Para descriptografar os bits de timestamp escondidos no token, é preciso saber qual chave usar
      Se você trocar a chave periodicamente, o gerenciamento de chaves fica muito complexo, e ainda surge o problema de como descobrir a chave certa em cada caso
  • Por que usar a versão 4 em vez da versão 8?
    O v4 significa bits aleatórios, mas na prática aqui eles não são tão aleatórios assim
    O v8 não impõe restrições quanto ao significado dos bits

    • Também não sei a resposta certa, mas se a entropia for alta talvez dê para enxergar isso como algo semelhante a um PRNG com seed
      Como o objetivo desse esquema é justamente parecer aleatório por fora, talvez o v8 acabasse chamando mais atenção