Browse Source

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

Thomas Waldmann 1 year ago
parent
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
             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:
     BORG_LIST_FORMAT
         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
         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
         +++++++++++++++++++++++++++++

+ 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.passphrase import Passphrase, PasswordRetriesExceeded, PassphraseWrong
 from ..helpers import msgpack
+from ..helpers import workarounds
 from ..item import Key, EncryptedKey, want_bytes
 from ..manifest import Manifest
 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 . 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):
     """Unsupported payload type {}. A newer version is required to access this repository."""
@@ -267,6 +271,8 @@ class KeyBase:
         offset = data.index(tam_hmac)
         data[offset : offset + 64] = bytes(64)
         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")
         if not hmac.compare_digest(calculated_hmac, tam_hmac):
             raise TAMInvalid()
@@ -800,6 +806,19 @@ class AuthenticatedKeyBase(AESKeyBase, FlexiKey):
     # It's only authenticated, not encrypted.
     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):
         success = super().load(target, passphrase)
         self.logically_encrypted = False

+ 8 - 3
src/borg/repoobj.py

@@ -1,8 +1,11 @@
 from struct import Struct
 
-from .helpers import msgpack
+from .helpers import msgpack, workarounds
 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:
     meta_len_hdr = Struct("<H")  # 16bit unsigned int
@@ -110,7 +113,8 @@ class RepoObj:
             compressor_cls, compression_level = Compressor.detect(compr_hdr)
             compressor = compressor_cls(level=compression_level)
             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:
             meta, data = None, None
         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)
         if decompress:
             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:
             meta, data = None, None
         return meta_compressed if want_compressed else meta, data_compressed if want_compressed else data