distinguish auxiliary file errors
authorPhilipp Gesang <philipp.gesang@intra2net.com>
Thu, 11 May 2017 15:40:21 +0000 (17:40 +0200)
committerThomas Jarosch <thomas.jarosch@intra2net.com>
Mon, 2 Apr 2018 11:34:08 +0000 (13:34 +0200)
Auxiliary files that grow larger than the maximum defined
encrypted file size cause an irrecoverable error because their
fixed IV is being reused. Add a new exception to distinguish this
specific case. Encrypted auxiliary files thus never consist of
more than one object, no on-the-fly continuation is permitted
like with ordinary files.

deltatar/crypto.py
deltatar/tarfile.py
testing/test_deltatar.py

index f2dd19c..782d4af 100755 (executable)
@@ -133,6 +133,11 @@ class InvalidIVFixedPart (Exception):
     pass
 
 
+class InvalidFileCounter (Exception):
+    """IV fixed part not in supplied list."""
+    pass
+
+
 class DuplicateIV (Exception):
     """IV reused."""
     pass
@@ -534,13 +539,13 @@ class Crypto (object):
                                     % (cnt, AES_GCM_IV_CNT_MAX))
         if cnt == AES_GCM_IV_CNT_INFOFILE:
             if self.info_counter_used is True:
-                raise InvalidParameter ("attempted to reuse info file counter "
-                                        "%d: must be unique" % cnt)
+                raise InvalidFileCounter ("attempted to reuse info file "
+                                          "counter %d: must be unique" % cnt)
             self.info_counter_used = True
         elif cnt == AES_GCM_IV_CNT_INDEX:
             if self.index_counter_used is True:
-                raise InvalidParameter ("attempted to reuse index file counter "
-                                        "%d: must be unique" % cnt)
+                raise InvalidFileCounter ("attempted to reuse index file "
+                                          " counter %d: must be unique" % cnt)
             self.index_counter_used = True
         if cnt <= AES_GCM_IV_CNT_MAX:
             self.cnt = cnt
@@ -579,7 +584,10 @@ class Crypto (object):
         if self.enc is None:
             raise RuntimeError ("process: context not initialized")
         self.stats ["in"] += len (buf)
-        out = self.enc.update (buf)
+        try:
+            out = self.enc.update (buf)
+        except cryptography.exceptions.AlreadyFinalized as exn:
+            raise InternalError (exn)
         self.stats ["out"] += len (out)
         return out
 
@@ -687,6 +695,7 @@ class Encrypt (Crypto):
                                         "invalid type %s; please pass an "
                                         "integer instead" % type (counter))
             self.set_object_counter (counter)
+
         self.iv = self.iv_make ()
         if self.paramenc == "aes-gcm":
             self.enc = Cipher \
index ff4b239..4bfc2a9 100644 (file)
@@ -577,7 +577,12 @@ class _Stream:
 
     def __del__(self):
         if hasattr(self, "closed") and not self.closed:
-            self.close()
+            try:
+                self.close()
+            except crypto.InternalError:
+                # context already finalized due to abort but close() tried
+                # to use it
+                pass
 
 
     def next (self, name):
index 9e903db..a7b3d92 100644 (file)
@@ -218,6 +218,44 @@ class DeltaTarTest(BaseTest):
                 assert value == self.md5sum(key)
 
 
+    def test_create_backup_index_max_file_length (self):
+        """
+        Creates a full backup including one file that exceeds the (purposely
+        lowered) upper bound on GCM encrypted objects. This will yield two
+        encrypted objects for one plaintext file.
+
+        Success is verified by splitting the archive at object boundaries and
+        counting the parts.
+        """
+        if self.MODE_COMPRESSES is True:
+            raise SkipTest ("GCM file length test not meaningful with compression.")
+        if self.ENCRYPTION is None:
+            raise SkipTest ("GCM file length applies only to encrypted backups.")
+
+        new_max = 5000
+        crypto._testing_set_PDTCRYPT_MAX_OBJ_SIZE \
+                    ("I am fully aware that this will void my warranty.",
+                     new_max)
+
+        password, paramversion = self.ENCRYPTION
+        deltatar = DeltaTar (mode=self.MODE, password=password,
+                             crypto_paramversion=paramversion,
+                             logger=self.consoleLogger)
+
+        self.hash = dict ()
+        os.makedirs ("source_dir2")
+        for i in range (42):
+            f = "source_dir2/dummy_%rd" % i
+            self.hash [f] = self.create_file (f, i)
+
+        try:
+            deltatar.create_full_backup \
+                    (source_path="source_dir2", backup_path="backup_dir")
+        except crypto.InvalidFileCounter:
+            pass
+        shutil.rmtree ("source_dir2")
+
+
     def test_check_index_checksum(self):
         '''
         Creates a full backup and checks the index' checksum of files