From d7d26f18ca87733acf2308937f08f63b713ca0c0 Mon Sep 17 00:00:00 2001 From: "Joshua (D) Drake" <136637981+ChronicallyJD@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:37:57 -0600 Subject: [PATCH] docs: document the unlogged-table fork tweak limitation (M5) An unlogged relation's init fork is reset onto the main fork by PostgreSQL with a raw copy_file() in reinit.c, outside the storage hooks. For that copy of encrypted bytes to decrypt as the main fork, CalcBlockIv encrypts the init fork with the main fork's XTS tweak (INIT_FORKNUM aliases MAIN_FORKNUM). The tradeoff is that the two forks share the tweak at each block, so an attacker with the data files can tell which main-fork blocks are byte-identical to the init-fork template. This is required by PostgreSQL's own unlogged-relation reset and is a confidentiality-only limitation of XTS, so it is documented in the threat model rather than changed. Also expands the CalcBlockIv comment with the security rationale. Co-Authored-By: Claude Opus 4.8 (1M context) --- documentation/docs/concepts/threat-model.md | 10 ++++++++++ src/encryption/enc_tde.c | 13 ++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/documentation/docs/concepts/threat-model.md b/documentation/docs/concepts/threat-model.md index 0ca22c0..ba2f430 100644 --- a/documentation/docs/concepts/threat-model.md +++ b/documentation/docs/concepts/threat-model.md @@ -52,6 +52,15 @@ running server: confidentiality, not authentication. `open_pg_tde` does not currently detect deliberate modification of ciphertext on disk. Authenticated page encryption is a separate, larger initiative; see the design note on authenticated pages. +- **Unlogged table forks.** An unlogged table keeps an init fork that + PostgreSQL copies over the main fork to reset the table after a crash. That + reset is a raw file copy of encrypted bytes performed by the server outside + the storage hooks, so the init fork and the main fork must share the same XTS + tweak at each block position for the copy to decrypt afterward. As a result, + an attacker with the data files can tell which main-fork blocks are + byte-identical to the init-fork template. This is limited to unlogged tables, + reveals only block equality and not the contents of either fork, and does not + affect regular (logged) tables. - **Column-level protection from the server.** All encryption is at the storage layer, so the server necessarily decrypts data to operate on it. `open_pg_tde` does not provide client-side or column-level encryption that @@ -84,4 +93,5 @@ running server: | Privileged user on a running server | No | | Catalog metadata and statistics | No | | Tamper detection (integrity) | No | +| Block equality between an unlogged table's init and main forks | No | | Encryption key exposure via the catalog | Not applicable (keys are not in the catalog) | diff --git a/src/encryption/enc_tde.c b/src/encryption/enc_tde.c index 4bdf4a0..6feee2a 100644 --- a/src/encryption/enc_tde.c +++ b/src/encryption/enc_tde.c @@ -188,7 +188,18 @@ CalcBlockIv(ForkNumber forknum, BlockNumber bn, const unsigned char *base_iv, un { memset(iv, 0, TDE_BLOCK_IV_LEN); - /* The init fork is copied to the main fork so we must use the same IV */ + /* + * The init fork is copied to the main fork so we must use the same IV. + * + * PostgreSQL resets an unlogged relation after a crash by raw-copying the + * init fork file over the main fork (copy_file() in reinit.c), which the + * storage hooks never see. For that copy of already-encrypted bytes to + * decrypt as the main fork, the init fork must be encrypted with the main + * fork's tweak. This is intentional. The tradeoff is that the two forks + * share the XTS tweak at each block, so their ciphertext reveals which + * blocks are byte-identical; that limitation is documented in the threat + * model (unlogged table forks). + */ iv[7] = forknum == INIT_FORKNUM ? MAIN_FORKNUM : forknum; iv[12] = bn >> 24;