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:
Thomas Waldmann 2022-03-05 23:38:58 +01:00
parent e4e243be0b
commit 37f237d3e0
6 changed files with 14 additions and 156 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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