add header corruption tests
authorPhilipp Gesang <philipp.gesang@intra2net.com>
Thu, 10 Aug 2017 15:01:42 +0000 (17:01 +0200)
committerThomas Jarosch <thomas.jarosch@intra2net.com>
Mon, 2 Apr 2018 11:34:09 +0000 (13:34 +0200)
We hit them where it hurts:

    * for compressed backups, flip a bit in the magic;
    * for encrypted backups, flip a bit in the tag.

In either case, normal restore must fail, and disaster recovery
will be incomplete.

runtests.py
testing/test_recover.py

index 3605964..d1ed888 100755 (executable)
@@ -22,7 +22,12 @@ import unittest
 from testing.test_crypto import HeaderTest, AESGCMTest
 from testing.test_multivol import MultivolGnuFormatTest, MultivolPaxFormatTest
 from testing.test_concat_compress import ConcatCompressTest
-from testing.test_recover import RecoverTest, RecoverGZTest, RecoverGZAESTest
+from testing.test_recover import \
+      RecoverCorruptPayloadTest \
+    , RecoverCorruptPayloadGZTest \
+    , RecoverCorruptPayloadGZAESTest \
+    , RecoverCorruptHeaderGZTest \
+    , RecoverCorruptHeaderGZAESTest
 from testing.test_rescue_tar import RescueTarTest
 from testing.test_encryption import EncryptionTest
 from testing.test_deltatar import (DeltaTarTest, DeltaTar2Test,
@@ -57,7 +62,11 @@ if __name__ == "__main__":
                          , DeltaTarAes128ConcatTest
                          , HeaderTest, AESGCMTest
                          # testing.test_recover
-                         , RecoverTest, RecoverGZTest, RecoverGZAESTest
+                         , RecoverCorruptPayloadTest
+                         , RecoverCorruptPayloadGZTest
+                         , RecoverCorruptPayloadGZAESTest
+                         , RecoverCorruptHeaderGZTest
+                         , RecoverCorruptHeaderGZAESTest
                          ]:
                 try:
                     t = group (n)
index 52dcbee..5ecfb1f 100644 (file)
@@ -69,6 +69,41 @@ def is_pdt_encrypted (fname):
     return True
 
 
+def corrupt_header (_, fname, compress, encrypt):
+    """
+    Modify a significant byte in the object header of the format.
+    """
+    if encrypt is True: # damage GCM tag
+        flip_bits (fname, crypto.HDR_OFF_TAG + 1)
+    elif compress is True: # invalidate magic
+        flip_bits (fname, 1)
+    else: # Fudge checksum. From tar(5):
+        #
+        #       struct header_gnu_tar {
+        #               char name[100];
+        #               char mode[8];
+        #               char uid[8];
+        #               char gid[8];
+        #               char size[12];
+        #               char mtime[12];
+        #               char checksum[8];
+        #               …
+        flip_bits (fname, 100 + 8 + 8 + 8 + 12 + 12 + 1)
+
+
+def corrupt_payload_start (_, fname, compress, encrypt):
+    """
+    Modify the byte following the object header structure of the format.
+    """
+    if encrypt is True:
+        flip_bits (fname, crypto.PDTCRYPT_HDR_SIZE + 1)
+    elif compress is True:
+        flip_bits (fname, gz_header_size (fname) + 1)
+    else:
+        flip_bits (fname, tarfile.BLOCKSIZE + 1)
+
+
+
 ###############################################################################
 ## tests                                                                     ##
 ###############################################################################
@@ -81,6 +116,7 @@ class RecoverTest (BaseTest):
     COMPRESSION = None
     PASSWORD    = None
     FAILURES    = 0
+    CORRUPT     = corrupt_payload_start
 
 
     def setUp(self):
@@ -159,12 +195,9 @@ class RecoverTest (BaseTest):
         shutil.rmtree (self.dst_path)
         shutil.rmtree (self.src_path)
 
-        if self.PASSWORD is not None:
-            flip_bits (backup_full, crypto.PDTCRYPT_HDR_SIZE + 1)
-        elif self.COMPRESSION is not None:
-            flip_bits (backup_full, gz_header_size (backup_full) + 1)
-        else:
-            flip_bits (backup_full, tarfile.BLOCKSIZE + 1)
+        self.CORRUPT (backup_full,
+                      self.COMPRESSION is not None,
+                      self.PASSWORD    is not None)
 
         # normal restore must fail
         try:
@@ -173,9 +206,16 @@ class RecoverTest (BaseTest):
         except tarfile.CompressionError:
             if self.PASSWORD is not None or self.COMPRESSION is not None:
                 pass
+            else:
+                raise
         except tarfile.ReadError:
-            if self.PASSWORD is not None or self.COMPRESSION is not None:
+            # can happen with all three modes
+            pass
+        except tarfile.DecryptionError:
+            if self.PASSWORD is not None:
                 pass
+            else:
+                raise
 
         os.chdir (self.pwd) # not restored due to the error above
         # but recover will succeed
@@ -184,6 +224,7 @@ class RecoverTest (BaseTest):
                                          "%s/%s" % (bak_path, index_file)
                                      ])
 
+        print ("¤¤¤ failed", failed)
         assert len (failed) == self.FAILURES
 
         # with one file missing
@@ -199,14 +240,34 @@ class RecoverTest (BaseTest):
         shutil.rmtree (self.dst_path)
 
 
-class RecoverGZTest (RecoverTest):
+class RecoverCorruptPayloadTest (RecoverTest):
+    COMPRESSION = None
+    PASSWORD    = None
+    FAILURES    = 0
+
+
+class RecoverCorruptPayloadGZTest (RecoverTest):
+    COMPRESSION = "#gz"
+    PASSWORD    = None
+    FAILURES    = 1
+
+
+class RecoverCorruptPayloadGZAESTest (RecoverTest):
+    COMPRESSION = "#gz"
+    PASSWORD    = TEST_PASSWORD
+    FAILURES    = 1
+
+
+class RecoverCorruptHeaderGZTest (RecoverTest):
     COMPRESSION = "#gz"
     PASSWORD    = None
     FAILURES    = 1
+    CORRUPT     = corrupt_header
 
 
-class RecoverGZAESTest (RecoverTest):
+class RecoverCorruptHeaderGZAESTest (RecoverTest):
     COMPRESSION = "#gz"
     PASSWORD    = TEST_PASSWORD
     FAILURES    = 1
+    CORRUPT     = corrupt_header