diff --git a/src/borg/archive.py b/src/borg/archive.py index 808f901fb6..ef4217a9b9 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1992,6 +1992,19 @@ def valid_archive(obj): except msgpack.UnpackException: continue if valid_archive(archive): + # **after** doing the low-level checks and having a strong indication that we + # are likely looking at an archive item here, also check the TAM authentication: + try: + archive, verified = self.key.unpack_and_verify_archive(data, force_tam_not_required=False) + except IntegrityError: + # TAM issues - do not accept this archive! + # either somebody is trying to attack us with a fake archive data or + # we have an ancient archive made before TAM was a thing (borg < 1.0.9) **and** this repo + # was not correctly upgraded to borg 1.2.5 (see advisory at top of the changelog). + # borg can't tell the difference, so it has to assume this archive might be an attack + # and drops this archive. + continue + # note: if we get here and verified is False, a TAM is not required. archive = ArchiveItem(internal_dict=archive) name = archive.name logger.info("Found archive %s", name) diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index 338888b3af..ebaee58a2b 100644 --- a/src/borg/crypto/key.py +++ b/src/borg/crypto/key.py @@ -72,6 +72,15 @@ class TAMRequiredError(IntegrityError): traceback = False +class ArchiveTAMRequiredError(TAMRequiredError): + __doc__ = textwrap.dedent( + """ + Archive '{}' is unauthenticated, but it is required for this repository. + """ + ).strip() + traceback = False + + class TAMInvalid(IntegrityError): __doc__ = IntegrityError.__doc__ traceback = False @@ -81,6 +90,15 @@ def __init__(self): super().__init__("Manifest authentication did not verify") +class ArchiveTAMInvalid(IntegrityError): + __doc__ = IntegrityError.__doc__ + traceback = False + + def __init__(self): + # Error message becomes: "Data integrity error: Archive authentication did not verify" + super().__init__("Archive authentication did not verify") + + class TAMUnsupportedSuiteError(IntegrityError): """Could not verify manifest: Unsupported suite {!r}; a newer version is needed.""" @@ -279,6 +297,46 @@ def unpack_and_verify_manifest(self, data, force_tam_not_required=False): logger.debug("TAM-verified manifest") return unpacked, True + def unpack_and_verify_archive(self, data, force_tam_not_required=False): + """Unpack msgpacked *data* and return (object, did_verify).""" + tam_required = self.tam_required + if force_tam_not_required and tam_required: + logger.warning("Archive authentication DISABLED.") + tam_required = False + data = bytearray(data) + unpacker = get_limited_unpacker("archive") + unpacker.feed(data) + unpacked = unpacker.unpack() + if b"tam" not in unpacked: + if tam_required: + archive_name = unpacked.get(b"name", b"").decode("ascii", "replace") + raise ArchiveTAMRequiredError(archive_name) + else: + logger.debug("TAM not found and not required") + return unpacked, False + tam = unpacked.pop(b"tam", None) + if not isinstance(tam, dict): + raise ArchiveTAMInvalid() + tam_type = tam.get(b"type", b"").decode("ascii", "replace") + if tam_type != "HKDF_HMAC_SHA512": + if tam_required: + raise TAMUnsupportedSuiteError(repr(tam_type)) + else: + logger.debug("Ignoring TAM made with unsupported suite, since TAM is not required: %r", tam_type) + return unpacked, False + tam_hmac = tam.get(b"hmac") + tam_salt = tam.get(b"salt") + if not isinstance(tam_salt, bytes) or not isinstance(tam_hmac, bytes): + raise ArchiveTAMInvalid() + offset = data.index(tam_hmac) + data[offset : offset + 64] = bytes(64) + tam_key = self._tam_key(tam_salt, context=b"archive") + calculated_hmac = hmac.digest(tam_key, data, "sha512") + if not hmac.compare_digest(calculated_hmac, tam_hmac): + raise ArchiveTAMInvalid() + logger.debug("TAM-verified archive") + return unpacked, True + class PlaintextKey(KeyBase): TYPE = KeyType.PLAINTEXT diff --git a/src/borg/helpers/msgpack.py b/src/borg/helpers/msgpack.py index ddbd95944a..dce6eb33fb 100644 --- a/src/borg/helpers/msgpack.py +++ b/src/borg/helpers/msgpack.py @@ -219,10 +219,10 @@ def get_limited_unpacker(kind): args = dict(use_list=False, max_buffer_size=3 * max(BUFSIZE, MAX_OBJECT_SIZE)) # return tuples, not lists if kind in ("server", "client"): pass # nothing special - elif kind in ("manifest", "key"): + elif kind in ("manifest", "archive", "key"): args.update(dict(use_list=True, object_hook=StableDict)) # default value else: - raise ValueError('kind must be "server", "client", "manifest" or "key"') + raise ValueError('kind must be "server", "client", "manifest", "archive" or "key"') return Unpacker(**args) diff --git a/src/borg/testsuite/archiver/check_cmd.py b/src/borg/testsuite/archiver/check_cmd.py index b5db325386..5ddce3437c 100644 --- a/src/borg/testsuite/archiver/check_cmd.py +++ b/src/borg/testsuite/archiver/check_cmd.py @@ -6,7 +6,6 @@ from ...archive import ChunkBuffer from ...constants import * # NOQA from ...helpers import bin_to_hex -from ...helpers import msgpack from ...manifest import Manifest from ...repository import Repository from . import cmd, src_file, create_src_archive, open_archive, generate_archiver_tests, RK_ENCRYPTION @@ -233,17 +232,16 @@ def test_manifest_rebuild_duplicate_archive(archivers, request): manifest = repository.get(Manifest.MANIFEST_ID) corrupted_manifest = manifest + b"corrupted!" repository.put(Manifest.MANIFEST_ID, corrupted_manifest) - archive = msgpack.packb( - { - "command_line": "", - "item_ptrs": [], - "hostname": "foo", - "username": "bar", - "name": "archive1", - "time": "2016-12-15T18:49:51.849711", - "version": 2, - } - ) + archive_dict = { + "command_line": "", + "item_ptrs": [], + "hostname": "foo", + "username": "bar", + "name": "archive1", + "time": "2016-12-15T18:49:51.849711", + "version": 2, + } + archive = repo_objs.key.pack_and_authenticate_metadata(archive_dict, context=b"archive") archive_id = repo_objs.id_hash(archive) repository.put(archive_id, repo_objs.format(archive_id, {}, archive)) repository.commit(compact=False)