diff --git a/conftest.py b/conftest.py index 3a7c278a..6166b475 100644 --- a/conftest.py +++ b/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): diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 7eb25bac..a3258ce8 100644 --- a/src/borg/archiver.py +++ b/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, diff --git a/src/borg/constants.py b/src/borg/constants.py index 5fb6b9a6..4c0f4387 100644 --- a/src/borg/constants.py +++ b/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' diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index 077ad550..d117b658 100644 --- a/src/borg/crypto/key.py +++ b/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): - return data + 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 encrypt_key_file(self, data, passphrase): + 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_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): diff --git a/src/borg/helpers/passphrase.py b/src/borg/helpers/passphrase.py index 5a0acd12..52ecc11b 100644 --- a/src/borg/helpers/passphrase.py +++ b/src/borg/helpers/passphrase.py @@ -141,6 +141,8 @@ class Passphrase(str): return '' 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, diff --git a/src/borg/item.pyx b/src/borg/item.pyx index 1f91f523..85cfc95e 100644 --- a/src/borg/item.pyx +++ b/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): diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 48637b1f..97464627 100644 --- a/src/borg/testsuite/archiver.py +++ b/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): diff --git a/src/borg/testsuite/crypto.py b/src/borg/testsuite/crypto.py index baf8cba2..3465bb8f 100644 --- a/src/borg/testsuite/crypto.py +++ b/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( 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) diff --git a/src/borg/testsuite/key.py b/src/borg/testsuite/key.py index 14297d48..4c84756a 100644 --- a/src/borg/testsuite/key.py +++ b/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')