From e3abcdf0310f070b33c83c161556b2a8b2e6a69a Mon Sep 17 00:00:00 2001 From: Philipp Gesang Date: Mon, 24 Apr 2017 11:23:53 +0200 Subject: [PATCH] implement split mode for CLI encryption --- deltatar/crypto.py | 119 +++++++++++++++++++++++++++++++++++++++++++++------ 1 files changed, 105 insertions(+), 14 deletions(-) diff --git a/deltatar/crypto.py b/deltatar/crypto.py index 5fa4048..5b1c004 100755 --- a/deltatar/crypto.py +++ b/deltatar/crypto.py @@ -750,6 +750,11 @@ class Decrypt (Crypto): ## freestanding invocation ############################################################################### +PDTCRYPT_DECRYPT = 1 << 0 # decrypt archive with password +PDTCRYPT_SPLIT = 1 << 1 # split archive into individual objects + +PDTCRYPT_SPLITNAME = "pdtcrypt-object-%d.bin" + PDTCRYPT_VERBOSE = False PDTCRYPT_STRICTIVS = False PDTCRYPT_BLOCKSIZE = 1 << 12 @@ -761,12 +766,15 @@ SELF = None class PDTDecryptionError (Exception): """Decryption failed.""" +class PDTSplitError (Exception): + """Decryption failed.""" + def noise (*a, **b): print (file=sys.stderr, *a, **b) -def depdtcrypt (pw, ins, outs): +def depdtcrypt (mode, pw, ins, outs): """ Remove PDTCRYPT layer from obj encrypted with pw. Used on a Deltatar backup this will yield a (possibly Gzip compressed) tarball. @@ -778,6 +786,45 @@ def depdtcrypt (pw, ins, outs): total_pt = 0 # total plaintext bytes total_ct = 0 # total ciphertext bytes total_read = 0 # total bytes read + outfile = None # Python file object for output + + def nextout (_): + """Dummy for non-split mode: output file does not vary.""" + return outs + + if mode & PDTCRYPT_SPLIT: + def nextout (outfile): + """ + We were passed an fd as outs for accessing the destination + directory where extracted archive components are supposed + to end up in. + """ + + if outfile is None: + if PDTCRYPT_VERBOSE is True: + noise ("PDT: no output file to close at this point") + else: + if PDTCRYPT_VERBOSE is True: + noise ("PDT: release output file %r" % outfile) + # cleanup happens automatically by the GC; the next + # line will error out on account of an invalid fd + #outfile.close () + + assert total_obj > 0 + fname = PDTCRYPT_SPLITNAME % total_obj + try: + outfd = os.open (fname, os.O_CREAT | os.O_EXCL | os.O_WRONLY, + 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) + + return os.fdopen (outfd, "wb", closefd=True) + def tell (s): """ESPIPE is normal on stdio stream.""" @@ -787,14 +834,14 @@ def depdtcrypt (pw, ins, outs): if exn.errno == 29: return -1 - def out (pt): + def out (pt, outfile): npt = len (pt) nonlocal total_pt total_pt += npt if PDTCRYPT_VERBOSE is True: noise ("PDT:\t· decrypt plaintext %d B" % (npt)) try: - nn = outs.write (pt) + nn = outfile.write (pt) except OSError as exn: # probably ENOSPC raise DecryptionError ("error (%s)" % exn) if nn != npt: @@ -813,7 +860,7 @@ def depdtcrypt (pw, ins, outs): raise DecryptionError ("error finalizing object %d (%d B): " "%r" % (total_obj, len (pt), exn)) \ from exn - out (pt) + out (pt, outfile) if PDTCRYPT_VERBOSE is True: noise ("PDT:\t· object validated") @@ -841,7 +888,12 @@ def depdtcrypt (pw, ins, outs): pretty.splitlines (), "")) ctcurrent = ctleft = hdr ["ctsize"] decr.next (hdr) - total_obj += 1 + + total_obj += 1 # used in file counter with split mode + + # finalization complete or skipped in case of first object in + # stream; create a new output file if necessary + outfile = nextout (outfile) if PDTCRYPT_VERBOSE is True: noise ("PDT: %d decrypt obj no. %d, %d B" @@ -871,7 +923,7 @@ def depdtcrypt (pw, ins, outs): if PDTCRYPT_VERBOSE is True: noise ("PDT:\t· decrypt ciphertext %d B" % (nct)) pt = decr.process (ct) - out (pt) + out (pt, outfile) def deptdcrypt_mk_stream (kind, path): @@ -910,14 +962,20 @@ def usage (err=False): out = print if err is True: out = noise - out ("usage: %s { --help | [ -v ] PASSWORD -i { - | SOURCE } -o { - | DESTINATION } }" - % SELF) + out ("usage: %s { --help" % SELF) + out (" | [ -v ] { PASSWORD } { { -i | --in } { - | SOURCE } }") + out (" { { -o | --out } { - | DESTINATION } }") + out (" { -D | --no-decrypt } { -S | --split }") out ("") out ("\twhere") out ("\t\tPASSWORD password to derive the encryption key from") + out ("\t\t-s enforce strict handling of initialization vectors") out ("\t\t-i SOURCE file name to read from") out ("\t\t-o DESTINATION file to write output to") out ("\t\t-v print extra info") + out ("\t\t-S split into files at object boundaries; this") + out ("\t\t requires DESTINATION to refer to directory") + out ("\t\t-D PDT header and ciphertext passthrough") out ("") out ("\tinstead of filenames, “-” may used to specify stdin / stdout") out ("") @@ -926,6 +984,7 @@ def usage (err=False): def parse_argv (argv): global SELF + mode = PDTCRYPT_DECRYPT pw = None insspec = None outsspec = None @@ -949,6 +1008,12 @@ def parse_argv (argv): elif arg in [ "-o", "--out", "--dest", "--sink" ]: outsspec = next (argvi) if PDTCRYPT_VERBOSE is True: noise ("PDT: decrypt to %s" % outsspec) + elif arg in [ "-S", "--split" ]: + mode |= PDTCRYPT_SPLIT + if PDTCRYPT_VERBOSE is True: noise ("PDT: split files") + elif arg in [ "-D", "--no-decrypt" ]: + mode &= ~PDTCRYPT_DECRYPT + if PDTCRYPT_VERBOSE is True: noise ("PDT: not decrypting") else: if pw is None: pw = arg @@ -959,23 +1024,49 @@ def parse_argv (argv): usage (err=True) raise Unreachable - if pw is None: - noise ("ERROR: no password given") + if mode & PDTCRYPT_DECRYPT and pw is None: + noise ("ERROR: encryption requested but no password given") noise ("") usage (err=True) raise Unreachable # default to stdout ins = deptdcrypt_mk_stream (PDTCRYPT_SOURCE, insspec or "-") - outs = deptdcrypt_mk_stream (PDTCRYPT_SINK , outsspec or "-") - return pw, ins, outs + if mode & PDTCRYPT_SPLIT: # destination must be directory + if outsspec is None or outsspec == "-": + noise ("ERROR: split mode is incompatible with stdout sink") + noise ("") + usage (err=True) + raise Unreachable + + 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: + noise ("ERROR: cannot create target directory “%s”" % outsspec) + noise ("") + usage (err=True) + raise Unreachable + except NotADirectoryError as exn: + noise ("ERROR: target path “%s” is not a directory" % outsspec) + noise ("") + usage (err=True) + raise Unreachable + else: + outs = deptdcrypt_mk_stream (PDTCRYPT_SINK , outsspec or "-") + return mode, pw, ins, outs def main (argv): - pw, ins, outs = parse_argv (argv) + mode, pw, ins, outs = parse_argv (argv) try: total_read, total_obj, total_ct, total_pt = \ - depdtcrypt (pw, ins, outs) + depdtcrypt (mode, pw, ins, outs) except DecryptionError as exn: noise ("PDT: Decryption failed:") noise ("PDT:") -- 1.7.1