- 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
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
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
shiftdesloca o timestamp pelo intervalo fornecidohttps://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 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
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 timeno momento da geração para viabilizar o ataqueOutro 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 uploadse 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
psqluma 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 UXEu 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ãoNo fim das contas, às vezes parece que há certo exagero em fingir preocupação com vazamento de informação crítica
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?
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
Como o objetivo desse esquema é justamente parecer aleatório por fora, talvez o v8 acabasse chamando mais atenção