mirror of https://github.com/borgbackup/borg.git
Merge pull request #6188 from hexagonrecursion/nonce
docs: impact of deleting path/to/repo/nonce
This commit is contained in:
commit
4fa9f1faa4
26
docs/faq.rst
26
docs/faq.rst
|
@ -710,6 +710,32 @@ Send a private email to the :ref:`security contact <security-contact>`
|
||||||
if you think you have discovered a security issue.
|
if you think you have discovered a security issue.
|
||||||
Please disclose security issues responsibly.
|
Please disclose security issues responsibly.
|
||||||
|
|
||||||
|
How important are the nonce files?
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
Borg uses :ref:`AES-CTR encryption <borg_security_critique>`. An
|
||||||
|
essential part of AES-CTR is a sequential counter that must **never**
|
||||||
|
repeat. If the same value of the counter is used twice in the same repository,
|
||||||
|
an attacker can decrypt the data. The counter is stored in the home directory
|
||||||
|
of each user ($HOME/.config/borg/security/$REPO_ID/nonce) as well as
|
||||||
|
in the repository (/path/to/repo/nonce). When creating a new archive borg uses
|
||||||
|
the highest of the two values. The value of the counter in the repository may be
|
||||||
|
higher than your local value if another user has created an archive more recently
|
||||||
|
than you did.
|
||||||
|
|
||||||
|
Since the nonce is not necessary to read the data that is already encrypted,
|
||||||
|
``borg info``, ``borg list``, ``borg extract`` and ``borg mount`` should work
|
||||||
|
just fine without it.
|
||||||
|
|
||||||
|
If the the nonce file stored in the repo is lost, but you still have your local copy,
|
||||||
|
borg will recreate the repository nonce file the next time you run ``borg create``.
|
||||||
|
This should be safe for repositories that are only used from one user account
|
||||||
|
on one machine.
|
||||||
|
|
||||||
|
For repositories that are used by multiple users and/or from multiple machines
|
||||||
|
it is safest to avoid running *any* commands that modify the repository after
|
||||||
|
the nonce is deleted or if you suspect it may have been tampered with. See :ref:`attack_model`.
|
||||||
|
|
||||||
Common issues
|
Common issues
|
||||||
#############
|
#############
|
||||||
|
|
||||||
|
|
|
@ -384,6 +384,10 @@ class ArchiverTestCaseBase(BaseTestCase):
|
||||||
class ArchiverTestCase(ArchiverTestCaseBase):
|
class ArchiverTestCase(ArchiverTestCaseBase):
|
||||||
requires_hardlinks = pytest.mark.skipif(not are_hardlinks_supported(), reason='hardlinks not supported')
|
requires_hardlinks = pytest.mark.skipif(not are_hardlinks_supported(), reason='hardlinks not supported')
|
||||||
|
|
||||||
|
def get_security_dir(self):
|
||||||
|
repository_id = bin_to_hex(self._extract_repository_id(self.repository_path))
|
||||||
|
return get_security_dir(repository_id)
|
||||||
|
|
||||||
def test_basic_functionality(self):
|
def test_basic_functionality(self):
|
||||||
have_root = self.create_test_files()
|
have_root = self.create_test_files()
|
||||||
# fork required to test show-rc output
|
# fork required to test show-rc output
|
||||||
|
@ -718,8 +722,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
||||||
self.cmd('init', '--encryption=repokey', self.repository_location)
|
self.cmd('init', '--encryption=repokey', self.repository_location)
|
||||||
# Delete cache & security database, AKA switch to user perspective
|
# Delete cache & security database, AKA switch to user perspective
|
||||||
self.cmd('delete', '--cache-only', self.repository_location)
|
self.cmd('delete', '--cache-only', self.repository_location)
|
||||||
repository_id = bin_to_hex(self._extract_repository_id(self.repository_path))
|
shutil.rmtree(self.get_security_dir())
|
||||||
shutil.rmtree(get_security_dir(repository_id))
|
|
||||||
with environment_variable(BORG_PASSPHRASE=None):
|
with environment_variable(BORG_PASSPHRASE=None):
|
||||||
# This is the part were the user would be tricked, e.g. she assumes that BORG_PASSPHRASE
|
# 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,
|
# is set, while it isn't. Previously this raised no warning,
|
||||||
|
@ -732,11 +735,10 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
||||||
|
|
||||||
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))
|
security_dir = self.get_security_dir()
|
||||||
os.rename(self.repository_path, self.repository_path + '_new')
|
os.rename(self.repository_path, self.repository_path + '_new')
|
||||||
with environment_variable(BORG_RELOCATED_REPO_ACCESS_IS_OK='yes'):
|
with environment_variable(BORG_RELOCATED_REPO_ACCESS_IS_OK='yes'):
|
||||||
self.cmd('info', self.repository_location + '_new')
|
self.cmd('info', self.repository_location + '_new')
|
||||||
security_dir = get_security_dir(repository_id)
|
|
||||||
with open(os.path.join(security_dir, 'location')) as fd:
|
with open(os.path.join(security_dir, 'location')) as fd:
|
||||||
location = fd.read()
|
location = fd.read()
|
||||||
assert location == Location(self.repository_location + '_new').canonical_path()
|
assert location == Location(self.repository_location + '_new').canonical_path()
|
||||||
|
@ -751,9 +753,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
||||||
|
|
||||||
def test_security_dir_compat(self):
|
def test_security_dir_compat(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))
|
with open(os.path.join(self.get_security_dir(), 'location'), 'w') as fd:
|
||||||
security_dir = get_security_dir(repository_id)
|
|
||||||
with open(os.path.join(security_dir, 'location'), 'w') as fd:
|
|
||||||
fd.write('something outdated')
|
fd.write('something outdated')
|
||||||
# This is fine, because the cache still has the correct information. security_dir and cache can disagree
|
# This is fine, because the cache still has the correct information. security_dir and cache can disagree
|
||||||
# if older versions are used to confirm a renamed repository.
|
# if older versions are used to confirm a renamed repository.
|
||||||
|
@ -761,8 +761,6 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
||||||
|
|
||||||
def test_unknown_unencrypted(self):
|
def test_unknown_unencrypted(self):
|
||||||
self.cmd('init', '--encryption=none', self.repository_location)
|
self.cmd('init', '--encryption=none', self.repository_location)
|
||||||
repository_id = bin_to_hex(self._extract_repository_id(self.repository_path))
|
|
||||||
security_dir = get_security_dir(repository_id)
|
|
||||||
# Ok: repository is known
|
# Ok: repository is known
|
||||||
self.cmd('info', self.repository_location)
|
self.cmd('info', self.repository_location)
|
||||||
|
|
||||||
|
@ -772,7 +770,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
||||||
|
|
||||||
# Needs confirmation: cache and security dir both gone (eg. another host or rm -rf ~)
|
# Needs confirmation: cache and security dir both gone (eg. another host or rm -rf ~)
|
||||||
shutil.rmtree(self.cache_path)
|
shutil.rmtree(self.cache_path)
|
||||||
shutil.rmtree(security_dir)
|
shutil.rmtree(self.get_security_dir())
|
||||||
if self.FORK_DEFAULT:
|
if self.FORK_DEFAULT:
|
||||||
self.cmd('info', self.repository_location, exit_code=EXIT_ERROR)
|
self.cmd('info', self.repository_location, exit_code=EXIT_ERROR)
|
||||||
else:
|
else:
|
||||||
|
@ -3506,6 +3504,53 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02
|
||||||
self.assert_in('this-repository-does-not-exist', output)
|
self.assert_in('this-repository-does-not-exist', output)
|
||||||
self.assert_not_in('this-repository-does-not-exist::test', output)
|
self.assert_not_in('this-repository-does-not-exist::test', output)
|
||||||
|
|
||||||
|
def test_can_read_repo_even_if_nonce_is_deleted(self):
|
||||||
|
"""Nonce is only used for encrypting new data.
|
||||||
|
|
||||||
|
It should be possible to retrieve the data from an archive even if
|
||||||
|
both the client and the server forget the nonce"""
|
||||||
|
self.create_regular_file('file1', contents=b'Hello, borg')
|
||||||
|
self.cmd('init', '--encryption=repokey', self.repository_location)
|
||||||
|
self.cmd('create', self.repository_location + '::test', 'input')
|
||||||
|
# Oops! We have removed the repo-side memory of the nonce!
|
||||||
|
# See https://github.com/borgbackup/borg/issues/5858
|
||||||
|
os.remove(os.path.join(self.repository_path, 'nonce'))
|
||||||
|
# Oops! The client has lost the nonce too!
|
||||||
|
os.remove(os.path.join(self.get_security_dir(), 'nonce'))
|
||||||
|
|
||||||
|
# The repo should still be readable
|
||||||
|
repo_info = self.cmd('info', self.repository_location)
|
||||||
|
assert 'All archives:' in repo_info
|
||||||
|
repo_list = self.cmd('list', self.repository_location)
|
||||||
|
assert 'test' in repo_list
|
||||||
|
# The archive should still be readable
|
||||||
|
archive_info = self.cmd('info', self.repository_location + '::test')
|
||||||
|
assert 'Archive name: test\n' in archive_info
|
||||||
|
archive_list = self.cmd('list', self.repository_location + '::test')
|
||||||
|
assert 'file1' in archive_list
|
||||||
|
# Extracting the archive should work
|
||||||
|
with changedir('output'):
|
||||||
|
self.cmd('extract', self.repository_location + '::test')
|
||||||
|
self.assert_dirs_equal('input', 'output/input')
|
||||||
|
|
||||||
|
def test_recovery_from_deleted_repo_nonce(self):
|
||||||
|
"""We should be able to recover if path/to/repo/nonce is deleted.
|
||||||
|
|
||||||
|
The nonce is stored in two places: in the repo and in $HOME.
|
||||||
|
The nonce in the repo is only needed when multiple clients use the same
|
||||||
|
repo. Otherwise we can just use our own copy of the nonce.
|
||||||
|
"""
|
||||||
|
self.create_regular_file('file1', contents=b'Hello, borg')
|
||||||
|
self.cmd('init', '--encryption=repokey', self.repository_location)
|
||||||
|
self.cmd('create', self.repository_location + '::test', 'input')
|
||||||
|
# Oops! We have removed the repo-side memory of the nonce!
|
||||||
|
# See https://github.com/borgbackup/borg/issues/5858
|
||||||
|
nonce = os.path.join(self.repository_path, 'nonce')
|
||||||
|
os.remove(nonce)
|
||||||
|
|
||||||
|
self.cmd('create', self.repository_location + '::test2', 'input')
|
||||||
|
assert os.path.exists(nonce)
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available')
|
@unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available')
|
||||||
class ArchiverTestCaseBinary(ArchiverTestCase):
|
class ArchiverTestCaseBinary(ArchiverTestCase):
|
||||||
|
|
Loading…
Reference in New Issue