- 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ư và 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 locality và hiệ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ực và mô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
Ý 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ảoVề 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ó
tskhác nhau nhưngrandgiố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
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_bcủ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ậyNgay cả
uuidv7()mới của PostgreSQL 18 cũng lấp đầy toàn bộrand_atừ dấu thời gian độ chính xác cao, và điều đó vẫn không sai theo RFCNế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
shiftsẽ dịch chuyển dấu thời gian theo một khoảng cho trướchttps://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
bigserialPKID, nhưng thiếu triển khai đa nền tảng, đặc biệt là hỗ trợ trongpgcryptokhá yếu nêntô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
uuidcông khai và mộtbigintPK 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
uuidcó 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 nhauNgay 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
Từ giá trị
uuidv4trong request có thể đổi lại thànhuuidv7trong DBBả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
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/GUIDmà không công bố riêng thời điểm uploadNế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
psqlcó 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ềuTô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ẫuCuố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
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?
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
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