Przeglądaj źródła

Merge pull request #6412 from ThomasWaldmann/remove-passphrasekey

remove PassphraseKey code and borg key migrate-to-repokey command
TW 3 lat temu
rodzic
commit
0714339d3f

+ 2 - 2
docs/faq.rst

@@ -1289,8 +1289,8 @@ There are some caveats:
   This means that data added by Borg won't deduplicate with the existing data
   This means that data added by Borg won't deduplicate with the existing data
   stored by Attic. The effect is lessened if the files cache is used with Borg.
   stored by Attic. The effect is lessened if the files cache is used with Borg.
 - Repositories in "passphrase" mode *must* be migrated to "repokey" mode using
 - Repositories in "passphrase" mode *must* be migrated to "repokey" mode using
-  :ref:`borg_key_migrate-to-repokey`. Borg does not support the "passphrase" mode
-  any other way.
+  "borg key migrate-to-repokey" (only available in borg <= 1.2.x). Borg does not
+  support the "passphrase" mode in any other way.
 
 
 Why is my backup bigger than with attic?
 Why is my backup bigger than with attic?
 ----------------------------------------
 ----------------------------------------

+ 1 - 8
docs/internals/security.rst

@@ -129,7 +129,7 @@ which is generally seen as the most robust way to create an authenticated
 encryption scheme from encryption and message authentication primitives.
 encryption scheme from encryption and message authentication primitives.
 
 
 Every operation (encryption, MAC / authentication, chunk ID derivation)
 Every operation (encryption, MAC / authentication, chunk ID derivation)
-uses independent, random keys generated by `os.urandom`_ [#]_.
+uses independent, random keys generated by `os.urandom`_.
 
 
 Borg does not support unauthenticated encryption -- only authenticated encryption
 Borg does not support unauthenticated encryption -- only authenticated encryption
 schemes are supported. No unauthenticated encryption schemes will be added
 schemes are supported. No unauthenticated encryption schemes will be added
@@ -208,13 +208,6 @@ untrusted, but a trusted synchronization channel exists between
 clients, the security database could be synchronized between them over
 clients, the security database could be synchronized between them over
 said trusted channel. This is not part of Borg's functionality.
 said trusted channel. This is not part of Borg's functionality.
 
 
-.. [#] Using the :ref:`borg key migrate-to-repokey <borg_key_migrate-to-repokey>`
-       command a user can convert repositories created using Attic in "passphrase"
-       mode to "repokey" mode. In this case the keys were directly derived from
-       the user's passphrase at some point using PBKDF2.
-
-       Borg does not support "passphrase" mode otherwise any more.
-
 .. _key_encryption:
 .. _key_encryption:
 
 
 Offline key security
 Offline key security

+ 0 - 14
docs/usage/upgrade.rst

@@ -14,17 +14,3 @@ Examples
     converting borg 0.xx to borg current
     converting borg 0.xx to borg current
     no key file found for repository
     no key file found for repository
 
 
-.. _borg_key_migrate-to-repokey:
-
-Upgrading a passphrase encrypted attic repo
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-attic offered a "passphrase" encryption mode, but this was removed in borg 1.0
-and replaced by the "repokey" mode (which stores the passphrase-protected
-encryption key into the repository config).
-
-Thus, to upgrade a "passphrase" attic repo to a "repokey" borg repo, 2 steps
-are needed, in this order:
-
-- borg upgrade repo
-- borg key migrate-to-repokey repo

+ 1 - 44
src/borg/archiver.py

@@ -44,7 +44,7 @@ try:
     from .cache import Cache, assert_secure, SecurityManager
     from .cache import Cache, assert_secure, SecurityManager
     from .constants import *  # NOQA
     from .constants import *  # NOQA
     from .compress import CompressionSpec
     from .compress import CompressionSpec
-    from .crypto.key import key_creator, key_argument_names, tam_required_file, tam_required, RepoKey, PassphraseKey
+    from .crypto.key import key_creator, key_argument_names, tam_required_file, tam_required, RepoKey
     from .crypto.keymanager import KeyManager
     from .crypto.keymanager import KeyManager
     from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, EXIT_SIGNAL_BASE
     from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, EXIT_SIGNAL_BASE
     from .helpers import Error, NoManifestError, set_ec
     from .helpers import Error, NoManifestError, set_ec
@@ -400,22 +400,6 @@ class Archiver:
             manager.import_keyfile(args)
             manager.import_keyfile(args)
         return EXIT_SUCCESS
         return EXIT_SUCCESS
 
 
-    @with_repository(manifest=False)
-    def do_migrate_to_repokey(self, args, repository):
-        """Migrate passphrase -> repokey"""
-        manifest_data = repository.get(Manifest.MANIFEST_ID)
-        key_old = PassphraseKey.detect(repository, manifest_data)
-        key_new = RepoKey(repository)
-        key_new.target = repository
-        key_new.repository_id = repository.id
-        key_new.enc_key = key_old.enc_key
-        key_new.enc_hmac_key = key_old.enc_hmac_key
-        key_new.id_key = key_old.id_key
-        key_new.chunk_seed = key_old.chunk_seed
-        key_new.change_passphrase()  # option to change key protection passphrase, save
-        logger.info('Key updated')
-        return EXIT_SUCCESS
-
     def do_benchmark_crud(self, args):
     def do_benchmark_crud(self, args):
         """Benchmark Create, Read, Update, Delete for archives."""
         """Benchmark Create, Read, Update, Delete for archives."""
         def measurement_run(repo, path):
         def measurement_run(repo, path):
@@ -4266,33 +4250,6 @@ class Archiver:
         subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='',
         subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='',
                                type=location_validator(archive=False))
                                type=location_validator(archive=False))
 
 
-        migrate_to_repokey_epilog = process_epilog("""
-        This command migrates a repository from passphrase mode (removed in Borg 1.0)
-        to repokey mode.
-
-        You will be first asked for the repository passphrase (to open it in passphrase
-        mode). This is the same passphrase as you used to use for this repo before 1.0.
-
-        It will then derive the different secrets from this passphrase.
-
-        Then you will be asked for a new passphrase (twice, for safety). This
-        passphrase will be used to protect the repokey (which contains these same
-        secrets in encrypted form). You may use the same passphrase as you used to
-        use, but you may also use a different one.
-
-        After migrating to repokey mode, you can change the passphrase at any time.
-        But please note: the secrets will always stay the same and they could always
-        be derived from your (old) passphrase-mode passphrase.
-        """)
-        subparser = key_parsers.add_parser('migrate-to-repokey', parents=[common_parser], add_help=False,
-                                          description=self.do_migrate_to_repokey.__doc__,
-                                          epilog=migrate_to_repokey_epilog,
-                                          formatter_class=argparse.RawDescriptionHelpFormatter,
-                                          help='migrate passphrase-mode repository to repokey')
-        subparser.set_defaults(func=self.do_migrate_to_repokey)
-        subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='',
-                               type=location_validator(archive=False))
-
         # borg list
         # borg list
         list_epilog = process_epilog("""
         list_epilog = process_epilog("""
         This command lists the contents of a repository or an archive.
         This command lists the contents of a repository or an archive.

+ 9 - 62
src/borg/crypto/key.py

@@ -119,9 +119,7 @@ def key_argument_names():
 def identify_key(manifest_data):
 def identify_key(manifest_data):
     key_type = manifest_data[0]
     key_type = manifest_data[0]
     if key_type == PassphraseKey.TYPE:
     if key_type == PassphraseKey.TYPE:
-        # we just dispatch to repokey mode and assume the passphrase was migrated to a repokey.
-        # see also comment in PassphraseKey class.
-        return RepoKey
+        return RepoKey  # see comment in PassphraseKey class.
 
 
     for key in AVAILABLE_KEY_TYPES:
     for key in AVAILABLE_KEY_TYPES:
         if key.TYPE == key_type:
         if key.TYPE == key_type:
@@ -327,8 +325,7 @@ class ID_BLAKE2b_256:
     def id_hash(self, data):
     def id_hash(self, data):
         return blake2b_256(self.id_key, data)
         return blake2b_256(self.id_key, data)
 
 
-    def init_from_random_data(self, data=None):
-        assert data is None  # PassphraseKey is the only caller using *data*
+    def init_from_random_data(self):
         super().init_from_random_data()
         super().init_from_random_data()
         self.enc_hmac_key = random_blake2b_256_key()
         self.enc_hmac_key = random_blake2b_256_key()
         self.id_key = random_blake2b_256_key()
         self.id_key = random_blake2b_256_key()
@@ -347,8 +344,6 @@ class ID_HMAC_SHA_256:
 
 
 class AESKeyBase(KeyBase):
 class AESKeyBase(KeyBase):
     """
     """
-    Common base class shared by KeyfileKey and PassphraseKey
-
     Chunks are encrypted using 256bit AES in Counter Mode (CTR)
     Chunks are encrypted using 256bit AES in Counter Mode (CTR)
 
 
     Payload layout: TYPE(1) + HMAC(32) + NONCE(8) + CIPHERTEXT
     Payload layout: TYPE(1) + HMAC(32) + NONCE(8) + CIPHERTEXT
@@ -386,9 +381,8 @@ class AESKeyBase(KeyBase):
         self.assert_id(id, data)
         self.assert_id(id, data)
         return data
         return data
 
 
-    def init_from_random_data(self, data=None):
-        if data is None:
-            data = os.urandom(100)
+    def init_from_random_data(self):
+        data = os.urandom(100)
         self.enc_key = data[0:32]
         self.enc_key = data[0:32]
         self.enc_hmac_key = data[32:64]
         self.enc_hmac_key = data[32:64]
         self.id_key = data[64:96]
         self.id_key = data[64:96]
@@ -523,59 +517,13 @@ class Passphrase(str):
         return pbkdf2_hmac('sha256', self.encode('utf-8'), salt, iterations, length)
         return pbkdf2_hmac('sha256', self.encode('utf-8'), salt, iterations, length)
 
 
 
 
-class PassphraseKey(ID_HMAC_SHA_256, AESKeyBase):
-    # This mode was killed in borg 1.0, see: https://github.com/borgbackup/borg/issues/97
-    # Reasons:
-    # - you can never ever change your passphrase for existing repos.
-    # - you can never ever use a different iterations count for existing repos.
-    # "Killed" means:
-    # - there is no automatic dispatch to this class via type byte
-    # - --encryption=passphrase is an invalid argument now
-    # This class is kept for a while to support migration from passphrase to repokey mode.
+class PassphraseKey:
+    # this is only a stub, repos with this mode could not be created any more since borg 1.0, see #97.
+    # in borg 1.3 all of its code and also the "borg key migrate-to-repokey" command was removed.
+    # if you still need to, you can use "borg key migrate-to-repokey" with borg 1.0, 1.1 and 1.2.
+    # Nowadays, we just dispatch this to RepoKey and assume the passphrase was migrated to a repokey.
     TYPE = 0x01
     TYPE = 0x01
     NAME = 'passphrase'
     NAME = 'passphrase'
-    ARG_NAME = None
-    STORAGE = KeyBlobStorage.NO_STORAGE
-
-    iterations = 100000  # must not be changed ever!
-
-    @classmethod
-    def create(cls, repository, args):
-        key = cls(repository)
-        logger.warning('WARNING: "passphrase" mode is unsupported since borg 1.0.')
-        passphrase = Passphrase.new(allow_empty=False)
-        key.init(repository, passphrase)
-        return key
-
-    @classmethod
-    def detect(cls, repository, manifest_data):
-        prompt = 'Enter passphrase for %s: ' % repository._location.canonical_path()
-        key = cls(repository)
-        passphrase = Passphrase.env_passphrase()
-        if passphrase is None:
-            passphrase = Passphrase.getpass(prompt)
-        for retry in range(1, 3):
-            key.init(repository, passphrase)
-            try:
-                key.decrypt(None, manifest_data)
-                key.init_ciphers(manifest_data)
-                key._passphrase = passphrase
-                return key
-            except IntegrityError:
-                passphrase = Passphrase.getpass(prompt)
-        else:
-            raise PasswordRetriesExceeded
-
-    def change_passphrase(self):
-        class ImmutablePassphraseError(Error):
-            """The passphrase for this encryption key type can't be changed."""
-
-        raise ImmutablePassphraseError
-
-    def init(self, repository, passphrase):
-        self.init_from_random_data(passphrase.kdf(repository.id, self.iterations, 100))
-        self.init_ciphers()
-        self.tam_required = False
 
 
 
 
 class KeyfileKeyBase(AESKeyBase):
 class KeyfileKeyBase(AESKeyBase):
@@ -888,7 +836,6 @@ class Blake2AuthenticatedKey(ID_BLAKE2b_256, AuthenticatedKeyBase):
 
 
 AVAILABLE_KEY_TYPES = (
 AVAILABLE_KEY_TYPES = (
     PlaintextKey,
     PlaintextKey,
-    PassphraseKey,
     KeyfileKey, RepoKey, AuthenticatedKey,
     KeyfileKey, RepoKey, AuthenticatedKey,
     Blake2KeyfileKey, Blake2RepoKey, Blake2AuthenticatedKey,
     Blake2KeyfileKey, Blake2RepoKey, Blake2AuthenticatedKey,
 )
 )

+ 1 - 26
src/borg/testsuite/key.py

@@ -7,7 +7,7 @@ from binascii import hexlify, unhexlify
 import pytest
 import pytest
 
 
 from ..crypto.key import Passphrase, PasswordRetriesExceeded, bin_to_hex
 from ..crypto.key import Passphrase, PasswordRetriesExceeded, bin_to_hex
-from ..crypto.key import PlaintextKey, PassphraseKey, AuthenticatedKey, RepoKey, KeyfileKey, \
+from ..crypto.key import PlaintextKey, AuthenticatedKey, RepoKey, KeyfileKey, \
     Blake2KeyfileKey, Blake2RepoKey, Blake2AuthenticatedKey
     Blake2KeyfileKey, Blake2RepoKey, Blake2AuthenticatedKey
 from ..crypto.key import ID_HMAC_SHA_256, ID_BLAKE2b_256
 from ..crypto.key import ID_HMAC_SHA_256, ID_BLAKE2b_256
 from ..crypto.key import TAMRequiredError, TAMInvalid, TAMUnsupportedSuiteError, UnsupportedManifestError
 from ..crypto.key import TAMRequiredError, TAMInvalid, TAMUnsupportedSuiteError, UnsupportedManifestError
@@ -182,31 +182,6 @@ class TestKey:
         key = Blake2KeyfileKey.detect(self.MockRepository(), self.keyfile_blake2_cdata)
         key = Blake2KeyfileKey.detect(self.MockRepository(), self.keyfile_blake2_cdata)
         assert key.decrypt(self.keyfile_blake2_id, self.keyfile_blake2_cdata) == b'payload'
         assert key.decrypt(self.keyfile_blake2_id, self.keyfile_blake2_cdata) == b'payload'
 
 
-    def test_passphrase(self, keys_dir, monkeypatch):
-        monkeypatch.setenv('BORG_PASSPHRASE', 'test')
-        key = PassphraseKey.create(self.MockRepository(), None)
-        assert key.cipher.next_iv() == 0
-        assert hexlify(key.id_key) == b'793b0717f9d8fb01c751a487e9b827897ceea62409870600013fbc6b4d8d7ca6'
-        assert hexlify(key.enc_hmac_key) == b'b885a05d329a086627412a6142aaeb9f6c54ab7950f996dd65587251f6bc0901'
-        assert hexlify(key.enc_key) == b'2ff3654c6daf7381dbbe718d2b20b4f1ea1e34caa6cc65f6bb3ac376b93fed2a'
-        assert key.chunk_seed == -775740477
-        manifest = key.encrypt(b'ABC')
-        assert key.cipher.extract_iv(manifest) == 0
-        manifest2 = key.encrypt(b'ABC')
-        assert manifest != manifest2
-        assert key.decrypt(None, manifest) == key.decrypt(None, manifest2)
-        assert key.cipher.extract_iv(manifest2) == 1
-        iv = key.cipher.extract_iv(manifest)
-        key2 = PassphraseKey.detect(self.MockRepository(), manifest)
-        assert key2.cipher.next_iv() == iv + key2.cipher.block_count(len(manifest))
-        assert key.id_key == key2.id_key
-        assert key.enc_hmac_key == key2.enc_hmac_key
-        assert key.enc_key == key2.enc_key
-        assert key.chunk_seed == key2.chunk_seed
-        chunk = b'foo'
-        assert hexlify(key.id_hash(chunk)) == b'818217cf07d37efad3860766dcdf1d21e401650fed2d76ed1d797d3aae925990'
-        assert chunk == key2.decrypt(key2.id_hash(chunk), key.encrypt(chunk))
-
     def _corrupt_byte(self, key, data, offset):
     def _corrupt_byte(self, key, data, offset):
         data = bytearray(data)
         data = bytearray(data)
         data[offset] ^= 1
         data[offset] ^= 1