Selaa lähdekoodia

move key commands to archiver.keys

Thomas Waldmann 2 vuotta sitten
vanhempi
sitoutus
af2ee7aeb1
3 muutettua tiedostoa jossa 320 lisäystä ja 299 poistoa
  1. 1 0
      setup.cfg
  2. 3 299
      src/borg/archiver/__init__.py
  3. 316 0
      src/borg/archiver/keys.py

+ 1 - 0
setup.cfg

@@ -120,6 +120,7 @@ per_file_ignores =
     src/borg/archiver/benchmarks.py:F405
     src/borg/archiver/common.py:E501,F405
     src/borg/archiver/debug.py:F405
+    src/borg/archiver/keys.py:F405
     src/borg/archiver/tar.py:F405
     src/borg/cache.py:E127,E128,E402,E501,E722,W504
     src/borg/fuse.py:E402,E501,E722,W504

+ 3 - 299
src/borg/archiver/__init__.py

@@ -39,9 +39,6 @@ try:
     from ..constants import *  # NOQA
     from ..compress import CompressionSpec
     from ..crypto.key import key_creator, key_argument_names, tam_required_file
-    from ..crypto.key import AESOCBRepoKey, CHPORepoKey, Blake2AESOCBRepoKey, Blake2CHPORepoKey
-    from ..crypto.key import AESOCBKeyfileKey, CHPOKeyfileKey, Blake2AESOCBKeyfileKey, Blake2CHPOKeyfileKey
-    from ..crypto.keymanager import KeyManager
     from ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, EXIT_SIGNAL_BASE
     from ..helpers import Error, NoManifestError, set_ec
     from ..helpers import positive_int_validator, location_validator, archivename_validator, ChunkerParams, Location
@@ -109,10 +106,11 @@ def get_func(args):
 
 from .benchmarks import BenchmarkMixIn
 from .debug import DebugMixIn
+from .keys import KeysMixIn
 from .tar import TarMixIn
 
 
-class Archiver(DebugMixIn, TarMixIn, BenchmarkMixIn):
+class Archiver(DebugMixIn, TarMixIn, BenchmarkMixIn, KeysMixIn):
     def __init__(self, lock_wait=None, prog=None):
         self.exit_code = EXIT_SUCCESS
         self.lock_wait = lock_wait
@@ -316,134 +314,6 @@ class Archiver(DebugMixIn, TarMixIn, BenchmarkMixIn):
             return EXIT_WARNING
         return EXIT_SUCCESS
 
-    @with_repository(compatibility=(Manifest.Operation.CHECK,))
-    def do_change_passphrase(self, args, repository, manifest, key):
-        """Change repository key file passphrase"""
-        if not hasattr(key, "change_passphrase"):
-            print("This repository is not encrypted, cannot change the passphrase.")
-            return EXIT_ERROR
-        key.change_passphrase()
-        logger.info("Key updated")
-        if hasattr(key, "find_key"):
-            # print key location to make backing it up easier
-            logger.info("Key location: %s", key.find_key())
-        return EXIT_SUCCESS
-
-    @with_repository(exclusive=True, manifest=True, cache=True, compatibility=(Manifest.Operation.CHECK,))
-    def do_change_location(self, args, repository, manifest, key, cache):
-        """Change repository key location"""
-        if not hasattr(key, "change_passphrase"):
-            print("This repository is not encrypted, cannot change the key location.")
-            return EXIT_ERROR
-
-        if args.key_mode == "keyfile":
-            if isinstance(key, AESOCBRepoKey):
-                key_new = AESOCBKeyfileKey(repository)
-            elif isinstance(key, CHPORepoKey):
-                key_new = CHPOKeyfileKey(repository)
-            elif isinstance(key, Blake2AESOCBRepoKey):
-                key_new = Blake2AESOCBKeyfileKey(repository)
-            elif isinstance(key, Blake2CHPORepoKey):
-                key_new = Blake2CHPOKeyfileKey(repository)
-            else:
-                print("Change not needed or not supported.")
-                return EXIT_WARNING
-        if args.key_mode == "repokey":
-            if isinstance(key, AESOCBKeyfileKey):
-                key_new = AESOCBRepoKey(repository)
-            elif isinstance(key, CHPOKeyfileKey):
-                key_new = CHPORepoKey(repository)
-            elif isinstance(key, Blake2AESOCBKeyfileKey):
-                key_new = Blake2AESOCBRepoKey(repository)
-            elif isinstance(key, Blake2CHPOKeyfileKey):
-                key_new = Blake2CHPORepoKey(repository)
-            else:
-                print("Change not needed or not supported.")
-                return EXIT_WARNING
-
-        for name in (
-            "repository_id",
-            "enc_key",
-            "enc_hmac_key",
-            "id_key",
-            "chunk_seed",
-            "tam_required",
-            "sessionid",
-            "cipher",
-        ):
-            value = getattr(key, name)
-            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)
-
-        # rewrite the manifest with the new key, so that the key-type byte of the manifest changes
-        manifest.key = key_new
-        manifest.write()
-        repository.commit(compact=False)
-
-        # we need to rewrite cache config and security key-type info,
-        # so that the cached key-type will match the repo key-type.
-        cache.begin_txn()  # need to start a cache transaction, otherwise commit() does nothing.
-        cache.key = key_new
-        cache.commit()
-
-        loc = key_new.find_key() if hasattr(key_new, "find_key") else None
-        if args.keep:
-            logger.info(f"Key copied to {loc}")
-        else:
-            key.remove(key.target)  # remove key from current location
-            logger.info(f"Key moved to {loc}")
-
-        return EXIT_SUCCESS
-
-    @with_repository(exclusive=True, compatibility=(Manifest.Operation.CHECK,))
-    def do_change_algorithm(self, args, repository, manifest, key):
-        """Change repository key algorithm"""
-        if not hasattr(key, "change_passphrase"):
-            print("This repository is not encrypted, cannot change the algorithm.")
-            return EXIT_ERROR
-        key.save(key.target, key._passphrase, algorithm=KEY_ALGORITHMS[args.algorithm])
-        return EXIT_SUCCESS
-
-    @with_repository(lock=False, exclusive=False, manifest=False, cache=False)
-    def do_key_export(self, args, repository):
-        """Export the repository key for backup"""
-        manager = KeyManager(repository)
-        manager.load_keyblob()
-        if args.paper:
-            manager.export_paperkey(args.path)
-        else:
-            try:
-                if args.qr:
-                    manager.export_qr(args.path)
-                else:
-                    manager.export(args.path)
-            except IsADirectoryError:
-                self.print_error(f"'{args.path}' must be a file, not a directory")
-                return EXIT_ERROR
-        return EXIT_SUCCESS
-
-    @with_repository(lock=False, exclusive=False, manifest=False, cache=False)
-    def do_key_import(self, args, repository):
-        """Import the repository key from backup"""
-        manager = KeyManager(repository)
-        if args.paper:
-            if args.path:
-                self.print_error("with --paper import from file is not supported")
-                return EXIT_ERROR
-            manager.import_paperkey(args)
-        else:
-            if not args.path:
-                self.print_error("input file to import key from expected")
-                return EXIT_ERROR
-            if args.path != "-" and not os.path.exists(args.path):
-                self.print_error("input file does not exist: " + args.path)
-                return EXIT_ERROR
-            manager.import_keyfile(args)
-        return EXIT_SUCCESS
-
     @with_repository(fake="dry_run", exclusive=True, compatibility=(Manifest.Operation.WRITE,))
     def do_create(self, args, repository, manifest=None, key=None):
         """Create new archive"""
@@ -3683,173 +3553,7 @@ class Archiver(DebugMixIn, TarMixIn, BenchmarkMixIn):
             help="create the parent directories of the repository directory, if they are missing.",
         )
 
-        # borg key
-        subparser = subparsers.add_parser(
-            "key",
-            parents=[mid_common_parser],
-            add_help=False,
-            description="Manage a keyfile or repokey of a repository",
-            epilog="",
-            formatter_class=argparse.RawDescriptionHelpFormatter,
-            help="manage repository key",
-        )
-
-        key_parsers = subparser.add_subparsers(title="required arguments", metavar="<command>")
-        subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser))
-
-        key_export_epilog = process_epilog(
-            """
-        If repository encryption is used, the repository is inaccessible
-        without the key. This command allows one to backup this essential key.
-        Note that the backup produced does not include the passphrase itself
-        (i.e. the exported key stays encrypted). In order to regain access to a
-        repository, one needs both the exported key and the original passphrase.
-
-        There are three backup formats. The normal backup format is suitable for
-        digital storage as a file. The ``--paper`` backup format is optimized
-        for printing and typing in while importing, with per line checks to
-        reduce problems with manual input. The ``--qr-html`` creates a printable
-        HTML template with a QR code and a copy of the ``--paper``-formatted key.
-
-        For repositories using keyfile encryption the key is saved locally
-        on the system that is capable of doing backups. To guard against loss
-        of this key, the key needs to be backed up independently of the main
-        data backup.
-
-        For repositories using the repokey encryption the key is saved in the
-        repository in the config file. A backup is thus not strictly needed,
-        but guards against the repository becoming inaccessible if the file
-        is damaged for some reason.
-
-        Examples::
-
-            borg key export /path/to/repo > encrypted-key-backup
-            borg key export --paper /path/to/repo > encrypted-key-backup.txt
-            borg key export --qr-html /path/to/repo > encrypted-key-backup.html
-            # Or pass the output file as an argument instead of redirecting stdout:
-            borg key export /path/to/repo encrypted-key-backup
-            borg key export --paper /path/to/repo encrypted-key-backup.txt
-            borg key export --qr-html /path/to/repo encrypted-key-backup.html
-
-
-        """
-        )
-        subparser = key_parsers.add_parser(
-            "export",
-            parents=[common_parser],
-            add_help=False,
-            description=self.do_key_export.__doc__,
-            epilog=key_export_epilog,
-            formatter_class=argparse.RawDescriptionHelpFormatter,
-            help="export repository key for backup",
-        )
-        subparser.set_defaults(func=self.do_key_export)
-        subparser.add_argument("path", metavar="PATH", nargs="?", type=str, help="where to store the backup")
-        subparser.add_argument(
-            "--paper",
-            dest="paper",
-            action="store_true",
-            help="Create an export suitable for printing and later type-in",
-        )
-        subparser.add_argument(
-            "--qr-html",
-            dest="qr",
-            action="store_true",
-            help="Create an html file suitable for printing and later type-in or qr scan",
-        )
-
-        key_import_epilog = process_epilog(
-            """
-        This command restores a key previously backed up with the export command.
-
-        If the ``--paper`` option is given, the import will be an interactive
-        process in which each line is checked for plausibility before
-        proceeding to the next line. For this format PATH must not be given.
-
-        For repositories using keyfile encryption, the key file which ``borg key
-        import`` writes to depends on several factors. If the ``BORG_KEY_FILE``
-        environment variable is set and non-empty, ``borg key import`` creates
-        or overwrites that file named by ``$BORG_KEY_FILE``. Otherwise, ``borg
-        key import`` searches in the ``$BORG_KEYS_DIR`` directory for a key file
-        associated with the repository. If a key file is found in
-        ``$BORG_KEYS_DIR``, ``borg key import`` overwrites it; otherwise, ``borg
-        key import`` creates a new key file in ``$BORG_KEYS_DIR``.
-        """
-        )
-        subparser = key_parsers.add_parser(
-            "import",
-            parents=[common_parser],
-            add_help=False,
-            description=self.do_key_import.__doc__,
-            epilog=key_import_epilog,
-            formatter_class=argparse.RawDescriptionHelpFormatter,
-            help="import repository key from backup",
-        )
-        subparser.set_defaults(func=self.do_key_import)
-        subparser.add_argument(
-            "path", metavar="PATH", nargs="?", type=str, help="path to the backup ('-' to read from stdin)"
-        )
-        subparser.add_argument(
-            "--paper",
-            dest="paper",
-            action="store_true",
-            help="interactively import from a backup done with ``--paper``",
-        )
-
-        change_passphrase_epilog = process_epilog(
-            """
-        The key files used for repository encryption are optionally passphrase
-        protected. This command can be used to change this passphrase.
-
-        Please note that this command only changes the passphrase, but not any
-        secret protected by it (like e.g. encryption/MAC keys or chunker seed).
-        Thus, changing the passphrase after passphrase and borg key got compromised
-        does not protect future (nor past) backups to the same repository.
-        """
-        )
-        subparser = key_parsers.add_parser(
-            "change-passphrase",
-            parents=[common_parser],
-            add_help=False,
-            description=self.do_change_passphrase.__doc__,
-            epilog=change_passphrase_epilog,
-            formatter_class=argparse.RawDescriptionHelpFormatter,
-            help="change repository passphrase",
-        )
-        subparser.set_defaults(func=self.do_change_passphrase)
-
-        change_location_epilog = process_epilog(
-            """
-        Change the location of a borg key. The key can be stored at different locations:
-
-        - keyfile: locally, usually in the home directory
-        - repokey: inside the repo (in the repo config)
-
-        Please note:
-
-        This command does NOT change the crypto algorithms, just the key location,
-        thus you must ONLY give the key location (keyfile or repokey).
-        """
-        )
-        subparser = key_parsers.add_parser(
-            "change-location",
-            parents=[common_parser],
-            add_help=False,
-            description=self.do_change_location.__doc__,
-            epilog=change_location_epilog,
-            formatter_class=argparse.RawDescriptionHelpFormatter,
-            help="change key location",
-        )
-        subparser.set_defaults(func=self.do_change_location)
-        subparser.add_argument(
-            "key_mode", metavar="KEY_LOCATION", choices=("repokey", "keyfile"), help="select key location"
-        )
-        subparser.add_argument(
-            "--keep",
-            dest="keep",
-            action="store_true",
-            help="keep the key also at the current location (default: remove it)",
-        )
+        self.build_parser_keys(subparsers, common_parser, mid_common_parser)
 
         # borg list
         list_epilog = (

+ 316 - 0
src/borg/archiver/keys.py

@@ -0,0 +1,316 @@
+import argparse
+import functools
+import os
+
+from ..constants import *  # NOQA
+from ..crypto.key import AESOCBRepoKey, CHPORepoKey, Blake2AESOCBRepoKey, Blake2CHPORepoKey
+from ..crypto.key import AESOCBKeyfileKey, CHPOKeyfileKey, Blake2AESOCBKeyfileKey, Blake2CHPOKeyfileKey
+from ..crypto.keymanager import KeyManager
+from ..helpers import Manifest
+
+from .common import with_repository
+
+from ..logger import create_logger
+
+logger = create_logger(__name__)
+
+
+class KeysMixIn:
+    @with_repository(compatibility=(Manifest.Operation.CHECK,))
+    def do_change_passphrase(self, args, repository, manifest, key):
+        """Change repository key file passphrase"""
+        if not hasattr(key, "change_passphrase"):
+            print("This repository is not encrypted, cannot change the passphrase.")
+            return EXIT_ERROR
+        key.change_passphrase()
+        logger.info("Key updated")
+        if hasattr(key, "find_key"):
+            # print key location to make backing it up easier
+            logger.info("Key location: %s", key.find_key())
+        return EXIT_SUCCESS
+
+    @with_repository(exclusive=True, manifest=True, cache=True, compatibility=(Manifest.Operation.CHECK,))
+    def do_change_location(self, args, repository, manifest, key, cache):
+        """Change repository key location"""
+        if not hasattr(key, "change_passphrase"):
+            print("This repository is not encrypted, cannot change the key location.")
+            return EXIT_ERROR
+
+        if args.key_mode == "keyfile":
+            if isinstance(key, AESOCBRepoKey):
+                key_new = AESOCBKeyfileKey(repository)
+            elif isinstance(key, CHPORepoKey):
+                key_new = CHPOKeyfileKey(repository)
+            elif isinstance(key, Blake2AESOCBRepoKey):
+                key_new = Blake2AESOCBKeyfileKey(repository)
+            elif isinstance(key, Blake2CHPORepoKey):
+                key_new = Blake2CHPOKeyfileKey(repository)
+            else:
+                print("Change not needed or not supported.")
+                return EXIT_WARNING
+        if args.key_mode == "repokey":
+            if isinstance(key, AESOCBKeyfileKey):
+                key_new = AESOCBRepoKey(repository)
+            elif isinstance(key, CHPOKeyfileKey):
+                key_new = CHPORepoKey(repository)
+            elif isinstance(key, Blake2AESOCBKeyfileKey):
+                key_new = Blake2AESOCBRepoKey(repository)
+            elif isinstance(key, Blake2CHPOKeyfileKey):
+                key_new = Blake2CHPORepoKey(repository)
+            else:
+                print("Change not needed or not supported.")
+                return EXIT_WARNING
+
+        for name in (
+            "repository_id",
+            "enc_key",
+            "enc_hmac_key",
+            "id_key",
+            "chunk_seed",
+            "tam_required",
+            "sessionid",
+            "cipher",
+        ):
+            value = getattr(key, name)
+            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)
+
+        # rewrite the manifest with the new key, so that the key-type byte of the manifest changes
+        manifest.key = key_new
+        manifest.write()
+        repository.commit(compact=False)
+
+        # we need to rewrite cache config and security key-type info,
+        # so that the cached key-type will match the repo key-type.
+        cache.begin_txn()  # need to start a cache transaction, otherwise commit() does nothing.
+        cache.key = key_new
+        cache.commit()
+
+        loc = key_new.find_key() if hasattr(key_new, "find_key") else None
+        if args.keep:
+            logger.info(f"Key copied to {loc}")
+        else:
+            key.remove(key.target)  # remove key from current location
+            logger.info(f"Key moved to {loc}")
+
+        return EXIT_SUCCESS
+
+    @with_repository(exclusive=True, compatibility=(Manifest.Operation.CHECK,))
+    def do_change_algorithm(self, args, repository, manifest, key):
+        """Change repository key algorithm"""
+        if not hasattr(key, "change_passphrase"):
+            print("This repository is not encrypted, cannot change the algorithm.")
+            return EXIT_ERROR
+        key.save(key.target, key._passphrase, algorithm=KEY_ALGORITHMS[args.algorithm])
+        return EXIT_SUCCESS
+
+    @with_repository(lock=False, exclusive=False, manifest=False, cache=False)
+    def do_key_export(self, args, repository):
+        """Export the repository key for backup"""
+        manager = KeyManager(repository)
+        manager.load_keyblob()
+        if args.paper:
+            manager.export_paperkey(args.path)
+        else:
+            try:
+                if args.qr:
+                    manager.export_qr(args.path)
+                else:
+                    manager.export(args.path)
+            except IsADirectoryError:
+                self.print_error(f"'{args.path}' must be a file, not a directory")
+                return EXIT_ERROR
+        return EXIT_SUCCESS
+
+    @with_repository(lock=False, exclusive=False, manifest=False, cache=False)
+    def do_key_import(self, args, repository):
+        """Import the repository key from backup"""
+        manager = KeyManager(repository)
+        if args.paper:
+            if args.path:
+                self.print_error("with --paper import from file is not supported")
+                return EXIT_ERROR
+            manager.import_paperkey(args)
+        else:
+            if not args.path:
+                self.print_error("input file to import key from expected")
+                return EXIT_ERROR
+            if args.path != "-" and not os.path.exists(args.path):
+                self.print_error("input file does not exist: " + args.path)
+                return EXIT_ERROR
+            manager.import_keyfile(args)
+        return EXIT_SUCCESS
+
+    def build_parser_keys(self, subparsers, common_parser, mid_common_parser):
+
+        from .common import process_epilog
+
+        subparser = subparsers.add_parser(
+            "key",
+            parents=[mid_common_parser],
+            add_help=False,
+            description="Manage a keyfile or repokey of a repository",
+            epilog="",
+            formatter_class=argparse.RawDescriptionHelpFormatter,
+            help="manage repository key",
+        )
+
+        key_parsers = subparser.add_subparsers(title="required arguments", metavar="<command>")
+        subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser))
+
+        key_export_epilog = process_epilog(
+            """
+        If repository encryption is used, the repository is inaccessible
+        without the key. This command allows one to backup this essential key.
+        Note that the backup produced does not include the passphrase itself
+        (i.e. the exported key stays encrypted). In order to regain access to a
+        repository, one needs both the exported key and the original passphrase.
+
+        There are three backup formats. The normal backup format is suitable for
+        digital storage as a file. The ``--paper`` backup format is optimized
+        for printing and typing in while importing, with per line checks to
+        reduce problems with manual input. The ``--qr-html`` creates a printable
+        HTML template with a QR code and a copy of the ``--paper``-formatted key.
+
+        For repositories using keyfile encryption the key is saved locally
+        on the system that is capable of doing backups. To guard against loss
+        of this key, the key needs to be backed up independently of the main
+        data backup.
+
+        For repositories using the repokey encryption the key is saved in the
+        repository in the config file. A backup is thus not strictly needed,
+        but guards against the repository becoming inaccessible if the file
+        is damaged for some reason.
+
+        Examples::
+
+            borg key export /path/to/repo > encrypted-key-backup
+            borg key export --paper /path/to/repo > encrypted-key-backup.txt
+            borg key export --qr-html /path/to/repo > encrypted-key-backup.html
+            # Or pass the output file as an argument instead of redirecting stdout:
+            borg key export /path/to/repo encrypted-key-backup
+            borg key export --paper /path/to/repo encrypted-key-backup.txt
+            borg key export --qr-html /path/to/repo encrypted-key-backup.html
+
+
+        """
+        )
+        subparser = key_parsers.add_parser(
+            "export",
+            parents=[common_parser],
+            add_help=False,
+            description=self.do_key_export.__doc__,
+            epilog=key_export_epilog,
+            formatter_class=argparse.RawDescriptionHelpFormatter,
+            help="export repository key for backup",
+        )
+        subparser.set_defaults(func=self.do_key_export)
+        subparser.add_argument("path", metavar="PATH", nargs="?", type=str, help="where to store the backup")
+        subparser.add_argument(
+            "--paper",
+            dest="paper",
+            action="store_true",
+            help="Create an export suitable for printing and later type-in",
+        )
+        subparser.add_argument(
+            "--qr-html",
+            dest="qr",
+            action="store_true",
+            help="Create an html file suitable for printing and later type-in or qr scan",
+        )
+
+        key_import_epilog = process_epilog(
+            """
+        This command restores a key previously backed up with the export command.
+
+        If the ``--paper`` option is given, the import will be an interactive
+        process in which each line is checked for plausibility before
+        proceeding to the next line. For this format PATH must not be given.
+
+        For repositories using keyfile encryption, the key file which ``borg key
+        import`` writes to depends on several factors. If the ``BORG_KEY_FILE``
+        environment variable is set and non-empty, ``borg key import`` creates
+        or overwrites that file named by ``$BORG_KEY_FILE``. Otherwise, ``borg
+        key import`` searches in the ``$BORG_KEYS_DIR`` directory for a key file
+        associated with the repository. If a key file is found in
+        ``$BORG_KEYS_DIR``, ``borg key import`` overwrites it; otherwise, ``borg
+        key import`` creates a new key file in ``$BORG_KEYS_DIR``.
+        """
+        )
+        subparser = key_parsers.add_parser(
+            "import",
+            parents=[common_parser],
+            add_help=False,
+            description=self.do_key_import.__doc__,
+            epilog=key_import_epilog,
+            formatter_class=argparse.RawDescriptionHelpFormatter,
+            help="import repository key from backup",
+        )
+        subparser.set_defaults(func=self.do_key_import)
+        subparser.add_argument(
+            "path", metavar="PATH", nargs="?", type=str, help="path to the backup ('-' to read from stdin)"
+        )
+        subparser.add_argument(
+            "--paper",
+            dest="paper",
+            action="store_true",
+            help="interactively import from a backup done with ``--paper``",
+        )
+
+        change_passphrase_epilog = process_epilog(
+            """
+        The key files used for repository encryption are optionally passphrase
+        protected. This command can be used to change this passphrase.
+
+        Please note that this command only changes the passphrase, but not any
+        secret protected by it (like e.g. encryption/MAC keys or chunker seed).
+        Thus, changing the passphrase after passphrase and borg key got compromised
+        does not protect future (nor past) backups to the same repository.
+        """
+        )
+        subparser = key_parsers.add_parser(
+            "change-passphrase",
+            parents=[common_parser],
+            add_help=False,
+            description=self.do_change_passphrase.__doc__,
+            epilog=change_passphrase_epilog,
+            formatter_class=argparse.RawDescriptionHelpFormatter,
+            help="change repository passphrase",
+        )
+        subparser.set_defaults(func=self.do_change_passphrase)
+
+        change_location_epilog = process_epilog(
+            """
+        Change the location of a borg key. The key can be stored at different locations:
+
+        - keyfile: locally, usually in the home directory
+        - repokey: inside the repo (in the repo config)
+
+        Please note:
+
+        This command does NOT change the crypto algorithms, just the key location,
+        thus you must ONLY give the key location (keyfile or repokey).
+        """
+        )
+        subparser = key_parsers.add_parser(
+            "change-location",
+            parents=[common_parser],
+            add_help=False,
+            description=self.do_change_location.__doc__,
+            epilog=change_location_epilog,
+            formatter_class=argparse.RawDescriptionHelpFormatter,
+            help="change key location",
+        )
+        subparser.set_defaults(func=self.do_change_location)
+        subparser.add_argument(
+            "key_mode", metavar="KEY_LOCATION", choices=("repokey", "keyfile"), help="select key location"
+        )
+        subparser.add_argument(
+            "--keep",
+            dest="keep",
+            action="store_true",
+            help="keep the key also at the current location (default: remove it)",
+        )