return path
- def initialize_encryption (self, mode):
+ def initialize_encryption (self, mode, strict_validation=True):
+ """
+ :type strict_validation: bool
+ :param strict_validation: Enable strict IV checking in the crypto
+ layer. Should be disabled when dealing with
+ potentially corrupted data.
+ """
password = self.password
key = self.crypto_key
nacl = self.nacl
version=self.crypto_version,
paramversion=self.crypto_paramversion)
if mode == CRYPTO_MODE_DECRYPT:
- return crypto.Decrypt (password=password, key=key)
+ return crypto.Decrypt (password=password, key=key,
+ strict_ivs=strict_validation)
raise Exception ("invalid encryption mode [%r]" % mode)
- def open_auxiliary_file(self, path, mode='r', kind=AUXILIARY_FILE_INDEX):
+ def open_auxiliary_file(self, path, mode='r', kind=AUXILIARY_FILE_INDEX,
+ strict_validation=True):
'''
Given the specified configuration, opens a file for reading or writing,
inheriting the encryption and compression settings from the backup.
if mode == "w":
crypto_ctx = self.initialize_encryption (CRYPTO_MODE_ENCRYPT)
elif mode == "r":
- crypto_ctx = self.initialize_encryption (CRYPTO_MODE_DECRYPT)
+ crypto_ctx = self.initialize_encryption (CRYPTO_MODE_DECRYPT,
+ strict_validation=strict_validation)
if crypto_ctx is not None:
if kind == AUXILIARY_FILE_INFO:
index_sink.close()
- def iterate_index_path(self, index_path):
+ def iterate_index_path(self, index_path, strict_validation=True):
'''
Returns an index iterator. Internally, it uses a classic iterator class.
We do that instead of just yielding so that the iterator object can have
Allows this iterator to be used with the "with" statement
'''
if self.f is None:
- self.f = self.delta_tar.open_auxiliary_file(self.index_path, 'r')
+ self.f = self.delta_tar.open_auxiliary_file \
+ (self.index_path,
+ 'r',
+ strict_validation=strict_validation)
# check index header
j, l_no = self.delta_tar._parse_json_line(self.f, 0)
if j.get("type", '') != 'python-delta-tar-index' or\
if self.delta_tar.password is not None:
decryptor = crypto.Decrypt \
(password=self.delta_tar.password,
- key=self.delta_tar.crypto_key)
+ key=self.delta_tar.crypto_key,
+ strict_ivs=False)
self.tar_obj = tarfile.TarFile.open(self.tar_path,
mode='r' + self.delta_tar.mode,
format=tarfile.GNU_FORMAT,
tarobj.open_volume(volume_path, encryption=encryption)
if self.decryptor is None:
- self.decryptor = self.initialize_encryption (CRYPTO_MODE_DECRYPT)
+ self.decryptor = \
+ self.initialize_encryption (CRYPTO_MODE_DECRYPT,
+ strict_validation=False)
backup_path = os.path.dirname(backup_tar_path)
if not os.path.isabs(backup_path):
def restore_backup(self, target_path, backup_indexes_paths=[],
backup_tar_path=None, restore_callback=None,
- disaster=tarfile.TOLERANCE_STRICT, backup_index=None):
+ disaster=tarfile.TOLERANCE_STRICT, backup_index=None,
+ strict_validation=True):
'''
Restores a backup.
# setup for decrypting payload
if self.decryptor is None:
- self.decryptor = self.initialize_encryption (CRYPTO_MODE_DECRYPT)
+ self.decryptor = \
+ self.initialize_encryption (CRYPTO_MODE_DECRYPT,
+ strict_validation=strict_validation)
if mode == 'tar':
index_it = self.iterate_tar_path(backup_tar_path)
try:
# get iterator from newest index at _data[0]
index1 = helper._data[0]["path"]
- index_it = self.iterate_index_path(index1)
+ index_it = \
+ self.iterate_index_path(index1,
+ strict_validation=strict_validation)
except tarfile.DecryptionError as exn:
self.logger.error("failed to decrypt file [%s]: %s; is this an "
"actual encrypted index file?"
backup_index=backup_index,
disaster=disaster)
+ index_decryptor = helper._data[0]["decryptor"]
dir_it = self._recursive_walk_dir('.')
dir_path_it = self.jsonize_path_iterator(dir_it)
"""
Walk the index, extracting objects in disaster mode. Bad files are
reported along with a reason.
+
+ *Security considerations*: In *recovery mode* the headers of encrypted
+ objects are assumed damaged and GCM tags are not validated so
+ modification of cryptographically relevant parts of the header (more
+ specifically, the initalization vectors) can no longer be detected. If
+ an attacker can manipulate the encrypted backup set and has access to
+ the plaintext of some of the contents, they may be able to obtain the
+ plaintext of other encrypted objects by injecting initialization
+ vectors. For this reason *recovery mode* should only be used to
+ emergency situations and the contents of the resulting files should be
+ validated manually if possible and not be disclosed to untrusted
+ parties.
"""
return self.restore_backup(target_path,
backup_indexes_paths=backup_indexes_paths,
- disaster=tarfile.TOLERANCE_RECOVER)
+ disaster=tarfile.TOLERANCE_RECOVER,
+ strict_validation=False)
def rescue_backup(self, target_path, backup_tar_path,
More aggressive “unfsck” mode: do not rely on the index data as the
files may be corrupt; skim files for header-like information and
attempt to retrieve the data.
+
+ *Security considerations*: As with *recovery mode*, in *rescue mode*
+ the headers of encrypted objects are assumed damaged and GCM tags are
+ not validated so modification of cryptographically relevant parts of
+ the header (more specifically, the initalization vectors) can no longer
+ be detected. If an attacker can manipulate the encrypted backup set and
+ has access to the plaintext of some of the contents, they may be able
+ to obtain the plaintext of other encrypted objects by injecting
+ initialization vectors. For this reason *rescue mode* should only be
+ used to emergency situations and the contents of the resulting files
+ should be validated manually if possible and not be disclosed to
+ untrusted parties.
"""
def gen_volume_name (nvol):
return os.path.join (os.path.dirname (backup_tar_path),
return self.restore_backup(target_path,
backup_index=backup_index,
backup_tar_path=backup_tar_path,
- disaster=tarfile.TOLERANCE_RESCUE)
+ disaster=tarfile.TOLERANCE_RESCUE,
+ strict_validation=False)
def _parse_json_line(self, f, l_no):
self._decryptors = []
self._disaster = disaster
+ # Disable strict checking for linearly increasing IVs when running
+ # in rescue or recover mode.
+ strict_validation = disaster == tarfile.TOLERANCE_STRICT
+
try:
import grp, pwd
except ImportError:
decryptor = None
if self._password is not None:
decryptor = crypto.Decrypt (password=self._password,
- key=self._crypto_key)
+ key=self._crypto_key,
+ strict_ivs=strict_validation)
# make paths absolute to avoid cwd problems
if not os.path.isabs(index):
self._deltatar.logger.warning('tarfile: %s' % e)
@staticmethod
- def new_volume_handler(deltarobj, cwd, is_full, backup_path, encryption, tarobj, base_name, volume_number):
+ def new_volume_handler(deltarobj, cwd, is_full, backup_path, decryptor, tarobj, base_name, volume_number):
'''
- Handles the new volumes
+ Set up a new volume and perform the tasks necessary for transitioning
+ to the next one.
'''
volume_name = deltarobj.volume_name_func(backup_path, is_full,
volume_number, guess_name=True)
# we convert relative paths into absolute because CWD is changed
if not os.path.isabs(volume_path):
volume_path = os.path.join(cwd, volume_path)
- tarobj.open_volume(volume_path, encryption=encryption)
+
+ tarobj.open_volume(volume_path, encryption=decryptor)
def restore_file(self, file_data, index_data, path, l_no, unprefixed_path):
'''