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
KazukiKandaKK
added a commit
to KazukiKandaKK/borg
that referenced
this pull request
Nov 1, 2026
…r last_written PR borgbackup#10497 (sealed files cache) removed CacheConfig.save(), so on current master the borg2 cache config file's mtime stays at cache creation time and no longer tracks last use. last_written is now the newer of: - the security location file's mtime (rewritten by Cache.close() via security_manager.save() on every run; also gives security-only repos a value) - the newest mtime of the regular files below the cache dir, tracked during the existing cache_stats() walk that already computes size The JSON field is renamed from cache_config_mtime to last_written. Epilog/docs/man page say where the timestamp comes from and say "regular file" to match what the code tracks. The long run-on epilog sentence is split into shorter sentences. Tests assert which source wins (location-newer, cache-newer including a nested file) and the cache-only case now checks the exact mtime instead of only non-null.
ThomasWaldmann
pushed a commit
that referenced
this pull request
Nov 1, 2026
…0453 * known-repos: new command to list locally known repositories Lists repositories this local Borg installation has state for (security info and/or a local cache) without accessing any repository, local or remote. Scans both the current borg2 security/cache directories and, separately, the legacy borg 1.x security/cache directories (which can be different default locations, e.g. on macOS). Does not follow symlinks anywhere in the scanned trees (repo-id directories, or the location/key-type files inside them), since borg only ever writes plain directories and regular files there. Key-type labels are derived from the key classes themselves (LEGACY_KEY_TYPES + AVAILABLE_KEY_TYPES), matching how "borg repo-info" names crypto suites, rather than a separately hand-maintained table. dir_size() pushes each directory handle (root, and any subdirectory opened while walking the cache tree) onto its explicit stack before calling list_names() on it, not after, so cleanup can always close it even if list_names() raises something other than OSError (it only catches OSError itself) - e.g. KeyboardInterrupt. Covered by two unit tests that monkeypatch list_names() to raise and then check the fd via os.fstat(). has_cache and has_security_info are tri-state (True/False/None, not a plain bool): an inspection failure (e.g. EACCES on a repo-id directory, or on the security/cache root itself, including a root that opens but fails during its own listing) is reported as unknown rather than being collapsed into a false "no". A repo-id directory that could be listed but not opened/classified is still included in the results with its fields set to unknown, instead of silently disappearing from the output. The JSON/text field for which state-directory layout a repository was discovered under is named "layout" (values "borg2" / "legacy" / null), not "version", so it cannot be confused with Borg's actual repository format version (see "borg repo-info"). Adds docs/usage/known-repos.rst{,.inc} and the usage.rst toctree entry, and the generated man page. * known-repos: simplify after maintainer review, drop leftover defenses Rewrite the scanner around ordinary OS file access instead of extra symlink/size defenses, collapse duplicated scan/table/test logic, and surface real inspection failures (ENOTDIR, unparseable cache config) instead of treating them as absence. Also forward create=False through legacy get_keys_dir(), refresh the usage example, keep tests to the remaining behavioral surface, and shorten a few overly verbose test docstrings. * known-repos: fix black formatting, restore generated usage/man docs black wants the process_epilog("""...""") call's opening triple-quote on its own line and the closing paren on a separate line. docs/usage/known-repos.rst still includes known-repos.rst.inc, which a previous commit deleted along with the generated man page - this broke the docs build (InputError: no such file). Regenerate both with scripts/make.py build_usage/build_man, following the convention other recently added commands (copy, find) already use. * known-repos: address second-review minor points - location_mismatch now compares the security location and cache previous_location through normalize_local_path(), the same way assert_location_matches() does, so an NFC vs. NFD unicode-form difference alone isn't flagged as a mismatch on macOS (#2913). Added a regression test for it. - The table no longer truncates the repository id to 15 chars - users acting on an entry (e.g. removing a stale cache dir) need the full id. Strengthened the existing test assertion to match. Updated the epilog and the usage example accordingly (full id was previously JSON-only), and regenerated docs/usage/known-repos.rst.inc and docs/man/borg-known-repos.1 to match. - legacy/fs.py: dropped the "(and now also the known-repos command)" historical phrasing from the module docstring; the following paragraph already lists both callers. - Epilog: mentioned that a security-only repo gets layout "unknown" when BORG_SECURITY_DIR makes the borg2 and legacy security roots coincide. Thomas noted the behavior itself is fine either way, just worth a brief mention. * known-repos: address PyPy msgpack warning and Windows encoding failures Fixes two CI failures reported in maintainer follow-up review: - PyPy: test_known_repos_without_repo_argument and test_known_repos_multiple_sorted_by_id failed with JSONDecodeError because pure-python msgpack emits PURE_PYTHON_MSGPACK_WARNING on PyPy, which contaminated exec_cmd() output. Introduce cmd_without_repo() helper that filters the warning line identically to cmd() while avoiding --repo injection, as suggested. - Windows: test_known_repos_location_mismatch_ignores_nfc_nfd_difference crashed with UnicodeEncodeError writing NFD path in cp1252. Skip the NFD test on Windows, as suggested. - Windows/scanner: read state files using default locale encoding matching borg's SaveFile/SyncFile implementation, dropping explicit encoding="utf-8", as suggested. * known-repos: use security location mtime + cache newest-file mtime for last_written PR #10497 (sealed files cache) removed CacheConfig.save(), so on current master the borg2 cache config file's mtime stays at cache creation time and no longer tracks last use. last_written is now the newer of: - the security location file's mtime (rewritten by Cache.close() via security_manager.save() on every run; also gives security-only repos a value) - the newest mtime of the regular files below the cache dir, tracked during the existing cache_stats() walk that already computes size The JSON field is renamed from cache_config_mtime to last_written. Epilog/docs/man page say where the timestamp comes from and say "regular file" to match what the code tracks. The long run-on epilog sentence is split into shorter sentences. Tests assert which source wins (location-newer, cache-newer including a nested file) and the cache-only case now checks the exact mtime instead of only non-null.
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