diff --git a/docs/changes.rst b/docs/changes.rst index 39ea315d72..b76075ec57 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -168,12 +168,22 @@ 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``. 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 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/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/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/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/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..f36f5064c1 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 @@ -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" @@ -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..f5dd417a59 100644 --- a/src/borg/archiver/repo_info_cmd.py +++ b/src/borg/archiver/repo_info_cmd.py @@ -22,15 +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) - 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. - encryption += "No (%s, %s)" % (mode, key.NAME) if mode else "No" + 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, 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 d0750bdc0e..0062412c56 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): @@ -189,11 +210,13 @@ 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" - - # 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. @@ -296,8 +319,8 @@ def unpack_archive(self, data): 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" chunk_seed = 0 crypt_key = b"" # makes .derive_key() work, nothing secret here @@ -332,6 +355,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 +988,7 @@ def decrypt(self, id, data): class AuthenticatedKey(ID_HMAC_SHA_256, AuthenticatedKeyBase): TYPE = KeyType.AUTHENTICATED TYPES_ACCEPTABLE = {TYPE} - NAME = "authenticated" - ARG_NAME = "authenticated" + ENC_NAME = "authenticated" # IDHASH_NAME = "sha256" via ID_HMAC_SHA_256 mix-in # ------------ new crypto ------------ @@ -977,6 +1001,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) @@ -984,8 +1010,7 @@ def id_hash(self, data): 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 class AEADKeyBase(KeyBase): @@ -1111,32 +1136,28 @@ 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" + 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" - 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" + ENC_NAME = "aes256-ocb" # IDHASH_NAME = "blake3" via ID_BLAKE3_256 mix-in CIPHERSUITE = AES256_OCB 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/helpers/parseformat.py b/src/borg/helpers/parseformat.py index a94e3c3d95..61252d802f 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -1330,9 +1330,12 @@ 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"): + if getattr(key, "storage", None) == KeyBlobStorage.KEYFILE: data["encryption"]["keyfile"] = key.find_key() if cache: data["cache"] = cache diff --git a/src/borg/legacy/crypto/key.py b/src/borg/legacy/crypto/key.py index f6e708a0d8..e001de5693 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) @@ -88,8 +90,7 @@ def init_from_random_data(self): class Blake2AuthenticatedKey(ID_BLAKE2b_256, AuthenticatedKeyBase): # type: ignore[misc] 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 @@ -101,8 +102,7 @@ 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" - 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 @@ -111,8 +111,7 @@ 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" - 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 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..3b70f1eed2 100644 --- a/src/borg/testsuite/archiver/key_cmds_test.py +++ b/src/borg/testsuite/archiver/key_cmds_test.py @@ -48,12 +48,12 @@ 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 + 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): @@ -68,12 +68,12 @@ 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 + 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)" 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..7d79909e91 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, 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"), + ], +) +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/archiver/repo_info_cmd_test.py b/src/borg/testsuite/archiver/repo_info_cmd_test.py index a4b7c36cee..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 @@ -27,5 +27,16 @@ def test_info_json(archivers, request): assert "last_modified" in repository checkts(repository["last_modified"]) - assert info_repo["encryption"]["mode"] == RK_ENCRYPTION[13:] - assert "keyfile" not in info_repo["encryption"] + 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"] # 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 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"]) 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