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.
This commit is contained in:
Andrey Andreyevich Bienkowski 2022-04-07 14:22:34 +00:00 committed by GitHub
parent 6c38bf3fdc
commit 56c27a99d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 325 additions and 45 deletions

View File

@ -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):

View File

@ -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,

View File

@ -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(HMACSHA256), 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'

View File

@ -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):

View File

@ -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,

View File

@ -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):

View File

@ -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):

View File

@ -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)

View File

@ -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')