Quellcode durchsuchen

BORG_WORKAROUNDS=authenticated_no_key to extract from authenticated repos without key, fixes #7700

Thomas Waldmann vor 1 Jahr
Ursprung
Commit
3051473168

+ 16 - 0
docs/usage/general/environment.rst.inc

@@ -104,6 +104,22 @@ General:
             caused EROFS. You will need this to make archives from volume shadow copies
             caused EROFS. You will need this to make archives from volume shadow copies
             in WSL1 (Windows Subsystem for Linux 1).
             in WSL1 (Windows Subsystem for Linux 1).
 
 
+        authenticated_no_key
+            Work around a lost passphrase or key for an ``authenticated`` mode repository
+            (these are only authenticated, but not encrypted).
+            If the key is missing in the repository config, add ``key = anything`` there.
+
+            This workaround is **only** for emergencies and **only** to extract data
+            from an affected repository (read-only access)::
+
+                BORG_WORKAROUNDS=authenticated_no_key borg extract repo::archive
+
+            After you have extracted all data you need, you MUST delete the repository::
+
+                BORG_WORKAROUNDS=authenticated_no_key borg delete repo
+
+            Now you can init a fresh repo. Make sure you do not use the workaround any more.
+
 Output formatting:
 Output formatting:
     BORG_LIST_FORMAT
     BORG_LIST_FORMAT
         Giving the default value for ``borg list --format=X``.
         Giving the default value for ``borg list --format=X``.

+ 2 - 0
src/borg/archiver/rcreate_cmd.py

@@ -152,6 +152,8 @@ class RCreateMixIn:
 
 
         If you do **not** want to encrypt the contents of your backups, but still want to detect
         If you do **not** want to encrypt the contents of your backups, but still want to detect
         malicious tampering use an `authenticated` mode. It's like `repokey` minus encryption.
         malicious tampering use an `authenticated` mode. It's like `repokey` minus encryption.
+        To normally work with ``authenticated`` repos, you will need the passphrase, but
+        there is an emergency workaround, see ``BORG_WORKAROUNDS=authenticated_no_key`` docs.
 
 
         Creating a related repository
         Creating a related repository
         +++++++++++++++++++++++++++++
         +++++++++++++++++++++++++++++

+ 19 - 0
src/borg/crypto/key.py

@@ -20,6 +20,7 @@ from ..helpers import get_limited_unpacker
 from ..helpers import bin_to_hex
 from ..helpers import bin_to_hex
 from ..helpers.passphrase import Passphrase, PasswordRetriesExceeded, PassphraseWrong
 from ..helpers.passphrase import Passphrase, PasswordRetriesExceeded, PassphraseWrong
 from ..helpers import msgpack
 from ..helpers import msgpack
+from ..helpers import workarounds
 from ..item import Key, EncryptedKey, want_bytes
 from ..item import Key, EncryptedKey, want_bytes
 from ..manifest import Manifest
 from ..manifest import Manifest
 from ..platform import SaveFile
 from ..platform import SaveFile
@@ -30,6 +31,9 @@ from .low_level import AES, bytes_to_int, num_cipher_blocks, hmac_sha256, blake2
 from .low_level import AES256_CTR_HMAC_SHA256, AES256_CTR_BLAKE2b, AES256_OCB, CHACHA20_POLY1305
 from .low_level import AES256_CTR_HMAC_SHA256, AES256_CTR_BLAKE2b, AES256_OCB, CHACHA20_POLY1305
 from . import low_level
 from . import low_level
 
 
+# workaround for lost passphrase or key in "authenticated" or "authenticated-blake2" mode
+AUTHENTICATED_NO_KEY = "authenticated_no_key" in workarounds
+
 
 
 class UnsupportedPayloadError(Error):
 class UnsupportedPayloadError(Error):
     """Unsupported payload type {}. A newer version is required to access this repository."""
     """Unsupported payload type {}. A newer version is required to access this repository."""
@@ -267,6 +271,8 @@ class KeyBase:
         offset = data.index(tam_hmac)
         offset = data.index(tam_hmac)
         data[offset : offset + 64] = bytes(64)
         data[offset : offset + 64] = bytes(64)
         tam_key = self._tam_key(tam_salt, context=b"manifest")
         tam_key = self._tam_key(tam_salt, context=b"manifest")
+        if AUTHENTICATED_NO_KEY:
+            return unpacked, True  # True is a lie.
         calculated_hmac = hmac.digest(tam_key, data, "sha512")
         calculated_hmac = hmac.digest(tam_key, data, "sha512")
         if not hmac.compare_digest(calculated_hmac, tam_hmac):
         if not hmac.compare_digest(calculated_hmac, tam_hmac):
             raise TAMInvalid()
             raise TAMInvalid()
@@ -800,6 +806,19 @@ class AuthenticatedKeyBase(AESKeyBase, FlexiKey):
     # It's only authenticated, not encrypted.
     # It's only authenticated, not encrypted.
     logically_encrypted = False
     logically_encrypted = False
 
 
+    def _load(self, key_data, passphrase):
+        if AUTHENTICATED_NO_KEY:
+            # fake _load if we have no key or passphrase
+            NOPE = bytes(32)  # 256 bit all-zero
+            self.repository_id = NOPE
+            self.enc_key = NOPE
+            self.enc_hmac_key = NOPE
+            self.id_key = NOPE
+            self.chunk_seed = 0
+            self.tam_required = False
+            return True
+        return super()._load(key_data, passphrase)
+
     def load(self, target, passphrase):
     def load(self, target, passphrase):
         success = super().load(target, passphrase)
         success = super().load(target, passphrase)
         self.logically_encrypted = False
         self.logically_encrypted = False

+ 8 - 3
src/borg/repoobj.py

@@ -1,8 +1,11 @@
 from struct import Struct
 from struct import Struct
 
 
-from .helpers import msgpack
+from .helpers import msgpack, workarounds
 from .compress import Compressor, LZ4_COMPRESSOR, get_compressor
 from .compress import Compressor, LZ4_COMPRESSOR, get_compressor
 
 
+# workaround for lost passphrase or key in "authenticated" or "authenticated-blake2" mode
+AUTHENTICATED_NO_KEY = "authenticated_no_key" in workarounds
+
 
 
 class RepoObj:
 class RepoObj:
     meta_len_hdr = Struct("<H")  # 16bit unsigned int
     meta_len_hdr = Struct("<H")  # 16bit unsigned int
@@ -110,7 +113,8 @@ class RepoObj:
             compressor_cls, compression_level = Compressor.detect(compr_hdr)
             compressor_cls, compression_level = Compressor.detect(compr_hdr)
             compressor = compressor_cls(level=compression_level)
             compressor = compressor_cls(level=compression_level)
             meta, data = compressor.decompress(dict(meta_compressed), data_compressed[:psize])
             meta, data = compressor.decompress(dict(meta_compressed), data_compressed[:psize])
-            self.key.assert_id(id, data)
+            if not AUTHENTICATED_NO_KEY:
+                self.key.assert_id(id, data)
         else:
         else:
             meta, data = None, None
             meta, data = None, None
         return meta_compressed if want_compressed else meta, data_compressed if want_compressed else data
         return meta_compressed if want_compressed else meta, data_compressed if want_compressed else data
@@ -170,7 +174,8 @@ class RepoObj1:  # legacy
         meta_compressed["csize"] = len(data_compressed)
         meta_compressed["csize"] = len(data_compressed)
         if decompress:
         if decompress:
             meta, data = compressor.decompress(None, data_compressed)
             meta, data = compressor.decompress(None, data_compressed)
-            self.key.assert_id(id, data)
+            if not AUTHENTICATED_NO_KEY:
+                self.key.assert_id(id, data)
         else:
         else:
             meta, data = None, None
             meta, data = None, None
         return meta_compressed if want_compressed else meta, data_compressed if want_compressed else data
         return meta_compressed if want_compressed else meta, data_compressed if want_compressed else data