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
163 lines
6.4 KiB
JavaScript
163 lines
6.4 KiB
JavaScript
// Personuppgifter i en append-only-logg.
|
|
//
|
|
// Revisionen (docs/QUALITY-AUDIT.md, C-3) fann att systemet inte kunde
|
|
// uppfylla artikel 17: databastriggern hindrar aktivt radering, och
|
|
// kundens namn, telefonnummer, e-post, registreringsnummer och VIN
|
|
// ligger i klartext i loggen.
|
|
//
|
|
// "Append-only" och "radering på begäran" är inte oförenliga, men de
|
|
// måste förenas medvetet. Lösningen här är krypto-shredding:
|
|
//
|
|
// Identifierande fält lagras krypterade med en nyckel som är unik per
|
|
// registrerad person. Radering sker genom att nyckeln förstörs. Loggen
|
|
// förblir intakt och hashverifierbar — det som blir oåtkomligt är
|
|
// identifieringen, inte protokollet över vad som kontrollerades.
|
|
//
|
|
// Det är den enda konstruktion jag känner till där bevisvärdet överlever
|
|
// en raderingsbegäran. Ett ärende där kunden begärt radering kan
|
|
// fortfarande visa att lufttrycket mättes till 2,4 bar av en behörig
|
|
// tekniker klockan 08:42 — bara inte längre vem bilen tillhörde.
|
|
//
|
|
// Vad som INTE krypteras: mätvärden, observationer, kontrollresultat,
|
|
// tidsstämplar, teknikerns roll. De är verksamhetsdata och bär inget
|
|
// personvärde i sig. Att kryptera allt vore enklare att beskriva och
|
|
// sämre i praktiken — då raderas beviset tillsammans med personuppgiften.
|
|
|
|
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
import { arendetypNu } from "./sprak/index.mjs";
|
|
|
|
/**
|
|
* Fält som räknas som identifierande och därför krypteras.
|
|
*
|
|
* Registreringsnummer och VIN står med avsikt med. De pekar ut ett
|
|
* fordon, men ett fordon pekar i praktiken ut en ägare — EDPB behandlar
|
|
* VIN som personuppgift när kopplingen finns, och här finns den.
|
|
*/
|
|
export const IDENTIFIERANDE = {
|
|
objekt_identifierat: ["identifierare", "vin", "agare", "kund"],
|
|
arbetsorder_skannad: [
|
|
"kund_namn",
|
|
"kund_foretag",
|
|
"kund_telefon",
|
|
"kund_epost",
|
|
"fordon_regnr",
|
|
"fordon_vin",
|
|
],
|
|
kundbeslut: ["kontaktperson"],
|
|
};
|
|
|
|
export const MASKERAT = "[raderat på begäran]";
|
|
|
|
/**
|
|
* Krypterar ett värde med den registrerades nyckel.
|
|
* Formatet bär nyckelns id, så att en post går att koppla till rätt
|
|
* nyckel utan att nyckeln behöver finnas.
|
|
*/
|
|
export function skydda(värde, nyckelId, nyckel) {
|
|
const iv = randomBytes(12);
|
|
const chiffer = createCipheriv("aes-256-gcm", nyckel, iv);
|
|
const ut = Buffer.concat([chiffer.update(String(värde), "utf8"), chiffer.final()]);
|
|
return `pd1:${nyckelId}:${iv.toString("base64url")}:${chiffer.getAuthTag().toString("base64url")}:${ut.toString("base64url")}`;
|
|
}
|
|
|
|
/**
|
|
* Öppnar ett skyddat värde.
|
|
*
|
|
* Saknas nyckeln returneras MASKERAT — det är det normala utfallet efter
|
|
* en radering, inte ett fel. Ett kastat undantag här skulle göra hela
|
|
* ärendet oläsbart, vilket vore precis fel: resten av bevisningen ska
|
|
* överleva.
|
|
*/
|
|
export function öppna(skyddat, nycklar) {
|
|
if (typeof skyddat !== "string" || !skyddat.startsWith("pd1:")) return skyddat;
|
|
const [, nyckelId, iv, tagg, data] = skyddat.split(":");
|
|
const nyckel = nycklar.get(nyckelId);
|
|
if (!nyckel) return MASKERAT;
|
|
try {
|
|
const chiffer = createDecipheriv("aes-256-gcm", nyckel, Buffer.from(iv, "base64url"));
|
|
chiffer.setAuthTag(Buffer.from(tagg, "base64url"));
|
|
return chiffer.update(Buffer.from(data, "base64url"), undefined, "utf8") + chiffer.final("utf8");
|
|
} catch {
|
|
// Fel nyckel eller manipulerad chiffertext. Att skilja de två fallen
|
|
// åt i ett felmeddelande vore ett orakel; behandla dem lika.
|
|
return MASKERAT;
|
|
}
|
|
}
|
|
|
|
/** Krypterar de identifierande fälten i en händelse. */
|
|
export function skyddaHändelse(handelse, nyckelId, nyckel) {
|
|
const fält = IDENTIFIERANDE[handelse.typ];
|
|
if (!fält) return handelse;
|
|
|
|
if (handelse.typ === "arbetsorder_skannad") {
|
|
return {
|
|
...handelse,
|
|
falt: (handelse.falt ?? []).map((f) =>
|
|
fält.includes(f.id) && f.varde ? { ...f, varde: skydda(f.varde, nyckelId, nyckel) } : f,
|
|
),
|
|
};
|
|
}
|
|
if (handelse.typ === "objekt_identifierat") {
|
|
const objekt = { ...handelse.objekt };
|
|
for (const n of fält) if (objekt[n]) objekt[n] = skydda(objekt[n], nyckelId, nyckel);
|
|
return { ...handelse, objekt };
|
|
}
|
|
const ut = { ...handelse };
|
|
for (const n of fält) if (ut[n]) ut[n] = skydda(ut[n], nyckelId, nyckel);
|
|
return ut;
|
|
}
|
|
|
|
/** Öppnar de identifierande fälten igen för visning. */
|
|
export function öppnaHändelse(handelse, nycklar) {
|
|
const fält = IDENTIFIERANDE[handelse?.typ];
|
|
if (!fält) return handelse;
|
|
|
|
if (handelse.typ === "arbetsorder_skannad") {
|
|
return { ...handelse, falt: (handelse.falt ?? []).map((f) => ({ ...f, varde: öppna(f.varde, nycklar) })) };
|
|
}
|
|
if (handelse.typ === "objekt_identifierat") {
|
|
const objekt = { ...handelse.objekt };
|
|
for (const n of fält) if (objekt[n]) objekt[n] = öppna(objekt[n], nycklar);
|
|
return { ...handelse, objekt };
|
|
}
|
|
const ut = { ...handelse };
|
|
for (const n of fält) if (ut[n]) ut[n] = öppna(ut[n], nycklar);
|
|
return ut;
|
|
}
|
|
|
|
/**
|
|
* Gallringsbeslut.
|
|
*
|
|
* Retention är inte en teknisk detalj utan ett juridiskt ställningstagande,
|
|
* och det skiljer sig mellan ärendetyper: ett garantiärende måste kunna
|
|
* visas upp under garantitiden, ett kontantärende inte. Standardvärdena
|
|
* nedan är utgångspunkter — varje organisation ska sätta sina egna, och
|
|
* den som inte gör ett aktivt val får det försiktigaste.
|
|
*/
|
|
export const GALLRING_MANADER = {
|
|
Warranty: 120, // Följer den längsta rimliga garantitiden på en drivlina.
|
|
Goodwill: 120,
|
|
Insurance: 120, // Preskription för försäkringskrav.
|
|
Complaint: 36, // Konsumentköplagens reklamationsrätt.
|
|
"Used-vehicle warranty": 60,
|
|
standard: 36,
|
|
};
|
|
|
|
/**
|
|
* Gallringsdatum för ett avslutat ärende.
|
|
*
|
|
* Ärendetypen normaliseras genom ARENDETYP_ARV innan den slås upp. Det
|
|
* är inte en artighet mot gamla data utan en spärr: när typerna bytte
|
|
* språk hade ett garantiärende vars logg säger "Garanti" fallit ur
|
|
* tabellen och tagit standardvärdet 36 månader i stället för 120 —
|
|
* alltså raderat garantiunderlaget sju år för tidigt, tyst, och
|
|
* oåterkalleligt. Ett uppslag som missar ska ge det FÖRSIKTIGASTE
|
|
* utfallet; här gav det det farligaste.
|
|
*/
|
|
export function gallringsdatum(arendetyp, avslutat, manader = GALLRING_MANADER) {
|
|
const antal = manader[arendetypNu(arendetyp)] ?? manader.standard;
|
|
const d = new Date(avslutat);
|
|
d.setMonth(d.getMonth() + antal);
|
|
return d;
|
|
}
|