Files
alva/felsokning/app/services/gemensam/kedja.mjs
T
Bernt 04e0a1e8ec
Guidad Felsökning / test-och-bygg (push) Has been cancelled
Guidad Felsökning / tjanster (push) Has been cancelled
Guidad Felsökning / terraform (push) Has been cancelled
Guidad Felsökning / leverantorskedja (push) Has been cancelled
Guidad Felsökning / aterstallning (push) Has been cancelled
Guidad Felsökning / publicera (push) Has been cancelled
Guidad Felsökning / driftsatt (push) Has been cancelled
ALVA deployment config
2026-08-08 13:12:13 +00:00

109 lines
4.8 KiB
JavaScript

// ALVA-SPEC-070 · Hashkedjan.
//
// ---- Varför triggrar inte räcker ----------------------------------------
//
// Loggen är append-only, genomdrivet av databastriggrar. Panelgranskningen
// (docs/PANELGRANSKNING-2026-08.md, datasäkerhetssätet) satte fingret på
// gränsen för det skyddet: en trigger skyddar mot APPLIKATIONEN, inte mot
// den som äger databasen. Den som har superuser släpper triggern, ändrar
// raden och återskapar triggern, och ingenting syns.
//
// För ett system vars hela värde är bevisvärde är det fel viloläge. En
// hashkedja flyttar skyddet från behörighet till matematik: varje händelse
// bär en hash av sitt eget innehåll OCH föregående händelses hash. Den som
// ändrar en rad i efterhand bryter varje efterföljande länk, och brottet
// går att peka ut — inte bara konstatera.
//
// ---- Vad kedjan bevisar, och inte ---------------------------------------
//
// Kedjan bevisar att loggen inte ändrats SEDAN DEN SKREVS, i den ordning
// den skrevs. Den bevisar inte att det som skrevs var sant — det är
// grindens och evidensmodellens uppgift — och den bevisar ingenting om
// tiden FÖRE serverns mottagande. Rapporten ska säga just det och inte
// mer: "innehållet är oförändrat sedan mottagandet".
//
// Förseglingen vid avslut (HMAC över roten med en nyckel utanför
// databasen) skyddar mot nästa angrepp i ordningen: den som äger databasen
// och räknar OM hela kedjan efter sin ändring. Utan nyckeln kan en
// omräknad kedja inte förseglas om, och en försegling som inte stämmer
// mot roten är beviset.
//
// ---- Konstruktion --------------------------------------------------------
//
// länk_n = SHA256(länk_{n-1} · id · tidpunkt · anvandare · digest)
//
// `digest` är innehållshashen av den kanoniska händelsen — samma
// `klientdigest` som kollisionsprövningen redan använder, så kedjan och
// dubblettskyddet kan aldrig ha olika uppfattning om vad en händelse
// "är". Fälten skiljs med radbrytning: utan avskiljare vore
// ("ab","c") och ("a","bc") samma länk.
//
// Första länken utgår från ärende-id:t i stället för en tom sträng, så
// att två ärenden med identisk första händelse ändå får olika kedjor —
// en kedja ska inte gå att lyfta från ett ärende till ett annat.
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
/** Kedjans startvärde för ett ärende. */
export const grund = (arendeId) =>
createHash("sha256").update(`alva-kedja\n${arendeId}`).digest("hex");
/** Nästa länk. Alla fält är strängar; digest är händelsens innehållshash. */
export function lank(foregaende, { id, tidpunkt, anvandare, digest }) {
return createHash("sha256")
.update(`${foregaende}\n${id}\n${tidpunkt}\n${anvandare}\n${digest ?? ""}`)
.digest("hex");
}
/**
* Verifierar en hel kedja mot lagrade länkar.
*
* @param arendeId ärendet kedjan tillhör
* @param rader [{id, tidpunkt, anvandare, digest, kedjehash}] i kedjeordning
* @returns {ok, rot, brott} — brott pekar ut FÖRSTA raden som inte
* stämmer. Allt efter den är följdfel och räknas inte upp:
* den som läser ska laga ett brott i taget, inte skrämmas av
* hundra.
*
* Rader utan lagrad kedjehash (skrivna innan kolumnen fanns) bryter inte
* kedjan men märks som `okedjade` — tystnad hade låtit gammal data se
* starkare ut än den är, och det är precis den sortens övertro kedjan
* finns för att förhindra.
*/
export function verifiera(arendeId, rader) {
let h = grund(arendeId);
let okedjade = 0;
for (let i = 0; i < rader.length; i++) {
const rad = rader[i];
h = lank(h, rad);
if (rad.kedjehash == null) {
okedjade++;
continue;
}
if (rad.kedjehash !== h) {
return { ok: false, rot: null, okedjade, brott: { index: i, id: rad.id } };
}
}
return { ok: true, rot: rader.length > 0 ? h : grund(arendeId), okedjade };
}
/**
* Förseglar en kedjerot med en nyckel som hålls utanför databasen.
*
* HMAC, inte hash: poängen är att den som äger databasen inte kan räkna
* om förseglingen efter en ändring. Nyckeln är serverns, inte
* teknikerns — förseglingen intygar att PLATTFORMEN såg denna rot vid
* denna tid, ingenting om vem som tryckte på knappen. Det intyget står
* händelsens serverägda `anvandare`-fält för.
*/
export function forsegla(nyckel, arendeId, rot, tidpunkt) {
return createHmac("sha256", nyckel).update(`${arendeId}\n${rot}\n${tidpunkt}`).digest("hex");
}
/** Prövar en försegling. Konstanttid — en försegling är en hemlighet. */
export function provaForsegling(nyckel, arendeId, rot, tidpunkt, forsegling) {
const ratt = Buffer.from(forsegla(nyckel, arendeId, rot, tidpunkt), "hex");
const given = Buffer.from(String(forsegling ?? ""), "hex");
return ratt.length === given.length && given.length > 0 && timingSafeEqual(ratt, given);
}