ALVA deployment config
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
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
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user