Merge pull request #2503 from enkore/f/authenticated-pp

key: authenticated mode = unencrypted
This commit is contained in:
enkore 2017-05-12 21:37:07 +02:00 committed by GitHub
commit 1dd53f0e03
4 changed files with 70 additions and 8 deletions

View File

@ -133,13 +133,24 @@ Version 1.1.0b6 (unreleased)
Compatibility notes:
- Repositories in a repokey mode with a blank passphrase are now treated
as unencrypted repositories for security checks
- Repositories in the "repokey" and "repokey-blake2" modes with an empty passphrase
are now treated as unencrypted repositories for security checks
(e.g. BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK).
- 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.
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)
----------------------------

View File

@ -130,7 +130,7 @@ class SecurityManager:
self.save(manifest, key, cache)
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" +
"Do you want to continue? [yN] ")
if not yes(msg, false_msg="Aborting.", invalid_msg="Invalid answer, aborting.",

View File

@ -129,15 +129,31 @@ def tam_required(repository):
class KeyBase:
# Numeric key type ID, must fit in one byte.
TYPE = None # override in subclasses
# Human-readable name
NAME = 'UNDEFINED'
# Name used in command line / API (e.g. borg init --encryption=...)
ARG_NAME = 'UNDEFINED'
# Storage type (no key blob storage / keyfile / repo)
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):
self.TYPE_STR = bytes([self.TYPE])
self.repository = repository
@ -234,7 +250,7 @@ class PlaintextKey(KeyBase):
STORAGE = KeyBlobStorage.NO_STORAGE
chunk_seed = 0
passphrase_protected = False
logically_encrypted = False
def __init__(self, repository):
super().__init__(repository)
@ -314,7 +330,8 @@ class ID_HMAC_SHA_256:
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)
@ -330,7 +347,7 @@ class AESKeyBase(KeyBase):
MAC = hmac_sha256
passphrase_protected = True
logically_encrypted = True
def encrypt(self, chunk):
data = self.compressor.compress(chunk)
@ -705,7 +722,7 @@ class RepoKey(ID_HMAC_SHA_256, KeyfileKeyBase):
def load(self, target, passphrase):
# While the repository is encrypted, we consider a repokey repository with a blank
# 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:
target = self.repository
@ -717,7 +734,7 @@ class RepoKey(ID_HMAC_SHA_256, KeyfileKeyBase):
return success
def save(self, target, passphrase):
self.passphrase_protected = passphrase != ''
self.logically_encrypted = passphrase != ''
key_data = self._save(passphrase)
key_data = key_data.encode('utf-8') # remote repo: msgpack issue #99, giving bytes
target.save_key(key_data)
@ -749,6 +766,29 @@ class AuthenticatedKey(ID_BLAKE2b_256, RepoKey):
ARG_NAME = 'authenticated'
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):
data = self.compressor.compress(chunk)
return b''.join([self.TYPE_STR, data])

View File

@ -11,6 +11,7 @@ from ..crypto.key import Passphrase, PasswordRetriesExceeded, bin_to_hex
from ..crypto.key import PlaintextKey, PassphraseKey, KeyfileKey, RepoKey, Blake2KeyfileKey, Blake2RepoKey, \
AuthenticatedKey
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 ..helpers import IntegrityError
from ..helpers import Location
@ -224,6 +225,16 @@ class TestKey:
id[12] = 0
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):
plaintext = b'123456789'
encrypted = key.encrypt(plaintext)