Skip to content

Commit

Permalink
Merge pull request #6261 from hexagonrecursion/bp-nonce
Browse files Browse the repository at this point in the history
Backport: Doc: impact of deleting path/to/repo/nonce
  • Loading branch information
ThomasWaldmann committed Feb 7, 2022
2 parents 377971e + f7039d5 commit 16219f3
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 10 deletions.
26 changes: 26 additions & 0 deletions docs/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,32 @@ Send a private email to the :ref:`security contact <security-contact>`
if you think you have discovered a security issue.
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
#############

Expand Down
65 changes: 55 additions & 10 deletions src/borg/testsuite/archiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,10 @@ def create_test_files(self):
class ArchiverTestCase(ArchiverTestCaseBase):
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):
have_root = self.create_test_files()
# fork required to test show-rc output
Expand Down Expand Up @@ -720,8 +724,7 @@ def test_repository_swap_detection_repokey_blank_passphrase(self):
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))
shutil.rmtree(self.get_security_dir())
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,
Expand All @@ -734,11 +737,10 @@ def test_repository_swap_detection_repokey_blank_passphrase(self):

def test_repository_move(self):
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')
with environment_variable(BORG_RELOCATED_REPO_ACCESS_IS_OK='yes'):
self.cmd('info', self.repository_location + '_new')
security_dir = get_security_dir(repository_id)
with open(os.path.join(security_dir, 'location')) as fd:
location = fd.read()
assert location == Location(self.repository_location + '_new').canonical_path()
Expand All @@ -753,18 +755,14 @@ def test_repository_move(self):

def test_security_dir_compat(self):
self.cmd('init', '--encryption=repokey', self.repository_location)
repository_id = bin_to_hex(self._extract_repository_id(self.repository_path))
security_dir = get_security_dir(repository_id)
with open(os.path.join(security_dir, 'location'), 'w') as fd:
with open(os.path.join(self.get_security_dir(), 'location'), 'w') as fd:
fd.write('something outdated')
# 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.
self.cmd('info', self.repository_location)

def test_unknown_unencrypted(self):
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
self.cmd('info', self.repository_location)

Expand All @@ -774,7 +772,7 @@ def test_unknown_unencrypted(self):

# Needs confirmation: cache and security dir both gone (eg. another host or rm -rf ~)
shutil.rmtree(self.cache_path)
shutil.rmtree(security_dir)
shutil.rmtree(self.get_security_dir())
if self.FORK_DEFAULT:
self.cmd('info', self.repository_location, exit_code=EXIT_ERROR)
else:
Expand Down Expand Up @@ -3168,6 +3166,53 @@ def patched_setxattr_EACCES(*args, **kwargs):
with patch.object(xattr, 'setxattr', patched_setxattr_EACCES):
self.cmd('extract', self.repository_location + '::test', exit_code=EXIT_WARNING)

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')
class ArchiverTestCaseBinary(ArchiverTestCase):
Expand Down

0 comments on commit 16219f3

Please sign in to comment.