Repository navigation
seal the files cache with the repository key, fixes #5041 - #10497
Merged
ThomasWaldmann merged 2 commits intoOct 10, 2026
Merged
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #10497 +/- ##
==========================================
+ Coverage 89.06% 89.25% +0.18%
==========================================
Files 103 104 +1
Lines 19685 19893 +208
Branches 3081 3118 +37
==========================================
+ Hits 17533 17756 +223
+ Misses 1490 1478 -12
+ Partials 662 659 -3 ☔ View full report in Codecov by Harness. |
A sealed stream cuts a byte stream into 1 MiB frames and puts every frame into the key's envelope (encrypted and authenticated in the encrypting modes, authenticated only in the authenticated-* modes), so neither the writer nor the reader needs memory for more than a frame. The AAD of a frame is a caller-given context plus the frame number and a "last frame" flag, so the reader detects changed, reordered, dropped or duplicated frames, truncation and trailing data. Every frame is sealed with encrypt_oneshot(), so sealed streams are thread-safe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#5041 The files cache (file sizes, timestamps, inode numbers, chunk lists and --digests content digests) was stored as plaintext msgpack in the local cache directory, protected only by a checksum without a key, so anyone who could read the cache directory could read it, and anyone who could write it could make borg skip changed files. It is now written as a sealed stream: encrypted and authenticated with the repository key (authenticated only in the authenticated-* modes), bound to the repository id and the file name of the files cache. A files cache that fails the verification is ignored with a warning (the exit code is not changed) and rebuilt from the previous archive. The checksums in the [integrity] section of the cache config are not needed any more, so the cache config is no longer rewritten on every cache close(). The FAQ about cache security now describes what is protected and what is not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ThomasWaldmann
force-pushed
the
encrypt-files-cache
branch
from
October 10, 2026 22:07
7e8e0bd to
185c5a3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The files cache (
files.<suffix>in the local cache directory) was stored as plaintext msgpack: file sizes, timestamps, inode numbers, chunk lists and, withborg create --digests, unkeyed digests of the whole file contents. Its only protection was a checksum without a key in the cacheconfig. So anyone who could read the cache directory could read that metadata, and anyone who could write to it could make borg treat changed files as unchanged.This PR protects the files cache with the repository key, like the
index/andcache/objects in the repository (#9819, #10235).Fixes #5041.
Changes
Commit 1: sealed streams (
borg.crypto.sealed_stream)A reusable layer that cuts a byte stream into 1 MiB frames and puts every frame into the key's envelope. Frames are encrypted and authenticated in the encrypting modes, authenticated only in the
authenticated-*modes.encrypt_oneshot(): its own random session id, IV 0. That makes the layer thread-safe. It costs about 3 µs extra per frame, so about 3 ms per GiB, and nothing extra when reading.Cache.close(), when the in-memory files cache and chunks index are still loaded.encrypt_oneshot()docstring is reworded: frames of about 1 MiB are fine, millions of chunks are not.Commit 2: the files cache uses it
b"borg-files-cache\0"+ repository id + the file name of the files cache. A files cache copied from another repository (with the same key material) or from another archive series is rejected.[integrity]checksum section of the cache config is removed (IntegrityCheckedFilestays for reading borg 1.x repos). Nothing in the config changes after its creation any more, soCache.close()no longer rewrites it, and the "Saving cache config" progress message is gone.files.<store hash of the series name>.data-structures.rst: new "Sealed streams" section, files cache on-disk format, the checksumming section (now only about borg 1.x repos);security.rst;frontends.rstprogress example.Measurements
Serialization layer only, 1M files cache entries (98 MiB on disk), on a ramdisk:
IntegrityCheckedFile)Writing got faster, because
IntegrityCheckedFilehashed every smallwrite()separately, while the new writer collects data into 1 MiB frames first.Tests
testsuite/crypto/sealed_stream_test.py: round trips at and around the frame boundaries, the frame size limit, and the one-off sessions.authenticated-*key types.cache_test.py: a round trip over many frames, an empty cache, a missing cache (no warning), and that no plaintext is left in the file.[integrity]section and is not rewritten byclose().create_cmd_test.py: a corrupted files cache gives a warning and rc 0, and the unchanged file is still found unchanged.Local run on macOS: full suite green, except
test_migrate_lock_alive[archiver], a FUSE mount timeout that also fails locally without this change.🤖 Generated with Claude Code