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