package backupformat import ( "bytes" "crypto/sha256" "crypto/subtle" "encoding/binary" "encoding/hex" "encoding/json" "errors" "fmt" "io" ) // Section ist ein gelesener Abschnitt eines Containers. type Section struct { // Type ist die Art des Abschnitts. Type SectionType // Flags beschreiben Eigenschaften des Abschnitts. Flags SectionFlags // Content ist der geprüfte Inhalt. Content []byte // Digest ist die Prüfsumme aus dem Abschnittskopf. Digest string } // Reader liest einen Backup Container. // // Gelesen wird sequenziell und prüfend: jeder Abschnitt wird gegen seine // Prüfsumme gehalten, bevor sein Inhalt weitergereicht wird. Ein beschädigter // Container darf niemals unbemerkt in eine Wiederherstellung fließen // (PROMPT.md §14). type Reader struct { // inputReader ist die Quelle des Containers. inputReader io.Reader // header ist der gelesene Container-Header. header ContainerHeader // formatVersion ist die Formatversion des Containers. formatVersion uint16 // contentDigest führt die Gesamtprüfsumme mit. contentDigest interface { io.Writer Sum([]byte) []byte } // sectionsRead zählt die gelesenen Abschnitte. sectionsRead int // bytesRead zählt die gelesenen Byte ohne den Footer. bytesRead int64 // manifestDigest ist die Prüfsumme des Manifestabschnitts. manifestDigest string // footer ist der gelesene Footer; er liegt erst am Ende vor. footer *ContainerFooter } // NewReader öffnet einen Container und liest dessen Header. // // Version und Kennung werden sofort geprüft: ein fremdes oder unlesbares // Format wird abgelehnt, bevor irgendein Inhalt gedeutet wird. func NewReader(inputReader io.Reader) (*Reader, error) { containerReader := &Reader{ inputReader: inputReader, contentDigest: sha256.New(), } if readError := containerReader.readContainerHeader(); readError != nil { return nil, readError } return containerReader, nil } // readContainerHeader liest Kennung, Version und Header. func (containerReader *Reader) readContainerHeader() error { magicBuffer := make([]byte, magicLength) if readError := containerReader.readFullAndDigest(magicBuffer); readError != nil { // Zu wenige Daten für die Kennung bedeuten: das ist kein Container. return ErrNotAContainer } if !bytes.Equal(magicBuffer, containerMagic[:]) { return ErrNotAContainer } versionBuffer := make([]byte, versionLength) if readError := containerReader.readFullAndDigest(versionBuffer); readError != nil { return ErrContainerTruncated } containerReader.formatVersion = binary.BigEndian.Uint16(versionBuffer) if !IsVersionReadable(containerReader.formatVersion) { return describeVersionMismatch(containerReader.formatVersion) } headerLengthBuffer := make([]byte, headerLengthFieldLength) if readError := containerReader.readFullAndDigest(headerLengthBuffer); readError != nil { return ErrContainerTruncated } headerLength := binary.BigEndian.Uint32(headerLengthBuffer) // Eine verfälschte Längenangabe darf keine unbegrenzte Speicheranforderung auslösen. if headerLength > maximumHeaderLength { return fmt.Errorf("%w: der header ist mit %d byte unplausibel groß", ErrNotAContainer, headerLength) } headerBuffer := make([]byte, headerLength) if readError := containerReader.readFullAndDigest(headerBuffer); readError != nil { return ErrContainerTruncated } if unmarshalError := json.Unmarshal(headerBuffer, &containerReader.header); unmarshalError != nil { return fmt.Errorf("%w: der header ist unlesbar", ErrNotAContainer) } return nil } // Header liefert den Container-Header. func (containerReader *Reader) Header() ContainerHeader { return containerReader.header } // FormatVersion liefert die Formatversion des Containers. func (containerReader *Reader) FormatVersion() uint16 { return containerReader.formatVersion } // NextSection liest den nächsten Abschnitt. // // Am Ende der Abschnittskette wird io.EOF geliefert; der Footer ist dann // gelesen und über Footer() abrufbar. func (containerReader *Reader) NextSection() (*Section, error) { sectionHeaderBuffer := make([]byte, sectionHeaderLength) bytesRead, readError := io.ReadFull(containerReader.inputReader, sectionHeaderBuffer) // Ein Abschnittskopf, der wie ein Footer beginnt, beendet die Kette. if bytesRead >= magicLength && bytes.Equal(sectionHeaderBuffer[:magicLength], footerMagic[:]) { return nil, containerReader.readFooter(sectionHeaderBuffer[:bytesRead]) } if readError != nil { if errors.Is(readError, io.EOF) || errors.Is(readError, io.ErrUnexpectedEOF) { // Die Daten enden, ohne dass ein Footer kam. return nil, ErrContainerTruncated } return nil, fmt.Errorf("der container konnte nicht gelesen werden: %w", readError) } // Der Kopf gehört zur Gesamtprüfsumme. _, _ = containerReader.contentDigest.Write(sectionHeaderBuffer) containerReader.bytesRead += int64(len(sectionHeaderBuffer)) sectionType := SectionType(sectionHeaderBuffer[0]) sectionFlags := SectionFlags(sectionHeaderBuffer[1]) sectionLength := binary.BigEndian.Uint64(sectionHeaderBuffer[2:10]) expectedDigest := sectionHeaderBuffer[10 : 10+digestLength] if sectionLength > maximumSectionLength { return nil, fmt.Errorf("%w: %d byte", ErrSectionTooLarge, sectionLength) } sectionContent := make([]byte, sectionLength) if _, contentError := io.ReadFull(containerReader.inputReader, sectionContent); contentError != nil { return nil, ErrContainerTruncated } containerReader.bytesRead += int64(sectionLength) actualDigest := sha256.Sum256(sectionContent) // Ein als Datenstrom geschriebener Abschnitt trägt keine Prüfsumme im Kopf: // sie stand dort noch nicht fest. Seine Unversehrtheit sichern die // Prüfsummen der einzelnen Chunks im Verzeichnis und die Gesamtprüfsumme // im Footer, die beide weiter unten geprüft werden. if !sectionFlags.Has(FlagDeferredDigest) { // Der Vergleich läuft in konstanter Zeit: eine Prüfsumme ist ein // Sicherheitsmerkmal, kein bloßer Vergleichswert. if subtle.ConstantTimeCompare(actualDigest[:], expectedDigest) != 1 { return nil, fmt.Errorf("%w: %s (erwartet %s, tatsächlich %s)", ErrSectionCorrupted, sectionType, hex.EncodeToString(expectedDigest), hex.EncodeToString(actualDigest[:])) } } _, _ = containerReader.contentDigest.Write(sectionContent) // Ein unbekannter Abschnitt darf nur übersprungen werden, wenn er nicht als // erforderlich gekennzeichnet ist. Andernfalls fehlte etwas Wesentliches, // und ein Weiterarbeiten täuschte Vollständigkeit vor (PROMPT.md §140). if !isKnownSectionType(sectionType) && sectionFlags.Has(FlagRequired) { return nil, fmt.Errorf("%w: Abschnittstyp %d. Bitte Syncova aktualisieren", ErrUnknownRequiredSection, uint8(sectionType)) } containerReader.sectionsRead++ if sectionType == SectionManifest { containerReader.manifestDigest = hex.EncodeToString(actualDigest[:]) } return &Section{ Type: sectionType, Flags: sectionFlags, Content: sectionContent, Digest: hex.EncodeToString(actualDigest[:]), }, nil } // readFooter liest den Footer und prüft den Abschlussvermerk. func (containerReader *Reader) readFooter(alreadyRead []byte) error { // Der Kennungsteil wurde bereits gelesen; es fehlen Längenfeld und Inhalt. remainingHeader := alreadyRead[magicLength:] footerLengthBuffer := make([]byte, headerLengthFieldLength) copiedBytes := copy(footerLengthBuffer, remainingHeader) if copiedBytes < headerLengthFieldLength { if _, readError := io.ReadFull(containerReader.inputReader, footerLengthBuffer[copiedBytes:]); readError != nil { return ErrContainerTruncated } } footerLength := binary.BigEndian.Uint32(footerLengthBuffer) if footerLength > maximumHeaderLength { return fmt.Errorf("%w: der footer ist unplausibel groß", ErrIncompleteBackup) } footerBuffer := make([]byte, footerLength) // Teile des Footers können bereits im Puffer des Abschnittskopfs liegen. alreadyBuffered := remainingHeader[min(len(remainingHeader), headerLengthFieldLength):] copiedFooterBytes := copy(footerBuffer, alreadyBuffered) if copiedFooterBytes < int(footerLength) { if _, readError := io.ReadFull(containerReader.inputReader, footerBuffer[copiedFooterBytes:]); readError != nil { return ErrContainerTruncated } } var containerFooter ContainerFooter if unmarshalError := json.Unmarshal(footerBuffer, &containerFooter); unmarshalError != nil { return fmt.Errorf("%w: der footer ist unlesbar", ErrIncompleteBackup) } // Ohne ausdrücklichen Abschlussvermerk gilt der Container als unvollständig. if !containerFooter.Complete { return ErrIncompleteBackup } // Die Gesamtprüfsumme deckt Header und alle Abschnitte ab. Sie deckt auch // den Austausch eines vollständigen Abschnitts samt seiner Prüfsumme auf. actualContentHash := hex.EncodeToString(containerReader.contentDigest.Sum(nil)) if subtle.ConstantTimeCompare([]byte(actualContentHash), []byte(containerFooter.ContentHash)) != 1 { return fmt.Errorf("%w (erwartet %s, tatsächlich %s)", ErrIntegrityMismatch, containerFooter.ContentHash, actualContentHash) } // Eine abweichende Abschnittszahl deckt einen entfernten Abschnitt auf. if containerFooter.SectionCount != containerReader.sectionsRead { return fmt.Errorf("%w: der footer nennt %d abschnitte, gelesen wurden %d", ErrIntegrityMismatch, containerFooter.SectionCount, containerReader.sectionsRead) } if containerFooter.ManifestHash != containerReader.manifestDigest { return fmt.Errorf("%w: die prüfsumme des manifests stimmt nicht", ErrIntegrityMismatch) } containerReader.footer = &containerFooter return io.EOF } // Footer liefert den Footer, sofern der Container vollständig gelesen wurde. // // Vorher ist er nil: der Abschlussvermerk steht erst am Ende fest. func (containerReader *Reader) Footer() *ContainerFooter { return containerReader.footer } // IsComplete meldet, ob der Container einen gültigen Abschlussvermerk trägt. func (containerReader *Reader) IsComplete() bool { return containerReader.footer != nil && containerReader.footer.Complete } // readFullAndDigest liest genau die angeforderte Menge und führt die Prüfsumme mit. func (containerReader *Reader) readFullAndDigest(targetBuffer []byte) error { if _, readError := io.ReadFull(containerReader.inputReader, targetBuffer); readError != nil { return readError } _, _ = containerReader.contentDigest.Write(targetBuffer) containerReader.bytesRead += int64(len(targetBuffer)) return nil } // isKnownSectionType meldet, ob diese Programmversion einen Abschnittstyp kennt. func isKnownSectionType(sectionType SectionType) bool { switch sectionType { case SectionManifest, SectionChunkIndex, SectionBlockMap, SectionDataChunks, SectionSourceMetadata, SectionIntegrityInfo: return true default: return false } }