## 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
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.
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."""
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:
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")
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"
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):
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 ("")
def parse_argv (argv):
global SELF
+ mode = PDTCRYPT_DECRYPT
pw = None
insspec = None
outsspec = None
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
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:")