diff --git a/docs/changes.rst b/docs/changes.rst index a15f7fb03..e511716a4 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -133,15 +133,18 @@ Version 1.1.0b6 (unreleased) Compatibility notes: -- Repositories in a repokey mode (including "authenticated" mode) with a - blank passphrase are now treated as unencrypted repositories for security checks +- Repositories in the "repokey" and "repokey-blake2" modes with an empty passphrase + are now treated as unencrypted repositories for security checks (e.g. BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK). - Running "borg init" via a "borg serve --append-only" server will *not* create an append-only repository anymore. Use "borg init --append-only" to initialize an append-only repository. + Repositories in the "authenticated" mode are now treated as the unencrypted repositories + they are. + Previously there would be no prompts nor messages if an unknown repository - in one of these modes with a blank passphrase was encountered. This would + in one of these modes with an empty passphrase was encountered. This would allow an attacker to swap a repository, if one assumed that the lack of password prompts was due to a set BORG_PASSPHRASE. diff --git a/src/borg/cache.py b/src/borg/cache.py index aeb9d3d4d..40ed925a6 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -130,7 +130,7 @@ class SecurityManager: self.save(manifest, key, cache) def assert_access_unknown(self, warn_if_unencrypted, key): - if warn_if_unencrypted and not key.passphrase_protected and not self.known(): + if warn_if_unencrypted and not key.logically_encrypted and not self.known(): msg = ("Warning: Attempting to access a previously unknown unencrypted repository!\n" + "Do you want to continue? [yN] ") if not yes(msg, false_msg="Aborting.", invalid_msg="Invalid answer, aborting.", diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index 772b4ae5d..24bb81038 100644 --- a/src/borg/crypto/key.py +++ b/src/borg/crypto/key.py @@ -129,15 +129,31 @@ def tam_required(repository): class KeyBase: + # Numeric key type ID, must fit in one byte. TYPE = None # override in subclasses # Human-readable name NAME = 'UNDEFINED' + # Name used in command line / API (e.g. borg init --encryption=...) ARG_NAME = 'UNDEFINED' + # Storage type (no key blob storage / keyfile / repo) STORAGE = KeyBlobStorage.NO_STORAGE + # Seed for the buzhash chunker (borg.algorithms.chunker.Chunker) + # type: int + chunk_seed = None + + # Whether this *particular instance* is encrypted from a practical point of view, + # i.e. when it's using encryption with a empty passphrase, then + # that may be *technically* called encryption, but for all intents and purposes + # that's as good as not encrypting in the first place, and this member should be False. + # + # The empty passphrase is also special because Borg tries it first when no passphrase + # was supplied, and if an empty passphrase works, then Borg won't ask for one. + logically_encrypted = False + def __init__(self, repository): self.TYPE_STR = bytes([self.TYPE]) self.repository = repository @@ -234,7 +250,7 @@ class PlaintextKey(KeyBase): STORAGE = KeyBlobStorage.NO_STORAGE chunk_seed = 0 - passphrase_protected = False + logically_encrypted = False def __init__(self, repository): super().__init__(repository) @@ -314,7 +330,8 @@ class ID_HMAC_SHA_256: class AESKeyBase(KeyBase): - """Common base class shared by KeyfileKey and PassphraseKey + """ + Common base class shared by KeyfileKey and PassphraseKey Chunks are encrypted using 256bit AES in Counter Mode (CTR) @@ -330,7 +347,7 @@ class AESKeyBase(KeyBase): MAC = hmac_sha256 - passphrase_protected = True + logically_encrypted = True def encrypt(self, chunk): data = self.compressor.compress(chunk) @@ -705,7 +722,7 @@ class RepoKey(ID_HMAC_SHA_256, KeyfileKeyBase): 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 != '' + self.logically_encrypted = passphrase != '' # what we get in target is just a repo location, but we already have the repo obj: target = self.repository @@ -717,7 +734,7 @@ class RepoKey(ID_HMAC_SHA_256, KeyfileKeyBase): return success def save(self, target, passphrase): - self.passphrase_protected = passphrase != '' + self.logically_encrypted = passphrase != '' key_data = self._save(passphrase) key_data = key_data.encode('utf-8') # remote repo: msgpack issue #99, giving bytes target.save_key(key_data) @@ -750,16 +767,16 @@ class AuthenticatedKey(ID_BLAKE2b_256, RepoKey): STORAGE = KeyBlobStorage.REPO # It's only authenticated, not encrypted. - passphrase_protected = False + logically_encrypted = False def load(self, target, passphrase): success = super().load(target, passphrase) - self.passphrase_protected = False + self.logically_encrypted = False return success def save(self, target, passphrase): super().save(target, passphrase) - self.passphrase_protected = False + self.logically_encrypted = False def encrypt(self, chunk): data = self.compressor.compress(chunk)