mirror of
https://github.com/borgbackup/borg.git
synced 2025-02-22 14:11:27 +00:00
Merge pull request #2503 from enkore/f/authenticated-pp
key: authenticated mode = unencrypted
This commit is contained in:
commit
1dd53f0e03
4 changed files with 70 additions and 8 deletions
|
@ -133,13 +133,24 @@ Version 1.1.0b6 (unreleased)
|
||||||
|
|
||||||
Compatibility notes:
|
Compatibility notes:
|
||||||
|
|
||||||
- Repositories in a repokey mode with a blank passphrase are now treated
|
- Repositories in the "repokey" and "repokey-blake2" modes with an empty passphrase
|
||||||
as unencrypted repositories for security checks
|
are now treated as unencrypted repositories for security checks
|
||||||
(e.g. BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK).
|
(e.g. BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK).
|
||||||
- Running "borg init" via a "borg serve --append-only" server will *not* create
|
- Running "borg init" via a "borg serve --append-only" server will *not* create
|
||||||
an append-only repository anymore. Use "borg init --append-only" to initialize
|
an append-only repository anymore. Use "borg init --append-only" to initialize
|
||||||
an append-only repository.
|
an append-only repository.
|
||||||
|
|
||||||
|
Repositories in the "authenticated" mode are now treated as the unencrypted repositories
|
||||||
|
they are.
|
||||||
|
|
||||||
|
Previously there would be no prompts nor messages if an unknown repository
|
||||||
|
in one of these modes with an empty passphrase was encountered. This would
|
||||||
|
allow an attacker to swap a repository, if one assumed that the lack of
|
||||||
|
password prompts was due to a set BORG_PASSPHRASE.
|
||||||
|
|
||||||
|
Since the "trick" does not work if BORG_PASSPHRASE is set, this does generally
|
||||||
|
not affect scripts.
|
||||||
|
|
||||||
Version 1.1.0b5 (2017-04-30)
|
Version 1.1.0b5 (2017-04-30)
|
||||||
----------------------------
|
----------------------------
|
||||||
|
|
||||||
|
|
|
@ -130,7 +130,7 @@ def assert_secure(self, manifest, key, cache):
|
||||||
self.save(manifest, key, cache)
|
self.save(manifest, key, cache)
|
||||||
|
|
||||||
def assert_access_unknown(self, warn_if_unencrypted, key):
|
def assert_access_unknown(self, warn_if_unencrypted, key):
|
||||||
if warn_if_unencrypted and not key.passphrase_protected and not self.known():
|
if warn_if_unencrypted and not key.logically_encrypted and not self.known():
|
||||||
msg = ("Warning: Attempting to access a previously unknown unencrypted repository!\n" +
|
msg = ("Warning: Attempting to access a previously unknown unencrypted repository!\n" +
|
||||||
"Do you want to continue? [yN] ")
|
"Do you want to continue? [yN] ")
|
||||||
if not yes(msg, false_msg="Aborting.", invalid_msg="Invalid answer, aborting.",
|
if not yes(msg, false_msg="Aborting.", invalid_msg="Invalid answer, aborting.",
|
||||||
|
|
|
@ -129,15 +129,31 @@ def tam_required(repository):
|
||||||
|
|
||||||
|
|
||||||
class KeyBase:
|
class KeyBase:
|
||||||
|
# Numeric key type ID, must fit in one byte.
|
||||||
TYPE = None # override in subclasses
|
TYPE = None # override in subclasses
|
||||||
|
|
||||||
# Human-readable name
|
# Human-readable name
|
||||||
NAME = 'UNDEFINED'
|
NAME = 'UNDEFINED'
|
||||||
|
|
||||||
# Name used in command line / API (e.g. borg init --encryption=...)
|
# Name used in command line / API (e.g. borg init --encryption=...)
|
||||||
ARG_NAME = 'UNDEFINED'
|
ARG_NAME = 'UNDEFINED'
|
||||||
|
|
||||||
# Storage type (no key blob storage / keyfile / repo)
|
# Storage type (no key blob storage / keyfile / repo)
|
||||||
STORAGE = KeyBlobStorage.NO_STORAGE
|
STORAGE = KeyBlobStorage.NO_STORAGE
|
||||||
|
|
||||||
|
# Seed for the buzhash chunker (borg.algorithms.chunker.Chunker)
|
||||||
|
# type: int
|
||||||
|
chunk_seed = None
|
||||||
|
|
||||||
|
# Whether this *particular instance* is encrypted from a practical point of view,
|
||||||
|
# i.e. when it's using encryption with a empty passphrase, then
|
||||||
|
# that may be *technically* called encryption, but for all intents and purposes
|
||||||
|
# that's as good as not encrypting in the first place, and this member should be False.
|
||||||
|
#
|
||||||
|
# The empty passphrase is also special because Borg tries it first when no passphrase
|
||||||
|
# was supplied, and if an empty passphrase works, then Borg won't ask for one.
|
||||||
|
logically_encrypted = False
|
||||||
|
|
||||||
def __init__(self, repository):
|
def __init__(self, repository):
|
||||||
self.TYPE_STR = bytes([self.TYPE])
|
self.TYPE_STR = bytes([self.TYPE])
|
||||||
self.repository = repository
|
self.repository = repository
|
||||||
|
@ -234,7 +250,7 @@ class PlaintextKey(KeyBase):
|
||||||
STORAGE = KeyBlobStorage.NO_STORAGE
|
STORAGE = KeyBlobStorage.NO_STORAGE
|
||||||
|
|
||||||
chunk_seed = 0
|
chunk_seed = 0
|
||||||
passphrase_protected = False
|
logically_encrypted = False
|
||||||
|
|
||||||
def __init__(self, repository):
|
def __init__(self, repository):
|
||||||
super().__init__(repository)
|
super().__init__(repository)
|
||||||
|
@ -314,7 +330,8 @@ def id_hash(self, data):
|
||||||
|
|
||||||
|
|
||||||
class AESKeyBase(KeyBase):
|
class AESKeyBase(KeyBase):
|
||||||
"""Common base class shared by KeyfileKey and PassphraseKey
|
"""
|
||||||
|
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)
|
||||||
|
|
||||||
|
@ -330,7 +347,7 @@ class AESKeyBase(KeyBase):
|
||||||
|
|
||||||
MAC = hmac_sha256
|
MAC = hmac_sha256
|
||||||
|
|
||||||
passphrase_protected = True
|
logically_encrypted = True
|
||||||
|
|
||||||
def encrypt(self, chunk):
|
def encrypt(self, chunk):
|
||||||
data = self.compressor.compress(chunk)
|
data = self.compressor.compress(chunk)
|
||||||
|
@ -705,7 +722,7 @@ def get_new_target(self, args):
|
||||||
def load(self, target, passphrase):
|
def load(self, target, passphrase):
|
||||||
# While the repository is encrypted, we consider a repokey repository with a blank
|
# While the repository is encrypted, we consider a repokey repository with a blank
|
||||||
# passphrase an unencrypted repository.
|
# passphrase an unencrypted repository.
|
||||||
self.passphrase_protected = passphrase != ''
|
self.logically_encrypted = passphrase != ''
|
||||||
|
|
||||||
# what we get in target is just a repo location, but we already have the repo obj:
|
# what we get in target is just a repo location, but we already have the repo obj:
|
||||||
target = self.repository
|
target = self.repository
|
||||||
|
@ -717,7 +734,7 @@ def load(self, target, passphrase):
|
||||||
return success
|
return success
|
||||||
|
|
||||||
def save(self, target, passphrase):
|
def save(self, target, passphrase):
|
||||||
self.passphrase_protected = passphrase != ''
|
self.logically_encrypted = passphrase != ''
|
||||||
key_data = self._save(passphrase)
|
key_data = self._save(passphrase)
|
||||||
key_data = key_data.encode('utf-8') # remote repo: msgpack issue #99, giving bytes
|
key_data = key_data.encode('utf-8') # remote repo: msgpack issue #99, giving bytes
|
||||||
target.save_key(key_data)
|
target.save_key(key_data)
|
||||||
|
@ -749,6 +766,29 @@ class AuthenticatedKey(ID_BLAKE2b_256, RepoKey):
|
||||||
ARG_NAME = 'authenticated'
|
ARG_NAME = 'authenticated'
|
||||||
STORAGE = KeyBlobStorage.REPO
|
STORAGE = KeyBlobStorage.REPO
|
||||||
|
|
||||||
|
# It's only authenticated, not encrypted.
|
||||||
|
logically_encrypted = False
|
||||||
|
|
||||||
|
def load(self, target, passphrase):
|
||||||
|
success = super().load(target, passphrase)
|
||||||
|
self.logically_encrypted = False
|
||||||
|
return success
|
||||||
|
|
||||||
|
def save(self, target, passphrase):
|
||||||
|
super().save(target, passphrase)
|
||||||
|
self.logically_encrypted = False
|
||||||
|
|
||||||
|
def extract_nonce(self, payload):
|
||||||
|
# This is called during set-up of the AES ciphers we're not actually using for this
|
||||||
|
# key. Therefore the return value of this method doesn't matter; it's just around
|
||||||
|
# to not have it crash should key identification be run against a very small chunk
|
||||||
|
# by "borg check" when the manifest is lost. (The manifest is always large enough
|
||||||
|
# to have the original method read some garbage from bytes 33-41). (Also, the return
|
||||||
|
# value must be larger than the 41 byte bloat of the original format).
|
||||||
|
if payload[0] != self.TYPE:
|
||||||
|
raise IntegrityError('Manifest: Invalid encryption envelope')
|
||||||
|
return 42
|
||||||
|
|
||||||
def encrypt(self, chunk):
|
def encrypt(self, chunk):
|
||||||
data = self.compressor.compress(chunk)
|
data = self.compressor.compress(chunk)
|
||||||
return b''.join([self.TYPE_STR, data])
|
return b''.join([self.TYPE_STR, data])
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
from ..crypto.key import PlaintextKey, PassphraseKey, KeyfileKey, RepoKey, Blake2KeyfileKey, Blake2RepoKey, \
|
from ..crypto.key import PlaintextKey, PassphraseKey, KeyfileKey, RepoKey, Blake2KeyfileKey, Blake2RepoKey, \
|
||||||
AuthenticatedKey
|
AuthenticatedKey
|
||||||
from ..crypto.key import TAMRequiredError, TAMInvalid, TAMUnsupportedSuiteError, UnsupportedManifestError
|
from ..crypto.key import TAMRequiredError, TAMInvalid, TAMUnsupportedSuiteError, UnsupportedManifestError
|
||||||
|
from ..crypto.key import identify_key
|
||||||
from ..crypto.low_level import bytes_to_long, num_aes_blocks
|
from ..crypto.low_level import bytes_to_long, num_aes_blocks
|
||||||
from ..helpers import IntegrityError
|
from ..helpers import IntegrityError
|
||||||
from ..helpers import Location
|
from ..helpers import Location
|
||||||
|
@ -224,6 +225,16 @@ def test_decrypt_integrity(self, monkeypatch, keys_dir):
|
||||||
id[12] = 0
|
id[12] = 0
|
||||||
key.decrypt(id, data)
|
key.decrypt(id, data)
|
||||||
|
|
||||||
|
def test_roundtrip(self, key):
|
||||||
|
repository = key.repository
|
||||||
|
plaintext = b'foo'
|
||||||
|
encrypted = key.encrypt(plaintext)
|
||||||
|
identified_key_class = identify_key(encrypted)
|
||||||
|
assert identified_key_class == key.__class__
|
||||||
|
loaded_key = identified_key_class.detect(repository, encrypted)
|
||||||
|
decrypted = loaded_key.decrypt(None, encrypted)
|
||||||
|
assert decrypted == plaintext
|
||||||
|
|
||||||
def test_decrypt_decompress(self, key):
|
def test_decrypt_decompress(self, key):
|
||||||
plaintext = b'123456789'
|
plaintext = b'123456789'
|
||||||
encrypted = key.encrypt(plaintext)
|
encrypted = key.encrypt(plaintext)
|
||||||
|
|
Loading…
Reference in a new issue