From a808459ebf849af05248bbdac939ac1cada58f11 Mon Sep 17 00:00:00 2001 From: Philipp Gesang Date: Tue, 22 Aug 2017 17:06:41 +0200 Subject: [PATCH] implement dump mode for tolerant decryption Utilize the safe dirfd based implementation from split mode to write extracted objects to a target directory. --- deltatar/crypto.py | 154 +++++++++++++++++++++++++++++++++------------------- 1 files changed, 98 insertions(+), 56 deletions(-) diff --git a/deltatar/crypto.py b/deltatar/crypto.py index 7528281..538e4db 100755 --- a/deltatar/crypto.py +++ b/deltatar/crypto.py @@ -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) -- 1.7.1