mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
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.
66 lines
2.4 KiB
TypeScript
66 lines
2.4 KiB
TypeScript
/**
|
|
* Runs the encryption key check once per process at boot and records the
|
|
* verdict for the request path (`./key-mismatch-state.ts`).
|
|
*
|
|
* Returns true when the process must refuse: the caller then starts neither
|
|
* the queue producer nor the worker, because a process holding the wrong key
|
|
* must never write. The process keeps running so `/api/health` and the
|
|
* explanation page can say why; a crash loop would hide the reason in a log
|
|
* that the people most likely to hit this (a NAS app catalog install) never
|
|
* open.
|
|
*/
|
|
import {
|
|
checkEncryptionKeyCanaries,
|
|
keyMismatchLogBlock,
|
|
type CanaryClient,
|
|
type InconclusiveReason,
|
|
} from "@/lib/crypto/canary";
|
|
import { setKeyMismatchState } from "./key-mismatch-state";
|
|
|
|
const INCONCLUSIVE_EXPLANATION: Record<InconclusiveReason, string> = {
|
|
"single-value": "the only stored value found under that id did not open.",
|
|
mixed:
|
|
"the oldest stored values under that id do not open, newer ones do. " +
|
|
"That is what a wrong key that served for a while leaves behind; check " +
|
|
"that this is the key the database was first written with.",
|
|
unsampled:
|
|
"the values stored under that id are too large to probe at start-up.",
|
|
incomplete:
|
|
"the probe of the stored data did not finish in time or a table could " +
|
|
"not be read.",
|
|
};
|
|
|
|
export async function runBootKeyCheck(client: CanaryClient): Promise<boolean> {
|
|
const outcome = await checkEncryptionKeyCanaries(client);
|
|
if (outcome.state === "mismatch") {
|
|
setKeyMismatchState({
|
|
keyIds: outcome.keyIds,
|
|
detectedAt: new Date().toISOString(),
|
|
});
|
|
console.error(keyMismatchLogBlock(outcome.keyIds));
|
|
return true;
|
|
}
|
|
setKeyMismatchState(null);
|
|
if (outcome.state === "error") {
|
|
// The check could not run. The loaders and the database each have their
|
|
// own loud signal for that; serving stays on.
|
|
console.warn(
|
|
`[boot] Encryption key check skipped: ${outcome.message.slice(0, 300)}`,
|
|
);
|
|
} else {
|
|
for (const { keyId, reason } of outcome.inconclusive) {
|
|
console.warn(
|
|
`[boot] Encryption key check inconclusive for key id ${keyId}: ` +
|
|
`${INCONCLUSIVE_EXPLANATION[reason]} Serving; no key recorded; ` +
|
|
"the check runs again at the next start.",
|
|
);
|
|
}
|
|
if (outcome.written.length > 0) {
|
|
console.info(
|
|
`[boot] Encryption key check: recorded key id(s) ${outcome.written.join(", ")}`,
|
|
);
|
|
}
|
|
}
|
|
return false;
|
|
}
|