mirror of
https://github.com/borgbackup/borg.git
synced 2024-12-26 17:57:59 +00:00
Merge pull request #2489 from enkore/issue/2169
consider repokey w/o passphrase == unencrypted
This commit is contained in:
commit
c805adc267
4 changed files with 56 additions and 10 deletions
|
@ -128,6 +128,15 @@ The best check that everything is ok is to run a dry-run extraction::
|
||||||
Changelog
|
Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
Version 1.1.0b6 (unreleased)
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Compatibility notes:
|
||||||
|
|
||||||
|
- Repositories in a repokey mode with a blank passphrase are now treated
|
||||||
|
as unencrypted repositories for security checks
|
||||||
|
(e.g. BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK).
|
||||||
|
|
||||||
Version 1.1.0b5 (2017-04-30)
|
Version 1.1.0b5 (2017-04-30)
|
||||||
----------------------------
|
----------------------------
|
||||||
|
|
||||||
|
|
|
@ -130,7 +130,7 @@ def assert_secure(self, manifest, key, cache):
|
||||||
self.save(manifest, key, cache)
|
self.save(manifest, key, cache)
|
||||||
|
|
||||||
def assert_access_unknown(self, warn_if_unencrypted, key):
|
def assert_access_unknown(self, warn_if_unencrypted, key):
|
||||||
if warn_if_unencrypted and isinstance(key, PlaintextKey) and not self.known():
|
if warn_if_unencrypted and not key.passphrase_protected and not self.known():
|
||||||
msg = ("Warning: Attempting to access a previously unknown unencrypted repository!\n" +
|
msg = ("Warning: Attempting to access a previously unknown unencrypted repository!\n" +
|
||||||
"Do you want to continue? [yN] ")
|
"Do you want to continue? [yN] ")
|
||||||
if not yes(msg, false_msg="Aborting.", invalid_msg="Invalid answer, aborting.",
|
if not yes(msg, false_msg="Aborting.", invalid_msg="Invalid answer, aborting.",
|
||||||
|
|
|
@ -234,6 +234,7 @@ class PlaintextKey(KeyBase):
|
||||||
STORAGE = KeyBlobStorage.NO_STORAGE
|
STORAGE = KeyBlobStorage.NO_STORAGE
|
||||||
|
|
||||||
chunk_seed = 0
|
chunk_seed = 0
|
||||||
|
passphrase_protected = False
|
||||||
|
|
||||||
def __init__(self, repository):
|
def __init__(self, repository):
|
||||||
super().__init__(repository)
|
super().__init__(repository)
|
||||||
|
@ -329,6 +330,8 @@ class AESKeyBase(KeyBase):
|
||||||
|
|
||||||
MAC = hmac_sha256
|
MAC = hmac_sha256
|
||||||
|
|
||||||
|
passphrase_protected = True
|
||||||
|
|
||||||
def encrypt(self, chunk):
|
def encrypt(self, chunk):
|
||||||
data = self.compressor.compress(chunk)
|
data = self.compressor.compress(chunk)
|
||||||
self.nonce_manager.ensure_reservation(num_aes_blocks(len(data)))
|
self.nonce_manager.ensure_reservation(num_aes_blocks(len(data)))
|
||||||
|
@ -700,6 +703,10 @@ def get_new_target(self, args):
|
||||||
return self.repository
|
return self.repository
|
||||||
|
|
||||||
def load(self, target, passphrase):
|
def load(self, target, passphrase):
|
||||||
|
# While the repository is encrypted, we consider a repokey repository with a blank
|
||||||
|
# passphrase an unencrypted repository.
|
||||||
|
self.passphrase_protected = passphrase != ''
|
||||||
|
|
||||||
# what we get in target is just a repo location, but we already have the repo obj:
|
# what we get in target is just a repo location, but we already have the repo obj:
|
||||||
target = self.repository
|
target = self.repository
|
||||||
key_data = target.load_key()
|
key_data = target.load_key()
|
||||||
|
@ -710,6 +717,7 @@ def load(self, target, passphrase):
|
||||||
return success
|
return success
|
||||||
|
|
||||||
def save(self, target, passphrase):
|
def save(self, target, passphrase):
|
||||||
|
self.passphrase_protected = passphrase != ''
|
||||||
key_data = self._save(passphrase)
|
key_data = self._save(passphrase)
|
||||||
key_data = key_data.encode('utf-8') # remote repo: msgpack issue #99, giving bytes
|
key_data = key_data.encode('utf-8') # remote repo: msgpack issue #99, giving bytes
|
||||||
target.save_key(key_data)
|
target.save_key(key_data)
|
||||||
|
|
|
@ -560,7 +560,8 @@ def test_repository_swap_detection(self):
|
||||||
if self.FORK_DEFAULT:
|
if self.FORK_DEFAULT:
|
||||||
self.cmd('create', self.repository_location + '::test.2', 'input', exit_code=EXIT_ERROR)
|
self.cmd('create', self.repository_location + '::test.2', 'input', exit_code=EXIT_ERROR)
|
||||||
else:
|
else:
|
||||||
self.assert_raises(Cache.EncryptionMethodMismatch, lambda: self.cmd('create', self.repository_location + '::test.2', 'input'))
|
with pytest.raises(Cache.EncryptionMethodMismatch):
|
||||||
|
self.cmd('create', self.repository_location + '::test.2', 'input')
|
||||||
|
|
||||||
def test_repository_swap_detection2(self):
|
def test_repository_swap_detection2(self):
|
||||||
self.create_test_files()
|
self.create_test_files()
|
||||||
|
@ -573,7 +574,8 @@ def test_repository_swap_detection2(self):
|
||||||
if self.FORK_DEFAULT:
|
if self.FORK_DEFAULT:
|
||||||
self.cmd('create', self.repository_location + '_encrypted::test.2', 'input', exit_code=EXIT_ERROR)
|
self.cmd('create', self.repository_location + '_encrypted::test.2', 'input', exit_code=EXIT_ERROR)
|
||||||
else:
|
else:
|
||||||
self.assert_raises(Cache.RepositoryAccessAborted, lambda: self.cmd('create', self.repository_location + '_encrypted::test.2', 'input'))
|
with pytest.raises(Cache.RepositoryAccessAborted):
|
||||||
|
self.cmd('create', self.repository_location + '_encrypted::test.2', 'input')
|
||||||
|
|
||||||
def test_repository_swap_detection_no_cache(self):
|
def test_repository_swap_detection_no_cache(self):
|
||||||
self.create_test_files()
|
self.create_test_files()
|
||||||
|
@ -589,7 +591,8 @@ def test_repository_swap_detection_no_cache(self):
|
||||||
if self.FORK_DEFAULT:
|
if self.FORK_DEFAULT:
|
||||||
self.cmd('create', self.repository_location + '::test.2', 'input', exit_code=EXIT_ERROR)
|
self.cmd('create', self.repository_location + '::test.2', 'input', exit_code=EXIT_ERROR)
|
||||||
else:
|
else:
|
||||||
self.assert_raises(Cache.EncryptionMethodMismatch, lambda: self.cmd('create', self.repository_location + '::test.2', 'input'))
|
with pytest.raises(Cache.EncryptionMethodMismatch):
|
||||||
|
self.cmd('create', self.repository_location + '::test.2', 'input')
|
||||||
|
|
||||||
def test_repository_swap_detection2_no_cache(self):
|
def test_repository_swap_detection2_no_cache(self):
|
||||||
self.create_test_files()
|
self.create_test_files()
|
||||||
|
@ -607,6 +610,30 @@ def test_repository_swap_detection2_no_cache(self):
|
||||||
with pytest.raises(Cache.RepositoryAccessAborted):
|
with pytest.raises(Cache.RepositoryAccessAborted):
|
||||||
self.cmd('create', self.repository_location + '_encrypted::test.2', 'input')
|
self.cmd('create', self.repository_location + '_encrypted::test.2', 'input')
|
||||||
|
|
||||||
|
def test_repository_swap_detection_repokey_blank_passphrase(self):
|
||||||
|
# Check that a repokey repo with a blank passphrase is considered like a plaintext repo.
|
||||||
|
self.create_test_files()
|
||||||
|
# User initializes her repository with her passphrase
|
||||||
|
self.cmd('init', '--encryption=repokey', self.repository_location)
|
||||||
|
self.cmd('create', self.repository_location + '::test', 'input')
|
||||||
|
# Attacker replaces it with her own repository, which is encrypted but has no passphrase set
|
||||||
|
shutil.rmtree(self.repository_path)
|
||||||
|
with environment_variable(BORG_PASSPHRASE=''):
|
||||||
|
self.cmd('init', '--encryption=repokey', self.repository_location)
|
||||||
|
# Delete cache & security database, AKA switch to user perspective
|
||||||
|
self.cmd('delete', '--cache-only', self.repository_location)
|
||||||
|
repository_id = bin_to_hex(self._extract_repository_id(self.repository_path))
|
||||||
|
shutil.rmtree(get_security_dir(repository_id))
|
||||||
|
with environment_variable(BORG_PASSPHRASE=None):
|
||||||
|
# This is the part were the user would be tricked, e.g. she assumes that BORG_PASSPHRASE
|
||||||
|
# is set, while it isn't. Previously this raised no warning,
|
||||||
|
# since the repository is, technically, encrypted.
|
||||||
|
if self.FORK_DEFAULT:
|
||||||
|
self.cmd('create', self.repository_location + '::test.2', 'input', exit_code=EXIT_ERROR)
|
||||||
|
else:
|
||||||
|
with pytest.raises(Cache.CacheInitAbortedError):
|
||||||
|
self.cmd('create', self.repository_location + '::test.2', 'input')
|
||||||
|
|
||||||
def test_repository_move(self):
|
def test_repository_move(self):
|
||||||
self.cmd('init', '--encryption=repokey', self.repository_location)
|
self.cmd('init', '--encryption=repokey', self.repository_location)
|
||||||
repository_id = bin_to_hex(self._extract_repository_id(self.repository_path))
|
repository_id = bin_to_hex(self._extract_repository_id(self.repository_path))
|
||||||
|
@ -2254,7 +2281,8 @@ def test_key_import_errors(self):
|
||||||
if self.FORK_DEFAULT:
|
if self.FORK_DEFAULT:
|
||||||
self.cmd('key', 'import', self.repository_location, export_file, exit_code=2)
|
self.cmd('key', 'import', self.repository_location, export_file, exit_code=2)
|
||||||
else:
|
else:
|
||||||
self.assert_raises(NotABorgKeyFile, lambda: self.cmd('key', 'import', self.repository_location, export_file))
|
with pytest.raises(NotABorgKeyFile):
|
||||||
|
self.cmd('key', 'import', self.repository_location, export_file)
|
||||||
|
|
||||||
with open(export_file, 'w') as fd:
|
with open(export_file, 'w') as fd:
|
||||||
fd.write('BORG_KEY a0a0a0\n')
|
fd.write('BORG_KEY a0a0a0\n')
|
||||||
|
@ -2262,7 +2290,8 @@ def test_key_import_errors(self):
|
||||||
if self.FORK_DEFAULT:
|
if self.FORK_DEFAULT:
|
||||||
self.cmd('key', 'import', self.repository_location, export_file, exit_code=2)
|
self.cmd('key', 'import', self.repository_location, export_file, exit_code=2)
|
||||||
else:
|
else:
|
||||||
self.assert_raises(RepoIdMismatch, lambda: self.cmd('key', 'import', self.repository_location, export_file))
|
with pytest.raises(RepoIdMismatch):
|
||||||
|
self.cmd('key', 'import', self.repository_location, export_file)
|
||||||
|
|
||||||
def test_key_export_paperkey(self):
|
def test_key_export_paperkey(self):
|
||||||
repo_id = 'e294423506da4e1ea76e8dcdf1a3919624ae3ae496fddf905610c351d3f09239'
|
repo_id = 'e294423506da4e1ea76e8dcdf1a3919624ae3ae496fddf905610c351d3f09239'
|
||||||
|
@ -2675,13 +2704,13 @@ def test_remote_repo_restrict_to_path(self):
|
||||||
self.cmd('init', '--encryption=repokey', self.repository_location)
|
self.cmd('init', '--encryption=repokey', self.repository_location)
|
||||||
# restricted to repo directory itself, fail for other directories with same prefix:
|
# restricted to repo directory itself, fail for other directories with same prefix:
|
||||||
with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', self.repository_path]):
|
with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', self.repository_path]):
|
||||||
self.assert_raises(PathNotAllowed,
|
with pytest.raises(PathNotAllowed):
|
||||||
lambda: self.cmd('init', '--encryption=repokey', self.repository_location + '_0'))
|
self.cmd('init', '--encryption=repokey', self.repository_location + '_0')
|
||||||
|
|
||||||
# restricted to a completely different path:
|
# restricted to a completely different path:
|
||||||
with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo']):
|
with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo']):
|
||||||
self.assert_raises(PathNotAllowed,
|
with pytest.raises(PathNotAllowed):
|
||||||
lambda: self.cmd('init', '--encryption=repokey', self.repository_location + '_1'))
|
self.cmd('init', '--encryption=repokey', self.repository_location + '_1')
|
||||||
path_prefix = os.path.dirname(self.repository_path)
|
path_prefix = os.path.dirname(self.repository_path)
|
||||||
# restrict to repo directory's parent directory:
|
# restrict to repo directory's parent directory:
|
||||||
with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', path_prefix]):
|
with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', path_prefix]):
|
||||||
|
|
Loading…
Reference in a new issue