|
@@ -30,7 +30,7 @@ from .compress import Compressor
|
|
from .upgrader import AtticRepositoryUpgrader, BorgRepositoryUpgrader
|
|
from .upgrader import AtticRepositoryUpgrader, BorgRepositoryUpgrader
|
|
from .repository import Repository
|
|
from .repository import Repository
|
|
from .cache import Cache
|
|
from .cache import Cache
|
|
-from .key import key_creator, RepoKey, PassphraseKey
|
|
|
|
|
|
+from .key import key_creator, tam_required_file, tam_required, RepoKey, PassphraseKey
|
|
from .keymanager import KeyManager
|
|
from .keymanager import KeyManager
|
|
from .archive import backup_io, BackupOSError, Archive, ArchiveChecker, CHUNKER_PARAMS, is_special
|
|
from .archive import backup_io, BackupOSError, Archive, ArchiveChecker, CHUNKER_PARAMS, is_special
|
|
from .remote import RepositoryServer, RemoteRepository, cache_if_remote
|
|
from .remote import RepositoryServer, RemoteRepository, cache_if_remote
|
|
@@ -48,10 +48,12 @@ def argument(args, str_or_bool):
|
|
"""If bool is passed, return it. If str is passed, retrieve named attribute from args."""
|
|
"""If bool is passed, return it. If str is passed, retrieve named attribute from args."""
|
|
if isinstance(str_or_bool, str):
|
|
if isinstance(str_or_bool, str):
|
|
return getattr(args, str_or_bool)
|
|
return getattr(args, str_or_bool)
|
|
|
|
+ if isinstance(str_or_bool, (list, tuple)):
|
|
|
|
+ return any(getattr(args, item) for item in str_or_bool)
|
|
return str_or_bool
|
|
return str_or_bool
|
|
|
|
|
|
|
|
|
|
-def with_repository(fake=False, create=False, lock=True, exclusive=False, manifest=True, cache=False):
|
|
|
|
|
|
+def with_repository(fake=False, invert_fake=False, create=False, lock=True, exclusive=False, manifest=True, cache=False):
|
|
"""
|
|
"""
|
|
Method decorator for subcommand-handling methods: do_XYZ(self, args, repository, …)
|
|
Method decorator for subcommand-handling methods: do_XYZ(self, args, repository, …)
|
|
|
|
|
|
@@ -68,7 +70,7 @@ def with_repository(fake=False, create=False, lock=True, exclusive=False, manife
|
|
def wrapper(self, args, **kwargs):
|
|
def wrapper(self, args, **kwargs):
|
|
location = args.location # note: 'location' must be always present in args
|
|
location = args.location # note: 'location' must be always present in args
|
|
append_only = getattr(args, 'append_only', False)
|
|
append_only = getattr(args, 'append_only', False)
|
|
- if argument(args, fake):
|
|
|
|
|
|
+ if argument(args, fake) ^ invert_fake:
|
|
return method(self, args, repository=None, **kwargs)
|
|
return method(self, args, repository=None, **kwargs)
|
|
elif location.proto == 'ssh':
|
|
elif location.proto == 'ssh':
|
|
repository = RemoteRepository(location, create=create, exclusive=argument(args, exclusive),
|
|
repository = RemoteRepository(location, create=create, exclusive=argument(args, exclusive),
|
|
@@ -127,7 +129,8 @@ class Archiver:
|
|
@with_repository(create=True, exclusive=True, manifest=False)
|
|
@with_repository(create=True, exclusive=True, manifest=False)
|
|
def do_init(self, args, repository):
|
|
def do_init(self, args, repository):
|
|
"""Initialize an empty repository"""
|
|
"""Initialize an empty repository"""
|
|
- logger.info('Initializing repository at "%s"' % args.location.canonical_path())
|
|
|
|
|
|
+ path = args.location.canonical_path()
|
|
|
|
+ logger.info('Initializing repository at "%s"' % path)
|
|
key = key_creator(repository, args)
|
|
key = key_creator(repository, args)
|
|
manifest = Manifest(key, repository)
|
|
manifest = Manifest(key, repository)
|
|
manifest.key = key
|
|
manifest.key = key
|
|
@@ -135,6 +138,19 @@ class Archiver:
|
|
repository.commit()
|
|
repository.commit()
|
|
with Cache(repository, key, manifest, warn_if_unencrypted=False):
|
|
with Cache(repository, key, manifest, warn_if_unencrypted=False):
|
|
pass
|
|
pass
|
|
|
|
+ if key.tam_required:
|
|
|
|
+ tam_file = tam_required_file(repository)
|
|
|
|
+ open(tam_file, 'w').close()
|
|
|
|
+ logger.warning(
|
|
|
|
+ '\n'
|
|
|
|
+ 'By default repositories initialized with this version will produce security\n'
|
|
|
|
+ 'errors if written to with an older version (up to and including Borg 1.0.8).\n'
|
|
|
|
+ '\n'
|
|
|
|
+ 'If you want to use these older versions, you can disable the check by runnning:\n'
|
|
|
|
+ 'borg upgrade --disable-tam \'%s\'\n'
|
|
|
|
+ '\n'
|
|
|
|
+ 'See https://borgbackup.readthedocs.io/en/stable/changes.html#pre-1-0-9-manifest-spoofing-vulnerability '
|
|
|
|
+ 'for details about the security implications.', path)
|
|
return self.exit_code
|
|
return self.exit_code
|
|
|
|
|
|
@with_repository(exclusive=True, manifest=False)
|
|
@with_repository(exclusive=True, manifest=False)
|
|
@@ -161,6 +177,7 @@ class Archiver:
|
|
def do_change_passphrase(self, args, repository, manifest, key):
|
|
def do_change_passphrase(self, args, repository, manifest, key):
|
|
"""Change repository key file passphrase"""
|
|
"""Change repository key file passphrase"""
|
|
key.change_passphrase()
|
|
key.change_passphrase()
|
|
|
|
+ logger.info('Key updated')
|
|
return EXIT_SUCCESS
|
|
return EXIT_SUCCESS
|
|
|
|
|
|
@with_repository(lock=False, exclusive=False, manifest=False, cache=False)
|
|
@with_repository(lock=False, exclusive=False, manifest=False, cache=False)
|
|
@@ -209,6 +226,7 @@ class Archiver:
|
|
key_new.id_key = key_old.id_key
|
|
key_new.id_key = key_old.id_key
|
|
key_new.chunk_seed = key_old.chunk_seed
|
|
key_new.chunk_seed = key_old.chunk_seed
|
|
key_new.change_passphrase() # option to change key protection passphrase, save
|
|
key_new.change_passphrase() # option to change key protection passphrase, save
|
|
|
|
+ logger.info('Key updated')
|
|
return EXIT_SUCCESS
|
|
return EXIT_SUCCESS
|
|
|
|
|
|
@with_repository(fake='dry_run', exclusive=True)
|
|
@with_repository(fake='dry_run', exclusive=True)
|
|
@@ -705,21 +723,57 @@ class Archiver:
|
|
DASHES)
|
|
DASHES)
|
|
return self.exit_code
|
|
return self.exit_code
|
|
|
|
|
|
- def do_upgrade(self, args):
|
|
|
|
|
|
+ @with_repository(fake=('tam', 'disable_tam'), invert_fake=True, manifest=False, exclusive=True)
|
|
|
|
+ def do_upgrade(self, args, repository, manifest=None, key=None):
|
|
"""upgrade a repository from a previous version"""
|
|
"""upgrade a repository from a previous version"""
|
|
- # mainly for upgrades from Attic repositories,
|
|
|
|
- # but also supports borg 0.xx -> 1.0 upgrade.
|
|
|
|
|
|
+ if args.tam:
|
|
|
|
+ manifest, key = Manifest.load(repository, force_tam_not_required=args.force)
|
|
|
|
+
|
|
|
|
+ if not manifest.tam_verified or not manifest.config.get(b'tam_required', False):
|
|
|
|
+ # The standard archive listing doesn't include the archive ID like in borg 1.1.x
|
|
|
|
+ print('Manifest contents:')
|
|
|
|
+ for archive_info in manifest.list_archive_infos(sort_by='ts'):
|
|
|
|
+ print(format_archive(archive_info), '[%s]' % bin_to_hex(archive_info.id))
|
|
|
|
+ manifest.config[b'tam_required'] = True
|
|
|
|
+ manifest.write()
|
|
|
|
+ repository.commit()
|
|
|
|
+ if not key.tam_required:
|
|
|
|
+ key.tam_required = True
|
|
|
|
+ key.change_passphrase(key._passphrase)
|
|
|
|
+ print('Key updated')
|
|
|
|
+ if hasattr(key, 'find_key'):
|
|
|
|
+ print('Key location:', key.find_key())
|
|
|
|
+ if not tam_required(repository):
|
|
|
|
+ tam_file = tam_required_file(repository)
|
|
|
|
+ open(tam_file, 'w').close()
|
|
|
|
+ print('Updated security database')
|
|
|
|
+ elif args.disable_tam:
|
|
|
|
+ manifest, key = Manifest.load(repository, force_tam_not_required=True)
|
|
|
|
+ if tam_required(repository):
|
|
|
|
+ os.unlink(tam_required_file(repository))
|
|
|
|
+ if key.tam_required:
|
|
|
|
+ key.tam_required = False
|
|
|
|
+ key.change_passphrase(key._passphrase)
|
|
|
|
+ print('Key updated')
|
|
|
|
+ if hasattr(key, 'find_key'):
|
|
|
|
+ print('Key location:', key.find_key())
|
|
|
|
+ manifest.config[b'tam_required'] = False
|
|
|
|
+ manifest.write()
|
|
|
|
+ repository.commit()
|
|
|
|
+ else:
|
|
|
|
+ # mainly for upgrades from Attic repositories,
|
|
|
|
+ # but also supports borg 0.xx -> 1.0 upgrade.
|
|
|
|
|
|
- repo = AtticRepositoryUpgrader(args.location.path, create=False)
|
|
|
|
- try:
|
|
|
|
- repo.upgrade(args.dry_run, inplace=args.inplace, progress=args.progress)
|
|
|
|
- except NotImplementedError as e:
|
|
|
|
- print("warning: %s" % e)
|
|
|
|
- repo = BorgRepositoryUpgrader(args.location.path, create=False)
|
|
|
|
- try:
|
|
|
|
- repo.upgrade(args.dry_run, inplace=args.inplace, progress=args.progress)
|
|
|
|
- except NotImplementedError as e:
|
|
|
|
- print("warning: %s" % e)
|
|
|
|
|
|
+ repo = AtticRepositoryUpgrader(args.location.path, create=False)
|
|
|
|
+ try:
|
|
|
|
+ repo.upgrade(args.dry_run, inplace=args.inplace, progress=args.progress)
|
|
|
|
+ except NotImplementedError as e:
|
|
|
|
+ print("warning: %s" % e)
|
|
|
|
+ repo = BorgRepositoryUpgrader(args.location.path, create=False)
|
|
|
|
+ try:
|
|
|
|
+ repo.upgrade(args.dry_run, inplace=args.inplace, progress=args.progress)
|
|
|
|
+ except NotImplementedError as e:
|
|
|
|
+ print("warning: %s" % e)
|
|
return self.exit_code
|
|
return self.exit_code
|
|
|
|
|
|
def do_debug_info(self, args):
|
|
def do_debug_info(self, args):
|
|
@@ -1613,6 +1667,32 @@ class Archiver:
|
|
|
|
|
|
upgrade_epilog = textwrap.dedent("""
|
|
upgrade_epilog = textwrap.dedent("""
|
|
Upgrade an existing Borg repository.
|
|
Upgrade an existing Borg repository.
|
|
|
|
+
|
|
|
|
+ Borg 1.x.y upgrades
|
|
|
|
+ -------------------
|
|
|
|
+
|
|
|
|
+ Use ``borg upgrade --tam REPO`` to require manifest authentication
|
|
|
|
+ introduced with Borg 1.0.9 to address security issues. This means
|
|
|
|
+ that modifying the repository after doing this with a version prior
|
|
|
|
+ to 1.0.9 will raise a validation error, so only perform this upgrade
|
|
|
|
+ after updating all clients using the repository to 1.0.9 or newer.
|
|
|
|
+
|
|
|
|
+ This upgrade should be done on each client for safety reasons.
|
|
|
|
+
|
|
|
|
+ If a repository is accidentally modified with a pre-1.0.9 client after
|
|
|
|
+ this upgrade, use ``borg upgrade --tam --force REPO`` to remedy it.
|
|
|
|
+
|
|
|
|
+ If you routinely do this you might not want to enable this upgrade
|
|
|
|
+ (which will leave you exposed to the security issue). You can
|
|
|
|
+ reverse the upgrade by issuing ``borg upgrade --disable-tam REPO``.
|
|
|
|
+
|
|
|
|
+ See
|
|
|
|
+ https://borgbackup.readthedocs.io/en/stable/changes.html#pre-1-0-9-manifest-spoofing-vulnerability
|
|
|
|
+ for details.
|
|
|
|
+
|
|
|
|
+ Attic and Borg 0.xx to Borg 1.x
|
|
|
|
+ -------------------------------
|
|
|
|
+
|
|
This currently supports converting an Attic repository to Borg and also
|
|
This currently supports converting an Attic repository to Borg and also
|
|
helps with converting Borg 0.xx to 1.0.
|
|
helps with converting Borg 0.xx to 1.0.
|
|
|
|
|
|
@@ -1665,6 +1745,12 @@ class Archiver:
|
|
default=False, action='store_true',
|
|
default=False, action='store_true',
|
|
help="""rewrite repository in place, with no chance of going back to older
|
|
help="""rewrite repository in place, with no chance of going back to older
|
|
versions of the repository.""")
|
|
versions of the repository.""")
|
|
|
|
+ subparser.add_argument('--force', dest='force', action='store_true',
|
|
|
|
+ help="""Force upgrade""")
|
|
|
|
+ subparser.add_argument('--tam', dest='tam', action='store_true',
|
|
|
|
+ help="""Enable manifest authentication (in key and cache) (Borg 1.0.9 and later)""")
|
|
|
|
+ subparser.add_argument('--disable-tam', dest='disable_tam', action='store_true',
|
|
|
|
+ help="""Disable manifest authentication (in key and cache)""")
|
|
subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='',
|
|
subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='',
|
|
type=location_validator(archive=False),
|
|
type=location_validator(archive=False),
|
|
help='path to the repository to be upgraded')
|
|
help='path to the repository to be upgraded')
|