package repository import ( "context" "crypto/sha256" "encoding/hex" "errors" "fmt" "log/slog" "os" "path/filepath" ) // WriteTransformedChunk legt einen Block ab, dessen gespeicherte Form von // seinem Klartext abweicht. // // Die Kennung bleibt der Hash des **Klartextes**. Nur so wirkt die // Deduplizierung: verschlüsselte Daten sind bei jedem Durchgang verschieden, // zwei gleiche Ursprungsblöcke ergäben also nie denselben Geheimtext. Die // Zuordnung Klartext-Kennung → abgelegte Form ist der Kern jedes // deduplizierenden und zugleich verschlüsselnden Backup-Systems. // // Die Unversehrtheit der abgelegten Form sichert nicht die Kennung, sondern // die zurückgegebene Prüfsumme: sie gehört ins Manifest, damit ein // Integritätslauf ohne Entschlüsselungsschlüssel prüfen kann. func (localRepository *LocalRepository) WriteTransformedChunk(writeContext context.Context, plaintextIdentifier string, storedData []byte) (storedDigest string, wasNew bool, writeError error) { if validationError := validateChunkIdentifier(plaintextIdentifier); validationError != nil { return "", false, validationError } targetPath, pathError := localRepository.chunkPath(plaintextIdentifier) if pathError != nil { return "", false, pathError } digestOfStoredForm := sha256.Sum256(storedData) storedDigest = hex.EncodeToString(digestOfStoredForm[:]) // Liegt der Block bereits vor, wird nichts erneut geschrieben. Die // vorhandene Fassung bleibt maßgeblich: ein zweiter Durchgang erzeugte // wegen der Verschlüsselung andere Bytes bei identischem Klartext. if _, statError := os.Stat(targetPath); statError == nil { existingDigest, digestError := localRepository.digestOfStoredChunk(plaintextIdentifier) if digestError != nil { return "", false, digestError } return existingDigest, false, nil } if directoryError := ensureDirectory(filepath.Dir(targetPath)); directoryError != nil { return "", false, directoryError } if atomicWriteError := writeFileAtomically(targetPath, storedData, dataFilePermissions); atomicWriteError != nil { return "", false, fmt.Errorf("der chunk konnte nicht geschrieben werden: %w", atomicWriteError) } localRepository.protectStoredChunk(targetPath) return storedDigest, true, nil } // StoredDigestOfChunk liefert die Prüfsumme der abgelegten Form eines Blocks. // // Der Weg wird gebraucht, wenn ein Block bereits vorliegt: sein Eintrag im // Manifest braucht die Prüfsumme dennoch, damit ein Integritätslauf ihn prüfen kann. func (localRepository *LocalRepository) StoredDigestOfChunk(queryContext context.Context, chunkIdentifier string) (string, error) { return localRepository.digestOfStoredChunk(chunkIdentifier) } // digestOfStoredChunk berechnet die Prüfsumme der abgelegten Form eines Blocks. func (localRepository *LocalRepository) digestOfStoredChunk(chunkIdentifier string) (string, error) { targetPath, pathError := localRepository.chunkPath(chunkIdentifier) if pathError != nil { return "", pathError } storedData, readError := os.ReadFile(targetPath) if readError != nil { if errors.Is(readError, os.ErrNotExist) { return "", fmt.Errorf("%w (kennung %s)", ErrChunkNotFound, chunkIdentifier) } return "", fmt.Errorf("der chunk konnte nicht gelesen werden: %w", readError) } digestOfStoredForm := sha256.Sum256(storedData) return hex.EncodeToString(digestOfStoredForm[:]), nil } // ReadStoredChunk liest die abgelegte Form eines Blocks. // // Geprüft wird gegen die im Manifest vermerkte Prüfsumme der gespeicherten // Form, nicht gegen die Klartext-Kennung: der Klartext liegt hier noch nicht // vor. Die abschließende Prüfung gegen die Kennung erfolgt in der Backup // Engine, nachdem der Block entschlüsselt und entpackt wurde. // // Ist expectedStoredDigest leer, unterbleibt die Prüfung an dieser Stelle — // dann muss der Aufrufer sie zwingend selbst vornehmen. func (localRepository *LocalRepository) ReadStoredChunk(readContext context.Context, chunkIdentifier string, expectedStoredDigest string) ([]byte, error) { targetPath, pathError := localRepository.chunkPath(chunkIdentifier) if pathError != nil { return nil, pathError } storedData, readError := os.ReadFile(targetPath) if readError != nil { if errors.Is(readError, os.ErrNotExist) { return nil, fmt.Errorf("%w (kennung %s)", ErrChunkNotFound, chunkIdentifier) } return nil, fmt.Errorf("der chunk konnte nicht gelesen werden: %w", readError) } if expectedStoredDigest == "" { return storedData, nil } digestOfStoredForm := sha256.Sum256(storedData) actualDigest := hex.EncodeToString(digestOfStoredForm[:]) if actualDigest != expectedStoredDigest { localRepository.logger.Error("beschädigter chunk erkannt", slog.String("chunk_id", chunkIdentifier), slog.String("expected_stored_digest", expectedStoredDigest), slog.String("actual_stored_digest", actualDigest)) return nil, fmt.Errorf("%w (kennung %s)", ErrChunkCorrupted, chunkIdentifier) } return storedData, nil } // VerifyStoredChunk prüft einen abgelegten Block gegen eine erwartete Prüfsumme. // // Der Weg erlaubt es dem Integritätslauf, auch verschlüsselte Blöcke zu prüfen, // ohne einen Schlüssel zu benötigen (PROMPT.md §14). func (localRepository *LocalRepository) VerifyStoredChunk(verifyContext context.Context, chunkIdentifier string, expectedStoredDigest string) error { _, readError := localRepository.ReadStoredChunk(verifyContext, chunkIdentifier, expectedStoredDigest) return readError } // WriteTransformedChunkForTest überschreibt einen abgelegten Block. // // Der Weg dient ausschließlich Tests, die eine Beschädigung auf dem Datenträger // nachstellen. Im Betrieb darf ein abgelegter Block niemals überschrieben // werden: seine Kennung beschreibt genau einen Inhalt. func (localRepository *LocalRepository) WriteTransformedChunkForTest(chunkIdentifier string, storedData []byte) (string, bool, error) { targetPath, pathError := localRepository.chunkPath(chunkIdentifier) if pathError != nil { return "", false, pathError } if chmodError := os.Chmod(targetPath, dataFilePermissions); chmodError != nil && !errors.Is(chmodError, os.ErrNotExist) { return "", false, chmodError } if writeError := writeFileAtomically(targetPath, storedData, dataFilePermissions); writeError != nil { return "", false, writeError } digestOfStoredForm := sha256.Sum256(storedData) return hex.EncodeToString(digestOfStoredForm[:]), true, nil }