// Package repository implementiert die Ablage der Backup-Nutzdaten. // // Kerngedanke (PROMPT.md §2.4, SYNCOVA_ARCHITECTURE.md §11): Ein Repository ist // selbstbeschreibend. Sämtliche Angaben, die zur Wiederherstellung nötig sind, // liegen im Repository selbst — niemals ausschließlich in PostgreSQL. Geht der // Control Server verloren, muss ein Repository allein durch Scannen wieder // nutzbar werden. package repository import ( "encoding/json" "errors" "fmt" "time" ) // FormatVersion ist die Version des Repository-Formats. // // Sie wird bei jedem Öffnen geprüft. Ein Repository neueren Formats wird nicht // angetastet: eine ältere Programmversion könnte es sonst beschädigen // (PROMPT.md §13). const FormatVersion = 1 // FormatIdentifier kennzeichnet ein Syncova-Repository eindeutig. // // Er verhindert, dass ein beliebiges Verzeichnis versehentlich als Repository // angesprochen und dabei überschrieben wird. const FormatIdentifier = "syncova-repository" // HashAlgorithm benennt das Verfahren zur Bildung der Chunk-Kennung. type HashAlgorithm string const ( // HashAlgorithmSHA256 ist das Verfahren der Formatversion 1. // // SHA-256 ist kryptografisch sicher (PROMPT.md §9), Bestandteil der // Standardbibliothek und wird auf allen Zielplattformen hardwarebeschleunigt. // Eine Fremdbibliothek brächte hier keinen Vorteil, aber eine zusätzliche // Abhängigkeit im sicherheitskritischsten Pfad des Produkts. HashAlgorithmSHA256 HashAlgorithm = "sha256" ) // RepositoryKind beschreibt die Betriebsart eines Repositorys. type RepositoryKind string const ( // KindLocal ist ein gewöhnliches Repository im Dateisystem. KindLocal RepositoryKind = "local" // KindHardenedLinux ist ein gehärtetes Repository mit Retention Lock. // // Gelöscht werden darf hier erst nach Ablauf der Aufbewahrungsfrist // (PROMPT.md §15). KindHardenedLinux RepositoryKind = "hardened_linux" ) // Descriptor beschreibt ein Repository und liegt in seinem Wurzelverzeichnis. // // Der Descriptor ist der Einstiegspunkt jedes Wiederaufbaus: er benennt Format, // Verfahren und Betriebsart, ohne die die abgelegten Daten nicht deutbar wären. type Descriptor struct { // Identifier kennzeichnet die Datei als Syncova-Repository. Identifier string `json:"identifier"` // FormatVersion ist die Version des Repository-Formats. FormatVersion int `json:"format_version"` // RepositoryID ist der dauerhafte Bezeichner dieses Repositorys. // // Er bleibt auch dann gültig, wenn das Repository an einen anderen Pfad // oder an einen neu aufgesetzten Control Server angehängt wird. RepositoryID string `json:"repository_id"` // Name ist die sprechende Bezeichnung des Repositorys. Name string `json:"name"` // Kind ist die Betriebsart. Kind RepositoryKind `json:"kind"` // HashAlgorithm ist das Verfahren der Chunk-Kennungen. HashAlgorithm HashAlgorithm `json:"hash_algorithm"` // ChunkFanoutDepth ist die Zahl der Unterverzeichnisebenen der Chunk-Ablage. ChunkFanoutDepth int `json:"chunk_fanout_depth"` // Immutable meldet, ob ein Retention Lock gilt. Immutable bool `json:"immutable"` // RetentionSeconds ist die Aufbewahrungsfrist neuer Backups in Sekunden. // // Sie steht im Descriptor und nicht in der Datenbank, weil das Repository // ohne Control Server deutbar bleiben muss: Wer es an einen fremden Server // anhaengt, soll die geltende Frist vorfinden und nicht die des neuen // Servers untergeschoben bekommen. // // Null bedeutet: die Standardfrist von 30 Tagen (PROMPT.md §119). RetentionSeconds int64 `json:"retention_seconds,omitempty"` // MinimumRetentionSeconds ist die kuerzeste je zulaessige Frist. // // Sie ist die eigentliche Sperre gegen den Angriff „Frist auf null setzen, // dann alles loeschen". Einmal gesetzt, laesst sie sich nicht mehr // verringern — auch nicht von einem Administrator. MinimumRetentionSeconds int64 `json:"minimum_retention_seconds,omitempty"` // EncryptionRequired meldet, ob Backups verschlüsselt abgelegt werden müssen. EncryptionRequired bool `json:"encryption_required"` // CreatedAt ist der Anlagezeitpunkt in UTC. CreatedAt time.Time `json:"created_at"` // CreatedByVersion ist die Programmversion, die das Repository angelegt hat. // // Bei einem Wiederaufbau lässt sich damit feststellen, welche Software das // Format geschrieben hat. CreatedByVersion string `json:"created_by_version"` } // Fehler des Repository-Formats. var ( // ErrNotARepository meldet ein Verzeichnis ohne gültigen Descriptor. ErrNotARepository = errors.New("das verzeichnis enthält kein syncova-repository") // ErrUnsupportedFormat meldet eine nicht unterstützte Formatversion. ErrUnsupportedFormat = errors.New("die formatversion des repositorys wird von dieser programmversion nicht unterstützt") // ErrRepositoryExists meldet ein bereits vorhandenes Repository. ErrRepositoryExists = errors.New("in diesem verzeichnis existiert bereits ein repository") ) // Validate prüft einen gelesenen Descriptor auf Verwendbarkeit. func (descriptor Descriptor) Validate() error { if descriptor.Identifier != FormatIdentifier { return ErrNotARepository } // Ein neueres Format darf nicht beschrieben werden: diese Programmversion // kennt seine Regeln nicht und könnte Daten unbrauchbar machen. if descriptor.FormatVersion > FormatVersion { return fmt.Errorf("%w: gefunden wurde Version %d, unterstützt wird bis Version %d", ErrUnsupportedFormat, descriptor.FormatVersion, FormatVersion) } if descriptor.FormatVersion < 1 { return fmt.Errorf("%w: die Formatversion %d ist ungültig", ErrUnsupportedFormat, descriptor.FormatVersion) } if descriptor.HashAlgorithm != HashAlgorithmSHA256 { return fmt.Errorf("%w: das Hashverfahren %q ist unbekannt", ErrUnsupportedFormat, descriptor.HashAlgorithm) } if descriptor.RepositoryID == "" { return fmt.Errorf("%w: dem Repository fehlt seine Kennung", ErrNotARepository) } return nil } // encodeDescriptor serialisiert einen Descriptor. // // Die Ausgabe ist bewusst eingerückt: bei einem Wiederaufbau von Hand muss ein // Administrator die Datei lesen können. func encodeDescriptor(descriptor Descriptor) ([]byte, error) { encodedDescriptor, marshalError := json.MarshalIndent(descriptor, "", " ") if marshalError != nil { return nil, fmt.Errorf("der repository-descriptor konnte nicht erzeugt werden: %w", marshalError) } return append(encodedDescriptor, '\n'), nil } // decodeDescriptor liest einen Descriptor. func decodeDescriptor(rawDescriptor []byte) (Descriptor, error) { var descriptor Descriptor if unmarshalError := json.Unmarshal(rawDescriptor, &descriptor); unmarshalError != nil { // Eine unlesbare Datei bedeutet nicht zwingend ein defektes Repository, // aber sie darf keinesfalls als gültig durchgehen. return Descriptor{}, fmt.Errorf("%w: der descriptor ist unlesbar", ErrNotARepository) } return descriptor, nil } // RetentionPeriod liefert die Aufbewahrungsfrist neuer Backups. // // Ohne ausdrückliche Angabe gilt die Standardfrist. Eine Frist von null wäre // die gefährlichste Vorgabe: Der Betreiber hielte sein Repository für gehärtet, // während jedes Backup sofort löschbar wäre. func (descriptor Descriptor) RetentionPeriod() time.Duration { if descriptor.RetentionSeconds <= 0 { return DefaultRetentionPeriod } return time.Duration(descriptor.RetentionSeconds) * time.Second } // MinimumRetentionPeriod liefert die kürzeste zulässige Aufbewahrungsfrist. func (descriptor Descriptor) MinimumRetentionPeriod() time.Duration { if descriptor.MinimumRetentionSeconds <= 0 { return 0 } return time.Duration(descriptor.MinimumRetentionSeconds) * time.Second } // DefaultRetentionPeriod ist die Aufbewahrungsfrist ohne ausdrückliche Angabe. // // 30 Tage entsprechen der Standardvorgabe aus PROMPT.md §119. const DefaultRetentionPeriod = 30 * 24 * time.Hour