mirror of
https://github.com/borgbackup/borg.git
synced 2024-11-04 00:05:45 +03:00
Merge pull request #6412 from ThomasWaldmann/remove-passphrasekey
remove PassphraseKey code and borg key migrate-to-repokey command
This commit is contained in:
commit
0714339d3f
@ -1289,8 +1289,8 @@ There are some caveats:
|
||||
This means that data added by Borg won't deduplicate with the existing data
|
||||
stored by Attic. The effect is lessened if the files cache is used with Borg.
|
||||
- Repositories in "passphrase" mode *must* be migrated to "repokey" mode using
|
||||
:ref:`borg_key_migrate-to-repokey`. Borg does not support the "passphrase" mode
|
||||
any other way.
|
||||
"borg key migrate-to-repokey" (only available in borg <= 1.2.x). Borg does not
|
||||
support the "passphrase" mode in any other way.
|
||||
|
||||
Why is my backup bigger than with attic?
|
||||
----------------------------------------
|
||||
|
@ -129,7 +129,7 @@ which is generally seen as the most robust way to create an authenticated
|
||||
encryption scheme from encryption and message authentication primitives.
|
||||
|
||||
Every operation (encryption, MAC / authentication, chunk ID derivation)
|
||||
uses independent, random keys generated by `os.urandom`_ [#]_.
|
||||
uses independent, random keys generated by `os.urandom`_.
|
||||
|
||||
Borg does not support unauthenticated encryption -- only authenticated encryption
|
||||
schemes are supported. No unauthenticated encryption schemes will be added
|
||||
@ -208,13 +208,6 @@ untrusted, but a trusted synchronization channel exists between
|
||||
clients, the security database could be synchronized between them over
|
||||
said trusted channel. This is not part of Borg's functionality.
|
||||
|
||||
.. [#] Using the :ref:`borg key migrate-to-repokey <borg_key_migrate-to-repokey>`
|
||||
command a user can convert repositories created using Attic in "passphrase"
|
||||
mode to "repokey" mode. In this case the keys were directly derived from
|
||||
the user's passphrase at some point using PBKDF2.
|
||||
|
||||
Borg does not support "passphrase" mode otherwise any more.
|
||||
|
||||
.. _key_encryption:
|
||||
|
||||
Offline key security
|
||||
|
@ -14,17 +14,3 @@ Examples
|
||||
converting borg 0.xx to borg current
|
||||
no key file found for repository
|
||||
|
||||
.. _borg_key_migrate-to-repokey:
|
||||
|
||||
Upgrading a passphrase encrypted attic repo
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
attic offered a "passphrase" encryption mode, but this was removed in borg 1.0
|
||||
and replaced by the "repokey" mode (which stores the passphrase-protected
|
||||
encryption key into the repository config).
|
||||
|
||||
Thus, to upgrade a "passphrase" attic repo to a "repokey" borg repo, 2 steps
|
||||
are needed, in this order:
|
||||
|
||||
- borg upgrade repo
|
||||
- borg key migrate-to-repokey repo
|
||||
|
@ -44,7 +44,7 @@ try:
|
||||
from .cache import Cache, assert_secure, SecurityManager
|
||||
from .constants import * # NOQA
|
||||
from .compress import CompressionSpec
|
||||
from .crypto.key import key_creator, key_argument_names, tam_required_file, tam_required, RepoKey, PassphraseKey
|
||||
from .crypto.key import key_creator, key_argument_names, tam_required_file, tam_required, RepoKey
|
||||
from .crypto.keymanager import KeyManager
|
||||
from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, EXIT_SIGNAL_BASE
|
||||
from .helpers import Error, NoManifestError, set_ec
|
||||
@ -400,22 +400,6 @@ class Archiver:
|
||||
manager.import_keyfile(args)
|
||||
return EXIT_SUCCESS
|
||||
|
||||
@with_repository(manifest=False)
|
||||
def do_migrate_to_repokey(self, args, repository):
|
||||
"""Migrate passphrase -> repokey"""
|
||||
manifest_data = repository.get(Manifest.MANIFEST_ID)
|
||||
key_old = PassphraseKey.detect(repository, manifest_data)
|
||||
key_new = RepoKey(repository)
|
||||
key_new.target = repository
|
||||
key_new.repository_id = repository.id
|
||||
key_new.enc_key = key_old.enc_key
|
||||
key_new.enc_hmac_key = key_old.enc_hmac_key
|
||||
key_new.id_key = key_old.id_key
|
||||
key_new.chunk_seed = key_old.chunk_seed
|
||||
key_new.change_passphrase() # option to change key protection passphrase, save
|
||||
logger.info('Key updated')
|
||||
return EXIT_SUCCESS
|
||||
|
||||
def do_benchmark_crud(self, args):
|
||||
"""Benchmark Create, Read, Update, Delete for archives."""
|
||||
def measurement_run(repo, path):
|
||||
@ -4266,33 +4250,6 @@ class Archiver:
|
||||
subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='',
|
||||
type=location_validator(archive=False))
|
||||
|
||||
migrate_to_repokey_epilog = process_epilog("""
|
||||
This command migrates a repository from passphrase mode (removed in Borg 1.0)
|
||||
to repokey mode.
|
||||
|
||||
You will be first asked for the repository passphrase (to open it in passphrase
|
||||
mode). This is the same passphrase as you used to use for this repo before 1.0.
|
||||
|
||||
It will then derive the different secrets from this passphrase.
|
||||
|
||||
Then you will be asked for a new passphrase (twice, for safety). This
|
||||
passphrase will be used to protect the repokey (which contains these same
|
||||
secrets in encrypted form). You may use the same passphrase as you used to
|
||||
use, but you may also use a different one.
|
||||
|
||||
After migrating to repokey mode, you can change the passphrase at any time.
|
||||
But please note: the secrets will always stay the same and they could always
|
||||
be derived from your (old) passphrase-mode passphrase.
|
||||
""")
|
||||
subparser = key_parsers.add_parser('migrate-to-repokey', parents=[common_parser], add_help=False,
|
||||
description=self.do_migrate_to_repokey.__doc__,
|
||||
epilog=migrate_to_repokey_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help='migrate passphrase-mode repository to repokey')
|
||||
subparser.set_defaults(func=self.do_migrate_to_repokey)
|
||||
subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='',
|
||||
type=location_validator(archive=False))
|
||||
|
||||
# borg list
|
||||
list_epilog = process_epilog("""
|
||||
This command lists the contents of a repository or an archive.
|
||||
|
@ -119,9 +119,7 @@ def key_argument_names():
|
||||
def identify_key(manifest_data):
|
||||
key_type = manifest_data[0]
|
||||
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
|
||||
return RepoKey # see comment in PassphraseKey class.
|
||||
|
||||
for key in AVAILABLE_KEY_TYPES:
|
||||
if key.TYPE == key_type:
|
||||
@ -327,8 +325,7 @@ class ID_BLAKE2b_256:
|
||||
def id_hash(self, data):
|
||||
return blake2b_256(self.id_key, data)
|
||||
|
||||
def init_from_random_data(self, data=None):
|
||||
assert data is None # PassphraseKey is the only caller using *data*
|
||||
def init_from_random_data(self):
|
||||
super().init_from_random_data()
|
||||
self.enc_hmac_key = random_blake2b_256_key()
|
||||
self.id_key = random_blake2b_256_key()
|
||||
@ -347,8 +344,6 @@ class ID_HMAC_SHA_256:
|
||||
|
||||
class AESKeyBase(KeyBase):
|
||||
"""
|
||||
Common base class shared by KeyfileKey and PassphraseKey
|
||||
|
||||
Chunks are encrypted using 256bit AES in Counter Mode (CTR)
|
||||
|
||||
Payload layout: TYPE(1) + HMAC(32) + NONCE(8) + CIPHERTEXT
|
||||
@ -386,9 +381,8 @@ class AESKeyBase(KeyBase):
|
||||
self.assert_id(id, data)
|
||||
return data
|
||||
|
||||
def init_from_random_data(self, data=None):
|
||||
if data is None:
|
||||
data = os.urandom(100)
|
||||
def init_from_random_data(self):
|
||||
data = os.urandom(100)
|
||||
self.enc_key = data[0:32]
|
||||
self.enc_hmac_key = data[32:64]
|
||||
self.id_key = data[64:96]
|
||||
@ -523,59 +517,13 @@ class Passphrase(str):
|
||||
return pbkdf2_hmac('sha256', self.encode('utf-8'), salt, iterations, length)
|
||||
|
||||
|
||||
class PassphraseKey(ID_HMAC_SHA_256, AESKeyBase):
|
||||
# This mode was killed in borg 1.0, see: https://github.com/borgbackup/borg/issues/97
|
||||
# Reasons:
|
||||
# - you can never ever change your passphrase for existing repos.
|
||||
# - you can never ever use a different iterations count for existing repos.
|
||||
# "Killed" means:
|
||||
# - there is no automatic dispatch to this class via type byte
|
||||
# - --encryption=passphrase is an invalid argument now
|
||||
# This class is kept for a while to support migration from passphrase to repokey mode.
|
||||
class PassphraseKey:
|
||||
# this is only a stub, repos with this mode could not be created any more since borg 1.0, see #97.
|
||||
# in borg 1.3 all of its code and also the "borg key migrate-to-repokey" command was removed.
|
||||
# if you still need to, you can use "borg key migrate-to-repokey" with borg 1.0, 1.1 and 1.2.
|
||||
# Nowadays, we just dispatch this to RepoKey and assume the passphrase was migrated to a repokey.
|
||||
TYPE = 0x01
|
||||
NAME = 'passphrase'
|
||||
ARG_NAME = None
|
||||
STORAGE = KeyBlobStorage.NO_STORAGE
|
||||
|
||||
iterations = 100000 # must not be changed ever!
|
||||
|
||||
@classmethod
|
||||
def create(cls, repository, args):
|
||||
key = cls(repository)
|
||||
logger.warning('WARNING: "passphrase" mode is unsupported since borg 1.0.')
|
||||
passphrase = Passphrase.new(allow_empty=False)
|
||||
key.init(repository, passphrase)
|
||||
return key
|
||||
|
||||
@classmethod
|
||||
def detect(cls, repository, manifest_data):
|
||||
prompt = 'Enter passphrase for %s: ' % repository._location.canonical_path()
|
||||
key = cls(repository)
|
||||
passphrase = Passphrase.env_passphrase()
|
||||
if passphrase is None:
|
||||
passphrase = Passphrase.getpass(prompt)
|
||||
for retry in range(1, 3):
|
||||
key.init(repository, passphrase)
|
||||
try:
|
||||
key.decrypt(None, manifest_data)
|
||||
key.init_ciphers(manifest_data)
|
||||
key._passphrase = passphrase
|
||||
return key
|
||||
except IntegrityError:
|
||||
passphrase = Passphrase.getpass(prompt)
|
||||
else:
|
||||
raise PasswordRetriesExceeded
|
||||
|
||||
def change_passphrase(self):
|
||||
class ImmutablePassphraseError(Error):
|
||||
"""The passphrase for this encryption key type can't be changed."""
|
||||
|
||||
raise ImmutablePassphraseError
|
||||
|
||||
def init(self, repository, passphrase):
|
||||
self.init_from_random_data(passphrase.kdf(repository.id, self.iterations, 100))
|
||||
self.init_ciphers()
|
||||
self.tam_required = False
|
||||
|
||||
|
||||
class KeyfileKeyBase(AESKeyBase):
|
||||
@ -888,7 +836,6 @@ class Blake2AuthenticatedKey(ID_BLAKE2b_256, AuthenticatedKeyBase):
|
||||
|
||||
AVAILABLE_KEY_TYPES = (
|
||||
PlaintextKey,
|
||||
PassphraseKey,
|
||||
KeyfileKey, RepoKey, AuthenticatedKey,
|
||||
Blake2KeyfileKey, Blake2RepoKey, Blake2AuthenticatedKey,
|
||||
)
|
||||
|
@ -7,7 +7,7 @@ from binascii import hexlify, unhexlify
|
||||
import pytest
|
||||
|
||||
from ..crypto.key import Passphrase, PasswordRetriesExceeded, bin_to_hex
|
||||
from ..crypto.key import PlaintextKey, PassphraseKey, AuthenticatedKey, RepoKey, KeyfileKey, \
|
||||
from ..crypto.key import PlaintextKey, AuthenticatedKey, RepoKey, KeyfileKey, \
|
||||
Blake2KeyfileKey, Blake2RepoKey, Blake2AuthenticatedKey
|
||||
from ..crypto.key import ID_HMAC_SHA_256, ID_BLAKE2b_256
|
||||
from ..crypto.key import TAMRequiredError, TAMInvalid, TAMUnsupportedSuiteError, UnsupportedManifestError
|
||||
@ -182,31 +182,6 @@ class TestKey:
|
||||
key = Blake2KeyfileKey.detect(self.MockRepository(), self.keyfile_blake2_cdata)
|
||||
assert key.decrypt(self.keyfile_blake2_id, self.keyfile_blake2_cdata) == b'payload'
|
||||
|
||||
def test_passphrase(self, keys_dir, monkeypatch):
|
||||
monkeypatch.setenv('BORG_PASSPHRASE', 'test')
|
||||
key = PassphraseKey.create(self.MockRepository(), None)
|
||||
assert key.cipher.next_iv() == 0
|
||||
assert hexlify(key.id_key) == b'793b0717f9d8fb01c751a487e9b827897ceea62409870600013fbc6b4d8d7ca6'
|
||||
assert hexlify(key.enc_hmac_key) == b'b885a05d329a086627412a6142aaeb9f6c54ab7950f996dd65587251f6bc0901'
|
||||
assert hexlify(key.enc_key) == b'2ff3654c6daf7381dbbe718d2b20b4f1ea1e34caa6cc65f6bb3ac376b93fed2a'
|
||||
assert key.chunk_seed == -775740477
|
||||
manifest = key.encrypt(b'ABC')
|
||||
assert key.cipher.extract_iv(manifest) == 0
|
||||
manifest2 = key.encrypt(b'ABC')
|
||||
assert manifest != manifest2
|
||||
assert key.decrypt(None, manifest) == key.decrypt(None, manifest2)
|
||||
assert key.cipher.extract_iv(manifest2) == 1
|
||||
iv = key.cipher.extract_iv(manifest)
|
||||
key2 = PassphraseKey.detect(self.MockRepository(), manifest)
|
||||
assert key2.cipher.next_iv() == iv + key2.cipher.block_count(len(manifest))
|
||||
assert key.id_key == key2.id_key
|
||||
assert key.enc_hmac_key == key2.enc_hmac_key
|
||||
assert key.enc_key == key2.enc_key
|
||||
assert key.chunk_seed == key2.chunk_seed
|
||||
chunk = b'foo'
|
||||
assert hexlify(key.id_hash(chunk)) == b'818217cf07d37efad3860766dcdf1d21e401650fed2d76ed1d797d3aae925990'
|
||||
assert chunk == key2.decrypt(key2.id_hash(chunk), key.encrypt(chunk))
|
||||
|
||||
def _corrupt_byte(self, key, data, offset):
|
||||
data = bytearray(data)
|
||||
data[offset] ^= 1
|
||||
|
Loading…
Reference in New Issue
Block a user