mirror of https://github.com/borgbackup/borg.git
remove PassphraseKey code and borg key migrate-to-repokey command
"passphrase" encryption mode repos can not be created since borg 1.0. back then, users were advised to switch existing repos of that type to repokey mode using the "borg key migrate-to-repokey" command. that command is still available in borg 1.0, 1.1 and 1.2, but not any more in borg >= 1.3. while we still might see the PassphraseKey.TYPE byte in old repos, it is handled by the RepoKey code since borg 1.0.
This commit is contained in:
parent
e4e243be0b
commit
37f237d3e0
|
@ -1289,8 +1289,8 @@ There are some caveats:
|
||||||
This means that data added by Borg won't deduplicate with the existing data
|
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.
|
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
|
- Repositories in "passphrase" mode *must* be migrated to "repokey" mode using
|
||||||
:ref:`borg_key_migrate-to-repokey`. Borg does not support the "passphrase" mode
|
"borg key migrate-to-repokey" (only available in borg <= 1.2.x). Borg does not
|
||||||
any other way.
|
support the "passphrase" mode in any other way.
|
||||||
|
|
||||||
Why is my backup bigger than with attic?
|
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.
|
encryption scheme from encryption and message authentication primitives.
|
||||||
|
|
||||||
Every operation (encryption, MAC / authentication, chunk ID derivation)
|
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
|
Borg does not support unauthenticated encryption -- only authenticated encryption
|
||||||
schemes are supported. No unauthenticated encryption schemes will be added
|
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
|
clients, the security database could be synchronized between them over
|
||||||
said trusted channel. This is not part of Borg's functionality.
|
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:
|
.. _key_encryption:
|
||||||
|
|
||||||
Offline key security
|
Offline key security
|
||||||
|
|
|
@ -14,17 +14,3 @@ Examples
|
||||||
converting borg 0.xx to borg current
|
converting borg 0.xx to borg current
|
||||||
no key file found for repository
|
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 .cache import Cache, assert_secure, SecurityManager
|
||||||
from .constants import * # NOQA
|
from .constants import * # NOQA
|
||||||
from .compress import CompressionSpec
|
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 .crypto.keymanager import KeyManager
|
||||||
from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, EXIT_SIGNAL_BASE
|
from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, EXIT_SIGNAL_BASE
|
||||||
from .helpers import Error, NoManifestError, set_ec
|
from .helpers import Error, NoManifestError, set_ec
|
||||||
|
@ -400,22 +400,6 @@ class Archiver:
|
||||||
manager.import_keyfile(args)
|
manager.import_keyfile(args)
|
||||||
return EXIT_SUCCESS
|
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):
|
def do_benchmark_crud(self, args):
|
||||||
"""Benchmark Create, Read, Update, Delete for archives."""
|
"""Benchmark Create, Read, Update, Delete for archives."""
|
||||||
def measurement_run(repo, path):
|
def measurement_run(repo, path):
|
||||||
|
@ -4266,33 +4250,6 @@ class Archiver:
|
||||||
subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='',
|
subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='',
|
||||||
type=location_validator(archive=False))
|
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
|
# borg list
|
||||||
list_epilog = process_epilog("""
|
list_epilog = process_epilog("""
|
||||||
This command lists the contents of a repository or an archive.
|
This command lists the contents of a repository or an archive.
|
||||||
|
|
|
@ -119,9 +119,7 @@ def key_argument_names():
|
||||||
def identify_key(manifest_data):
|
def identify_key(manifest_data):
|
||||||
key_type = manifest_data[0]
|
key_type = manifest_data[0]
|
||||||
if key_type == PassphraseKey.TYPE:
|
if key_type == PassphraseKey.TYPE:
|
||||||
# we just dispatch to repokey mode and assume the passphrase was migrated to a repokey.
|
return RepoKey # see comment in PassphraseKey class.
|
||||||
# see also comment in PassphraseKey class.
|
|
||||||
return RepoKey
|
|
||||||
|
|
||||||
for key in AVAILABLE_KEY_TYPES:
|
for key in AVAILABLE_KEY_TYPES:
|
||||||
if key.TYPE == key_type:
|
if key.TYPE == key_type:
|
||||||
|
@ -327,8 +325,7 @@ class ID_BLAKE2b_256:
|
||||||
def id_hash(self, data):
|
def id_hash(self, data):
|
||||||
return blake2b_256(self.id_key, data)
|
return blake2b_256(self.id_key, data)
|
||||||
|
|
||||||
def init_from_random_data(self, data=None):
|
def init_from_random_data(self):
|
||||||
assert data is None # PassphraseKey is the only caller using *data*
|
|
||||||
super().init_from_random_data()
|
super().init_from_random_data()
|
||||||
self.enc_hmac_key = random_blake2b_256_key()
|
self.enc_hmac_key = random_blake2b_256_key()
|
||||||
self.id_key = random_blake2b_256_key()
|
self.id_key = random_blake2b_256_key()
|
||||||
|
@ -347,8 +344,6 @@ class ID_HMAC_SHA_256:
|
||||||
|
|
||||||
class AESKeyBase(KeyBase):
|
class AESKeyBase(KeyBase):
|
||||||
"""
|
"""
|
||||||
Common base class shared by KeyfileKey and PassphraseKey
|
|
||||||
|
|
||||||
Chunks are encrypted using 256bit AES in Counter Mode (CTR)
|
Chunks are encrypted using 256bit AES in Counter Mode (CTR)
|
||||||
|
|
||||||
Payload layout: TYPE(1) + HMAC(32) + NONCE(8) + CIPHERTEXT
|
Payload layout: TYPE(1) + HMAC(32) + NONCE(8) + CIPHERTEXT
|
||||||
|
@ -386,8 +381,7 @@ class AESKeyBase(KeyBase):
|
||||||
self.assert_id(id, data)
|
self.assert_id(id, data)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def init_from_random_data(self, data=None):
|
def init_from_random_data(self):
|
||||||
if data is None:
|
|
||||||
data = os.urandom(100)
|
data = os.urandom(100)
|
||||||
self.enc_key = data[0:32]
|
self.enc_key = data[0:32]
|
||||||
self.enc_hmac_key = data[32:64]
|
self.enc_hmac_key = data[32:64]
|
||||||
|
@ -523,59 +517,13 @@ class Passphrase(str):
|
||||||
return pbkdf2_hmac('sha256', self.encode('utf-8'), salt, iterations, length)
|
return pbkdf2_hmac('sha256', self.encode('utf-8'), salt, iterations, length)
|
||||||
|
|
||||||
|
|
||||||
class PassphraseKey(ID_HMAC_SHA_256, AESKeyBase):
|
class PassphraseKey:
|
||||||
# This mode was killed in borg 1.0, see: https://github.com/borgbackup/borg/issues/97
|
# this is only a stub, repos with this mode could not be created any more since borg 1.0, see #97.
|
||||||
# Reasons:
|
# in borg 1.3 all of its code and also the "borg key migrate-to-repokey" command was removed.
|
||||||
# - you can never ever change your passphrase for existing repos.
|
# if you still need to, you can use "borg key migrate-to-repokey" with borg 1.0, 1.1 and 1.2.
|
||||||
# - you can never ever use a different iterations count for existing repos.
|
# Nowadays, we just dispatch this to RepoKey and assume the passphrase was migrated to a repokey.
|
||||||
# "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.
|
|
||||||
TYPE = 0x01
|
TYPE = 0x01
|
||||||
NAME = 'passphrase'
|
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):
|
class KeyfileKeyBase(AESKeyBase):
|
||||||
|
@ -888,7 +836,6 @@ class Blake2AuthenticatedKey(ID_BLAKE2b_256, AuthenticatedKeyBase):
|
||||||
|
|
||||||
AVAILABLE_KEY_TYPES = (
|
AVAILABLE_KEY_TYPES = (
|
||||||
PlaintextKey,
|
PlaintextKey,
|
||||||
PassphraseKey,
|
|
||||||
KeyfileKey, RepoKey, AuthenticatedKey,
|
KeyfileKey, RepoKey, AuthenticatedKey,
|
||||||
Blake2KeyfileKey, Blake2RepoKey, Blake2AuthenticatedKey,
|
Blake2KeyfileKey, Blake2RepoKey, Blake2AuthenticatedKey,
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,7 +7,7 @@ from binascii import hexlify, unhexlify
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ..crypto.key import Passphrase, PasswordRetriesExceeded, bin_to_hex
|
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
|
Blake2KeyfileKey, Blake2RepoKey, Blake2AuthenticatedKey
|
||||||
from ..crypto.key import ID_HMAC_SHA_256, ID_BLAKE2b_256
|
from ..crypto.key import ID_HMAC_SHA_256, ID_BLAKE2b_256
|
||||||
from ..crypto.key import TAMRequiredError, TAMInvalid, TAMUnsupportedSuiteError, UnsupportedManifestError
|
from ..crypto.key import TAMRequiredError, TAMInvalid, TAMUnsupportedSuiteError, UnsupportedManifestError
|
||||||
|
@ -182,31 +182,6 @@ class TestKey:
|
||||||
key = Blake2KeyfileKey.detect(self.MockRepository(), self.keyfile_blake2_cdata)
|
key = Blake2KeyfileKey.detect(self.MockRepository(), self.keyfile_blake2_cdata)
|
||||||
assert key.decrypt(self.keyfile_blake2_id, self.keyfile_blake2_cdata) == b'payload'
|
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):
|
def _corrupt_byte(self, key, data, offset):
|
||||||
data = bytearray(data)
|
data = bytearray(data)
|
||||||
data[offset] ^= 1
|
data[offset] ^= 1
|
||||||
|
|
Loading…
Reference in New Issue