From 29a760e2b3b119ef03ea73e34942890f8ee45b02 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 15 Jun 2026 17:27:00 +0200 Subject: [PATCH 1/6] repo-create: split --encryption into --encryption + --id-hash, #9168 The combined --encryption value packed two orthogonal dimensions (cipher / AE algorithm and id hash function) into a single string, causing a combinatorial explosion of mode names. Key location was already split out into --key-location. Now: - --encryption selects only the cipher / AE algorithm: none, authenticated, aes256-ocb, chacha20-poly1305 - --id-hash selects the id hash function: sha256 (default) or blake3 - --key-location (unchanged) selects key storage: repokey (default) or keyfile The old combined names were removed (clean break): select a BLAKE3 suite via --encryption ... --id-hash blake3 instead of blake3-*. aes-ocb was renamed to aes256-ocb (key NAME shown by repo-info and ARG_NAME in JSON updated to match). "none" has no key, so it only supports the sha256 id hash. No on-disk format, key-type byte, or crypto behavior changes: the existing key classes form a clean cross-product of {cipher} x {id-hash}, selected via the new ENC_NAME / IDHASH_NAME class attributes. Co-Authored-By: Claude Opus 4.8 --- docs/changes.rst | 10 +- docs/usage/repo-create.rst.inc | 110 +++++++++--------- docs/usage/transfer.rst | 10 +- src/borg/archiver/repo_create_cmd.py | 85 +++++++------- src/borg/archiver/repo_info_cmd.py | 7 +- src/borg/crypto/key.py | 65 +++++++++-- src/borg/testsuite/archiver/__init__.py | 8 +- src/borg/testsuite/archiver/check_cmd_test.py | 4 +- src/borg/testsuite/archiver/key_cmds_test.py | 12 +- .../archiver/repo_create_cmd_test.py | 38 ++++++ src/borg/testsuite/benchmark_test.py | 2 +- 11 files changed, 218 insertions(+), 133 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 39ea315d72..61533a7cb7 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -168,12 +168,20 @@ above. New features: +- repo-create: split ``--encryption`` into orthogonal options. ``--encryption`` now + selects only the cipher / AE algorithm (``none``, ``authenticated``, ``aes256-ocb`` + or ``chacha20-poly1305``), the new ``--id-hash`` selects the id hash function + (``sha256`` (default) or ``blake3``), and ``--key-location`` (already present) selects + the key storage. The old combined names were removed: select a BLAKE3 suite via + ``--encryption ... --id-hash blake3`` instead of ``blake3-*``, and note that + ``aes-ocb`` was renamed to ``aes256-ocb``. #9168 - key: unify keyfile/repokey key classes and locate the key independently of the manifest key-type byte. Borg now tries keyfiles first and repokeys afterwards until a passphrase unlocks a key, so where a key is stored (keyfile vs repokey) is a per-key property rather than a separate key class. The key-type byte still selects the crypto suite (id hash, MAC, cipher). ``borg repo-create --encryption`` now takes - only the crypto suite (e.g. ``aes-ocb``, ``chacha20-poly1305``, ``blake3-aes-ocb``); + only the crypto suite (e.g. ``aes256-ocb``, ``chacha20-poly1305``; see #9168 above for + the further split into ``--encryption`` and ``--id-hash``); choose the storage location with the new ``--key-location=repokey|keyfile`` option (default: ``repokey``). The old combined modes (``repokey-aes-ocb`` etc.) were removed. ``borg key import`` also gained ``--key-location``. #9743 diff --git a/docs/usage/repo-create.rst.inc b/docs/usage/repo-create.rst.inc index 23b664b76a..7f75c1fe1a 100644 --- a/docs/usage/repo-create.rst.inc +++ b/docs/usage/repo-create.rst.inc @@ -12,23 +12,25 @@ borg repo-create .. class:: borg-options-table - +-------------------------------------------------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | **options** | - +-------------------------------------------------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | | ``--other-repo SRC_REPOSITORY`` | reuse the key material from the other repository | - +-------------------------------------------------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | | ``--from-borg1`` | other repository is Borg 1.x | - +-------------------------------------------------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | | ``-e MODE``, ``--encryption MODE`` | select encryption crypto suite **(required)** | - +-------------------------------------------------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | | ``--key-location LOCATION`` | where to store the key: 'repokey' (in the repository, default) or 'keyfile' (in the local keys directory). Ignored for the 'none' mode (which has no key). | - +-------------------------------------------------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | | ``--copy-crypt-key`` | copy the crypt_key (used for authenticated encryption) from the key of the other repository (default: new random key). | - +-------------------------------------------------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | .. class:: borg-common-opt-ref | - | | - | :ref:`common_options` | - +-------------------------------------------------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ + +-------------------------------------------------------+------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **options** | + +-------------------------------------------------------+------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--other-repo SRC_REPOSITORY`` | reuse the key material from the other repository | + +-------------------------------------------------------+------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--from-borg1`` | other repository is Borg 1.x | + +-------------------------------------------------------+------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-e ENCRYPTION``, ``--encryption ENCRYPTION`` | select cipher / AE algorithm: 'none', 'authenticated', 'aes256-ocb' or 'chacha20-poly1305' **(required)** | + +-------------------------------------------------------+------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-i HASH``, ``--id-hash HASH`` | select the id hash function: 'sha256' (default) or 'blake3'. The 'none' encryption only supports 'sha256'. | + +-------------------------------------------------------+------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--key-location LOCATION`` | where to store the key: 'repokey' (in the repository, default) or 'keyfile' (in the local keys directory). Ignored for the 'none' mode (which has no key). | + +-------------------------------------------------------+------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--copy-crypt-key`` | copy the crypt_key (used for authenticated encryption) from the key of the other repository (default: new random key). | + +-------------------------------------------------------+------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ .. raw:: html @@ -44,10 +46,11 @@ borg repo-create options --other-repo SRC_REPOSITORY reuse the key material from the other repository - --from-borg1 other repository is Borg 1.x - -e MODE, --encryption MODE select encryption crypto suite **(required)** - --key-location LOCATION where to store the key: 'repokey' (in the repository, default) or 'keyfile' (in the local keys directory). Ignored for the 'none' mode (which has no key). - --copy-crypt-key copy the crypt_key (used for authenticated encryption) from the key of the other repository (default: new random key). + --from-borg1 other repository is Borg 1.x + -e ENCRYPTION, --encryption ENCRYPTION select cipher / AE algorithm: 'none', 'authenticated', 'aes256-ocb' or 'chacha20-poly1305' **(required)** + -i HASH, --id-hash HASH select the id hash function: 'sha256' (default) or 'blake3'. The 'none' encryption only supports 'sha256'. + --key-location LOCATION where to store the key: 'repokey' (in the repository, default) or 'keyfile' (in the local keys directory). Ignored for the 'none' mode (which has no key). + --copy-crypt-key copy the crypt_key (used for authenticated encryption) from the key of the other repository (default: new random key). :ref:`common_options` @@ -73,7 +76,7 @@ tips will come below): :: - borg repo-create --encryption aes-ocb --key-location repokey + borg repo-create --encryption aes256-ocb --key-location repokey Borg will: @@ -115,14 +118,29 @@ a different keyboard layout. You can change your passphrase for existing repositories at any time; it will not affect the encryption/decryption key or other secrets. -Choosing an encryption mode -+++++++++++++++++++++++++++ +Choosing a crypto suite ++++++++++++++++++++++++ Depending on your hardware, hashing and crypto performance may vary widely. The easiest way to find out what is fastest is to run ``borg benchmark cpu``. -The encryption mode (``--encryption``) only selects the crypto suite (id hash, encryption -and authentication). Where the key is stored is chosen separately with ``--key-location``: +A crypto suite is selected by three orthogonal options: + +``--encryption`` (**required**) selects the cipher / authenticated-encryption algorithm: + +- ``aes256-ocb``: AES256 in OCB mode (encryption + authentication). +- ``chacha20-poly1305``: ChaCha20 + Poly1305 (encryption + authentication). +- ``authenticated``: no encryption, but still authenticates your data (tamper detection). +- ``none``: no encryption and no authentication (see the warning below). + +``--id-hash`` selects the id hash function (used for chunk ids and authentication): + +- ``sha256`` (default): HMAC-SHA-256 (or plain SHA-256 for the ``none`` encryption). +- ``blake3``: BLAKE3. Often faster on CPUs without SHA hardware acceleration. + +The ``none`` encryption has no key, so it only supports the ``sha256`` id hash. + +``--key-location`` selects where the key is stored (orthogonal to the crypto suite): - ``repokey`` (default): the key is stored in the repository (under ``keys/``). Pick this if you want ease-of-use and "passphrase" security is good enough. @@ -130,43 +148,19 @@ and authentication). Where the key is stored is chosen separately with ``--key-l this if you want "passphrase and having-the-key" security. You can move the key between these locations later with ``borg key change-location``. -This also applies to the ``authenticated*`` modes: they do not encrypt your data, but they -still have a key (used for the id hash and authentication), so ``--key-location`` selects -where that key is stored, just like for the encrypted modes. -``--key-location`` is only ignored for the ``none`` mode, which has no key at all. - -The following table is roughly sorted in order of preference, the better ones are -in the upper part of the table, in the lower part is the old and/or unsafe(r) stuff: - -.. nanorst: inline-fill - -+-----------------------------------+--------------+----------------+--------------------+ -| Encryption mode | ID-Hash | Encryption | Authentication | -+-----------------------------------+--------------+----------------+--------------------+ -| blake3-chacha20-poly1305 | BLAKE3 | CHACHA20 | POLY1305 | -+-----------------------------------+--------------+----------------+--------------------+ -| chacha20-poly1305 | HMAC-SHA-256 | CHACHA20 | POLY1305 | -+-----------------------------------+--------------+----------------+--------------------+ -| blake3-aes-ocb | BLAKE3 | AES256-OCB | AES256-OCB | -+-----------------------------------+--------------+----------------+--------------------+ -| aes-ocb | HMAC-SHA-256 | AES256-OCB | AES256-OCB | -+-----------------------------------+--------------+----------------+--------------------+ -| authenticated-blake3 | BLAKE3 | none | BLAKE3 | -+-----------------------------------+--------------+----------------+--------------------+ -| authenticated | HMAC-SHA-256 | none | HMAC-SHA256 | -+-----------------------------------+--------------+----------------+--------------------+ -| none | SHA-256 | none | none | -+-----------------------------------+--------------+----------------+--------------------+ - -.. nanorst: inline-replace - -`none` mode uses no encryption and no authentication. You are advised NOT to use this mode +This also applies to the ``authenticated`` encryption: it does not encrypt your data, but it +still has a key (used for the id hash and authentication), so ``--key-location`` selects +where that key is stored, just like for the encrypted suites. +``--key-location`` is only ignored for the ``none`` encryption, which has no key at all. + +`none` encryption uses no encryption and no authentication. You are advised NOT to use this as it would expose you to a Denial-of-Service risk (due to how the :ref:`internals_hashindex` works) and other issues (confidentiality, tampering, ...) in case of malicious activity in the repository. If you do **not** want to encrypt the contents of your backups, but still want to detect -malicious tampering, use an `authenticated` mode. It is like `repokey` minus encryption. +malicious tampering, use ``--encryption authenticated``. It is like an encrypted suite +minus the data encryption. To normally work with ``authenticated`` repositories, you will need the passphrase, but there is an emergency workaround; see ``BORG_WORKAROUNDS=authenticated_no_key`` docs. diff --git a/docs/usage/transfer.rst b/docs/usage/transfer.rst index 9a85d36080..22e729debe 100644 --- a/docs/usage/transfer.rst +++ b/docs/usage/transfer.rst @@ -21,13 +21,13 @@ locations and passphrases first: # 1. Create a new "related" repository: # Here, the existing Borg 1.x repository used repokey (and AES-CTR mode), - # thus we use aes-ocb for the new Borg 2.0 repository. + # thus we use aes256-ocb for the new Borg 2.0 repository. # Staying with the same chunk ID algorithm (hmac-sha256) and with the same # key material (via BORG_OTHER_REPO) will make deduplication work # between old archives (copied with borg transfer) and future ones. # The AEAD cipher does not matter (everything must be re-encrypted and # re-authenticated anyway); you could also choose chacha20-poly1305. - $ borg repo-create -e aes-ocb + $ borg repo-create -e aes256-ocb # 2. Check what and how much it would transfer: $ borg transfer --from-borg1 --dry-run @@ -46,15 +46,15 @@ locations and passphrases first: # 1. Create a new "related" repository: # Here, the existing Borg 1.x repository used repokey-blake2 (and AES-CTR mode), - # thus we use blake3-aes-ocb for the new Borg 2.0 repository. + # thus we use aes256-ocb with --id-hash blake3 for the new Borg 2.0 repository. # We need to change from blake2 to blake3, because blake2 is not supported # for borg2 repos (blake3 is much faster). Because we change how chunk IDs are # computed, we need to re-chunk everything while doing the transfer. # The chunker parameters you provide here should be the same as you will # use for all future Borg 2.0 archives. # The AEAD cipher does not matter (everything must be re-encrypted and - # re-authenticated anyway); you could also choose blake3-chacha20-poly1305. - $ borg repo-create -e blake3-aes-ocb + # re-authenticated anyway); you could also choose -e chacha20-poly1305 -i blake3. + $ borg repo-create -e aes256-ocb -i blake3 $ export CHUNKER_PARAMS="buzhash64,19,23,21,4095" # 2. Check what and how much it would transfer: diff --git a/src/borg/archiver/repo_create_cmd.py b/src/borg/archiver/repo_create_cmd.py index f021349d46..7e493617fd 100644 --- a/src/borg/archiver/repo_create_cmd.py +++ b/src/borg/archiver/repo_create_cmd.py @@ -1,7 +1,7 @@ from ._common import with_repository, with_other_repository, Highlander from ..cache import Cache from ..constants import * # NOQA -from ..crypto.key import key_creator, key_argument_names +from ..crypto.key import key_creator, encryption_argument_names, id_hash_argument_names from ..helpers import CancelledByUser from ..helpers import location_validator, Location from ..helpers.argparsing import ArgumentParser @@ -77,7 +77,7 @@ def build_parser_repo_create(self, subparsers, common_parser, mid_common_parser) :: - borg repo-create --encryption aes-ocb --key-location repokey + borg repo-create --encryption aes256-ocb --key-location repokey Borg will: @@ -119,14 +119,29 @@ def build_parser_repo_create(self, subparsers, common_parser, mid_common_parser) You can change your passphrase for existing repositories at any time; it will not affect the encryption/decryption key or other secrets. - Choosing an encryption mode - +++++++++++++++++++++++++++ + Choosing a crypto suite + +++++++++++++++++++++++ Depending on your hardware, hashing and crypto performance may vary widely. The easiest way to find out what is fastest is to run ``borg benchmark cpu``. - The encryption mode (``--encryption``) only selects the crypto suite (id hash, encryption - and authentication). Where the key is stored is chosen separately with ``--key-location``: + A crypto suite is selected by three orthogonal options: + + ``--encryption`` (**required**) selects the cipher / authenticated-encryption algorithm: + + - ``aes256-ocb``: AES256 in OCB mode (encryption + authentication). + - ``chacha20-poly1305``: ChaCha20 + Poly1305 (encryption + authentication). + - ``authenticated``: no encryption, but still authenticates your data (tamper detection). + - ``none``: no encryption and no authentication (see the warning below). + + ``--id-hash`` selects the id hash function (used for chunk ids and authentication): + + - ``sha256`` (default): HMAC-SHA-256 (or plain SHA-256 for the ``none`` encryption). + - ``blake3``: BLAKE3. Often faster on CPUs without SHA hardware acceleration. + + The ``none`` encryption has no key, so it only supports the ``sha256`` id hash. + + ``--key-location`` selects where the key is stored (orthogonal to the crypto suite): - ``repokey`` (default): the key is stored in the repository (under ``keys/``). Pick this if you want ease-of-use and "passphrase" security is good enough. @@ -134,43 +149,19 @@ def build_parser_repo_create(self, subparsers, common_parser, mid_common_parser) this if you want "passphrase and having-the-key" security. You can move the key between these locations later with ``borg key change-location``. - This also applies to the ``authenticated*`` modes: they do not encrypt your data, but they - still have a key (used for the id hash and authentication), so ``--key-location`` selects - where that key is stored, just like for the encrypted modes. - ``--key-location`` is only ignored for the ``none`` mode, which has no key at all. - - The following table is roughly sorted in order of preference, the better ones are - in the upper part of the table, in the lower part is the old and/or unsafe(r) stuff: - - .. nanorst: inline-fill - - +-----------------------------------+--------------+----------------+--------------------+ - | Encryption mode | ID-Hash | Encryption | Authentication | - +-----------------------------------+--------------+----------------+--------------------+ - | blake3-chacha20-poly1305 | BLAKE3 | CHACHA20 | POLY1305 | - +-----------------------------------+--------------+----------------+--------------------+ - | chacha20-poly1305 | HMAC-SHA-256 | CHACHA20 | POLY1305 | - +-----------------------------------+--------------+----------------+--------------------+ - | blake3-aes-ocb | BLAKE3 | AES256-OCB | AES256-OCB | - +-----------------------------------+--------------+----------------+--------------------+ - | aes-ocb | HMAC-SHA-256 | AES256-OCB | AES256-OCB | - +-----------------------------------+--------------+----------------+--------------------+ - | authenticated-blake3 | BLAKE3 | none | BLAKE3 | - +-----------------------------------+--------------+----------------+--------------------+ - | authenticated | HMAC-SHA-256 | none | HMAC-SHA256 | - +-----------------------------------+--------------+----------------+--------------------+ - | none | SHA-256 | none | none | - +-----------------------------------+--------------+----------------+--------------------+ - - .. nanorst: inline-replace - - `none` mode uses no encryption and no authentication. You are advised NOT to use this mode + This also applies to the ``authenticated`` encryption: it does not encrypt your data, but it + still has a key (used for the id hash and authentication), so ``--key-location`` selects + where that key is stored, just like for the encrypted suites. + ``--key-location`` is only ignored for the ``none`` encryption, which has no key at all. + + `none` encryption uses no encryption and no authentication. You are advised NOT to use this as it would expose you to a Denial-of-Service risk (due to how the :ref:`internals_hashindex` works) and other issues (confidentiality, tampering, ...) in case of malicious activity in the repository. If you do **not** want to encrypt the contents of your backups, but still want to detect - malicious tampering, use an `authenticated` mode. It is like `repokey` minus encryption. + malicious tampering, use ``--encryption authenticated``. It is like an encrypted suite + minus the data encryption. To normally work with ``authenticated`` repositories, you will need the passphrase, but there is an emergency workaround; see ``BORG_WORKAROUNDS=authenticated_no_key`` docs. @@ -217,12 +208,24 @@ def build_parser_repo_create(self, subparsers, common_parser, mid_common_parser) subparser.add_argument( "-e", "--encryption", - metavar="MODE", + metavar="ENCRYPTION", dest="encryption", required=True, - choices=key_argument_names(), + choices=encryption_argument_names(), + action=Highlander, + help="select cipher / AE algorithm: 'none', 'authenticated', 'aes256-ocb' or " + "'chacha20-poly1305' **(required)**", + ) + subparser.add_argument( + "-i", + "--id-hash", + metavar="HASH", + dest="id_hash", + choices=id_hash_argument_names(), + default="sha256", action=Highlander, - help="select encryption crypto suite **(required)**", + help="select the id hash function: 'sha256' (default) or 'blake3'. " + "The 'none' encryption only supports 'sha256'.", ) subparser.add_argument( "--key-location", diff --git a/src/borg/archiver/repo_info_cmd.py b/src/borg/archiver/repo_info_cmd.py index 3fda372542..3c3f1af086 100644 --- a/src/borg/archiver/repo_info_cmd.py +++ b/src/borg/archiver/repo_info_cmd.py @@ -25,9 +25,10 @@ def do_repo_info(self, args, repository, manifest, cache): # storage (keyfile/repokey) is a per-key property now; the crypto suite is key.NAME. storage = getattr(key, "storage", None) mode = {KeyBlobStorage.KEYFILE: "keyfile", KeyBlobStorage.REPO: "repokey"}.get(storage) - if key.NAME in ("plaintext", "authenticated", "authenticated BLAKE3"): - # authenticated modes do not encrypt data, but (unlike plaintext) still have a key - # that is stored as a keyfile or repokey, so show that location when there is one. + if key.ENC_NAME in ("none", "authenticated"): + # the "none" and "authenticated" encryptions do not encrypt data; "authenticated" + # (unlike "none"/plaintext) still has a key stored as a keyfile or repokey, so show + # that location when there is one. encryption += "No (%s, %s)" % (mode, key.NAME) if mode else "No" else: encryption += "Yes (%s, %s)" % (mode, key.NAME) if mode else "Yes (%s)" % key.NAME diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index d0750bdc0e..6156aa85e2 100644 --- a/src/borg/crypto/key.py +++ b/src/borg/crypto/key.py @@ -127,16 +127,37 @@ class UnsupportedKeyFormatError(Error): def key_creator(repository, args, *, other_key=None): + # the crypto suite is selected by two orthogonal dimensions: the cipher / AE algorithm + # (--encryption) and the id hash function (--id-hash). id-hash is always significant, so e.g. + # "--encryption none --id-hash blake3" finds no match and is rejected (none only supports sha256). + enc = args.encryption + id_hash = getattr(args, "id_hash", "sha256") for key in AVAILABLE_KEY_TYPES: - if key.ARG_NAME == args.encryption: - assert key.ARG_NAME is not None + if key.ENC_NAME == enc and key.IDHASH_NAME == id_hash: return key.create(repository, args, other_key=other_key) - else: - raise ValueError('Invalid encryption mode "%s"' % args.encryption) + raise Error( + f'Unsupported --encryption "{enc}" / --id-hash "{id_hash}" combination ' + f'(the "none" encryption only supports the "sha256" id-hash).' + ) -def key_argument_names(): - return [key.ARG_NAME for key in AVAILABLE_KEY_TYPES if key.ARG_NAME] +def encryption_argument_names(): + # distinct cipher / AE algorithm names offered by "borg repo-create --encryption", in the + # order the key types are listed in AVAILABLE_KEY_TYPES (deduplicated, sha256/blake3 variants merge). + names = [] + for key in AVAILABLE_KEY_TYPES: + if key.ENC_NAME and key.ENC_NAME not in names: + names.append(key.ENC_NAME) + return names + + +def id_hash_argument_names(): + # distinct id hash function names offered by "borg repo-create --id-hash". + names = [] + for key in AVAILABLE_KEY_TYPES: + if key.IDHASH_NAME and key.IDHASH_NAME not in names: + names.append(key.IDHASH_NAME) + return names def identify_key(manifest_data): @@ -195,6 +216,14 @@ class KeyBase: # Name used in command line / API (e.g. borg init --encryption=...) ARG_NAME = "UNDEFINED" + # The two orthogonal dimensions a creatable crypto suite is selected by on the command line: + # ENC_NAME -> "borg repo-create --encryption" (cipher / AE algorithm) + # IDHASH_NAME -> "borg repo-create --id-hash" (id hash function) + # (key location is the third dimension, handled separately via --key-location). + # None means "not creatable this way" (e.g. legacy read-only classes). + ENC_NAME: ClassVar[str] = None # override in creatable subclasses + IDHASH_NAME: ClassVar[str] = None # override in creatable subclasses (or via id-hash mix-in) + # Storage type (no key blob storage / keyfile / repo). This is only a default seed for the # per-instance self.storage; keyfile vs repokey is a property of an individual key, not the class. STORAGE: ClassVar[str] = KeyBlobStorage.NO_STORAGE @@ -298,6 +327,8 @@ class PlaintextKey(KeyBase): TYPES_ACCEPTABLE = {TYPE} NAME = "plaintext" ARG_NAME = "none" + ENC_NAME = "none" + IDHASH_NAME = "sha256" # plain sha256(data), no key; blake3 is not supported for "none" chunk_seed = 0 crypt_key = b"" # makes .derive_key() work, nothing secret here @@ -332,6 +363,8 @@ class ID_HMAC_SHA_256: The id_key length must be 32 bytes. """ + IDHASH_NAME = "sha256" + def id_hash(self, data): return hmac_sha256(self.id_key, data) @@ -963,8 +996,9 @@ def decrypt(self, id, data): class AuthenticatedKey(ID_HMAC_SHA_256, AuthenticatedKeyBase): TYPE = KeyType.AUTHENTICATED TYPES_ACCEPTABLE = {TYPE} - NAME = "authenticated" + NAME = "authenticated SHA256" ARG_NAME = "authenticated" + ENC_NAME = "authenticated" # IDHASH_NAME = "sha256" via ID_HMAC_SHA_256 mix-in # ------------ new crypto ------------ @@ -977,6 +1011,8 @@ class ID_BLAKE3_256: The id_key length must be 32 bytes. """ + IDHASH_NAME = "blake3" + def id_hash(self, data): return blake3(data, key=self.id_key).digest(length=32) @@ -986,6 +1022,7 @@ class Blake3AuthenticatedKey(ID_BLAKE3_256, AuthenticatedKeyBase): TYPES_ACCEPTABLE = {TYPE} NAME = "authenticated BLAKE3" ARG_NAME = "authenticated-blake3" + ENC_NAME = "authenticated" # IDHASH_NAME = "blake3" via ID_BLAKE3_256 mix-in class AEADKeyBase(KeyBase): @@ -1111,24 +1148,27 @@ def init_ciphers(self, manifest_data=None, iv=0): class AESOCBKey(ID_HMAC_SHA_256, AEADKeyBase, FlexiKey): TYPE = KeyType.AESOCB TYPES_ACCEPTABLE = {TYPE} - NAME = "AES-OCB" - ARG_NAME = "aes-ocb" + NAME = "SHA256 AES256-OCB" + ARG_NAME = "aes256-ocb" + ENC_NAME = "aes256-ocb" # IDHASH_NAME = "sha256" via ID_HMAC_SHA_256 mix-in CIPHERSUITE = AES256_OCB class CHPOKey(ID_HMAC_SHA_256, AEADKeyBase, FlexiKey): TYPE = KeyType.CHPO TYPES_ACCEPTABLE = {TYPE} - NAME = "ChaCha20-Poly1305" + NAME = "SHA256 ChaCha20-Poly1305" ARG_NAME = "chacha20-poly1305" + ENC_NAME = "chacha20-poly1305" # IDHASH_NAME = "sha256" via ID_HMAC_SHA_256 mix-in CIPHERSUITE = CHACHA20_POLY1305 class Blake3AESOCBKey(ID_BLAKE3_256, AEADKeyBase, FlexiKey): TYPE = KeyType.BLAKE3AESOCB TYPES_ACCEPTABLE = {TYPE} - NAME = "BLAKE3 AES-OCB" - ARG_NAME = "blake3-aes-ocb" + NAME = "BLAKE3 AES256-OCB" + ARG_NAME = "blake3-aes256-ocb" + ENC_NAME = "aes256-ocb" # IDHASH_NAME = "blake3" via ID_BLAKE3_256 mix-in CIPHERSUITE = AES256_OCB @@ -1137,6 +1177,7 @@ class Blake3CHPOKey(ID_BLAKE3_256, AEADKeyBase, FlexiKey): TYPES_ACCEPTABLE = {TYPE} NAME = "BLAKE3 ChaCha20-Poly1305" ARG_NAME = "blake3-chacha20-poly1305" + ENC_NAME = "chacha20-poly1305" # IDHASH_NAME = "blake3" via ID_BLAKE3_256 mix-in CIPHERSUITE = CHACHA20_POLY1305 diff --git a/src/borg/testsuite/archiver/__init__.py b/src/borg/testsuite/archiver/__init__.py index de59c41193..64422cb769 100644 --- a/src/borg/testsuite/archiver/__init__.py +++ b/src/borg/testsuite/archiver/__init__.py @@ -31,10 +31,10 @@ from ..platform.platform_test import is_win32 from ...xattr import get_all -# --encryption now selects only the crypto suite; key storage is chosen with --key-location -# (default: repokey). RK_* stays a single token (repokey is the default); for keyfile storage, -# pass KF_ENCRYPTION together with KF_LOCATION. -RK_ENCRYPTION = "--encryption=aes-ocb" +# --encryption selects only the cipher / AE algorithm; the id hash is chosen with --id-hash +# (default: sha256) and key storage with --key-location (default: repokey). RK_* stays a single +# token (defaults apply); for keyfile storage, pass KF_ENCRYPTION together with KF_LOCATION. +RK_ENCRYPTION = "--encryption=aes256-ocb" KF_ENCRYPTION = "--encryption=chacha20-poly1305" KF_LOCATION = "--key-location=keyfile" diff --git a/src/borg/testsuite/archiver/check_cmd_test.py b/src/borg/testsuite/archiver/check_cmd_test.py index 17804d7990..fdd7651afd 100644 --- a/src/borg/testsuite/archiver/check_cmd_test.py +++ b/src/borg/testsuite/archiver/check_cmd_test.py @@ -369,7 +369,7 @@ def test_extra_chunks(archivers, request): cmd(archiver, "check", "-v", exit_code=0) # check does not deal with orphans anymore -@pytest.mark.parametrize("init_args", [["--encryption=aes-ocb"], ["--encryption", "none"]]) +@pytest.mark.parametrize("init_args", [["--encryption=aes256-ocb"], ["--encryption", "none"]]) def test_verify_data(archivers, request, init_args): archiver = request.getfixturevalue(archivers) if archiver.get_kind() != "local": @@ -405,7 +405,7 @@ def test_verify_data(archivers, request, init_args): assert f"{src_file}: Missing file chunk detected" in output -@pytest.mark.parametrize("init_args", [["--encryption=aes-ocb"], ["--encryption", "none"]]) +@pytest.mark.parametrize("init_args", [["--encryption=aes256-ocb"], ["--encryption", "none"]]) def test_corrupted_file_chunk(archivers, request, init_args): ## similar to test_verify_data, but here we let the low level repository-only checks discover the issue. diff --git a/src/borg/testsuite/archiver/key_cmds_test.py b/src/borg/testsuite/archiver/key_cmds_test.py index 0897063758..418f8b0726 100644 --- a/src/borg/testsuite/archiver/key_cmds_test.py +++ b/src/borg/testsuite/archiver/key_cmds_test.py @@ -48,7 +48,7 @@ def test_change_location_to_keyfile(archivers, request): def test_change_location_to_b3keyfile(archivers, request): archiver = request.getfixturevalue(archivers) - cmd(archiver, "repo-create", "--encryption=blake3-aes-ocb") + cmd(archiver, "repo-create", "--encryption=aes256-ocb", "--id-hash=blake3") log = cmd(archiver, "repo-info") assert "(repokey, BLAKE3" in log cmd(archiver, "key", "change-location", "keyfile") @@ -68,7 +68,7 @@ def test_change_location_to_repokey(archivers, request): def test_change_location_to_b3repokey(archivers, request): archiver = request.getfixturevalue(archivers) - cmd(archiver, "repo-create", "--encryption=blake3-aes-ocb", KF_LOCATION) + cmd(archiver, "repo-create", "--encryption=aes256-ocb", "--id-hash=blake3", KF_LOCATION) log = cmd(archiver, "repo-info") assert "(keyfile, BLAKE3" in log cmd(archiver, "key", "change-location", "repokey") @@ -81,12 +81,12 @@ def test_change_location_authenticated_to_keyfile(archivers, request): archiver = request.getfixturevalue(archivers) cmd(archiver, "repo-create", "--encryption=authenticated") log = cmd(archiver, "repo-info") - assert "(repokey, authenticated)" in log + assert "(repokey, authenticated SHA256)" in log cmd(archiver, "key", "change-location", "keyfile") [key_filename] = os.listdir(archiver.keys_path) assert key_filename # key blob now lives as a keyfile log = cmd(archiver, "repo-info") - assert "(keyfile, authenticated)" in log + assert "(keyfile, authenticated SHA256)" in log def test_change_location_authenticated_to_repokey(archivers, request): @@ -94,11 +94,11 @@ def test_change_location_authenticated_to_repokey(archivers, request): cmd(archiver, "repo-create", "--encryption=authenticated", KF_LOCATION) assert os.listdir(archiver.keys_path) # key blob created as a keyfile log = cmd(archiver, "repo-info") - assert "(keyfile, authenticated)" in log + assert "(keyfile, authenticated SHA256)" in log cmd(archiver, "key", "change-location", "repokey") assert os.listdir(archiver.keys_path) == [] # keyfile removed after moving into the repo log = cmd(archiver, "repo-info") - assert "(repokey, authenticated)" in log + assert "(repokey, authenticated SHA256)" in log def test_keyfile_name_is_content_sha256(archivers, request): diff --git a/src/borg/testsuite/archiver/repo_create_cmd_test.py b/src/borg/testsuite/archiver/repo_create_cmd_test.py index 04446b6783..9ba4e94563 100644 --- a/src/borg/testsuite/archiver/repo_create_cmd_test.py +++ b/src/borg/testsuite/archiver/repo_create_cmd_test.py @@ -34,6 +34,44 @@ def test_repo_create_requires_encryption_option(archivers, request): cmd(archiver, "repo-create", exit_code=2) +@pytest.mark.parametrize( + "extra_args, expected", + [ + # --encryption x --id-hash -> crypto suite shown by "borg repo-info" + (["--encryption=aes256-ocb"], "Yes (repokey, SHA256 AES256-OCB)"), # default id-hash is sha256 + (["--encryption=aes256-ocb", "--id-hash=sha256"], "Yes (repokey, SHA256 AES256-OCB)"), + (["--encryption=aes256-ocb", "--id-hash=blake3"], "Yes (repokey, BLAKE3 AES256-OCB)"), + (["--encryption=chacha20-poly1305"], "Yes (repokey, SHA256 ChaCha20-Poly1305)"), + (["--encryption=chacha20-poly1305", "--id-hash=blake3"], "Yes (repokey, BLAKE3 ChaCha20-Poly1305)"), + (["--encryption=authenticated"], "No (repokey, authenticated SHA256)"), + (["--encryption=authenticated", "--id-hash=blake3"], "No (repokey, authenticated BLAKE3)"), + (["--encryption=none"], "No"), + ], +) +def test_repo_create_encryption_id_hash_combinations(archivers, request, extra_args, expected): + archiver = request.getfixturevalue(archivers) + cmd(archiver, "repo-create", *extra_args) + info = cmd(archiver, "repo-info") + assert expected in info + + +def test_repo_create_none_rejects_blake3(archivers, request): + # "none" (plaintext) has no key, so it only supports the sha256 id-hash. + archiver = request.getfixturevalue(archivers) + arg = ("repo-create", "--encryption=none", "--id-hash=blake3") + if archiver.FORK_DEFAULT: + cmd(archiver, *arg, exit_code=2) + else: + with pytest.raises(Error): + cmd(archiver, *arg) + + +def test_repo_create_rejects_legacy_combined_mode(archivers, request): + # clean break: the old combined "--encryption" names are no longer accepted (argparse choices). + archiver = request.getfixturevalue(archivers) + cmd(archiver, "repo-create", "--encryption=blake3-aes-ocb", exit_code=2) + + def test_repo_create_refuse_to_overwrite_keyfile(archivers, request, monkeypatch): # BORG_KEY_FILE=something borg repo-create should quit if "something" already exists. # See: https://github.com/borgbackup/borg/pull/6046 diff --git a/src/borg/testsuite/benchmark_test.py b/src/borg/testsuite/benchmark_test.py index c0423c62be..9fd93e3505 100644 --- a/src/borg/testsuite/benchmark_test.py +++ b/src/borg/testsuite/benchmark_test.py @@ -27,7 +27,7 @@ def repo_url(request, tmpdir, monkeypatch): tmpdir.remove(rec=1) -@pytest.fixture(params=["none", "aes-ocb"]) +@pytest.fixture(params=["none", "aes256-ocb"]) def repo(request, cmd_fixture, repo_url): cmd_fixture(f"--repo={repo_url}", "repo-create", "--encryption", request.param) return repo_url From 479a77b512c4dc88fe4999c19aea0d6a8394f610 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 15 Jun 2026 18:31:27 +0200 Subject: [PATCH 2/6] repo-info/list --json: report encryption + id_hash separately, #9168 Stop using key.ARG_NAME (the combined crypto-suite name) for the JSON output. The "encryption" object now mirrors the split CLI options: the "mode" field is replaced by "encryption" (cipher / AE algorithm, key.ENC_NAME) and "id_hash" (id hash function, key.IDHASH_NAME). This is a breaking change to the documented JSON API. Co-Authored-By: Claude Opus 4.8 --- docs/changes.rst | 4 +++- docs/internals/frontends.rst | 17 +++++++++++------ src/borg/helpers/parseformat.py | 5 ++++- .../testsuite/archiver/repo_info_cmd_test.py | 3 ++- .../testsuite/archiver/repo_list_cmd_test.py | 3 ++- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 61533a7cb7..b76075ec57 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -174,7 +174,9 @@ New features: (``sha256`` (default) or ``blake3``), and ``--key-location`` (already present) selects the key storage. The old combined names were removed: select a BLAKE3 suite via ``--encryption ... --id-hash blake3`` instead of ``blake3-*``, and note that - ``aes-ocb`` was renamed to ``aes256-ocb``. #9168 + ``aes-ocb`` was renamed to ``aes256-ocb``. The JSON output (``--json``) reflects this + too: the ``encryption.mode`` field was replaced by separate ``encryption.encryption`` + (cipher / AE algorithm) and ``encryption.id_hash`` fields. #9168 - key: unify keyfile/repokey key classes and locate the key independently of the manifest key-type byte. Borg now tries keyfiles first and repokeys afterwards until a passphrase unlocks a key, so where a key is stored (keyfile vs repokey) is a diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst index 7eff6714d8..fd436aff00 100644 --- a/docs/internals/frontends.rst +++ b/docs/internals/frontends.rst @@ -298,10 +298,12 @@ last_modified The *encryption* key, if present, contains: -mode - Textual encryption mode name (same as :ref:`borg_repo-create` ``--encryption`` names) +encryption + Textual cipher / AE algorithm name (same as :ref:`borg_repo-create` ``--encryption`` names) +id_hash + Textual id hash function name (same as :ref:`borg_repo-create` ``--id-hash`` names) keyfile - Path to the local key file used for access. Depending on *mode* this key may be absent. + Path to the local key file used for access. Depending on the key location this may be absent. The *cache* key, if present, contains: @@ -334,7 +336,8 @@ Example *borg info* output:: } }, "encryption": { - "mode": "repokey" + "encryption": "aes256-ocb", + "id_hash": "sha256" }, "repository": { "id": "0cbe6166b46627fd26b97f8831e2ca97584280a46714ef84d2b668daf8271a23", @@ -410,7 +413,8 @@ Example of a simple archive listing (``borg list --last 1 --json``):: } ], "encryption": { - "mode": "repokey" + "encryption": "aes256-ocb", + "id_hash": "sha256" }, "repository": { "id": "0cbe6166b46627fd26b97f8831e2ca97584280a46714ef84d2b668daf8271a23", @@ -463,7 +467,8 @@ The same archive with more information (``borg info --last 1 --json``):: } }, "encryption": { - "mode": "repokey" + "encryption": "aes256-ocb", + "id_hash": "sha256" }, "repository": { "id": "0cbe6166b46627fd26b97f8831e2ca97584280a46714ef84d2b668daf8271a23", diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index a94e3c3d95..a707f7f826 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -1330,7 +1330,10 @@ def default(self, o): def basic_json_data(manifest, *, cache=None, extra=None): key = manifest.key data = extra or {} - data |= {"repository": BorgJsonEncoder().default(manifest.repository), "encryption": {"mode": key.ARG_NAME}} + data |= { + "repository": BorgJsonEncoder().default(manifest.repository), + "encryption": {"encryption": key.ENC_NAME, "id_hash": key.IDHASH_NAME}, + } data["repository"]["last_modified"] = OutputTimestamp(manifest.last_timestamp) if key.NAME.startswith("key file"): data["encryption"]["keyfile"] = key.find_key() diff --git a/src/borg/testsuite/archiver/repo_info_cmd_test.py b/src/borg/testsuite/archiver/repo_info_cmd_test.py index a4b7c36cee..e954a53aa8 100644 --- a/src/borg/testsuite/archiver/repo_info_cmd_test.py +++ b/src/borg/testsuite/archiver/repo_info_cmd_test.py @@ -27,5 +27,6 @@ def test_info_json(archivers, request): assert "last_modified" in repository checkts(repository["last_modified"]) - assert info_repo["encryption"]["mode"] == RK_ENCRYPTION[13:] + assert info_repo["encryption"]["encryption"] == RK_ENCRYPTION[13:] # --encryption=aes256-ocb + assert info_repo["encryption"]["id_hash"] == "sha256" # default id-hash assert "keyfile" not in info_repo["encryption"] diff --git a/src/borg/testsuite/archiver/repo_list_cmd_test.py b/src/borg/testsuite/archiver/repo_list_cmd_test.py index d360cc0f7a..d2f8228ccb 100644 --- a/src/borg/testsuite/archiver/repo_list_cmd_test.py +++ b/src/borg/testsuite/archiver/repo_list_cmd_test.py @@ -147,7 +147,8 @@ def test_repo_list_json(archivers, request): repository = list_repo["repository"] assert len(repository["id"]) == 64 checkts(repository["last_modified"]) - assert list_repo["encryption"]["mode"] == RK_ENCRYPTION[13:] + assert list_repo["encryption"]["encryption"] == RK_ENCRYPTION[13:] # --encryption=aes256-ocb + assert list_repo["encryption"]["id_hash"] == "sha256" # default id-hash assert "keyfile" not in list_repo["encryption"] archive0 = list_repo["archives"][0] checkts(archive0["time"]) From 2f59abe2058f41e63d8138aa2f7da4f908168e11 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 15 Jun 2026 18:39:41 +0200 Subject: [PATCH 3/6] crypto: drop dead ARG_NAME; give legacy key classes ENC_NAME/IDHASH_NAME ARG_NAME had no readers left after the JSON output switched to ENC_NAME/IDHASH_NAME, so remove it from all (current and legacy) key classes. Give the legacy (borg 1.x, read-only) key classes the two dimensions so that repo-info/list --json reports a meaningful crypto suite for legacy repos instead of null/null: - AESCTRKey: encryption=aes256-ctr, id_hash=sha256 - Blake2AESCTRKey: encryption=aes256-ctr, id_hash=blake2 - Blake2AuthenticatedKey: encryption=authenticated, id_hash=blake2 IDHASH_NAME="blake2" is set on the ID_BLAKE2b_256 mix-in (parallel to the ID_HMAC_SHA_256 / ID_BLAKE3_256 mix-ins). These legacy values never become CLI choices: encryption_argument_names()/id_hash_argument_names() only iterate AVAILABLE_KEY_TYPES, not LEGACY_KEY_TYPES. Co-Authored-By: Claude Opus 4.8 --- src/borg/crypto/key.py | 10 ---------- src/borg/legacy/crypto/key.py | 8 +++++--- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index 6156aa85e2..7a6bc3d237 100644 --- a/src/borg/crypto/key.py +++ b/src/borg/crypto/key.py @@ -213,9 +213,6 @@ class KeyBase: # Human-readable name NAME = "UNDEFINED" - # Name used in command line / API (e.g. borg init --encryption=...) - ARG_NAME = "UNDEFINED" - # The two orthogonal dimensions a creatable crypto suite is selected by on the command line: # ENC_NAME -> "borg repo-create --encryption" (cipher / AE algorithm) # IDHASH_NAME -> "borg repo-create --id-hash" (id hash function) @@ -326,7 +323,6 @@ class PlaintextKey(KeyBase): TYPE = KeyType.PLAINTEXT TYPES_ACCEPTABLE = {TYPE} NAME = "plaintext" - ARG_NAME = "none" ENC_NAME = "none" IDHASH_NAME = "sha256" # plain sha256(data), no key; blake3 is not supported for "none" @@ -997,7 +993,6 @@ class AuthenticatedKey(ID_HMAC_SHA_256, AuthenticatedKeyBase): TYPE = KeyType.AUTHENTICATED TYPES_ACCEPTABLE = {TYPE} NAME = "authenticated SHA256" - ARG_NAME = "authenticated" ENC_NAME = "authenticated" # IDHASH_NAME = "sha256" via ID_HMAC_SHA_256 mix-in @@ -1021,7 +1016,6 @@ class Blake3AuthenticatedKey(ID_BLAKE3_256, AuthenticatedKeyBase): TYPE = KeyType.BLAKE3AUTHENTICATED TYPES_ACCEPTABLE = {TYPE} NAME = "authenticated BLAKE3" - ARG_NAME = "authenticated-blake3" ENC_NAME = "authenticated" # IDHASH_NAME = "blake3" via ID_BLAKE3_256 mix-in @@ -1149,7 +1143,6 @@ class AESOCBKey(ID_HMAC_SHA_256, AEADKeyBase, FlexiKey): TYPE = KeyType.AESOCB TYPES_ACCEPTABLE = {TYPE} NAME = "SHA256 AES256-OCB" - ARG_NAME = "aes256-ocb" ENC_NAME = "aes256-ocb" # IDHASH_NAME = "sha256" via ID_HMAC_SHA_256 mix-in CIPHERSUITE = AES256_OCB @@ -1158,7 +1151,6 @@ class CHPOKey(ID_HMAC_SHA_256, AEADKeyBase, FlexiKey): TYPE = KeyType.CHPO TYPES_ACCEPTABLE = {TYPE} NAME = "SHA256 ChaCha20-Poly1305" - ARG_NAME = "chacha20-poly1305" ENC_NAME = "chacha20-poly1305" # IDHASH_NAME = "sha256" via ID_HMAC_SHA_256 mix-in CIPHERSUITE = CHACHA20_POLY1305 @@ -1167,7 +1159,6 @@ class Blake3AESOCBKey(ID_BLAKE3_256, AEADKeyBase, FlexiKey): TYPE = KeyType.BLAKE3AESOCB TYPES_ACCEPTABLE = {TYPE} NAME = "BLAKE3 AES256-OCB" - ARG_NAME = "blake3-aes256-ocb" ENC_NAME = "aes256-ocb" # IDHASH_NAME = "blake3" via ID_BLAKE3_256 mix-in CIPHERSUITE = AES256_OCB @@ -1176,7 +1167,6 @@ class Blake3CHPOKey(ID_BLAKE3_256, AEADKeyBase, FlexiKey): TYPE = KeyType.BLAKE3CHPO TYPES_ACCEPTABLE = {TYPE} NAME = "BLAKE3 ChaCha20-Poly1305" - ARG_NAME = "blake3-chacha20-poly1305" ENC_NAME = "chacha20-poly1305" # IDHASH_NAME = "blake3" via ID_BLAKE3_256 mix-in CIPHERSUITE = CHACHA20_POLY1305 diff --git a/src/borg/legacy/crypto/key.py b/src/borg/legacy/crypto/key.py index f6e708a0d8..e1d1b9ff08 100644 --- a/src/borg/legacy/crypto/key.py +++ b/src/borg/legacy/crypto/key.py @@ -74,6 +74,8 @@ class ID_BLAKE2b_256: The id_key length must be 32 bytes. """ + IDHASH_NAME = "blake2" + def id_hash(self, data): return blake2b_256(self.id_key, data) @@ -89,7 +91,7 @@ class Blake2AuthenticatedKey(ID_BLAKE2b_256, AuthenticatedKeyBase): # type: ign TYPE = KeyType.BLAKE2AUTHENTICATED TYPES_ACCEPTABLE = {TYPE} NAME = "authenticated BLAKE2b" - ARG_NAME = "authenticated-blake2" + ENC_NAME = "authenticated" # IDHASH_NAME = "blake2" via ID_BLAKE2b_256 mix-in; read-only (borg 1.x) # borg 1.x AES-CTR keys. keyfile and repokey are no longer separate classes - storage is a per-key @@ -102,7 +104,7 @@ class AESCTRKey(Pbkdf2FileMixin, ID_HMAC_SHA_256, AESKeyBase, FlexiKey): # type TYPES_ACCEPTABLE = {KeyType.KEYFILE, KeyType.REPO, KeyType.PASSPHRASE} TYPE = KeyType.KEYFILE NAME = "AES-CTR HMAC-SHA256" - ARG_NAME = None # not creatable: borg 1.x compatibility (read-only) + ENC_NAME = "aes256-ctr" # IDHASH_NAME = "sha256" via ID_HMAC_SHA_256 mix-in; read-only (borg 1.x) STORAGE = KeyBlobStorage.REPO # seed default; actual per-key storage is tracked in self.storage on load LOCATION_CONFIGURABLE = True # borg 1.x had keyfile and repokey variants CIPHERSUITE = AES256_CTR_HMAC_SHA256 @@ -112,7 +114,7 @@ class Blake2AESCTRKey(Pbkdf2FileMixin, ID_BLAKE2b_256, AESKeyBase, FlexiKey): # TYPES_ACCEPTABLE = {KeyType.BLAKE2KEYFILE, KeyType.BLAKE2REPO} TYPE = KeyType.BLAKE2KEYFILE NAME = "AES-CTR BLAKE2b" - ARG_NAME = None # not creatable: borg 1.x compatibility (read-only) + ENC_NAME = "aes256-ctr" # IDHASH_NAME = "blake2" via ID_BLAKE2b_256 mix-in; read-only (borg 1.x) STORAGE = KeyBlobStorage.REPO # seed default; actual per-key storage is tracked in self.storage on load LOCATION_CONFIGURABLE = True # borg 1.x had keyfile and repokey variants CIPHERSUITE = AES256_CTR_BLAKE2b From ee2aee0a3126754012b472370d664652baf22b0b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 15 Jun 2026 19:31:35 +0200 Subject: [PATCH 4/6] repo-info/list --json: emit encryption.keyfile based on key storage, not NAME The keyfile detection used `key.NAME.startswith("key file")`, but since the keyfile/repokey unification no key class has such a NAME, so encryption.keyfile was never emitted (even for keyfile repos). Decide it from the key storage (KeyBlobStorage.KEYFILE), matching the text repo-info output. Add a test. Co-Authored-By: Claude Opus 4.8 --- src/borg/helpers/parseformat.py | 2 +- src/borg/testsuite/archiver/repo_info_cmd_test.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index a707f7f826..61252d802f 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -1335,7 +1335,7 @@ def basic_json_data(manifest, *, cache=None, extra=None): "encryption": {"encryption": key.ENC_NAME, "id_hash": key.IDHASH_NAME}, } data["repository"]["last_modified"] = OutputTimestamp(manifest.last_timestamp) - if key.NAME.startswith("key file"): + if getattr(key, "storage", None) == KeyBlobStorage.KEYFILE: data["encryption"]["keyfile"] = key.find_key() if cache: data["cache"] = cache diff --git a/src/borg/testsuite/archiver/repo_info_cmd_test.py b/src/borg/testsuite/archiver/repo_info_cmd_test.py index e954a53aa8..9722124a1e 100644 --- a/src/borg/testsuite/archiver/repo_info_cmd_test.py +++ b/src/borg/testsuite/archiver/repo_info_cmd_test.py @@ -1,7 +1,7 @@ import json from ...constants import * # NOQA -from . import checkts, cmd, create_regular_file, generate_archiver_tests, RK_ENCRYPTION +from . import checkts, cmd, create_regular_file, generate_archiver_tests, RK_ENCRYPTION, KF_ENCRYPTION, KF_LOCATION pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,remote,binary") # NOQA @@ -29,4 +29,14 @@ def test_info_json(archivers, request): checkts(repository["last_modified"]) assert info_repo["encryption"]["encryption"] == RK_ENCRYPTION[13:] # --encryption=aes256-ocb assert info_repo["encryption"]["id_hash"] == "sha256" # default id-hash - assert "keyfile" not in info_repo["encryption"] + assert "keyfile" not in info_repo["encryption"] # repokey storage -> no keyfile path + + +def test_info_json_keyfile(archivers, request): + # for keyfile storage, --json reports the local key file path under encryption.keyfile + archiver = request.getfixturevalue(archivers) + cmd(archiver, "repo-create", KF_ENCRYPTION, KF_LOCATION) + + info_repo = json.loads(cmd(archiver, "repo-info", "--json")) + keyfile = info_repo["encryption"]["keyfile"] + assert keyfile # a (non-empty) path string to the local key file From 98b428b14699587c1ef8592426a7e385255c834b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 15 Jun 2026 19:32:13 +0200 Subject: [PATCH 5/6] repo-create: gate key/passphrase warning on ENC_NAME, not the NAME display string The "you need KEY AND PASSPHRASE" warning was gated on key.NAME != "plaintext", a brittle dependency on a human-readable display string. Use the cipher dimension instead: only plaintext has ENC_NAME == "none"; every key-bearing suite (including the authenticated ones, which should warn) differs. Co-Authored-By: Claude Opus 4.8 --- src/borg/archiver/repo_create_cmd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/archiver/repo_create_cmd.py b/src/borg/archiver/repo_create_cmd.py index 7e493617fd..f36f5064c1 100644 --- a/src/borg/archiver/repo_create_cmd.py +++ b/src/borg/archiver/repo_create_cmd.py @@ -32,7 +32,7 @@ def do_repo_create(self, args, repository, *, other_repository=None, other_manif manifest.write() with Cache(repository, manifest, warn_if_unencrypted=False): pass - if key.NAME != "plaintext": + if key.ENC_NAME != "none": # any key-bearing suite (everything except plaintext "none") logger.warning( "\n" "IMPORTANT: you will need both KEY AND PASSPHRASE to access this repository!\n" From 2eef6c8035ebee592b88089f4edb16de94e17c85 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 15 Jun 2026 19:38:32 +0200 Subject: [PATCH 6/6] crypto: drop the NAME attribute; repo-info builds the suite string from ENC_NAME/IDHASH_NAME NAME was only read by "borg repo-info" to show the crypto suite. Remove it from all (current and legacy) key classes and let repo-info assemble the display from the two real dimensions instead: ", , ", e.g. "Encrypted: Yes (repokey, aes256-ocb, sha256)" or "Encrypted: No (repokey, authenticated, blake3)". Co-Authored-By: Claude Opus 4.8 --- docs/usage/repo-info.rst | 2 +- src/borg/archiver/repo_info_cmd.py | 8 +++++--- src/borg/crypto/key.py | 10 ---------- src/borg/legacy/crypto/key.py | 3 --- src/borg/testsuite/archiver/key_cmds_test.py | 16 ++++++++-------- .../testsuite/archiver/repo_create_cmd_test.py | 14 +++++++------- 6 files changed, 21 insertions(+), 32 deletions(-) diff --git a/docs/usage/repo-info.rst b/docs/usage/repo-info.rst index 00a158930b..a5f30880f5 100644 --- a/docs/usage/repo-info.rst +++ b/docs/usage/repo-info.rst @@ -7,7 +7,7 @@ Examples $ borg repo-info Repository ID: 0e85a7811022326c067acb2a7181d5b526b7d2f61b34470fb8670c440a67f1a9 Location: /Users/tw/w/borg/path/to/repo - Encrypted: Yes (repokey AES-OCB) + Encrypted: Yes (repokey, aes256-ocb, sha256) Cache: /Users/tw/.cache/borg/0e85a7811022326c067acb2a7181d5b526b7d2f61b34470fb8670c440a67f1a9 Security dir: /Users/tw/.config/borg/security/0e85a7811022326c067acb2a7181d5b526b7d2f61b34470fb8670c440a67f1a9 Original size: 152.14 MB diff --git a/src/borg/archiver/repo_info_cmd.py b/src/borg/archiver/repo_info_cmd.py index 3c3f1af086..f5dd417a59 100644 --- a/src/borg/archiver/repo_info_cmd.py +++ b/src/borg/archiver/repo_info_cmd.py @@ -22,16 +22,18 @@ def do_repo_info(self, args, repository, manifest, cache): json_print(info) else: encryption = "Encrypted: " - # storage (keyfile/repokey) is a per-key property now; the crypto suite is key.NAME. + # storage (keyfile/repokey) is a per-key property now; the crypto suite is described by + # the two dimensions: cipher / AE algorithm (ENC_NAME) and id hash function (IDHASH_NAME). storage = getattr(key, "storage", None) mode = {KeyBlobStorage.KEYFILE: "keyfile", KeyBlobStorage.REPO: "repokey"}.get(storage) + suite = "%s, %s" % (key.ENC_NAME, key.IDHASH_NAME) if key.ENC_NAME in ("none", "authenticated"): # the "none" and "authenticated" encryptions do not encrypt data; "authenticated" # (unlike "none"/plaintext) still has a key stored as a keyfile or repokey, so show # that location when there is one. - encryption += "No (%s, %s)" % (mode, key.NAME) if mode else "No" + encryption += "No (%s, %s)" % (mode, suite) if mode else "No" else: - encryption += "Yes (%s, %s)" % (mode, key.NAME) if mode else "Yes (%s)" % key.NAME + encryption += "Yes (%s, %s)" % (mode, suite) if mode else "Yes (%s)" % suite if storage == KeyBlobStorage.KEYFILE: encryption += "\nKey file: %s" % key.find_key() info["encryption"] = encryption diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index 7a6bc3d237..0062412c56 100644 --- a/src/borg/crypto/key.py +++ b/src/borg/crypto/key.py @@ -210,9 +210,6 @@ class KeyBase: # set of key type IDs the class can handle as input TYPES_ACCEPTABLE: set[int] = None # override in subclasses - # Human-readable name - NAME = "UNDEFINED" - # The two orthogonal dimensions a creatable crypto suite is selected by on the command line: # ENC_NAME -> "borg repo-create --encryption" (cipher / AE algorithm) # IDHASH_NAME -> "borg repo-create --id-hash" (id hash function) @@ -322,7 +319,6 @@ def unpack_archive(self, data): class PlaintextKey(KeyBase): TYPE = KeyType.PLAINTEXT TYPES_ACCEPTABLE = {TYPE} - NAME = "plaintext" ENC_NAME = "none" IDHASH_NAME = "sha256" # plain sha256(data), no key; blake3 is not supported for "none" @@ -992,7 +988,6 @@ def decrypt(self, id, data): class AuthenticatedKey(ID_HMAC_SHA_256, AuthenticatedKeyBase): TYPE = KeyType.AUTHENTICATED TYPES_ACCEPTABLE = {TYPE} - NAME = "authenticated SHA256" ENC_NAME = "authenticated" # IDHASH_NAME = "sha256" via ID_HMAC_SHA_256 mix-in @@ -1015,7 +1010,6 @@ def id_hash(self, data): class Blake3AuthenticatedKey(ID_BLAKE3_256, AuthenticatedKeyBase): TYPE = KeyType.BLAKE3AUTHENTICATED TYPES_ACCEPTABLE = {TYPE} - NAME = "authenticated BLAKE3" ENC_NAME = "authenticated" # IDHASH_NAME = "blake3" via ID_BLAKE3_256 mix-in @@ -1142,7 +1136,6 @@ def init_ciphers(self, manifest_data=None, iv=0): class AESOCBKey(ID_HMAC_SHA_256, AEADKeyBase, FlexiKey): TYPE = KeyType.AESOCB TYPES_ACCEPTABLE = {TYPE} - NAME = "SHA256 AES256-OCB" ENC_NAME = "aes256-ocb" # IDHASH_NAME = "sha256" via ID_HMAC_SHA_256 mix-in CIPHERSUITE = AES256_OCB @@ -1150,7 +1143,6 @@ class AESOCBKey(ID_HMAC_SHA_256, AEADKeyBase, FlexiKey): class CHPOKey(ID_HMAC_SHA_256, AEADKeyBase, FlexiKey): TYPE = KeyType.CHPO TYPES_ACCEPTABLE = {TYPE} - NAME = "SHA256 ChaCha20-Poly1305" ENC_NAME = "chacha20-poly1305" # IDHASH_NAME = "sha256" via ID_HMAC_SHA_256 mix-in CIPHERSUITE = CHACHA20_POLY1305 @@ -1158,7 +1150,6 @@ class CHPOKey(ID_HMAC_SHA_256, AEADKeyBase, FlexiKey): class Blake3AESOCBKey(ID_BLAKE3_256, AEADKeyBase, FlexiKey): TYPE = KeyType.BLAKE3AESOCB TYPES_ACCEPTABLE = {TYPE} - NAME = "BLAKE3 AES256-OCB" ENC_NAME = "aes256-ocb" # IDHASH_NAME = "blake3" via ID_BLAKE3_256 mix-in CIPHERSUITE = AES256_OCB @@ -1166,7 +1157,6 @@ class Blake3AESOCBKey(ID_BLAKE3_256, AEADKeyBase, FlexiKey): class Blake3CHPOKey(ID_BLAKE3_256, AEADKeyBase, FlexiKey): TYPE = KeyType.BLAKE3CHPO TYPES_ACCEPTABLE = {TYPE} - NAME = "BLAKE3 ChaCha20-Poly1305" ENC_NAME = "chacha20-poly1305" # IDHASH_NAME = "blake3" via ID_BLAKE3_256 mix-in CIPHERSUITE = CHACHA20_POLY1305 diff --git a/src/borg/legacy/crypto/key.py b/src/borg/legacy/crypto/key.py index e1d1b9ff08..e001de5693 100644 --- a/src/borg/legacy/crypto/key.py +++ b/src/borg/legacy/crypto/key.py @@ -90,7 +90,6 @@ def init_from_random_data(self): class Blake2AuthenticatedKey(ID_BLAKE2b_256, AuthenticatedKeyBase): # type: ignore[misc] TYPE = KeyType.BLAKE2AUTHENTICATED TYPES_ACCEPTABLE = {TYPE} - NAME = "authenticated BLAKE2b" ENC_NAME = "authenticated" # IDHASH_NAME = "blake2" via ID_BLAKE2b_256 mix-in; read-only (borg 1.x) @@ -103,7 +102,6 @@ class Blake2AuthenticatedKey(ID_BLAKE2b_256, AuthenticatedKeyBase): # type: ign class AESCTRKey(Pbkdf2FileMixin, ID_HMAC_SHA_256, AESKeyBase, FlexiKey): # type: ignore[misc] TYPES_ACCEPTABLE = {KeyType.KEYFILE, KeyType.REPO, KeyType.PASSPHRASE} TYPE = KeyType.KEYFILE - NAME = "AES-CTR HMAC-SHA256" ENC_NAME = "aes256-ctr" # IDHASH_NAME = "sha256" via ID_HMAC_SHA_256 mix-in; read-only (borg 1.x) STORAGE = KeyBlobStorage.REPO # seed default; actual per-key storage is tracked in self.storage on load LOCATION_CONFIGURABLE = True # borg 1.x had keyfile and repokey variants @@ -113,7 +111,6 @@ class AESCTRKey(Pbkdf2FileMixin, ID_HMAC_SHA_256, AESKeyBase, FlexiKey): # type class Blake2AESCTRKey(Pbkdf2FileMixin, ID_BLAKE2b_256, AESKeyBase, FlexiKey): # type: ignore[misc] TYPES_ACCEPTABLE = {KeyType.BLAKE2KEYFILE, KeyType.BLAKE2REPO} TYPE = KeyType.BLAKE2KEYFILE - NAME = "AES-CTR BLAKE2b" ENC_NAME = "aes256-ctr" # IDHASH_NAME = "blake2" via ID_BLAKE2b_256 mix-in; read-only (borg 1.x) STORAGE = KeyBlobStorage.REPO # seed default; actual per-key storage is tracked in self.storage on load LOCATION_CONFIGURABLE = True # borg 1.x had keyfile and repokey variants diff --git a/src/borg/testsuite/archiver/key_cmds_test.py b/src/borg/testsuite/archiver/key_cmds_test.py index 418f8b0726..3b70f1eed2 100644 --- a/src/borg/testsuite/archiver/key_cmds_test.py +++ b/src/borg/testsuite/archiver/key_cmds_test.py @@ -50,10 +50,10 @@ def test_change_location_to_b3keyfile(archivers, request): archiver = request.getfixturevalue(archivers) cmd(archiver, "repo-create", "--encryption=aes256-ocb", "--id-hash=blake3") log = cmd(archiver, "repo-info") - assert "(repokey, BLAKE3" in log + assert "(repokey, aes256-ocb, blake3)" in log cmd(archiver, "key", "change-location", "keyfile") log = cmd(archiver, "repo-info") - assert "(keyfile, BLAKE3" in log + assert "(keyfile, aes256-ocb, blake3)" in log def test_change_location_to_repokey(archivers, request): @@ -70,10 +70,10 @@ def test_change_location_to_b3repokey(archivers, request): archiver = request.getfixturevalue(archivers) cmd(archiver, "repo-create", "--encryption=aes256-ocb", "--id-hash=blake3", KF_LOCATION) log = cmd(archiver, "repo-info") - assert "(keyfile, BLAKE3" in log + assert "(keyfile, aes256-ocb, blake3)" in log cmd(archiver, "key", "change-location", "repokey") log = cmd(archiver, "repo-info") - assert "(repokey, BLAKE3" in log + assert "(repokey, aes256-ocb, blake3)" in log def test_change_location_authenticated_to_keyfile(archivers, request): @@ -81,12 +81,12 @@ def test_change_location_authenticated_to_keyfile(archivers, request): archiver = request.getfixturevalue(archivers) cmd(archiver, "repo-create", "--encryption=authenticated") log = cmd(archiver, "repo-info") - assert "(repokey, authenticated SHA256)" in log + assert "(repokey, authenticated, sha256)" in log cmd(archiver, "key", "change-location", "keyfile") [key_filename] = os.listdir(archiver.keys_path) assert key_filename # key blob now lives as a keyfile log = cmd(archiver, "repo-info") - assert "(keyfile, authenticated SHA256)" in log + assert "(keyfile, authenticated, sha256)" in log def test_change_location_authenticated_to_repokey(archivers, request): @@ -94,11 +94,11 @@ def test_change_location_authenticated_to_repokey(archivers, request): cmd(archiver, "repo-create", "--encryption=authenticated", KF_LOCATION) assert os.listdir(archiver.keys_path) # key blob created as a keyfile log = cmd(archiver, "repo-info") - assert "(keyfile, authenticated SHA256)" in log + assert "(keyfile, authenticated, sha256)" in log cmd(archiver, "key", "change-location", "repokey") assert os.listdir(archiver.keys_path) == [] # keyfile removed after moving into the repo log = cmd(archiver, "repo-info") - assert "(repokey, authenticated SHA256)" in log + assert "(repokey, authenticated, sha256)" in log def test_keyfile_name_is_content_sha256(archivers, request): diff --git a/src/borg/testsuite/archiver/repo_create_cmd_test.py b/src/borg/testsuite/archiver/repo_create_cmd_test.py index 9ba4e94563..7d79909e91 100644 --- a/src/borg/testsuite/archiver/repo_create_cmd_test.py +++ b/src/borg/testsuite/archiver/repo_create_cmd_test.py @@ -38,13 +38,13 @@ def test_repo_create_requires_encryption_option(archivers, request): "extra_args, expected", [ # --encryption x --id-hash -> crypto suite shown by "borg repo-info" - (["--encryption=aes256-ocb"], "Yes (repokey, SHA256 AES256-OCB)"), # default id-hash is sha256 - (["--encryption=aes256-ocb", "--id-hash=sha256"], "Yes (repokey, SHA256 AES256-OCB)"), - (["--encryption=aes256-ocb", "--id-hash=blake3"], "Yes (repokey, BLAKE3 AES256-OCB)"), - (["--encryption=chacha20-poly1305"], "Yes (repokey, SHA256 ChaCha20-Poly1305)"), - (["--encryption=chacha20-poly1305", "--id-hash=blake3"], "Yes (repokey, BLAKE3 ChaCha20-Poly1305)"), - (["--encryption=authenticated"], "No (repokey, authenticated SHA256)"), - (["--encryption=authenticated", "--id-hash=blake3"], "No (repokey, authenticated BLAKE3)"), + (["--encryption=aes256-ocb"], "Yes (repokey, aes256-ocb, sha256)"), # default id-hash is sha256 + (["--encryption=aes256-ocb", "--id-hash=sha256"], "Yes (repokey, aes256-ocb, sha256)"), + (["--encryption=aes256-ocb", "--id-hash=blake3"], "Yes (repokey, aes256-ocb, blake3)"), + (["--encryption=chacha20-poly1305"], "Yes (repokey, chacha20-poly1305, sha256)"), + (["--encryption=chacha20-poly1305", "--id-hash=blake3"], "Yes (repokey, chacha20-poly1305, blake3)"), + (["--encryption=authenticated"], "No (repokey, authenticated, sha256)"), + (["--encryption=authenticated", "--id-hash=blake3"], "No (repokey, authenticated, blake3)"), (["--encryption=none"], "No"), ], )