Преглед изворни кода

migrate-to-repokey command, dispatch passphrase type to repokey handler

every chunk has the encryption key type as first byte and we do not want to rewrite the whole repo
to change the passphrase type to repokey type. thus we simply dispatch this type to repokey
handler.
if there is a repokey that contains the same secrets as they were derived from the passphrase, it will just work.
if there is none yet, one needs to run migrate-to-repokey command to create it.
Thomas Waldmann пре 9 година
родитељ
комит
2f9b643edb
4 измењених фајлова са 56 додато и 9 уклоњено
  1. 44 1
      borg/archiver.py
  2. 8 4
      borg/key.py
  3. 3 3
      borg/testsuite/archiver.py
  4. 1 1
      borg/testsuite/benchmark.py

+ 44 - 1
borg/archiver.py

@@ -26,7 +26,7 @@ from .compress import Compressor, COMPR_BUFFER
 from .upgrader import AtticRepositoryUpgrader
 from .repository import Repository
 from .cache import Cache
-from .key import key_creator
+from .key import key_creator, RepoKey, PassphraseKey
 from .archive import Archive, ArchiveChecker, CHUNKER_PARAMS
 from .remote import RepositoryServer, RemoteRepository, cache_if_remote
 
@@ -124,6 +124,23 @@ class Archiver:
         key.change_passphrase()
         return EXIT_SUCCESS
 
+    def do_migrate_to_repokey(self, args):
+        """Migrate passphrase -> repokey"""
+        repository = self.open_repository(args)
+        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
+        return EXIT_SUCCESS
+
+        return EXIT_SUCCESS
+
     def do_create(self, args):
         """Create new archive"""
         matcher = PatternMatcher(fallback=True)
@@ -873,6 +890,32 @@ class Archiver:
         subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='',
                                type=location_validator(archive=False))
 
+        migrate_to_repokey_epilog = textwrap.dedent("""
+        This command migrates a repository from passphrase mode (not supported any
+        more) 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 = subparsers.add_parser('migrate-to-repokey', parents=[common_parser],
+                                          description=self.do_migrate_to_repokey.__doc__,
+                                          epilog=migrate_to_repokey_epilog,
+                                          formatter_class=argparse.RawDescriptionHelpFormatter)
+        subparser.set_defaults(func=self.do_migrate_to_repokey)
+        subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='',
+                               type=location_validator(archive=False))
+
         create_epilog = textwrap.dedent("""
         This command creates a backup archive containing all files found while recursively
         traversing all paths specified. The archive will consume almost no disk space for

+ 8 - 4
borg/key.py

@@ -47,8 +47,10 @@ def key_factory(repository, manifest_data):
         return KeyfileKey.detect(repository, manifest_data)
     elif key_type == RepoKey.TYPE:
         return RepoKey.detect(repository, manifest_data)
-    elif key_type == PassphraseKey.TYPE:  # deprecated, kill in 1.x
-        return PassphraseKey.detect(repository, manifest_data)
+    elif key_type == PassphraseKey.TYPE:
+        # this mode was killed in borg 1.0, see: https://github.com/borgbackup/borg/issues/97
+        # we just dispatch to repokey mode and assume the passphrase was migrated to a repokey.
+        return RepoKey.detect(repository, manifest_data)
     elif key_type == PlaintextKey.TYPE:
         return PlaintextKey.detect(repository, manifest_data)
     else:
@@ -132,7 +134,8 @@ class AESKeyBase(KeyBase):
         return b''.join((self.TYPE_STR, hmac, data))
 
     def decrypt(self, id, data):
-        if data[0] != self.TYPE:
+        if not (data[0] == self.TYPE or \
+            data[0] == PassphraseKey.TYPE and isinstance(self, RepoKey)):
             raise IntegrityError('Invalid encryption envelope')
         hmac_given = memoryview(data)[1:33]
         hmac_computed = memoryview(HMAC(self.enc_hmac_key, memoryview(data)[33:], sha256).digest())
@@ -148,7 +151,8 @@ class AESKeyBase(KeyBase):
         return data
 
     def extract_nonce(self, payload):
-        if payload[0] != self.TYPE:
+        if not (payload[0] == self.TYPE or \
+            payload[0] == PassphraseKey.TYPE and isinstance(self, RepoKey)):
             raise IntegrityError('Invalid encryption envelope')
         nonce = bytes_to_long(payload[33:41])
         return nonce

+ 3 - 3
borg/testsuite/archiver.py

@@ -426,7 +426,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
     def test_repository_swap_detection(self):
         self.create_test_files()
         os.environ['BORG_PASSPHRASE'] = 'passphrase'
-        self.cmd('init', '--encryption=passphrase', self.repository_location)
+        self.cmd('init', '--encryption=repokey', self.repository_location)
         repository_id = self._extract_repository_id(self.repository_path)
         self.cmd('create', self.repository_location + '::test', 'input')
         shutil.rmtree(self.repository_path)
@@ -442,7 +442,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.create_test_files()
         self.cmd('init', '--encryption=none', self.repository_location + '_unencrypted')
         os.environ['BORG_PASSPHRASE'] = 'passphrase'
-        self.cmd('init', '--encryption=passphrase', self.repository_location + '_encrypted')
+        self.cmd('init', '--encryption=repokey', self.repository_location + '_encrypted')
         self.cmd('create', self.repository_location + '_encrypted::test', 'input')
         shutil.rmtree(self.repository_path + '_encrypted')
         os.rename(self.repository_path + '_unencrypted', self.repository_path + '_encrypted')
@@ -986,7 +986,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.verify_aes_counter_uniqueness('keyfile')
 
     def test_aes_counter_uniqueness_passphrase(self):
-        self.verify_aes_counter_uniqueness('passphrase')
+        self.verify_aes_counter_uniqueness('repokey')
 
     def test_debug_dump_archive_items(self):
         self.create_test_files()

+ 1 - 1
borg/testsuite/benchmark.py

@@ -25,7 +25,7 @@ def repo_url(request, tmpdir):
     tmpdir.remove(rec=1)
 
 
-@pytest.fixture(params=["none", "passphrase"])
+@pytest.fixture(params=["none", "repokey"])
 def repo(request, cmd, repo_url):
     cmd('init', '--encryption', request.param, repo_url)
     return repo_url