Implementing

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
FieldBitsu128 positions (LSB = 0)Changed?
n name20108–127No
L payload, upper (the spec's "left")2880–107Encrypted
v UUID version476–79No
M payload, middle1264–75Encrypted
r UUID variant262–63No
t TNID variant260–61Set to the output variant
R payload, lower (the spec's "right")600–59Encrypted

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.RRRR

Algorithm

FF1 format-preserving encryption (NIST SP 800-38G):

ParameterValue
Block cipherAES-128
Key128 bits, big-endian byte order
Radix16 (hexadecimal)
Input25 digits, most significant first
TweakEmpty (0 bytes)
Rounds10

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)

  1. Extract the three payload runs.
  2. Join them: upper ‖ middle ‖ lower, 100 bits.
  3. Convert to 25 hex digits, most significant first.
  4. FF1-encrypt.
  5. Convert the 25 digits back to 100 bits.
  6. Split back into the three runs.
  7. 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.