Skip to content

Commit 22b74b5

Browse files
committed
Add uuidv7_encrypt() and uuidv7_decrypt() functions
These functions can be used to prevent leaking the timestamp from UUID-v7 values. The UUID-v7 parts holding the timestamp are encrypted and a UUID-v4 or v8 is produced from the result. The encrypted values are not meant to be stored and looked up; they are simply different representations of the inputs. They're meant to be passed to uuidv7_decrypt() to find back the original values.
1 parent 652deb3 commit 22b74b5

5 files changed

Lines changed: 154 additions & 4 deletions

File tree

LICENSE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Copyright and License
22

3-
Copyright (c) 2024, Daniel Vérité
3+
Copyright (c) 2024-2025, Daniel Vérité
44

55
Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies.
66

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
EXTENSION = uuidv7-sql
2-
EXTVERSION = 1.0
2+
EXTVERSION = 1.1
33
PG_CONFIG = pg_config
44

55
DATA = $(wildcard sql/*.sql)

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Pure SQL functions to use UUIDs v7 in PostgreSQL.
33

44
The functions are packaged as an extension ("uuidv7-sql")
55
for convenience, but they may also be created individually
6-
by sourcing all or parts of the [creation script](sql/uuidv7-sql--1.0.sql).
6+
by sourcing all or parts of the [creation scripts](sql/).
77

88
## Extension installation
99
The Makefile uses the [PGXS infrastructure](https://www.postgresql.org/docs/current/static/extend-pgxs.html)
@@ -43,3 +43,9 @@ Extract the timestamp with millisecond precision from the given UUID v7 value.
4343

4444
### `uuidv7_boundary(timestamptz) -> uuid`
4545
Generate a non-random uuidv7 with the given timestamp (first 48 bits) and all random bits to 0. As the smallest possible uuidv7 for that timestamp, it may be used as a boundary for tables partitioned by ranges of UUID.
46+
47+
### `uuidv7_encrypt(input uuid, bytea crypt_key, [uuid_ver int]) -> uuid`
48+
Transform a UUID-v7 value into an equivalent UUID-v4 or UUID-v8 (passing 4 or 8 in `uuid_ver`) that does not leak the timestamp. The bit fields `unix_ts_ms` + `rand_a` + 4 more bits (total: 64 bits) from the UUID-v7 value are encrypted with an [XTEA cipher](https://en.wikipedia.org/wiki/XTEA). The 16-byte `crypt_key` parameter is the encryption key.
49+
50+
### `uuidv7_decrypt(input uuid, bytea crypt_key) -> uuid`
51+
Decrypt a UUID (either v4 or v8) produced by `uuidv7_encrypt()` with the same `crypt_key`.

sql/uuidv7-sql--1.0--1.1.sql

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
-- \echo Use "ALTER EXTENSION "uuidv7-sql" UPDATE TO '1.1'" to load this file. \quit
2+
3+
/*
4+
Transform a UUID-v7 value into an equivalent UUID-v4 or UUID-v8 (passing
5+
4 or 8 in "uuid_ver"), that does not reveal the timestamp.
6+
The fields unix_ts_ms + rand_a + 4 more bits (to reach 64 bits) are encrypted
7+
with an XTEA cipher. The 16-byte "crypt_key" parameter is the encryption key.
8+
*/
9+
CREATE FUNCTION uuidv7_encrypt(input uuid, crypt_key bytea, uuid_ver int default 4)
10+
RETURNS uuid
11+
SET bytea_output to 'hex'
12+
AS $$
13+
DECLARE
14+
key128 int8[4];
15+
uuid_parts int8;
16+
v0 int8;
17+
v1 int8;
18+
f_sum int8:=0;
19+
uuid_bits bit(128);
20+
r1 int8;
21+
r2 int8;
22+
rounds constant int:=32;
23+
BEGIN
24+
IF octet_length(crypt_key)<>16 THEN
25+
raise exception 'Encryption key must be 16 bytes long';
26+
END IF;
27+
/* transfer encryption key into 4x32 bits */
28+
FOR i IN 1..4 LOOP
29+
key128[i] = right(substring(crypt_key from 1+(i-1)*4 for 4)::text,-1)::bit(32)::int8;
30+
END LOOP;
31+
32+
uuid_bits = right(uuid_send(input)::text, -1)::bit(128); -- convert \x... to x...
33+
34+
/* Extract and concatenate the UUID-v7 bit fields to encrypt:
35+
unix_t + 4 arbitrary bits from rand_b + rand_a */
36+
uuid_parts := (substring(uuid_bits from 1 for 48) ||
37+
substring(uuid_bits from 93 for 4) ||
38+
substring(uuid_bits from 53 for 12)
39+
)::bit(64)::int8;
40+
41+
/* encrypt this 64-bit number with an XTEA Feistel network */
42+
v0 := (uuid_parts>>32)&4294967295;
43+
v1 := uuid_parts&4294967295;
44+
45+
FOR i in 1..rounds LOOP
46+
v0 := (v0 + ((
47+
((v1<<4)&4294967295 # (v1>>5))
48+
+ v1)&4294967295
49+
#
50+
(f_sum + key128[1+(f_sum&3)])&4294967295
51+
))&4294967295;
52+
f_sum := (f_sum + 2654435769) & 4294967295;
53+
v1 := (v1 + ((
54+
((v0<<4)&4294967295 # (v0>>5))
55+
+ v0)&4294967295
56+
#
57+
(f_sum + key128[1+((f_sum>>11)&3)])&4294967295
58+
))&4294967295;
59+
END LOOP; /* encrypted value at the end = v0<<32 + v1 */
60+
61+
/* Place the bit fields into their final positions */
62+
r1 := (v0::bit(32) || (v1>>16)::bit(16) || uuid_ver::bit(4) || (v1&4095)::bit(12))::bit(64)::int8;
63+
r2 := (substring(uuid_bits from 65 for 28) || substring(v1::bit(32) from 17 for 4)
64+
|| substring(uuid_bits from 97 for 32))::bit(64)::int8;
65+
66+
RETURN encode(int8send(r1) || int8send(r2), 'hex')::uuid;
67+
END
68+
$$ LANGUAGE plpgsql strict immutable;
69+
70+
COMMENT ON FUNCTION uuidv7_encrypt(uuid, bytea, int)
71+
IS 'Encrypt a UUID-v7 into an equivalent UUID-v4 or UUID-v8 with an XTEA block cipher';
72+
73+
74+
75+
/*
76+
Transform a UUID encrypted by encrypt_uuidv7() back to the original
77+
UUID-v7 value.
78+
"crypt_key" must be the same 16-byte key that was used to encrypt.
79+
*/
80+
CREATE FUNCTION uuidv7_decrypt(input uuid, crypt_key bytea)
81+
RETURNS uuid
82+
SET bytea_output to 'hex'
83+
AS $$
84+
DECLARE
85+
key128 int8[4];
86+
uuid_parts int8;
87+
v0 int8;
88+
v1 int8;
89+
uuid_bits bit(128);
90+
r1 int8;
91+
r2 int8;
92+
rounds constant int:=32;
93+
f_sum int8 := (2654435769 * rounds)&4294967295;
94+
BEGIN
95+
IF octet_length(crypt_key)<>16 THEN
96+
raise exception 'Encryption key must be 16 bytes long';
97+
END IF;
98+
/* transfer encryption key into 4x32 bits */
99+
FOR i IN 1..4 LOOP
100+
key128[i] = right(substring(crypt_key from 1+(i-1)*4 for 4)::text,-1)::bit(32)::int8;
101+
END LOOP;
102+
103+
uuid_bits = right(uuid_send(input)::text, -1)::bit(128);
104+
105+
/* Extract and concatenate the UUID-v4 bit fields to decrypt:
106+
random_a + 4 bits from random_c + random_b */
107+
uuid_parts := (substring(uuid_bits from 1 for 48) ||
108+
substring(uuid_bits from 93 for 4) ||
109+
substring(uuid_bits from 53 for 12)
110+
)::bit(64)::int8;
111+
112+
/* decrypt this 64-bit number with an XTEA Feistel network */
113+
v0 := (uuid_parts>>32)&4294967295;
114+
v1 := uuid_parts&4294967295;
115+
FOR i in 1..32 LOOP
116+
v1 := (v1 - ((
117+
((v0<<4)&4294967295 # (v0>>5))
118+
+ v0)&4294967295
119+
#
120+
(f_sum + key128[1+((f_sum>>11)&3)])&4294967295
121+
))&4294967295;
122+
123+
f_sum := (f_sum - 2654435769)& 4294967295;
124+
125+
v0 := (v0 - ((
126+
((v1<<4)&4294967295 # (v1>>5))
127+
+ v1)&4294967295
128+
#
129+
(f_sum + key128[1+(f_sum&3)])&4294967295
130+
))&4294967295;
131+
END LOOP;
132+
133+
/* Place the bit fields into their final positions, and set version to 7 */
134+
r1 := (v0::bit(32) || (v1>>16)::bit(16) || '0111'::bit(4) || (v1&4095)::bit(12))::bit(64)::int8;
135+
r2 := (substring(uuid_bits from 65 for 28) || substring(v1::bit(32) from 17 for 4)
136+
|| substring(uuid_bits from 97 for 32))::bit(64)::int8;
137+
138+
RETURN encode(int8send(r1) || int8send(r2), 'hex')::uuid;
139+
END
140+
$$ LANGUAGE plpgsql strict immutable;
141+
142+
COMMENT ON FUNCTION uuidv7_decrypt(uuid, bytea)
143+
IS 'Decrypt a UUID produced by uuidv7_encrypt()'
144+

uuidv7-sql.control

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# uuidv7-sql extension
22
comment = 'Pure SQL functions to handle UUIDs version 7 (see RFC 9562)'
3-
default_version = '1.0'
3+
default_version = '1.1'
44
relocatable = true
55
superuser = false

0 commit comments

Comments
 (0)