21 điểm bởi GN⁺ 2025-09-19 | 1 bình luận | Chia sẻ qua WhatsApp
  • UUIDv47 cho phép lưu UUIDv7 có thể sắp xếp trong cơ sở dữ liệu, đồng thời cung cấp cho API bên ngoài giá trị trông giống UUIDv4
  • Chỉ trường dấu thời gian được che bằng XOR để bảo vệ thông tin thời gian của UUIDv7, còn các trường ngẫu nhiên khác được giữ nguyên
  • Che bằng khóa 128-bit dùng SipHash-2-4, giúp bảo vệ thông tin an toàn mà không làm tăng rủi ro lộ khóa
  • encode/decode mang tính xác định và có thể đảo ngược, đồng thời vẫn giữ được tính ngẫu nhiên nên rủi ro va chạm thấp
  • Kết quả benchmark cho thấy hiệu năng rất nhanh và cách tích hợp đơn giản, dễ dàng kết nối với cơ sở dữ liệu như PostgreSQL

Tổng quan dự án và ý nghĩa

  • UUIDv47 là thư viện C mã nguồn mở giúp đồng thời đạt được bảo vệ quyền riêng tưxử lý hiệu năng cao bằng cách lưu UUIDv7 thuận lợi cho sắp xếp và lập chỉ mục bên trong cơ sở dữ liệu, trong khi chỉ lộ ra giá trị trông giống UUIDv4 cho API và các hệ thống bên ngoài
  • So với các thuật toán chuyển đổi UUID khác, dự án có điểm mạnh khác biệt như ánh xạ có thể đảo ngược, tương thích RFC, tính bảo mật với đặc tính không thể khôi phục khóa, zero-deps và cấu trúc chỉ cần nhúng một file header đơn giản

Các đặc điểm chính

  • Header-only C (C89), có thể tích hợp đơn giản mà không cần phụ thuộc bên ngoài
  • Chỉ che bằng XOR trường dấu thời gian của UUIDv7 để ngăn lộ thông tin thời gian, còn các trường ngẫu nhiên khác không thay đổi
  • Dùng SipHash-2-4 có khóa để che, cho phép bảo vệ thông tin an toàn bằng khóa 128-bit
  • Quá trình encode/decode là xác định và hoàn toàn đảo ngược được (có thể khôi phục chính xác bản gốc)
  • Hỗ trợ ánh xạ nhanh giữa UUID dùng để lưu trong cơ sở dữ liệu (v7) và UUID dùng để lộ ra bên ngoài (v4)
  • Cung cấp nhiều ví dụ phong phú như mã kiểm thử và công cụ benchmark

Mục đích sử dụng và lợi ích

  • Có thể tận dụng UUIDv7 có thể sắp xếp để tối đa hóa index localityhiệu quả phân trang trong DB
  • Bên ngoài chỉ lộ ra mẫu trông giống UUIDv4, từ đó ngăn rò rỉ dấu thời gian và việc theo dõi
  • Dùng SipHash nên không thể khôi phục khóa, bảo đảm an toàn cho khóa bí mật
  • Xử lý bit phiên bản/biến thể tương thích RFC
  • Tốc độ hoạt động nhanh, hiệu quả ngay cả trong xử lý thời gian thựcmôi trường tạo dữ liệu số lượng lớn

Cấu trúc chính và nguyên lý hoạt động bên trong

Bố cục UUIDv7

  • ts_ms_be: dấu thời gian big-endian 48-bit
  • ver: nibble cao của byte thứ 6 (0x7=DB, 0x4=bên ngoài)
  • rand_a: giá trị ngẫu nhiên 12-bit
  • var: RFC variant (0b10)
  • rand_b: giá trị ngẫu nhiên 62-bit

Logic che và ánh xạ (Façade mapping)

  • Mã hóa: ts48 XOR mask48(R), đặt version=4
  • Giải mã: encTS XOR mask48(R), đặt version=7
  • Không thay đổi các trường ngẫu nhiên
  • Dùng trường ngẫu nhiên 10 byte làm đầu vào cho SipHash
  • Che bằng XOR có thể được đảo ngược ngay khi biết khóa

Mô hình bảo mật

  • Mục tiêu: khóa không bị lộ ngay cả khi có thể chọn đầu vào một cách có chủ đích
  • Triển khai: sử dụng SipHash-2-4, một hàm giả ngẫu nhiên có khóa (PRF)
  • Sử dụng khóa 128-bit, khuyến nghị dẫn xuất khóa qua HKDF hoặc tương tự
  • Khi xoay vòng khóa, nên không lưu trong UUID mà duy trì riêng một key ID nhỏ

API công khai (C)

  • uuidv47_encode_v4facade : chuyển v7→v4
  • uuidv47_decode_v4facade : khôi phục v4→v7
  • Cung cấp thêm các hàm liên quan đến đặt phiên bản, phân tích cú pháp và định dạng

Hiệu năng và benchmark

  • Ở phép toán che SipHash (10B), tốc độ đạt dưới 14ns/op, còn toàn bộ vòng encode+decode round-trip ở mức 33ns/op (theo Apple M1)
  • Bảo đảm xử lý nhanh ngay cả khi tạo và ánh xạ UUID với số lượng lớn
  • Hiệu năng tối ưu với tùy chọn -O3 -march=native

Tích hợp và mở rộng

  • Khuyến nghị xử lý encode/decode tại ranh giới API
  • Khi tích hợp với PostgreSQL, nên viết extension C
  • Khi sharding, có thể băm v4 façade bằng xxh3, SipHash, v.v.

Khác

  • Có thêm các bản port sang ngôn ngữ khác: Go (n2p5/uuid47) v.v.
  • Hàm băm khuyến nghị: xxHash không phải PRF nên có nguy cơ rò rỉ thông tin, khuyến nghị dùng SipHash

Giấy phép

  • Giấy phép MIT (Stateless Limited, 2025)

1 bình luận

 
GN⁺ 2025-09-19
Ý kiến trên Hacker News
  • Xin chào, mình là tác giả của uuidv47. Ý tưởng cơ bản là dùng UUIDv7 ở bên trong để đảm bảo khả năng lập chỉ mục và sắp xếp trong cơ sở dữ liệu, nhưng bên ngoài thì hiển thị giá trị trông như UUIDv4 để không làm lộ các mẫu thời gian cho client
    Cách hoạt động là XOR mask dấu thời gian 48-bit bằng luồng SipHash-2-4 được suy ra từ trường ngẫu nhiên của UUID
    Các bit ngẫu nhiên được giữ nguyên, phiên bản được chuyển từ 7 ở bên trong sang 4 ở bên ngoài, và giá trị biến thể RFC cũng được giữ nguyên
    Ánh xạ là injective: cấu trúc (ts, rand) → (encTS, rand)
    Giải mã dùng cách encTS ⊕ mask, nên có thể round-trip hoàn hảo
    Về mặt bảo mật, vì SipHash là PRF nên dù nhìn thấy giá trị đã được đóng gói từ bên ngoài thì khóa cũng không bị lộ
    Nếu sai khóa thì dấu thời gian cũng sẽ ra hoàn toàn khác
    Cũng có thể hỗ trợ xoay vòng khóa bằng cách quản lý key-ID ở bên ngoài
    Hiệu năng chỉ cỡ một lần SipHash cho 10 byte, cộng vài lần load/store 48-bit nên overhead ở mức nano giây, header-only C11, không phụ thuộc bên ngoài, không cần cấp phát
    Đã kiểm thử bằng vector tham chiếu SipHash, round-trip encode/decode, và kiểm thử tính bất biến của version/variant
    Mình muốn nghe góp ý

    • Tôi thích ý tưởng này
      UUID thường được tạo ở phía client, mà với cách này thì có vẻ không làm được
      Nếu vẫn nhận UUID do client tạo rồi trả lại phiên bản đã mask, chẳng phải sẽ có lỗ hổng vì ai đó có thể đưa ra hai UUID có ts khác nhau nhưng rand giống nhau sao?
      Cuối cùng thì có phải cách này chỉ phù hợp khi tự tạo trực tiếp UUIDv7 không?

    • Tôi có hai ý kiến

      1. Cách này làm mất khả năng để bên khác tận dụng thêm giá trị của UUID v7, nên với phía dùng API thì hơi đáng tiếc
      2. Nếu API bên ngoài và cách lưu trữ nội bộ khác nhau, thì lúc nào cũng phải đi qua bước chuyển đổi này nên việc quản lý sẽ hơi rắc rối hơn
        Tôi không chắc mức bất tiện đó có đáng để đánh đổi hay không
    • Mối lo lớn nhất là chất lượng entropy của các bit ngẫu nhiên
      UUIDv7 chú trọng tránh va chạm hơn, nên trọng tâm là xác suất trùng lặp chứ không phải tính khó đoán
      Vì vậy theo RFC, tính ngẫu nhiên không bị bắt buộc ở mức must mà chỉ được khuyến nghị ở mức should; có những triển khai dùng PRNG yếu hoặc bộ đếm, thậm chí còn nhét thêm dữ liệu đồng hồ vào chỗ bit ngẫu nhiên (xem RFC9562 s6.2 & s6.9)
      Do đó, nếu dùng trực tiếp rand_a, rand_b của v7 làm seed cho PRF thì có thể nguy hiểm hơn tưởng tượng nếu dữ liệu đến từ bên ngoài ranh giới tin cậy
      Ngay cả uuidv7() mới của PostgreSQL 18 cũng lấp đầy toàn bộ rand_a từ dấu thời gian độ chính xác cao, và điều đó vẫn không sai theo RFC
      Nếu nhìn các UUID được tạo trong lúc import hàng loạt, thì cuối cùng cách v7-to-v4 này cũng sẽ cho phép gom nhóm nên có thể làm lộ thông tin
      Với việc thu thập dữ liệu linh kiện động cơ thì có thể không sao, nhưng nếu là dữ liệu định danh gắn trực tiếp với con người thì cần cẩn trọng
      Kết lại, trừ khi tự bảo đảm được entropy đáng tin cậy, lược đồ này vẫn có thể làm rò rỉ thông tin về thời điểm, số sê-ri hoặc tương quan, nên nhất định phải tự kiểm tra mã nguồn triển khai v7

    • Tôi nghĩ đây không phải ý hay
      Trong PostgreSQL 18, tham số tùy chọn shift sẽ dịch chuyển dấu thời gian theo một khoảng cho trước
      https://www.postgresql.org/docs/18/functions-uuid.html

  • Vài năm trước tôi từng tự làm một lược đồ riêng, dùng ID số tăng tuần tự trong DB và bên ngoài thì lộ ra chuỗi ngẫu nhiên ngắn dài 4~20 ký tự
    Khi đó tôi dùng một instance tùy biến của họ mã Speck, và tôi thấy nó khá chắc chắn và cũng khá hợp lý
    Tôi đã hoàn thành nhưng chưa công bố vì dự án định dùng nó bị hoãn lại
    Năm nay hoặc năm sau tôi dự định sẽ công bố chính thức tài liệu đó
    Tôi cũng có ghi chú tổng hợp khá rõ cách triển khai, ưu và nhược điểm, ai quan tâm có thể tham khảo
    https://temp.chrismorgan.info/2025-09-17-tesid/

    • Trước đây tôi cũng từng thử dùng Speck để làm rối bigserial PKID, nhưng thiếu triển khai đa nền tảng, đặc biệt là hỗ trợ trong pgcrypto khá yếu nên
      tôi chọn base58(AES_K1(id{8} || HMAC_K2(id{8})[0..7]))
      Kết quả dài hơn, thường khoảng 22 ký tự, nhưng có thể triển khai ở gần như mọi môi trường và hiệu năng cũng đủ làm tôi hài lòng

    • Ý tưởng hay đấy
      Một khái niệm tương tự là sqids (tên cũ: hashids), cũng đáng tham khảo
      https://sqids.org/

  • Tôi từng có trải nghiệm tương tự: dùng hai cột, một uuid công khai và một bigint PK không lộ ra API (chuyện này là từ rất lâu trước khi có uuidv7)
    Tính tiện của uuid có kém đi một chút, nhưng nếu chỉ cần tách PK ra cho gọn thì ưu điểm là có thể dễ dàng gộp các bản dump DB khác nhau
    Ngay cả nếu tra cứu bằng hash thì có lẽ vẫn phải dùng hai cột, dù cũng có thể là tôi đang hiểu sai cách hoạt động của hash

    • Phép chuyển đổi có thể đảo ngược bằng khóa mật mã bí mật
      Từ giá trị uuidv4 trong request có thể đổi lại thành uuidv7 trong DB
  • Bản thân ý tưởng thì thú vị, nhưng tôi mong cơ sở dữ liệu tự hỗ trợ trực tiếp kiểu xử lý này
    Tức là có thể chuyển đổi qua lại giữa UUIDv7 và “UUIDv4”, và trong truy vấn cũng có thể phân biệt rõ để dùng hai định dạng đó

  • Dự án thực sự rất ngầu
    Tôi đã thử làm một bản triển khai bằng Go, tận dụng thư viện siphash của dchest
    https://github.com/n2p5/uuid47
    Tham khảo: https://github.com/dchest/siphash

  • Dự án khá thú vị, nhưng tôi muốn xem một ví dụ thực tế cho thấy rủi ro bị lộ phần thời gian trong uuid v7

    • Có thể dẫn đến việc lộ mẫu hành vi hoặc chuỗi hành động của người dùng trong những tình huống nhạy cảm

      • “Chồng cũ: nhìn vào userID trên trang hẹn hò của em là biết chắc em tạo tài khoản ở bữa tiệc của Tom rồi nhé?”
      • “Em bảo múi giờ của em là XYZ mà, sao log imageID (vốn gắn với thời điểm tạo) lúc nào cũng hiện 3 giờ sáng vậy?”
        Với các tin nhắn riêng lẻ hay giao dịch thời gian thực thì có thể không quan trọng, nhưng với tạo tài khoản người dùng hoặc dữ liệu dài hạn thì ai đó có thể lợi dụng để truy vết danh tính
    • Trước đây tôi từng brute force một phần UUID làm khóa AES trong một CTF
      Vì khóa được suy ra một phần từ nguồn thời gian, nên chỉ cần biết system time lúc tạo khóa là có thể tấn công được
      Một ví dụ đơn giản khác là dịch vụ chia sẻ tệp chỉ công khai cấu trúc website.com/GUID mà không công bố riêng thời điểm upload
      Nếu dùng UUIDv7 thì chỉ riêng nó thôi cũng cho phép ước đoán thời gian tải tệp lên
      Điều này có thể chưa chắc là mối đe dọa bảo mật lớn, nhưng vẫn là lộ thông tin ngoài ý muốn

    • Ví dụ hãy tưởng tượng một hệ thống lưu dữ liệu y tế
      Sau khi chụp MRI để phân tích, kết quả được tải lên gần như ngay lập tức, và giả sử bạn định xóa thông tin cá nhân
      Nhưng với dấu thời gian trong uuidv7, người ngoài có thể phân tích tương quan và kết luận rằng “ngày đó chỉ có một người chụp MRI, vậy đây là MRI của người đó”

  • Điều khó chịu nhất của uuidv7 là trong danh sách, con người rất khó tự nhìn bằng mắt để so sánh (diff)
    Nếu trong psql có một lớp hiển thị đưa các bit ngẫu nhiên lên trước mà vẫn giữ việc sắp xếp thực tế theo dấu thời gian thì UX sẽ cải thiện cực nhiều

    • Tôi chỉ tập cho mình thói quen nhìn phần cuối của UUID thôi

    • Bạn chỉ cần tự viết một hàm rồi dùng trong truy vấn
      Ví dụ, chuyển sang biểu diễn hex rồi đảo chuỗi, hoặc in ra dưới dạng base64 đảo ngược thì sẽ ngắn hơn và dễ phân biệt hơn

  • Cách này có vẻ rất ổn
    Nhưng việc làm quá lên về chuyện lộ dấu thời gian, hay cho rằng lộ ID tuần tự đồng nghĩa với tăng bề mặt tấn công và lộ thông tin kinh doanh, có vẻ giống lo xa hơn là vấn đề bảo mật thực sự
    Chỉ cần định kỳ cộng thêm một số ngẫu nhiên lớn vào giá trị int, bạn vẫn giữ được tính đơn điệu tăng (monotonic) mà người ngoài cũng khó đoán ra mẫu
    Cuối cùng tôi thấy cũng có phần hơi cường điệu khi tỏ ra lo lắng về rò rỉ thông tin quan trọng

    • Thứ bị lộ ở đây không phải thông tin kinh doanh mà là thông tin về client
      Bản thân lượng thông tin hệ thống làm rò rỉ có thể không nhiều ý nghĩa, nhưng nếu quan sát trên quy mô lớn hoặc theo chuỗi thời gian thì có thể suy luận thêm dữ liệu khác
      Ví dụ như bài nói SpiegelMining của David Kriesel, chỉ cần thu thập ngày bài báo và tác giả cũng có thể rút ra được ai nghỉ phép khi nào
      Nếu đem so sánh dữ liệu của nhiều tác giả, thậm chí chuyện hẹn hò trong nội bộ cũng có thể bị lộ hết
  • Tôi thắc mắc tại sao không dùng khóa mã hóa khác nhau cho mỗi phiên, rồi chỉ lộ ra ID đã được mã hóa ở bên ngoài
    Như vậy DB chỉ cần dùng ID tuần tự đơn giản là đủ phải không?

    • Để giải mã các bit dấu thời gian bị ẩn trong token thì phải biết cần dùng khóa nào
      Nếu cứ định kỳ đổi khóa thì việc quản lý khóa sẽ cực kỳ phức tạp, và cũng sẽ phát sinh vấn đề làm sao biết lúc nào phải dùng đúng khóa nào
  • Tôi thắc mắc vì sao không dùng version 8 thay vì version 4
    v4 mang nghĩa là bit ngẫu nhiên, trong khi ở đây thực tế lại không hẳn ngẫu nhiên đến vậy
    v8 thì không ràng buộc ý nghĩa bit

    • Tôi cũng không biết đáp án chắc chắn, nhưng nếu entropy đủ cao thì có thể xem nó như PRNG có seed
      Mục đích của cách này vốn là để nó trông ngẫu nhiên từ bên ngoài, nên có khi dùng v8 lại càng gây chú ý hơn vậy