21 puntos por GN⁺ 2025-09-19 | 1 comentarios | Compartir por WhatsApp
  • UUIDv47 permite almacenar UUIDv7 ordenables en la base de datos mientras entrega a la API externa valores que parecen UUIDv4
  • Solo enmascara con XOR el campo de marca de tiempo para proteger la información temporal de UUIDv7, y conserva intactos los demás campos aleatorios
  • Usa una clave de 128 bits con SipHash-2-4 para aplicar el enmascaramiento, lo que permite proteger la información de forma segura sin riesgo de exponer la clave
  • El encode/decode es determinista y reversible, y mantiene la aleatoriedad para reducir el riesgo de colisiones
  • Los resultados de benchmark muestran un rendimiento muy rápido y una integración sencilla, con fácil conexión a bases de datos como PostgreSQL

Resumen del proyecto y su importancia

  • UUIDv47 es una biblioteca open source en C que logra al mismo tiempo protección de privacidad y procesamiento de alto rendimiento al guardar internamente en la base de datos UUIDv7 convenientes para ordenamiento e indexación, mientras expone hacia APIs y sistemas externos valores que parecen UUIDv4
  • Frente a otros algoritmos de transformación de UUID, ofrece ventajas diferenciadas como mapeo reversible, compatibilidad con RFC, seguridad con imposibilidad de recuperar la clave, zero-deps y una estructura simple basada solo en un archivo de encabezado

Características principales

  • Header-only C (C89), con integración sencilla sin dependencias externas
  • Solo enmascara con XOR el campo de marca de tiempo de UUIDv7 para evitar la exposición de información temporal, dejando sin cambios el resto de los campos aleatorios
  • Utiliza SipHash-2-4 con clave para el enmascaramiento, permitiendo proteger la información de forma segura con una clave de 128 bits
  • El proceso de encode/decode es determinista y completamente reversible (se puede restaurar exactamente el original)
  • Soporta mapeo rápido entre UUID para almacenamiento en base de datos (v7) y exposición externa (v4)
  • Incluye abundantes ejemplos como código de prueba y herramientas de benchmark

Objetivos de uso y beneficios

  • Permite aprovechar UUIDv7 ordenables para maximizar la localidad del índice y la eficiencia de paginación dentro de la BD
  • Hacia afuera expone solo un patrón que parece UUIDv4, evitando la filtración de marcas de tiempo y el rastreo
  • Usa SipHash, por lo que la clave no puede recuperarse y se garantiza la seguridad de la clave secreta
  • Manejo de bits de versión/variante compatible con RFC
  • Su velocidad de operación lo hace eficiente incluso en procesamiento en tiempo real y entornos de generación masiva

Estructura principal y funcionamiento interno

UUIDv7 Layout

  • ts_ms_be: marca de tiempo big-endian de 48 bits
  • ver: nibble alto del 6.º byte (0x7=BD, 0x4=externo)
  • rand_a: valor aleatorio de 12 bits
  • var: variante RFC (0b10)
  • rand_b: valor aleatorio de 62 bits

Lógica de enmascaramiento y mapeo (Façade mapping)

  • Codificación: ts48 XOR mask48(R), configurar version=4
  • Decodificación: encTS XOR mask48(R), configurar version=7
  • No se modifican los campos aleatorios
  • Se usan 10 bytes de campo aleatorio como entrada de SipHash
  • El enmascaramiento XOR puede revertirse de inmediato si se conoce la clave

Modelo de seguridad

  • Objetivo: que la clave no se exponga incluso si se eligen entradas de forma selectiva
  • Implementación: uso de SipHash-2-4, una función seudoaleatoria con clave (PRF)
  • Uso de clave de 128 bits; se recomienda derivación de clave mediante HKDF u otros métodos
  • Al rotar claves, se recomienda no almacenarlas dentro del UUID y mantener aparte un pequeño key ID

API pública (C)

  • uuidv47_encode_v4facade : conversión v7→v4
  • uuidv47_decode_v4facade : restauración v4→v7
  • También ofrece funciones relacionadas con configuración de versión, parsing y formatting

Rendimiento y benchmarks

  • En la operación de enmascaramiento SipHash (10B), alcanza menos de 14ns/op, y el round trip completo de encode+decode ronda los 33ns/op (sobre Apple M1)
  • Garantiza procesamiento rápido incluso en generación y mapeo masivo de UUID
  • Máximo rendimiento con las opciones -O3 -march=native

Integración y expansión

  • Se recomienda manejar encode/decode en el límite de la API
  • Para integración con PostgreSQL, se sugiere escribir una extensión en C
  • En escenarios de sharding, el façade v4 puede hashearse con xxh3, SipHash, etc.

Otros

  • Hay ports a otros lenguajes, como Go (n2p5/uuid47)
  • Hash recomendado: xxHash no es una PRF y puede implicar riesgo de filtración de información; se recomienda usar SipHash

Licencia

  • Licencia MIT (Stateless Limited, 2025)

1 comentarios

 
GN⁺ 2025-09-19
Opiniones de Hacker News
  • Hola, soy el autor de uuidv47. La idea básica es usar UUIDv7 internamente para obtener indexación y ordenabilidad en la base de datos, pero mostrar hacia afuera un valor que se vea como UUIDv4 para no exponer a los clientes patrones de tiempo.
    Funciona haciendo una máscara XOR del timestamp de 48 bits con un flujo SipHash-2-4 derivado del campo aleatorio del UUID.
    Los bits aleatorios se conservan tal cual, la versión cambia de 7 internamente a 4 externamente, y también se mantiene el valor de variante RFC.
    El mapeo es inyectivo: tiene la forma (ts, rand) → (encTS, rand).
    La decodificación es encTS ⊕ mask, así que la conversión de ida y vuelta es perfecta.
    En seguridad, como SipHash es un PRF, aunque alguien vea el valor empaquetado desde afuera no se filtra la clave.
    Si la clave es incorrecta, el timestamp también sale completamente distinto.
    También se puede soportar rotación de claves con administración externa del key-ID.
    En rendimiento, es una ejecución de SipHash por cada 10 bytes y unas pocas operaciones de carga/almacenamiento de 48 bits, así que el overhead está en el orden de nanosegundos; es solo header en C11, sin dependencias externas y sin necesidad de asignaciones.
    Las pruebas cubren vectores de referencia de SipHash, encode/decode de ida y vuelta, y tests de invariancia de versión/variante.
    Me interesa recibir feedback.

    • Me gusta la idea.
      Los UUID muchas veces se generan del lado del cliente, y con este enfoque eso no parece posible.
      ¿Si aceptaras UUID generados por el cliente y devolvieras la versión enmascarada, no aparecería una vulnerabilidad porque alguien podría darte dos UUID con distinto ts pero el mismo rand?
      En el fondo, me pregunto si esto solo aplica cuando generas tú mismo el UUIDv7.

    • Tengo dos observaciones.

      1. Esto elimina la posibilidad de que otros aprovechen más el valor de UUID v7, lo cual desde el punto de vista de quien consume la API es una lástima.
      2. Si la API externa y el formato de almacenamiento interno son distintos, siempre hay que pasar por esta conversión, así que la operación se vuelve un poco más engorrosa de mantener.
        No sé si ese nivel de molestia compensa realmente.
    • Mi mayor preocupación es la calidad de la entropía de los bits aleatorios.
      UUIDv7 se enfoca más en evitar colisiones, así que prioriza menos la impredecibilidad que la probabilidad de colisión.
      Por eso, según el RFC, lo no aleatorio no está exigido como must sino solo recomendado como should, y hay implementaciones que usan PRNG débiles o contadores, e incluso otras que meten datos adicionales del reloj en lugar de bits aleatorios (referencia: RFC9562 s6.2 & s6.9).
      Entonces, usar directamente rand_a y rand_b de v7 como seed para el PRF puede ser más riesgoso de lo que parece si esos datos vienen de fuera del límite de confianza.
      Incluso el nuevo uuidv7() de PostgreSQL 18 llena rand_a completo con el timestamp de alta precisión, y eso igual cumple con el RFC.
      Si miras UUID generados en importaciones masivas, al final este esquema de v7-a-v4 también permite agruparlos y por lo tanto puede filtrar información.
      Para cosas como recolección de datos de partes de motores quizá no haya problema, pero si se trata de identificadores ligados directamente a personas hay que tener cuidado.
      En resumen, mientras no se garantice explícitamente una entropía confiable, este esquema también puede filtrar información de timing, serie o correlación, así que es indispensable revisar directamente la fuente de la implementación v7.

    • Me parece una mala idea.
      En PostgreSQL 18, el parámetro opcional shift desplaza el timestamp por el intervalo indicado.
      https://www.postgresql.org/docs/18/functions-uuid.html

  • Hace unos años diseñé mi propio esquema usando IDs numéricos secuenciales crecientes en la base de datos, y exponiendo hacia afuera cadenas aleatorias cortas de entre 4 y 20 caracteres.
    Para eso usé una instancia personalizada de la familia de cifrados Speck, y me parece robusta y bastante razonable.
    Lo terminé, pero como fui posponiendo el proyecto real donde iba a usarlo, no lo publiqué.
    Planeo publicar formalmente ese material este año o el próximo.
    También tengo notas bien organizadas sobre la implementación, sus ventajas y desventajas, por si a alguien le interesa.
    https://temp.chrismorgan.info/2025-09-17-tesid/

    • Yo también intenté hace tiempo ofuscar un PKID bigserial con Speck, pero faltaban implementaciones multiplataforma y especialmente en pgcrypto el soporte era flojo, así que terminé eligiendo base58(AES_K1(id{8} || HMAC_K2(id{8})[0..7])).
      El resultado suele medir unas 22 letras, así que es más largo, pero se puede implementar en casi cualquier entorno y el rendimiento me resulta más que suficiente.

    • Buena idea.
      En una línea parecida, también vale la pena mirar sqids (antes llamado hashids).
      https://sqids.org/

  • Tuve una experiencia similar hace tiempo: manejábamos dos columnas, un uuid público y un bigint PK que no se exponía por la API (esto fue mucho antes de que existiera uuidv7).
    Era menos cómodo que usar solo uuid, pero si lograbas excluir bien el PK, la ventaja era que podías fusionar fácilmente dumps de distintas bases de datos.
    Incluso si haces búsquedas basadas en hash, igual parece que terminarías necesitando dos columnas, aunque también podría ser que yo esté entendiendo mal cómo funciona este hash.

    • La transformación puede revertirse con una clave criptográfica secreta.
      Puedes convertir en la request el valor uuidv4 al uuidv7 de la base de datos.
  • La idea en sí me parece interesante, pero ojalá la base de datos diera soporte directo a este tipo de procesamiento.
    Es decir, que pudiera convertir UUIDv7 a “UUIDv4” y viceversa, y que en las consultas también se pudieran usar ambos formatos distinguiéndolos explícitamente.

  • Es un proyecto realmente genial.
    Hice una implementación en Go usando la librería siphash de dchest.
    https://github.com/n2p5/uuid47
    Referencia: https://github.com/dchest/siphash

  • El proyecto me parece interesante, pero me gustaría ver un ejemplo concreto de cómo la parte temporal de uuid v7 puede llegar a exponer información en la práctica.

    • Podría exponerte a situaciones problemáticas si se filtran patrones o secuencias de comportamiento de los usuarios.

      • “Exmarido: viendo tu userID del sitio de citas, está clarísimo que te abriste la cuenta en la fiesta de Tom, ¿no?”
      • “Dices que tu TZ es XYZ, pero por los logs del imageID (único por momento de creación) parece que siempre aparecen a las 3 de la madrugada, ¿no?”
        Para mensajes individuales o transacciones en tiempo real quizá no importe, pero cuando se trata de creación de cuentas de usuarios o datos de largo plazo alguien podría abusar de eso para rastrear identidades.
    • Una vez, en un CTF, hice brute force de parte de un UUID que se usaba como clave AES.
      Como la clave derivaba parcialmente de una fuente temporal, bastaba con averiguar la hora del sistema en que se generó para poder atacarla.
      Otro ejemplo sencillo: en un servicio para compartir archivos, aunque solo publiques algo como sitio.com/GUID y no muestres por separado cuándo se subió el archivo,
      si usas UUIDv7 ese valor por sí solo ya permite estimar la hora de subida.
      Tal vez no siempre sea una gran amenaza de seguridad, pero sí es una filtración involuntaria de información.

    • Por ejemplo, imagina un sistema que almacena datos médicos.
      Si para análisis se suben resultados justo después de tomar una resonancia magnética, y aunque luego se elimine la información personal,
      con el timestamp de uuidv7 se puede hacer correlación externa y concluir: “Ese día solo una persona se hizo una resonancia, así que ya sé de quién es”.

  • Lo más incómodo de uuidv7 es que en una lista es dificilísimo compararlos visualmente (hacer diff) a simple vista.
    Si en psql hubiera una capa de visualización donde los bits aleatorios aparecieran primero y se mantuviera el orden real según el timestamp, sería una mejora enorme de UX.

    • Yo simplemente me acostumbré a mirar solo la parte final del UUID.

    • También puedes crear tu propia función y usarla en una query.
      Por ejemplo, mostrarlo como representación hex y luego invertir la cadena, o sacarlo en base64 invertido, lo haría más corto y más fácil de distinguir.

  • Esto se ve bastante bien.
    Pero me parece que el alarmismo por exponer timestamps, y la idea de que exponer IDs secuenciales equivale automáticamente a exposición de ataques o de información de negocio, se acerca más a una preocupación exagerada que a un problema de seguridad real.
    Bastaría con sumar periódicamente un valor aleatorio grande a un int para mantener la propiedad monotónica y al mismo tiempo dificultar que un observador externo entienda el patrón.
    Al final, siento que a veces se exagera demasiado bajo la apariencia de preocuparse por filtraciones importantes.

    • Lo que se expone aquí no es información del negocio, sino información del cliente.
      Puede que la información que filtra el sistema por sí sola no signifique mucho, pero cuando la observas en volumen o en series temporales sí permite inferir datos adicionales.
      Como ejemplo, en la charla SpiegelMining de David Kriesel, con solo recolectar fechas de artículos periodísticos y autores ya se podían extraer patrones de quién se iba de vacaciones y cuándo.
      Si comparas datos de varios autores, incluso podrían quedar al descubierto relaciones dentro de la empresa y cosas así.
  • Me pregunto por qué no usar una clave criptográfica diferente por sesión y exponer hacia afuera solo IDs cifrados.
    Así la base de datos podría usar simplemente IDs secuenciales normales.

    • Para descifrar los bits de timestamp ocultos en el token tienes que saber qué clave usar.
      Si cambias la clave periódicamente, la gestión de claves se vuelve muy complicada, y además surge el problema de cómo encontrar en cada caso la clave correcta.
  • Me pregunto por qué no se usó versión 8 en vez de versión 4.
    v4 significa bits aleatorios, pero en realidad esto no es tan aleatorio.
    v8 no impone restricciones sobre el significado de los bits.

    • No sé cuál sea la respuesta correcta, pero si la entropía es alta quizá podría verse como un PRNG con semilla.
      Como la meta de este esquema es justamente parecer aleatorio hacia afuera, hasta podría ser que v8 llamara más la atención.