implement dump mode for tolerant decryption
authorPhilipp Gesang <philipp.gesang@intra2net.com>
Tue, 22 Aug 2017 15:06:41 +0000 (17:06 +0200)
committerThomas Jarosch <thomas.jarosch@intra2net.com>
Mon, 2 Apr 2018 11:34:09 +0000 (13:34 +0200)
Utilize the safe dirfd based implementation from split mode to
write extracted objects to a target directory.

deltatar/crypto.py

index 7528281..538e4db 100755 (executable)
@@ -135,6 +135,7 @@ from functools import reduce, partial
 import mmap
 import os
 import struct
+import stat
 import sys
 import time
 import types
@@ -590,12 +591,12 @@ def inspect_hdr (fd, off):
     return HDR_CAND_GOOD, hdr
 
 
-def try_decrypt (fd, off, hdr, secret, fname=None):
+def try_decrypt (ifd, off, hdr, secret, ofd=-1):
     """
-    Attempt to decrypt the object in the (seekable) descriptor *fd* starting at
-    *off* using the metadata in *hdr* and *secret*. An output file can be
-    specified with *fname*; if it is *None*, the decrypted payload will be
-    discarded.
+    Attempt to decrypt the object in the (seekable) descriptor *ifd* starting
+    at *off* using the metadata in *hdr* and *secret*. An output fd can be
+    specified with *ofd*; if it is *-1* – the default –, the decrypted payload
+    will be discarded.
 
     Always creates a fresh decryptor, so validation steps across objects don’t
     apply.
@@ -612,20 +613,22 @@ def try_decrypt (fd, off, hdr, secret, fname=None):
     else:
         raise RuntimeError
 
-    if fname is not None: raise NotImplementedError
-
     decr.next (hdr)
 
     try:
-        os.lseek (fd, pos, os.SEEK_SET)
+        os.lseek (ifd, pos, os.SEEK_SET)
         while ctleft > 0:
             cnksiz = min (ctleft, PDTCRYPT_BLOCKSIZE)
-            cnk    = os.read (fd, cnksiz)
+            cnk    = os.read (ifd, cnksiz)
             ctleft -= cnksiz
             pos    += cnksiz
-            _pt    = decr.process (cnk)
+            pt     = decr.process (cnk)
+            if ofd != -1:
+                os.write (ofd, pt)
+        pt = decr.done ()
+        if len (pt) > 0 and ofd != -1:
+            os.write (ofd, pt)
 
-        _pt = decr.done ()
     except Exception as exn:
         noise ("PDT: error decrypting object %d–%d@%d, %d B remaining [%s]"
                % (off, off + hdr ["ctsize"], pos, ctleft, exn))
@@ -1357,6 +1360,27 @@ _testing_set_AES_GCM_IV_CNT_MAX = \
 _testing_set_PDTCRYPT_MAX_OBJ_SIZE = \
         partial (_patch_global, "PDTCRYPT_MAX_OBJ_SIZE")
 
+def open2_dump_file (fname, dir_fd, force=False):
+    outfd = -1
+
+    oflags = os.O_CREAT | os.O_WRONLY
+    if PDTCRYPT_OVERWRITE is True:
+        oflags |= os.O_TRUNC
+    else:
+        oflags |= os.O_EXCL
+
+    try:
+        outfd = os.open (fname, oflags,
+                         stat.S_IRUSR | stat.S_IWUSR, dir_fd=dir_fd)
+    except FileExistsError as exn:
+        noise ("PDT: refusing to overwrite existing file %s" % fname)
+        noise ("")
+        raise RuntimeError ("destination file %s already exists" % fname)
+    if PDTCRYPT_VERBOSE is True:
+        noise ("PDT: new output file %s (fd=%d)" % (fname, outfd))
+
+    return outfd
+
 ###############################################################################
 ## freestanding invocation
 ###############################################################################
@@ -1377,7 +1401,8 @@ PDTCRYPT_DECRYPT   = 1 << 0 # decrypt archive with password
 PDTCRYPT_SPLIT     = 1 << 1 # split archive into individual objects
 PDTCRYPT_HASH      = 1 << 2 # output scrypt hash for file and given password
 
-PDTCRYPT_SPLITNAME = "pdtcrypt-object-%d.bin"
+PDTCRYPT_SPLITNAME  = "pdtcrypt-object-%d.bin"
+PDTCRYPT_RESCUENAME = "pdtcrypt-rescue-object-%0.5d.bin"
 
 PDTCRYPT_VERBOSE   = False
 PDTCRYPT_STRICTIVS = False
@@ -1489,20 +1514,9 @@ def depdtcrypt (mode, secret, ins, outs):
             assert total_obj > 0
             fname = PDTCRYPT_SPLITNAME % total_obj
             try:
-                oflags = os.O_CREAT | os.O_WRONLY
-                if PDTCRYPT_OVERWRITE is True:
-                    oflags |= os.O_TRUNC
-                else:
-                    oflags |= os.O_EXCL
-                outfd = os.open (fname, oflags, 0o600, dir_fd=outs)
-                if PDTCRYPT_VERBOSE is True:
-                    noise ("PDT: new output file %s → %d" % (fname, outfd))
-            except FileExistsError as exn:
-                noise ("PDT: refusing to overwrite existing file %s" % fname)
-                noise ("")
-                raise PDTSplitError ("destination file %s already exists"
-                                     % fname)
-
+                outfd = open2_dump_file (fname, outs, force=PDTCRYPT_OVERWRITE)
+            except RuntimeError as exn:
+                raise PDTSplitError (exn)
             return os.fdopen (outfd, "wb", closefd=True)
 
 
@@ -1739,12 +1753,15 @@ def noise_output_candidates (cands, indent=8, cols=PDTCRYPT_TT_COLUMNS):
         noise (line)
 
 
-def mode_scan (secret, fname, nacl=None):
+def mode_scan (secret, fname, outs=None, nacl=None):
     """
     Dissect a binary file, looking for PDTCRYPT headers and objects.
+
+    If *outs* is supplied, recoverable data will be dumped into the specified
+    directory.
     """
     try:
-        fd = os.open (fname, os.O_RDONLY)
+        ifd = os.open (fname, os.O_RDONLY)
     except FileNotFoundError:
         noise ("PDT: failed to open %s readonly" % fname)
         noise ("")
@@ -1753,7 +1770,7 @@ def mode_scan (secret, fname, nacl=None):
     try:
         if PDTCRYPT_VERBOSE is True:
             noise ("PDT: scan for potential sync points")
-        cands = locate_hdr_candidates (fd)
+        cands = locate_hdr_candidates (ifd)
         if len (cands) == 0:
             noise ("PDT: scan complete: input does not contain potential PDT "
                    "headers; giving up.")
@@ -1762,24 +1779,35 @@ def mode_scan (secret, fname, nacl=None):
             noise ("PDT: scan complete: found %d candidates:" % len (cands))
             noise_output_candidates (cands)
     except:
-        os.close (fd)
+        os.close (ifd)
         raise
 
     junk, todo = [], []
     try:
+        nobj = 0
         for cand in cands:
-            vdt, hdr = inspect_hdr (fd, cand)
+            nobj += 1
+            vdt, hdr = inspect_hdr (ifd, cand)
             if vdt == HDR_CAND_JUNK:
                 junk.append (cand)
             else:
                 off0 = cand + PDTCRYPT_HDR_SIZE
                 if PDTCRYPT_VERBOSE is True:
-                    noise ("PDT: read payload @%d" % off0)
+                    noise ("PDT: obj %d: read payload @%d" % (nobj, off0))
                     pretty = hdr_fmt_pretty (hdr)
                     noise (reduce (lambda a, e: (a + "\n" if a else "") + "PDT:\t· " + e,
                                 pretty.splitlines (), ""))
 
-                ok = try_decrypt (fd, off0, hdr, secret) == hdr ["ctsize"]
+                ofd = -1
+                if outs is not None:
+                    ofname = PDTCRYPT_RESCUENAME % nobj
+                    ofd = open2_dump_file (ofname, outs, force=PDTCRYPT_OVERWRITE)
+
+                try:
+                    ok = try_decrypt (ifd, off0, hdr, secret, ofd=ofd) == hdr ["ctsize"]
+                finally:
+                    if ofd != -1:
+                        os.close (ofd)
                 if vdt == HDR_CAND_GOOD and ok is True:
                     noise ("PDT: %d → ✓ valid object %d–%d"
                            % (cand, off0, off0 + hdr ["ctsize"]))
@@ -1795,7 +1823,7 @@ def mode_scan (secret, fname, nacl=None):
                 else:
                     raise Unreachable
     finally:
-        os.close (fd)
+        os.close (ifd)
 
     if len (junk) == 0:
         noise ("PDT: all headers ok")
@@ -1851,6 +1879,7 @@ def parse_argv (argv):
     secret        = None
     insspec       = None
     outsspec      = None
+    outs          = None
     nacl          = None
     scrypt_format = PDTCRYPT_SCRYPT_DEFAULT
 
@@ -1932,7 +1961,15 @@ def parse_argv (argv):
                 else:
                     bail ("ERROR: unexpected positional argument “%s”" % arg)
             elif subcommand == PDTCRYPT_SUB_SCAN:
-                pass
+                if arg in [ "-o", "--out", "--dest", "--sink" ]:
+                    outsspec = checked_arg ()
+                    if PDTCRYPT_VERBOSE is True: noise ("PDT: decrypt to %s" % outsspec)
+                elif arg in [ "-f", "--force" ]:
+                    global PDTCRYPT_OVERWRITE
+                    PDTCRYPT_OVERWRITE = True
+                    if PDTCRYPT_VERBOSE is True: noise ("PDT: overwrite existing files")
+                else:
+                    bail ("ERROR: unexpected positional argument “%s”" % arg)
 
     if secret is None:
         if PDTCRYPT_VERBOSE is True:
@@ -1954,12 +1991,37 @@ def parse_argv (argv):
         elif mode & PDTCRYPT_DECRYPT:
             bail ("ERROR: encryption requested but no password given")
 
+    if mode & PDTCRYPT_SPLIT and outsspec is None:
+        bail ("ERROR: split mode is incompatible with stdout sink "
+              "(the default)")
+
+    if subcommand == PDTCRYPT_SUB_SCAN and outsspec is None:
+        pass # no output by default in scan mode
+    elif mode & PDTCRYPT_SPLIT or subcommand == PDTCRYPT_SUB_SCAN:
+        # destination must be directory
+        if outsspec == "-":
+            bail ("ERROR: mode is incompatible with stdout sink")
+        try:
+            try:
+                os.makedirs (outsspec, 0o700)
+            except FileExistsError:
+                # if it’s a directory with appropriate perms, everything is
+                # good; otherwise, below invocation of open(2) will fail
+                pass
+            outs = os.open (outsspec, os.O_DIRECTORY, 0o600)
+        except FileNotFoundError as exn:
+            bail ("ERROR: cannot create target directory “%s”" % outsspec)
+        except NotADirectoryError as exn:
+            bail ("ERROR: target path “%s” is not a directory" % outsspec)
+    else:
+        outs = deptdcrypt_mk_stream (PDTCRYPT_SINK, outsspec or "-")
+
     if subcommand == PDTCRYPT_SUB_SCAN:
         if insspec is None:
             bail ("ERROR: please supply an input file for scanning")
         if insspec == '-':
             bail ("ERROR: input must be seekable; please specify a file")
-        return True, partial (mode_scan, secret, insspec, nacl)
+        return True, partial (mode_scan, secret, insspec, outs, nacl=nacl)
 
     if subcommand == PDTCRYPT_SUB_SCRYPT:
         if secret [0] == PDTCRYPT_SECRET_KEY:
@@ -1978,26 +2040,6 @@ def parse_argv (argv):
         return True, partial (mode_scrypt, secret [1].encode (), ins, nacl,
                               fmt=scrypt_format)
 
-    if mode & PDTCRYPT_SPLIT: # destination must be directory
-        if outsspec is None or outsspec == "-":
-            bail ("ERROR: split mode is incompatible with stdout sink")
-
-        try:
-            try:
-                os.makedirs (outsspec, 0o700)
-            except FileExistsError:
-                # if it’s a directory with appropriate perms, everything is
-                # good; otherwise, below invocation of open(2) will fail
-                pass
-            outs = os.open (outsspec, os.O_DIRECTORY, 0o600)
-        except FileNotFoundError as exn:
-            bail ("ERROR: cannot create target directory “%s”" % outsspec)
-        except NotADirectoryError as exn:
-            bail ("ERROR: target path “%s” is not a directory" % outsspec)
-
-    else:
-        outs = deptdcrypt_mk_stream (PDTCRYPT_SINK, outsspec or "-")
-
     return True, partial (mode_depdtcrypt, mode, secret, ins, outs)