diff --git a/src/borg/archive.py b/src/borg/archive.py index 9c60b611a..49bec6205 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1813,6 +1813,19 @@ class ArchiveChecker: 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 ac998854c..3ec399156 100644 --- a/src/borg/crypto/key.py +++ b/src/borg/crypto/key.py @@ -89,6 +89,13 @@ 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 @@ -98,6 +105,15 @@ class TAMInvalid(IntegrityError): 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.""" traceback = False @@ -266,6 +282,46 @@ class KeyBase: 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 = 0x02 diff --git a/src/borg/helpers/msgpack.py b/src/borg/helpers/msgpack.py index 3c6565639..309c98834 100644 --- a/src/borg/helpers/msgpack.py +++ b/src/borg/helpers/msgpack.py @@ -209,6 +209,12 @@ def get_limited_unpacker(kind): max_str_len=255, # archive name object_hook=StableDict, )) + elif kind == 'archive': + args.update(dict(use_list=True, # default value + max_map_len=100, # ARCHIVE_KEYS ~= 20 + max_str_len=10000, # comment + object_hook=StableDict, + )) elif kind == 'key': args.update(dict(use_list=True, # default value max_array_len=0, # not used diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index c586d2e25..8f34a56e3 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -3957,7 +3957,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): corrupted_manifest = manifest + b'corrupted!' repository.put(Manifest.MANIFEST_ID, corrupted_manifest) - archive = msgpack.packb({ + archive_dict = { 'cmdline': [], 'items': [], 'hostname': 'foo', @@ -3965,7 +3965,8 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): 'name': 'archive1', 'time': '2016-12-15T18:49:51.849711', 'version': 1, - }) + } + archive = key.pack_and_authenticate_metadata(archive_dict, context=b'archive') archive_id = key.id_hash(archive) repository.put(archive_id, key.encrypt(archive)) repository.commit(compact=False)