Repository: borgbackup/borg

Issue:
Key export impossible for encryption types: keyfile-blake2 repokey-blake2 authenticated
```bash
#!/bin/bash
encryption="keyfile-blake2 repokey-blake2 authenticated"
for type in $encryption; do
	echo "########"
	echo $type
	rm -r test_repo
	borg init -e $type test_repo
	borg key export --paper test_repo/ dummy.key
	echo "########"
done
```

gives:

########
keyfile-blake2
Enter new passphrase: 
Enter same passphrase again: 
Do you want your passphrase to be displayed for verification? [yN]: n
[infomessage]
Keytype 4 is unknown.
########
########
repokey-blake2
Enter new passphrase: 
Enter same passphrase again: 
Do you want your passphrase to be displayed for verification? [yN]: 
[infomessage]
Keytype 5 is unknown.
########
########
authenticated
Enter new passphrase: 
Enter same passphrase again: 
Do you want your passphrase to be displayed for verification? [yN]: 
[infomessage]
Keytype 6 is unknown.
########



Discussion:
It seems that none of those keytypes is implemeted in [keymanger.py](src/borg/keymanger.py)

Resolving patch:
diff --git a/src/borg/key.py b/src/borg/key.py
--- a/src/borg/key.py
+++ b/src/borg/key.py
@@ -87,45 +87,38 @@ class TAMUnsupportedSuiteError(IntegrityError):
     traceback = False
 
 
+class KeyBlobStorage:
+    NO_STORAGE = 'no_storage'
+    KEYFILE = 'keyfile'
+    REPO = 'repository'
+
+
 def key_creator(repository, args):
-    if args.encryption == 'keyfile':
-        return KeyfileKey.create(repository, args)
-    elif args.encryption == 'repokey':
-        return RepoKey.create(repository, args)
-    elif args.encryption == 'keyfile-blake2':
-        return Blake2KeyfileKey.create(repository, args)
-    elif args.encryption == 'repokey-blake2':
-        return Blake2RepoKey.create(repository, args)
-    elif args.encryption == 'authenticated':
-        return AuthenticatedKey.create(repository, args)
-    elif args.encryption == 'none':
-        return PlaintextKey.create(repository, args)
+    for key in AVAILABLE_KEY_TYPES:
+        if key.ARG_NAME == args.encryption:
+            return key.create(repository, args)
     else:
         raise ValueError('Invalid encryption mode "%s"' % args.encryption)
 
 
-def key_factory(repository, manifest_data):
+def identify_key(manifest_data):
     key_type = manifest_data[0]
-    if key_type == KeyfileKey.TYPE:
-        return KeyfileKey.detect(repository, manifest_data)
-    elif key_type == RepoKey.TYPE:
-        return RepoKey.detect(repository, manifest_data)
-    elif key_type == PassphraseKey.TYPE:
+    if key_type == PassphraseKey.TYPE:
         # we just dispatch to repokey mode and assume the passphrase was migrated to a repokey.
         # see also comment in PassphraseKey class.
-        return RepoKey.detect(repository, manifest_data)
-    elif key_type == PlaintextKey.TYPE:
-        return PlaintextKey.detect(repository, manifest_data)
-    elif key_type == Blake2KeyfileKey.TYPE:
-        return Blake2KeyfileKey.detect(repository, manifest_data)
-    elif key_type == Blake2RepoKey.TYPE:
-        return Blake2RepoKey.detect(repository, manifest_data)
-    elif key_type == AuthenticatedKey.TYPE:
-        return AuthenticatedKey.detect(repository, manifest_data)
+        return RepoKey
+
+    for key in AVAILABLE_KEY_TYPES:
+        if key.TYPE == key_type:
+            return key
     else:
         raise UnsupportedPayloadError(key_type)
 
 
+def key_factory(repository, manifest_data):
+    return identify_key(manifest_data).detect(repository, manifest_data)
+
+
 def tam_required_file(repository):
     security_dir = get_security_dir(bin_to_hex(repository.id))
     return os.path.join(security_dir, 'tam_required')
@@ -139,6 +132,13 @@ def tam_required(repository):
 class KeyBase:
     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
+
     def __init__(self, repository):
         self.TYPE_STR = bytes([self.TYPE])
         self.repository = repository
@@ -236,6 +236,8 @@ def unpack_and_verify_manifest(self, data, force_tam_not_required=False):
 class PlaintextKey(KeyBase):
     TYPE = 0x02
     NAME = 'plaintext'
+    ARG_NAME = 'none'
+    STORAGE = KeyBlobStorage.NO_STORAGE
 
     chunk_seed = 0
 
@@ -466,6 +468,9 @@ class PassphraseKey(ID_HMAC_SHA_256, AESKeyBase):
     # This class is kept for a while to support migration from passphrase to repokey mode.
     TYPE = 0x01
     NAME = 'passphrase'
+    ARG_NAME = None
+    STORAGE = KeyBlobStorage.NO_STORAGE
+
     iterations = 100000  # must not be changed ever!
 
     @classmethod
@@ -623,6 +628,9 @@ def get_new_target(self, args):
 class KeyfileKey(ID_HMAC_SHA_256, KeyfileKeyBase):
     TYPE = 0x00
     NAME = 'key file'
+    ARG_NAME = 'keyfile'
+    STORAGE = KeyBlobStorage.KEYFILE
+
     FILE_ID = 'BORG_KEY'
 
     def sanity_check(self, filename, id):
@@ -683,6 +691,8 @@ def save(self, target, passphrase):
 class RepoKey(ID_HMAC_SHA_256, KeyfileKeyBase):
     TYPE = 0x03
     NAME = 'repokey'
+    ARG_NAME = 'repokey'
+    STORAGE = KeyBlobStorage.REPO
 
     def find_key(self):
         loc = self.repository._location.canonical_path()
@@ -715,6 +725,9 @@ def save(self, target, passphrase):
 class Blake2KeyfileKey(ID_BLAKE2b_256, KeyfileKey):
     TYPE = 0x04
     NAME = 'key file BLAKE2b'
+    ARG_NAME = 'keyfile-blake2'
+    STORAGE = KeyBlobStorage.KEYFILE
+
     FILE_ID = 'BORG_KEY'
     MAC = blake2b_256
 
@@ -722,12 +735,17 @@ class Blake2KeyfileKey(ID_BLAKE2b_256, KeyfileKey):
 class Blake2RepoKey(ID_BLAKE2b_256, RepoKey):
     TYPE = 0x05
     NAME = 'repokey BLAKE2b'
+    ARG_NAME = 'repokey-blake2'
+    STORAGE = KeyBlobStorage.REPO
+
     MAC = blake2b_256
 
 
 class AuthenticatedKey(ID_BLAKE2b_256, RepoKey):
     TYPE = 0x06
     NAME = 'authenticated BLAKE2b'
+    ARG_NAME = 'authenticated'
+    STORAGE = KeyBlobStorage.REPO
 
     def encrypt(self, chunk):
         chunk = self.compress(chunk)
@@ -742,3 +760,11 @@ def decrypt(self, id, data, decompress=True):
         data = self.compressor.decompress(payload)
         self.assert_id(id, data)
         return Chunk(data)
+
+
+AVAILABLE_KEY_TYPES = (
+    PlaintextKey,
+    PassphraseKey,
+    KeyfileKey, RepoKey,
+    Blake2KeyfileKey, Blake2RepoKey, AuthenticatedKey,
+)
diff --git a/src/borg/keymanager.py b/src/borg/keymanager.py
--- a/src/borg/keymanager.py
+++ b/src/borg/keymanager.py
@@ -4,7 +4,7 @@
 from hashlib import sha256
 import pkgutil
 
-from .key import KeyfileKey, RepoKey, PassphraseKey, KeyfileNotFoundError, PlaintextKey
+from .key import KeyfileKey, KeyfileNotFoundError, KeyBlobStorage, identify_key
 from .helpers import Manifest, NoManifestError, Error, yes, bin_to_hex
 from .repository import Repository
 
@@ -31,10 +31,6 @@ def sha256_truncated(data, num):
     return h.hexdigest()[:num]
 
 
-KEYBLOB_LOCAL = 'local'
-KEYBLOB_REPO = 'repo'
-
-
 class KeyManager:
     def __init__(self, repository):
         self.repository = repository
@@ -42,32 +38,27 @@ def __init__(self, repository):
         self.keyblob_storage = None
 
         try:
-            cdata = self.repository.get(Manifest.MANIFEST_ID)
+            manifest_data = self.repository.get(Manifest.MANIFEST_ID)
         except Repository.ObjectNotFound:
             raise NoManifestError
 
-        key_type = cdata[0]
-        if key_type == KeyfileKey.TYPE:
-            self.keyblob_storage = KEYBLOB_LOCAL
-        elif key_type == RepoKey.TYPE or key_type == PassphraseKey.TYPE:
-            self.keyblob_storage = KEYBLOB_REPO
-        elif key_type == PlaintextKey.TYPE:
+        key = identify_key(manifest_data)
+        self.keyblob_storage = key.STORAGE
+        if self.keyblob_storage == KeyBlobStorage.NO_STORAGE:
             raise UnencryptedRepo()
-        else:
-            raise UnknownKeyType(key_type)
 
     def load_keyblob(self):
-        if self.keyblob_storage == KEYBLOB_LOCAL:
+        if self.keyblob_storage == KeyBlobStorage.KEYFILE:
             k = KeyfileKey(self.repository)
             target = k.find_key()
             with open(target, 'r') as fd:
                 self.keyblob = ''.join(fd.readlines()[1:])
 
-        elif self.keyblob_storage == KEYBLOB_REPO:
+        elif self.keyblob_storage == KeyBlobStorage.REPO:
             self.keyblob = self.repository.load_key().decode()
 
     def store_keyblob(self, args):
-        if self.keyblob_storage == KEYBLOB_LOCAL:
+        if self.keyblob_storage == KeyBlobStorage.KEYFILE:
             k = KeyfileKey(self.repository)
             try:
                 target = k.find_key()
@@ -75,7 +66,7 @@ def store_keyblob(self, args):
                 target = k.get_new_target(args)
 
             self.store_keyfile(target)
-        elif self.keyblob_storage == KEYBLOB_REPO:
+        elif self.keyblob_storage == KeyBlobStorage.REPO:
             self.repository.save_key(self.keyblob.encode('utf-8'))
 
     def get_keyfile_data(self):


Test patch: