Skip to content

key: unify keyfile/repokey classes, locate key independent of type byte (#9743)#9767

Merged
ThomasWaldmann merged 2 commits into
borgbackup:masterfrom
ThomasWaldmann:unify-keyfile-repokey
Jun 13, 2026
Merged

key: unify keyfile/repokey classes, locate key independent of type byte (#9743)#9767
ThomasWaldmann merged 2 commits into
borgbackup:masterfrom
ThomasWaldmann:unify-keyfile-repokey

Conversation

@ThomasWaldmann

Copy link
Copy Markdown
Member

What

Make key location independent of the manifest key-type byte, and collapse the duplicated keyfile/repokey key classes into one class per crypto suite.

Builds on #9762 (multiple borg keys per repository), which is already merged to master, so this PR contains only the unify commit.

Why

Borg used to read the manifest's key-type byte and then look for the key in exactly one place (keyfile or repokey) depending on the key class that byte selected. As a result every crypto suite was duplicated into a keyfile class and a repokey class that differed only in TYPE, NAME, ARG_NAME and STORAGE.

How

  • Detection is storage-agnostic: it tries keyfiles first, then repokeys, until a passphrase unlocks a key. The type byte still selects the crypto suite (id hash, MAC, cipher) to instantiate. Where a key is stored (keyfile vs repokey) is now a per-key property (self.storage), not a separate class — so a repository may even hold a mix of keyfile- and repo-stored borg keys.
  • Class pairs collapsed into one class per suite:
    • modern AEAD: AESOCBKey, CHPOKey, Blake3AESOCBKey, Blake3CHPOKey
    • legacy borg 1.x (read-only): AESCTRKey, Blake2AESCTRKey
      There is now exactly one type byte per modern suite (the separate repokey type bytes 0x11/0x21/0x31/0x41 were removed; borg2 is beta and only needs to read repos it created). identify_key() matches on TYPES_ACCEPTABLE.
  • CLI: --encryption selects only the crypto suite (aes-ocb, chacha20-poly1305, blake3-aes-ocb, blake3-chacha20-poly1305, authenticated*, none); the storage location is chosen with the new --key-location=repokey|keyfile (default repokey). The old combined modes (repokey-aes-ocb etc.) were removed. borg key import also gained --key-location. borg key change-location no longer swaps key classes or rewrites the manifest; it just re-saves the unlocked key at the new location.
  • Keyfile removal (key remove, change-location) now overwrites the keyfile with random data via secure_erase() before unlinking, consistent with save().

Compatibility

  • borg 1.x legacy read compatibility is preserved — the legacy class merge is a behavior-preserving rename and the legacy type bytes (incl. PASSPHRASE) stay in TYPES_ACCEPTABLE.
  • No backward compatibility kept for repos created by older borg2 betas (intentional, per the beta status).

Tests

  • Full suite green locally (1633 passed, 82 skipped).
  • Added/updated tests: storage-agnostic unlock, change-location both directions, mixed keyfile+repokey multi-key repos, --key-location round-trips.
  • Manually verified end-to-end (create keyfile repo → move to repokey → unlock with the keyfile gone; mixed-storage multi-key repo unlocks from both passphrases).

🤖 Generated with Claude Code

…te (borgbackup#9743)

Borg used to read the manifest's key-type byte and then look for the key in
exactly one place (keyfile or repokey) depending on the key class that byte
selected. As a result every crypto suite was duplicated into a keyfile class
and a repokey class that differed only in TYPE, NAME, ARG_NAME and STORAGE.

Now key *location* is independent of the type byte: detection tries keyfiles
first and repokeys afterwards until a passphrase unlocks a key. The type byte
still selects the crypto suite (id hash, MAC, cipher) to instantiate. Where a
key is stored (keyfile vs repokey) is therefore a per-key property
(self.storage), not a separate class, so a repository may even hold a mix of
keyfile- and repo-stored borg keys.

With storage decoupled from class identity, the keyfile/repokey class pairs
collapse into one class per crypto suite:
- modern AEAD: AESOCBKey, CHPOKey, Blake3AESOCBKey, Blake3CHPOKey
- legacy borg 1.x (read-only): AESCTRKey, Blake2AESCTRKey
There is now exactly one type byte per modern crypto suite (the old separate
repokey type bytes 0x11/0x21/0x31/0x41 were removed; borg2 is beta and only
needs to read repos it created). identify_key() matches on TYPES_ACCEPTABLE.

CLI: --encryption selects only the crypto suite (aes-ocb, chacha20-poly1305,
blake3-aes-ocb, blake3-chacha20-poly1305, authenticated*, none); the storage
location is chosen with the new --key-location=repokey|keyfile (default
repokey). The old combined modes (repokey-aes-ocb etc.) were removed.
borg key import also gained --key-location. borg key change-location no longer
swaps key classes or rewrites the manifest; it just re-saves the unlocked key
at the new location.

Keyfile removal (key remove, change-location) now overwrites the keyfile with
random data via secure_erase() before unlinking, consistent with save().

borg 1.x legacy read compatibility is preserved (the legacy class merge is a
behavior-preserving rename; the legacy type bytes incl. PASSPHRASE stay in
TYPES_ACCEPTABLE).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.11111% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.91%. Comparing base (af0904f) to head (8f4231d).
⚠️ Report is 5 commits behind head on master.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/borg/crypto/key.py 86.15% 2 Missing and 7 partials ⚠️
src/borg/archiver/key_cmds.py 64.28% 3 Missing and 2 partials ⚠️
src/borg/crypto/keymanager.py 83.33% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #9767      +/-   ##
==========================================
+ Coverage   84.85%   84.91%   +0.06%     
==========================================
  Files          92       92              
  Lines       15172    15107      -65     
  Branches     2273     2260      -13     
==========================================
- Hits        12874    12828      -46     
+ Misses       1593     1581      -12     
+ Partials      705      698       -7     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@PhrozenByte

Copy link
Copy Markdown
Contributor

🎉 🚀

I'd like to reiterate the idea from #9168 of dropping the former --encryption none in favour of a separate --unsafe-unencrypted option (or similar), and repurposing --encryption none for the former authenticated mode instead (authenticated-blake3 could become blake3-none). Also see our discussion and findings in #9104.

@ThomasWaldmann

Copy link
Copy Markdown
Member Author

@PhrozenByte Scope of this PR is only finding the key no matter where it is (keyfile vs. repokey) and decouple the key location from type-bytes in the repository.

I put these 2 tickets into the b22 milestone to have a look at them again later.

The authenticated and authenticated-blake3 modes do not encrypt data, but
they still have a real key (id/auth key material) stored as a key blob.
That blob can live as a keyfile or as a repokey just like the encrypted
modes, so make it configurable instead of always forcing repokey storage.

- AuthenticatedKeyBase: set LOCATION_CONFIGURABLE = True so --key-location
  (at repo-create) and "borg key change-location" apply.
- key change-location: only copy sessionid/cipher when present (those are
  AEAD-only; authenticated keys do not have them).
- repo-info: report the key storage location for authenticated keys too,
  and handle the authenticated-blake3 variant (was only "authenticated").
- repo-create help: stop claiming authenticated* has no keyfile/repokey
  storage; only "none" truly has no key.
- add change-location round-trip tests for authenticated mode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ThomasWaldmann ThomasWaldmann marked this pull request as ready for review June 13, 2026 19:01
@ThomasWaldmann ThomasWaldmann merged commit b2d54c6 into borgbackup:master Jun 13, 2026
19 checks passed
@ThomasWaldmann ThomasWaldmann deleted the unify-keyfile-repokey branch June 13, 2026 20:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants