浏览代码

init --other-location=OTHER_REPO: reuse key material from OTHER_REPO, fixes #6554

it potentially will ask for the passphrase for the key of OTHERREPO.
for the newly created repo, it will use the same passphrase.

it will copy: enc_key, enc_hmac_key, id_key, chunker_seed.

keeping the id_key (and id algorithm) and the chunker seed (and chunker
algorithm and parameters) is desirable for deduplication.
the id algorithm is usually either HMAC-SHA256 or BLAKE2b.

keeping the enc_key / enc_hmac_key must be implemented carefully:
A) AES-CTR -> AES-CTR is INSECURE due to nonce reuse, thus not allowed.
B) AES-CTR -> AEAD with session keys is secure.
C) AEAD with session keys -> AEAD with session keys is secure.

AEAD modes with session keys: AES-OCB and CHACHA20-POLY1305.
Thomas Waldmann 3 年之前
父节点
当前提交
4b070040d6
共有 3 个文件被更改,包括 79 次插入21 次删除
  1. 6 2
      src/borg/archiver.py
  2. 72 18
      src/borg/crypto/key.py
  3. 1 1
      src/borg/testsuite/archiver.py

+ 6 - 2
src/borg/archiver.py

@@ -339,12 +339,13 @@ class Archiver:
         return EXIT_SUCCESS
 
     @with_repository(create=True, exclusive=True, manifest=False)
-    def do_init(self, args, repository):
+    @with_other_repository(key=True, compatibility=(Manifest.Operation.READ, ))
+    def do_init(self, args, repository, *, other_repository=None, other_key=None):
         """Initialize an empty repository"""
         path = args.location.canonical_path()
         logger.info('Initializing repository at "%s"' % path)
         try:
-            key = key_creator(repository, args)
+            key = key_creator(repository, args, other_key=other_key)
         except (EOFError, KeyboardInterrupt):
             repository.destroy()
             return EXIT_WARNING
@@ -4425,6 +4426,9 @@ class Archiver:
         subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='',
                                type=location_validator(archive=False),
                                help='repository to create')
+        subparser.add_argument('--other-location', metavar='OTHER_REPOSITORY', dest='other_location',
+                               type=location_validator(archive=False, other=True),
+                               help='reuse the key material from the other repository')
         subparser.add_argument('-e', '--encryption', metavar='MODE', dest='encryption', required=True,
                                choices=key_argument_names(),
                                help='select encryption key mode **(required)**')

+ 72 - 18
src/borg/crypto/key.py

@@ -85,11 +85,11 @@ class TAMUnsupportedSuiteError(IntegrityError):
     traceback = False
 
 
-def key_creator(repository, args):
+def key_creator(repository, args, *, other_key=None):
     for key in AVAILABLE_KEY_TYPES:
         if key.ARG_NAME == args.encryption:
             assert key.ARG_NAME is not None
-            return key.create(repository, args)
+            return key.create(repository, args, other_key=other_key)
     else:
         raise ValueError('Invalid encryption mode "%s"' % args.encryption)
 
@@ -262,7 +262,7 @@ class PlaintextKey(KeyBase):
         self.tam_required = False
 
     @classmethod
-    def create(cls, repository, args):
+    def create(cls, repository, args, **kw):
         logger.info('Encryption NOT enabled.\nUse the "--encryption=repokey|keyfile" to enable encryption.')
         return cls(repository)
 
@@ -367,15 +367,27 @@ class AESKeyBase(KeyBase):
         self.assert_id(id, data)
         return data
 
+    def init_from_given_data(self, *, enc_key, enc_hmac_key, id_key, chunk_seed):
+        assert len(enc_key) >= 32
+        assert len(enc_hmac_key) >= 32
+        assert len(id_key) >= 32
+        assert isinstance(chunk_seed, int)
+        self.enc_key = enc_key
+        self.enc_hmac_key = enc_hmac_key
+        self.id_key = id_key
+        self.chunk_seed = chunk_seed
+
     def init_from_random_data(self):
         data = os.urandom(100)
-        self.enc_key = data[0:32]
-        self.enc_hmac_key = data[32:64]
-        self.id_key = data[64:96]
-        self.chunk_seed = bytes_to_int(data[96:100])
+        chunk_seed = bytes_to_int(data[96:100])
         # Convert to signed int32
-        if self.chunk_seed & 0x80000000:
-            self.chunk_seed = self.chunk_seed - 0xffffffff - 1
+        if chunk_seed & 0x80000000:
+            chunk_seed = chunk_seed - 0xffffffff - 1
+        self.init_from_given_data(
+            enc_key=data[0:32],
+            enc_hmac_key=data[32:64],
+            id_key=data[64:96],
+            chunk_seed=chunk_seed)
 
     def init_ciphers(self, manifest_data=None):
         self.cipher = self.CIPHERSUITE(mac_key=self.enc_hmac_key, enc_key=self.enc_key, header_len=1, aad_offset=1)
@@ -572,11 +584,41 @@ class FlexiKey:
         self.save(self.target, passphrase, algorithm=self._encrypted_key_algorithm)
 
     @classmethod
-    def create(cls, repository, args):
-        passphrase = Passphrase.new(allow_empty=True)
+    def create(cls, repository, args, *, other_key=None):
         key = cls(repository)
         key.repository_id = repository.id
-        key.init_from_random_data()
+        if other_key is not None:
+            if isinstance(other_key, PlaintextKey):
+                raise Error("Copying key material from an unencrypted repository is not possible.")
+            if isinstance(key, AESKeyBase):
+                # user must use an AEADKeyBase subclass (AEAD modes with session keys)
+                raise Error("Copying key material to an AES-CTR based mode is insecure and unsupported.")
+            # avoid breaking the deduplication by changing the id hash
+            same_ids = (
+                # these use HMAC-SHA256 IDs:
+                isinstance(other_key, (RepoKey, KeyfileKey))
+                and
+                isinstance(key, (AESOCBRepoKey, AESOCBKeyfileKey,
+                                 CHPORepoKey, CHPOKeyfileKey))
+                or
+                # these use BLAKE2b IDs:
+                isinstance(other_key, (Blake2RepoKey, Blake2KeyfileKey))
+                and
+                isinstance(key, (Blake2AESOCBRepoKey, Blake2AESOCBKeyfileKey,
+                                 Blake2CHPORepoKey, Blake2CHPOKeyfileKey))
+            )
+            if not same_ids:
+                # either keep HMAC-SHA256 or keep BLAKE2b!
+                raise Error("You must keep the same ID hash (HMAC-SHA256 or BLAKE2b) or deduplication will break.")
+            key.init_from_given_data(
+                enc_key=other_key.enc_key,
+                enc_hmac_key=other_key.enc_hmac_key,
+                id_key=other_key.id_key,
+                chunk_seed=other_key.chunk_seed)
+            passphrase = other_key._passphrase
+        else:
+            key.init_from_random_data()
+            passphrase = Passphrase.new(allow_empty=True)
         key.init_ciphers()
         target = key.get_new_target(args)
         key.save(target, passphrase, create=True, algorithm=KEY_ALGORITHMS[args.key_algorithm])
@@ -851,15 +893,27 @@ class AEADKeyBase(KeyBase):
         self.assert_id(id, data)
         return data
 
+    def init_from_given_data(self, *, enc_key, enc_hmac_key, id_key, chunk_seed):
+        assert len(enc_key) >= 32
+        assert len(enc_hmac_key) >= 32
+        assert len(id_key) >= 32
+        assert isinstance(chunk_seed, int)
+        self.enc_key = enc_key
+        self.enc_hmac_key = enc_hmac_key
+        self.id_key = id_key
+        self.chunk_seed = chunk_seed
+
     def init_from_random_data(self):
         data = os.urandom(100)
-        self.enc_key = data[0:32]
-        self.enc_hmac_key = data[32:64]
-        self.id_key = data[64:96]
-        self.chunk_seed = bytes_to_int(data[96:100])
+        chunk_seed = bytes_to_int(data[96:100])
         # Convert to signed int32
-        if self.chunk_seed & 0x80000000:
-            self.chunk_seed = self.chunk_seed - 0xffffffff - 1
+        if chunk_seed & 0x80000000:
+            chunk_seed = chunk_seed - 0xffffffff - 1
+        self.init_from_given_data(
+            enc_key=data[0:32],
+            enc_hmac_key=data[32:64],
+            id_key=data[64:96],
+            chunk_seed=chunk_seed)
 
     def _get_session_key(self, sessionid):
         assert len(sessionid) == 24  # 192bit

+ 1 - 1
src/borg/testsuite/archiver.py

@@ -2878,7 +2878,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         assert "is invalid" in output
 
     def test_init_interrupt(self):
-        def raise_eof(*args):
+        def raise_eof(*args, **kwargs):
             raise EOFError
 
         with patch.object(FlexiKey, 'create', raise_eof):