Selaa lähdekoodia

Argon2 the second part: implement key encryption / decryption (#6469)

Argon2 the second part: implement encryption/decryption of argon2 keys

borg init --key-algorithm=argon2 (new default, older pbkdf2 also still available)

borg key change-passphrase: keep key algorithm the same
borg key change-location: keep key algorithm the same

use env var BORG_TESTONLY_WEAKEN_KDF=1 to resource limit (cpu, memory, ...) the kdf when running the automated tests.
Andrey Andreyevich Bienkowski 3 vuotta sitten
vanhempi
sitoutus
56c27a99d0

+ 2 - 8
conftest.py

@@ -2,14 +2,6 @@ import os
 
 import pytest
 
-# IMPORTANT keep this above all other borg imports to avoid inconsistent values
-# for `from borg.constants import PBKDF2_ITERATIONS` (or star import) usages before
-# this is executed
-from borg import constants
-# no fixture-based monkey-patching since star-imports are used for the constants module
-constants.PBKDF2_ITERATIONS = 1
-
-
 # needed to get pretty assertion failures in unit tests:
 if hasattr(pytest, 'register_assert_rewrite'):
     pytest.register_assert_rewrite('borg.testsuite')
@@ -36,6 +28,8 @@ def clean_env(tmpdir_factory, monkeypatch):
             if key.startswith('BORG_') and key not in ('BORG_FUSE_IMPL', )]
     for key in keys:
         monkeypatch.delenv(key, raising=False)
+    # Speed up tests
+    monkeypatch.setenv("BORG_TESTONLY_WEAKEN_KDF", "1")
 
 
 def pytest_report_header(config, startdir):

+ 3 - 1
src/borg/archiver.py

@@ -398,7 +398,8 @@ class Archiver:
             setattr(key_new, name, value)
 
         key_new.target = key_new.get_new_target(args)
-        key_new.save(key_new.target, key._passphrase, create=True)  # save with same passphrase
+        # save with same passphrase and algorithm
+        key_new.save(key_new.target, key._passphrase, create=True, algorithm=key._encrypted_key_algorithm)
 
         # rewrite the manifest with the new key, so that the key-type byte of the manifest changes
         manifest.key = key_new
@@ -4323,6 +4324,7 @@ class Archiver:
                                help='Set storage quota of the new repository (e.g. 5G, 1.5T). Default: no quota.')
         subparser.add_argument('--make-parent-dirs', dest='make_parent_dirs', action='store_true',
                                help='create the parent directories of the repository directory, if they are missing.')
+        subparser.add_argument('--key-algorithm', dest='key_algorithm', default='argon2', choices=list(KEY_ALGORITHMS))
 
         # borg key
         subparser = subparsers.add_parser('key', parents=[mid_common_parser], add_help=False,

+ 12 - 0
src/borg/constants.py

@@ -103,6 +103,18 @@ DASHES = '-' * 78
 
 PBKDF2_ITERATIONS = 100000
 
+# https://www.rfc-editor.org/rfc/rfc9106.html#section-4-6.2
+ARGON2_ARGS = {'time_cost': 3, 'memory_cost': 2**16, 'parallelism': 4, 'type': 'id'}
+ARGON2_SALT_BYTES = 16
+
+# Maps the CLI argument to our internal identifier for the format
+KEY_ALGORITHMS = {
+    # encrypt-and-MAC, kdf: PBKDF2(HMAC−SHA256), encryption: AES256-CTR, authentication: HMAC-SHA256
+    'pbkdf2': 'sha256',
+    # encrypt-then-MAC, kdf: argon2, encryption: AES256-CTR, authentication: HMAC-SHA256
+    'argon2': 'argon2 aes256-ctr hmac-sha256',
+}
+
 
 class KeyBlobStorage:
     NO_STORAGE = 'no_storage'

+ 81 - 15
src/borg/crypto/key.py

@@ -25,6 +25,7 @@ from ..platform import SaveFile
 from .nonces import NonceManager
 from .low_level import AES, bytes_to_long, long_to_bytes, bytes_to_int, num_cipher_blocks, hmac_sha256, blake2b_256, hkdf_hmac_sha512
 from .low_level import AES256_CTR_HMAC_SHA256, AES256_CTR_BLAKE2b, AES256_OCB, CHACHA20_POLY1305
+from . import low_level
 
 
 class UnsupportedPayloadError(Error):
@@ -51,6 +52,10 @@ class RepoKeyNotFoundError(Error):
     """No key entry found in the config of repository {}."""
 
 
+class UnsupportedKeyFormatError(Error):
+    """Your borg key is stored in an unsupported format. Try using a newer version of borg."""
+
+
 class TAMRequiredError(IntegrityError):
     __doc__ = textwrap.dedent("""
     Manifest is unauthenticated, but it is required for this repository.
@@ -430,15 +435,54 @@ class FlexiKey:
         unpacker = get_limited_unpacker('key')
         unpacker.feed(data)
         data = unpacker.unpack()
-        enc_key = EncryptedKey(internal_dict=data)
-        assert enc_key.version == 1
-        assert enc_key.algorithm == 'sha256'
-        key = passphrase.kdf(enc_key.salt, enc_key.iterations, 32)
-        data = AES(key, b'\0'*16).decrypt(enc_key.data)
-        if hmac.compare_digest(hmac_sha256(key, data), enc_key.hash):
+        encrypted_key = EncryptedKey(internal_dict=data)
+        if encrypted_key.version != 1:
+            raise UnsupportedKeyFormatError()
+        else:
+            self._encrypted_key_algorithm = encrypted_key.algorithm
+            if encrypted_key.algorithm == 'sha256':
+                return self.decrypt_key_file_pbkdf2(encrypted_key, passphrase)
+            elif encrypted_key.algorithm == 'argon2 aes256-ctr hmac-sha256':
+                return self.decrypt_key_file_argon2(encrypted_key, passphrase)
+            else:
+                raise UnsupportedKeyFormatError()
+
+    def decrypt_key_file_pbkdf2(self, encrypted_key, passphrase):
+        key = passphrase.kdf(encrypted_key.salt, encrypted_key.iterations, 32)
+        data = AES(key, b'\0'*16).decrypt(encrypted_key.data)
+        if hmac.compare_digest(hmac_sha256(key, data), encrypted_key.hash):
             return data
+        return None
+
+    def decrypt_key_file_argon2(self, encrypted_key, passphrase):
+        key = passphrase.argon2(
+            output_len_in_bytes=64,
+            salt=encrypted_key.salt,
+            time_cost=encrypted_key.argon2_time_cost,
+            memory_cost=encrypted_key.argon2_memory_cost,
+            parallelism=encrypted_key.argon2_parallelism,
+            type=encrypted_key.argon2_type,
+        )
+        enc_key, mac_key = key[:32], key[32:]
+        ae_cipher = AES256_CTR_HMAC_SHA256(
+            iv=0, header_len=0, aad_offset=0,
+            enc_key=enc_key,
+            mac_key=mac_key,
+        )
+        try:
+            return ae_cipher.decrypt(encrypted_key.data)
+        except low_level.IntegrityError:
+            return None
+
+    def encrypt_key_file(self, data, passphrase, algorithm):
+        if algorithm == 'sha256':
+            return self.encrypt_key_file_pbkdf2(data, passphrase)
+        elif algorithm == 'argon2 aes256-ctr hmac-sha256':
+            return self.encrypt_key_file_argon2(data, passphrase)
+        else:
+            raise ValueError(f'Unexpected algorithm: {algorithm}')
 
-    def encrypt_key_file(self, data, passphrase):
+    def encrypt_key_file_pbkdf2(self, data, passphrase):
         salt = os.urandom(32)
         iterations = PBKDF2_ITERATIONS
         key = passphrase.kdf(salt, iterations, 32)
@@ -454,7 +498,29 @@ class FlexiKey:
         )
         return msgpack.packb(enc_key.as_dict())
 
-    def _save(self, passphrase):
+    def encrypt_key_file_argon2(self, data, passphrase):
+        salt = os.urandom(ARGON2_SALT_BYTES)
+        key = passphrase.argon2(
+            output_len_in_bytes=64,
+            salt=salt,
+            **ARGON2_ARGS,
+        )
+        enc_key, mac_key = key[:32], key[32:]
+        ae_cipher = AES256_CTR_HMAC_SHA256(
+            iv=0, header_len=0, aad_offset=0,
+            enc_key=enc_key,
+            mac_key=mac_key,
+        )
+        encrypted_key = EncryptedKey(
+            version=1,
+            algorithm='argon2 aes256-ctr hmac-sha256',
+            salt=salt,
+            data=ae_cipher.encrypt(data),
+            **{'argon2_' + k: v for k, v in ARGON2_ARGS.items()},
+        )
+        return msgpack.packb(encrypted_key.as_dict())
+
+    def _save(self, passphrase, algorithm):
         key = Key(
             version=1,
             repository_id=self.repository_id,
@@ -464,14 +530,14 @@ class FlexiKey:
             chunk_seed=self.chunk_seed,
             tam_required=self.tam_required,
         )
-        data = self.encrypt_key_file(msgpack.packb(key.as_dict()), passphrase)
+        data = self.encrypt_key_file(msgpack.packb(key.as_dict()), passphrase, algorithm)
         key_data = '\n'.join(textwrap.wrap(b2a_base64(data).decode('ascii')))
         return key_data
 
     def change_passphrase(self, passphrase=None):
         if passphrase is None:
             passphrase = Passphrase.new(allow_empty=True)
-        self.save(self.target, passphrase)
+        self.save(self.target, passphrase, algorithm=self._encrypted_key_algorithm)
 
     @classmethod
     def create(cls, repository, args):
@@ -481,7 +547,7 @@ class FlexiKey:
         key.init_from_random_data()
         key.init_ciphers()
         target = key.get_new_target(args)
-        key.save(target, passphrase, create=True)
+        key.save(target, passphrase, create=True, algorithm=KEY_ALGORITHMS[args.key_algorithm])
         logger.info('Key in "%s" created.' % target)
         logger.info('Keep this key safe. Your data will be inaccessible without it.')
         return key
@@ -581,8 +647,8 @@ class FlexiKey:
             self.target = target
         return success
 
-    def save(self, target, passphrase, create=False):
-        key_data = self._save(passphrase)
+    def save(self, target, passphrase, algorithm, create=False):
+        key_data = self._save(passphrase, algorithm)
         if self.STORAGE == KeyBlobStorage.KEYFILE:
             if create and os.path.isfile(target):
                 # if a new keyfile key repository is created, ensure that an existing keyfile of another
@@ -657,8 +723,8 @@ class AuthenticatedKeyBase(AESKeyBase, FlexiKey):
         self.logically_encrypted = False
         return success
 
-    def save(self, target, passphrase, create=False):
-        super().save(target, passphrase, create=create)
+    def save(self, target, passphrase, algorithm, create=False):
+        super().save(target, passphrase, algorithm, create=create)
         self.logically_encrypted = False
 
     def init_ciphers(self, manifest_data=None):

+ 7 - 0
src/borg/helpers/passphrase.py

@@ -141,6 +141,8 @@ class Passphrase(str):
         return '<Passphrase "***hidden***">'
 
     def kdf(self, salt, iterations, length):
+        if os.environ.get("BORG_TESTONLY_WEAKEN_KDF") == "1":
+            iterations = 1
         return pbkdf2_hmac('sha256', self.encode('utf-8'), salt, iterations, length)
 
     def argon2(
@@ -152,6 +154,11 @@ class Passphrase(str):
         parallelism,
         type: Literal['i', 'd', 'id']
     ) -> bytes:
+        if os.environ.get("BORG_TESTONLY_WEAKEN_KDF") == "1":
+            time_cost = 1
+            parallelism = 1
+            # 8 is the smallest value that avoids the "Memory cost is too small" exception
+            memory_cost = 8
         type_map = {
             'i': argon2.low_level.Type.I,
             'd': argon2.low_level.Type.D,

+ 7 - 1
src/borg/item.pyx

@@ -7,6 +7,7 @@ from .helpers import bigint_to_int, int_to_bigint
 from .helpers import StableDict
 from .helpers import format_file_size
 
+
 cdef extern from "_item.c":
     object _object_to_optr(object obj)
     object _optr_to_object(object bytes)
@@ -294,7 +295,8 @@ class EncryptedKey(PropDict):
     If a EncryptedKey shall be serialized, give as_dict() method output to msgpack packer.
     """
 
-    VALID_KEYS = {'version', 'algorithm', 'iterations', 'salt', 'hash', 'data'}  # str-typed keys
+    VALID_KEYS = { 'version', 'algorithm', 'iterations', 'salt', 'hash', 'data',
+                   'argon2_time_cost', 'argon2_memory_cost', 'argon2_parallelism', 'argon2_type' }
 
     __slots__ = ("_dict", )  # avoid setting attributes not supported by properties
 
@@ -304,6 +306,10 @@ class EncryptedKey(PropDict):
     salt = PropDict._make_property('salt', bytes)
     hash = PropDict._make_property('hash', bytes)
     data = PropDict._make_property('data', bytes)
+    argon2_time_cost = PropDict._make_property('argon2_time_cost', int)
+    argon2_memory_cost = PropDict._make_property('argon2_memory_cost', int)
+    argon2_parallelism = PropDict._make_property('argon2_parallelism', int)
+    argon2_type = PropDict._make_property('argon2_type', str, encode=str.encode, decode=bytes.decode)
 
 
 class Key(PropDict):

+ 46 - 1
src/borg/testsuite/archiver.py

@@ -16,7 +16,7 @@ import sys
 import tempfile
 import time
 import unittest
-from binascii import unhexlify, b2a_base64
+from binascii import unhexlify, b2a_base64, a2b_base64
 from configparser import ConfigParser
 from datetime import datetime
 from datetime import timezone
@@ -3585,6 +3585,51 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02
         self.cmd('create', self.repository_location + '::test2', 'input')
         assert os.path.exists(nonce)
 
+    def test_init_defaults_to_argon2(self):
+        """https://github.com/borgbackup/borg/issues/747#issuecomment-1076160401"""
+        self.cmd('init', '--encryption=repokey', self.repository_location)
+        with Repository(self.repository_path) as repository:
+            key = msgpack.unpackb(a2b_base64(repository.load_key()))
+        assert key[b'algorithm'] == b'argon2 aes256-ctr hmac-sha256'
+
+    def test_init_with_explicit_key_algorithm(self):
+        """https://github.com/borgbackup/borg/issues/747#issuecomment-1076160401"""
+        self.cmd('init', '--encryption=repokey', '--key-algorithm=pbkdf2', self.repository_location)
+        with Repository(self.repository_path) as repository:
+            key = msgpack.unpackb(a2b_base64(repository.load_key()))
+        assert key[b'algorithm'] == b'sha256'
+
+    def verify_change_passphrase_does_not_change_algorithm(self, given_algorithm, expected_algorithm):
+        self.cmd('init', '--encryption=repokey', '--key-algorithm', given_algorithm, self.repository_location)
+        os.environ['BORG_NEW_PASSPHRASE'] = 'newpassphrase'
+
+        self.cmd('key', 'change-passphrase', self.repository_location)
+
+        with Repository(self.repository_path) as repository:
+            key = msgpack.unpackb(a2b_base64(repository.load_key()))
+            assert key[b'algorithm'] == expected_algorithm
+
+    def test_change_passphrase_does_not_change_algorithm_argon2(self):
+        self.verify_change_passphrase_does_not_change_algorithm('argon2', b'argon2 aes256-ctr hmac-sha256')
+
+    def test_change_passphrase_does_not_change_algorithm_pbkdf2(self):
+        self.verify_change_passphrase_does_not_change_algorithm('pbkdf2', b'sha256')
+
+    def verify_change_location_does_not_change_algorithm(self, given_algorithm, expected_algorithm):
+        self.cmd('init', '--encryption=keyfile', '--key-algorithm', given_algorithm, self.repository_location)
+
+        self.cmd('key', 'change-location', self.repository_location, 'repokey')
+
+        with Repository(self.repository_path) as repository:
+            key = msgpack.unpackb(a2b_base64(repository.load_key()))
+            assert key[b'algorithm'] == expected_algorithm
+
+    def test_change_location_does_not_change_algorithm_argon2(self):
+        self.verify_change_location_does_not_change_algorithm('argon2', b'argon2 aes256-ctr hmac-sha256')
+
+    def test_change_location_does_not_change_algorithm_pbkdf2(self):
+        self.verify_change_location_does_not_change_algorithm('pbkdf2', b'sha256')
+
 
 @unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available')
 class ArchiverTestCaseBinary(ArchiverTestCase):

+ 147 - 0
src/borg/testsuite/crypto.py

@@ -1,9 +1,19 @@
 from binascii import hexlify
+from unittest.mock import MagicMock
+import unittest
+from binascii import a2b_base64
+
+import pytest
 
 from ..crypto.low_level import AES256_CTR_HMAC_SHA256, AES256_OCB, CHACHA20_POLY1305, UNENCRYPTED, \
                                IntegrityError, is_libressl
 from ..crypto.low_level import bytes_to_long, bytes_to_int, long_to_bytes
 from ..crypto.low_level import hkdf_hmac_sha512
+from ..crypto.low_level import AES, hmac_sha256
+from ..crypto.key import KeyfileKey, UnsupportedKeyFormatError, RepoKey
+from ..helpers.passphrase import Passphrase
+from ..helpers import msgpack
+from ..constants import KEY_ALGORITHMS
 
 from . import BaseTestCase
 
@@ -248,3 +258,140 @@ class CryptoTestCase(BaseTestCase):
 
         okm = hkdf_hmac_sha512(ikm, salt, info, l)
         assert okm == bytes.fromhex('1407d46013d98bc6decefcfee55f0f90b0c7f63d68eb1a80eaf07e953cfc0a3a5240a155d6e4daa965bb')
+
+
+def test_decrypt_key_file_argon2_aes256_ctr_hmac_sha256(monkeypatch):
+    plain = b'hello'
+    # echo -n "hello, pass phrase" | argon2 saltsaltsaltsalt -id -t 1 -k 8 -p 1 -l 64 -r
+    key = bytes.fromhex('d07cc7f9cfb483303e0b9fec176b2a9c559bb70c3a9fb0d5f9c0c23527cd09570212449f09f8cd28c1a41b73fa0098e889c3f2642e87c392e51f95d70d248d9d')
+    ae_cipher = AES256_CTR_HMAC_SHA256(
+        iv=0, header_len=0, aad_offset=0,
+        enc_key=key[:32],
+        mac_key=key[32:],
+    )
+
+    envelope = ae_cipher.encrypt(plain)
+
+    encrypted = msgpack.packb({
+        'version': 1,
+        'salt': b'salt'*4,
+        'argon2_time_cost': 1,
+        'argon2_memory_cost': 8,
+        'argon2_parallelism': 1,
+        'argon2_type': b'id',
+        'algorithm': 'argon2 aes256-ctr hmac-sha256',
+        'data': envelope,
+    })
+    monkeypatch.setenv('BORG_PASSPHRASE', "hello, pass phrase")
+    passphrase = Passphrase.new()
+    key = KeyfileKey(None)
+
+    decrypted = key.decrypt_key_file(encrypted, passphrase)
+
+    assert decrypted == plain
+
+
+def test_decrypt_key_file_pbkdf2_sha256_aes256_ctr_hmac_sha256(monkeypatch):
+    plain = b'hello'
+    salt = b'salt'*4
+    monkeypatch.setenv('BORG_PASSPHRASE', "hello, pass phrase")
+    passphrase = Passphrase.new()
+    key = passphrase.kdf(salt, iterations=1, length=32)
+    hash = hmac_sha256(key, plain)
+    data = AES(key, b'\0'*16).encrypt(plain)
+    encrypted = msgpack.packb({
+        'version': 1,
+        'algorithm': 'sha256',
+        'iterations': 1,
+        'salt': salt,
+        'data': data,
+        'hash': hash,
+    })
+    key = KeyfileKey(None)
+
+    decrypted = key.decrypt_key_file(encrypted, passphrase)
+
+    assert decrypted == plain
+
+
+def test_decrypt_key_file_unsupported_algorithm(monkeypatch):
+    """We will add more algorithms in the future. We should raise a helpful error."""
+    monkeypatch.setenv('BORG_PASSPHRASE', "hello, pass phrase")
+    passphrase = Passphrase.new()
+    key = KeyfileKey(None)
+    encrypted = msgpack.packb({
+        'algorithm': 'THIS ALGORITHM IS NOT SUPPORTED',
+        'version': 1,
+    })
+
+    with pytest.raises(UnsupportedKeyFormatError):
+        key.decrypt_key_file(encrypted, passphrase)
+
+
+def test_decrypt_key_file_v2_is_unsupported(monkeypatch):
+    """There may eventually be a version 2 of the format. For now we should raise a helpful error."""
+    monkeypatch.setenv('BORG_PASSPHRASE', "hello, pass phrase")
+    passphrase = Passphrase.new()
+    key = KeyfileKey(None)
+    encrypted = msgpack.packb({
+        'version': 2,
+    })
+
+    with pytest.raises(UnsupportedKeyFormatError):
+        key.decrypt_key_file(encrypted, passphrase)
+
+
+@pytest.mark.parametrize('cli_argument, expected_algorithm', KEY_ALGORITHMS.items())
+def test_key_file_roundtrip(monkeypatch, cli_argument, expected_algorithm):
+    def to_dict(key):
+        extract = 'repository_id', 'enc_key', 'enc_hmac_key', 'id_key', 'chunk_seed'
+        return {a: getattr(key, a) for a in extract}
+
+    repository = MagicMock(id=b'repository_id')
+    monkeypatch.setenv('BORG_PASSPHRASE', "hello, pass phrase")
+
+    save_me = RepoKey.create(repository, args=MagicMock(key_algorithm=cli_argument))
+    saved = repository.save_key.call_args.args[0]
+    repository.load_key.return_value = saved
+    load_me = RepoKey.detect(repository, manifest_data=None)
+
+    assert to_dict(load_me) == to_dict(save_me)
+    assert msgpack.unpackb(a2b_base64(saved))[b'algorithm'] == expected_algorithm.encode()
+
+
+@unittest.mock.patch('getpass.getpass')
+def test_repo_key_detect_does_not_raise_integrity_error(getpass, monkeypatch):
+    """https://github.com/borgbackup/borg/pull/6469#discussion_r832670411
+
+    This is a regression test for a bug I introduced and fixed:
+
+    Traceback (most recent call last):
+      File "/home/user/borg-master/src/borg/testsuite/crypto.py", line 384, in test_repo_key_detect_does_not_raise_integrity_error
+        RepoKey.detect(repository, manifest_data=None)
+      File "/home/user/borg-master/src/borg/crypto/key.py", line 402, in detect
+        if not key.load(target, passphrase):
+      File "/home/user/borg-master/src/borg/crypto/key.py", line 654, in load
+        success = self._load(key_data, passphrase)
+      File "/home/user/borg-master/src/borg/crypto/key.py", line 418, in _load
+        data = self.decrypt_key_file(cdata, passphrase)
+      File "/home/user/borg-master/src/borg/crypto/key.py", line 444, in decrypt_key_file
+        return self.decrypt_key_file_argon2(encrypted_key, passphrase)
+      File "/home/user/borg-master/src/borg/crypto/key.py", line 470, in decrypt_key_file_argon2
+        return ae_cipher.decrypt(encrypted_key.data)
+      File "src/borg/crypto/low_level.pyx", line 302, in borg.crypto.low_level.AES256_CTR_BASE.decrypt
+        self.mac_verify(<const unsigned char *> idata.buf+aoffset, alen,
+      File "src/borg/crypto/low_level.pyx", line 382, in borg.crypto.low_level.AES256_CTR_HMAC_SHA256.mac_verify
+        raise IntegrityError('MAC Authentication failed')
+    borg.crypto.low_level.IntegrityError: MAC Authentication failed
+
+    1. FlexiKey.decrypt_key_file() is supposed to signal the decryption failure by returning None
+    2. FlexiKey.detect() relies on that interface - it tries an empty passphrase before prompting the user
+    3. my initial implementation of decrypt_key_file_argon2() was simply passing through the IntegrityError() from AES256_CTR_BASE.decrypt()
+    """
+    repository = MagicMock(id=b'repository_id')
+    getpass.return_value = "hello, pass phrase"
+    monkeypatch.setenv('BORG_DISPLAY_PASSPHRASE', 'no')
+    RepoKey.create(repository, args=MagicMock(key_algorithm='argon2'))
+    repository.load_key.return_value = repository.save_key.call_args.args[0]
+
+    RepoKey.detect(repository, manifest_data=None)

+ 19 - 18
src/borg/testsuite/key.py

@@ -25,16 +25,17 @@ from ..helpers import msgpack
 class TestKey:
     class MockArgs:
         location = Location(tempfile.mkstemp()[1])
+        key_algorithm = "argon2"
 
     keyfile2_key_file = """
         BORG_KEY 0000000000000000000000000000000000000000000000000000000000000000
-        hqppdGVyYXRpb25zzgABhqCkaGFzaNoAIMyonNI+7Cjv0qHi0AOBM6bLGxACJhfgzVD2oq
-        bIS9SFqWFsZ29yaXRobaZzaGEyNTakc2FsdNoAINNK5qqJc1JWSUjACwFEWGTdM7Nd0a5l
-        1uBGPEb+9XM9p3ZlcnNpb24BpGRhdGHaANAYDT5yfPpU099oBJwMomsxouKyx/OG4QIXK2
-        hQCG2L2L/9PUu4WIuKvGrsXoP7syemujNfcZws5jLp2UPva4PkQhQsrF1RYDEMLh2eF9Ol
-        rwtkThq1tnh7KjWMG9Ijt7/aoQtq0zDYP/xaFF8XXSJxiyP5zjH5+spB6RL0oQHvbsliSh
-        /cXJq7jrqmrJ1phd6dg4SHAM/i+hubadZoS6m25OQzYAW09wZD/phG8OVa698Z5ed3HTaT
-        SmrtgJL3EoOKgUI9d6BLE4dJdBqntifo""".strip()
+        hqlhbGdvcml0aG2mc2hhMjU2pGRhdGHaAN4u2SiN7hqISe3OA8raBWNuvHn1R50ZU7HVCn
+        11vTJNEaj9soxUaIGcW+pAB2N5yYoKMg/sGCMuZa286iJ008DvN99rf/ORfcKrK2GmzslO
+        N3uv9Tk9HtqV/Sq5zgM9xuY9rEeQGDQVQ+AOsFamJqSUrAemGJbJqw9IerXC/jN4XPnX6J
+        pi1cXCFxHfDaEhmWrkdPNoZdirCv/eP/dOVOLmwU58YsS+MvkZNfEa16el/fSb/ENdrwJ/
+        2aYMQrDdk1d5MYzkjotv/KpofNwPXZchu2EwH7OIHWQjEVL1DZWkaGFzaNoAIO/7qn1hr3
+        F84MsMMiqpbz4KVICeBZhfAaTPs4W7BC63qml0ZXJhdGlvbnPOAAGGoKRzYWx02gAgLENQ
+        2uVCoR7EnAoiRzn8J+orbojKtJlNCnQ31SSC8rendmVyc2lvbgE=""".strip()
 
     keyfile2_cdata = unhexlify(re.sub(r'\W', '', """
         0055f161493fcfc16276e8c31493c4641e1eb19a79d0326fad0291e5a9c98e5933
@@ -44,17 +45,17 @@ class TestKey:
 
     keyfile_blake2_key_file = """
         BORG_KEY 0000000000000000000000000000000000000000000000000000000000000000
-        hqlhbGdvcml0aG2mc2hhMjU2pGRhdGHaAZBu680Do3CmfWzeMCwe48KJi3Vps9mEDy7MKF
-        TastsEhiAd1RQMuxfZpklkLeddMMWk+aPtFiURRFb02JLXV5cKRC1o2ZDdiNa0nao+o6+i
-        gUjjsea9TAu25t3vxh8uQWs5BuKRLBRr0nUgrSd0IYMUgn+iVbLJRzCCssvxsklkwQxN3F
-        Y+MvBnn8kUXSeoSoQ2l0fBHzq94Y7LMOm/owMam5URnE8/UEc6ZXBrbyX4EXxDtUqJcs+D
-        i451thtlGdigDLpvf9nyK66mjiCpPCTCgtlzq0Pe1jcdhnsUYLg+qWzXZ7e2opEZoC6XxS
-        3DIuBOxG3Odqj9IKB+6/kl94vz98awPWFSpYcLZVWu7sIP38ZkUK+ad5MHTo/LvTuZdFnd
-        iqKzZIDUJl3Zl1WGmP/0xVOmfIlznkCZy4d3SMuujwIcqQ5kDvwDRPpdhBBk+UWQY5vFXk
-        kR1NBNLSTyhAzu3fiUmFl0qZ+UWPRkGAEBy/NuoEibrWwab8BX97cATyvnmOqYkU9PT0C6
-        l2l9E4bPpGhhc2jaACDnIa8KgKv84/b5sjaMgSZeIVkuKSLJy2NN8zoH8lnd36ppdGVyYX
-        Rpb25zzgABhqCkc2FsdNoAIEJLlLh7q74j3q53856H5GgzA1HH+aW5bA/as544+PGkp3Zl
-        cnNpb24B""".strip()
+        hqlhbGdvcml0aG2mc2hhMjU2pGRhdGHaAZ7VCsTjbLhC1ipXOyhcGn7YnROEhP24UQvOCi
+        Oar1G+JpwgO9BIYaiCODUpzPuDQEm6WxyTwEneJ3wsuyeqyh7ru2xo9FAUKRf6jcqqZnan
+        ycTfktkUC+CPhKR7W6MTu5fPvy99chyL09/RGdD15aswR5PjNoFu4626sfMrBReyPdlxqt
+        F80m+fbNE/vln2Trqoz9EMHQ3IxjIK4q0m4Aj7TwCu7ZankFtwt898+tYsWE7lb2Ps/gXB
+        F8PM/5wHpYps2AKhDCpwKp5HyqIqlF5IzR2ydL9QP20QBjp/rSi6b+xwrfxNJZfw78f8ef
+        A2Yj7xIsxNQ0kmVmTL/UF6d7+Mw1JfurWrySiDU7QQ+RiZpWUZ0DdReB+e4zn6/KNKC884
+        34SGywADuLIQe2FKU+5jBCbutEyEGILQbAR/cgeLy5+V2XwXMJh4ytwXVIeT6Lk+qhYAdz
+        Klx4ub7XijKcOxJyBE+4k33DAhcfIT2r4/sxgMhXrIOEQPKsMAixzdcqVYkpou+6c4PZeL
+        nr+UjfJwOqK1BlWk1NgwE4GXYIKkaGFzaNoAIAzjUtpBPPh6kItZtHQZvnQG6FpucZNfBC
+        UTHFJg343jqml0ZXJhdGlvbnPOAAGGoKRzYWx02gAgz3YaUZZ/s+UWywj97EY5b4KhtJYi
+        qkPqtDDxs2j/T7+ndmVyc2lvbgE=""".strip()
 
     keyfile_blake2_cdata = bytes.fromhex('04fdf9475cf2323c0ba7a99ddc011064f2e7d039f539f2e448'
                                          '0e6f5fc6ff9993d604040404040404098c8cee1c6db8c28947')