Repository: borgbackup/borg

Issue:
empty repokey == no repokey
master branch / borg 1.3: #6415 needs to be able to remove a repokey.

as there is no rpc api for that (we only have load and save key), it is done by saving an empty key.

problem: an empty key does not raise `configparser.NoOptionError` any more which is expected clientside by `find_key` for the case when there is no repokey.

as long as the manifest type byte is in sync with the actual key location, this is no problem because for the repokey type byte we WILL have a repokey there, but still a bit unpretty and bad for inconsistent cases.


Discussion:
maybe `Repository.load_key` should just return an empty key and not raise `configparser.NoOptionError`.

we could use `keydata = self.config.get('repository', 'key', fallback='').strip()`.

callers should check whether they got something non-empty.
TODO: if the repo is remote, does that `configparser.NoOptionError` get through to the client side correctly?
This happens if there is no `key` entry in the config of a remote repo:
```
Traceback (most recent call last):

  File "/usr/lib/python3.10/configparser.py", line 790, in get
    value = d[option]

  File "/usr/lib/python3.10/collections/__init__.py", line 986, in __getitem__
    return self.__missing__(key)            # support subclasses that define __missing__

  File "/usr/lib/python3.10/collections/__init__.py", line 978, in __missing__
    raise KeyError(key)

KeyError: 'key'


During handling of the above exception, another exception occurred:


Traceback (most recent call last):

  File "/usr/lib/python3/dist-packages/borg/remote.py", line 240, in serve
    res = f(**args)

  File "/usr/lib/python3/dist-packages/borg/repository.py", line 341, in load_key
    keydata = self.config.get('repository', 'key')

  File "/usr/lib/python3.10/configparser.py", line 793, in get
    raise NoOptionError(option, section)

configparser.NoOptionError: No option 'key' in section: 'repository'

Borg server: Platform: Linux backup 5.15.0-25-generic #25-Ubuntu SMP Wed Mar 30 15:54:22 UTC 2022 x86_64
Borg server: Linux: Unknown Linux  
Borg server: Borg: 1.2.0  Python: CPython 3.10.4 msgpack: 1.0.3 fuse: llfuse 1.3.8 [pyfuse3,llfuse]
Borg server: PID: 4220  CWD: /home/borg
Borg server: sys.argv: ['/usr/bin/borg', 'serve', '--restrict-to-repository', '/mnt/backup/server.borg']
Borg server: SSH_ORIGINAL_COMMAND: 'borg serve --umask=077'
Platform: Linux server 5.10.0-13-amd64 #1 SMP Debian 5.10.106-1 (2022-03-17) x86_64
Linux: Unknown Linux  
Borg: 1.1.16  Python: CPython 3.9.2 msgpack: 0.5.6.+borg1
PID: 19817  CWD: /usr/local/bin
sys.argv: ['/usr/bin/borg', 'list']
SSH_ORIGINAL_COMMAND: None
```

This happens if there is an empty `key` option in a remote repo config:
```
Local Exception
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/borg/archiver.py", line 4690, in main
    exit_code = archiver.run(args)
  File "/usr/lib/python3/dist-packages/borg/archiver.py", line 4622, in run
    return set_ec(func(args))
  File "/usr/lib/python3/dist-packages/borg/archiver.py", line 164, in wrapper
    kwargs['manifest'], kwargs['key'] = Manifest.load(repository, compatibility)
  File "/usr/lib/python3/dist-packages/borg/helpers.py", line 382, in load
    key = key_factory(repository, cdata)
  File "/usr/lib/python3/dist-packages/borg/crypto/key.py", line 134, in key_factory
    return identify_key(manifest_data).detect(repository, manifest_data)
  File "/usr/lib/python3/dist-packages/borg/crypto/key.py", line 605, in detect
    if not key.load(target, passphrase):
  File "/usr/lib/python3/dist-packages/borg/crypto/key.py", line 791, in load
    success = self._load(key_data, passphrase)
  File "/usr/lib/python3/dist-packages/borg/crypto/key.py", line 620, in _load
    data = self.decrypt_key_file(cdata, passphrase)
  File "/usr/lib/python3/dist-packages/borg/crypto/key.py", line 638, in decrypt_key_file
    data = unpacker.unpack()
  File "src/borg/algorithms/msgpack/_unpacker.pyx", line 485, in borg.algorithms.msgpack._unpacker.Unpacker.unpack
  File "src/borg/algorithms/msgpack/_unpacker.pyx", line 442, in borg.algorithms.msgpack._unpacker.Unpacker._unpack
borg.algorithms.msgpack.exceptions.OutOfData: No more data to unpack.

Platform: Linux server 5.10.0-13-amd64 #1 SMP Debian 5.10.106-1 (2022-03-17) x86_64
Linux: Unknown Linux  
Borg: 1.1.16  Python: CPython 3.9.2 msgpack: 0.5.6.+borg1
PID: 19831  CWD: /usr/local/bin
sys.argv: ['/usr/bin/borg', 'list']
SSH_ORIGINAL_COMMAND: None
```

Resolving patch:
diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py
--- a/src/borg/crypto/key.py
+++ b/src/borg/crypto/key.py
@@ -1,4 +1,3 @@
-import configparser
 import getpass
 import os
 import shlex
@@ -775,11 +774,11 @@ class RepoKey(ID_HMAC_SHA_256, KeyfileKeyBase):
 
     def find_key(self):
         loc = self.repository._location.canonical_path()
-        try:
-            self.repository.load_key()
-            return loc
-        except configparser.NoOptionError:
+        key = self.repository.load_key()
+        if not key:
+            # if we got an empty key, it means there is no key.
             raise RepoKeyNotFoundError(loc) from None
+        return loc
 
     def get_new_target(self, args):
         return self.repository
@@ -792,6 +791,10 @@ def load(self, target, passphrase):
         # what we get in target is just a repo location, but we already have the repo obj:
         target = self.repository
         key_data = target.load_key()
+        if not key_data:
+            # if we got an empty key, it means there is no key.
+            loc = target._location.canonical_path()
+            raise RepoKeyNotFoundError(loc) from None
         key_data = key_data.decode('utf-8')  # remote repo: msgpack issue #99, getting bytes
         success = self._load(key_data, passphrase)
         if success:
diff --git a/src/borg/crypto/keymanager.py b/src/borg/crypto/keymanager.py
--- a/src/borg/crypto/keymanager.py
+++ b/src/borg/crypto/keymanager.py
@@ -7,7 +7,7 @@
 from ..helpers import Manifest, NoManifestError, Error, yes, bin_to_hex, dash_open
 from ..repository import Repository
 
-from .key import KeyfileKey, KeyfileNotFoundError, KeyBlobStorage, identify_key
+from .key import KeyfileKey, KeyfileNotFoundError, RepoKeyNotFoundError, KeyBlobStorage, identify_key
 
 
 class UnencryptedRepo(Error):
@@ -56,7 +56,12 @@ def load_keyblob(self):
                 self.keyblob = ''.join(fd.readlines()[1:])
 
         elif self.keyblob_storage == KeyBlobStorage.REPO:
-            self.keyblob = self.repository.load_key().decode()
+            key_data = self.repository.load_key().decode()
+            if not key_data:
+                # if we got an empty key, it means there is no key.
+                loc = self.repository._location.canonical_path()
+                raise RepoKeyNotFoundError(loc) from None
+            self.keyblob = key_data
 
     def store_keyblob(self, args):
         if self.keyblob_storage == KeyBlobStorage.KEYFILE:
diff --git a/src/borg/repository.py b/src/borg/repository.py
--- a/src/borg/repository.py
+++ b/src/borg/repository.py
@@ -337,7 +337,8 @@ def save_key(self, keydata):
         self.save_config(self.path, self.config)
 
     def load_key(self):
-        keydata = self.config.get('repository', 'key')
+        keydata = self.config.get('repository', 'key', fallback='').strip()
+        # note: if we return an empty string, it means there is no repo key
         return keydata.encode('utf-8')  # remote repo: msgpack issue #99, returning bytes
 
     def get_free_nonce(self):


Test patch: