Merge pull request #6821 from ThomasWaldmann/remove-legacy-repo-creation-borg2

rcreate: remove legacy crypto for new repos, fixes #6490
This commit is contained in:
TW 2022-06-30 21:33:46 +02:00 committed by GitHub
commit 2ab254cea0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 266 additions and 431 deletions

View file

@ -303,7 +303,6 @@ class build_man(Command):
'key_change-passphrase': 'key',
'key_change-location': 'key',
'key_change-algorithm': 'key',
'key_export': 'key',
'key_import': 'key',
'key_migrate-to-repokey': 'key',

View file

@ -48,6 +48,8 @@ try:
from .compress import CompressionSpec, ZLIB, ZLIB_legacy, ObfuscateSize
from .crypto.key import key_creator, key_argument_names, tam_required_file, tam_required
from .crypto.key import RepoKey, KeyfileKey, Blake2RepoKey, Blake2KeyfileKey, FlexiKey
from .crypto.key import AESOCBRepoKey, CHPORepoKey, Blake2AESOCBRepoKey, Blake2CHPORepoKey
from .crypto.key import AESOCBKeyfileKey, CHPOKeyfileKey, Blake2AESOCBKeyfileKey, Blake2CHPOKeyfileKey
from .crypto.keymanager import KeyManager
from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, EXIT_SIGNAL_BASE
from .helpers import Error, NoManifestError, set_ec
@ -503,28 +505,32 @@ class Archiver:
return EXIT_ERROR
if args.key_mode == 'keyfile':
if isinstance(key, RepoKey):
key_new = KeyfileKey(repository)
elif isinstance(key, Blake2RepoKey):
key_new = Blake2KeyfileKey(repository)
elif isinstance(key, (KeyfileKey, Blake2KeyfileKey)):
print(f"Location already is {args.key_mode}")
return EXIT_SUCCESS
if isinstance(key, AESOCBRepoKey):
key_new = AESOCBKeyfileKey(repository)
elif isinstance(key, CHPORepoKey):
key_new = CHPOKeyfileKey(repository)
elif isinstance(key, Blake2AESOCBRepoKey):
key_new = Blake2AESOCBKeyfileKey(repository)
elif isinstance(key, Blake2CHPORepoKey):
key_new = Blake2CHPOKeyfileKey(repository)
else:
raise Error("Unsupported key type")
print("Change not needed or not supported.")
return EXIT_WARNING
if args.key_mode == 'repokey':
if isinstance(key, KeyfileKey):
key_new = RepoKey(repository)
elif isinstance(key, Blake2KeyfileKey):
key_new = Blake2RepoKey(repository)
elif isinstance(key, (RepoKey, Blake2RepoKey)):
print(f"Location already is {args.key_mode}")
return EXIT_SUCCESS
if isinstance(key, AESOCBKeyfileKey):
key_new = AESOCBRepoKey(repository)
elif isinstance(key, CHPOKeyfileKey):
key_new = CHPORepoKey(repository)
elif isinstance(key, Blake2AESOCBKeyfileKey):
key_new = Blake2AESOCBRepoKey(repository)
elif isinstance(key, Blake2CHPOKeyfileKey):
key_new = Blake2CHPORepoKey(repository)
else:
raise Error("Unsupported key type")
print("Change not needed or not supported.")
return EXIT_WARNING
for name in ('repository_id', 'enc_key', 'enc_hmac_key', 'id_key', 'chunk_seed',
'tam_required', 'nonce_manager', 'cipher'):
'tam_required', 'sessionid', 'cipher'):
value = getattr(key, name)
setattr(key_new, name, value)
@ -4374,22 +4380,6 @@ class Archiver:
If you do **not** want to encrypt the contents of your backups, but still want to detect
malicious tampering use an `authenticated` mode. It's like `repokey` minus encryption.
Key derivation functions
++++++++++++++++++++++++
- ``--key-algorithm argon2`` is the default and is recommended.
The key encryption key is derived from your passphrase via argon2-id.
Argon2 is considered more modern and secure than pbkdf2.
Our implementation of argon2-based key algorithm follows the cryptographic best practices:
- It derives two separate keys from your passphrase: one to encrypt your key and another one
to sign it. ``--key-algorithm pbkdf2`` uses the same key for both.
- It uses encrypt-then-mac instead of encrypt-and-mac used by ``--key-algorithm pbkdf2``
Neither is inherently linked to the key derivation function, but since we were going
to break backwards compatibility anyway we took the opportunity to fix all 3 issues at once.
""")
subparser = subparsers.add_parser('rcreate', parents=[common_parser], add_help=False,
description=self.do_rcreate.__doc__, epilog=rcreate_epilog,
@ -4412,8 +4402,6 @@ 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),
help='the algorithm we use to derive a key encryption key from your passphrase. Default: argon2')
# borg key
subparser = subparsers.add_parser('key', parents=[mid_common_parser], add_help=False,
@ -4538,46 +4526,6 @@ class Archiver:
subparser.add_argument('--keep', dest='keep', action='store_true',
help='keep the key also at the current location (default: remove it)')
change_algorithm_epilog = process_epilog("""
Change the algorithm we use to encrypt and authenticate the borg key.
Important: In a `repokey` mode (e.g. repokey-blake2) all users share the same key.
In this mode upgrading to `argon2` will make it impossible to access the repo for users who use an old version of borg.
We recommend upgrading to the latest stable version.
Important: In a `keyfile` mode (e.g. keyfile-blake2) each user has their own key (in ``~/.config/borg/keys``).
In this mode this command will only change the key used by the current user.
If you want to upgrade to `argon2` to strengthen security, you will have to upgrade each user's key individually.
Your repository is encrypted and authenticated with a key that is randomly generated by ``borg init``.
The key is encrypted and authenticated with your passphrase.
We currently support two choices:
1. argon2 - recommended. This algorithm is used by default when initialising a new repository.
The key encryption key is derived from your passphrase via argon2-id.
Argon2 is considered more modern and secure than pbkdf2.
2. pbkdf2 - the legacy algorithm. Use this if you want to access your repo via old versions of borg.
The key encryption key is derived from your passphrase via PBKDF2-HMAC-SHA256.
Examples::
# Upgrade an existing key to argon2
borg key change-algorithm /path/to/repo argon2
# Downgrade to pbkdf2 - use this if upgrading borg is not an option
borg key change-algorithm /path/to/repo pbkdf2
""")
subparser = key_parsers.add_parser('change-algorithm', parents=[common_parser], add_help=False,
description=self.do_change_algorithm.__doc__,
epilog=change_algorithm_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help='change key algorithm')
subparser.set_defaults(func=self.do_change_algorithm)
subparser.add_argument('algorithm', metavar='ALGORITHM', choices=list(KEY_ALGORITHMS),
help='select key algorithm')
# borg list
list_epilog = process_epilog("""
This command lists the contents of an archive.

View file

@ -98,7 +98,7 @@ def identify_key(manifest_data):
if key_type == KeyType.PASSPHRASE: # legacy, see comment in KeyType class.
return RepoKey
for key in AVAILABLE_KEY_TYPES:
for key in LEGACY_KEY_TYPES + AVAILABLE_KEY_TYPES:
if key.TYPE == key_type:
return key
else:
@ -620,7 +620,7 @@ class FlexiKey:
passphrase = Passphrase.new(allow_empty=True)
key.init_ciphers()
target = key.get_new_target(args)
key.save(target, passphrase, create=True, algorithm=KEY_ALGORITHMS[args.key_algorithm])
key.save(target, passphrase, create=True, algorithm=KEY_ALGORITHMS['argon2'])
logger.info('Key in "%s" created.' % target)
logger.info('Keep this key safe. Your data will be inaccessible without it.')
return key
@ -977,7 +977,7 @@ class CHPORepoKey(ID_HMAC_SHA_256, AEADKeyBase, FlexiKey):
class Blake2AESOCBKeyfileKey(ID_BLAKE2b_256, AEADKeyBase, FlexiKey):
TYPES_ACCEPTABLE = {KeyType.BLAKE2AESOCBKEYFILE, KeyType.BLAKE2AESOCBREPO}
TYPE = KeyType.BLAKE2AESOCBKEYFILE
NAME = 'key file Blake2b AES-OCB'
NAME = 'key file BLAKE2b AES-OCB'
ARG_NAME = 'keyfile-blake2-aes-ocb'
STORAGE = KeyBlobStorage.KEYFILE
CIPHERSUITE = AES256_OCB
@ -986,7 +986,7 @@ class Blake2AESOCBKeyfileKey(ID_BLAKE2b_256, AEADKeyBase, FlexiKey):
class Blake2AESOCBRepoKey(ID_BLAKE2b_256, AEADKeyBase, FlexiKey):
TYPES_ACCEPTABLE = {KeyType.BLAKE2AESOCBKEYFILE, KeyType.BLAKE2AESOCBREPO}
TYPE = KeyType.BLAKE2AESOCBREPO
NAME = 'repokey Blake2b AES-OCB'
NAME = 'repokey BLAKE2b AES-OCB'
ARG_NAME = 'repokey-blake2-aes-ocb'
STORAGE = KeyBlobStorage.REPO
CIPHERSUITE = AES256_OCB
@ -995,7 +995,7 @@ class Blake2AESOCBRepoKey(ID_BLAKE2b_256, AEADKeyBase, FlexiKey):
class Blake2CHPOKeyfileKey(ID_BLAKE2b_256, AEADKeyBase, FlexiKey):
TYPES_ACCEPTABLE = {KeyType.BLAKE2CHPOKEYFILE, KeyType.BLAKE2CHPOREPO}
TYPE = KeyType.BLAKE2CHPOKEYFILE
NAME = 'key file Blake2b ChaCha20-Poly1305'
NAME = 'key file BLAKE2b ChaCha20-Poly1305'
ARG_NAME = 'keyfile-blake2-chacha20-poly1305'
STORAGE = KeyBlobStorage.KEYFILE
CIPHERSUITE = CHACHA20_POLY1305
@ -1004,16 +1004,23 @@ class Blake2CHPOKeyfileKey(ID_BLAKE2b_256, AEADKeyBase, FlexiKey):
class Blake2CHPORepoKey(ID_BLAKE2b_256, AEADKeyBase, FlexiKey):
TYPES_ACCEPTABLE = {KeyType.BLAKE2CHPOKEYFILE, KeyType.BLAKE2CHPOREPO}
TYPE = KeyType.BLAKE2CHPOREPO
NAME = 'repokey Blake2b ChaCha20-Poly1305'
NAME = 'repokey BLAKE2b ChaCha20-Poly1305'
ARG_NAME = 'repokey-blake2-chacha20-poly1305'
STORAGE = KeyBlobStorage.REPO
CIPHERSUITE = CHACHA20_POLY1305
LEGACY_KEY_TYPES = (
# legacy (AES-CTR based) crypto
KeyfileKey, RepoKey,
Blake2KeyfileKey, Blake2RepoKey,
)
AVAILABLE_KEY_TYPES = (
# these are available encryption modes for new repositories
# not encrypted modes
PlaintextKey,
KeyfileKey, RepoKey, AuthenticatedKey,
Blake2KeyfileKey, Blake2RepoKey, Blake2AuthenticatedKey,
AuthenticatedKey, Blake2AuthenticatedKey,
# new crypto
AESOCBKeyfileKey, AESOCBRepoKey,
CHPOKeyfileKey, CHPORepoKey,

View file

@ -7,7 +7,7 @@ from hashlib import sha256
from ..helpers import Manifest, NoManifestError, Error, yes, bin_to_hex, dash_open
from ..repository import Repository
from .key import KeyfileKey, KeyfileNotFoundError, RepoKeyNotFoundError, KeyBlobStorage, identify_key
from .key import CHPOKeyfileKey, KeyfileNotFoundError, RepoKeyNotFoundError, KeyBlobStorage, identify_key
class UnencryptedRepo(Error):
@ -50,7 +50,7 @@ class KeyManager:
def load_keyblob(self):
if self.keyblob_storage == KeyBlobStorage.KEYFILE:
k = KeyfileKey(self.repository)
k = CHPOKeyfileKey(self.repository)
target = k.find_key()
with open(target) as fd:
self.keyblob = ''.join(fd.readlines()[1:])
@ -65,7 +65,7 @@ class KeyManager:
def store_keyblob(self, args):
if self.keyblob_storage == KeyBlobStorage.KEYFILE:
k = KeyfileKey(self.repository)
k = CHPOKeyfileKey(self.repository)
target = k.get_existing_or_new_target(args)
self.store_keyfile(target)
@ -73,7 +73,7 @@ class KeyManager:
self.repository.save_key(self.keyblob.encode('utf-8'))
def get_keyfile_data(self):
data = f'{KeyfileKey.FILE_ID} {bin_to_hex(self.repository.id)}\n'
data = f'{CHPOKeyfileKey.FILE_ID} {bin_to_hex(self.repository.id)}\n'
data += self.keyblob
if not self.keyblob.endswith('\n'):
data += '\n'
@ -136,7 +136,7 @@ class KeyManager:
fd.write(export)
def import_keyfile(self, args):
file_id = KeyfileKey.FILE_ID
file_id = CHPOKeyfileKey.FILE_ID
first_line = file_id + ' ' + bin_to_hex(self.repository.id) + '\n'
with dash_open(args.path, 'r') as fd:
file_first_line = fd.read(len(first_line))

View file

@ -613,7 +613,7 @@ cdef class CHACHA20_POLY1305(_AEAD_BASE):
super().__init__(key, iv=iv, header_len=header_len, aad_offset=aad_offset)
cdef class AES:
cdef class AES: # legacy
"""A thin wrapper around the OpenSSL EVP cipher API - for legacy code, like key file encryption"""
cdef CIPHER cipher
cdef EVP_CIPHER_CTX *ctx

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,7 @@ from .key import TestKey
from ..archive import Statistics
from ..cache import AdHocCache
from ..compress import CompressionSpec
from ..crypto.key import RepoKey
from ..crypto.key import AESOCBRepoKey
from ..hashindex import ChunkIndex, CacheSynchronizer
from ..helpers import Manifest
from ..repository import Repository
@ -218,7 +218,7 @@ class TestAdHocCache:
@pytest.fixture
def key(self, repository, monkeypatch):
monkeypatch.setenv('BORG_PASSPHRASE', 'test')
key = RepoKey.create(repository, TestKey.MockArgs())
key = AESOCBRepoKey.create(repository, TestKey.MockArgs())
key.compressor = CompressionSpec('none').compressor
return key

View file

@ -9,7 +9,7 @@ from ..crypto.low_level import AES256_CTR_HMAC_SHA256, AES256_OCB, CHACHA20_POLY
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, RepoKey, FlexiKey
from ..crypto.key import CHPOKeyfileKey, AESOCBRepoKey, FlexiKey
from ..helpers import msgpack
from . import BaseTestCase
@ -264,7 +264,7 @@ def test_decrypt_key_file_argon2_chacha20_poly1305():
'algorithm': 'argon2 chacha20-poly1305',
'data': envelope,
})
key = KeyfileKey(None)
key = CHPOKeyfileKey(None)
decrypted = key.decrypt_key_file(encrypted, "hello, pass phrase")
@ -286,7 +286,7 @@ def test_decrypt_key_file_pbkdf2_sha256_aes256_ctr_hmac_sha256():
'data': data,
'hash': hash,
})
key = KeyfileKey(None)
key = CHPOKeyfileKey(None)
decrypted = key.decrypt_key_file(encrypted, passphrase)
@ -325,7 +325,7 @@ def test_repo_key_detect_does_not_raise_integrity_error(getpass, monkeypatch):
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'))
AESOCBRepoKey.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)
AESOCBRepoKey.detect(repository, manifest_data=None)

View file

@ -8,9 +8,10 @@ from unittest.mock import MagicMock
import pytest
from ..crypto.key import bin_to_hex
from ..crypto.key import PlaintextKey, AuthenticatedKey, RepoKey, KeyfileKey, \
Blake2KeyfileKey, Blake2RepoKey, Blake2AuthenticatedKey, \
AESOCBKeyfileKey, AESOCBRepoKey, CHPOKeyfileKey, CHPORepoKey
from ..crypto.key import PlaintextKey, AuthenticatedKey, Blake2AuthenticatedKey
from ..crypto.key import RepoKey, KeyfileKey, Blake2RepoKey, Blake2KeyfileKey
from ..crypto.key import AESOCBRepoKey, AESOCBKeyfileKey, CHPORepoKey, CHPOKeyfileKey
from ..crypto.key import Blake2AESOCBRepoKey, Blake2AESOCBKeyfileKey, Blake2CHPORepoKey, Blake2CHPOKeyfileKey
from ..crypto.key import ID_HMAC_SHA_256, ID_BLAKE2b_256
from ..crypto.key import TAMRequiredError, TAMInvalid, TAMUnsupportedSuiteError, UnsupportedManifestError, UnsupportedKeyFormatError
from ..crypto.key import identify_key
@ -76,16 +77,17 @@ class TestKey:
return tmpdir
@pytest.fixture(params=(
# not encrypted
PlaintextKey,
AuthenticatedKey,
KeyfileKey,
RepoKey,
AuthenticatedKey,
Blake2KeyfileKey,
Blake2RepoKey,
Blake2AuthenticatedKey,
AuthenticatedKey, Blake2AuthenticatedKey,
# legacy crypto
KeyfileKey, Blake2KeyfileKey,
RepoKey, Blake2RepoKey,
# new crypto
AESOCBKeyfileKey, AESOCBRepoKey,
Blake2AESOCBKeyfileKey, Blake2AESOCBRepoKey,
CHPOKeyfileKey, CHPORepoKey,
Blake2CHPOKeyfileKey, Blake2CHPORepoKey,
))
def key(self, request, monkeypatch):
monkeypatch.setenv('BORG_PASSPHRASE', 'test')
@ -143,33 +145,21 @@ class TestKey:
id = key.id_hash(chunk)
assert chunk == key2.decrypt(id, key.encrypt(id, chunk))
def test_keyfile_nonce_rollback_protection(self, monkeypatch, keys_dir):
monkeypatch.setenv('BORG_PASSPHRASE', 'test')
repository = self.MockRepository()
with open(os.path.join(get_security_dir(repository.id_str), 'nonce'), "w") as fd:
fd.write("0000000000002000")
key = KeyfileKey.create(repository, self.MockArgs())
chunk = b'ABC'
id = key.id_hash(chunk)
data = key.encrypt(id, chunk)
assert key.cipher.extract_iv(data) == 0x2000
assert key.decrypt(id, data) == chunk
def test_keyfile_kfenv(self, tmpdir, monkeypatch):
keyfile = tmpdir.join('keyfile')
monkeypatch.setenv('BORG_KEY_FILE', str(keyfile))
monkeypatch.setenv('BORG_PASSPHRASE', 'testkf')
assert not keyfile.exists()
key = KeyfileKey.create(self.MockRepository(), self.MockArgs())
key = CHPOKeyfileKey.create(self.MockRepository(), self.MockArgs())
assert keyfile.exists()
chunk = b'ABC'
chunk_id = key.id_hash(chunk)
chunk_cdata = key.encrypt(chunk_id, chunk)
key = KeyfileKey.detect(self.MockRepository(), chunk_cdata)
key = CHPOKeyfileKey.detect(self.MockRepository(), chunk_cdata)
assert chunk == key.decrypt(chunk_id, chunk_cdata)
keyfile.remove()
with pytest.raises(FileNotFoundError):
KeyfileKey.detect(self.MockRepository(), chunk_cdata)
CHPOKeyfileKey.detect(self.MockRepository(), chunk_cdata)
def test_keyfile2(self, monkeypatch, keys_dir):
with keys_dir.join('keyfile').open('w') as fd:
@ -275,7 +265,7 @@ class TestTAM:
@pytest.fixture
def key(self, monkeypatch):
monkeypatch.setenv('BORG_PASSPHRASE', 'test')
return KeyfileKey.create(TestKey.MockRepository(), TestKey.MockArgs())
return CHPOKeyfileKey.create(TestKey.MockRepository(), TestKey.MockArgs())
def test_unpack_future(self, key):
blob = b'\xc1\xc1\xc1\xc1foobar'
@ -385,7 +375,7 @@ class TestTAM:
def test_decrypt_key_file_unsupported_algorithm():
"""We will add more algorithms in the future. We should raise a helpful error."""
key = KeyfileKey(None)
key = CHPOKeyfileKey(None)
encrypted = msgpack.packb({
'algorithm': 'THIS ALGORITHM IS NOT SUPPORTED',
'version': 1,
@ -397,7 +387,7 @@ def test_decrypt_key_file_unsupported_algorithm():
def test_decrypt_key_file_v2_is_unsupported():
"""There may eventually be a version 2 of the format. For now we should raise a helpful error."""
key = KeyfileKey(None)
key = CHPOKeyfileKey(None)
encrypted = msgpack.packb({
'version': 2,
})
@ -406,8 +396,7 @@ def test_decrypt_key_file_v2_is_unsupported():
key.decrypt_key_file(encrypted, "hello, pass phrase")
@pytest.mark.parametrize('cli_argument, expected_algorithm', KEY_ALGORITHMS.items())
def test_key_file_roundtrip(monkeypatch, cli_argument, expected_algorithm):
def test_key_file_roundtrip(monkeypatch):
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}
@ -415,10 +404,10 @@ def test_key_file_roundtrip(monkeypatch, cli_argument, expected_algorithm):
repository = MagicMock(id=b'repository_id')
monkeypatch.setenv('BORG_PASSPHRASE', "hello, pass phrase")
save_me = RepoKey.create(repository, args=MagicMock(key_algorithm=cli_argument))
save_me = AESOCBRepoKey.create(repository, args=MagicMock(key_algorithm='argon2'))
saved = repository.save_key.call_args.args[0]
repository.load_key.return_value = saved
load_me = RepoKey.detect(repository, manifest_data=None)
load_me = AESOCBRepoKey.detect(repository, manifest_data=None)
assert to_dict(load_me) == to_dict(save_me)
assert msgpack.unpackb(a2b_base64(saved))['algorithm'] == expected_algorithm
assert msgpack.unpackb(a2b_base64(saved))['algorithm'] == KEY_ALGORITHMS['argon2']