V0/V1 encryption
View this section in the spec ↗Encrypts a TNIDv0 (variant 0) into a TNIDv1 (variant 1) that, without the key, is indistinguishable from a random TNIDv1. Reversible.
Use case
TNIDv0 exposes creation time, as UUIDv7 does: when, in what order, how fast. Common setup: TNIDv0 in the database for index performance, TNIDv1 to clients.
Payload bits
nnnn.nnnn.nnnn.nnnn.nnnn.LLLL.LLLL.LLLL-
LLLL.LLLL.LLLL.LLLL-
vvvv.MMMM.MMMM.MMMM-
rrtt.RRRR.RRRR.RRRR-
RRRR.RRRR.RRRR.RRRR.RRRR.RRRR.RRRR.RRRR.RRRR.RRRR.RRRR.RRRR| Field | Bits | u128 positions (LSB = 0) | Changed? |
|---|---|---|---|
n name | 20 | 108–127 | No |
L payload, upper (the spec's "left") | 28 | 80–107 | Encrypted |
v UUID version | 4 | 76–79 | No |
M payload, middle | 12 | 64–75 | Encrypted |
r UUID variant | 2 | 62–63 | No |
t TNID variant | 2 | 60–61 | Set to the output variant |
R payload, lower (the spec's "right") | 60 | 0–59 | Encrypted |
Payload extraction
Upper ‖ middle ‖ lower as one 100-bit value, read as 25 hex digits, most significant first:
LLLL.LLLL.LLLL.LLLL.LLLL.LLLL.LLLL.MMMM.MMMM.MMMM.RRRR.RRRR.RRRR.
RRRR.RRRR.RRRR.RRRR.RRRR.RRRR.RRRR.RRRR.RRRR.RRRR.RRRR.RRRRAlgorithm
FF1 format-preserving encryption (NIST SP 800-38G):
| Parameter | Value |
|---|---|
| Block cipher | AES-128 |
| Key | 128 bits, big-endian byte order |
| Radix | 16 (hexadecimal) |
| Input | 25 digits, most significant first |
| Tweak | Empty (0 bytes) |
| Rounds | 10 |
Known V0/V1 pairs don't lower key recovery below 2¹²⁸, however many: FF1 with AES-128 reduces to AES. Small-domain FPE attacks don't apply (2¹⁰⁰ domain, no tweak to vary); operational advice: If an unencrypted TNID leaks.
Encrypt (V0 → V1)
- Extract the three payload runs.
- Join them: upper ‖ middle ‖ lower, 100 bits.
- Convert to 25 hex digits, most significant first.
- FF1-encrypt.
- Convert the 25 digits back to 100 bits.
- Split back into the three runs.
- Set the TNID variant to
0b01.
Already variant 1: SHOULD return the input unchanged.
Decrypt (V1 → V0)
Same steps; step 4 FF1-decrypts, step 7 sets 0b00. Already variant 0: SHOULD return the input unchanged.
Decryption is unauthenticated: FF1 permutes all 2¹⁰⁰ payloads and there is no MAC, so a wrong key, or a TNIDv1 that was never encrypted, still yields a TNIDv0, with no error.
Interoperability
Output MUST be byte-identical across implementations for identical input and key. Check against the Rust reference implementation and the encryption test vectors.