|
@@ -45,7 +45,7 @@ from .helpers import signal_handler, raising_signal_handler, SigHup, SigTerm
|
|
|
from .helpers import ErrorIgnoringTextIOWrapper
|
|
|
from .helpers import ProgressIndicatorPercent
|
|
|
from .item import Item
|
|
|
-from .key import key_creator, RepoKey, PassphraseKey
|
|
|
+from .key import key_creator, tam_required_file, tam_required, RepoKey, PassphraseKey
|
|
|
from .keymanager import KeyManager
|
|
|
from .platform import get_flags, umount
|
|
|
from .remote import RepositoryServer, RemoteRepository, cache_if_remote
|
|
@@ -61,10 +61,12 @@ def argument(args, str_or_bool):
|
|
|
"""If bool is passed, return it. If str is passed, retrieve named attribute from args."""
|
|
|
if isinstance(str_or_bool, str):
|
|
|
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
|
|
|
|
|
|
|
|
|
-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, …)
|
|
|
|
|
@@ -81,7 +83,7 @@ def with_repository(fake=False, create=False, lock=True, exclusive=False, manife
|
|
|
def wrapper(self, args, **kwargs):
|
|
|
location = args.location # note: 'location' must be always present in args
|
|
|
append_only = getattr(args, 'append_only', False)
|
|
|
- if argument(args, fake):
|
|
|
+ if argument(args, fake) ^ invert_fake:
|
|
|
return method(self, args, repository=None, **kwargs)
|
|
|
elif location.proto == 'ssh':
|
|
|
repository = RemoteRepository(location, create=create, exclusive=argument(args, exclusive),
|
|
@@ -182,7 +184,8 @@ class Archiver:
|
|
|
@with_repository(create=True, exclusive=True, manifest=False)
|
|
|
def do_init(self, args, 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)
|
|
|
try:
|
|
|
key = key_creator(repository, args)
|
|
|
except (EOFError, KeyboardInterrupt):
|
|
@@ -194,6 +197,19 @@ class Archiver:
|
|
|
repository.commit()
|
|
|
with Cache(repository, key, manifest, warn_if_unencrypted=False):
|
|
|
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
|
|
|
|
|
|
@with_repository(exclusive=True, manifest=False)
|
|
@@ -224,6 +240,7 @@ class Archiver:
|
|
|
def do_change_passphrase(self, args, repository, manifest, key):
|
|
|
"""Change repository key file passphrase"""
|
|
|
key.change_passphrase()
|
|
|
+ logger.info('Key updated')
|
|
|
return EXIT_SUCCESS
|
|
|
|
|
|
@with_repository(lock=False, exclusive=False, manifest=False, cache=False)
|
|
@@ -272,6 +289,7 @@ class Archiver:
|
|
|
key_new.id_key = key_old.id_key
|
|
|
key_new.chunk_seed = key_old.chunk_seed
|
|
|
key_new.change_passphrase() # option to change key protection passphrase, save
|
|
|
+ logger.info('Key updated')
|
|
|
return EXIT_SUCCESS
|
|
|
|
|
|
@with_repository(fake='dry_run', exclusive=True)
|
|
@@ -1046,21 +1064,57 @@ class Archiver:
|
|
|
DASHES, logger=logging.getLogger('borg.output.stats'))
|
|
|
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"""
|
|
|
- # 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)
|
|
|
|
|
|
- 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)
|
|
|
+ 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.archives.list(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)
|
|
|
return self.exit_code
|
|
|
|
|
|
@with_repository(cache=True, exclusive=True)
|
|
@@ -1735,7 +1789,7 @@ class Archiver:
|
|
|
help='manage repository key')
|
|
|
|
|
|
key_parsers = subparser.add_subparsers(title='required arguments', metavar='<command>')
|
|
|
- subparser.set_defaults(func=functools.partial(self.do_subcommand_help, subparser))
|
|
|
+ subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser))
|
|
|
|
|
|
key_export_epilog = textwrap.dedent("""
|
|
|
If repository encryption is used, the repository is inaccessible
|
|
@@ -2303,6 +2357,32 @@ class Archiver:
|
|
|
|
|
|
upgrade_epilog = textwrap.dedent("""
|
|
|
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
|
|
|
helps with converting Borg 0.xx to 1.0.
|
|
|
|
|
@@ -2355,6 +2435,12 @@ class Archiver:
|
|
|
default=False, action='store_true',
|
|
|
help="""rewrite repository in place, with no chance of going back to older
|
|
|
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='',
|
|
|
type=location_validator(archive=False),
|
|
|
help='path to the repository to be upgraded')
|
|
@@ -2525,7 +2611,7 @@ class Archiver:
|
|
|
help='debugging command (not intended for normal use)')
|
|
|
|
|
|
debug_parsers = subparser.add_subparsers(title='required arguments', metavar='<command>')
|
|
|
- subparser.set_defaults(func=functools.partial(self.do_subcommand_help, subparser))
|
|
|
+ subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser))
|
|
|
|
|
|
debug_info_epilog = textwrap.dedent("""
|
|
|
This command displays some system information that might be useful for bug
|
|
@@ -2698,7 +2784,9 @@ class Archiver:
|
|
|
def run(self, args):
|
|
|
os.umask(args.umask) # early, before opening files
|
|
|
self.lock_wait = args.lock_wait
|
|
|
- setup_logging(level=args.log_level, is_serve=args.func == self.do_serve) # do not use loggers before this!
|
|
|
+ # This works around http://bugs.python.org/issue9351
|
|
|
+ func = getattr(args, 'func', None) or getattr(args, 'fallback_func')
|
|
|
+ setup_logging(level=args.log_level, is_serve=func == self.do_serve) # do not use loggers before this!
|
|
|
self._setup_implied_logging(vars(args))
|
|
|
self._setup_topic_debugging(args)
|
|
|
if args.show_version:
|
|
@@ -2706,7 +2794,7 @@ class Archiver:
|
|
|
self.prerun_checks(logger)
|
|
|
if is_slow_msgpack():
|
|
|
logger.warning("Using a pure-python msgpack! This will result in lower performance.")
|
|
|
- return args.func(args)
|
|
|
+ return func(args)
|
|
|
|
|
|
|
|
|
def sig_info_handler(sig_no, stack): # pragma: no cover
|