|  | @@ -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
 |