Repository: borgbackup/borg

Issue:
1.2.0 crashes when calculating statistics with older archives containing %
<!--
Thank you for reporting an issue.

*IMPORTANT* -  *before* creating a new issue please look around:
 - Borgbackup documentation: http://borgbackup.readthedocs.io/en/stable/index.html
 - FAQ: https://borgbackup.readthedocs.io/en/stable/faq.html
 and
 - open issues in Github tracker: https://github.com/borgbackup/borg/issues
  
If you cannot find a similar problem, then create a new issue.

Please fill in as much of the template as possible.
-->

## Have you checked borgbackup docs, FAQ, and open Github issues?

Yes, error from #6063 looks similar, though this was in the filename instead of the archive name and affected an older version.

## Is this a BUG / ISSUE report or a QUESTION?

BUG

## System information

```
Platform: Linux pc-arch 5.16.16-arch1-1 #1 SMP PREEMPT Mon, 21 Mar 2022 22:59:40 +0000 x86_64
Linux: Arch Linux  
Borg: 1.2.0  Python: CPython 3.10.3 msgpack: 1.0.3 fuse: llfuse 1.4.1 [pyfuse3,llfuse]
PID: 112950  CWD: /mnt/backup/BorgRepos
sys.argv: ['/usr/bin/borg', 'info', 'Laptop::Home_t440s-22.085']
SSH_ORIGINAL_COMMAND: None
```


## Describe the problem you're observing.
I created a new archive on this 5 years old repo, the first time with borg 1.2.0. It ran as usual, but failed at the end to get the statistics:
```
$ borg create -svp -C lz4 --patterns- .... 'Laptop::Home_t440s-{now:%y.%j}' $SRC

Local Exception
Traceback (most recent call last):
  File "/usr/lib/python3.10/site-packages/borg/archive.py", line 654, in calc_stats
    stats = Statistics.from_raw_dict(**cache.pre12_meta[self.fpr])
KeyError: '956cea6445100d48399f29e530cb1a994b86de3fcdb3978aa595e0dbadb93f52'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/lib/python3.10/site-packages/borg/archiver.py", line 5089, in main
    exit_code = archiver.run(args)
  File "/usr/lib/python3.10/site-packages/borg/archiver.py", line 5020, in run
    return set_ec(func(args))
  File "/usr/lib/python3.10/site-packages/borg/archiver.py", line 181, in wrapper
    return method(self, args, repository=repository, cache=cache_, **kwargs)
  File "/usr/lib/python3.10/site-packages/borg/archiver.py", line 1392, in do_info
    return self._info_archives(args, repository, manifest, key, cache)
  File "/usr/lib/python3.10/site-packages/borg/archiver.py", line 1417, in _info_archives
    print(textwrap.dedent("""
  File "/usr/lib/python3.10/site-packages/borg/cache.py", line 414, in __str__
    return self.str_format.format(self.format_tuple())
  File "/usr/lib/python3.10/site-packages/borg/cache.py", line 441, in format_tuple
    stats = self.stats()
  File "/usr/lib/python3.10/site-packages/borg/cache.py", line 433, in stats
    stats = archive.calc_stats(self, want_unique=False)
  File "/usr/lib/python3.10/site-packages/borg/archive.py", line 656, in calc_stats
    stats = self._calc_stats(cache, want_unique=want_unique)
  File "/usr/lib/python3.10/site-packages/borg/archive.py", line 678, in _calc_stats
    pi.show(increase=1)
  File "/usr/lib/python3.10/site-packages/borg/helpers/progress.py", line 157, in show
    return self.output(self.msg % pct)
ValueError: unsupported format character 'y' (0x79) at index 45
```
Also checking with:
```
borg info Laptop::Home_t440s-22.085
```
fails with the same exception. Actually it fails for every archive in that repo. 
So I looked into the repo and compared it with others that did not have this problem. Difference is one previous archive that has a `%` in the name.
Borg 1.1.7 (and below) never had a problem with it.
#### Can you reproduce the problem?
Yes, start with 1.1.7 or older:
```sh
borg init repo/ -e repokey
borg create -svp 'repo::good-name' test/
borg create -svp 'repo::bad-%-name' test/
borg info repo::good-name # WORKS
```
Then update to 1.2.0, while keeping the same repo:
```sh
borg info repo::good-name # FAILS
...
ValueError: unsupported format character 'n' (0x6e) at index 41
```
Workaround:
```sh
borg rename repo::bad-%-name less-bad-name
borg info repo::good-name # works again
```

Bounty: https://app.bountysource.com/issues/106415617-1-2-0-crashes-when-calculating-statistics-with-older-archives-containing


Discussion:

Patch:
diff --git a/src/borg/archive.py b/src/borg/archive.py
index 7e16afb9..e8c0a1fb 100644
--- a/src/borg/archive.py
+++ b/src/borg/archive.py
@@ -49,6 +49,7 @@
 from .remote import cache_if_remote
 from .repository import Repository, LIST_SCAN_LIMIT
 
+has_lchmod = hasattr(os, 'lchmod')
 has_link = hasattr(os, 'link')
 
 
@@ -670,8 +671,10 @@ def add(id):
             archive_index = ChunkIndex()
             sync = CacheSynchronizer(archive_index)
             add(self.id)
+            # we must escape any % char in the archive name, because we use it in a format string, see #6500
+            arch_name_escd = self.name.replace('%', '%%')
             pi = ProgressIndicatorPercent(total=len(self.metadata.items),
-                                          msg='Calculating statistics for archive %s ... %%3d%%%%' % self.name,
+                                          msg='Calculating statistics for archive %s ... %%3.0f%%%%' % arch_name_escd,
                                           msgid='archive.calc_stats')
             for id, chunk in zip(self.metadata.items, self.repository.get_many(self.metadata.items)):
                 pi.show(increase=1)
@@ -876,18 +879,10 @@ def restore_attrs(self, path, item, symlink=False, fd=None):
                 pass
             if fd:
                 os.fchmod(fd, item.mode)
-            else:
-                # To check whether a particular function in the os module accepts False for its
-                # follow_symlinks parameter, the in operator on supports_follow_symlinks should be
-                # used. However, os.chmod is special as some platforms without a working lchmod() do
-                # have fchmodat(), which has a flag that makes it behave like lchmod(). fchmodat()
-                # is ignored when deciding whether or not os.chmod should be set in
-                # os.supports_follow_symlinks. Work around this by using try/except.
-                try:
-                    os.chmod(path, item.mode, follow_symlinks=False)
-                except NotImplementedError:
-                    if not symlink:
-                        os.chmod(path, item.mode)
+            elif not symlink:
+                os.chmod(path, item.mode)
+            elif has_lchmod:  # Not available on Linux
+                os.lchmod(path, item.mode)
             mtime = item.mtime
             if 'atime' in item:
                 atime = item.atime
diff --git a/src/borg/archiver.py b/src/borg/archiver.py
index a3258ce8..7eb25bac 100644
--- a/src/borg/archiver.py
+++ b/src/borg/archiver.py
@@ -398,8 +398,7 @@ def do_change_location(self, args, repository, manifest, key, cache):
             setattr(key_new, name, value)
 
         key_new.target = key_new.get_new_target(args)
-        # save with same passphrase and algorithm
-        key_new.save(key_new.target, key._passphrase, create=True, algorithm=key._encrypted_key_algorithm)
+        key_new.save(key_new.target, key._passphrase, create=True)  # save with same passphrase
 
         # rewrite the manifest with the new key, so that the key-type byte of the manifest changes
         manifest.key = key_new
@@ -4324,7 +4323,6 @@ def define_borg_mount(parser):
                                help='Set storage quota of the new repository (e.g. 5G, 1.5T). Default: no quota.')
         subparser.add_argument('--make-parent-dirs', dest='make_parent_dirs', action='store_true',
                                help='create the parent directories of the repository directory, if they are missing.')
-        subparser.add_argument('--key-algorithm', dest='key_algorithm', default='argon2', choices=list(KEY_ALGORITHMS))
 
         # borg key
         subparser = subparsers.add_parser('key', parents=[mid_common_parser], add_help=False,
diff --git a/src/borg/constants.py b/src/borg/constants.py
index 4c0f4387..5fb6b9a6 100644
--- a/src/borg/constants.py
+++ b/src/borg/constants.py
@@ -103,18 +103,6 @@
 
 PBKDF2_ITERATIONS = 100000
 
-# https://www.rfc-editor.org/rfc/rfc9106.html#section-4-6.2
-ARGON2_ARGS = {'time_cost': 3, 'memory_cost': 2**16, 'parallelism': 4, 'type': 'id'}
-ARGON2_SALT_BYTES = 16
-
-# Maps the CLI argument to our internal identifier for the format
-KEY_ALGORITHMS = {
-    # encrypt-and-MAC, kdf: PBKDF2(HMAC−SHA256), encryption: AES256-CTR, authentication: HMAC-SHA256
-    'pbkdf2': 'sha256',
-    # encrypt-then-MAC, kdf: argon2, encryption: AES256-CTR, authentication: HMAC-SHA256
-    'argon2': 'argon2 aes256-ctr hmac-sha256',
-}
-
 
 class KeyBlobStorage:
     NO_STORAGE = 'no_storage'
diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py
index d117b658..077ad550 100644
--- a/src/borg/crypto/key.py
+++ b/src/borg/crypto/key.py
@@ -25,7 +25,6 @@
 from .nonces import NonceManager
 from .low_level import AES, bytes_to_long, long_to_bytes, bytes_to_int, num_cipher_blocks, hmac_sha256, blake2b_256, hkdf_hmac_sha512
 from .low_level import AES256_CTR_HMAC_SHA256, AES256_CTR_BLAKE2b, AES256_OCB, CHACHA20_POLY1305
-from . import low_level
 
 
 class UnsupportedPayloadError(Error):
@@ -52,10 +51,6 @@ class RepoKeyNotFoundError(Error):
     """No key entry found in the config of repository {}."""
 
 
-class UnsupportedKeyFormatError(Error):
-    """Your borg key is stored in an unsupported format. Try using a newer version of borg."""
-
-
 class TAMRequiredError(IntegrityError):
     __doc__ = textwrap.dedent("""
     Manifest is unauthenticated, but it is required for this repository.
@@ -435,54 +430,15 @@ def decrypt_key_file(self, data, passphrase):
         unpacker = get_limited_unpacker('key')
         unpacker.feed(data)
         data = unpacker.unpack()
-        encrypted_key = EncryptedKey(internal_dict=data)
-        if encrypted_key.version != 1:
-            raise UnsupportedKeyFormatError()
-        else:
-            self._encrypted_key_algorithm = encrypted_key.algorithm
-            if encrypted_key.algorithm == 'sha256':
-                return self.decrypt_key_file_pbkdf2(encrypted_key, passphrase)
-            elif encrypted_key.algorithm == 'argon2 aes256-ctr hmac-sha256':
-                return self.decrypt_key_file_argon2(encrypted_key, passphrase)
-            else:
-                raise UnsupportedKeyFormatError()
-
-    def decrypt_key_file_pbkdf2(self, encrypted_key, passphrase):
-        key = passphrase.kdf(encrypted_key.salt, encrypted_key.iterations, 32)
-        data = AES(key, b'\0'*16).decrypt(encrypted_key.data)
-        if hmac.compare_digest(hmac_sha256(key, data), encrypted_key.hash):
+        enc_key = EncryptedKey(internal_dict=data)
+        assert enc_key.version == 1
+        assert enc_key.algorithm == 'sha256'
+        key = passphrase.kdf(enc_key.salt, enc_key.iterations, 32)
+        data = AES(key, b'\0'*16).decrypt(enc_key.data)
+        if hmac.compare_digest(hmac_sha256(key, data), enc_key.hash):
             return data
-        return None
-
-    def decrypt_key_file_argon2(self, encrypted_key, passphrase):
-        key = passphrase.argon2(
-            output_len_in_bytes=64,
-            salt=encrypted_key.salt,
-            time_cost=encrypted_key.argon2_time_cost,
-            memory_cost=encrypted_key.argon2_memory_cost,
-            parallelism=encrypted_key.argon2_parallelism,
-            type=encrypted_key.argon2_type,
-        )
-        enc_key, mac_key = key[:32], key[32:]
-        ae_cipher = AES256_CTR_HMAC_SHA256(
-            iv=0, header_len=0, aad_offset=0,
-            enc_key=enc_key,
-            mac_key=mac_key,
-        )
-        try:
-            return ae_cipher.decrypt(encrypted_key.data)
-        except low_level.IntegrityError:
-            return None
-
-    def encrypt_key_file(self, data, passphrase, algorithm):
-        if algorithm == 'sha256':
-            return self.encrypt_key_file_pbkdf2(data, passphrase)
-        elif algorithm == 'argon2 aes256-ctr hmac-sha256':
-            return self.encrypt_key_file_argon2(data, passphrase)
-        else:
-            raise ValueError(f'Unexpected algorithm: {algorithm}')
 
-    def encrypt_key_file_pbkdf2(self, data, passphrase):
+    def encrypt_key_file(self, data, passphrase):
         salt = os.urandom(32)
         iterations = PBKDF2_ITERATIONS
         key = passphrase.kdf(salt, iterations, 32)
@@ -498,29 +454,7 @@ def encrypt_key_file_pbkdf2(self, data, passphrase):
         )
         return msgpack.packb(enc_key.as_dict())
 
-    def encrypt_key_file_argon2(self, data, passphrase):
-        salt = os.urandom(ARGON2_SALT_BYTES)
-        key = passphrase.argon2(
-            output_len_in_bytes=64,
-            salt=salt,
-            **ARGON2_ARGS,
-        )
-        enc_key, mac_key = key[:32], key[32:]
-        ae_cipher = AES256_CTR_HMAC_SHA256(
-            iv=0, header_len=0, aad_offset=0,
-            enc_key=enc_key,
-            mac_key=mac_key,
-        )
-        encrypted_key = EncryptedKey(
-            version=1,
-            algorithm='argon2 aes256-ctr hmac-sha256',
-            salt=salt,
-            data=ae_cipher.encrypt(data),
-            **{'argon2_' + k: v for k, v in ARGON2_ARGS.items()},
-        )
-        return msgpack.packb(encrypted_key.as_dict())
-
-    def _save(self, passphrase, algorithm):
+    def _save(self, passphrase):
         key = Key(
             version=1,
             repository_id=self.repository_id,
@@ -530,14 +464,14 @@ def _save(self, passphrase, algorithm):
             chunk_seed=self.chunk_seed,
             tam_required=self.tam_required,
         )
-        data = self.encrypt_key_file(msgpack.packb(key.as_dict()), passphrase, algorithm)
+        data = self.encrypt_key_file(msgpack.packb(key.as_dict()), passphrase)
         key_data = '\n'.join(textwrap.wrap(b2a_base64(data).decode('ascii')))
         return key_data
 
     def change_passphrase(self, passphrase=None):
         if passphrase is None:
             passphrase = Passphrase.new(allow_empty=True)
-        self.save(self.target, passphrase, algorithm=self._encrypted_key_algorithm)
+        self.save(self.target, passphrase)
 
     @classmethod
     def create(cls, repository, args):
@@ -547,7 +481,7 @@ def create(cls, repository, args):
         key.init_from_random_data()
         key.init_ciphers()
         target = key.get_new_target(args)
-        key.save(target, passphrase, create=True, algorithm=KEY_ALGORITHMS[args.key_algorithm])
+        key.save(target, passphrase, create=True)
         logger.info('Key in "%s" created.' % target)
         logger.info('Keep this key safe. Your data will be inaccessible without it.')
         return key
@@ -647,8 +581,8 @@ def load(self, target, passphrase):
             self.target = target
         return success
 
-    def save(self, target, passphrase, algorithm, create=False):
-        key_data = self._save(passphrase, algorithm)
+    def save(self, target, passphrase, create=False):
+        key_data = self._save(passphrase)
         if self.STORAGE == KeyBlobStorage.KEYFILE:
             if create and os.path.isfile(target):
                 # if a new keyfile key repository is created, ensure that an existing keyfile of another
@@ -723,8 +657,8 @@ def load(self, target, passphrase):
         self.logically_encrypted = False
         return success
 
-    def save(self, target, passphrase, algorithm, create=False):
-        super().save(target, passphrase, algorithm, create=create)
+    def save(self, target, passphrase, create=False):
+        super().save(target, passphrase, create=create)
         self.logically_encrypted = False
 
     def init_ciphers(self, manifest_data=None):
diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py
index 08ec8df8..f595c8d3 100644
--- a/src/borg/helpers/parseformat.py
+++ b/src/borg/helpers/parseformat.py
@@ -341,33 +341,24 @@ class Location:
             (?P<archive>[^/]+)                              # archive name must not contain "/"
         )?$"""                                              # must match until the end
 
-    # host NAME, or host IP ADDRESS (v4 or v6, v6 must be in square brackets)
-    host_re = r"""
-        (?P<host>(
-            (?!\[)[^:/]+(?<!\])     # hostname or v4 addr, not containing : or / (does not match v6 addr: no brackets!)
-            |
-            \[[0-9a-fA-F:.]+\])     # ipv6 address in brackets
-        )
-    """
-
     # regexes for misc. kinds of supported location specifiers:
     ssh_re = re.compile(r"""
-        (?P<proto>ssh)://                                       # ssh://
-        """ + optional_user_re + host_re + r"""                 # user@  (optional), host name or address
-        (?::(?P<port>\d+))?                                     # :port (optional)
-        """ + abs_path_re + optional_archive_re, re.VERBOSE)    # path or path::archive
+        (?P<proto>ssh)://                                   # ssh://
+        """ + optional_user_re + r"""                       # user@  (optional)
+        (?P<host>([^:/]+|\[[0-9a-fA-F:.]+\]))(?::(?P<port>\d+))?  # host or host:port or [ipv6] or [ipv6]:port
+        """ + abs_path_re + optional_archive_re, re.VERBOSE)  # path or path::archive
 
     file_re = re.compile(r"""
-        (?P<proto>file)://                                      # file://
-        """ + file_path_re + optional_archive_re, re.VERBOSE)   # servername/path, path or path::archive
+        (?P<proto>file)://                                  # file://
+        """ + file_path_re + optional_archive_re, re.VERBOSE)  # servername/path, path or path::archive
 
-    # note: scp_re is also used for local paths
+    # note: scp_re is also use for local paths
     scp_re = re.compile(r"""
         (
-            """ + optional_user_re + host_re + r"""             # user@  (optional), host name or address
-            :                                                   # : (required!)
-        )?                                                      # user@host: part is optional
-        """ + scp_path_re + optional_archive_re, re.VERBOSE)    # path with optional archive
+            """ + optional_user_re + r"""                   # user@  (optional)
+            (?P<host>([^:/]+|\[[0-9a-fA-F:.]+\])):          # host: (don't match / or [ipv6] in host to disambiguate from file:)
+        )?                                                  # user@host: part is optional
+        """ + scp_path_re + optional_archive_re, re.VERBOSE)  # path with optional archive
 
     # get the repo from BORG_REPO env and the optional archive from param.
     # if the syntax requires giving REPOSITORY (see "borg mount"),
diff --git a/src/borg/helpers/passphrase.py b/src/borg/helpers/passphrase.py
index 52ecc11b..5a0acd12 100644
--- a/src/borg/helpers/passphrase.py
+++ b/src/borg/helpers/passphrase.py
@@ -141,8 +141,6 @@ def __repr__(self):
         return '<Passphrase "***hidden***">'
 
     def kdf(self, salt, iterations, length):
-        if os.environ.get("BORG_TESTONLY_WEAKEN_KDF") == "1":
-            iterations = 1
         return pbkdf2_hmac('sha256', self.encode('utf-8'), salt, iterations, length)
 
     def argon2(
@@ -154,11 +152,6 @@ def argon2(
         parallelism,
         type: Literal['i', 'd', 'id']
     ) -> bytes:
-        if os.environ.get("BORG_TESTONLY_WEAKEN_KDF") == "1":
-            time_cost = 1
-            parallelism = 1
-            # 8 is the smallest value that avoids the "Memory cost is too small" exception
-            memory_cost = 8
         type_map = {
             'i': argon2.low_level.Type.I,
             'd': argon2.low_level.Type.D,
diff --git a/src/borg/item.pyx b/src/borg/item.pyx
index 85cfc95e..1f91f523 100644
--- a/src/borg/item.pyx
+++ b/src/borg/item.pyx
@@ -7,7 +7,6 @@ from .helpers import bigint_to_int, int_to_bigint
 from .helpers import StableDict
 from .helpers import format_file_size
 
-
 cdef extern from "_item.c":
     object _object_to_optr(object obj)
     object _optr_to_object(object bytes)
@@ -295,8 +294,7 @@ class EncryptedKey(PropDict):
     If a EncryptedKey shall be serialized, give as_dict() method output to msgpack packer.
     """
 
-    VALID_KEYS = { 'version', 'algorithm', 'iterations', 'salt', 'hash', 'data',
-                   'argon2_time_cost', 'argon2_memory_cost', 'argon2_parallelism', 'argon2_type' }
+    VALID_KEYS = {'version', 'algorithm', 'iterations', 'salt', 'hash', 'data'}  # str-typed keys
 
     __slots__ = ("_dict", )  # avoid setting attributes not supported by properties
 
@@ -306,10 +304,6 @@ class EncryptedKey(PropDict):
     salt = PropDict._make_property('salt', bytes)
     hash = PropDict._make_property('hash', bytes)
     data = PropDict._make_property('data', bytes)
-    argon2_time_cost = PropDict._make_property('argon2_time_cost', int)
-    argon2_memory_cost = PropDict._make_property('argon2_memory_cost', int)
-    argon2_parallelism = PropDict._make_property('argon2_parallelism', int)
-    argon2_type = PropDict._make_property('argon2_type', str, encode=str.encode, decode=bytes.decode)
 
 
 class Key(PropDict):


Test patch:
diff --git a/conftest.py b/conftest.py
index 6166b475..3a7c278a 100644
--- a/conftest.py
+++ b/conftest.py
@@ -2,6 +2,14 @@
 
 import pytest
 
+# IMPORTANT keep this above all other borg imports to avoid inconsistent values
+# for `from borg.constants import PBKDF2_ITERATIONS` (or star import) usages before
+# this is executed
+from borg import constants
+# no fixture-based monkey-patching since star-imports are used for the constants module
+constants.PBKDF2_ITERATIONS = 1
+
+
 # needed to get pretty assertion failures in unit tests:
 if hasattr(pytest, 'register_assert_rewrite'):
     pytest.register_assert_rewrite('borg.testsuite')
@@ -28,8 +36,6 @@ def clean_env(tmpdir_factory, monkeypatch):
             if key.startswith('BORG_') and key not in ('BORG_FUSE_IMPL', )]
     for key in keys:
         monkeypatch.delenv(key, raising=False)
-    # Speed up tests
-    monkeypatch.setenv("BORG_TESTONLY_WEAKEN_KDF", "1")
 
 
 def pytest_report_header(config, startdir):
diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py
index 97464627..48637b1f 100644
--- a/src/borg/testsuite/archiver.py
+++ b/src/borg/testsuite/archiver.py
@@ -16,7 +16,7 @@
 import tempfile
 import time
 import unittest
-from binascii import unhexlify, b2a_base64, a2b_base64
+from binascii import unhexlify, b2a_base64
 from configparser import ConfigParser
 from datetime import datetime
 from datetime import timezone
@@ -3585,51 +3585,6 @@ def test_recovery_from_deleted_repo_nonce(self):
         self.cmd('create', self.repository_location + '::test2', 'input')
         assert os.path.exists(nonce)
 
-    def test_init_defaults_to_argon2(self):
-        """https://github.com/borgbackup/borg/issues/747#issuecomment-1076160401"""
-        self.cmd('init', '--encryption=repokey', self.repository_location)
-        with Repository(self.repository_path) as repository:
-            key = msgpack.unpackb(a2b_base64(repository.load_key()))
-        assert key[b'algorithm'] == b'argon2 aes256-ctr hmac-sha256'
-
-    def test_init_with_explicit_key_algorithm(self):
-        """https://github.com/borgbackup/borg/issues/747#issuecomment-1076160401"""
-        self.cmd('init', '--encryption=repokey', '--key-algorithm=pbkdf2', self.repository_location)
-        with Repository(self.repository_path) as repository:
-            key = msgpack.unpackb(a2b_base64(repository.load_key()))
-        assert key[b'algorithm'] == b'sha256'
-
-    def verify_change_passphrase_does_not_change_algorithm(self, given_algorithm, expected_algorithm):
-        self.cmd('init', '--encryption=repokey', '--key-algorithm', given_algorithm, self.repository_location)
-        os.environ['BORG_NEW_PASSPHRASE'] = 'newpassphrase'
-
-        self.cmd('key', 'change-passphrase', self.repository_location)
-
-        with Repository(self.repository_path) as repository:
-            key = msgpack.unpackb(a2b_base64(repository.load_key()))
-            assert key[b'algorithm'] == expected_algorithm
-
-    def test_change_passphrase_does_not_change_algorithm_argon2(self):
-        self.verify_change_passphrase_does_not_change_algorithm('argon2', b'argon2 aes256-ctr hmac-sha256')
-
-    def test_change_passphrase_does_not_change_algorithm_pbkdf2(self):
-        self.verify_change_passphrase_does_not_change_algorithm('pbkdf2', b'sha256')
-
-    def verify_change_location_does_not_change_algorithm(self, given_algorithm, expected_algorithm):
-        self.cmd('init', '--encryption=keyfile', '--key-algorithm', given_algorithm, self.repository_location)
-
-        self.cmd('key', 'change-location', self.repository_location, 'repokey')
-
-        with Repository(self.repository_path) as repository:
-            key = msgpack.unpackb(a2b_base64(repository.load_key()))
-            assert key[b'algorithm'] == expected_algorithm
-
-    def test_change_location_does_not_change_algorithm_argon2(self):
-        self.verify_change_location_does_not_change_algorithm('argon2', b'argon2 aes256-ctr hmac-sha256')
-
-    def test_change_location_does_not_change_algorithm_pbkdf2(self):
-        self.verify_change_location_does_not_change_algorithm('pbkdf2', b'sha256')
-
 
 @unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available')
 class ArchiverTestCaseBinary(ArchiverTestCase):
diff --git a/src/borg/testsuite/crypto.py b/src/borg/testsuite/crypto.py
index 3465bb8f..baf8cba2 100644
--- a/src/borg/testsuite/crypto.py
+++ b/src/borg/testsuite/crypto.py
@@ -1,19 +1,9 @@
 from binascii import hexlify
-from unittest.mock import MagicMock
-import unittest
-from binascii import a2b_base64
-
-import pytest
 
 from ..crypto.low_level import AES256_CTR_HMAC_SHA256, AES256_OCB, CHACHA20_POLY1305, UNENCRYPTED, \
                                IntegrityError, is_libressl
 from ..crypto.low_level import bytes_to_long, bytes_to_int, long_to_bytes
 from ..crypto.low_level import hkdf_hmac_sha512
-from ..crypto.low_level import AES, hmac_sha256
-from ..crypto.key import KeyfileKey, UnsupportedKeyFormatError, RepoKey
-from ..helpers.passphrase import Passphrase
-from ..helpers import msgpack
-from ..constants import KEY_ALGORITHMS
 
 from . import BaseTestCase
 
@@ -258,140 +248,3 @@ def test_hkdf_hmac_sha512_5(self):
 
         okm = hkdf_hmac_sha512(ikm, salt, info, l)
         assert okm == bytes.fromhex('1407d46013d98bc6decefcfee55f0f90b0c7f63d68eb1a80eaf07e953cfc0a3a5240a155d6e4daa965bb')
-
-
-def test_decrypt_key_file_argon2_aes256_ctr_hmac_sha256(monkeypatch):
-    plain = b'hello'
-    # echo -n "hello, pass phrase" | argon2 saltsaltsaltsalt -id -t 1 -k 8 -p 1 -l 64 -r
-    key = bytes.fromhex('d07cc7f9cfb483303e0b9fec176b2a9c559bb70c3a9fb0d5f9c0c23527cd09570212449f09f8cd28c1a41b73fa0098e889c3f2642e87c392e51f95d70d248d9d')
-    ae_cipher = AES256_CTR_HMAC_SHA256(
-        iv=0, header_len=0, aad_offset=0,
-        enc_key=key[:32],
-        mac_key=key[32:],
-    )
-
-    envelope = ae_cipher.encrypt(plain)
-
-    encrypted = msgpack.packb({
-        'version': 1,
-        'salt': b'salt'*4,
-        'argon2_time_cost': 1,
-        'argon2_memory_cost': 8,
-        'argon2_parallelism': 1,
-        'argon2_type': b'id',
-        'algorithm': 'argon2 aes256-ctr hmac-sha256',
-        'data': envelope,
-    })
-    monkeypatch.setenv('BORG_PASSPHRASE', "hello, pass phrase")
-    passphrase = Passphrase.new()
-    key = KeyfileKey(None)
-
-    decrypted = key.decrypt_key_file(encrypted, passphrase)
-
-    assert decrypted == plain
-
-
-def test_decrypt_key_file_pbkdf2_sha256_aes256_ctr_hmac_sha256(monkeypatch):
-    plain = b'hello'
-    salt = b'salt'*4
-    monkeypatch.setenv('BORG_PASSPHRASE', "hello, pass phrase")
-    passphrase = Passphrase.new()
-    key = passphrase.kdf(salt, iterations=1, length=32)
-    hash = hmac_sha256(key, plain)
-    data = AES(key, b'\0'*16).encrypt(plain)
-    encrypted = msgpack.packb({
-        'version': 1,
-        'algorithm': 'sha256',
-        'iterations': 1,
-        'salt': salt,
-        'data': data,
-        'hash': hash,
-    })
-    key = KeyfileKey(None)
-
-    decrypted = key.decrypt_key_file(encrypted, passphrase)
-
-    assert decrypted == plain
-
-
-def test_decrypt_key_file_unsupported_algorithm(monkeypatch):
-    """We will add more algorithms in the future. We should raise a helpful error."""
-    monkeypatch.setenv('BORG_PASSPHRASE', "hello, pass phrase")
-    passphrase = Passphrase.new()
-    key = KeyfileKey(None)
-    encrypted = msgpack.packb({
-        'algorithm': 'THIS ALGORITHM IS NOT SUPPORTED',
-        'version': 1,
-    })
-
-    with pytest.raises(UnsupportedKeyFormatError):
-        key.decrypt_key_file(encrypted, passphrase)
-
-
-def test_decrypt_key_file_v2_is_unsupported(monkeypatch):
-    """There may eventually be a version 2 of the format. For now we should raise a helpful error."""
-    monkeypatch.setenv('BORG_PASSPHRASE', "hello, pass phrase")
-    passphrase = Passphrase.new()
-    key = KeyfileKey(None)
-    encrypted = msgpack.packb({
-        'version': 2,
-    })
-
-    with pytest.raises(UnsupportedKeyFormatError):
-        key.decrypt_key_file(encrypted, passphrase)
-
-
-@pytest.mark.parametrize('cli_argument, expected_algorithm', KEY_ALGORITHMS.items())
-def test_key_file_roundtrip(monkeypatch, cli_argument, expected_algorithm):
-    def to_dict(key):
-        extract = 'repository_id', 'enc_key', 'enc_hmac_key', 'id_key', 'chunk_seed'
-        return {a: getattr(key, a) for a in extract}
-
-    repository = MagicMock(id=b'repository_id')
-    monkeypatch.setenv('BORG_PASSPHRASE', "hello, pass phrase")
-
-    save_me = RepoKey.create(repository, args=MagicMock(key_algorithm=cli_argument))
-    saved = repository.save_key.call_args.args[0]
-    repository.load_key.return_value = saved
-    load_me = RepoKey.detect(repository, manifest_data=None)
-
-    assert to_dict(load_me) == to_dict(save_me)
-    assert msgpack.unpackb(a2b_base64(saved))[b'algorithm'] == expected_algorithm.encode()
-
-
-@unittest.mock.patch('getpass.getpass')
-def test_repo_key_detect_does_not_raise_integrity_error(getpass, monkeypatch):
-    """https://github.com/borgbackup/borg/pull/6469#discussion_r832670411
-
-    This is a regression test for a bug I introduced and fixed:
-
-    Traceback (most recent call last):
-      File "/home/user/borg-master/src/borg/testsuite/crypto.py", line 384, in test_repo_key_detect_does_not_raise_integrity_error
-        RepoKey.detect(repository, manifest_data=None)
-      File "/home/user/borg-master/src/borg/crypto/key.py", line 402, in detect
-        if not key.load(target, passphrase):
-      File "/home/user/borg-master/src/borg/crypto/key.py", line 654, in load
-        success = self._load(key_data, passphrase)
-      File "/home/user/borg-master/src/borg/crypto/key.py", line 418, in _load
-        data = self.decrypt_key_file(cdata, passphrase)
-      File "/home/user/borg-master/src/borg/crypto/key.py", line 444, in decrypt_key_file
-        return self.decrypt_key_file_argon2(encrypted_key, passphrase)
-      File "/home/user/borg-master/src/borg/crypto/key.py", line 470, in decrypt_key_file_argon2
-        return ae_cipher.decrypt(encrypted_key.data)
-      File "src/borg/crypto/low_level.pyx", line 302, in borg.crypto.low_level.AES256_CTR_BASE.decrypt
-        self.mac_verify(<const unsigned char *> idata.buf+aoffset, alen,
-      File "src/borg/crypto/low_level.pyx", line 382, in borg.crypto.low_level.AES256_CTR_HMAC_SHA256.mac_verify
-        raise IntegrityError('MAC Authentication failed')
-    borg.crypto.low_level.IntegrityError: MAC Authentication failed
-
-    1. FlexiKey.decrypt_key_file() is supposed to signal the decryption failure by returning None
-    2. FlexiKey.detect() relies on that interface - it tries an empty passphrase before prompting the user
-    3. my initial implementation of decrypt_key_file_argon2() was simply passing through the IntegrityError() from AES256_CTR_BASE.decrypt()
-    """
-    repository = MagicMock(id=b'repository_id')
-    getpass.return_value = "hello, pass phrase"
-    monkeypatch.setenv('BORG_DISPLAY_PASSPHRASE', 'no')
-    RepoKey.create(repository, args=MagicMock(key_algorithm='argon2'))
-    repository.load_key.return_value = repository.save_key.call_args.args[0]
-
-    RepoKey.detect(repository, manifest_data=None)
diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py
index 8014101a..1dd66d02 100644
--- a/src/borg/testsuite/helpers.py
+++ b/src/borg/testsuite/helpers.py
@@ -100,10 +100,6 @@ def test_ssh(self, monkeypatch, keys_dir):
         assert repr(Location('ssh://user@[2001:db8::192.0.2.1]/some/path')) == \
             "Location(proto='ssh', user='user', host='2001:db8::192.0.2.1', port=None, path='/some/path', archive=None)"
         assert Location('ssh://user@[2001:db8::192.0.2.1]/some/path').to_key_filename() == keys_dir + '2001_db8__192_0_2_1__some_path'
-        assert repr(Location('ssh://user@[2a02:0001:0002:0003:0004:0005:0006:0007]/some/path')) == \
-            "Location(proto='ssh', user='user', host='2a02:0001:0002:0003:0004:0005:0006:0007', port=None, path='/some/path', archive=None)"
-        assert repr(Location('ssh://user@[2a02:0001:0002:0003:0004:0005:0006:0007]:1234/some/path')) == \
-            "Location(proto='ssh', user='user', host='2a02:0001:0002:0003:0004:0005:0006:0007', port=1234, path='/some/path', archive=None)"
 
     def test_file(self, monkeypatch, keys_dir):
         monkeypatch.delenv('BORG_REPO', raising=False)
@@ -136,8 +132,6 @@ def test_scp(self, monkeypatch, keys_dir):
         assert repr(Location('user@[2001:db8::192.0.2.1]:/some/path')) == \
             "Location(proto='ssh', user='user', host='2001:db8::192.0.2.1', port=None, path='/some/path', archive=None)"
         assert Location('user@[2001:db8::192.0.2.1]:/some/path').to_key_filename() == keys_dir + '2001_db8__192_0_2_1__some_path'
-        assert repr(Location('user@[2a02:0001:0002:0003:0004:0005:0006:0007]:/some/path')) == \
-            "Location(proto='ssh', user='user', host='2a02:0001:0002:0003:0004:0005:0006:0007', port=None, path='/some/path', archive=None)"
 
     def test_smb(self, monkeypatch, keys_dir):
         monkeypatch.delenv('BORG_REPO', raising=False)
diff --git a/src/borg/testsuite/key.py b/src/borg/testsuite/key.py
index 4c84756a..14297d48 100644
--- a/src/borg/testsuite/key.py
+++ b/src/borg/testsuite/key.py
@@ -25,17 +25,16 @@
 class TestKey:
     class MockArgs:
         location = Location(tempfile.mkstemp()[1])
-        key_algorithm = "argon2"
 
     keyfile2_key_file = """
         BORG_KEY 0000000000000000000000000000000000000000000000000000000000000000
-        hqlhbGdvcml0aG2mc2hhMjU2pGRhdGHaAN4u2SiN7hqISe3OA8raBWNuvHn1R50ZU7HVCn
-        11vTJNEaj9soxUaIGcW+pAB2N5yYoKMg/sGCMuZa286iJ008DvN99rf/ORfcKrK2GmzslO
-        N3uv9Tk9HtqV/Sq5zgM9xuY9rEeQGDQVQ+AOsFamJqSUrAemGJbJqw9IerXC/jN4XPnX6J
-        pi1cXCFxHfDaEhmWrkdPNoZdirCv/eP/dOVOLmwU58YsS+MvkZNfEa16el/fSb/ENdrwJ/
-        2aYMQrDdk1d5MYzkjotv/KpofNwPXZchu2EwH7OIHWQjEVL1DZWkaGFzaNoAIO/7qn1hr3
-        F84MsMMiqpbz4KVICeBZhfAaTPs4W7BC63qml0ZXJhdGlvbnPOAAGGoKRzYWx02gAgLENQ
-        2uVCoR7EnAoiRzn8J+orbojKtJlNCnQ31SSC8rendmVyc2lvbgE=""".strip()
+        hqppdGVyYXRpb25zzgABhqCkaGFzaNoAIMyonNI+7Cjv0qHi0AOBM6bLGxACJhfgzVD2oq
+        bIS9SFqWFsZ29yaXRobaZzaGEyNTakc2FsdNoAINNK5qqJc1JWSUjACwFEWGTdM7Nd0a5l
+        1uBGPEb+9XM9p3ZlcnNpb24BpGRhdGHaANAYDT5yfPpU099oBJwMomsxouKyx/OG4QIXK2
+        hQCG2L2L/9PUu4WIuKvGrsXoP7syemujNfcZws5jLp2UPva4PkQhQsrF1RYDEMLh2eF9Ol
+        rwtkThq1tnh7KjWMG9Ijt7/aoQtq0zDYP/xaFF8XXSJxiyP5zjH5+spB6RL0oQHvbsliSh
+        /cXJq7jrqmrJ1phd6dg4SHAM/i+hubadZoS6m25OQzYAW09wZD/phG8OVa698Z5ed3HTaT
+        SmrtgJL3EoOKgUI9d6BLE4dJdBqntifo""".strip()
 
     keyfile2_cdata = unhexlify(re.sub(r'\W', '', """
         0055f161493fcfc16276e8c31493c4641e1eb19a79d0326fad0291e5a9c98e5933
@@ -45,17 +44,17 @@ class MockArgs:
 
     keyfile_blake2_key_file = """
         BORG_KEY 0000000000000000000000000000000000000000000000000000000000000000
-        hqlhbGdvcml0aG2mc2hhMjU2pGRhdGHaAZ7VCsTjbLhC1ipXOyhcGn7YnROEhP24UQvOCi
-        Oar1G+JpwgO9BIYaiCODUpzPuDQEm6WxyTwEneJ3wsuyeqyh7ru2xo9FAUKRf6jcqqZnan
-        ycTfktkUC+CPhKR7W6MTu5fPvy99chyL09/RGdD15aswR5PjNoFu4626sfMrBReyPdlxqt
-        F80m+fbNE/vln2Trqoz9EMHQ3IxjIK4q0m4Aj7TwCu7ZankFtwt898+tYsWE7lb2Ps/gXB
-        F8PM/5wHpYps2AKhDCpwKp5HyqIqlF5IzR2ydL9QP20QBjp/rSi6b+xwrfxNJZfw78f8ef
-        A2Yj7xIsxNQ0kmVmTL/UF6d7+Mw1JfurWrySiDU7QQ+RiZpWUZ0DdReB+e4zn6/KNKC884
-        34SGywADuLIQe2FKU+5jBCbutEyEGILQbAR/cgeLy5+V2XwXMJh4ytwXVIeT6Lk+qhYAdz
-        Klx4ub7XijKcOxJyBE+4k33DAhcfIT2r4/sxgMhXrIOEQPKsMAixzdcqVYkpou+6c4PZeL
-        nr+UjfJwOqK1BlWk1NgwE4GXYIKkaGFzaNoAIAzjUtpBPPh6kItZtHQZvnQG6FpucZNfBC
-        UTHFJg343jqml0ZXJhdGlvbnPOAAGGoKRzYWx02gAgz3YaUZZ/s+UWywj97EY5b4KhtJYi
-        qkPqtDDxs2j/T7+ndmVyc2lvbgE=""".strip()
+        hqlhbGdvcml0aG2mc2hhMjU2pGRhdGHaAZBu680Do3CmfWzeMCwe48KJi3Vps9mEDy7MKF
+        TastsEhiAd1RQMuxfZpklkLeddMMWk+aPtFiURRFb02JLXV5cKRC1o2ZDdiNa0nao+o6+i
+        gUjjsea9TAu25t3vxh8uQWs5BuKRLBRr0nUgrSd0IYMUgn+iVbLJRzCCssvxsklkwQxN3F
+        Y+MvBnn8kUXSeoSoQ2l0fBHzq94Y7LMOm/owMam5URnE8/UEc6ZXBrbyX4EXxDtUqJcs+D
+        i451thtlGdigDLpvf9nyK66mjiCpPCTCgtlzq0Pe1jcdhnsUYLg+qWzXZ7e2opEZoC6XxS
+        3DIuBOxG3Odqj9IKB+6/kl94vz98awPWFSpYcLZVWu7sIP38ZkUK+ad5MHTo/LvTuZdFnd
+        iqKzZIDUJl3Zl1WGmP/0xVOmfIlznkCZy4d3SMuujwIcqQ5kDvwDRPpdhBBk+UWQY5vFXk
+        kR1NBNLSTyhAzu3fiUmFl0qZ+UWPRkGAEBy/NuoEibrWwab8BX97cATyvnmOqYkU9PT0C6
+        l2l9E4bPpGhhc2jaACDnIa8KgKv84/b5sjaMgSZeIVkuKSLJy2NN8zoH8lnd36ppdGVyYX
+        Rpb25zzgABhqCkc2FsdNoAIEJLlLh7q74j3q53856H5GgzA1HH+aW5bA/as544+PGkp3Zl
+        cnNpb24B""".strip()
 
     keyfile_blake2_cdata = bytes.fromhex('04fdf9475cf2323c0ba7a99ddc011064f2e7d039f539f2e448'
                                          '0e6f5fc6ff9993d604040404040404098c8cee1c6db8c28947')