fix(encryption): sample several stored values before declaring a key mismatch

The boot check decided on the first value it found under a key id, so
one damaged or foreign-sealed row could take a correctly keyed server
down with 503 on every request. The probe now samples up to five
values, one per column first. One value that opens clears the key; a
mismatch needs at least two values and none opening. A single value
that does not open is inconclusive: the server keeps serving, writes no
canary, logs a warning and probes again at the next start.

Integration setup truncates encryption_key_canaries between tests.
This commit is contained in:
Marc-André Bombeck committed 2026-10-03 18:27:28 +02:00
1 parent ced17569bf
commit b04b1f2995
5 files changed
+215 -37

No files matched your search

@@ -170,6 +170,7 @@ describe("boot encryption key check (real Postgres)", () => {
username: "existing",
email: "existing@example.test",
codexAccessTokenEncrypted: encrypt("a stored token"),
codexRefreshTokenEncrypted: encrypt("another stored token"),
},
});
@@ -186,6 +187,42 @@ describe("boot encryption key check (real Postgres)", () => {
});
});
describe("boot key check probe over existing data (real Postgres)", () => {
it("one damaged row among good ones still lets the right key through", async () => {
const prisma = getPrismaClient();
await prisma.user.create({
data: {
username: "damaged",
email: "damaged@example.test",
codexAccessTokenEncrypted:
"v1." + Buffer.alloc(40, 7).toString("base64"),
codexRefreshTokenEncrypted: encrypt("a good token"),
},
});
expect(await runBootKeyCheck(prisma)).toBe(false);
expect(isKeyMismatch()).toBe(false);
expect(await prisma.encryptionKeyCanary.count()).toBe(1);
});
it("a single value that does not open neither refuses nor writes a canary", async () => {
const prisma = getPrismaClient();
await prisma.user.create({
data: {
username: "single",
email: "single@example.test",
codexAccessTokenEncrypted: encrypt("the only token"),
},
});
useKey(KEY_B);
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
expect(await runBootKeyCheck(prisma)).toBe(false);
expect(warn.mock.calls[0]?.[0]).toContain("inconclusive");
warn.mockRestore();
expect(isKeyMismatch()).toBe(false);
expect(await prisma.encryptionKeyCanary.count()).toBe(0);
});
});
describe("encryption key backup step (real Postgres)", () => {
it("is due, refuses a stale confirmation, records the right one, and is due again after a re-key", async () => {
const prisma = getPrismaClient();
+3
View File
@@ -51,6 +51,9 @@ export async function truncateAllTables(client: PrismaClient): Promise<void> {
"coach_usage",
"data_backups",
"devices",
// Instance-wide, keyed by encryption key id: a canary left by one test
// would make the next test's boot check verify instead of write.
"encryption_key_canaries",
"host_metrics",
"idempotency_keys",
"integration_statuses",