mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
Probe the oldest stored values before recording an encryption key
The boot key check recorded whatever key was configured whenever its probe found nothing to try. It only read string columns, so an install whose ciphertext sat in Bytes columns (notes, documents, ECG, symptoms) read as empty; a probe that ran out of time did the same; and rows written by a process that served under a wrong key could open on the next boot and get that key recorded as the right one. The probe now reads every registered column, string and Bytes, in all four stored layouts, and tries the oldest values first, ordered by the row's updatedAt (else createdAt, else primary key). A key is recorded only when the oldest value that opens is preceded by at most one that does not, or when no column holds a single value under the key id. A time-out, an unreadable table, oversized values or mixed results are inconclusive: the process serves, records nothing and checks again at the next start. When the first insert loses a race, the stored record is read back and must open under this process's key, else the outcome is a mismatch. A rotation that walked every column without errors removes the record of each previous key id that no longer holds any value. The refusal log block and the self-hosting pages explain how to reset the check for a key id.
This commit is contained in:
1 parent
3a99c8cfee
commit
2c295bbea8
9 files changed
+1142
-199
No files matched your search
@@ -17,6 +17,10 @@
|
||||
* non-zero, because an operator reads a zero here as permission to drop the
|
||||
* previous key, and "nothing left" and "never looked" must not print alike.
|
||||
*
|
||||
* After a complete, error-free run it checks every other configured key id
|
||||
* for remaining values and removes the boot key check's record (canary) of
|
||||
* each id that holds none, naming the columns of each id that still does.
|
||||
*
|
||||
* Usage:
|
||||
* ENCRYPTION_KEYS='{"v1":"<old>","v2":"<new>"}' \
|
||||
* ENCRYPTION_ACTIVE_KEY_ID=v2 \
|
||||
@@ -27,6 +31,7 @@ import { Buffer } from "node:buffer";
|
||||
import { PrismaClient } from "@/generated/prisma/client";
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import { decrypt, encrypt, extractKeyId, getActiveKeyId } from "@/lib/crypto";
|
||||
import { retireCanariesWithoutData } from "@/lib/crypto/canary";
|
||||
import {
|
||||
rotateColumn,
|
||||
type CorpusClient,
|
||||
@@ -1034,6 +1039,30 @@ async function main() {
|
||||
`drop the previous key:\n ${notWalked.join("\n ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
// ───── Key-check records of retired key ids ─────
|
||||
// The boot key check keeps one canary per configured key id. Once no
|
||||
// registered column holds a single value under an id, that id's key can
|
||||
// leave ENCRYPTION_KEYS, and its canary goes with it: left behind, it would
|
||||
// refuse a later, different key configured under the same id. Only after a
|
||||
// complete, error-free run; the existence check walks every column itself
|
||||
// rather than trusting the counts above.
|
||||
if (notWalked.length === 0 && totalErrors === 0) {
|
||||
const { removed, remaining } = await retireCanariesWithoutData(prisma);
|
||||
for (const [keyId, columns] of Object.entries(remaining)) {
|
||||
console.log(
|
||||
`\nKey id '${keyId}': values remain in ${columns.length} column(s); ` +
|
||||
`keep its key:\n ${columns.join("\n ")}`,
|
||||
);
|
||||
}
|
||||
for (const keyId of removed) {
|
||||
console.log(
|
||||
`\nKey id '${keyId}': no values remain. Its key-check record was ` +
|
||||
`removed; its key can now leave ENCRYPTION_KEYS.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.$disconnect();
|
||||
if (notWalked.length > 0) process.exit(4);
|
||||
process.exit(totalErrors > 0 ? 3 : 0);
|
||||
|
||||
Reference in new issue
Block a user