Selaa lähdekoodia

Merge pull request #2503 from enkore/f/authenticated-pp

key: authenticated mode = unencrypted
enkore 8 vuotta sitten
vanhempi
sitoutus
1dd53f0e03
4 muutettua tiedostoa jossa 70 lisäystä ja 8 poistoa
  1. 13 2
      docs/changes.rst
  2. 1 1
      src/borg/cache.py
  3. 45 5
      src/borg/crypto/key.py
  4. 11 0
      src/borg/testsuite/key.py

+ 13 - 2
docs/changes.rst

@@ -133,13 +133,24 @@ Version 1.1.0b6 (unreleased)
 
 
 Compatibility notes:
 Compatibility notes:
 
 
-- Repositories in a repokey 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).
   (e.g. BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK).
 - Running "borg init" via a "borg serve --append-only" server will *not* create
 - 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 anymore. Use "borg init --append-only" to initialize
   an append-only repository.
   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 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.
+
+  Since the "trick" does not work if BORG_PASSPHRASE is set, this does generally
+  not affect scripts.
+
 Version 1.1.0b5 (2017-04-30)
 Version 1.1.0b5 (2017-04-30)
 ----------------------------
 ----------------------------
 
 

+ 1 - 1
src/borg/cache.py

@@ -130,7 +130,7 @@ class SecurityManager:
             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 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" +
             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.",

+ 45 - 5
src/borg/crypto/key.py

@@ -129,15 +129,31 @@ def tam_required(repository):
 
 
 
 
 class KeyBase:
 class KeyBase:
+    # Numeric key type ID, must fit in one byte.
     TYPE = None  # override in subclasses
     TYPE = None  # override in subclasses
 
 
     # Human-readable name
     # Human-readable name
     NAME = 'UNDEFINED'
     NAME = 'UNDEFINED'
+
     # Name used in command line / API (e.g. borg init --encryption=...)
     # Name used in command line / API (e.g. borg init --encryption=...)
     ARG_NAME = 'UNDEFINED'
     ARG_NAME = 'UNDEFINED'
+
     # Storage type (no key blob storage / keyfile / repo)
     # Storage type (no key blob storage / keyfile / repo)
     STORAGE = KeyBlobStorage.NO_STORAGE
     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):
     def __init__(self, repository):
         self.TYPE_STR = bytes([self.TYPE])
         self.TYPE_STR = bytes([self.TYPE])
         self.repository = repository
         self.repository = repository
@@ -234,7 +250,7 @@ class PlaintextKey(KeyBase):
     STORAGE = KeyBlobStorage.NO_STORAGE
     STORAGE = KeyBlobStorage.NO_STORAGE
 
 
     chunk_seed = 0
     chunk_seed = 0
-    passphrase_protected = False
+    logically_encrypted = False
 
 
     def __init__(self, repository):
     def __init__(self, repository):
         super().__init__(repository)
         super().__init__(repository)
@@ -314,7 +330,8 @@ class ID_HMAC_SHA_256:
 
 
 
 
 class AESKeyBase(KeyBase):
 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)
     Chunks are encrypted using 256bit AES in Counter Mode (CTR)
 
 
@@ -330,7 +347,7 @@ class AESKeyBase(KeyBase):
 
 
     MAC = hmac_sha256
     MAC = hmac_sha256
 
 
-    passphrase_protected = True
+    logically_encrypted = True
 
 
     def encrypt(self, chunk):
     def encrypt(self, chunk):
         data = self.compressor.compress(chunk)
         data = self.compressor.compress(chunk)
@@ -705,7 +722,7 @@ class RepoKey(ID_HMAC_SHA_256, KeyfileKeyBase):
     def load(self, target, passphrase):
     def load(self, target, passphrase):
         # While the repository is encrypted, we consider a repokey repository with a blank
         # While the repository is encrypted, we consider a repokey repository with a blank
         # passphrase an unencrypted repository.
         # 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:
         # what we get in target is just a repo location, but we already have the repo obj:
         target = self.repository
         target = self.repository
@@ -717,7 +734,7 @@ class RepoKey(ID_HMAC_SHA_256, KeyfileKeyBase):
         return success
         return success
 
 
     def save(self, target, passphrase):
     def save(self, target, passphrase):
-        self.passphrase_protected = passphrase != ''
+        self.logically_encrypted = 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)
@@ -749,6 +766,29 @@ class AuthenticatedKey(ID_BLAKE2b_256, RepoKey):
     ARG_NAME = 'authenticated'
     ARG_NAME = 'authenticated'
     STORAGE = KeyBlobStorage.REPO
     STORAGE = KeyBlobStorage.REPO
 
 
+    # It's only authenticated, not encrypted.
+    logically_encrypted = False
+
+    def load(self, target, passphrase):
+        success = super().load(target, passphrase)
+        self.logically_encrypted = False
+        return success
+
+    def save(self, target, passphrase):
+        super().save(target, passphrase)
+        self.logically_encrypted = False
+
+    def extract_nonce(self, payload):
+        # This is called during set-up of the AES ciphers we're not actually using for this
+        # key. Therefore the return value of this method doesn't matter; it's just around
+        # to not have it crash should key identification be run against a very small chunk
+        # by "borg check" when the manifest is lost. (The manifest is always large enough
+        # to have the original method read some garbage from bytes 33-41). (Also, the return
+        # value must be larger than the 41 byte bloat of the original format).
+        if payload[0] != self.TYPE:
+            raise IntegrityError('Manifest: Invalid encryption envelope')
+        return 42
+
     def encrypt(self, chunk):
     def encrypt(self, chunk):
         data = self.compressor.compress(chunk)
         data = self.compressor.compress(chunk)
         return b''.join([self.TYPE_STR, data])
         return b''.join([self.TYPE_STR, data])

+ 11 - 0
src/borg/testsuite/key.py

@@ -11,6 +11,7 @@ from ..crypto.key import Passphrase, PasswordRetriesExceeded, bin_to_hex
 from ..crypto.key import PlaintextKey, PassphraseKey, KeyfileKey, RepoKey, Blake2KeyfileKey, Blake2RepoKey, \
 from ..crypto.key import PlaintextKey, PassphraseKey, KeyfileKey, RepoKey, Blake2KeyfileKey, Blake2RepoKey, \
     AuthenticatedKey
     AuthenticatedKey
 from ..crypto.key import TAMRequiredError, TAMInvalid, TAMUnsupportedSuiteError, UnsupportedManifestError
 from ..crypto.key import TAMRequiredError, TAMInvalid, TAMUnsupportedSuiteError, UnsupportedManifestError
+from ..crypto.key import identify_key
 from ..crypto.low_level import bytes_to_long, num_aes_blocks
 from ..crypto.low_level import bytes_to_long, num_aes_blocks
 from ..helpers import IntegrityError
 from ..helpers import IntegrityError
 from ..helpers import Location
 from ..helpers import Location
@@ -224,6 +225,16 @@ class TestKey:
             id[12] = 0
             id[12] = 0
             key.decrypt(id, data)
             key.decrypt(id, data)
 
 
+    def test_roundtrip(self, key):
+        repository = key.repository
+        plaintext = b'foo'
+        encrypted = key.encrypt(plaintext)
+        identified_key_class = identify_key(encrypted)
+        assert identified_key_class == key.__class__
+        loaded_key = identified_key_class.detect(repository, encrypted)
+        decrypted = loaded_key.decrypt(None, encrypted)
+        assert decrypted == plaintext
+
     def test_decrypt_decompress(self, key):
     def test_decrypt_decompress(self, key):
         plaintext = b'123456789'
         plaintext = b'123456789'
         encrypted = key.encrypt(plaintext)
         encrypted = key.encrypt(plaintext)