Objektspeicher verschluesseln, statt es dem Anbieter zu ueberlassen
Im S3-Treiber stand nur ServerSideEncryption: 'AES256'. Das ist
Verschluesselung mit dem Schluessel des Anbieters - er entschluesselt beim
Ausliefern selbst. Gegen eine verlorene Platte im Rechenzentrum hilft das,
gegen den Anbieter und gegen gestohlene Zugangsdaten zum Eimer nicht. In
diesem Eimer liegen Bescheide, Arztbriefe und Kontoauszuege.
Der Dateitreiber hat es von Anfang an richtig gemacht. Der Umschlag zieht
deshalb nach src/lib/speicher/umschlag.ts um und wird von beiden benutzt,
statt ihn ein zweites Mal zu schreiben.
Neu ist die Bindung an den Ablageort: Der Schluessel des Objekts wandert
in die Zusatzdaten. Wer Schreibrechte auf den Eimer hat, kann die Bytes
von Dokument A damit nicht unter dem Namen von Dokument B ablegen - beim
Lesen schlaegt die Pruefung fehl, statt den falschen Bescheid mit
gueltiger Pruefsumme auszuliefern.
Damit fallen die presigned URLs weg. Sie liessen den Browser direkt beim
Anbieter laden; was dort liegt, ist jetzt Geheimtext. Die Auslieferung
geht ueber dieselbe eigene Route wie beim Dateitreiber - die prueft
zusaetzlich die Berechtigung und schreibt einen Audit-Eintrag. Ein
weitergeleiteter Link allein oeffnet also nichts mehr.
Bestehende Dateien bleiben durchgehend lesbar: Der Treiber erkennt am
Kopf, ob ein Umschlag da ist, und reicht alles andere unveraendert durch.
Auch die alten Umschlaege ohne Ortsbindung gehen weiter auf - sonst waere
die Verbesserung ein Datenverlust. Nachgezogen wird mit
npm run speicher:verschluesseln # nur schauen
npm run speicher:verschluesseln -- --anwenden
Geprueft: 12 Tests gegen drei Mutationen, ein Trockenlauf ueber die 120
Dateien der Entwicklungsablage, und eine Probe mit einer Klartextdatei,
die nach dem Lauf verschluesselt auf der Platte liegt und sich trotzdem
korrekt zurueeklesen laesst.
Refs #38
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
entschluesselnBinaer,
|
||||
istVerschluesselt,
|
||||
KOPF_LAENGE,
|
||||
verschluesselnBinaer,
|
||||
} from '@/lib/speicher/umschlag'
|
||||
|
||||
/**
|
||||
* Der Umschlag um abgelegte Dateien.
|
||||
*
|
||||
* Der Dateitreiber hat ihn von Anfang an benutzt, der Objektspeicher nicht:
|
||||
* Dort stand nur ServerSideEncryption: 'AES256' - Verschluesselung mit dem
|
||||
* Schluessel des Anbieters, der beim Ausliefern selbst entschluesselt.
|
||||
* Gegen eine verlorene Platte im Rechenzentrum hilft das, gegen den
|
||||
* Anbieter und gegen gestohlene Zugangsdaten zum Eimer nicht.
|
||||
*
|
||||
* Seit v1.11.0 liegt derselbe Umschlag um beides. Diese Tests halten die
|
||||
* drei Eigenschaften fest, auf die es dabei ankommt: Er geht wieder auf,
|
||||
* er merkt Veraenderungen, und er bindet den Inhalt an seinen Platz.
|
||||
*/
|
||||
|
||||
const INHALT = Buffer.from('Bescheid des Sozialamts vom 3. Maerz, Seite 1 von 2.', 'utf8')
|
||||
const ORT = 'betreute/abc123/dokumente/eins.pdf'
|
||||
|
||||
describe('Umschlag um abgelegte Dateien', () => {
|
||||
it('geht wieder auf', () => {
|
||||
const zu = verschluesselnBinaer(INHALT, ORT)
|
||||
|
||||
expect(entschluesselnBinaer(zu, ORT).equals(INHALT)).toBe(true)
|
||||
})
|
||||
|
||||
it('sieht dem Geheimtext den Klartext nicht an', () => {
|
||||
const zu = verschluesselnBinaer(INHALT, ORT)
|
||||
|
||||
expect(zu.includes(Buffer.from('Sozialamt', 'utf8'))).toBe(false)
|
||||
expect(istVerschluesselt(zu)).toBe(true)
|
||||
expect(zu.length).toBe(KOPF_LAENGE + INHALT.length)
|
||||
})
|
||||
|
||||
it('erzeugt bei gleichem Inhalt zwei verschiedene Geheimtexte', () => {
|
||||
// Sonst liesse sich an zwei gleich aussehenden Objekten ablesen, dass
|
||||
// dasselbe Dokument zweimal in der Akte liegt.
|
||||
const a = verschluesselnBinaer(INHALT, ORT)
|
||||
const b = verschluesselnBinaer(INHALT, ORT)
|
||||
|
||||
expect(a.equals(b)).toBe(false)
|
||||
expect(entschluesselnBinaer(a, ORT).equals(entschluesselnBinaer(b, ORT))).toBe(true)
|
||||
})
|
||||
|
||||
it('merkt, wenn am Geheimtext gedreht wurde', () => {
|
||||
const zu = verschluesselnBinaer(INHALT, ORT)
|
||||
const letzte = zu.length - 1
|
||||
zu.writeUInt8(zu.readUInt8(letzte) ^ 0x01, letzte)
|
||||
|
||||
expect(() => entschluesselnBinaer(zu, ORT)).toThrow()
|
||||
})
|
||||
|
||||
it('merkt, wenn am Kopf gedreht wurde', () => {
|
||||
const zu = verschluesselnBinaer(INHALT, ORT)
|
||||
// Ein Bit im IV.
|
||||
zu.writeUInt8(zu.readUInt8(8) ^ 0x01, 8)
|
||||
|
||||
expect(() => entschluesselnBinaer(zu, ORT)).toThrow()
|
||||
})
|
||||
|
||||
describe('Bindung an den Ablageort', () => {
|
||||
it('laesst sich nicht unter einem anderen Namen auspacken', () => {
|
||||
// Der Angriff dahinter: Wer Schreibrechte auf den Eimer hat, legt die
|
||||
// Bytes von Dokument A unter dem Namen von Dokument B ab. Ohne die
|
||||
// Bindung lieferte die Anwendung anschliessend den falschen Bescheid
|
||||
// aus - und zwar mit gueltiger Pruefsumme fuer den falschen Inhalt.
|
||||
const zu = verschluesselnBinaer(INHALT, ORT)
|
||||
|
||||
expect(() =>
|
||||
entschluesselnBinaer(zu, 'betreute/abc123/dokumente/zwei.pdf'),
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('laesst sich nicht in eine fremde Akte verschieben', () => {
|
||||
const zu = verschluesselnBinaer(INHALT, ORT)
|
||||
|
||||
expect(() => entschluesselnBinaer(zu, 'betreute/xyz789/dokumente/eins.pdf')).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Rueckwaertsvertraeglichkeit', () => {
|
||||
it('reicht Inhalte ohne Umschlag unveraendert durch', () => {
|
||||
// Genau der Zustand, in dem die Objekte im Speicher liegen, bis der
|
||||
// Migrationslauf sie umgeschrieben hat. Wuerde hier geworfen, waere
|
||||
// nach dem Update kein einziges altes Dokument mehr abrufbar.
|
||||
const roh = Buffer.from('%PDF-1.4 ganz gewoehnlich', 'utf8')
|
||||
|
||||
expect(istVerschluesselt(roh)).toBe(false)
|
||||
expect(entschluesselnBinaer(roh, ORT).equals(roh)).toBe(true)
|
||||
})
|
||||
|
||||
it('haelt auch etwas fuer unverpackt, das zufaellig kurz ist', () => {
|
||||
const winzig = Buffer.from('abc', 'utf8')
|
||||
|
||||
expect(istVerschluesselt(winzig)).toBe(false)
|
||||
expect(entschluesselnBinaer(winzig, ORT).equals(winzig)).toBe(true)
|
||||
})
|
||||
|
||||
it('oeffnet einen Umschlag, der noch ohne Ortsbindung geschrieben wurde', () => {
|
||||
// So hat der Dateitreiber bis v1.10.2 geschrieben. Diese Dateien
|
||||
// muessen weiter aufgehen, sonst waere die Verbesserung ein
|
||||
// Datenverlust.
|
||||
const alt = verschluesselnBinaer(INHALT)
|
||||
|
||||
expect(entschluesselnBinaer(alt, ORT).equals(INHALT)).toBe(true)
|
||||
expect(entschluesselnBinaer(alt).equals(INHALT)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('kommt mit einer leeren Datei zurecht', () => {
|
||||
const leer = Buffer.alloc(0)
|
||||
const zu = verschluesselnBinaer(leer, ORT)
|
||||
|
||||
expect(entschluesselnBinaer(zu, ORT).length).toBe(0)
|
||||
})
|
||||
|
||||
it('kommt mit einer groesseren Datei zurecht', () => {
|
||||
const gross = Buffer.alloc(3 * 1024 * 1024, 0x41)
|
||||
const zu = verschluesselnBinaer(gross, ORT)
|
||||
|
||||
expect(entschluesselnBinaer(zu, ORT).equals(gross)).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user