Shipped a timing leak via early-exit memcmp + secret branch. Flatline is a single-header toolbox for constant-time C (CT compare/select/lookup/div) guided by B.I.D.—no Branch on secrets, no secret Index, no variable-latency ops. DUDECT tests; feedback on LTO/autovec welcome
Hi, I’m the author of uuidv47. The idea is simple: keep UUIDv7 internally for database indexing and sortability, but emit UUIDv4-looking façades externally so clients don’t see timing patterns.
How it works: the 48-bit timestamp is XOR-masked with a keyed SipHash-2-4 stream derived from the UUID’s random field. The random bits are preserved, the version flips between 7 (inside) and 4 (outside), and the RFC variant is kept. The mapping is injective: (ts, rand) → (encTS, rand). Decode is just encTS ⊕ mask, so round-trip is exact.
Security: SipHash is a PRF, so observing façades doesn’t leak the key. Wrong key = wrong timestamp. Rotation can be done with a key-ID outside the UUID.
Performance: one SipHash over 10 bytes + a couple of 48-bit loads/stores. Nanosecond overhead, header-only C11, no deps, allocation-free.
Tests: SipHash reference vectors, round-trip encode/decode, and version/variant invariants.
UUIDs are often generated client-side. Am I right in thinking that this isn’t possible with this approach? Even if you let clients give you UUIDs and they gave them back the masked versions, wouldn't you be vulnerable to a client providing two UUIDs with different ts and the same rand? So this is only designed for when you are generating the UUIDv7s yourself?
Any version of UUID except v4 on the client side would be a mistake- as you are relying on it to provide extra information such as a timestamp which might be manipulated.
Of course, UUIDv4 on the client side is not without risk either- needing to validate uniqueness and not re-use of some other ID. For the UUIDv7 on client side- you could add some sanity validation- but really I think it’s best avoided.
There’s a whole bunch of use-cases where the ability for a user to mess with the timestamp is not a problem. Who cares if a user screws up the ordering of items in a collection only they see? But if you can attack the private key by generating many different ciphertexts for the same rand, that might let you defeat the purpose of this masking.
I concede my above comment was speaking in generalities and has plenty of exceptions. However, I do prefer to fallback on safe defaults, and letting the client choose the UUIDv7 could certainly have some unsafe consequences.
creating your uuids client side has a risk of clients toying with the uuids.
creating them server-side risks having a network error cause a client to have requested a resource be created without receiving its id due to a network error before receiving the response, risking double submissions and generally bad recovery options from the UI.
if you need users to provide uuids for consistent network operations, you can have an endpoint responsible for generating signed uuids that expire after a short interval, thereby controlling uuid-time drift (must be used within 1-5 minutes, perhaps), ensuring the client can't forge them to mess with your backend, and still provide a nice and stable client-side-uuid system.
for the uuidv47 thing, you would apply their XOR trick prior to sending the UUID to the user. you presumably just reverse the XOR trick to get the UUIDv7 back from the UUIDv4 you passed them.
1. You implicitly take away someone else's hypothetical benefit of leveraging UUID v7, which is disappointing for any consumer of your API.
2. By storing the UUIDs differently on your API service from internally, you're going to make your life just a tiny bit harder because now you have to go through this indirection of conversion, and I'm not sure if this is worth it.
> 1. Unless API explicitly guarantees that property, relying on that is bad idea. I wouldn't.
With a sufficient number of users of an API,
it does not matter what you promise in the contract:
all observable behaviors of your system
will be depended on by somebody.
Sure, but that's not really the point is it? If you get a UUID you can store it as a UUID. If the UUID happens to come around as a v7 you get some better behavior in your database, and if it does not, then it does not but there is nothing you can do about.
You're missing the point here. You can always go from ordered to randomness. You cannot go from randomness to ordered. So by intentionally removing the useful properties of UUIDv7, you're taking away some external API consumers' hypothetical possibility to leverage benefits. If I know (as an API consumer) that I have a database that for whatever reason prefers evenly distributed primary keys or something similar, I can always accomplish that by hashing. I just can never go the other way.
Never use someone else's synthetic key as your primary key. If you want ordered keys, even if the API is giving out sequential integers, you should still use your own sequential IDs.
I take your point, but I think your hypothetical is a wonderful example of Hyrum's Law. And for that reason, if I was going to go to the trouble of mapping my internal v7 uuids into something more random for public consumption, then I'd be sure generate something that doesn't look like a uuid at all so nobody gets any funny ideas about what they can do with it.
I'm not saying anything about better or worse. I'm saying that UUID v4 by definition has high entropy and UUID v7 does not. You can always go from low to high entropy, but not the other way around.
Not just a small inconvenience—because there's no human readable way to tell the difference between v4 and v7 IDs, you have to guess and check whether or not the ID your server process is logging is a pre-conversion or post-conversion ID
If you must not leak timestamps then you also cannot really have timestamp ordering internally because you will happen to start leak that out in other ways through collection based endpoints.
Not necessarily. For instance, in situations where unprivileged users can only see single items but privileged users can see collections. But yeah, time-ordering leaks information to people who can see the collection.
This scheme potentially leaks timestamp, serialisation, and record-correlation data because the specification of UUIDv7 allows for partial timestamps and incrementing counters in the so-called random bits, which are passed through undisturbed.
So it is not generally fit for that purpose either.
Those seem like standard needs for any kind of CRUD app, so I would call this approach pretty useful. Currently I do something similar by keeping a private primary uuidv7 key with a btree index (a sortable index), and a separate public uuidv4 with a hash index (a lookup index), which is a workable but annoying arrangement. This solution achieves the same effect and is simpler.
My biggest concern is the entropic quality of the random bits, since the design of UUIDv7 is fundamentally more concerned with collisions than predictability; consequently, although the standard says SHOULD for their nonguessability it isn't a MUST, and leaves room for implementations that use a weak PRNG, or that increment a counter, or even place additional clock data in the apparently random bits (ref. RFC9562 s6.2 & s6.9).
So there's definitely some gotchas with relying on rand_a and rand_b in UUIDv7 for seeding a PRF, and when ingesting data from devices outside of your trust boundary (as may be the case with high-volume telemetry), even if you wrote the code they basically can't be trusted for this purpose, and if those bits are undisturbed in the output it's certainly a problem if the idea was to obfuscate serialisation, timing, or correlation.
Even generations we might assume are safe may not be completely safe; for example, the new uuidv7() in PostgreSQL 18 fills rand_a entirely from the high precision part of the timestamp, and this is RFC compliant. So if an import routine generates a big batch of such UUIDs, this v7-to-v4 scheme discloses output bits that can be used to relate individual records as part of the same group. That might be fine for data points pertaining to a vehicle engine. It might not be fine for identifiers that relate to people.
So, since not all UUIDv7 is created alike, I'd add a strong caveat: unless generating the rand_a and rand_b bits entirely oneself with a high degree of confidence in their nonguessibility, then this scheme may still leak information regarding timing, sequence, or correlation of records, and you will have to read the source code of your UUIDv7 implementation to know for sure.